From 59d1cb07527c7afe2484172f1f85776af1b1261e Mon Sep 17 00:00:00 2001 From: mboudet Date: Sat, 27 Mar 2021 17:58:46 +0100 Subject: [PATCH 001/318] Adding date entity (1 of ?) --- Pipfile | 1 + askomics/libaskomics/CsvFile.py | 17 +++- askomics/libaskomics/File.py | 18 ++++- askomics/libaskomics/SparqlQuery.py | 29 +++++++ .../react/src/routes/integration/csvtable.jsx | 1 + askomics/react/src/routes/query/attribute.jsx | 81 ++++++++++++++++++- askomics/react/src/routes/query/query.jsx | 56 ++++++++++++- package.json | 1 + 8 files changed, 194 insertions(+), 10 deletions(-) diff --git a/Pipfile b/Pipfile index 7ad74567..688fa871 100644 --- a/Pipfile +++ b/Pipfile @@ -28,6 +28,7 @@ configparser = "*" tld = "*" argh = "*" python-ldap = "*" +python-dateutil = "*" [dev-packages] pytest = "*" diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 8ae9cade..5914b7f6 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -193,10 +193,10 @@ def guess_column_type(self, values, header_index): 'strand': ('strand', ), 'start': ('start', 'begin'), 'end': ('end', 'stop'), - 'datetime': ('date', 'time', 'birthday', 'day') + 'date': ('date', 'time', 'birthday', 'day') } - date_regex = re.compile(r'^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}') + date_regex = re.compile(r'^[0-9]{4}-[0-9]{2}-[0-9]{2}') # First, detect boolean values if self.are_boolean(values): @@ -214,7 +214,7 @@ def guess_column_type(self, values, header_index): if stype == 'strand' and len(set(list(filter(None, values)))) > 2: break # Test if date respect a date format - if stype == 'datetime' and all(date_regex.match(val) for val in values): + if stype == 'date' and all(date_regex.match(val) for val in values): break return stype @@ -408,6 +408,13 @@ def set_rdf_abstraction(self): rdf_range = rdflib.XSD.boolean rdf_type = rdflib.OWL.DatatypeProperty + # Date + elif self.columns_type[index] == "date": + attribute = self.rdfize(attribute_name) + label = rdflib.Literal(attribute_name) + rdf_range = rdflib.XSD.dateTime + rdf_type = rdflib.OWL.DatatypeProperty + # Text (default) else: attribute = self.rdfize(attribute_name) @@ -545,6 +552,10 @@ def generate_rdf_content(self): else: attribute = rdflib.Literal("false", datatype=rdflib.XSD.boolean) + elif current_type == "date": + relation = self.rdfize(current_header) + attribute = rdflib.Literal(self.convert_type(cell)) + # default is text else: relation = self.rdfize(current_header) diff --git a/askomics/libaskomics/File.py b/askomics/libaskomics/File.py index 8675817c..ce7ef9b9 100644 --- a/askomics/libaskomics/File.py +++ b/askomics/libaskomics/File.py @@ -1,6 +1,7 @@ import datetime import os import time +from dateutil import parser from urllib.parse import quote from askomics.libaskomics.Params import Params @@ -410,12 +411,16 @@ def get_rdf_type(self, value): float(value) return rdflib.XSD.decimal except ValueError: - return rdflib.XSD.string + try: + parser.parse(value, dayfirst=True) + return rdflib.XSD.dateTime + except parser.ParserError: + return rdflib.XSD.string return rdflib.XSD.string def convert_type(self, value): - """Convert a value to a int or float or text + """Convert a value to a date, an int or float or text Parameters ---------- @@ -427,12 +432,19 @@ def convert_type(self, value): string/float/int the converted value """ + try: + return parser.parse(value, dayfirst=True) + except parser.ParserError: + return value try: return int(value) except ValueError: try: return float(value) except ValueError: - return value + try: + return parser.parse(value, dayfirst=True) + except parser.ParserError: + return value return value diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 044a0b25..198e0533 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -1171,6 +1171,35 @@ def build_query_from_json(self, preview=False, for_editor=False): )) var_to_replace.append((obj, var_2)) + if attribute["type"] == "date": + if attribute["visible"] or Utils.check_key_in_list_of_dict(attribute["filters"], "filterValue") or attribute["id"] in linked_attributes: + subject = self.format_sparql_variable("{}{}_uri".format(attribute["entityLabel"], attribute["nodeId"])) + predicate = "<{}>".format(attribute["uri"]) + obj = self.format_sparql_variable("{}{}_{}".format(attribute["entityLabel"], attribute["nodeId"], attribute["label"])) + self.store_triple({ + "subject": subject, + "predicate": predicate, + "object": obj, + "optional": True if attribute["optional"] else False + }, block_id, sblock_id, pblock_ids) + if attribute["visible"]: + self.selects.append(obj) + # filters + for filtr in attribute["filters"]: + if filtr["filterValue"] != "" and not attribute["optional"] and not attribute["linked"]: + if filtr['filterSign'] == "=": + self.store_value("VALUES {} {{ {} }} .".format(obj, filtr["filterValue"]), block_id, sblock_id, pblock_ids) + else: + filter_string = "FILTER ( {} {} {} ) .".format(obj, filtr["filterSign"], filtr["filterValue"]) + self.store_filter(filter_string, block_id, sblock_id, pblock_ids) + if attribute["linked"]: + var_2 = self.format_sparql_variable("{}{}_{}".format( + attributes[attribute["linkedWith"]]["entity_label"], + attributes[attribute["linkedWith"]]["entity_id"], + attributes[attribute["linkedWith"]]["label"] + )) + var_to_replace.append((obj, var_2)) + # Category if attribute["type"] == "category": if attribute["visible"] or attribute["filterSelectedValues"] != [] or attribute["id"] in strands or attribute["id"] in linked_attributes: diff --git a/askomics/react/src/routes/integration/csvtable.jsx b/askomics/react/src/routes/integration/csvtable.jsx index bf195ba9..f8c0547e 100644 --- a/askomics/react/src/routes/integration/csvtable.jsx +++ b/askomics/react/src/routes/integration/csvtable.jsx @@ -101,6 +101,7 @@ export default class CsvTable extends Component { + diff --git a/askomics/react/src/routes/query/attribute.jsx b/askomics/react/src/routes/query/attribute.jsx index b537c2fb..ecbd57f9 100644 --- a/askomics/react/src/routes/query/attribute.jsx +++ b/askomics/react/src/routes/query/attribute.jsx @@ -1,7 +1,8 @@ -import React, { Component } from 'react' +import React, { Component} from 'react' import axios from 'axios' import { Input, FormGroup, CustomInput, FormFeedback } from 'reactstrap' import { Redirect } from 'react-router-dom' +import DatePicker from "react-datepicker"; import ErrorDiv from '../error/error' import WaitingDiv from '../../components/waiting' import update from 'react-addons-update' @@ -23,9 +24,12 @@ export default class AttributeBox extends Component { this.handleFilterCategory = this.props.handleFilterCategory.bind(this) this.handleFilterNumericSign = this.props.handleFilterNumericSign.bind(this) this.handleFilterNumericValue = this.props.handleFilterNumericValue.bind(this) + this.handleFilterDate = this.props.handleFilterDate.bind(this) + this.handleFilterDateValue = this.props.handleFilterDateValue.bind(this) this.toggleLinkAttribute = this.props.toggleLinkAttribute.bind(this) this.handleChangeLink = this.props.handleChangeLink.bind(this) this.toggleAddNumFilter = this.props.toggleAddNumFilter.bind(this) + this.toggleAddDateFilter = this.props.toggleAddDateFilter.bind(this) } subNums (id) { @@ -362,6 +366,73 @@ export default class AttributeBox extends Component { ) } + renderDate () { + let eyeIcon = 'attr-icon fas fa-eye-slash inactive' + if (this.props.attribute.visible) { + eyeIcon = 'attr-icon fas fa-eye' + } + + let optionalIcon = 'attr-icon fas fa-question-circle inactive' + if (this.props.attribute.optional) { + optionalIcon = 'attr-icon fas fa-question-circle' + } + + let linkIcon = 'attr-icon fas fa-unlink inactive' + if (this.props.attribute.linked) { + linkIcon = 'attr-icon fas fa-link' + } + + let sign_display = { + '=': '=', + '>': '<', + '<': '<', + } + + let form + let numberOfFilters = this.props.attribute.filters.length - 1 + + if (this.props.attribute.linked) { + form = this.renderLinker() + } else { + form = ( + + {this.props.attribute.filters.map((filter, index) => { + return ( + + + + + ) + })} +
+ + {Object.keys(sign_display).map(sign => { + return + })} + + +
+ {this.handleFilterDateValue(data, this.props.attribute.id, index)}} /> + {index == numberOfFilters ? : <>} +
+
+ ) + } + + return ( +
+ +
+ + + +
+ {form} +
+ ) + } + + render () { let box = null if (this.props.attribute.type == 'text' || this.props.attribute.type == 'uri') { @@ -376,6 +447,9 @@ export default class AttributeBox extends Component { if (this.props.attribute.type == 'boolean') { box = this.renderBoolean() } + if (this.props.attribute.type == 'date') { + box = this.renderDate() + } return box } } @@ -393,5 +467,8 @@ AttributeBox.propTypes = { toggleLinkAttribute: PropTypes.func, handleChangeLink: PropTypes.func, attribute: PropTypes.object, - graph: PropTypes.object + graph: PropTypes.object, + handleFilterDate: PropTypes.func, + toggleAddDateFilter: PropTypes.func, + handleFilterDateValue: PropTypes.func } diff --git a/askomics/react/src/routes/query/query.jsx b/askomics/react/src/routes/query/query.jsx index a66fda88..73382f56 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/query.jsx @@ -185,6 +185,9 @@ export default class Query extends Component { if (typeUri == "http://www.w3.org/2001/XMLSchema#boolean") { return "boolean" } + if (typeUri == "http://www.w3.org/2001/XMLSchema#dateType") { + return "date" + } } attributeExistInAbstraction (attrUri, entityUri) { @@ -335,6 +338,15 @@ export default class Query extends Component { nodeAttribute.filterSelectedValues = [] } + if (attributeType == 'date') { + nodeAttribute.filters = [ + { + filterValue: new Date(), + filterSign: "=" + } + ] + } + return nodeAttribute } }).filter(attr => {return attr != null})) @@ -1065,6 +1077,46 @@ export default class Query extends Component { } } + handleFilterDate (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filters.map((filter, index) => { + if (index == event.target.dataset.index) { + filter.filterSign = event.target.value + } + }) + } + }) + this.updateGraphState() + } + + toggleAddDateFilter (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filters.push({ + filterValue: new Date(), + filterSign: "=" + }) + } + }) + this.updateGraphState() + } + + handleFilterDateValue (date, id, index) { + if (!isNaN(date)) { + this.graphState.attr.map(attr => { + if (attr.id == id) { + attr.filters.map((filter, index) => { + if (index == index) { + filter.filterValue = date + } + }) + } + }) + this.updateGraphState() + } + } + toggleLinkAttribute (event) { this.graphState.attr.map(attr => { if (attr.id == event.target.id) { @@ -1338,8 +1390,8 @@ export default class Query extends Component { warningDiskSpace = (
- Your files (uploaded files and results) take {this.utils.humanFileSize(this.state.diskSpace, true)} of space - (you have {this.utils.humanFileSize(this.state.config.user.quota, true)} allowed). + Your files (uploaded files and results) take {this.utils.humanFileSize(this.state.diskSpace, true)} of space + (you have {this.utils.humanFileSize(this.state.config.user.quota, true)} allowed). Please delete some before save queries or contact an admin to increase your quota
diff --git a/package.json b/package.json index 128e7227..888bc7cf 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "react-bootstrap-table2-paginator": "^2.1.2", "react-contextmenu": "^2.14.0", "react-dom": "^16.13.1", + "react-datepicker": "^3.6.0", "react-force-graph": "^1.39.2", "react-resize-detector": "^5.2.0", "react-router": "^5.2.0", From c5cd6a0c2fe84dfb4c90ad5c33b807812e79037c Mon Sep 17 00:00:00 2001 From: mboudet Date: Sat, 27 Mar 2021 23:00:29 +0000 Subject: [PATCH 002/318] Date 2/? --- askomics/libaskomics/File.py | 4 ---- askomics/libaskomics/SparqlQuery.py | 5 +++-- askomics/libaskomics/TriplestoreExplorer.py | 3 ++- askomics/react/src/routes/query/attribute.jsx | 18 +++++++++++------- askomics/react/src/routes/query/query.jsx | 17 ++++++++++------- 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/askomics/libaskomics/File.py b/askomics/libaskomics/File.py index ce7ef9b9..aba95928 100644 --- a/askomics/libaskomics/File.py +++ b/askomics/libaskomics/File.py @@ -432,10 +432,6 @@ def convert_type(self, value): string/float/int the converted value """ - try: - return parser.parse(value, dayfirst=True) - except parser.ParserError: - return value try: return int(value) except ValueError: diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 198e0533..9278d87c 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -1,3 +1,4 @@ +import datetime import re import textwrap @@ -1188,9 +1189,9 @@ def build_query_from_json(self, preview=False, for_editor=False): for filtr in attribute["filters"]: if filtr["filterValue"] != "" and not attribute["optional"] and not attribute["linked"]: if filtr['filterSign'] == "=": - self.store_value("VALUES {} {{ {} }} .".format(obj, filtr["filterValue"]), block_id, sblock_id, pblock_ids) + self.store_value("VALUES {} {{ '{}' }} .".format(obj, filtr["filterValue"]), block_id, sblock_id, pblock_ids) else: - filter_string = "FILTER ( {} {} {} ) .".format(obj, filtr["filterSign"], filtr["filterValue"]) + filter_string = "FILTER ( {} {} '{}'^^xsd:dateTime ) .".format(obj, filtr["filterSign"], filtr["filterValue"]) self.store_filter(filter_string, block_id, sblock_id, pblock_ids) if attribute["linked"]: var_2 = self.format_sparql_variable("{}{}_{}".format( diff --git a/askomics/libaskomics/TriplestoreExplorer.py b/askomics/libaskomics/TriplestoreExplorer.py index 1bc9667c..b0fda43a 100644 --- a/askomics/libaskomics/TriplestoreExplorer.py +++ b/askomics/libaskomics/TriplestoreExplorer.py @@ -393,7 +393,8 @@ def get_abstraction_attributes(self): litterals = ( "http://www.w3.org/2001/XMLSchema#string", "http://www.w3.org/2001/XMLSchema#decimal", - "http://www.w3.org/2001/XMLSchema#boolean" + "http://www.w3.org/2001/XMLSchema#boolean", + "http://www.w3.org/2001/XMLSchema#dateTime" ) query_launcher = SparqlQueryLauncher(self.app, self.session) diff --git a/askomics/react/src/routes/query/attribute.jsx b/askomics/react/src/routes/query/attribute.jsx index ecbd57f9..dea6ad85 100644 --- a/askomics/react/src/routes/query/attribute.jsx +++ b/askomics/react/src/routes/query/attribute.jsx @@ -24,12 +24,12 @@ export default class AttributeBox extends Component { this.handleFilterCategory = this.props.handleFilterCategory.bind(this) this.handleFilterNumericSign = this.props.handleFilterNumericSign.bind(this) this.handleFilterNumericValue = this.props.handleFilterNumericValue.bind(this) - this.handleFilterDate = this.props.handleFilterDate.bind(this) this.handleFilterDateValue = this.props.handleFilterDateValue.bind(this) this.toggleLinkAttribute = this.props.toggleLinkAttribute.bind(this) this.handleChangeLink = this.props.handleChangeLink.bind(this) this.toggleAddNumFilter = this.props.toggleAddNumFilter.bind(this) this.toggleAddDateFilter = this.props.toggleAddDateFilter.bind(this) + this.handleDateFilter = this.props.handleDateFilter.bind(this) } subNums (id) { @@ -384,7 +384,7 @@ export default class AttributeBox extends Component { let sign_display = { '=': '=', - '>': '<', + '>': '>', '<': '<', } @@ -400,7 +400,7 @@ export default class AttributeBox extends Component { return ( - + {Object.keys(sign_display).map(sign => { return })} @@ -408,7 +408,11 @@ export default class AttributeBox extends Component {
- {this.handleFilterDateValue(data, this.props.attribute.id, index)}} /> + { + event.target = {value:date, id: this.props.attribute.id, dataset:{index: index}}; + this.handleFilterDateValue(event) + }} /> {index == numberOfFilters ? : <>}
@@ -466,9 +470,9 @@ AttributeBox.propTypes = { handleFilterNumericValue: PropTypes.func, toggleLinkAttribute: PropTypes.func, handleChangeLink: PropTypes.func, + toggleAddDateFilter: PropTypes.func, + handleFilterDateValue: PropTypes.func, + handleDateFilter: PropTypes.func, attribute: PropTypes.object, graph: PropTypes.object, - handleFilterDate: PropTypes.func, - toggleAddDateFilter: PropTypes.func, - handleFilterDateValue: PropTypes.func } diff --git a/askomics/react/src/routes/query/query.jsx b/askomics/react/src/routes/query/query.jsx index 73382f56..164b8f9a 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/query.jsx @@ -185,7 +185,7 @@ export default class Query extends Component { if (typeUri == "http://www.w3.org/2001/XMLSchema#boolean") { return "boolean" } - if (typeUri == "http://www.w3.org/2001/XMLSchema#dateType") { + if (typeUri == "http://www.w3.org/2001/XMLSchema#dateTime") { return "date" } } @@ -1077,7 +1077,7 @@ export default class Query extends Component { } } - handleFilterDate (event) { + handleDateFilter (event) { this.graphState.attr.map(attr => { if (attr.id == event.target.id) { attr.filters.map((filter, index) => { @@ -1102,13 +1102,13 @@ export default class Query extends Component { this.updateGraphState() } - handleFilterDateValue (date, id, index) { - if (!isNaN(date)) { + handleFilterDateValue (event) { + if (!isNaN(event.target.value)) { this.graphState.attr.map(attr => { - if (attr.id == id) { + if (attr.id == event.target.id) { attr.filters.map((filter, index) => { - if (index == index) { - filter.filterValue = date + if (index == event.target.dataset.index) { + filter.filterValue = event.target.value } }) } @@ -1427,6 +1427,9 @@ export default class Query extends Component { handleFilterNumericValue={p => this.handleFilterNumericValue(p)} toggleLinkAttribute={p => this.toggleLinkAttribute(p)} toggleAddNumFilter={p => this.toggleAddNumFilter(p)} + toggleAddDateFilter={p => this.toggleAddDateFilter(p)} + handleFilterDateValue={p => this.handleFilterDateValue(p)} + handleDateFilter={p => this.handleDateFilter(p)} /> ) } From 0d018bcf5c80df8943e4f064be1112708c6e7adf Mon Sep 17 00:00:00 2001 From: mboudet Date: Sun, 28 Mar 2021 12:37:54 +0200 Subject: [PATCH 003/318] dateTime > date --- askomics/libaskomics/CsvFile.py | 2 +- askomics/libaskomics/File.py | 6 +++--- askomics/libaskomics/SparqlQuery.py | 3 +-- askomics/libaskomics/TriplestoreExplorer.py | 2 +- askomics/react/src/routes/query/query.jsx | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 5914b7f6..d2719008 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -412,7 +412,7 @@ def set_rdf_abstraction(self): elif self.columns_type[index] == "date": attribute = self.rdfize(attribute_name) label = rdflib.Literal(attribute_name) - rdf_range = rdflib.XSD.dateTime + rdf_range = rdflib.XSD.date rdf_type = rdflib.OWL.DatatypeProperty # Text (default) diff --git a/askomics/libaskomics/File.py b/askomics/libaskomics/File.py index aba95928..ddec789f 100644 --- a/askomics/libaskomics/File.py +++ b/askomics/libaskomics/File.py @@ -412,8 +412,8 @@ def get_rdf_type(self, value): return rdflib.XSD.decimal except ValueError: try: - parser.parse(value, dayfirst=True) - return rdflib.XSD.dateTime + parser.parse(value, dayfirst=True).date() + return rdflib.XSD.date except parser.ParserError: return rdflib.XSD.string @@ -439,7 +439,7 @@ def convert_type(self, value): return float(value) except ValueError: try: - return parser.parse(value, dayfirst=True) + return parser.parse(value, dayfirst=True).date() except parser.ParserError: return value diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 9278d87c..570a3f5d 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -1,4 +1,3 @@ -import datetime import re import textwrap @@ -1191,7 +1190,7 @@ def build_query_from_json(self, preview=False, for_editor=False): if filtr['filterSign'] == "=": self.store_value("VALUES {} {{ '{}' }} .".format(obj, filtr["filterValue"]), block_id, sblock_id, pblock_ids) else: - filter_string = "FILTER ( {} {} '{}'^^xsd:dateTime ) .".format(obj, filtr["filterSign"], filtr["filterValue"]) + filter_string = "FILTER ( {} {} '{}'^^xsd:date ) .".format(obj, filtr["filterSign"], filtr["filterValue"]) self.store_filter(filter_string, block_id, sblock_id, pblock_ids) if attribute["linked"]: var_2 = self.format_sparql_variable("{}{}_{}".format( diff --git a/askomics/libaskomics/TriplestoreExplorer.py b/askomics/libaskomics/TriplestoreExplorer.py index b0fda43a..50557c6c 100644 --- a/askomics/libaskomics/TriplestoreExplorer.py +++ b/askomics/libaskomics/TriplestoreExplorer.py @@ -394,7 +394,7 @@ def get_abstraction_attributes(self): "http://www.w3.org/2001/XMLSchema#string", "http://www.w3.org/2001/XMLSchema#decimal", "http://www.w3.org/2001/XMLSchema#boolean", - "http://www.w3.org/2001/XMLSchema#dateTime" + "http://www.w3.org/2001/XMLSchema#date" ) query_launcher = SparqlQueryLauncher(self.app, self.session) diff --git a/askomics/react/src/routes/query/query.jsx b/askomics/react/src/routes/query/query.jsx index 164b8f9a..45e00903 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/query.jsx @@ -185,7 +185,7 @@ export default class Query extends Component { if (typeUri == "http://www.w3.org/2001/XMLSchema#boolean") { return "boolean" } - if (typeUri == "http://www.w3.org/2001/XMLSchema#dateTime") { + if (typeUri == "http://www.w3.org/2001/XMLSchema#date") { return "date" } } From b767b0e8b422d898d23536ac222bc8a0915d0efd Mon Sep 17 00:00:00 2001 From: mboudet Date: Sun, 28 Mar 2021 15:14:34 +0000 Subject: [PATCH 004/318] Layout --- askomics/libaskomics/SparqlQuery.py | 2 +- askomics/react/src/routes.jsx | 1 + askomics/react/src/routes/query/attribute.jsx | 4 +++- askomics/react/src/routes/query/query.jsx | 6 ++++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 9278d87c..17ada630 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -1187,7 +1187,7 @@ def build_query_from_json(self, preview=False, for_editor=False): self.selects.append(obj) # filters for filtr in attribute["filters"]: - if filtr["filterValue"] != "" and not attribute["optional"] and not attribute["linked"]: + if filtr["filterValue"] and not attribute["optional"] and not attribute["linked"]: if filtr['filterSign'] == "=": self.store_value("VALUES {} {{ '{}' }} .".format(obj, filtr["filterValue"]), block_id, sblock_id, pblock_ids) else: diff --git a/askomics/react/src/routes.jsx b/askomics/react/src/routes.jsx index 5b629b3c..92c2970f 100644 --- a/askomics/react/src/routes.jsx +++ b/askomics/react/src/routes.jsx @@ -22,6 +22,7 @@ import AskoFooter from './footer' import 'react-bootstrap-table-next/dist/react-bootstrap-table2.min.css' import 'bootstrap/dist/css/bootstrap.min.css' +import 'react-datepicker/dist/react-datepicker.css' export default class Routes extends Component { diff --git a/askomics/react/src/routes/query/attribute.jsx b/askomics/react/src/routes/query/attribute.jsx index dea6ad85..70656a6c 100644 --- a/askomics/react/src/routes/query/attribute.jsx +++ b/askomics/react/src/routes/query/attribute.jsx @@ -408,7 +408,9 @@ export default class AttributeBox extends Component {
- { event.target = {value:date, id: this.props.attribute.id, dataset:{index: index}}; this.handleFilterDateValue(event) diff --git a/askomics/react/src/routes/query/query.jsx b/askomics/react/src/routes/query/query.jsx index 164b8f9a..42c35968 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/query.jsx @@ -341,7 +341,7 @@ export default class Query extends Component { if (attributeType == 'date') { nodeAttribute.filters = [ { - filterValue: new Date(), + filterValue: null, filterSign: "=" } ] @@ -1094,7 +1094,7 @@ export default class Query extends Component { this.graphState.attr.map(attr => { if (attr.id == event.target.id) { attr.filters.push({ - filterValue: new Date(), + filterValue: null, filterSign: "=" }) } @@ -1103,6 +1103,8 @@ export default class Query extends Component { } handleFilterDateValue (event) { + console.log(event.target.value) + console.log(isNaN(event.target.value)) if (!isNaN(event.target.value)) { this.graphState.attr.map(attr => { if (attr.id == event.target.id) { From c5210074cfb5ab0c7be1b8635dac3b30ef9a0206 Mon Sep 17 00:00:00 2001 From: mboudet Date: Sun, 28 Mar 2021 17:52:27 +0000 Subject: [PATCH 005/318] Fix timezone issues.. hopefully --- askomics/libaskomics/SparqlQuery.py | 6 ++++-- askomics/react/src/routes/query/attribute.jsx | 7 ++++++- askomics/react/src/routes/query/query.jsx | 14 +++++++++++--- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 6c37b7f0..019b2116 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -1187,10 +1187,12 @@ def build_query_from_json(self, preview=False, for_editor=False): # filters for filtr in attribute["filters"]: if filtr["filterValue"] and not attribute["optional"] and not attribute["linked"]: + # COnvert datetime to date + val = filtr["filterValue"].split("T")[0] if filtr['filterSign'] == "=": - self.store_value("VALUES {} {{ '{}' }} .".format(obj, filtr["filterValue"]), block_id, sblock_id, pblock_ids) + self.store_value("VALUES {} {{ '{}'^^xsd:date }} .".format(obj, val), block_id, sblock_id, pblock_ids) else: - filter_string = "FILTER ( {} {} '{}'^^xsd:date ) .".format(obj, filtr["filterSign"], filtr["filterValue"]) + filter_string = "FILTER ( {} {} '{}'^^xsd:date ) .".format(obj, filtr["filterSign"], val) self.store_filter(filter_string, block_id, sblock_id, pblock_ids) if attribute["linked"]: var_2 = self.format_sparql_variable("{}{}_{}".format( diff --git a/askomics/react/src/routes/query/attribute.jsx b/askomics/react/src/routes/query/attribute.jsx index 70656a6c..358070b9 100644 --- a/askomics/react/src/routes/query/attribute.jsx +++ b/askomics/react/src/routes/query/attribute.jsx @@ -391,6 +391,8 @@ export default class AttributeBox extends Component { let form let numberOfFilters = this.props.attribute.filters.length - 1 + + if (this.props.attribute.linked) { form = this.renderLinker() } else { @@ -408,7 +410,10 @@ export default class AttributeBox extends Component {
- { diff --git a/askomics/react/src/routes/query/query.jsx b/askomics/react/src/routes/query/query.jsx index 6cdba322..fa87c218 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/query.jsx @@ -1102,15 +1102,22 @@ export default class Query extends Component { this.updateGraphState() } + // This is a pain, but JS will auto convert time to UTC + // And datepicker use the local timezone + // So without this, the day sent will be wrong + fixTimezoneOffset (date){ + if(!date){return null}; + return new Date(date.getTime() - (date.getTimezoneOffset() * 60000)); + } + + handleFilterDateValue (event) { - console.log(event.target.value) - console.log(isNaN(event.target.value)) if (!isNaN(event.target.value)) { this.graphState.attr.map(attr => { if (attr.id == event.target.id) { attr.filters.map((filter, index) => { if (index == event.target.dataset.index) { - filter.filterValue = event.target.value + filter.filterValue = this.fixTimezoneOffset(event.target.value) } }) } @@ -1343,6 +1350,7 @@ export default class Query extends Component { }) }).then(response => { if (this.props.location.state.redo) { + console.log(this.props.location.state.graphState) // redo a query this.graphState = this.props.location.state.graphState this.initId() From b6be1e91ab3384493ece6ea7717f2815f7dd51e2 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 29 Mar 2021 11:48:10 +0000 Subject: [PATCH 006/318] Fix autodetect --- askomics/libaskomics/CsvFile.py | 51 +++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index d2719008..8b3e0ca3 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -3,6 +3,7 @@ import rdflib import sys import traceback +from dateutil import parser from rdflib import BNode @@ -196,27 +197,25 @@ def guess_column_type(self, values, header_index): 'date': ('date', 'time', 'birthday', 'day') } - date_regex = re.compile(r'^[0-9]{4}-[0-9]{2}-[0-9]{2}') - # First, detect boolean values if self.are_boolean(values): return "boolean" # Then, detect special type with header for stype, expressions in special_types.items(): - for expression in expressions: - epression_regexp = ".*{}.*".format(expression.lower()) - if re.match(epression_regexp, self.header[header_index], re.IGNORECASE) is not None: - # Test if start and end are numerical - if stype in ('start', 'end') and not all(self.is_decimal(val) for val in values): - break - # test if strand is a category with 2 elements max - if stype == 'strand' and len(set(list(filter(None, values)))) > 2: - break - # Test if date respect a date format - if stype == 'date' and all(date_regex.match(val) for val in values): - break - return stype + # Need to check once if it match any subtype + expression_regexp = "|".join([".*{}.*".format(expression.lower()) for expression in expressions]) + if re.match(expression_regexp, self.header[header_index].lower(), re.IGNORECASE) is not None: + # Test if start and end are numerical + if stype in ('start', 'end') and not all(self.is_decimal(val) for val in values): + break + # test if strand is a category with 2 elements max + if stype == 'strand' and len(set(list(filter(None, values)))) > 2: + break + # Test if date respect a date format + if stype == 'date' and not all(self.is_date(val) for val in values): + break + return stype # Then, check goterm # if all((val.startswith("GO:") and val[3:].isdigit()) for val in values): @@ -275,6 +274,28 @@ def is_decimal(value): except ValueError: return False + @staticmethod + def is_date(value): + """Guess if a variable if a date + + Parameters + ---------- + value : + The var to test + + Returns + ------- + boolean + True if it's a date + """ + if value == "": + return True + try: + parser.parse(value, dayfirst=True).date() + return True + except parser.ParserError: + return False + @property def transposed_preview(self): """Transpose the preview From 37ceaf50dacf46f726ba7a4f9a501bc1b5207304 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 30 Mar 2021 15:36:28 +0200 Subject: [PATCH 007/318] Tests part1 --- test-data/transcripts.tsv | 22 +++++++++--------- test-data/transcripts_chunk1.tsv | 8 +++---- test-data/transcripts_chunk2.tsv | 8 +++---- test-data/transcripts_chunk3.tsv | 6 ++--- tests/conftest.py | 2 +- tests/results/abstraction.json | 13 ++++++++++- tests/results/preview_files.json | 38 +++++++++++++++++++++----------- 7 files changed, 60 insertions(+), 37 deletions(-) diff --git a/test-data/transcripts.tsv b/test-data/transcripts.tsv index 0af31eaa..e69abaa4 100644 --- a/test-data/transcripts.tsv +++ b/test-data/transcripts.tsv @@ -1,11 +1,11 @@ -transcript taxon featureName chromosomeName start end featureType strand biotype description -AT3G10490 Arabidopsis_thaliana ANAC052 At3 3267835 3270883 gene plus protein_coding NAC_domain_containing_protein_52_[Source:TAIR%3BAcc:AT3G10490] -AT3G13660 Arabidopsis_thaliana DIR22 At3 4464908 4465586 gene plus protein_coding Dirigent_protein_22_[Source:UniProtKB/Swiss-Prot%3BAcc:Q66GI2] -AT3G51470 Arabidopsis_thaliana na At3 19097787 19099275 gene minus protein_coding Probable_protein_phosphatase_2C_47_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9SD02] -AT3G10460 Arabidopsis_thaliana na At3 3255800 3256439 gene plus protein_coding Plant_self-incompatibility_protein_S1_family_[Source:TAIR%3BAcc:AT3G10460] -AT3G22640 Arabidopsis_thaliana PAP85 At3 8011724 8013902 gene minus protein_coding cupin_family_protein_[Source:TAIR%3BAcc:AT3G22640] -AT1G33615 Arabidopsis_thaliana na At1 12193325 12194374 ncRNA_gene minus ncRNA other_RNA_[Source:TAIR%3BAcc:AT1G33615] -AT5G41905 Arabidopsis_thaliana MIR166E At5 16775524 16775658 miRNA_gene minus miRNA MIR166/MIR166E%3B_miRNA_[Source:TAIR%3BAcc:AT5G41905] -AT1G57800 Arabidopsis_thaliana ORTH3 At1 21408623 21412283 gene minus protein_coding E3_ubiquitin-protein_ligase_ORTHRUS_3_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9FVS2] -AT1G49500 Arabidopsis_thaliana na At1 18321295 18322284 gene minus protein_coding unknown_protein%3B_FUNCTIONS_IN:_molecular_function_unknown%3B_INVOLVED_IN:_biological_process_unknown%3B_LOCATED_IN:_endomembrane_system%3B_EXPRESSED_IN:_19_plant_structures%3B_EXPRESSED_DURING:_10_growth_stages%3B_BEST_Arabidopsis_thaliana_protein_match_is:_u_/.../_protein_(TAIR:AT3G19030.1)%3B_Has_24_Blast_hits_to_24_proteins_in_2_species:_Archae_-_0%3B_Bacteria_-_0%3B_Metazoa_-_0%3B_Fungi_-_0%3B_Plants_-_24%3B_Viruses_-_0%3B_Other_Eukaryotes_-_0_(source:_NCBI_BLink)._[Source:TAIR%3BAcc:AT1G49500] -AT5G35334 Arabidopsis_thaliana na At5 13537917 13538984 gene plus transposable_element transposable_element_gene_[Source:TAIR%3BAcc:AT5G35334] \ No newline at end of file +transcript taxon featureName chromosomeName start end featureType strand biotype description date +AT3G10490 Arabidopsis_thaliana ANAC052 At3 3267835 3270883 gene plus protein_coding NAC_domain_containing_protein_52_[Source:TAIR%3BAcc:AT3G10490] 01/01/2000 +AT3G13660 Arabidopsis_thaliana DIR22 At3 4464908 4465586 gene plus protein_coding Dirigent_protein_22_[Source:UniProtKB/Swiss-Prot%3BAcc:Q66GI2] 02/01/2000 +AT3G51470 Arabidopsis_thaliana na At3 19097787 19099275 gene minus protein_coding Probable_protein_phosphatase_2C_47_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9SD02] 03/01/2000 +AT3G10460 Arabidopsis_thaliana na At3 3255800 3256439 gene plus protein_coding Plant_self-incompatibility_protein_S1_family_[Source:TAIR%3BAcc:AT3G10460] 04/01/2000 +AT3G22640 Arabidopsis_thaliana PAP85 At3 8011724 8013902 gene minus protein_coding cupin_family_protein_[Source:TAIR%3BAcc:AT3G22640] 05/01/2000 +AT1G33615 Arabidopsis_thaliana na At1 12193325 12194374 ncRNA_gene minus ncRNA other_RNA_[Source:TAIR%3BAcc:AT1G33615] 06/01/2000 +AT5G41905 Arabidopsis_thaliana MIR166E At5 16775524 16775658 miRNA_gene minus miRNA MIR166/MIR166E%3B_miRNA_[Source:TAIR%3BAcc:AT5G41905] 07/01/2000 +AT1G57800 Arabidopsis_thaliana ORTH3 At1 21408623 21412283 gene minus protein_coding E3_ubiquitin-protein_ligase_ORTHRUS_3_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9FVS2] 01/08/2000 +AT1G49500 Arabidopsis_thaliana na At1 18321295 18322284 gene minus protein_coding unknown_protein%3B_FUNCTIONS_IN:_molecular_function_unknown%3B_INVOLVED_IN:_biological_process_unknown%3B_LOCATED_IN:_endomembrane_system%3B_EXPRESSED_IN:_19_plant_structures%3B_EXPRESSED_DURING:_10_growth_stages%3B_BEST_Arabidopsis_thaliana_protein_match_is:_u_/.../_protein_(TAIR:AT3G19030.1)%3B_Has_24_Blast_hits_to_24_proteins_in_2_species:_Archae_-_0%3B_Bacteria_-_0%3B_Metazoa_-_0%3B_Fungi_-_0%3B_Plants_-_24%3B_Viruses_-_0%3B_Other_Eukaryotes_-_0_(source:_NCBI_BLink)._[Source:TAIR%3BAcc:AT1G49500] 09/01/2000 +AT5G35334 Arabidopsis_thaliana na At5 13537917 13538984 gene plus transposable_element transposable_element_gene_[Source:TAIR%3BAcc:AT5G35334] 10/01/2000 diff --git a/test-data/transcripts_chunk1.tsv b/test-data/transcripts_chunk1.tsv index 94f3ae1d..7822cfe6 100644 --- a/test-data/transcripts_chunk1.tsv +++ b/test-data/transcripts_chunk1.tsv @@ -1,4 +1,4 @@ -transcript taxon featureName chromosomeName start end featureType strand biotype description -AT3G10490 Arabidopsis_thaliana ANAC052 At3 3267835 3270883 gene plus protein_coding NAC_domain_containing_protein_52_[Source:TAIR%3BAcc:AT3G10490] -AT3G13660 Arabidopsis_thaliana DIR22 At3 4464908 4465586 gene plus protein_coding Dirigent_protein_22_[Source:UniProtKB/Swiss-Prot%3BAcc:Q66GI2] -AT3G51470 Arabidopsis_thaliana na At3 19097787 19099275 gene minus protein_coding Probable_protein_phosphatase_2C_47_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9SD02] \ No newline at end of file +transcript taxon featureName chromosomeName start end featureType strand biotype description date +AT3G10490 Arabidopsis_thaliana ANAC052 At3 3267835 3270883 gene plus protein_coding NAC_domain_containing_protein_52_[Source:TAIR%3BAcc:AT3G10490] 01/01/2000 +AT3G13660 Arabidopsis_thaliana DIR22 At3 4464908 4465586 gene plus protein_coding Dirigent_protein_22_[Source:UniProtKB/Swiss-Prot%3BAcc:Q66GI2] 02/01/20 +AT3G51470 Arabidopsis_thaliana na At3 19097787 19099275 gene minus protein_coding Probable_protein_phosphatase_2C_47_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9SD02] 03/01/20 diff --git a/test-data/transcripts_chunk2.tsv b/test-data/transcripts_chunk2.tsv index 9407bccf..0561de9a 100644 --- a/test-data/transcripts_chunk2.tsv +++ b/test-data/transcripts_chunk2.tsv @@ -1,4 +1,4 @@ -AT3G10460 Arabidopsis_thaliana na At3 3255800 3256439 gene plus protein_coding Plant_self-incompatibility_protein_S1_family_[Source:TAIR%3BAcc:AT3G10460] -AT3G22640 Arabidopsis_thaliana PAP85 At3 8011724 8013902 gene minus protein_coding cupin_family_protein_[Source:TAIR%3BAcc:AT3G22640] -AT1G33615 Arabidopsis_thaliana na At1 12193325 12194374 ncRNA_gene minus ncRNA other_RNA_[Source:TAIR%3BAcc:AT1G33615] -AT5G41905 Arabidopsis_thaliana MIR166E At5 16775524 16775658 miRNA_gene minus miRNA MIR166/MIR166E%3B_miRNA_[Source:TAIR%3BAcc:AT5G41905] \ No newline at end of file +AT3G10460 Arabidopsis_thaliana na At3 3255800 3256439 gene plus protein_coding Plant_self-incompatibility_protein_S1_family_[Source:TAIR%3BAcc:AT3G10460] 04/01/2000 +AT3G22640 Arabidopsis_thaliana PAP85 At3 8011724 8013902 gene minus protein_coding cupin_family_protein_[Source:TAIR%3BAcc:AT3G22640] 05/01/2000 +AT1G33615 Arabidopsis_thaliana na At1 12193325 12194374 ncRNA_gene minus ncRNA other_RNA_[Source:TAIR%3BAcc:AT1G33615] 06/01/2000 +AT5G41905 Arabidopsis_thaliana MIR166E At5 16775524 16775658 miRNA_gene minus miRNA MIR166/MIR166E%3B_miRNA_[Source:TAIR%3BAcc:AT5G41905] 07/01/2000 diff --git a/test-data/transcripts_chunk3.tsv b/test-data/transcripts_chunk3.tsv index a8799585..0d50fd01 100644 --- a/test-data/transcripts_chunk3.tsv +++ b/test-data/transcripts_chunk3.tsv @@ -1,3 +1,3 @@ -AT1G57800 Arabidopsis_thaliana ORTH3 At1 21408623 21412283 gene minus protein_coding E3_ubiquitin-protein_ligase_ORTHRUS_3_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9FVS2] -AT1G49500 Arabidopsis_thaliana na At1 18321295 18322284 gene minus protein_coding unknown_protein%3B_FUNCTIONS_IN:_molecular_function_unknown%3B_INVOLVED_IN:_biological_process_unknown%3B_LOCATED_IN:_endomembrane_system%3B_EXPRESSED_IN:_19_plant_structures%3B_EXPRESSED_DURING:_10_growth_stages%3B_BEST_Arabidopsis_thaliana_protein_match_is:_u_/.../_protein_(TAIR:AT3G19030.1)%3B_Has_24_Blast_hits_to_24_proteins_in_2_species:_Archae_-_0%3B_Bacteria_-_0%3B_Metazoa_-_0%3B_Fungi_-_0%3B_Plants_-_24%3B_Viruses_-_0%3B_Other_Eukaryotes_-_0_(source:_NCBI_BLink)._[Source:TAIR%3BAcc:AT1G49500] -AT5G35334 Arabidopsis_thaliana na At5 13537917 13538984 gene plus transposable_element transposable_element_gene_[Source:TAIR%3BAcc:AT5G35334] \ No newline at end of file +AT1G57800 Arabidopsis_thaliana ORTH3 At1 21408623 21412283 gene minus protein_coding E3_ubiquitin-protein_ligase_ORTHRUS_3_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9FVS2] 08/01/2000 +AT1G49500 Arabidopsis_thaliana na At1 18321295 18322284 gene minus protein_coding unknown_protein%3B_FUNCTIONS_IN:_molecular_function_unknown%3B_INVOLVED_IN:_biological_process_unknown%3B_LOCATED_IN:_endomembrane_system%3B_EXPRESSED_IN:_19_plant_structures%3B_EXPRESSED_DURING:_10_growth_stages%3B_BEST_Arabidopsis_thaliana_protein_match_is:_u_/.../_protein_(TAIR:AT3G19030.1)%3B_Has_24_Blast_hits_to_24_proteins_in_2_species:_Archae_-_0%3B_Bacteria_-_0%3B_Metazoa_-_0%3B_Fungi_-_0%3B_Plants_-_24%3B_Viruses_-_0%3B_Other_Eukaryotes_-_0_(source:_NCBI_BLink)._[Source:TAIR%3BAcc:AT1G49500] 09/01/2000 +AT5G35334 Arabidopsis_thaliana na At5 13537917 13538984 gene plus transposable_element transposable_element_gene_[Source:TAIR%3BAcc:AT5G35334] 10/01/2000 diff --git a/tests/conftest.py b/tests/conftest.py index dffca48e..ed8f733d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -369,7 +369,7 @@ def upload_and_integrate(self): # integrate int_transcripts = self.integrate_file({ "id": 1, - "columns_type": ["start_entity", "category", "text", "reference", "start", "end", "category", "strand", "text", "text"] + "columns_type": ["start_entity", "category", "text", "reference", "start", "end", "category", "strand", "text", "text", "date"] }) int_de = self.integrate_file({ diff --git a/tests/results/abstraction.json b/tests/results/abstraction.json index 9b15d1bd..0f7f16a5 100644 --- a/tests/results/abstraction.json +++ b/tests/results/abstraction.json @@ -529,6 +529,17 @@ "label": "score", "type": "http://www.w3.org/2001/XMLSchema#decimal", "uri": "http://askomics.org/test/data/score" + }, + { + "categories": [], + "entityUri": "http://askomics.org/test/data/transcript", + "faldo": null, + "graphs": [ + "urn:sparql:askomics_test:1_jdoe:transcripts.tsv_###TRANSCRIPTS_TIMESTAMP###" + ], + "label": "date", + "type": "http://www.w3.org/2001/XMLSchema#date", + "uri": "http://askomics.org/test/data/date" } ], "entities": [ @@ -602,4 +613,4 @@ "diskSpace": ###SIZE###, "error": false, "errorMessage": "" -} \ No newline at end of file +} diff --git a/tests/results/preview_files.json b/tests/results/preview_files.json index f3145669..e38881d1 100644 --- a/tests/results/preview_files.json +++ b/tests/results/preview_files.json @@ -14,7 +14,8 @@ "text", "strand", "text", - "text" + "text", + "date" ], "content_preview": [ { @@ -27,7 +28,8 @@ "start": "3267835", "strand": "plus", "taxon": "Arabidopsis_thaliana", - "transcript": "AT3G10490" + "transcript": "AT3G10490", + "date": "01/01/2000" }, { "biotype": "protein_coding", @@ -39,7 +41,8 @@ "start": "4464908", "strand": "plus", "taxon": "Arabidopsis_thaliana", - "transcript": "AT3G13660" + "transcript": "AT3G13660", + "date": "02/01/2000" }, { "biotype": "protein_coding", @@ -51,7 +54,8 @@ "start": "19097787", "strand": "minus", "taxon": "Arabidopsis_thaliana", - "transcript": "AT3G51470" + "transcript": "AT3G51470", + "date": "03/01/2000" }, { "biotype": "protein_coding", @@ -63,7 +67,8 @@ "start": "3255800", "strand": "plus", "taxon": "Arabidopsis_thaliana", - "transcript": "AT3G10460" + "transcript": "AT3G10460", + "date": "04/01/2000" }, { "biotype": "protein_coding", @@ -75,7 +80,8 @@ "start": "8011724", "strand": "minus", "taxon": "Arabidopsis_thaliana", - "transcript": "AT3G22640" + "transcript": "AT3G22640", + "date": "05/01/2000" }, { "biotype": "ncRNA", @@ -87,7 +93,8 @@ "start": "12193325", "strand": "minus", "taxon": "Arabidopsis_thaliana", - "transcript": "AT1G33615" + "transcript": "AT1G33615", + "date": "06/01/2000" }, { "biotype": "miRNA", @@ -99,7 +106,8 @@ "start": "16775524", "strand": "minus", "taxon": "Arabidopsis_thaliana", - "transcript": "AT5G41905" + "transcript": "AT5G41905", + "date": "07/01/2000" }, { "biotype": "protein_coding", @@ -111,7 +119,8 @@ "start": "21408623", "strand": "minus", "taxon": "Arabidopsis_thaliana", - "transcript": "AT1G57800" + "transcript": "AT1G57800", + "date": "08/01/2000" }, { "biotype": "protein_coding", @@ -123,7 +132,8 @@ "start": "18321295", "strand": "minus", "taxon": "Arabidopsis_thaliana", - "transcript": "AT1G49500" + "transcript": "AT1G49500", + "date": "09/01/2000" }, { "biotype": "transposable_element", @@ -135,7 +145,8 @@ "start": "13537917", "strand": "plus", "taxon": "Arabidopsis_thaliana", - "transcript": "AT5G35334" + "transcript": "AT5G35334", + "date": "10/01/2000" } ], "header": [ @@ -148,7 +159,8 @@ "featureType", "strand", "biotype", - "description" + "description", + "date" ] }, "id": 1, @@ -158,4 +170,4 @@ "error_message": "" } ] -} \ No newline at end of file +} From 99be7b4e254e43a1f3a2e05f5758d59d79c413bf Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 30 Mar 2021 16:02:22 +0200 Subject: [PATCH 008/318] Fix tests --- test-data/transcripts.tsv | 2 +- tests/results/data.json | 4 ++++ tests/test_api_admin.py | 2 +- tests/test_api_data.py | 2 +- tests/test_api_file.py | 4 ++-- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/test-data/transcripts.tsv b/test-data/transcripts.tsv index e69abaa4..6117b805 100644 --- a/test-data/transcripts.tsv +++ b/test-data/transcripts.tsv @@ -6,6 +6,6 @@ AT3G10460 Arabidopsis_thaliana na At3 3255800 3256439 gene plus protein_coding P AT3G22640 Arabidopsis_thaliana PAP85 At3 8011724 8013902 gene minus protein_coding cupin_family_protein_[Source:TAIR%3BAcc:AT3G22640] 05/01/2000 AT1G33615 Arabidopsis_thaliana na At1 12193325 12194374 ncRNA_gene minus ncRNA other_RNA_[Source:TAIR%3BAcc:AT1G33615] 06/01/2000 AT5G41905 Arabidopsis_thaliana MIR166E At5 16775524 16775658 miRNA_gene minus miRNA MIR166/MIR166E%3B_miRNA_[Source:TAIR%3BAcc:AT5G41905] 07/01/2000 -AT1G57800 Arabidopsis_thaliana ORTH3 At1 21408623 21412283 gene minus protein_coding E3_ubiquitin-protein_ligase_ORTHRUS_3_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9FVS2] 01/08/2000 +AT1G57800 Arabidopsis_thaliana ORTH3 At1 21408623 21412283 gene minus protein_coding E3_ubiquitin-protein_ligase_ORTHRUS_3_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9FVS2] 08/01/2000 AT1G49500 Arabidopsis_thaliana na At1 18321295 18322284 gene minus protein_coding unknown_protein%3B_FUNCTIONS_IN:_molecular_function_unknown%3B_INVOLVED_IN:_biological_process_unknown%3B_LOCATED_IN:_endomembrane_system%3B_EXPRESSED_IN:_19_plant_structures%3B_EXPRESSED_DURING:_10_growth_stages%3B_BEST_Arabidopsis_thaliana_protein_match_is:_u_/.../_protein_(TAIR:AT3G19030.1)%3B_Has_24_Blast_hits_to_24_proteins_in_2_species:_Archae_-_0%3B_Bacteria_-_0%3B_Metazoa_-_0%3B_Fungi_-_0%3B_Plants_-_24%3B_Viruses_-_0%3B_Other_Eukaryotes_-_0_(source:_NCBI_BLink)._[Source:TAIR%3BAcc:AT1G49500] 09/01/2000 AT5G35334 Arabidopsis_thaliana na At5 13537917 13538984 gene plus transposable_element transposable_element_gene_[Source:TAIR%3BAcc:AT5G35334] 10/01/2000 diff --git a/tests/results/data.json b/tests/results/data.json index b7811af2..4d92ef58 100644 --- a/tests/results/data.json +++ b/tests/results/data.json @@ -43,6 +43,10 @@ { "object": "http://askomics.org/test/data/At3_327", "predicat": "http://askomics.org/test/internal/includeInReference" + }, + { + "object": "2000-01-01", + "predicat": "http://askomics.org/test/data/date" } ], "error": false, diff --git a/tests/test_api_admin.py b/tests/test_api_admin.py index a0efd3ed..efaf3030 100644 --- a/tests/test_api_admin.py +++ b/tests/test_api_admin.py @@ -68,7 +68,7 @@ def test_get_files(self, client): 'date': info["transcripts"]["upload"]["file_date"], 'id': 1, 'name': 'transcripts.tsv', - 'size': 1986, + 'size': 2102, 'type': 'csv/tsv', 'user': 'jsmith' diff --git a/tests/test_api_data.py b/tests/test_api_data.py index 37be3d3c..9693f11c 100644 --- a/tests/test_api_data.py +++ b/tests/test_api_data.py @@ -63,7 +63,7 @@ def test_public_access(self, client): client.integrate_file({ "id": 1, - "columns_type": ["start_entity", "category", "text", "reference", "start", "end", "category", "strand", "text", "text"] + "columns_type": ["start_entity", "category", "text", "reference", "start", "end", "category", "strand", "text", "text", "date"] }, public=True) with open("tests/results/data.json", "r") as file: diff --git a/tests/test_api_file.py b/tests/test_api_file.py index f809df49..6af0896f 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -25,7 +25,7 @@ def test_get_files(self, client): 'date': info["transcripts"]["upload"]["file_date"], 'id': 1, 'name': 'transcripts.tsv', - 'size': 1986, + 'size': 2102, 'type': 'csv/tsv' }, { 'date': info["de"]["upload"]["file_date"], @@ -105,7 +105,7 @@ def test_edit_file(self, client): 'date': info["transcripts"]["upload"]["file_date"], 'id': 1, 'name': 'new name.tsv', - 'size': 1986, + 'size': 2102, 'type': 'csv/tsv' }, { 'date': info["de"]["upload"]["file_date"], From a34bf65284c663df0c990dbc79f1ead5d2efa164 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 30 Mar 2021 16:18:27 +0200 Subject: [PATCH 009/318] Final fix --- tests/test_api_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_api_file.py b/tests/test_api_file.py index 6af0896f..137836f4 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -73,7 +73,7 @@ def test_get_files(self, client): 'date': info["transcripts"]["upload"]["file_date"], 'id': 1, 'name': 'transcripts.tsv', - 'size': 1986, + 'size': 2102, 'type': 'csv/tsv' }] } From 332ebdf4d76a119058529d5907eae0f5cf4d20ba Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 30 Mar 2021 15:22:03 +0000 Subject: [PATCH 010/318] Added comparisons signs --- askomics/react/src/routes/query/attribute.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/askomics/react/src/routes/query/attribute.jsx b/askomics/react/src/routes/query/attribute.jsx index 358070b9..2dc0c2fa 100644 --- a/askomics/react/src/routes/query/attribute.jsx +++ b/askomics/react/src/routes/query/attribute.jsx @@ -384,10 +384,12 @@ export default class AttributeBox extends Component { let sign_display = { '=': '=', - '>': '>', '<': '<', + '<=': '≤', + '>': '>', + '>=': '≥', + '!=': '≠' } - let form let numberOfFilters = this.props.attribute.filters.length - 1 From 589b1ab30f21237055c8cce9038fec509eb61d38 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 31 Mar 2021 16:31:43 +0200 Subject: [PATCH 011/318] Update askomics/libaskomics/CsvFile.py Co-authored-by: Anthony Bretaudeau --- askomics/libaskomics/CsvFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 8b3e0ca3..9f76f4e1 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -276,7 +276,7 @@ def is_decimal(value): @staticmethod def is_date(value): - """Guess if a variable if a date + """Guess if a variable is a date Parameters ---------- From 9c50f38552ba4c4b5ed39cc12f1e6219b885ee18 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 31 Mar 2021 16:31:52 +0200 Subject: [PATCH 012/318] Update askomics/libaskomics/CsvFile.py Co-authored-by: Anthony Bretaudeau --- askomics/libaskomics/CsvFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 9f76f4e1..ad0f6680 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -203,7 +203,7 @@ def guess_column_type(self, values, header_index): # Then, detect special type with header for stype, expressions in special_types.items(): - # Need to check once if it match any subtype + # Need to check once if it matches any subtype expression_regexp = "|".join([".*{}.*".format(expression.lower()) for expression in expressions]) if re.match(expression_regexp, self.header[header_index].lower(), re.IGNORECASE) is not None: # Test if start and end are numerical From 695cef55ffa45b6bd1c0566b9f2f3b780f6b9cd6 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 31 Mar 2021 16:32:04 +0200 Subject: [PATCH 013/318] Update askomics/libaskomics/CsvFile.py Co-authored-by: Anthony Bretaudeau --- askomics/libaskomics/CsvFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index ad0f6680..5788f99d 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -212,7 +212,7 @@ def guess_column_type(self, values, header_index): # test if strand is a category with 2 elements max if stype == 'strand' and len(set(list(filter(None, values)))) > 2: break - # Test if date respect a date format + # Test if date respects a date format if stype == 'date' and not all(self.is_date(val) for val in values): break return stype From 2138f788c498756f701282a3039f67aa0780009f Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 8 Apr 2021 09:09:44 +0000 Subject: [PATCH 014/318] Fix console --- askomics/react/src/routes/sparql/sparql.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askomics/react/src/routes/sparql/sparql.jsx b/askomics/react/src/routes/sparql/sparql.jsx index 3500d537..f48690a3 100644 --- a/askomics/react/src/routes/sparql/sparql.jsx +++ b/askomics/react/src/routes/sparql/sparql.jsx @@ -67,7 +67,7 @@ export default class Sparql extends Component { endpoints: this.props.location.state.endpoints, diskSpace: this.props.location.state.diskSpace, config: this.props.location.state.config, - console_enabled: this.props.location.console_enabled, + console_enabled: this.props.location.state.console_enabled, waiting: false, }) } else { From 92a61d891c8fec190727e47a29409f6c615721d3 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 12 Apr 2021 07:50:44 +0000 Subject: [PATCH 015/318] Fix encoding in URIs --- askomics/api/data.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/askomics/api/data.py b/askomics/api/data.py index 10a77072..d3b23c94 100644 --- a/askomics/api/data.py +++ b/askomics/api/data.py @@ -1,4 +1,5 @@ """Api routes""" +import urllib.parse import sys import traceback @@ -33,10 +34,11 @@ def get_data(uri): # If the user do not have access to any endpoint (no viewable graph), skip if endpoints: + uri = urllib.parse.quote(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%s ?predicat ?object\n}" % (full_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() From 4fadecfe75cfa8f8ce3d1205c7ba75fba355c004 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 12 Apr 2021 07:58:30 +0000 Subject: [PATCH 016/318] Fix url rendering --- askomics/react/src/classes/utils.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/askomics/react/src/classes/utils.jsx b/askomics/react/src/classes/utils.jsx index 1392ff4c..bd08eec4 100644 --- a/askomics/react/src/classes/utils.jsx +++ b/askomics/react/src/classes/utils.jsx @@ -18,7 +18,8 @@ export default class Utils { // take last elem let last = splitList[splitList.length - 1] let splitList2 = last.split('#') - return splitList2[splitList2.length - 1] + + return decodeURI(splitList2[splitList2.length - 1]) } humanFileSize (bytes, si) { From 03aeea829fa97f5b9a713ef9e6beb527d03d2109 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 12 Apr 2021 08:01:16 +0000 Subject: [PATCH 017/318] Remove line return --- askomics/react/src/classes/utils.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/askomics/react/src/classes/utils.jsx b/askomics/react/src/classes/utils.jsx index bd08eec4..eb555ca3 100644 --- a/askomics/react/src/classes/utils.jsx +++ b/askomics/react/src/classes/utils.jsx @@ -17,8 +17,7 @@ export default class Utils { let splitList = url.split('/') // take last elem let last = splitList[splitList.length - 1] - let splitList2 = last.split('#') - + let splitList2 = last.split('#') return decodeURI(splitList2[splitList2.length - 1]) } From 5b0447a98bf4a3c6e65f65755e9f6f1ba4f1c54c Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 12 Apr 2021 08:02:16 +0000 Subject: [PATCH 018/318] Remove spaces --- askomics/react/src/classes/utils.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askomics/react/src/classes/utils.jsx b/askomics/react/src/classes/utils.jsx index eb555ca3..81fccc44 100644 --- a/askomics/react/src/classes/utils.jsx +++ b/askomics/react/src/classes/utils.jsx @@ -17,7 +17,7 @@ export default class Utils { let splitList = url.split('/') // take last elem let last = splitList[splitList.length - 1] - let splitList2 = last.split('#') + let splitList2 = last.split('#') return decodeURI(splitList2[splitList2.length - 1]) } From aee9b2777457f0c074e375d44eabdb2989e16aa3 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 13 Apr 2021 07:09:23 +0000 Subject: [PATCH 019/318] Fix optional search --- askomics/libaskomics/CsvFile.py | 4 +++- askomics/libaskomics/File.py | 5 ++++- askomics/libaskomics/SparqlQuery.py | 26 +++++++++++++++++++------- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 8ae9cade..d76e4557 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -492,7 +492,7 @@ def generate_rdf_content(self): symetric_relation = False # Skip entity and blank cells - if column_number == 0 or not cell: + if column_number == 0 or (not cell and not current_type == "strand"): continue # Relation @@ -505,6 +505,8 @@ def generate_rdf_content(self): # Category elif current_type in ('category', 'reference', 'strand'): potential_relation = self.rdfize(current_header) + if not cell: + cell = "unknown" if current_header not in self.category_values.keys(): # Add the category in dict, and the first value in a set self.category_values[current_header] = {cell, } diff --git a/askomics/libaskomics/File.py b/askomics/libaskomics/File.py index 8675817c..f32ef0b6 100644 --- a/askomics/libaskomics/File.py +++ b/askomics/libaskomics/File.py @@ -387,7 +387,10 @@ def get_faldo_strand(self, raw_strand): if raw_strand in ("-", "minus", "moins", "-1"): return self.faldo.ReverseStrandPosition - return self.faldo.BothStrandPosition + if raw_strand == "both": + return self.faldo.BothStrandPosition + + return self.faldo.StrandPosition def get_rdf_type(self, value): """get xsd type of a value diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 044a0b25..b1563c2f 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -548,7 +548,15 @@ def triple_dict_to_string(self, triple_dict): """ triple = "{} {} {} .".format(triple_dict["subject"], triple_dict["predicate"], triple_dict["object"]) if triple_dict["optional"]: - triple = "OPTIONAL {{{}}}".format(triple) + if triple_dict.get("nested_start"): + triple = "OPTIONAL {{{}".format(triple) + else: + triple = "OPTIONAL {{{}}}".format(triple) + # Close the }} if end of the nest + if triple_dict.get("nested_end"): + triple = " " + triple + "}}" + elif triple_dict.get("nested"): + triple = " " + triple return triple @@ -1184,14 +1192,16 @@ def build_query_from_json(self, preview=False, for_editor=False): "subject": node_uri, "predicate": category_name, "object": category_value_uri, - "optional": True if attribute["optional"] else False + "optional": True if attribute["optional"] else False, + "nested_start": True if attribute["optional"] else False }, block_id, sblock_id, pblock_ids) if attribute["visible"]: self.store_triple({ "subject": category_value_uri, "predicate": "rdfs:label", "object": category_label, - "optional": True if attribute["optional"] else False + "optional": True if attribute["optional"] else False, + "nested_end": True if attribute["optional"] else False }, block_id, sblock_id, pblock_ids) elif attribute["faldo"] and attribute["faldo"].endswith("faldoStrand"): category_name = 'faldo:location/faldo:begin/rdf:type' @@ -1214,21 +1224,23 @@ def build_query_from_json(self, preview=False, for_editor=False): "object": category_label, "optional": False }, block_id, sblock_id, pblock_ids) - self.store_value("VALUES {} {{ faldo:ReverseStrandPosition faldo:ForwardStrandPosition }} .".format(category_value_uri), block_id, sblock_id, pblock_ids) + self.store_value("VALUES {} {{ faldo:ReverseStrandPosition faldo:ForwardStrandPosition faldo:BothStrandPosition faldo:StrandPosition}} .".format(category_value_uri), block_id, sblock_id, pblock_ids) else: category_name = "<{}>".format(attribute["uri"]) self.store_triple({ "subject": node_uri, "predicate": category_name, "object": category_value_uri, - "optional": True if attribute["optional"] else False + "optional": True if attribute["optional"] else False, + "nested_start": True if attribute["optional"] else False }, block_id, sblock_id, pblock_ids) - if attribute["visible"]: + if attribute["visible"] and not attribute["optional"]: self.store_triple({ "subject": category_value_uri, "predicate": "rdfs:label", "object": category_label, - "optional": True if attribute["optional"] else False + "optional": True if attribute["optional"] else False, + "nested_end": True if attribute["optional"] else False }, block_id, sblock_id, pblock_ids) if attribute["visible"]: From 43819cb732bad2f9354c9c862fb2521bfecc62e5 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 13 Apr 2021 09:04:21 +0000 Subject: [PATCH 020/318] Fix default --- askomics/libaskomics/BedFile.py | 10 +++++++++- askomics/libaskomics/CsvFile.py | 2 +- askomics/libaskomics/File.py | 4 +--- askomics/libaskomics/GffFile.py | 8 +++++++- askomics/libaskomics/SparqlQuery.py | 2 +- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/askomics/libaskomics/BedFile.py b/askomics/libaskomics/BedFile.py index 6bed8555..cb45c8de 100644 --- a/askomics/libaskomics/BedFile.py +++ b/askomics/libaskomics/BedFile.py @@ -231,6 +231,14 @@ def generate_rdf_content(self): self.faldo_abstraction["strand"] = relation self.graph_chunk.add((entity, relation, attribute)) strand = True + else: + self.category_values["strand"] = {".", } + relation = self.namespace_data[self.format_uri("strand")] + attribute = self.namespace_data[self.format_uri(".")] + faldo_strand = self.get_faldo_strand(".") + self.faldo_abstraction["strand"] = relation + self.graph_chunk.add((entity, relation, attribute)) + strand = True if strand: if "strand" not in attribute_list: @@ -241,7 +249,7 @@ def generate_rdf_content(self): "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": ["+", "-", "."] }) # Score diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index d76e4557..b4015623 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -506,7 +506,7 @@ def generate_rdf_content(self): elif current_type in ('category', 'reference', 'strand'): potential_relation = self.rdfize(current_header) if not cell: - cell = "unknown" + cell = "unknown/both" if current_header not in self.category_values.keys(): # Add the category in dict, and the first value in a set self.category_values[current_header] = {cell, } diff --git a/askomics/libaskomics/File.py b/askomics/libaskomics/File.py index f32ef0b6..d441351a 100644 --- a/askomics/libaskomics/File.py +++ b/askomics/libaskomics/File.py @@ -387,10 +387,8 @@ def get_faldo_strand(self, raw_strand): if raw_strand in ("-", "minus", "moins", "-1"): return self.faldo.ReverseStrandPosition - if raw_strand == "both": - return self.faldo.BothStrandPosition + return self.faldo.BothStrandPosition - return self.faldo.StrandPosition def get_rdf_type(self, value): """get xsd type of a value diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index 55ff0453..96999a2a 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -272,6 +272,12 @@ def generate_rdf_content(self): faldo_strand = self.get_faldo_strand("-") self.faldo_abstraction["strand"] = relation # self.graph_chunk.add((entity, relation, attribute)) + else: + self.category_values["strand"] = {".", } + relation = self.namespace_data[self.format_uri("strand")] + attribute = self.namespace_data[self.format_uri(".")] + faldo_strand = self.get_faldo_strand(".") + self.faldo_abstraction["strand"] = relation if (feature.type, "strand") not in attribute_list: attribute_list.append((feature.type, "strand")) @@ -281,7 +287,7 @@ def generate_rdf_content(self): "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": ["+", "-", "."] }) # Qualifiers (9th columns) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index b1563c2f..7753720e 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -1224,7 +1224,7 @@ def build_query_from_json(self, preview=False, for_editor=False): "object": category_label, "optional": False }, block_id, sblock_id, pblock_ids) - self.store_value("VALUES {} {{ faldo:ReverseStrandPosition faldo:ForwardStrandPosition faldo:BothStrandPosition faldo:StrandPosition}} .".format(category_value_uri), block_id, sblock_id, pblock_ids) + self.store_value("VALUES {} {{ faldo:ReverseStrandPosition faldo:ForwardStrandPosition faldo:BothStrandPosition}} .".format(category_value_uri), block_id, sblock_id, pblock_ids) else: category_name = "<{}>".format(attribute["uri"]) self.store_triple({ From d8cb99f54891d79b2f30bfbd7444250c3f345ecb Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 13 Apr 2021 09:06:21 +0000 Subject: [PATCH 021/318] Lint --- askomics/libaskomics/File.py | 1 - 1 file changed, 1 deletion(-) diff --git a/askomics/libaskomics/File.py b/askomics/libaskomics/File.py index d441351a..8675817c 100644 --- a/askomics/libaskomics/File.py +++ b/askomics/libaskomics/File.py @@ -389,7 +389,6 @@ def get_faldo_strand(self, raw_strand): return self.faldo.BothStrandPosition - def get_rdf_type(self, value): """get xsd type of a value From 88358abd418d4061f80bfbd5deb050e4093b4b1d Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 13 Apr 2021 10:07:21 +0000 Subject: [PATCH 022/318] Tests --- tests/results/abstraction.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/results/abstraction.json b/tests/results/abstraction.json index 9b15d1bd..0848be0e 100644 --- a/tests/results/abstraction.json +++ b/tests/results/abstraction.json @@ -42,7 +42,11 @@ { "label": "-", "uri": "http://askomics.org/test/data/-" - } + }, + { + "label": ".", + "uri": "http://askomics.org/test/data/." + }, ], "entityUri": "http://askomics.org/test/data/gene", "faldo": "http://askomics.org/test/internal/faldoStrand", @@ -72,6 +76,10 @@ { "label": "-", "uri": "http://askomics.org/test/data/-" + }, + { + "label": ".", + "uri": "http://askomics.org/test/data/." } ], "entityUri": "http://askomics.org/test/data/transcript", @@ -602,4 +610,4 @@ "diskSpace": ###SIZE###, "error": false, "errorMessage": "" -} \ No newline at end of file +} From 497fb4317edbb7d5b524a8f7a79589c3e07c8546 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 13 Apr 2021 10:23:23 +0000 Subject: [PATCH 023/318] typo --- tests/results/abstraction.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/results/abstraction.json b/tests/results/abstraction.json index 0848be0e..9f5dcfa0 100644 --- a/tests/results/abstraction.json +++ b/tests/results/abstraction.json @@ -46,7 +46,7 @@ { "label": ".", "uri": "http://askomics.org/test/data/." - }, + } ], "entityUri": "http://askomics.org/test/data/gene", "faldo": "http://askomics.org/test/internal/faldoStrand", From 08b66143552056235ee5491c71bb980dff850c12 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 16 Apr 2021 09:39:46 +0000 Subject: [PATCH 024/318] Changed prediction --- askomics/libaskomics/CsvFile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index b4015623..96524015 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -210,8 +210,8 @@ def guess_column_type(self, values, header_index): # Test if start and end are numerical if stype in ('start', 'end') and not all(self.is_decimal(val) for val in values): break - # test if strand is a category with 2 elements max - if stype == 'strand' and len(set(list(filter(None, values)))) > 2: + # test if strand is a category with 3 elements max + if stype == 'strand' and len(set(list(filter(None, values)))) > 3: break # Test if date respect a date format if stype == 'datetime' and all(date_regex.match(val) for val in values): From ef1ac78ed04afac6509ba79f556049f90259659a Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 16 Apr 2021 09:57:43 +0000 Subject: [PATCH 025/318] Woops --- askomics/libaskomics/CsvFile.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index e576571d..8fc60860 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -203,19 +203,19 @@ def guess_column_type(self, values, header_index): # Then, detect special type with header for stype, expressions in special_types.items(): - for expression in expressions: - epression_regexp = ".*{}.*".format(expression.lower()) - if re.match(epression_regexp, self.header[header_index], re.IGNORECASE) is not None: - # Test if start and end are numerical - if stype in ('start', 'end') and not all(self.is_decimal(val) for val in values): - break - # test if strand is a category with 3 elements max - if stype == 'strand' and len(set(list(filter(None, values)))) > 3: - break - # Test if date respect a date format - if stype == 'datetime' and all(date_regex.match(val) for val in values): - break - return stype + # Need to check once if it matches any subtype + expression_regexp = "|".join([".*{}.*".format(expression.lower()) for expression in expressions]) + if re.match(expression_regexp, self.header[header_index].lower(), re.IGNORECASE) is not None: + # Test if start and end are numerical + if stype in ('start', 'end') and not all(self.is_decimal(val) for val in values): + break + # test if strand is a category with 3 elements max + if stype == 'strand' and len(set(list(filter(None, values)))) > 3: + break + # Test if date respects a date format + if stype == 'date' and not all(self.is_date(val) for val in values): + break + return stype # Then, check goterm # if all((val.startswith("GO:") and val[3:].isdigit()) for val in values): From e6775094b6ef30a6501a0711a5d674ebf94ab0df Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 16 Apr 2021 10:53:07 +0000 Subject: [PATCH 026/318] Double brace --- askomics/libaskomics/SparqlQuery.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 01ef628f..5403821d 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -554,7 +554,7 @@ def triple_dict_to_string(self, triple_dict): triple = "OPTIONAL {{{}}}".format(triple) # Close the }} if end of the nest if triple_dict.get("nested_end"): - triple = " " + triple + "}}" + triple = " " + triple + "}" elif triple_dict.get("nested"): triple = " " + triple @@ -1224,7 +1224,7 @@ def build_query_from_json(self, preview=False, for_editor=False): "predicate": category_name, "object": category_value_uri, "optional": True if attribute["optional"] else False, - "nested_start": True if attribute["optional"] else False + "nested_start": True if (attribute["optional"] and attribute["visible"]) else False }, block_id, sblock_id, pblock_ids) if attribute["visible"]: self.store_triple({ @@ -1263,9 +1263,9 @@ def build_query_from_json(self, preview=False, for_editor=False): "predicate": category_name, "object": category_value_uri, "optional": True if attribute["optional"] else False, - "nested_start": True if attribute["optional"] else False + "nested_start": True if (attribute["optional"] and attribute["visible"]) else False }, block_id, sblock_id, pblock_ids) - if attribute["visible"] and not attribute["optional"]: + if attribute["visible"]: self.store_triple({ "subject": category_value_uri, "predicate": "rdfs:label", From 3c07ff62e2311793cd9efd5eaf1cceb4523235f9 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 16 Apr 2021 14:53:55 +0000 Subject: [PATCH 027/318] Raise Exception if empty column name --- askomics/libaskomics/CsvFile.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 8fc60860..6b26ae78 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -108,6 +108,8 @@ def set_preview_and_header(self): # Store header header = next(reader) self.header = [h.strip() for h in header] + if not all(self.header): + raise Exception("Empty column in header") # Loop on lines preview = [] From 45f978afb91bf27fffde30e3e34cfa69edcc03ec Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 16 Apr 2021 15:14:12 +0000 Subject: [PATCH 028/318] Test for missing column name --- test-data/malformed.tsv | 2 ++ tests/results/preview_malformed_files.json | 26 ++++++++++++++++++++++ tests/test_api_file.py | 13 +++++++++++ 3 files changed, 41 insertions(+) create mode 100644 test-data/malformed.tsv create mode 100644 tests/results/preview_malformed_files.json diff --git a/test-data/malformed.tsv b/test-data/malformed.tsv new file mode 100644 index 00000000..a78566e8 --- /dev/null +++ b/test-data/malformed.tsv @@ -0,0 +1,2 @@ + column2 column3 +val1 val2 val3 diff --git a/tests/results/preview_malformed_files.json b/tests/results/preview_malformed_files.json new file mode 100644 index 00000000..c1a91892 --- /dev/null +++ b/tests/results/preview_malformed_files.json @@ -0,0 +1,26 @@ +{ + "error": false, + "errorMessage": "", + "previewFiles": [ + { + "data": { + "columns_type": [ + "start_entity", + "text", + "text", + ], + "content_preview": [], + "header": [ + "", + "column2", + "column3", + ] + }, + "id": 6, + "name": "malformed.tsv", + "type": "csv/tsv", + "error": true, + "error_message": "Malformated CSV/TSV (Empty column in header)" + } + ] +} diff --git a/tests/test_api_file.py b/tests/test_api_file.py index 137836f4..b5e422c0 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -331,6 +331,8 @@ def test_get_preview(self, client): client.log_user("jdoe") client.upload() + client.upload_file("test-data/malformed.tsv") + csv_data = { "filesId": [1, ] } @@ -343,9 +345,16 @@ def test_get_preview(self, client): "filesId": [42, ] } + malformed_data = { + "filesId": [6, ] + } + with open("tests/results/preview_files.json") as file: csv_expected = json.loads(file.read()) + with open("tests/results/preview_malformed_files.json") as file: + csv_malformed = json.loads(file.read()) + response = client.client.post('/api/files/preview', json=fake_data) assert response.status_code == 200 assert response.json == { @@ -354,6 +363,10 @@ def test_get_preview(self, client): 'previewFiles': [] } + response = client.client.post('/api/files/preview', json=malformed_data) + assert response.status_code == 200 + assert response.json == csv_malformed + response = client.client.post('/api/files/preview', json=csv_data) assert response.status_code == 200 assert response.json == csv_expected From 3e6c7b10fb369250fad181d6646187071f98d8b3 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 16 Apr 2021 15:29:17 +0000 Subject: [PATCH 029/318] Fix json file --- tests/results/preview_malformed_files.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/results/preview_malformed_files.json b/tests/results/preview_malformed_files.json index c1a91892..2035e650 100644 --- a/tests/results/preview_malformed_files.json +++ b/tests/results/preview_malformed_files.json @@ -7,13 +7,13 @@ "columns_type": [ "start_entity", "text", - "text", + "text" ], "content_preview": [], "header": [ "", "column2", - "column3", + "column3" ] }, "id": 6, From 88daf8ad23fba1a4fa0ffda184f77df7ebe0d6b3 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 28 Apr 2021 07:49:54 +0000 Subject: [PATCH 030/318] Fix ordering --- .../react/src/routes/sparql/resultstable.jsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/askomics/react/src/routes/sparql/resultstable.jsx b/askomics/react/src/routes/sparql/resultstable.jsx index 421f4223..b6c67e6c 100644 --- a/askomics/react/src/routes/sparql/resultstable.jsx +++ b/askomics/react/src/routes/sparql/resultstable.jsx @@ -9,11 +9,37 @@ import Utils from '../../classes/utils' export default class ResultsTable extends Component { constructor (props) { super(props) + this.state = { + filter_columns: {} + } this.utils = new Utils() + this.custom_compare = this.custom_compare.bind(this) + } + + custom_compare(a, b, column_name){ + let result, num_a, num_b; + if (typeof b === 'string') { + if (this.state.filter_columns[column_name]){ + num_a = Number(a) + num_b = Number(b) + if (Number.isNaN(num_a) || Number.isNaN(num_b)){ + this.state.filter_columns[column_name] = false + result = b.localeCompare(a); + } else { + result = num_a > num_b ? -1 : ((num_a < num_b) ? 1 : 0); + } + } else { + result = b.localeCompare(a); + } + } else { + result = a > b ? -1 : ((a < b) ? 1 : 0); + } + return result; } render () { let columns = this.props.header.map((colName, index) => { + this.state.filter_columns[colName] = true; return ({ dataField: colName, text: colName, @@ -24,6 +50,12 @@ export default class ResultsTable extends Component { return {this.utils.splitUrl(cell)} } return cell + }, + sortFunc: (a, b, order, dataField, rowA, rowB) => { + if (order === 'asc') { + return this.custom_compare(a,b, dataField); + } + return this.custom_compare(b,a, dataField); // desc } }) }) From eebc75a14d3ce3891b6f007edea6afb356bfad9e Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 28 Apr 2021 09:20:27 +0000 Subject: [PATCH 031/318] More proper react way --- .../react/src/routes/sparql/resultstable.jsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/askomics/react/src/routes/sparql/resultstable.jsx b/askomics/react/src/routes/sparql/resultstable.jsx index b6c67e6c..14a2f77f 100644 --- a/askomics/react/src/routes/sparql/resultstable.jsx +++ b/askomics/react/src/routes/sparql/resultstable.jsx @@ -9,9 +9,7 @@ import Utils from '../../classes/utils' export default class ResultsTable extends Component { constructor (props) { super(props) - this.state = { - filter_columns: {} - } + this.state = {} this.utils = new Utils() this.custom_compare = this.custom_compare.bind(this) } @@ -19,11 +17,13 @@ export default class ResultsTable extends Component { custom_compare(a, b, column_name){ let result, num_a, num_b; if (typeof b === 'string') { - if (this.state.filter_columns[column_name]){ + if (this.state[column_name] === true){ num_a = Number(a) num_b = Number(b) if (Number.isNaN(num_a) || Number.isNaN(num_b)){ - this.state.filter_columns[column_name] = false + this.setState({ + [column_name]: false + }) result = b.localeCompare(a); } else { result = num_a > num_b ? -1 : ((num_a < num_b) ? 1 : 0); @@ -37,9 +37,16 @@ export default class ResultsTable extends Component { return result; } + componentDidMount () { + let columns = this.props.header.map((colName, index) => { + this.setState({ + [colName]: true + }) + }) + } + render () { let columns = this.props.header.map((colName, index) => { - this.state.filter_columns[colName] = true; return ({ dataField: colName, text: colName, From 8ac96fbdd9899d1856920f178eb78b877fd0f2ef Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 28 Apr 2021 12:38:27 +0000 Subject: [PATCH 032/318] Cleaner way --- .../react/src/routes/sparql/resultstable.jsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/askomics/react/src/routes/sparql/resultstable.jsx b/askomics/react/src/routes/sparql/resultstable.jsx index 14a2f77f..a555cfc3 100644 --- a/askomics/react/src/routes/sparql/resultstable.jsx +++ b/askomics/react/src/routes/sparql/resultstable.jsx @@ -5,24 +5,28 @@ import WaitingDiv from '../../components/waiting' import { Badge } from 'reactstrap' import PropTypes from 'prop-types' import Utils from '../../classes/utils' +import update from 'immutability-helper'; export default class ResultsTable extends Component { constructor (props) { super(props) - this.state = {} + this.state = { + filter_columns: {} + } this.utils = new Utils() this.custom_compare = this.custom_compare.bind(this) } custom_compare(a, b, column_name){ + console.log(this.state) let result, num_a, num_b; if (typeof b === 'string') { - if (this.state[column_name] === true){ + if (this.state.filter_columns[column_name] === true){ num_a = Number(a) num_b = Number(b) if (Number.isNaN(num_a) || Number.isNaN(num_b)){ this.setState({ - [column_name]: false + filter_columns: update(this.state.filter_columns, {[column_name]: {$set: false}}) }) result = b.localeCompare(a); } else { @@ -38,10 +42,12 @@ export default class ResultsTable extends Component { } componentDidMount () { - let columns = this.props.header.map((colName, index) => { - this.setState({ - [colName]: true - }) + let filter_columns = {} + this.props.header.map((colName, index) => { + filter_columns[colName] = true; + }) + this.setState({ + filter_columns: filter_columns }) } From c35d280a88cd8df83ec44d4ee5da622ef8c881e8 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 28 Apr 2021 12:50:24 +0000 Subject: [PATCH 033/318] Remove debug --- askomics/react/src/routes/sparql/resultstable.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/askomics/react/src/routes/sparql/resultstable.jsx b/askomics/react/src/routes/sparql/resultstable.jsx index a555cfc3..f2592ee1 100644 --- a/askomics/react/src/routes/sparql/resultstable.jsx +++ b/askomics/react/src/routes/sparql/resultstable.jsx @@ -18,7 +18,6 @@ export default class ResultsTable extends Component { } custom_compare(a, b, column_name){ - console.log(this.state) let result, num_a, num_b; if (typeof b === 'string') { if (this.state.filter_columns[column_name] === true){ From e6408d7b3220b975c31b36378ac5e4716c98b782 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 30 Apr 2021 13:01:48 +0000 Subject: [PATCH 034/318] Fix --- askomics/libaskomics/SparqlQuery.py | 33 +++++++++++++++-------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 5403821d..424d42ca 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -511,25 +511,26 @@ def triple_block_to_string(self, triple_block): str Corresponding SPARQL """ - # if triple_block["type"] == "UNION": if triple_block["type"] in ("UNION", "MINUS"): - block_string = "" - length = len(triple_block) - 1 - i = 1 + block_string = "{\n" + i = 0 + current_spacing = " " for sblock in triple_block["sblocks"]: sblock_string = "{" - triples_string = '\n'.join([self.triple_dict_to_string(triple_dict) for triple_dict in sblock["triples"]]) - triples_string += '\n' - triples_string += '\n'.join([filtr for filtr in sblock["filters"]]) - triples_string += '\n' - triples_string += '\n'.join([value for value in sblock["values"]]) - sblock_string += "\n {}\n}}".format(triples_string) - - block_string += triple_block["type"] if i == length else "" - i += 1 + triples_string = '\n{}'.format(current_spacing * 2).join([self.triple_dict_to_string(triple_dict) for triple_dict in sblock["triples"]]) + triples_string += '\n{}'.format(current_spacing * 2) + triples_string += '\n{}'.format(current_spacing * 2).join([filtr for filtr in sblock["filters"]]) + triples_string += '\n{}'.format(current_spacing * 2) + triples_string += '\n{}'.format(current_spacing * 2).join([value for value in sblock["values"]]) + sblock_string += "\n{}{}\n{}}}".format(current_spacing * 2, triples_string, current_spacing * 2) + + block_string += "{}{} ".format(current_spacing * 2, triple_block["type"]) if (triple_block["type"] == "MINUS" or i > 0) else current_spacing * 2 block_string += sblock_string + "\n" + i += 1 + + block_string += current_spacing + "}\n" return block_string @@ -803,7 +804,7 @@ def replace_variables_in_triples(self, var_to_replace): var_target = tpl_var[1] for i, triple_dict in enumerate(self.triples): for key, value in triple_dict.items(): - if key != "optional": + if key not in ["optional", "nested", "nested_start", "nested_end"]: self.triples[i][key] = value.replace(var_source, var_target) for i, select in enumerate(self.selects): self.selects[i] = select.replace(var_source, var_target) @@ -830,7 +831,7 @@ def replace_variables_in_blocks(self, var_to_replace): # Iterate over triples for ntriple, triple_dict in enumerate(sblock["triples"]): for key, value in triple_dict.items(): - if key != "optional": + if key not in ["optional", "nested", "nested_start", "nested_end"]: self.triples_blocks[nblock]["sblocks"][nsblock]["triples"][ntriple][key] = value.replace(var_source, var_target) for i, filtr in enumerate(sblock["filters"]): @@ -1380,7 +1381,7 @@ def build_query_from_json(self, preview=False, for_editor=False): """.format( 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]), + blocks='\n '.join(self.triple_blocks_to_string()), filters='\n '.join(self.filters), values='\n '.join(self.values)) From 6ce8aa0a8d1c82dc779042e844af04f09d650eee Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 30 Apr 2021 13:18:29 +0000 Subject: [PATCH 035/318] Fix --- askomics/libaskomics/SparqlQuery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 424d42ca..46b004f1 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -1381,7 +1381,7 @@ def build_query_from_json(self, preview=False, for_editor=False): """.format( 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_blocks_to_string()), + blocks='\n '.join([self.triple_block_to_string(triple_block) for triple_block in self.triples_blocks]), filters='\n '.join(self.filters), values='\n '.join(self.values)) From ee19df99b27564059c49ea57085f0e512e821607 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 30 Apr 2021 13:19:42 +0000 Subject: [PATCH 036/318] Fix lint --- askomics/libaskomics/SparqlQuery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 46b004f1..502a9376 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -804,7 +804,7 @@ def replace_variables_in_triples(self, var_to_replace): var_target = tpl_var[1] for i, triple_dict in enumerate(self.triples): for key, value in triple_dict.items(): - if key not in ["optional", "nested", "nested_start", "nested_end"]: + if key not in ["optional", "nested", "nested_start", "nested_end"]: self.triples[i][key] = value.replace(var_source, var_target) for i, select in enumerate(self.selects): self.selects[i] = select.replace(var_source, var_target) @@ -831,7 +831,7 @@ def replace_variables_in_blocks(self, var_to_replace): # Iterate over triples for ntriple, triple_dict in enumerate(sblock["triples"]): for key, value in triple_dict.items(): - if key not in ["optional", "nested", "nested_start", "nested_end"]: + if key not in ["optional", "nested", "nested_start", "nested_end"]: self.triples_blocks[nblock]["sblocks"][nsblock]["triples"][ntriple][key] = value.replace(var_source, var_target) for i, filtr in enumerate(sblock["filters"]): From 5889093adad087e43a22fcce88b611c9b62bac52 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 3 May 2021 08:51:08 +0000 Subject: [PATCH 037/318] Exclude category --- askomics/libaskomics/SparqlQuery.py | 23 ++++++++++++------- askomics/react/src/routes/query/attribute.jsx | 8 +++++++ askomics/react/src/routes/query/query.jsx | 14 +++++++++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 502a9376..0ca12a56 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -1280,16 +1280,23 @@ def build_query_from_json(self, preview=False, for_editor=False): # values if attribute["filterSelectedValues"] != [] and not attribute["optional"] and not attribute["linked"]: uri_val_list = [] - for value in attribute["filterSelectedValues"]: - if attribute["faldo"] and attribute["faldo"].endswith("faldoStrand"): - value_var = faldo_strand - uri_val_list.append("<{}>".format(value)) - else: + if attribute["exclude"]: + for value in attribute["filterSelectedValues"]: value_var = category_value_uri uri_val_list.append("<{}>".format(value)) - - if uri_val_list: - self.store_value("VALUES {} {{ {} }}".format(value_var, ' '.join(uri_val_list)), block_id, sblock_id, pblock_ids) + filter_string = "FILTER ( {} NOT IN ( {} ) ) .".format(value_var, " ,".join(uri_val_list)) + self.store_filter(filter_string, block_id, sblock_id, pblock_ids) + else: + for value in attribute["filterSelectedValues"]: + if attribute["faldo"] and attribute["faldo"].endswith("faldoStrand"): + value_var = faldo_strand + uri_val_list.append("<{}>".format(value)) + else: + value_var = category_value_uri + uri_val_list.append("<{}>".format(value)) + + if uri_val_list: + self.store_value("VALUES {} {{ {} }}".format(value_var, ' '.join(uri_val_list)), block_id, sblock_id, pblock_ids) if attribute["linked"]: var_2 = self.format_sparql_variable("{}{}_{}Category".format( diff --git a/askomics/react/src/routes/query/attribute.jsx b/askomics/react/src/routes/query/attribute.jsx index 2dc0c2fa..de2b3417 100644 --- a/askomics/react/src/routes/query/attribute.jsx +++ b/askomics/react/src/routes/query/attribute.jsx @@ -19,6 +19,7 @@ export default class AttributeBox extends Component { this.toggleVisibility = this.props.toggleVisibility.bind(this) this.handleNegative = this.props.handleNegative.bind(this) this.toggleOptional = this.props.toggleOptional.bind(this) + this.toggleExclude = this.props.toggleExclude.bind(this) this.handleFilterType = this.props.handleFilterType.bind(this) this.handleFilterValue = this.props.handleFilterValue.bind(this) this.handleFilterCategory = this.props.handleFilterCategory.bind(this) @@ -292,6 +293,11 @@ export default class AttributeBox extends Component { linkIcon = 'attr-icon fas fa-link' } + let excludeIcon = 'attr-icon fas fa-ban inactive' + if (this.props.attribute.exclude) { + excludeIcon = 'attr-icon fas fa-ban' + } + let form if (this.props.attribute.linked) { @@ -315,6 +321,7 @@ export default class AttributeBox extends Component {
+
{form} @@ -471,6 +478,7 @@ AttributeBox.propTypes = { handleNegative: PropTypes.func, toggleVisibility: PropTypes.func, toggleOptional: PropTypes.func, + toggleExclude: PropTypes.func, toggleAddNumFilter: PropTypes.func, handleFilterType: PropTypes.func, handleFilterValue: PropTypes.func, diff --git a/askomics/react/src/routes/query/query.jsx b/askomics/react/src/routes/query/query.jsx index fa87c218..c6988585 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/query.jsx @@ -329,6 +329,7 @@ export default class Query extends Component { } if (attributeType == 'category') { + nodeAttribute.exclude = false nodeAttribute.filterValues = attr.categories nodeAttribute.filterSelectedValues = [] } @@ -989,6 +990,18 @@ export default class Query extends Component { this.updateGraphState() } + toggleExclude (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.exclude = !attr.exclude + if (attr.exclude) { + attr.visible = true + } + } + }) + this.updateGraphState() + } + toggleOptional (event) { this.graphState.attr.map(attr => { if (attr.id == event.target.id) { @@ -1428,6 +1441,7 @@ export default class Query extends Component { graph={this.state.graphState} handleChangeLink={p => this.handleChangeLink(p)} toggleVisibility={p => this.toggleVisibility(p)} + toggleExclude={p => this.toggleExclude(p)} handleNegative={p => this.handleNegative(p)} toggleOptional={p => this.toggleOptional(p)} handleFilterType={p => this.handleFilterType(p)} From 5bd558071117f229ae551fe1df448d0ea5185e7e Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 3 May 2021 14:18:57 +0000 Subject: [PATCH 038/318] Fix node issue --- askomics/react/src/routes/query/query.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/askomics/react/src/routes/query/query.jsx b/askomics/react/src/routes/query/query.jsx index c6988585..0e41ec66 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/query.jsx @@ -695,10 +695,11 @@ export default class Query extends Component { this.graphState.nodes.map(inode => { if (node.id == inode.id) { inode.suggested = false + inode.humanId = inode.humanId ? inode.humanId : this.getHumanNodeId(inode.uri) } }) // get attributes (only for nodes) - if (node.type =="node") { + if (node.type == "node") { this.setNodeAttributes(node.uri, node.id) } } From ed03a07e781c3bd06c85efc27aa96eeb1b3d2865 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 3 May 2021 15:31:22 +0000 Subject: [PATCH 039/318] Fix faldo --- askomics/libaskomics/SparqlQuery.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 0ca12a56..2d071644 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -1280,23 +1280,20 @@ def build_query_from_json(self, preview=False, for_editor=False): # values if attribute["filterSelectedValues"] != [] and not attribute["optional"] and not attribute["linked"]: uri_val_list = [] - if attribute["exclude"]: - for value in attribute["filterSelectedValues"]: + for value in attribute["filterSelectedValues"]: + if attribute["faldo"] and attribute["faldo"].endswith("faldoStrand"): + value_var = faldo_strand + uri_val_list.append("<{}>".format(value)) + else: value_var = category_value_uri uri_val_list.append("<{}>".format(value)) - filter_string = "FILTER ( {} NOT IN ( {} ) ) .".format(value_var, " ,".join(uri_val_list)) - self.store_filter(filter_string, block_id, sblock_id, pblock_ids) - else: - for value in attribute["filterSelectedValues"]: - if attribute["faldo"] and attribute["faldo"].endswith("faldoStrand"): - value_var = faldo_strand - uri_val_list.append("<{}>".format(value)) - else: - value_var = category_value_uri - uri_val_list.append("<{}>".format(value)) if uri_val_list: - self.store_value("VALUES {} {{ {} }}".format(value_var, ' '.join(uri_val_list)), block_id, sblock_id, pblock_ids) + if attribute["exclude"]: + filter_string = "FILTER ( {} NOT IN ( {} ) ) .".format(value_var, " ,".join(uri_val_list)) + self.store_filter(filter_string, block_id, sblock_id, pblock_ids) + else: + self.store_value("VALUES {} {{ {} }}".format(value_var, ' '.join(uri_val_list)), block_id, sblock_id, pblock_ids) if attribute["linked"]: var_2 = self.format_sparql_variable("{}{}_{}Category".format( From 04e98d175ebe67c99a168029883831648f4480cd Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 4 May 2021 15:41:00 +0200 Subject: [PATCH 040/318] [WIP] Token auth for API + added checks on most routes (#198) Api_key auth --- askomics/api/admin.py | 15 ++- askomics/api/auth.py | 58 +++++++++++- askomics/api/data.py | 2 + askomics/api/datasets.py | 23 ++++- askomics/api/file.py | 89 ++++++++++++++++-- askomics/api/galaxy.py | 33 +++++-- askomics/api/query.py | 25 ++++- askomics/api/results.py | 133 +++++++++++++++++++++++---- askomics/api/sparql.py | 45 ++++++--- askomics/api/start.py | 2 + askomics/libaskomics/BedFile.py | 7 +- askomics/libaskomics/CsvFile.py | 7 +- askomics/libaskomics/FilesHandler.py | 40 +++++++- askomics/libaskomics/GffFile.py | 8 +- askomics/libaskomics/LocalAuth.py | 16 ++-- askomics/tasks.py | 8 +- tests/test_api_auth.py | 24 ++++- tests/test_api_query.py | 2 +- tests/test_api_sparql.py | 8 +- 19 files changed, 461 insertions(+), 84 deletions(-) diff --git a/askomics/api/admin.py b/askomics/api/admin.py index bc6b114d..f464182d 100644 --- a/askomics/api/admin.py +++ b/askomics/api/admin.py @@ -2,7 +2,7 @@ import sys import traceback -from askomics.api.auth import admin_required +from askomics.api.auth import api_auth, admin_required from askomics.libaskomics.DatasetsHandler import DatasetsHandler from askomics.libaskomics.FilesHandler import FilesHandler from askomics.libaskomics.LocalAuth import LocalAuth @@ -16,6 +16,7 @@ @admin_bp.route('/api/admin/getusers', methods=['GET']) +@api_auth @admin_required def get_users(): """Get all users @@ -46,6 +47,7 @@ def get_users(): @admin_bp.route('/api/admin/getdatasets', methods=['GET']) +@api_auth @admin_required def get_datasets(): """Get all datasets @@ -76,6 +78,7 @@ def get_datasets(): @admin_bp.route('/api/admin/getfiles', methods=['GET']) +@api_auth @admin_required def get_files(): """Get all files info @@ -107,6 +110,7 @@ def get_files(): @admin_bp.route('/api/admin/getqueries', methods=['GET']) +@api_auth @admin_required def get_queries(): """Get all public queries @@ -138,6 +142,7 @@ def get_queries(): @admin_bp.route('/api/admin/setadmin', methods=['POST']) +@api_auth @admin_required def set_admin(): """change admin status of a user @@ -167,6 +172,7 @@ def set_admin(): @admin_bp.route('/api/admin/setquota', methods=["POST"]) +@api_auth @admin_required def set_quota(): """Change quota of a user @@ -200,6 +206,7 @@ def set_quota(): @admin_bp.route('/api/admin/setblocked', methods=['POST']) +@api_auth @admin_required def set_blocked(): """Change blocked status of a user @@ -229,6 +236,7 @@ def set_blocked(): @admin_bp.route('/api/admin/publicize_dataset', methods=['POST']) +@api_auth @admin_required def toogle_public_dataset(): """Toggle public status of a dataset @@ -269,6 +277,7 @@ def toogle_public_dataset(): @admin_bp.route('/api/admin/publicize_query', methods=['POST']) +@api_auth @admin_required def togle_public_query(): """Publish a query template from a result @@ -305,6 +314,7 @@ def togle_public_query(): @admin_bp.route("/api/admin/adduser", methods=["POST"]) +@api_auth @admin_required def add_user(): """Change blocked status of a user @@ -360,6 +370,7 @@ def add_user(): @admin_bp.route("/api/admin/delete_users", methods=["POST"]) +@api_auth @admin_required def delete_users(): """Delete users data @@ -411,6 +422,7 @@ def delete_users(): @admin_bp.route("/api/admin/delete_files", methods=["POST"]) +@api_auth @admin_required def delete_files(): """Delete files @@ -443,6 +455,7 @@ def delete_files(): @admin_bp.route("/api/admin/delete_datasets", methods=["POST"]) +@api_auth @admin_required def delete_datasets(): """Delete some datasets (db and triplestore) with a celery task diff --git a/askomics/api/auth.py b/askomics/api/auth.py index 69b68264..607735b2 100644 --- a/askomics/api/auth.py +++ b/askomics/api/auth.py @@ -26,6 +26,22 @@ def decorated_function(*args, **kwargs): return decorated_function +def api_auth(f): + """Get info from token""" + @wraps(f) + def decorated_function(*args, **kwargs): + """Login required decorator""" + if request.headers.get("X-API-KEY"): + key = request.headers.get("X-API-KEY") + local_auth = LocalAuth(current_app, session) + authentication = local_auth.authenticate_user_with_apikey(key) + if not authentication["error"]: + session["user"] = authentication["user"] + return f(*args, **kwargs) + + return decorated_function + + def admin_required(f): """Login required function""" @wraps(f) @@ -69,11 +85,17 @@ def signup(): 'error': True, 'errorMessage': "Account creation is disabled", 'user': {} - }), 500 + }), 400 user = {} data = request.get_json() + if not data: + return jsonify({ + 'error': True, + 'errorMessage': "Missing parameters", + 'user': {} + }), 400 local_auth = LocalAuth(current_app, session) local_auth.check_inputs(data) @@ -100,6 +122,12 @@ def login(): Information about the logged user """ data = request.get_json() + if not (data and data.get("login") and data.get("password")): + return jsonify({ + 'error': True, + 'errorMessage': "Missing login or password", + 'user': None + }), 400 local_auth = LocalAuth(current_app, session) authentication = local_auth.authenticate_user(data["login"], data["password"]) @@ -171,6 +199,11 @@ def update_profile(): The updated user """ data = request.get_json() + if not (data and any([key in data for key in ["newFname", "newLname", "newEmail"]])): + return jsonify({ + "error": True, + "errorMessage": "Missing parameters" + }), 400 local_auth = LocalAuth(current_app, session) updated_user = local_auth.update_profile(data, session['user']) @@ -195,6 +228,11 @@ def update_password(): The user """ data = request.get_json() + if not (data and all([key in data for key in ["oldPassword", "newPassword", "confPassword"]])): + return jsonify({ + "error": True, + "errorMessage": "Missing parameters" + }), 400 local_auth = LocalAuth(current_app, session) updated_user = local_auth.update_password(data, session['user']) @@ -238,6 +276,12 @@ def update_galaxy(): The user with his new apikey """ data = request.get_json() + if not (data and data.get("gurl") and data.get("gkey")): + return jsonify({ + 'error': True, + 'errorMessage': "Missing parameters", + 'user': session["user"] + }), 400 local_auth = LocalAuth(current_app, session) if session["user"]["galaxy"]: @@ -274,6 +318,11 @@ def logout(): def reset_password(): """Reset password route""" data = request.get_json() + if not data: + return jsonify({ + "error": True, + "errorMessage": "Missing parameters" + }), 400 # Send a reset link if "login" in data: @@ -318,7 +367,7 @@ def reset_password(): }) # Update password - else: + elif all([key in data for key in ["token", "password", "passwordConf"]]): try: local_auth = LocalAuth(current_app, session) result = local_auth.reset_password_with_token(data["token"], data["password"], data["passwordConf"]) @@ -333,6 +382,11 @@ def reset_password(): "error": result["error"], "errorMessage": result["message"] }) + else: + return jsonify({ + "error": True, + "errorMessage": "Missing parameters" + }), 400 @auth_bp.route("/api/auth/delete_account", methods=["GET"]) diff --git a/askomics/api/data.py b/askomics/api/data.py index d3b23c94..2e92d875 100644 --- a/askomics/api/data.py +++ b/askomics/api/data.py @@ -3,6 +3,7 @@ import sys import traceback +from askomics.api.auth import api_auth from askomics.libaskomics.SparqlQuery import SparqlQuery from askomics.libaskomics.SparqlQueryLauncher import SparqlQueryLauncher @@ -13,6 +14,7 @@ @data_bp.route('/api/data/', methods=['GET']) +@api_auth def get_data(uri): """Get information about uri diff --git a/askomics/api/datasets.py b/askomics/api/datasets.py index d84ef2bd..bceb125e 100644 --- a/askomics/api/datasets.py +++ b/askomics/api/datasets.py @@ -2,7 +2,7 @@ import sys import traceback -from askomics.api.auth import login_required, admin_required +from askomics.api.auth import login_required, admin_required, api_auth from askomics.libaskomics.DatasetsHandler import DatasetsHandler from flask import (Blueprint, current_app, jsonify, request, session) @@ -12,6 +12,7 @@ @datasets_bp.route('/api/datasets', methods=['GET']) +@api_auth @login_required def get_datasets(): """Get datasets information @@ -42,6 +43,7 @@ def get_datasets(): @datasets_bp.route('/api/datasets/delete', methods=['POST']) +@api_auth @login_required def delete_datasets(): """Delete some datasets (db and triplestore) with a celery task @@ -53,6 +55,13 @@ def delete_datasets(): errorMessage: the error message of error, else an empty string """ data = request.get_json() + if not (data and data.get("datasetsIdToDelete")): + return jsonify({ + 'datasets': [], + 'error': True, + 'errorMessage': "Missing datasetsIdToDelete parameter" + }), 400 + datasets_info = [] for dataset_id in data['datasetsIdToDelete']: datasets_info.append({'id': dataset_id}) @@ -99,6 +108,7 @@ def delete_datasets(): @datasets_bp.route('/api/datasets/public', methods=['POST']) +@api_auth @admin_required def toogle_public(): """Toggle public status of a dataset @@ -110,6 +120,13 @@ def toogle_public(): errorMessage: the error message of error, else an empty string """ data = request.get_json() + if not (data and data.get("id")): + return jsonify({ + 'datasets': [], + 'error': True, + 'errorMessage': "Missing id parameter" + }), 400 + datasets_info = [{'id': data["id"]}] try: @@ -118,8 +135,8 @@ def toogle_public(): datasets_handler.handle_datasets() for dataset in datasets_handler.datasets: - current_app.logger.debug(data["newStatus"]) - dataset.toggle_public(data["newStatus"]) + current_app.logger.debug(data.get("newStatus", False)) + dataset.toggle_public(data.get("newStatus", False)) datasets = datasets_handler.get_datasets() diff --git a/askomics/api/file.py b/askomics/api/file.py index fe422b71..d7ddaf16 100644 --- a/askomics/api/file.py +++ b/askomics/api/file.py @@ -3,7 +3,7 @@ import traceback import urllib -from askomics.api.auth import login_required +from askomics.api.auth import login_required, api_auth from askomics.libaskomics.FilesHandler import FilesHandler from askomics.libaskomics.FilesUtils import FilesUtils from askomics.libaskomics.Dataset import Dataset @@ -14,6 +14,7 @@ @file_bp.route('/api/files', methods=['GET', 'POST']) +@api_auth @login_required def get_files(): """Get files info of the logged user @@ -28,7 +29,8 @@ def get_files(): files_id = None if request.method == 'POST': data = request.get_json() - files_id = data['filesId'] + if data: + files_id = data.get('filesId') try: files_handler = FilesHandler(current_app, session) @@ -52,6 +54,7 @@ def get_files(): @file_bp.route('/api/files/editname', methods=['POST']) +@api_auth @login_required def edit_file(): """Edit file name @@ -65,6 +68,14 @@ def edit_file(): """ data = request.get_json() current_app.logger.debug(data) + if not (data and data.get("id") and data.get("newName")): + return jsonify({ + 'files': [], + 'diskSpace': 0, + 'error': True, + 'errorMessage': "Missing parameters" + }), 400 + files_id = [data["id"]] new_name = data["newName"] @@ -94,6 +105,7 @@ def edit_file(): @file_bp.route('/api/files/upload_chunk', methods=['POST']) +@api_auth @login_required def upload_chunk(): """Upload a file chunk @@ -113,9 +125,22 @@ def upload_chunk(): 'errorMessage': "Exceeded quota", "path": '', "error": True - }), 500 + }), 400 data = request.get_json() + if not (data and all([key in data for key in ["first", "last", "size", "name", "type", "size", "chunk"]])): + return jsonify({ + "path": '', + "error": True, + "errorMessage": "Missing parameters" + }), 400 + + if not (data["first"] or data.get("path")): + return jsonify({ + "path": '', + "error": True, + "errorMessage": "Missing path parameter" + }), 400 try: files = FilesHandler(current_app, session) @@ -135,6 +160,8 @@ def upload_chunk(): @file_bp.route('/api/files/upload_url', methods=["POST"]) +@api_auth +@login_required def upload_url(): """Upload a distant file with an URL @@ -151,9 +178,14 @@ def upload_url(): return jsonify({ 'errorMessage': "Exceeded quota", "error": True - }), 500 + }), 400 data = request.get_json() + if not (data and data.get("url")): + return jsonify({ + "error": True, + "errorMessage": "Missing url parameter" + }), 400 try: files = FilesHandler(current_app, session) @@ -171,6 +203,7 @@ def upload_url(): @file_bp.route('/api/files/preview', methods=['POST']) +@api_auth @login_required def get_preview(): """Get files preview @@ -183,6 +216,12 @@ def get_preview(): errorMessage: the error message of error, else an empty string """ data = request.get_json() + if not (data and data.get('filesId')): + return jsonify({ + 'previewFiles': [], + 'error': True, + 'errorMessage': "Missing filesId parameter" + }), 400 try: files_handler = FilesHandler(current_app, session) @@ -209,10 +248,10 @@ def get_preview(): @file_bp.route('/api/files/delete', methods=['POST']) +@api_auth @login_required def delete_files(): """Delete files - Returns ------- json @@ -220,11 +259,18 @@ def delete_files(): 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('filesIdToDelete')): + return jsonify({ + 'files': [], + 'error': True, + 'errorMessage': "Missing filesIdToDelete parameter" + }), 400 try: files = FilesHandler(current_app, session) - remaining_files = files.delete_files(data['filesIdToDelete']) + remaining_files = files.delete_files(data.get('filesIdToDelete', [])) except Exception as e: traceback.print_exc(file=sys.stdout) return jsonify({ @@ -241,6 +287,7 @@ def delete_files(): @file_bp.route('/api/files/integrate', methods=['POST']) +@api_auth @login_required def integrate(): """Integrate a file @@ -253,6 +300,12 @@ def integrate(): errorMessage: the error message of error, else an empty string """ data = request.get_json() + if not (data and data.get("fileId")): + return jsonify({ + 'error': True, + 'errorMessage': "Missing fileId parameter", + 'task_id': '' + }), 400 session_dict = {'user': session['user']} task = None @@ -264,15 +317,15 @@ def integrate(): for file in files_handler.files: - data["externalEndpoint"] = data["externalEndpoint"] if data["externalEndpoint"] else None - data["customUri"] = data["customUri"] if data["customUri"] else None + data["externalEndpoint"] = data["externalEndpoint"] if data.get("externalEndpoint") else None + data["customUri"] = data["customUri"] if data.get("customUri") else None dataset_info = { "celery_id": None, "file_id": file.id, "name": file.human_name, "graph_name": file.file_graph, - "public": data["public"] if session["user"]["admin"] else False + "public": data.get("public") if session["user"]["admin"] else False } dataset = Dataset(current_app, session, dataset_info) @@ -299,6 +352,7 @@ def integrate(): @file_bp.route('/api/files/ttl///', methods=['GET']) +@api_auth def serve_file(path, user_id, username): """Serve a static ttl file of a user @@ -326,3 +380,20 @@ def serve_file(path, user_id, username): ) return(send_from_directory(dir_path, path)) + + +@file_bp.route('/api/files/columns', methods=['GET']) +def get_column_types(): + """Give the list of available column types + + Returns + ------- + json + types: list of available column types + """ + + data = ["numeric", "text", "category", "boolean", "date", "reference", "strand", "start", "end", "general_relation", "symetric_relation"] + + return jsonify({ + "types": data + }) diff --git a/askomics/api/galaxy.py b/askomics/api/galaxy.py index 50181542..ea1bb702 100644 --- a/askomics/api/galaxy.py +++ b/askomics/api/galaxy.py @@ -4,7 +4,7 @@ import json -from askomics.api.auth import login_required +from askomics.api.auth import api_auth, login_required from askomics.libaskomics.FilesUtils import FilesUtils from askomics.libaskomics.Galaxy import Galaxy @@ -14,6 +14,7 @@ @galaxy_bp.route('/api/galaxy/datasets', methods=['GET', 'POST']) +@api_auth @login_required def get_datasets(): """Get galaxy datasets and histories of a user @@ -27,7 +28,8 @@ def get_datasets(): """ history_id = None if request.method == 'POST': - history_id = request.get_json()["history_id"] + if request.get_json(): + history_id = request.get_json().get("history_id") try: galaxy = Galaxy(current_app, session) @@ -50,6 +52,7 @@ def get_datasets(): @galaxy_bp.route('/api/galaxy/queries', methods=['GET', 'POST']) +@api_auth @login_required def get_queries(): """Get galaxy queries (json datasets) @@ -63,7 +66,8 @@ def get_queries(): """ history_id = None if request.method == 'POST': - history_id = request.get_json()["history_id"] + if request.get_json(): + history_id = request.get_json().get("history_id") try: galaxy = Galaxy(current_app, session) @@ -86,6 +90,7 @@ def get_queries(): @galaxy_bp.route('/api/galaxy/upload_datasets', methods=['POST']) +@api_auth @login_required def upload_datasets(): """Download a galaxy datasets into AskOmics @@ -104,9 +109,16 @@ def upload_datasets(): return jsonify({ 'errorMessage': "Exceeded quota", "error": True - }), 500 + }), 400 + + data = request.get_json() + if not (data and data.get("datasets_id")): + return jsonify({ + 'error': True, + 'errorMessage': "Missing datasets_id parameter" + }), 400 - datasets_id = request.get_json()["datasets_id"] + datasets_id = data["datasets_id"] try: galaxy = Galaxy(current_app, session) @@ -125,6 +137,7 @@ def upload_datasets(): @galaxy_bp.route('/api/galaxy/getdatasetcontent', methods=['POST']) +@api_auth @login_required def get_dataset_content(): """Download a galaxy datasets into AskOmics @@ -136,7 +149,15 @@ def get_dataset_content(): error: True if error, else False errorMessage: the error message of error, else an empty string """ - dataset_id = request.get_json()["dataset_id"] + + data = request.get_json() + if not (data and data.get("dataset_id")): + return jsonify({ + 'error': True, + 'errorMessage': "Missing dataset_id parameter" + }), 400 + + dataset_id = data["dataset_id"] try: galaxy = Galaxy(current_app, session) diff --git a/askomics/api/query.py b/askomics/api/query.py index 19d46947..369b8c57 100644 --- a/askomics/api/query.py +++ b/askomics/api/query.py @@ -1,7 +1,7 @@ import sys import traceback -from askomics.api.auth import login_required +from askomics.api.auth import api_auth, login_required from askomics.libaskomics.FilesUtils import FilesUtils from askomics.libaskomics.ResultsHandler import ResultsHandler from askomics.libaskomics.Result import Result @@ -16,6 +16,7 @@ @query_bp.route('/api/query/startpoints', methods=['GET']) +@api_auth def query(): """Get start points @@ -55,6 +56,7 @@ def query(): @query_bp.route('/api/query/abstraction', methods=['GET']) +@api_auth def get_abstraction(): """Get abstraction @@ -92,6 +94,7 @@ def get_abstraction(): @query_bp.route('/api/query/preview', methods=['POST']) +@api_auth def get_preview(): """Get a preview of query @@ -110,6 +113,13 @@ def get_preview(): header = [] else: data = request.get_json() + if not (data and data.get("graphState")): + return jsonify({ + 'resultsPreview': [], + 'headerPreview': [], + 'error': True, + 'errorMessage': "Missing graphState parameter" + }), 400 query = SparqlQuery(current_app, session, data["graphState"]) query.build_query_from_json(preview=True, for_editor=False) @@ -140,6 +150,7 @@ def get_preview(): @query_bp.route('/api/query/save_result', methods=['POST']) +@api_auth @login_required def save_result(): """Save a query in filesystem and db, using a celery task @@ -160,10 +171,18 @@ def save_result(): 'error': True, 'errorMessage': "Exceeded quota", 'task_id': None - }), 500 + }), 400 # Get query and endpoints and graphs of the query - query = SparqlQuery(current_app, session, request.get_json()["graphState"]) + data = request.get_json() + if not (data and data.get("graphState")): + return jsonify({ + 'task_id': None, + 'error': True, + 'errorMessage': "Missing graphState parameter" + }), 400 + + query = SparqlQuery(current_app, session, data["graphState"]) query.build_query_from_json(preview=False, for_editor=False) info = { diff --git a/askomics/api/results.py b/askomics/api/results.py index 31965eb3..d43f80ce 100644 --- a/askomics/api/results.py +++ b/askomics/api/results.py @@ -1,7 +1,7 @@ import traceback import sys -from askomics.api.auth import login_required, admin_required +from askomics.api.auth import api_auth, login_required, admin_required from askomics.libaskomics.FilesUtils import FilesUtils from askomics.libaskomics.ResultsHandler import ResultsHandler from askomics.libaskomics.Result import Result @@ -19,6 +19,7 @@ def can_access(user): @results_bp.route('/api/results', methods=['GET']) +@api_auth @login_required def get_results(): """Get ... @@ -56,6 +57,7 @@ def get_results(): @results_bp.route('/api/results/preview', methods=['POST']) +@api_auth @login_required def get_preview(): """Summary @@ -69,7 +71,17 @@ def get_preview(): errorMessage: the error message of error, else an empty string """ try: - file_id = request.get_json()["fileId"] + data = request.get_json() + if not (data and data.get("fileId")): + return jsonify({ + 'preview': [], + 'header': [], + 'id': None, + 'error': True, + 'errorMessage': "Missing file Id" + }), 400 + + file_id = data["fileId"] result_info = {"id": file_id} result = Result(current_app, session, result_info) headers, preview = result.get_file_preview() @@ -94,6 +106,7 @@ def get_preview(): @results_bp.route('/api/results/getquery', methods=["POST"]) +@api_auth def get_graph_and_sparql_query(): """Get query (graphState or Sparql) @@ -104,7 +117,19 @@ def get_graph_and_sparql_query(): errorMessage: the error message of error, else an empty string """ try: - file_id = request.get_json()["fileId"] + data = request.get_json() + if not (data and data.get("fileId")): + return jsonify({ + 'graphState': {}, + 'sparqlQuery': "", + 'graphs': [], + 'endpoints': [], + 'diskSpace': 0, + 'error': True, + 'errorMessage': "Missing fileId parameter" + }), 400 + + file_id = data["fileId"] result_info = {"id": file_id} result = Result(current_app, session, result_info) @@ -147,6 +172,7 @@ def get_graph_and_sparql_query(): @results_bp.route('/api/results/graphstate', methods=['POST']) +@api_auth def get_graph_state(): """Summary @@ -159,7 +185,16 @@ def get_graph_state(): errorMessage: the error message of error, else an empty string """ try: - file_id = request.get_json()["fileId"] + data = request.get_json() + if not (data and data.get("fileId")): + return jsonify({ + 'graphState': {}, + 'id': None, + 'error': True, + 'errorMessage': "Missing fileId parameter" + }), 400 + + file_id = data["fileId"] result_info = {"id": file_id} result = Result(current_app, session, result_info) graph_state = result.get_graph_state(formated=True) @@ -182,11 +217,19 @@ def get_graph_state(): @results_bp.route('/api/results/download', methods=['POST']) +@api_auth @login_required def download_result(): """Download result file""" try: - file_id = request.get_json()["fileId"] + data = request.get_json() + if not (data and data.get("fileId")): + return jsonify({ + 'error': True, + 'errorMessage': "Missing fileId parameter" + }), 400 + + file_id = data["fileId"] result_info = {"id": file_id} result = Result(current_app, session, result_info) dir_path = result.get_dir_path() @@ -203,6 +246,7 @@ def download_result(): @results_bp.route('/api/results/delete', methods=['POST']) +@api_auth @login_required def delete_result(): """Summary @@ -215,7 +259,15 @@ def delete_result(): errorMessage: the error message of error, else an empty string """ try: - files_id = request.get_json()["filesIdToDelete"] + data = request.get_json() + if not (data and data.get("filesIdToDelete")): + return jsonify({ + 'remainingFiles': {}, + 'error': True, + 'errorMessage': "Missing filesIdToDelete parameter" + }), 400 + + files_id = data["filesIdToDelete"] results_handler = ResultsHandler(current_app, session) remaining_files = results_handler.delete_results(files_id) except Exception as e: @@ -234,6 +286,7 @@ def delete_result(): @results_bp.route('/api/results/sparqlquery', methods=['POST']) +@api_auth @login_required def get_sparql_query(): """Get sparql query of result for the query editor @@ -249,7 +302,18 @@ def get_sparql_query(): files_utils = FilesUtils(current_app, session) disk_space = files_utils.get_size_occupied_by_user() if "user" in session else None - file_id = request.get_json()["fileId"] + data = request.get_json() + if not (data and data.get("fileId")): + return jsonify({ + 'query': {}, + 'graphs': [], + 'endpoints': [], + 'diskSpace': 0, + 'error': True, + 'errorMessage': "Missing fileId parameter" + }), 400 + + file_id = data["fileId"] result_info = {"id": file_id} result = Result(current_app, session, result_info) @@ -294,6 +358,7 @@ def get_sparql_query(): @results_bp.route('/api/results/description', methods=['POST']) +@api_auth @login_required def set_description(): """Update a result description @@ -306,9 +371,16 @@ def set_description(): errorMessage: the error message of error, else an empty string """ try: - json = request.get_json() - result_info = {"id": json["id"]} - new_desc = json["newDesc"] + data = request.get_json() + if not (data and data.get("id") and data.get("newDesc")): + return jsonify({ + 'files': [], + 'error': True, + 'errorMessage': "Missing parameters" + }), 400 + + result_info = {"id": data["id"]} + new_desc = data["newDesc"] result = Result(current_app, session, result_info) result.update_description(new_desc) @@ -332,6 +404,7 @@ def set_description(): @results_bp.route('/api/results/publish', methods=['POST']) +@api_auth @admin_required def publish_query(): """Publish a query template from a result @@ -343,11 +416,18 @@ def publish_query(): errorMessage: the error message of error, else an empty string """ try: - json = request.get_json() - result_info = {"id": json["id"]} + data = request.get_json() + if not (data and data.get("id")): + return jsonify({ + 'files': [], + 'error': True, + 'errorMessage': "Missing id parameter" + }), 400 + + result_info = {"id": data["id"]} result = Result(current_app, session, result_info) - result.publish_query(json["public"]) + result.publish_query(data.get("public", False)) results_handler = ResultsHandler(current_app, session) files = results_handler.get_files_info() @@ -368,6 +448,7 @@ def publish_query(): @results_bp.route('/api/results/template', methods=['POST']) +@api_auth @login_required def template_query(): """Template a query from a result @@ -379,11 +460,18 @@ def template_query(): errorMessage: the error message of error, else an empty string """ try: - json = request.get_json() - result_info = {"id": json["id"]} + data = request.get_json() + if not (data and data.get("id")): + return jsonify({ + 'files': [], + 'error': True, + 'errorMessage': "Missing id parameter" + }), 400 + + result_info = {"id": data["id"]} result = Result(current_app, session, result_info) - result.template_query(json["template"]) + result.template_query(data.get("template", False)) results_handler = ResultsHandler(current_app, session) files = results_handler.get_files_info() @@ -404,6 +492,7 @@ def template_query(): @results_bp.route('/api/results/send2galaxy', methods=['POST']) +@api_auth @login_required def send2galaxy(): """Send a result file into Galaxy @@ -415,10 +504,16 @@ def send2galaxy(): errorMessage: the error message of error, else an empty string """ try: - json = request.get_json() - result_info = {"id": json["fileId"]} + data = request.get_json() + if not (data and data.get("fileId") and data.get("fileToSend")): + return jsonify({ + 'error': True, + 'errorMessage': "Missing parameters" + }), 400 + + result_info = {"id": data["fileId"]} result = Result(current_app, session, result_info) - result.send2galaxy(json["fileToSend"]) + result.send2galaxy(data["fileToSend"]) except Exception as e: traceback.print_exc(file=sys.stdout) return jsonify({ diff --git a/askomics/api/sparql.py b/askomics/api/sparql.py index bda8c3ad..c9ace7ce 100644 --- a/askomics/api/sparql.py +++ b/askomics/api/sparql.py @@ -1,6 +1,6 @@ import traceback import sys -from askomics.api.auth import login_required +from askomics.api.auth import api_auth, login_required from askomics.libaskomics.FilesUtils import FilesUtils from askomics.libaskomics.Result import Result from askomics.libaskomics.SparqlQuery import SparqlQuery @@ -17,6 +17,7 @@ def can_access(user): @sparql_bp.route("/api/sparql/init", methods=["GET"]) +@api_auth @login_required def init(): """Get the default sparql query @@ -63,6 +64,7 @@ def init(): @sparql_bp.route('/api/sparql/previewquery', methods=['POST']) +@api_auth @login_required def query(): """Perform a sparql query @@ -76,9 +78,18 @@ def query(): if not can_access(session['user']): return jsonify({"error": True, "errorMessage": "Admin required"}), 401 - q = request.get_json()['query'] - graphs = request.get_json()['graphs'] - endpoints = request.get_json()['endpoints'] + data = request.get_json() + if not (data and data.get("query")): + return jsonify({ + 'error': True, + 'errorMessage': "Missing query parameter", + 'header': [], + 'data': [] + }), 400 + + q = data['query'] + graphs = data.get('graphs', []) + endpoints = data.get('endpoints', []) local_endpoint_f = current_app.iniconfig.get('triplestore', 'endpoint') try: @@ -93,7 +104,7 @@ def query(): 'errorMessage': "No graph selected in local triplestore", 'header': [], 'data': [] - }), 500 + }), 400 # No endpoint selected if not endpoints: @@ -102,7 +113,7 @@ def query(): 'errorMessage': "No endpoint selected", 'header': [], 'data': [] - }), 500 + }), 400 try: query = SparqlQuery(current_app, session) @@ -137,6 +148,7 @@ def query(): @sparql_bp.route('/api/sparql/savequery', methods=["POST"]) +@api_auth @login_required def save_query(): """Perform a sparql query @@ -150,9 +162,18 @@ def save_query(): if not can_access(session['user']): return jsonify({"error": True, "errorMessage": "Admin required"}), 401 - q = request.get_json()['query'] - graphs = request.get_json()['graphs'] - endpoints = request.get_json()['endpoints'] + data = request.get_json() + if not (data and data.get("query")): + return jsonify({ + 'error': True, + 'errorMessage': "Missing query parameter", + 'header': [], + 'data': [] + }), 400 + + q = data['query'] + graphs = data.get('graphs', []) + endpoints = data.get('endpoints', []) local_endpoint_f = current_app.iniconfig.get('triplestore', 'endpoint') try: @@ -166,7 +187,7 @@ def save_query(): 'error': True, 'errorMessage': "No graph selected in local triplestore", 'task_id': None - }), 500 + }), 400 # No endpoint selected if not endpoints: @@ -174,7 +195,7 @@ def save_query(): 'error': True, 'errorMessage': "No endpoint selected", 'task_id': None - }), 500 + }), 400 try: files_utils = FilesUtils(current_app, session) @@ -185,7 +206,7 @@ def save_query(): 'error': True, 'errorMessage': "Exceeded quota", 'task_id': None - }), 500 + }), 400 # Is query federated? query = SparqlQuery(current_app, session) diff --git a/askomics/api/start.py b/askomics/api/start.py index 37bb226f..eca8f91b 100644 --- a/askomics/api/start.py +++ b/askomics/api/start.py @@ -2,6 +2,7 @@ import sys import traceback +from askomics.api.auth import api_auth from askomics.libaskomics.LocalAuth import LocalAuth from askomics.libaskomics.Start import Start @@ -14,6 +15,7 @@ @start_bp.route('/api/start', methods=['GET']) +@api_auth def start(): """Starting route diff --git a/askomics/libaskomics/BedFile.py b/askomics/libaskomics/BedFile.py index cb45c8de..7e55a7be 100644 --- a/askomics/libaskomics/BedFile.py +++ b/askomics/libaskomics/BedFile.py @@ -66,7 +66,7 @@ def get_preview(self): "entity_name": self.entity_name } - def integrate(self, dataset_id, entity_name, public=True): + def integrate(self, dataset_id, entity_name="", public=True): """Integrate BED file Parameters @@ -77,7 +77,10 @@ def integrate(self, dataset_id, entity_name, public=True): Insert in public dataset """ self.public = public - self.entity_name = entity_name + if entity_name: + self.entity_name = entity_name + else: + self.entity_name = self.human_name File.integrate(self, dataset_id=dataset_id) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 6b26ae78..0431727d 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -329,7 +329,7 @@ def dialect(self): dialect = csv.Sniffer().sniff(contents, delimiters=';,\t ') return dialect - def integrate(self, dataset_id, forced_columns_type, forced_header_names=None, public=False): + def integrate(self, dataset_id, forced_columns_type=None, forced_header_names=None, public=False): """Integrate the file Parameters @@ -341,7 +341,10 @@ def integrate(self, dataset_id, forced_columns_type, forced_header_names=None, p """ self.public = public self.set_preview_and_header() - self.force_columns_type(forced_columns_type) + if forced_columns_type: + self.force_columns_type(forced_columns_type) + else: + self.set_columns_type() if forced_header_names: self.force_header_names(forced_header_names) File.integrate(self, dataset_id=dataset_id) diff --git a/askomics/libaskomics/FilesHandler.py b/askomics/libaskomics/FilesHandler.py index 3bef2a85..54bc495c 100644 --- a/askomics/libaskomics/FilesHandler.py +++ b/askomics/libaskomics/FilesHandler.py @@ -68,7 +68,7 @@ def handle_files(self, files_id): 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)) - def get_files_infos(self, files_id=None, return_path=False): + def get_files_infos(self, files_id=None, files_path=None, return_path=False): """Get files info Parameters @@ -97,6 +97,18 @@ def get_files_infos(self, files_id=None, return_path=False): rows = database.execute_sql_query(query, (self.session['user']['id'], ) + tuple(files_id)) + elif files_path: + subquery_str = '(' + ' OR '.join(['path = ?'] * len(files_path)) + ')' + + query = ''' + SELECT id, name, type, size, path, date + FROM files + WHERE user_id = ? + AND {} + '''.format(subquery_str) + + rows = database.execute_sql_query(query, (self.session['user']['id'], ) + tuple(files_path)) + else: query = ''' @@ -159,9 +171,16 @@ def get_file_name(self): string file name """ - return Utils.get_random_string(10) + name = Utils.get_random_string(10) + file_path = "{}/{}".format(self.upload_path, name) + # Make sure it is not in use already + while os.path.isfile(file_path): + name = Utils.get_random_string(10) + file_path = "{}/{}".format(self.upload_path, name) - def write_data_into_file(self, data, file_name, mode): + return name + + def write_data_into_file(self, data, file_name, mode, should_exist=False): """Write data into a file Parameters @@ -174,6 +193,13 @@ def write_data_into_file(self, data, file_name, mode): open mode (w or a) """ file_path = "{}/{}".format(self.upload_path, file_name) + if mode == "a": + if not os.path.isfile(file_path): + raise Exception("No file exists at this path") + # Check this path does not already exists in database (meaning, already uploaded) + if len(self.get_files_infos(files_path=[file_path])) > 0: + raise Exception("A file with this path already exists in database") + with open(file_path, mode) as file: file.write(data) @@ -263,7 +289,10 @@ def persist_chunk(self, chunk_info): except Exception as e: # Rollback try: - self.delete_file_from_fs("{}/{}".format(self.upload_path, file_name)) + file_path = "{}/{}".format(self.upload_path, file_name) + # Delete if it does not exists in DB + if len(self.get_files_infos(files_path=[file_path])) == 0: + self.delete_file_from_fs(file_path) except Exception: pass raise(e) @@ -334,7 +363,8 @@ def delete_files(self, files_id, admin=False): """ for fid in files_id: file_path = self.get_file_path(fid) - self.delete_file_from_fs(file_path) + if os.path.isfile(file_path): + self.delete_file_from_fs(file_path) self.delete_file_from_db(fid, admin=admin) if admin and self.session['user']['admin']: diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index 96999a2a..70804fc7 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -79,7 +79,7 @@ def get_preview(self): } } - def integrate(self, dataset_id, entities, public=True): + def integrate(self, dataset_id, entities=[], public=True): """Integrate GFF file Parameters @@ -90,7 +90,11 @@ def integrate(self, dataset_id, entities, public=True): Insert in public dataset """ self.public = public - self.entities_to_integrate = entities + if entities: + self.entities_to_integrate = entities + else: + self.set_preview() + self.entities_to_integrate = self.entities File.integrate(self, dataset_id=dataset_id) diff --git a/askomics/libaskomics/LocalAuth.py b/askomics/libaskomics/LocalAuth.py index cc15c7ba..4759cedb 100644 --- a/askomics/libaskomics/LocalAuth.py +++ b/askomics/libaskomics/LocalAuth.py @@ -46,36 +46,36 @@ def check_inputs(self, inputs, admin_add=False): User inputs """ - if not inputs['fname']: + if not inputs.get('fname'): self.error = True self.error_message.append('First name empty') - if not inputs['lname']: + if not inputs.get('lname'): self.error = True self.error_message.append('Last name empty') - if not inputs['username']: + if not inputs.get('username'): self.error = True self.error_message.append('Username name empty') - if not validate_email(inputs['email']): + if not validate_email(inputs.get('email')): self.error = True self.error_message.append('Not a valid email') if not admin_add: - if not inputs['password']: + if not inputs.get('password'): self.error = True self.error_message.append('Password empty') - if inputs['password'] != inputs['passwordconf']: + if inputs.get('password') != inputs.get('passwordconf'): self.error = True self.error_message.append("Passwords doesn't match") - if self.is_username_in_db(inputs['username']): + if self.is_username_in_db(inputs.get('username')): self.error = True self.error_message.append('Username already registered') - if self.is_email_in_db(inputs['email']): + if self.is_email_in_db(inputs.get('email')): self.error = True self.error_message.append('Email already registered') diff --git a/askomics/tasks.py b/askomics/tasks.py index 225ef8e5..e0059151 100644 --- a/askomics/tasks.py +++ b/askomics/tasks.py @@ -47,7 +47,7 @@ def integrate(self, session, data, host_url): files_handler = FilesHandler(app, session, host_url=host_url, external_endpoint=data["externalEndpoint"], custom_uri=data["customUri"]) files_handler.handle_files([data["fileId"], ]) - public = data["public"] if session["user"]["admin"] else False + public = data.get("public", False) if session["user"]["admin"] else False for file in files_handler.files: @@ -66,13 +66,13 @@ def integrate(self, session, data, host_url): dataset.update_in_db("started", update_date=True, update_graph=True) if file.type == "csv/tsv": - file.integrate(data["dataset_id"], data['columns_type'], data['header_names'], public=public) + file.integrate(data["dataset_id"], data.get('columns_type'), data.get('header_names'), public=public) elif file.type == "gff/gff3": - file.integrate(data["dataset_id"], data["entities"], public=public) + file.integrate(data["dataset_id"], data.get("entities"), public=public) elif file.type in ('rdf/ttl', 'rdf/xml', 'rdf/nt'): file.integrate(public=public) elif file.type == "bed": - file.integrate(data["dataset_id"], data["entity_name"], public=public) + file.integrate(data["dataset_id"], data.get("entity_name"), public=public) # done dataset.update_in_db("success", ntriples=file.ntriples) except Exception as e: diff --git a/tests/test_api_auth.py b/tests/test_api_auth.py index 4511cd7b..e952dcfd 100644 --- a/tests/test_api_auth.py +++ b/tests/test_api_auth.py @@ -134,7 +134,7 @@ def test_signup(self, client): # Account creation disabled in config file client.set_config("askomics", "disable_account_creation", "true") response = client.client.post("/api/auth/signup", json=ok_data) - assert response.status_code == 500 + assert response.status_code == 400 assert response.json == { "error": True, "errorMessage": "Account creation is disabled", @@ -1074,3 +1074,25 @@ def test_local_required(self, client): "error": True, "errorMessage": "Local user required" } + + def test_api_auth(self, client): + """test api_auth decorator""" + client.create_two_users() + response = client.client.get("/api/start") + + assert response.status_code == 200 + assert not response.json['config'].get("logged") + + # Log jdoe + response = client.client.get("/api/start", headers={'X-API-KEY': '0000000001'}) + + assert response.status_code == 200 + assert response.json['config'].get("logged") + assert response.json['config']['user']['username'] == 'jdoe' + + # Log jsmith + response = client.client.get("/api/start", headers={'X-API-KEY': '0000000002'}) + + assert response.status_code == 200 + assert response.json['config'].get("logged") + assert response.json['config']['user']['username'] == 'jsmith' diff --git a/tests/test_api_query.py b/tests/test_api_query.py index b31ddbd4..1d54ed42 100644 --- a/tests/test_api_query.py +++ b/tests/test_api_query.py @@ -247,7 +247,7 @@ def test_save_result(self, client): print(client.session) response = client.client.post('/api/query/save_result', json=data) - assert response.status_code == 500 + assert response.status_code == 400 assert response.json == { "error": True, "errorMessage": "Exceeded quota", diff --git a/tests/test_api_sparql.py b/tests/test_api_sparql.py index 242aa0b9..04226910 100644 --- a/tests/test_api_sparql.py +++ b/tests/test_api_sparql.py @@ -70,7 +70,7 @@ def test_preview(self, client): 'header': [], 'data': [] } - assert response.status_code == 500 + assert response.status_code == 400 assert self.equal_objects(response.json, expected) response = client.client.post("/api/sparql/previewquery", json=no_graph_data) @@ -80,7 +80,7 @@ def test_preview(self, client): 'header': [], 'data': [] } - assert response.status_code == 500 + assert response.status_code == 400 assert self.equal_objects(response.json, expected) def test_query(self, client): @@ -120,7 +120,7 @@ def test_query(self, client): 'header': [], 'data': [] } - assert response.status_code == 500 + assert response.status_code == 400 assert self.equal_objects(response.json, expected) response = client.client.post("/api/sparql/previewquery", json=no_graph_data) @@ -130,5 +130,5 @@ def test_query(self, client): 'header': [], 'data': [] } - assert response.status_code == 500 + assert response.status_code == 400 assert self.equal_objects(response.json, expected) From a17ba30dcec7be0049234f1a2ab8df12011b4b4f Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 11 May 2021 15:19:20 +0200 Subject: [PATCH 041/318] Fix #209 (#210) --- askomics/api/file.py | 11 ++++++----- askomics/api/query.py | 10 +++++----- askomics/api/sparql.py | 10 +++++----- tests/test_api_file.py | 9 ++++----- tests/test_api_query.py | 2 +- tests/test_api_sparql.py | 2 +- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/askomics/api/file.py b/askomics/api/file.py index d7ddaf16..62e9587e 100644 --- a/askomics/api/file.py +++ b/askomics/api/file.py @@ -295,7 +295,7 @@ def integrate(): Returns ------- json - task_id: celery task id + datasets_id: dataset ids error: True if error, else False errorMessage: the error message of error, else an empty string """ @@ -304,11 +304,12 @@ def integrate(): return jsonify({ 'error': True, 'errorMessage': "Missing fileId parameter", - 'task_id': '' + 'dataset_ids': None }), 400 session_dict = {'user': session['user']} task = None + dataset_ids = [] try: @@ -331,7 +332,7 @@ def integrate(): dataset = Dataset(current_app, session, dataset_info) dataset.save_in_db() data["dataset_id"] = dataset.id - + dataset_ids.append(dataset.id) task = current_app.celery.send_task('integrate', (session_dict, data, request.host_url)) dataset.update_celery(task.id) @@ -341,13 +342,13 @@ def integrate(): return jsonify({ 'error': True, 'errorMessage': str(e), - 'task_id': '' + 'dataset_ids': None }), 500 return jsonify({ 'error': False, 'errorMessage': '', - 'task_id': task.id if task else '' + 'dataset_ids': dataset_ids }) diff --git a/askomics/api/query.py b/askomics/api/query.py index 369b8c57..1f784ff3 100644 --- a/askomics/api/query.py +++ b/askomics/api/query.py @@ -158,7 +158,7 @@ def save_result(): Returns ------- json - task_id: celery task id + result_id: result id error: True if error, else False errorMessage: the error message of error, else an empty string """ @@ -170,14 +170,14 @@ def save_result(): return jsonify({ 'error': True, 'errorMessage': "Exceeded quota", - 'task_id': None + 'result_id': None }), 400 # Get query and endpoints and graphs of the query data = request.get_json() if not (data and data.get("graphState")): return jsonify({ - 'task_id': None, + 'result_id': None, 'error': True, 'errorMessage': "Missing graphState parameter" }), 400 @@ -204,11 +204,11 @@ def save_result(): return jsonify({ 'error': True, 'errorMessage': str(e), - 'task_id': None + 'result_id': None }), 500 return jsonify({ 'error': False, 'errorMessage': '', - 'task_id': task.id + 'result_id': info["id"] }) diff --git a/askomics/api/sparql.py b/askomics/api/sparql.py index c9ace7ce..c501ab92 100644 --- a/askomics/api/sparql.py +++ b/askomics/api/sparql.py @@ -186,7 +186,7 @@ def save_query(): return jsonify({ 'error': True, 'errorMessage': "No graph selected in local triplestore", - 'task_id': None + 'result_id': None }), 400 # No endpoint selected @@ -194,7 +194,7 @@ def save_query(): return jsonify({ 'error': True, 'errorMessage': "No endpoint selected", - 'task_id': None + 'result_id': None }), 400 try: @@ -205,7 +205,7 @@ def save_query(): return jsonify({ 'error': True, 'errorMessage': "Exceeded quota", - 'task_id': None + 'result_id': None }), 400 # Is query federated? @@ -240,11 +240,11 @@ def save_query(): return jsonify({ 'error': True, 'errorMessage': str(e), - 'task_id': None + 'result_id': None }), 500 return jsonify({ 'error': False, 'errorMessage': '', - 'task_id': task.id + 'result_id': info["id"] }) diff --git a/tests/test_api_file.py b/tests/test_api_file.py index b5e422c0..e27468d8 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -515,15 +515,14 @@ def test_integrate(self, client): assert len(response.json) == 3 assert not response.json["error"] assert response.json["errorMessage"] == '' - assert len(response.json["task_id"]) == 0 + assert response.json["dataset_ids"] == [] response = client.client.post('/api/files/integrate', json=tsv_data) assert response.status_code == 200 - # print(response.json) assert len(response.json) == 3 assert not response.json["error"] assert response.json["errorMessage"] == '' - assert len(response.json["task_id"]) == 36 + assert response.json["dataset_ids"] response = client.client.post('/api/files/integrate', json=gff_data) assert response.status_code == 200 @@ -531,7 +530,7 @@ def test_integrate(self, client): assert len(response.json) == 3 assert not response.json["error"] assert response.json["errorMessage"] == '' - assert len(response.json["task_id"]) == 36 + assert response.json["dataset_ids"] response = client.client.post('/api/files/integrate', json=bed_data) assert response.status_code == 200 @@ -539,7 +538,7 @@ def test_integrate(self, client): assert len(response.json) == 3 assert not response.json["error"] assert response.json["errorMessage"] == '' - assert len(response.json["task_id"]) == 36 + assert response.json["dataset_ids"] def test_serve_file(self, client): """Test /api/files/ttl/// route""" diff --git a/tests/test_api_query.py b/tests/test_api_query.py index 1d54ed42..6ece8c4f 100644 --- a/tests/test_api_query.py +++ b/tests/test_api_query.py @@ -251,7 +251,7 @@ def test_save_result(self, client): assert response.json == { "error": True, "errorMessage": "Exceeded quota", - "task_id": None + "result_id": None } # remove quota diff --git a/tests/test_api_sparql.py b/tests/test_api_sparql.py index 04226910..ecb66647 100644 --- a/tests/test_api_sparql.py +++ b/tests/test_api_sparql.py @@ -110,7 +110,7 @@ def test_query(self, client): assert response.status_code == 200 assert not response.json["error"] assert response.json["errorMessage"] == '' - assert 'task_id' in response.json + assert 'result_id' in response.json # 500 response = client.client.post("/api/sparql/previewquery", json=no_endpoint_data) From 85ef88bcbe0c87670cac79460c27b6044ef9e4a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 May 2021 15:19:33 +0200 Subject: [PATCH 042/318] Bump hosted-git-info from 2.8.8 to 2.8.9 (#208) Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9. - [Release notes](https://github.com/npm/hosted-git-info/releases) - [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md) - [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index d8410db2..72a00c66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1840,7 +1840,7 @@ "dependencies": { "debug": { "version": "github:ngokevin/debug#ef5f8e66d49ce8bc64c6f282c15f8b7164409e3a", - "from": "github:ngokevin/debug#noTimestamp" + "from": "github:ngokevin/debug#ef5f8e66d49ce8bc64c6f282c15f8b7164409e3a" } } }, @@ -4100,6 +4100,11 @@ "index-array-by": "^1.3.0" } }, + "date-fns": { + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.21.3.tgz", + "integrity": "sha512-HeYdzCaFflc1i4tGbj7JKMjM4cKGYoyxwcIIkHzNgCkX8xXDNJDZXgDDVchIWpN4eQc3lH37WarduXFZJOtxfw==" + }, "debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", @@ -5893,9 +5898,9 @@ } }, "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "http-cache-semantics": { @@ -7459,7 +7464,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "dev": true, + "optional": true }, "normalize-url": { "version": "4.5.0", @@ -8160,7 +8166,8 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true + "dev": true, + "optional": true }, "pify": { "version": "2.3.0", @@ -8651,6 +8658,18 @@ "object-assign": "^4.1.0" } }, + "react-datepicker": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-3.8.0.tgz", + "integrity": "sha512-iFVNEp8DJoX5yEvEiciM7sJKmLGrvE70U38KhpG13XrulNSijeHw1RZkhd/0UmuXR71dcZB/kdfjiidifstZjw==", + "requires": { + "classnames": "^2.2.6", + "date-fns": "^2.0.1", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.10.0", + "react-popper": "^1.3.8" + } + }, "react-dom": { "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", @@ -8694,6 +8713,11 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-onclickoutside": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.10.0.tgz", + "integrity": "sha512-7i2L3ef+0ILXpL6P+Hg304eCQswh4jl3ynwR71BSlMU49PE2uk31k8B2GkP6yE9s2D4jTGKnzuSpzWxu4YxfQQ==" + }, "react-popper": { "version": "1.3.11", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.11.tgz", From 5f92788551f19d6697b02015ea7c8f05dd6ccb58 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 18 May 2021 17:30:47 +0200 Subject: [PATCH 043/318] Fix #212 (#213) Update Pipfile --- Pipfile | 2 +- Pipfile.lock | 438 +++++++++++++++++++++++++-------------------------- 2 files changed, 213 insertions(+), 227 deletions(-) diff --git a/Pipfile b/Pipfile index 688fa871..0dde7cdf 100644 --- a/Pipfile +++ b/Pipfile @@ -5,7 +5,7 @@ name = "pypi" [packages] werkzeug = "==0.16.1" -flask = "*" +flask = "==1.1.4" flask-reverse-proxy-fix = "*" validate-email = "*" gunicorn = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 2c7340d7..6c8940c3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "422e45022cea5b02567bffbd97735beb8634771f2254d8a9106c7443c89eca67" + "sha256": "f208e36a35ade8b9d94b025ec7085c79f6b57dbd69ae9dd2b08c8b06418242fe" }, "pipfile-spec": 6, "requires": {}, @@ -16,10 +16,10 @@ "default": { "amqp": { "hashes": [ - "sha256:1e759a7f202d910939de6eca45c23a107f6b71111f41d1282c648e9ac3d21901", - "sha256:affdd263d8b8eb3c98170b78bf83867cdb6a14901d586e00ddb65bfe2f0c4e60" + "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2", + "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb" ], - "version": "==5.0.5" + "version": "==5.0.6" }, "argh": { "hashes": [ @@ -38,10 +38,10 @@ }, "billiard": { "hashes": [ - "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede", - "sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a" + "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547", + "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b" ], - "version": "==3.6.3.0" + "version": "==3.6.4.0" }, "bioblend": { "hashes": [ @@ -58,17 +58,26 @@ "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:f5021a398c898b9cf6815cc5171c146a601b935b55364c53e6516a2545ab740c", + "sha256:fe2bcf85d0f5f1888ed7d86c139e9d4e7d54e036c8ac54e929663d63548046a1" ], "index": "pypi", "version": "==1.78" @@ -145,19 +154,19 @@ }, "deepdiff": { "hashes": [ - "sha256:3d3da4bd7e01fb5202088658ed26427104c748dda56a0ecfac9ce9a1d2d00844", - "sha256:ae2cb98353309f93fbfdda4d77adb08fb303314d836bb6eac3d02ed71a10b40e" + "sha256:dd79b81c2d84bfa33aa9d94d456b037b68daff6bb87b80dfaa1eca04da68b349", + "sha256:e054fed9dfe0d83d622921cbb3a3d0b3a6dd76acd2b6955433a0a2d35147774a" ], "index": "pypi", - "version": "==5.2.3" + "version": "==5.5.0" }, "flask": { "hashes": [ - "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", - "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" + "sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196", + "sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22" ], "index": "pypi", - "version": "==1.1.2" + "version": "==1.1.4" }, "flask-reverse-proxy-fix": { "hashes": [ @@ -176,14 +185,15 @@ }, "gitpython": { "hashes": [ - "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b", - "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61" + "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135", + "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e" ], "index": "pypi", - "version": "==3.1.14" + "version": "==3.1.17" }, "gunicorn": { "hashes": [ + "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" ], "index": "pypi", @@ -198,11 +208,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:1cedf994a9b6885dcbb7ed40b24c332b1de3956319f4b1a0f07c0621d453accc", - "sha256:c9c1b6c7dbc62084f3e6a614a194eb16ded7947736c18e3300125d5c0a7a8b3c" + "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", + "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" ], "markers": "python_version < '3.8'", - "version": "==3.9.1" + "version": "==4.0.1" }, "isodate": { "hashes": [ @@ -234,60 +244,42 @@ }, "markupsafe": { "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", - "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", - "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", - "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", - "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", - "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", - "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", - "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", - "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", - "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", - "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" - ], - "version": "==1.1.1" + "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" }, "numpy": { "hashes": [ @@ -382,6 +374,14 @@ "index": "pypi", "version": "==0.16.0.1" }, + "python-dateutil": { + "hashes": [ + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" + ], + "index": "pypi", + "version": "==2.8.1" + }, "python-ldap": { "hashes": [ "sha256:4711cacf013e298754abd70058ccc995758177fb425f1c2d30e71adfc1d00aa5" @@ -474,18 +474,18 @@ "flask" ], "hashes": [ - "sha256:71de00c9711926816f750bc0f57ef2abbcb1bfbdf5378c601df7ec978f44857a", - "sha256:9221e985f425913204989d0e0e1cbb719e8b7fa10540f1bc509f660c06a34e66" + "sha256:c1227d38dca315ba35182373f129c3e2722e8ed999e52584e6aca7d287870739", + "sha256:c7d380a21281e15be3d9f67a3c4fbb4f800c481d88ff8d8931f39486dd7b4ada" ], "index": "pypi", - "version": "==1.0.0" + "version": "==1.1.0" }, "six": { "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "version": "==1.15.0" + "version": "==1.16.0" }, "smmap": { "hashes": [ @@ -518,12 +518,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", - "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", - "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "markers": "python_version < '3.8'", - "version": "==3.7.4.3" + "version": "==3.10.0.0" }, "urllib3": { "hashes": [ @@ -548,26 +548,26 @@ }, "watchdog": { "hashes": [ - "sha256:035f4816daf3c62e03503c267620f3aa8fc7472df85ff3ef1e0c100ea1ed2744", - "sha256:0f7e9de9ba84af15e9e9fc29c3b13c972daa4d2b11de29aa86b26a26bc877c06", - "sha256:13c9ff58508dce55ba416eb0ef7af5aa5858558f2ec51112f099fd03503b670b", - "sha256:19675b8d1f00dabe74a0e66d87980623250d9360a21612e8c27b70a4b214ceeb", - "sha256:1cd715c4fb803581ded8943f39a51f21c17375d009ca9e3398d6b20638863a70", - "sha256:1f518a6940cde8720b8826a705c164e6b9bd6cf8c00f14269ffac51e017e06ec", - "sha256:3e933f3567c4521dd1a5d59fd54a522cae90bebcbeb8b74b84a2f33c90f08388", - "sha256:41b1a773f364f232b5bc184688e8d60451745d9e0971ac60c648bd47be8f4733", - "sha256:532fedd993e75554671faa36cd04c580ced3fae084254a779afbbd8aaf00566b", - "sha256:74528772516228f6a015a647027057939ff0b695a0b864cb3037e8e1aabc7ca0", - "sha256:89102465764e453609463cf620e744da1b0aa1f9f321b05961e2e7e15b3c9d8b", - "sha256:a412b1914e27f67b0a10e1ee19b5d035a9f7c115a062bbbd640653d9820ba4c8", - "sha256:ac6adbdf32e1d180574f9d0819e80259ae48e68727e80c3d950ed5a023714c3e", - "sha256:adda34bfe6db05485c1dfcd98232bdec385f991fe16358750c2163473eefb985", - "sha256:d2fcbc15772a82cd139c803a513c45b0fbc72a10a8a34dc2a8b429110b6f1236", - "sha256:d54e187b76053982180532cb7fd31152201c438b348c456f699929f8a89e786d", - "sha256:e0114e48ee981b38e328eaa0d5a625c7b4fc144b8dc7f7637749d6b5f7fefb0e" + "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" ], "index": "pypi", - "version": "==2.0.2" + "version": "==2.1.1" }, "wcwidth": { "hashes": [ @@ -595,10 +595,10 @@ "develop": { "attrs": { "hashes": [ - "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", - "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" ], - "version": "==20.3.0" + "version": "==21.2.0" }, "certifi": { "hashes": [ @@ -622,6 +622,9 @@ "version": "==7.1.2" }, "coverage": { + "extras": [ + "toml" + ], "hashes": [ "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", @@ -694,11 +697,11 @@ }, "flake8": { "hashes": [ - "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff", - "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0" + "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", + "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" ], "index": "pypi", - "version": "==3.9.0" + "version": "==3.9.2" }, "future": { "hashes": [ @@ -715,11 +718,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:1cedf994a9b6885dcbb7ed40b24c332b1de3956319f4b1a0f07c0621d453accc", - "sha256:c9c1b6c7dbc62084f3e6a614a194eb16ded7947736c18e3300125d5c0a7a8b3c" + "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", + "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" ], "markers": "python_version < '3.8'", - "version": "==3.9.1" + "version": "==4.0.1" }, "iniconfig": { "hashes": [ @@ -767,60 +770,42 @@ }, "markupsafe": { "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", - "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", - "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", - "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", - "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", - "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", - "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", - "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", - "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", - "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", - "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" - ], - "version": "==1.1.1" + "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" }, "mccabe": { "hashes": [ @@ -839,9 +824,10 @@ }, "nltk": { "hashes": [ - "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35" + "sha256:240e23ab1ab159ef9940777d30c7c72d7e76d91877099218a7585370c11f6b9e", + "sha256:57d556abed621ab9be225cc6d2df1edce17572efb67a3d754630c9f8381503eb" ], - "version": "==3.5" + "version": "==3.6.2" }, "packaging": { "hashes": [ @@ -887,19 +873,19 @@ }, "pytest": { "hashes": [ - "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9", - "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839" + "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", + "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" ], "index": "pypi", - "version": "==6.2.2" + "version": "==6.2.4" }, "pytest-cov": { "hashes": [ - "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7", - "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da" + "sha256:8535764137fecce504a49c2b742288e3d34bc09eed298ad65963616cc98fd45e", + "sha256:95d4933dcbbacfa377bb60b29801daa30d90c33981ab2a79e9ab4452c165066e" ], "index": "pypi", - "version": "==2.11.1" + "version": "==2.12.0" }, "pyyaml": { "hashes": [ @@ -937,49 +923,49 @@ }, "regex": { "hashes": [ - "sha256:07ef35301b4484bce843831e7039a84e19d8d33b3f8b2f9aab86c376813d0139", - "sha256:13f50969028e81765ed2a1c5fcfdc246c245cf8d47986d5172e82ab1a0c42ee5", - "sha256:14de88eda0976020528efc92d0a1f8830e2fb0de2ae6005a6fc4e062553031fa", - "sha256:159fac1a4731409c830d32913f13f68346d6b8e39650ed5d704a9ce2f9ef9cb3", - "sha256:18e25e0afe1cf0f62781a150c1454b2113785401ba285c745acf10c8ca8917df", - "sha256:201e2619a77b21a7780580ab7b5ce43835e242d3e20fef50f66a8df0542e437f", - "sha256:360a01b5fa2ad35b3113ae0c07fb544ad180603fa3b1f074f52d98c1096fa15e", - "sha256:39c44532d0e4f1639a89e52355b949573e1e2c5116106a395642cbbae0ff9bcd", - "sha256:3d9356add82cff75413bec360c1eca3e58db4a9f5dafa1f19650958a81e3249d", - "sha256:3d9a7e215e02bd7646a91fb8bcba30bc55fd42a719d6b35cf80e5bae31d9134e", - "sha256:4651f839dbde0816798e698626af6a2469eee6d9964824bb5386091255a1694f", - "sha256:486a5f8e11e1f5bbfcad87f7c7745eb14796642323e7e1829a331f87a713daaa", - "sha256:4b8a1fb724904139149a43e172850f35aa6ea97fb0545244dc0b805e0154ed68", - "sha256:4c0788010a93ace8a174d73e7c6c9d3e6e3b7ad99a453c8ee8c975ddd9965643", - "sha256:4c2e364491406b7888c2ad4428245fc56c327e34a5dfe58fd40df272b3c3dab3", - "sha256:575a832e09d237ae5fedb825a7a5bc6a116090dd57d6417d4f3b75121c73e3be", - "sha256:5770a51180d85ea468234bc7987f5597803a4c3d7463e7323322fe4a1b181578", - "sha256:633497504e2a485a70a3268d4fc403fe3063a50a50eed1039083e9471ad0101c", - "sha256:63f3ca8451e5ff7133ffbec9eda641aeab2001be1a01878990f6c87e3c44b9d5", - "sha256:709f65bb2fa9825f09892617d01246002097f8f9b6dde8d1bb4083cf554701ba", - "sha256:808404898e9a765e4058bf3d7607d0629000e0a14a6782ccbb089296b76fa8fe", - "sha256:882f53afe31ef0425b405a3f601c0009b44206ea7f55ee1c606aad3cc213a52c", - "sha256:8bd4f91f3fb1c9b1380d6894bd5b4a519409135bec14c0c80151e58394a4e88a", - "sha256:8e65e3e4c6feadf6770e2ad89ad3deb524bcb03d8dc679f381d0568c024e0deb", - "sha256:976a54d44fd043d958a69b18705a910a8376196c6b6ee5f2596ffc11bff4420d", - "sha256:a0d04128e005142260de3733591ddf476e4902c0c23c1af237d9acf3c96e1b38", - "sha256:a0df9a0ad2aad49ea3c7f65edd2ffb3d5c59589b85992a6006354f6fb109bb18", - "sha256:a2ee026f4156789df8644d23ef423e6194fad0bc53575534101bb1de5d67e8ce", - "sha256:a59a2ee329b3de764b21495d78c92ab00b4ea79acef0f7ae8c1067f773570afa", - "sha256:b97ec5d299c10d96617cc851b2e0f81ba5d9d6248413cd374ef7f3a8871ee4a6", - "sha256:b98bc9db003f1079caf07b610377ed1ac2e2c11acc2bea4892e28cc5b509d8d5", - "sha256:b9d8d286c53fe0cbc6d20bf3d583cabcd1499d89034524e3b94c93a5ab85ca90", - "sha256:bcd945175c29a672f13fce13a11893556cd440e37c1b643d6eeab1988c8b209c", - "sha256:c66221e947d7207457f8b6f42b12f613b09efa9669f65a587a2a71f6a0e4d106", - "sha256:c782da0e45aff131f0bed6e66fbcfa589ff2862fc719b83a88640daa01a5aff7", - "sha256:cb4ee827857a5ad9b8ae34d3c8cc51151cb4a3fe082c12ec20ec73e63cc7c6f0", - "sha256:d47d359545b0ccad29d572ecd52c9da945de7cd6cf9c0cfcb0269f76d3555689", - "sha256:dc9963aacb7da5177e40874585d7407c0f93fb9d7518ec58b86e562f633f36cd", - "sha256:ea2f41445852c660ba7c3ebf7d70b3779b20d9ca8ba54485a17740db49f46932", - "sha256:f5d0c921c99297354cecc5a416ee4280bd3f20fd81b9fb671ca6be71499c3fdf", - "sha256:f85d6f41e34f6a2d1607e312820971872944f1661a73d33e1e82d35ea3305e14" - ], - "version": "==2021.3.17" + "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" }, "requests": { "hashes": [ @@ -991,10 +977,10 @@ }, "six": { "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "version": "==1.15.0" + "version": "==1.16.0" }, "toml": { "hashes": [ @@ -1051,19 +1037,19 @@ }, "tqdm": { "hashes": [ - "sha256:9fdf349068d047d4cfbe24862c425883af1db29bcddf4b0eeb2524f6fbdb23c7", - "sha256:d666ae29164da3e517fcf125e41d4fe96e5bb375cd87ff9763f6b38b5592fe33" + "sha256:daec693491c52e9498632dfbe9ccfc4882a557f5fa08982db1b4d3adbe0887c3", + "sha256:ebdebdb95e3477ceea267decfc0784859aa3df3e27e22d23b83e9b272bf157ae" ], - "version": "==4.59.0" + "version": "==4.60.0" }, "typing-extensions": { "hashes": [ - "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", - "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", - "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], "markers": "python_version < '3.8'", - "version": "==3.7.4.3" + "version": "==3.10.0.0" }, "urllib3": { "hashes": [ From cb25a85904f55e3156657c75b40faf636bd79065 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 21 May 2021 14:48:20 +0200 Subject: [PATCH 044/318] Fix #215 (#216) Fix filters --- askomics/react/src/routes/query/query.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/askomics/react/src/routes/query/query.jsx b/askomics/react/src/routes/query/query.jsx index 0e41ec66..c1a45459 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/query.jsx @@ -464,8 +464,8 @@ export default class Query extends Component { let resFilterNode let resFilterLink - let reNode = new RegExp(node.filterNode, 'g') - let reLink = new RegExp(node.filterLink, 'g') + let reNode = new RegExp(node.filterNode.toLowerCase(), 'g') + let reLink = new RegExp(node.filterLink.toLowerCase(), 'g') let specialNodeGroupId = incrementSpecialNodeGroupId ? incrementSpecialNodeGroupId : node.specialNodeGroupId From 9ee536c7b38f21aba2d1e7080a0c20cddab92710 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jun 2021 01:08:56 +0000 Subject: [PATCH 045/318] Bump urllib3 from 1.26.4 to 1.26.5 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.4 to 1.26.5. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.4...1.26.5) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Pipfile.lock | 298 +++++++++++++++++++++------------------------------ 1 file changed, 121 insertions(+), 177 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 6c8940c3..1a6c45bb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -105,10 +105,10 @@ }, "certifi": { "hashes": [ - "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", - "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", + "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" ], - "version": "==2020.12.5" + "version": "==2021.5.30" }, "chardet": { "hashes": [ @@ -139,10 +139,10 @@ }, "click-repl": { "hashes": [ - "sha256:9c4c3d022789cae912aad8a3f5e1d7c2cdd016ee1225b5212ad3e8691563cda5", - "sha256:b9f29d52abc4d6059f8e276132a111ab8d94980afe6a5432b9d996544afa95d5" + "sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b", + "sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8" ], - "version": "==0.1.6" + "version": "==0.2.0" }, "configparser": { "hashes": [ @@ -206,14 +206,6 @@ ], "version": "==2.10" }, - "importlib-metadata": { - "hashes": [ - "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", - "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" - ], - "markers": "python_version < '3.8'", - "version": "==4.0.1" - }, "isodate": { "hashes": [ "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", @@ -237,88 +229,78 @@ }, "kombu": { "hashes": [ - "sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006", - "sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c" + "sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d", + "sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a" ], - "version": "==5.0.2" + "version": "==5.1.0" }, "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:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + ], + "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:1676b0a292dd3c99e49305a16d7a9f42a4ab60ec522eac0d3dd20cdf362ac010", + "sha256:16f221035e8bd19b9dc9a57159e38d2dd060b48e93e1d843c49cb370b0f415fd", + "sha256:43909c8bb289c382170e0282158a38cf306a8ad2ff6dfadc447e90f9961bef43", + "sha256:4e465afc3b96dbc80cf4a5273e5e2b1e3451286361b4af70ce1adb2984d392f9", + "sha256:55b745fca0a5ab738647d0e4db099bd0a23279c32b31a783ad2ccea729e632df", + "sha256:5d050e1e4bc9ddb8656d7b4f414557720ddcca23a5b88dd7cff65e847864c400", + "sha256:637d827248f447e63585ca3f4a7d2dfaa882e094df6cfa177cc9cf9cd6cdf6d2", + "sha256:6690080810f77485667bfbff4f69d717c3be25e5b11bb2073e76bb3f578d99b4", + "sha256:66fbc6fed94a13b9801fb70b96ff30605ab0a123e775a5e7a26938b717c5d71a", + "sha256:67d44acb72c31a97a3d5d33d103ab06d8ac20770e1c5ad81bdb3f0c086a56cf6", + "sha256:6ca2b85a5997dabc38301a22ee43c82adcb53ff660b89ee88dded6b33687e1d8", + "sha256:6e51534e78d14b4a009a062641f465cfaba4fdcb046c3ac0b1f61dd97c861b1b", + "sha256:70eb5808127284c4e5c9e836208e09d685a7978b6a216db85960b1a112eeace8", + "sha256:830b044f4e64a76ba71448fce6e604c0fc47a0e54d8f6467be23749ac2cbd2fb", + "sha256:8b7bb4b9280da3b2856cb1fc425932f46fba609819ee1c62256f61799e6a51d2", + "sha256:a9c65473ebc342715cb2d7926ff1e202c26376c0dcaaee85a1fd4b8d8c1d3b2f", + "sha256:c1c09247ccea742525bdb5f4b5ceeacb34f95731647fe55774aa36557dbb5fa4", + "sha256:c5bf0e132acf7557fc9bb8ded8b53bbbbea8892f3c9a1738205878ca9434206a", + "sha256:db250fd3e90117e0312b611574cd1b3f78bec046783195075cbd7ba9c3d73f16", + "sha256:e515c9a93aebe27166ec9593411c58494fa98e5fcc219e47260d9ab8a1cc7f9f", + "sha256:e55185e51b18d788e49fe8305fd73ef4470596b33fc2c1ceb304566b99c71a69", + "sha256:ea9cff01e75a956dbee133fa8e5b68f2f92175233de2f88de3a682dd94deda65", + "sha256:f1452578d0516283c87608a5a5548b0cdde15b99650efdfd85182102ef7a7c17", + "sha256:f39a995e47cb8649673cfa0579fbdd1cdd33ea497d1728a6cb194d6252268e48" + ], + "version": "==1.20.3" }, "ordered-set": { "hashes": [ @@ -516,21 +498,13 @@ "index": "pypi", "version": "==0.12.5" }, - "typing-extensions": { - "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" - ], - "markers": "python_version < '3.8'", - "version": "==3.10.0.0" - }, "urllib3": { "hashes": [ - "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", - "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" + "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", + "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" ], - "version": "==1.26.4" + "index": "pypi", + "version": "==1.26.5" }, "validate-email": { "hashes": [ @@ -583,13 +557,6 @@ ], "index": "pypi", "version": "==0.16.1" - }, - "zipp": { - "hashes": [ - "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", - "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" - ], - "version": "==3.4.1" } }, "develop": { @@ -602,10 +569,10 @@ }, "certifi": { "hashes": [ - "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", - "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", + "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" ], - "version": "==2020.12.5" + "version": "==2021.5.30" }, "chardet": { "hashes": [ @@ -716,14 +683,6 @@ ], "version": "==2.10" }, - "importlib-metadata": { - "hashes": [ - "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", - "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" - ], - "markers": "python_version < '3.8'", - "version": "==4.0.1" - }, "iniconfig": { "hashes": [ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", @@ -770,42 +729,42 @@ }, "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:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + ], + "version": "==2.0.1" }, "mccabe": { "hashes": [ @@ -1037,33 +996,18 @@ }, "tqdm": { "hashes": [ - "sha256:daec693491c52e9498632dfbe9ccfc4882a557f5fa08982db1b4d3adbe0887c3", - "sha256:ebdebdb95e3477ceea267decfc0784859aa3df3e27e22d23b83e9b272bf157ae" - ], - "version": "==4.60.0" - }, - "typing-extensions": { - "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:736524215c690621b06fc89d0310a49822d75e599fcd0feb7cc742b98d692493", + "sha256:cd5791b5d7c3f2f1819efc81d36eb719a38e0906a7380365c556779f585ea042" ], - "markers": "python_version < '3.8'", - "version": "==3.10.0.0" + "version": "==4.61.0" }, "urllib3": { "hashes": [ - "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", - "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" + "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", + "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" ], - "version": "==1.26.4" - }, - "zipp": { - "hashes": [ - "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", - "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" - ], - "version": "==3.4.1" + "index": "pypi", + "version": "==1.26.5" } } } From 573c2aeee03221c051891cda258d29fa27431072 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 7 Jun 2021 17:06:17 +0200 Subject: [PATCH 046/318] Implement #218 (URI in csv files) (#223) Implement #218 --- askomics/libaskomics/CsvFile.py | 5 +- askomics/libaskomics/File.py | 46 +++- test-data/linked_uris.csv | 4 + test-data/uris.csv | 5 + tests/data/linked_uri_query.json | 320 ++++++++++++++++++++++++++ tests/data/uri_query.json | 96 ++++++++ tests/results/results_linked_uri.json | 26 +++ tests/results/results_uri.json | 26 +++ tests/test_uri.py | 66 ++++++ 9 files changed, 590 insertions(+), 4 deletions(-) create mode 100644 test-data/linked_uris.csv create mode 100644 test-data/uris.csv create mode 100644 tests/data/linked_uri_query.json create mode 100644 tests/data/uri_query.json create mode 100644 tests/results/results_linked_uri.json create mode 100644 tests/results/results_uri.json create mode 100644 tests/test_uri.py diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 0431727d..299a2965 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -500,9 +500,10 @@ def generate_rdf_content(self): continue # Entity - entity = self.namespace_entity[self.format_uri(row[0])] + entity = self.rdfize(row[0], custom_namespace=self.namespace_entity) + 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(row[0]))) + self.graph_chunk.add((entity, rdflib.RDFS.label, rdflib.Literal(label))) # Faldo faldo_reference = None diff --git a/askomics/libaskomics/File.py b/askomics/libaskomics/File.py index ddec789f..24292fa8 100644 --- a/askomics/libaskomics/File.py +++ b/askomics/libaskomics/File.py @@ -6,6 +6,7 @@ from askomics.libaskomics.Params import Params from askomics.libaskomics.Database import Database +from askomics.libaskomics.PrefixManager import PrefixManager from askomics.libaskomics.SparqlQueryLauncher import SparqlQueryLauncher from askomics.libaskomics.Utils import Utils from askomics.libaskomics.RdfGraph import RdfGraph @@ -198,10 +199,10 @@ def format_uri(self, string, remove_space=False): return quote(string.replace(' ', '')) return quote(string) - def rdfize(self, string): + def rdfize(self, string, custom_namespace=None): """Rdfize a string - Return the literal is string is an url, else, + Return the literal if string is an url, else, prefix it with askomics prefix Parameters @@ -216,9 +217,50 @@ def rdfize(self, string): """ if Utils.is_valid_url(string): return rdflib.URIRef(string) + elif ":" in string and len(string.split(":")) == 2: + prefix, val = string.split(":") + if prefix: + prefix_manager = PrefixManager(self.app, self.session) + namespace = prefix_manager.get_namespace(prefix) + if namespace: + return rdflib.URIRef("{}{}".format(namespace, val)) + else: + # If not prefix, default to entity prefix + string = val + if custom_namespace: + return custom_namespace[self.format_uri(string)] else: return self.namespace_data[self.format_uri(string)] + def get_uri_label(self, string): + """Labelize a string + + Try to extract a label from an URI + Parameters + ---------- + uri : string + Term to extract label from + + Returns + ------- + String + Label + """ + + if Utils.is_valid_url(string): + string = string.rstrip("/") + if "/" in string: + end_term = string.split("/")[-1].rstrip("#") + if "#" in end_term: + end_term = end_term.split("#")[-1] + else: + end_term = string + elif ":" in string and len(string.split(":")) == 2: + end_term = string.split(":")[-1] + else: + end_term = string + return end_term + def set_metadata(self): """Get a rdflib graph of the metadata diff --git a/test-data/linked_uris.csv b/test-data/linked_uris.csv new file mode 100644 index 00000000..86766007 --- /dev/null +++ b/test-data/linked_uris.csv @@ -0,0 +1,4 @@ +linked_uri,link@test_uri +luri1,https://myuri.com/myuri +luri2,rdf:myuri2 +luri3,:myuri3 diff --git a/test-data/uris.csv b/test-data/uris.csv new file mode 100644 index 00000000..2d0d8ae5 --- /dev/null +++ b/test-data/uris.csv @@ -0,0 +1,5 @@ +test_uri,mydata +https://myuri.com/myuri,data1 +rdf:myuri2,data2 +:myuri3,data3 +wrongprefix:myuri4,data4 \ No newline at end of file diff --git a/tests/data/linked_uri_query.json b/tests/data/linked_uri_query.json new file mode 100644 index 00000000..bfbef4a6 --- /dev/null +++ b/tests/data/linked_uri_query.json @@ -0,0 +1,320 @@ +{ + "graphState": { + "attr": [ + { + "entityLabel": "linked_uri", + "entityUris": [ + "http://askomics.org/test/data/linked_uri" + ], + "faldo": false, + "filterType": "exact", + "filterValue": "", + "humanNodeId": 1, + "id": 2, + "label": "Uri", + "linked": false, + "linkedWith": null, + "negative": false, + "nodeId": 1, + "optional": false, + "type": "uri", + "uri": "rdf:type", + "visible": false + }, + { + "entityLabel": "linked_uri", + "entityUris": [ + "http://askomics.org/test/data/linked_uri" + ], + "faldo": false, + "filterType": "exact", + "filterValue": "", + "humanNodeId": 1, + "id": 3, + "label": "Label", + "linked": false, + "linkedWith": null, + "negative": false, + "nodeId": 1, + "optional": false, + "type": "text", + "uri": "rdfs:label", + "visible": true + }, + { + "entityLabel": "test_uri", + "entityUris": [ + "http://askomics.org/test/data/test_uri" + ], + "faldo": false, + "filterType": "exact", + "filterValue": "", + "humanNodeId": 1, + "id": 6, + "label": "Uri", + "linked": false, + "linkedWith": null, + "negative": false, + "nodeId": 4, + "optional": false, + "type": "uri", + "uri": "rdf:type", + "visible": true + }, + { + "entityLabel": "test_uri", + "entityUris": [ + "http://askomics.org/test/data/test_uri" + ], + "faldo": false, + "filterType": "exact", + "filterValue": "", + "humanNodeId": 1, + "id": 7, + "label": "Label", + "linked": false, + "linkedWith": null, + "negative": false, + "nodeId": 4, + "optional": false, + "type": "text", + "uri": "rdfs:label", + "visible": true + }, + { + "entityLabel": "test_uri", + "entityUris": [ + "http://askomics.org/test/data/test_uri" + ], + "faldo": null, + "filterType": "exact", + "filterValue": "", + "humanNodeId": 1, + "id": 8, + "label": "mydata", + "linked": false, + "linkedWith": null, + "negative": false, + "nodeId": 4, + "optional": false, + "type": "text", + "uri": "http://askomics.org/test/data/mydata", + "visible": false + } + ], + "links": [ + { + "__controlPoints": null, + "__indexColor": "#9c0005", + "directed": true, + "id": 9, + "index": 0, + "label": "link", + "sameRef": false, + "sameStrand": false, + "selected": false, + "source": { + "__indexColor": "#ec0001", + "faldo": false, + "filterLink": "", + "filterNode": "", + "graphs": [], + "humanId": 1, + "id": 1, + "index": 0, + "label": "linked_uri", + "selected": false, + "specialNodeGroupId": null, + "specialNodeId": null, + "specialPreviousIds": [ + null, + null + ], + "suggested": false, + "type": "node", + "uri": "http://askomics.org/test/data/linked_uri", + "vx": -0.0003003627462221435, + "vy": 0.0004075881106325289, + "x": 22.29802567993402, + "y": 16.856149975826618 + }, + "strict": true, + "suggested": false, + "target": { + "__indexColor": "#d80002", + "faldo": false, + "filterLink": "", + "filterNode": "", + "graphs": [ + "urn:sparql:askomics:1_jdoe:uris.csv_1623055495" + ], + "humanId": 1, + "id": 4, + "index": 1, + "label": "test_uri", + "selected": true, + "specialNodeGroupId": null, + "specialNodeId": null, + "specialPreviousIds": [ + null, + null + ], + "suggested": false, + "type": "node", + "uri": "http://askomics.org/test/data/test_uri", + "vx": -0.00023729076359030172, + "vy": 0.00021021021380291738, + "x": -8.557144404461111, + "y": 7.4840022124046435 + }, + "type": "link", + "uri": "http://askomics.org/test/data/link" + }, + { + "__controlPoints": null, + "__indexColor": "#880006", + "directed": true, + "id": 12, + "index": 1, + "label": "link", + "sameRef": false, + "sameStrand": false, + "selected": false, + "source": { + "__indexColor": "#b00004", + "faldo": false, + "filterLink": "", + "filterNode": "", + "graphs": [], + "humanId": null, + "id": 10, + "index": 2, + "label": "linked_uri", + "selected": false, + "specialNodeGroupId": null, + "specialNodeId": null, + "specialPreviousIds": [ + null, + null + ], + "suggested": true, + "type": "node", + "uri": "http://askomics.org/test/data/linked_uri", + "vx": -0.0004446376825207742, + "vy": 0.00023908510760271463, + "x": -13.741863566665243, + "y": -24.339295304799222 + }, + "suggested": true, + "target": { + "__indexColor": "#d80002", + "faldo": false, + "filterLink": "", + "filterNode": "", + "graphs": [ + "urn:sparql:askomics:1_jdoe:uris.csv_1623055495" + ], + "humanId": 1, + "id": 4, + "index": 1, + "label": "test_uri", + "selected": true, + "specialNodeGroupId": null, + "specialNodeId": null, + "specialPreviousIds": [ + null, + null + ], + "suggested": false, + "type": "node", + "uri": "http://askomics.org/test/data/test_uri", + "vx": -0.00023729076359030172, + "vy": 0.00021021021380291738, + "x": -8.557144404461111, + "y": 7.4840022124046435 + }, + "type": "link", + "uri": "http://askomics.org/test/data/link" + } + ], + "nodes": [ + { + "__indexColor": "#ec0001", + "faldo": false, + "filterLink": "", + "filterNode": "", + "graphs": [], + "humanId": 1, + "id": 1, + "index": 0, + "label": "linked_uri", + "selected": false, + "specialNodeGroupId": null, + "specialNodeId": null, + "specialPreviousIds": [ + null, + null + ], + "suggested": false, + "type": "node", + "uri": "http://askomics.org/test/data/linked_uri", + "vx": -0.0003003627462221435, + "vy": 0.0004075881106325289, + "x": 22.29802567993402, + "y": 16.856149975826618 + }, + { + "__indexColor": "#d80002", + "faldo": false, + "filterLink": "", + "filterNode": "", + "graphs": [ + "urn:sparql:askomics:1_jdoe:uris.csv_1623055495" + ], + "humanId": 1, + "id": 4, + "index": 1, + "label": "test_uri", + "selected": true, + "specialNodeGroupId": null, + "specialNodeId": null, + "specialPreviousIds": [ + null, + null + ], + "suggested": false, + "type": "node", + "uri": "http://askomics.org/test/data/test_uri", + "vx": -0.00023729076359030172, + "vy": 0.00021021021380291738, + "x": -8.557144404461111, + "y": 7.4840022124046435 + }, + { + "__indexColor": "#b00004", + "faldo": false, + "filterLink": "", + "filterNode": "", + "graphs": [], + "humanId": null, + "id": 10, + "index": 2, + "label": "linked_uri", + "selected": false, + "specialNodeGroupId": null, + "specialNodeId": null, + "specialPreviousIds": [ + null, + null + ], + "suggested": true, + "type": "node", + "uri": "http://askomics.org/test/data/linked_uri", + "vx": -0.0004446376825207742, + "vy": 0.00023908510760271463, + "x": -13.741863566665243, + "y": -24.339295304799222 + } + ] + } +} diff --git a/tests/data/uri_query.json b/tests/data/uri_query.json new file mode 100644 index 00000000..f22b8cd6 --- /dev/null +++ b/tests/data/uri_query.json @@ -0,0 +1,96 @@ +{ + "graphState": { + "attr": [ + { + "entityLabel": "test_uri", + "entityUris": [ + "http://askomics.org/test/data/test_uri" + ], + "faldo": false, + "filterType": "exact", + "filterValue": "", + "humanNodeId": 1, + "id": 2, + "label": "Uri", + "linked": false, + "linkedWith": null, + "negative": false, + "nodeId": 1, + "optional": false, + "type": "uri", + "uri": "rdf:type", + "visible": true + }, + { + "entityLabel": "test_uri", + "entityUris": [ + "http://askomics.org/test/data/test_uri" + ], + "faldo": false, + "filterType": "exact", + "filterValue": "", + "humanNodeId": 1, + "id": 3, + "label": "Label", + "linked": false, + "linkedWith": null, + "negative": false, + "nodeId": 1, + "optional": false, + "type": "text", + "uri": "rdfs:label", + "visible": true + }, + { + "entityLabel": "test_uri", + "entityUris": [ + "http://askomics.org/test/data/test_uri" + ], + "faldo": null, + "filterType": "exact", + "filterValue": "", + "humanNodeId": 1, + "id": 4, + "label": "mydata", + "linked": false, + "linkedWith": null, + "negative": false, + "nodeId": 1, + "optional": false, + "type": "text", + "uri": "http://askomics.org/test/data/mydata", + "visible": false + } + ], + "links": [], + "nodes": [ + { + "__indexColor": "#ec0001", + "faldo": false, + "filterLink": "", + "filterNode": "", + "graphs": [ + "urn:sparql:askomics:1_jdoe:uris.csv_1623050448" + ], + "humanId": 1, + "id": 1, + "index": 0, + "label": "test_uri", + "selected": true, + "specialNodeGroupId": null, + "specialNodeId": null, + "specialPreviousIds": [ + null, + null + ], + "suggested": false, + "type": "node", + "uri": "http://askomics.org/test/data/test_uri", + "vx": 0, + "vy": 0, + "x": 0, + "y": 0 + } + ] + } +} diff --git a/tests/results/results_linked_uri.json b/tests/results/results_linked_uri.json new file mode 100644 index 00000000..a4d340e7 --- /dev/null +++ b/tests/results/results_linked_uri.json @@ -0,0 +1,26 @@ +{ + "error": false, + "errorMessage": "", + "headerPreview": [ + "linked_uri1_Label", + "test_uri4_uri", + "test_uri1_Label" + ], + "resultsPreview": [ + { + "linked_uri1_Label": "luri1", + "test_uri1_Label": "myuri", + "test_uri4_uri": "https://myuri.com/myuri" + }, + { + "linked_uri1_Label": "luri2", + "test_uri1_Label": "myuri2", + "test_uri4_uri": "http://www.w3.org/1999/02/22-rdf-syntax-ns#myuri2" + }, + { + "linked_uri1_Label": "luri3", + "test_uri1_Label": "myuri3", + "test_uri4_uri": "http://askomics.org/test/data/myuri3" + } + ] +} diff --git a/tests/results/results_uri.json b/tests/results/results_uri.json new file mode 100644 index 00000000..5278222e --- /dev/null +++ b/tests/results/results_uri.json @@ -0,0 +1,26 @@ +{ + "error": false, + "errorMessage": "", + "headerPreview": [ + "test_uri1_uri", + "test_uri1_Label" + ], + "resultsPreview": [ + { + "test_uri1_Label": "myuri", + "test_uri1_uri": "https://myuri.com/myuri" + }, + { + "test_uri1_Label": "myuri2", + "test_uri1_uri": "http://www.w3.org/1999/02/22-rdf-syntax-ns#myuri2" + }, + { + "test_uri1_Label": "myuri3", + "test_uri1_uri": "http://askomics.org/test/data/myuri3" + }, + { + "test_uri1_Label": "myuri4", + "test_uri1_uri": "http://askomics.org/test/data/wrongprefix%3Amyuri4" + } + ] +} diff --git a/tests/test_uri.py b/tests/test_uri.py new file mode 100644 index 00000000..0e0ae90f --- /dev/null +++ b/tests/test_uri.py @@ -0,0 +1,66 @@ +import json + +from . import AskomicsTestCase + + +class TestURIResults(AskomicsTestCase): + """Test correct URI interpretation""" + + def test_uri(self, client): + """Test entity uri interpretation""" + client.create_two_users() + client.log_user("jdoe") + client.upload_file("test-data/uris.csv") + + client.integrate_file({ + "id": 1, + "columns_type": ["start_entity", "text"] + }) + + with open("tests/data/uri_query.json") as file: + file_content = file.read() + + json_query = json.loads(file_content) + + with open("tests/results/results_uri.json") as file: + file_content = file.read() + + expected = json.loads(file_content) + + response = client.client.post('/api/query/preview', json=json_query) + + assert response.status_code == 200 + assert self.equal_objects(response.json, expected) + + def test_linked_uri(self, client): + """Test linked uri interpretation""" + client.create_two_users() + client.log_user("jdoe") + client.upload_file("test-data/uris.csv") + client.upload_file("test-data/linked_uris.csv") + + client.integrate_file({ + "id": 1, + "columns_type": ["start_entity", "text"] + }) + + client.integrate_file({ + "id": 2, + "columns_type": ["start_entity", "general_relation"] + }) + + with open("tests/data/linked_uri_query.json") as file: + file_content = file.read() + + json_query = json.loads(file_content) + + with open("tests/results/results_linked_uri.json") as file: + file_content = file.read() + + expected = json.loads(file_content) + + response = client.client.post('/api/query/preview', json=json_query) + print(response.json) + + assert response.status_code == 200 + assert self.equal_objects(response.json, expected) From 077b3e0fdea1fda1fd326e58fa84558904e62356 Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Tue, 8 Jun 2021 18:03:47 +0200 Subject: [PATCH 047/318] Fix #225: edit config file in temp file rather than in final destination --- cli/set_config.sh | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/cli/set_config.sh b/cli/set_config.sh index db888de5..cf1729b0 100644 --- a/cli/set_config.sh +++ b/cli/set_config.sh @@ -3,10 +3,10 @@ config_template_path="config/askomics.ini.template" config_path="config/askomics.ini" -# Create config file -if [[ ! -f $config_path ]]; then - cp $config_template_path $config_path -fi +tmpfile=$(mktemp /tmp/askomics.ini.XXXXXX) + +# Init config file +cp $config_template_path $tmpfile # Convert env to ini entry printenv | egrep "ASKO_" | while read setting @@ -14,7 +14,9 @@ do section=$(echo $setting | egrep -o "^ASKO[^=]+" | sed 's/^.\{5\}//g' | cut -d "_" -f 1) key=$(echo $setting | egrep -o "^ASKO[^=]+" | sed 's/^.\{5\}//g' | sed "s/$section\_//g") value=$(echo $setting | egrep -o "=.*$" | sed 's/^=//g') - # crudini --set ${config_path} "${section}" "${key}" "${value}" - python3 cli/config_updater.py -p $config_path -s "${section}" -k "${key}" -v "${value}" - $cmd + # crudini --set ${tmpfile} "${section}" "${key}" "${value}" + python3 cli/config_updater.py -p $tmpfile -s "${section}" -k "${key}" -v "${value}" done + +# config ready, copy to dest +cp $tmpfile $config_path From 227489df89d86481d23c0ec03cc42885bf6f0923 Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Tue, 8 Jun 2021 18:07:31 +0200 Subject: [PATCH 048/318] more sleeping --- docker/start_all.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/start_all.sh b/docker/start_all.sh index 9f286996..eb126032 100755 --- a/docker/start_all.sh +++ b/docker/start_all.sh @@ -30,6 +30,9 @@ while [[ ! -f /askomics/config/askomics.ini ]]; do sleep 1s done +# Wait a bit more, you never know... +sleep 1s + # Start Celery nohup make serve-celery &> /var/log/celery.log & From 00d0c6c24490f3a231dfde82603c8778c923af1b Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Wed, 9 Jun 2021 10:35:27 +0200 Subject: [PATCH 049/318] Revert "Bump urllib3 from 1.26.4 to 1.26.5" This reverts commit 9ee536c7b38f21aba2d1e7080a0c20cddab92710. --- Pipfile.lock | 298 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 177 insertions(+), 121 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 1a6c45bb..6c8940c3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -105,10 +105,10 @@ }, "certifi": { "hashes": [ - "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", - "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2021.5.30" + "version": "==2020.12.5" }, "chardet": { "hashes": [ @@ -139,10 +139,10 @@ }, "click-repl": { "hashes": [ - "sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b", - "sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8" + "sha256:9c4c3d022789cae912aad8a3f5e1d7c2cdd016ee1225b5212ad3e8691563cda5", + "sha256:b9f29d52abc4d6059f8e276132a111ab8d94980afe6a5432b9d996544afa95d5" ], - "version": "==0.2.0" + "version": "==0.1.6" }, "configparser": { "hashes": [ @@ -206,6 +206,14 @@ ], "version": "==2.10" }, + "importlib-metadata": { + "hashes": [ + "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", + "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" + ], + "markers": "python_version < '3.8'", + "version": "==4.0.1" + }, "isodate": { "hashes": [ "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", @@ -229,78 +237,88 @@ }, "kombu": { "hashes": [ - "sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d", - "sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a" + "sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006", + "sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c" ], - "version": "==5.1.0" + "version": "==5.0.2" }, "markupsafe": { "hashes": [ - "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", - "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", - "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", - "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", - "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", - "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", - "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", - "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", - "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", - "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", - "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", - "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", - "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", - "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", - "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", - "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", - "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", - "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", - "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", - "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", - "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", - "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", - "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", - "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", - "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", - "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", - "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", - "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", - "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", - "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", - "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", - "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", - "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", - "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" - ], - "version": "==2.0.1" + "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" }, "numpy": { "hashes": [ - "sha256:1676b0a292dd3c99e49305a16d7a9f42a4ab60ec522eac0d3dd20cdf362ac010", - "sha256:16f221035e8bd19b9dc9a57159e38d2dd060b48e93e1d843c49cb370b0f415fd", - "sha256:43909c8bb289c382170e0282158a38cf306a8ad2ff6dfadc447e90f9961bef43", - "sha256:4e465afc3b96dbc80cf4a5273e5e2b1e3451286361b4af70ce1adb2984d392f9", - "sha256:55b745fca0a5ab738647d0e4db099bd0a23279c32b31a783ad2ccea729e632df", - "sha256:5d050e1e4bc9ddb8656d7b4f414557720ddcca23a5b88dd7cff65e847864c400", - "sha256:637d827248f447e63585ca3f4a7d2dfaa882e094df6cfa177cc9cf9cd6cdf6d2", - "sha256:6690080810f77485667bfbff4f69d717c3be25e5b11bb2073e76bb3f578d99b4", - "sha256:66fbc6fed94a13b9801fb70b96ff30605ab0a123e775a5e7a26938b717c5d71a", - "sha256:67d44acb72c31a97a3d5d33d103ab06d8ac20770e1c5ad81bdb3f0c086a56cf6", - "sha256:6ca2b85a5997dabc38301a22ee43c82adcb53ff660b89ee88dded6b33687e1d8", - "sha256:6e51534e78d14b4a009a062641f465cfaba4fdcb046c3ac0b1f61dd97c861b1b", - "sha256:70eb5808127284c4e5c9e836208e09d685a7978b6a216db85960b1a112eeace8", - "sha256:830b044f4e64a76ba71448fce6e604c0fc47a0e54d8f6467be23749ac2cbd2fb", - "sha256:8b7bb4b9280da3b2856cb1fc425932f46fba609819ee1c62256f61799e6a51d2", - "sha256:a9c65473ebc342715cb2d7926ff1e202c26376c0dcaaee85a1fd4b8d8c1d3b2f", - "sha256:c1c09247ccea742525bdb5f4b5ceeacb34f95731647fe55774aa36557dbb5fa4", - "sha256:c5bf0e132acf7557fc9bb8ded8b53bbbbea8892f3c9a1738205878ca9434206a", - "sha256:db250fd3e90117e0312b611574cd1b3f78bec046783195075cbd7ba9c3d73f16", - "sha256:e515c9a93aebe27166ec9593411c58494fa98e5fcc219e47260d9ab8a1cc7f9f", - "sha256:e55185e51b18d788e49fe8305fd73ef4470596b33fc2c1ceb304566b99c71a69", - "sha256:ea9cff01e75a956dbee133fa8e5b68f2f92175233de2f88de3a682dd94deda65", - "sha256:f1452578d0516283c87608a5a5548b0cdde15b99650efdfd85182102ef7a7c17", - "sha256:f39a995e47cb8649673cfa0579fbdd1cdd33ea497d1728a6cb194d6252268e48" - ], - "version": "==1.20.3" + "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" }, "ordered-set": { "hashes": [ @@ -498,13 +516,21 @@ "index": "pypi", "version": "==0.12.5" }, + "typing-extensions": { + "hashes": [ + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + ], + "markers": "python_version < '3.8'", + "version": "==3.10.0.0" + }, "urllib3": { "hashes": [ - "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", - "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" + "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", + "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" ], - "index": "pypi", - "version": "==1.26.5" + "version": "==1.26.4" }, "validate-email": { "hashes": [ @@ -557,6 +583,13 @@ ], "index": "pypi", "version": "==0.16.1" + }, + "zipp": { + "hashes": [ + "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", + "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + ], + "version": "==3.4.1" } }, "develop": { @@ -569,10 +602,10 @@ }, "certifi": { "hashes": [ - "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", - "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2021.5.30" + "version": "==2020.12.5" }, "chardet": { "hashes": [ @@ -683,6 +716,14 @@ ], "version": "==2.10" }, + "importlib-metadata": { + "hashes": [ + "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", + "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" + ], + "markers": "python_version < '3.8'", + "version": "==4.0.1" + }, "iniconfig": { "hashes": [ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", @@ -729,42 +770,42 @@ }, "markupsafe": { "hashes": [ - "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", - "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", - "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", - "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", - "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", - "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", - "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", - "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", - "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", - "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", - "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", - "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", - "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", - "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", - "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", - "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", - "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", - "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", - "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", - "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", - "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", - "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", - "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", - "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", - "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", - "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", - "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", - "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", - "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", - "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", - "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", - "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", - "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", - "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" - ], - "version": "==2.0.1" + "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" }, "mccabe": { "hashes": [ @@ -996,18 +1037,33 @@ }, "tqdm": { "hashes": [ - "sha256:736524215c690621b06fc89d0310a49822d75e599fcd0feb7cc742b98d692493", - "sha256:cd5791b5d7c3f2f1819efc81d36eb719a38e0906a7380365c556779f585ea042" + "sha256:daec693491c52e9498632dfbe9ccfc4882a557f5fa08982db1b4d3adbe0887c3", + "sha256:ebdebdb95e3477ceea267decfc0784859aa3df3e27e22d23b83e9b272bf157ae" + ], + "version": "==4.60.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], - "version": "==4.61.0" + "markers": "python_version < '3.8'", + "version": "==3.10.0.0" }, "urllib3": { "hashes": [ - "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", - "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" + "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", + "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" ], - "index": "pypi", - "version": "==1.26.5" + "version": "==1.26.4" + }, + "zipp": { + "hashes": [ + "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", + "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + ], + "version": "==3.4.1" } } } From 50085a8ac8c5723cdea03da8783e79da6cc95da5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jun 2021 08:50:32 +0000 Subject: [PATCH 050/318] Bump normalize-url from 4.5.0 to 4.5.1 Bumps [normalize-url](https://github.com/sindresorhus/normalize-url) from 4.5.0 to 4.5.1. - [Release notes](https://github.com/sindresorhus/normalize-url/releases) - [Commits](https://github.com/sindresorhus/normalize-url/commits) --- updated-dependencies: - dependency-name: normalize-url dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 72a00c66..f714c066 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7468,9 +7468,9 @@ "optional": true }, "normalize-url": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", - "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", "dev": true }, "nosleep.js": { From bdcde65b5a5367a52ae2110fd83be4b6e86d3b23 Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Wed, 9 Jun 2021 10:55:59 +0200 Subject: [PATCH 051/318] add 4.2.2 changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25d4cc11..d7330494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This changelog was started for release 4.2.0. +## [4.2.2] - 2021-06-09 + +### Fixed + +- Fixed startup issue: race condition on config file creation + ## [4.2.1] - 2021-03-29 ### Fixed From 799f4083c566c4afc4f64ebfaa5d4aca5e8237f1 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 9 Jun 2021 14:09:11 +0200 Subject: [PATCH 052/318] 4.2.x (#228) 4.2.2 release --- package-lock.json | 2 +- package.json | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 72a00c66..5b16cf66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "AskOmics", - "version": "4.2.1", + "version": "4.2.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 38d50685..163e8419 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "ontology" ], "name": "AskOmics", - "version": "4.2.1", + "version": "4.2.2", "description": "Visual SPARQL query builder", "author": "Xavier Garnier", "license": "AGPL-3.0", diff --git a/setup.py b/setup.py index 688e603b..8d793f05 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='askomics', - version='4.2.1', + version='4.2.2', description=''' AskOmics is a visual SPARQL query interface supporting both intuitive data integration and querying while shielding the user from most of the From 5ddf8132f861190dc0c803df072b6063ef46ba13 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 10 Jun 2021 15:44:55 +0200 Subject: [PATCH 053/318] Add 'forms' (simple templates) (#224) Add 'forms' --- askomics/api/query.py | 4 + askomics/api/results.py | 162 ++- askomics/libaskomics/Database.py | 24 + askomics/libaskomics/Result.py | 82 +- askomics/libaskomics/ResultsHandler.py | 49 +- askomics/libaskomics/SparqlQuery.py | 4 +- askomics/react/src/routes.jsx | 4 + askomics/react/src/routes/ask/ask.jsx | 67 +- askomics/react/src/routes/form/attribute.jsx | 486 +++++++++ askomics/react/src/routes/form/entity.jsx | 35 + askomics/react/src/routes/form/query.jsx | 530 ++++++++++ .../react/src/routes/form_edit/attribute.jsx | 489 +++++++++ .../react/src/routes/form_edit/entity.jsx | 41 + askomics/react/src/routes/form_edit/query.jsx | 477 +++++++++ askomics/react/src/routes/query/attribute.jsx | 53 +- askomics/react/src/routes/query/query.jsx | 17 +- .../src/routes/results/resultsfilestable.jsx | 94 +- tests/conftest.py | 11 +- tests/data/graphState_simple_query_form.json | 940 +++++++++++++++++ ...graphState_simple_query_form_modified.json | 942 ++++++++++++++++++ tests/data/startpoints.json | 3 +- tests/results/results.json | 2 + tests/results/results_form.json | 26 + tests/results/startpoints.json | 3 +- tests/test_api_query.py | 2 + tests/test_api_results.py | 223 +++++ 26 files changed, 4740 insertions(+), 30 deletions(-) create mode 100644 askomics/react/src/routes/form/attribute.jsx create mode 100644 askomics/react/src/routes/form/entity.jsx create mode 100644 askomics/react/src/routes/form/query.jsx create mode 100644 askomics/react/src/routes/form_edit/attribute.jsx create mode 100644 askomics/react/src/routes/form_edit/entity.jsx create mode 100644 askomics/react/src/routes/form_edit/query.jsx create mode 100644 tests/data/graphState_simple_query_form.json create mode 100644 tests/data/graphState_simple_query_form_modified.json create mode 100644 tests/results/results_form.json diff --git a/askomics/api/query.py b/askomics/api/query.py index 1f784ff3..f5bfc8af 100644 --- a/askomics/api/query.py +++ b/askomics/api/query.py @@ -32,17 +32,20 @@ def query(): if "user" not in session and current_app.iniconfig.getboolean("askomics", "protect_public"): startpoints = [] public_queries = [] + public_form_queries = [] else: tse = TriplestoreExplorer(current_app, session) results_handler = ResultsHandler(current_app, session) startpoints = tse.get_startpoints() public_queries = results_handler.get_public_queries() + public_form_queries = results_handler.get_public_form_queries() except Exception as e: traceback.print_exc(file=sys.stdout) return jsonify({ 'startpoints': [], "publicQueries": [], + "publicFormQueries": [], 'error': True, 'errorMessage': str(e) }), 500 @@ -50,6 +53,7 @@ def query(): return jsonify({ 'startpoints': startpoints, "publicQueries": public_queries, + "publicFormQueries": public_form_queries, 'error': False, 'errorMessage': '' }) diff --git a/askomics/api/results.py b/askomics/api/results.py index d43f80ce..854aeb08 100644 --- a/askomics/api/results.py +++ b/askomics/api/results.py @@ -84,6 +84,15 @@ def get_preview(): file_id = data["fileId"] result_info = {"id": file_id} result = Result(current_app, session, result_info) + if not result: + return jsonify({ + 'preview': [], + 'header': [], + 'id': file_id, + 'error': True, + 'errorMessage': "You do not have access to this query" + }), 401 + headers, preview = result.get_file_preview() except Exception as e: @@ -132,6 +141,16 @@ def get_graph_and_sparql_query(): file_id = data["fileId"] result_info = {"id": file_id} result = Result(current_app, session, result_info) + if not result: + return jsonify({ + 'graphState': {}, + 'sparqlQuery': "", + 'graphs': [], + 'endpoints': [], + 'diskSpace': 0, + 'error': True, + 'errorMessage': "You do not have access to this query" + }), 401 # Get graph state and sparql query graph_state = result.get_graph_state(formated=True) @@ -195,9 +214,18 @@ def get_graph_state(): }), 400 file_id = data["fileId"] + formated = data.get("formated", True) + result_info = {"id": file_id} result = Result(current_app, session, result_info) - graph_state = result.get_graph_state(formated=True) + if not result: + return jsonify({ + 'graphState': {}, + 'id': file_id, + 'error': True, + 'errorMessage': "You do not have access to this graph" + }), 401 + graph_state = result.get_graph_state(formated=formated) except Exception as e: traceback.print_exc(file=sys.stdout) @@ -232,6 +260,11 @@ def download_result(): file_id = data["fileId"] result_info = {"id": file_id} result = Result(current_app, session, result_info) + if not result: + return jsonify({ + 'error': True, + 'errorMessage': "You do not have access to this result" + }), 401 dir_path = result.get_dir_path() file_name = result.get_file_name() @@ -317,6 +350,15 @@ def get_sparql_query(): result_info = {"id": file_id} result = Result(current_app, session, result_info) + if not result: + return jsonify({ + 'query': {}, + 'graphs': [], + 'endpoints': [], + 'diskSpace': 0, + 'error': True, + 'errorMessage': "You do not have access to this result" + }), 401 query = SparqlQuery(current_app, session) sparql = result.get_sparql_query() @@ -383,6 +425,12 @@ def set_description(): new_desc = data["newDesc"] result = Result(current_app, session, result_info) + if not result: + return jsonify({ + 'files': [], + 'error': True, + 'errorMessage': "You do not have access to this result" + }), 500 result.update_description(new_desc) results_handler = ResultsHandler(current_app, session) @@ -427,6 +475,12 @@ def publish_query(): result_info = {"id": data["id"]} result = Result(current_app, session, result_info) + if not result: + return jsonify({ + 'files': [], + 'error': True, + 'errorMessage': 'Failed to publish query: \n{}'.format("You do not have access to this query") + }), 401 result.publish_query(data.get("public", False)) results_handler = ResultsHandler(current_app, session) @@ -471,6 +525,12 @@ def template_query(): result_info = {"id": data["id"]} result = Result(current_app, session, result_info) + if not result: + return jsonify({ + 'files': [], + 'error': True, + 'errorMessage': 'Failed to publish query: \n{}'.format("You do not have access to this query") + }), 401 result.template_query(data.get("template", False)) results_handler = ResultsHandler(current_app, session) @@ -491,6 +551,56 @@ def template_query(): }) +@results_bp.route('/api/results/form', methods=['POST']) +@api_auth +@admin_required +def form_query(): + """Create a form from a result + + Returns + ------- + json + error: True if error, else False + errorMessage: the error message of error, else an empty string + """ + try: + data = request.get_json() + if not (data and data.get("id")): + return jsonify({ + 'files': [], + 'error': True, + 'errorMessage': "Missing id parameter" + }), 400 + + result_info = {"id": data["id"]} + + result = Result(current_app, session, result_info) + if not result: + return jsonify({ + 'files': [], + 'error': True, + 'errorMessage': 'Failed to publish query: \n{}'.format("You do not have access to this query") + }), 401 + result.form_query(data.get("form", False)) + + results_handler = ResultsHandler(current_app, session) + files = results_handler.get_files_info() + + except Exception as e: + traceback.print_exc(file=sys.stdout) + return jsonify({ + 'files': [], + 'error': True, + 'errorMessage': 'Failed to create form template query: \n{}'.format(str(e)) + }), 500 + + return jsonify({ + 'files': files, + 'error': False, + 'errorMessage': '' + }) + + @results_bp.route('/api/results/send2galaxy', methods=['POST']) @api_auth @login_required @@ -513,6 +623,11 @@ def send2galaxy(): result_info = {"id": data["fileId"]} result = Result(current_app, session, result_info) + if not result: + return jsonify({ + 'error': True, + 'errorMessage': 'Failed to publish query: \n{}'.format("You do not have access to this query") + }), 401 result.send2galaxy(data["fileToSend"]) except Exception as e: traceback.print_exc(file=sys.stdout) @@ -525,3 +640,48 @@ def send2galaxy(): 'error': False, 'errorMessage': '' }) + + +@results_bp.route('/api/results/save_form', methods=['POST']) +@api_auth +@admin_required +def save_form(): + """Update a form + + Returns + ------- + json + error: True if error, else False + errorMessage: the error message of error, else an empty string + """ + try: + # Get query and endpoints and graphs of the query + data = request.get_json() + if not (data and data.get("graphState") and data.get("formId")): + return jsonify({ + 'error': True, + 'errorMessage': "Missing graphState or formId parameter" + }), 400 + + result_info = {"id": data["formId"]} + + result = Result(current_app, session, result_info) + if not result: + return jsonify({ + 'error': True, + 'errorMessage': 'Failed to edit form: \n{}'.format("You do not have access to this form") + }), 401 + + result.update_graph(data.get("graphState")) + + except Exception as e: + traceback.print_exc(file=sys.stdout) + return jsonify({ + 'error': True, + 'errorMessage': str(e), + }), 500 + + return jsonify({ + 'error': False, + 'errorMessage': '' + }) diff --git a/askomics/libaskomics/Database.py b/askomics/libaskomics/Database.py index d5cf37ca..5afadeae 100644 --- a/askomics/libaskomics/Database.py +++ b/askomics/libaskomics/Database.py @@ -233,6 +233,8 @@ def create_results_table(self): traceback text, graphs_and_endpoints text, template boolean, + has_form_attr boolean, + form boolean, FOREIGN KEY(user_id) REFERENCES users(user_id) ) ''' @@ -296,6 +298,28 @@ def update_results_table(self): except Exception: pass + query = ''' + ALTER TABLE results + ADD has_form_attr boolean NULL + DEFAULT(0) + ''' + + try: + self.execute_sql_query(query) + except Exception: + pass + + query = ''' + ALTER TABLE results + ADD form boolean NULL + DEFAULT(0) + ''' + + try: + self.execute_sql_query(query) + except Exception: + pass + def create_endpoints_table(self): """Create the endpoints table""" query = ''' diff --git a/askomics/libaskomics/Result.py b/askomics/libaskomics/Result.py index e5e26bb8..07e74ca5 100644 --- a/askomics/libaskomics/Result.py +++ b/askomics/libaskomics/Result.py @@ -52,7 +52,8 @@ def __init__(self, app, session, result_info, force_no_db=False): if "id" in result_info and not force_no_db: self.id = result_info["id"] - self.set_info_from_db_with_id() + if not self.set_info_from_db_with_id(): + return None else: self.id = result_info["id"] if "id" in result_info else None self.graph_state = result_info["graph_state"] if "graph_state" in result_info else None @@ -65,6 +66,9 @@ def __init__(self, app, session, result_info, force_no_db=False): self.start = None self.end = None self.nrows = 0 + self.has_form_attr = False + self.template = False + self.form = False def clean_node(self, node): """Clean a node by removing coordinates and other stuff @@ -245,7 +249,7 @@ def set_info_from_db_with_id(self): if "user" in self.session: query = ''' - SELECT celery_id, path, graph_state, start, end, nrows, sparql_query, graphs_and_endpoints + SELECT celery_id, path, graph_state, start, end, nrows, sparql_query, graphs_and_endpoints, has_form_attr, template, form FROM results WHERE (user_id = ? OR public = ?) AND id = ? ''' @@ -254,13 +258,16 @@ def set_info_from_db_with_id(self): else: query = ''' - SELECT celery_id, path, graph_state, start, end, nrows, sparql_query, graphs_and_endpoints + SELECT celery_id, path, graph_state, start, end, nrows, sparql_query, graphs_and_endpoints, has_form_attr, template, form FROM results WHERE public = ? AND id = ? ''' rows = database.execute_sql_query(query, (True, self.id)) + if not rows: + return False + self.celery_id = rows[0][0] if rows[0][0] else '' self.file_path = rows[0][1] if rows[0][1] else '' self.file_name = os.path.basename(self.file_path) @@ -269,11 +276,16 @@ def set_info_from_db_with_id(self): self.end = rows[0][4] self.nrows = rows[0][5] self.sparql_query = rows[0][6] + self.has_form_attr = rows[0][8] if rows[0][8] else False + self.template = rows[0][9] if rows[0][9] else False + self.form = rows[0][10] if rows[0][10] else False gne = json.loads(rows[0][7]) if rows[0][7] else {"graphs": [], "endpoints": []} self.graphs = gne["graphs"] self.endpoints = gne["endpoints"] + return True + def get_file_preview(self): """Get a preview of the results file @@ -355,6 +367,8 @@ def save_in_db(self): ?, NULL, ?, + ?, + ?, ? ) ''' @@ -368,6 +382,8 @@ def save_in_db(self): "Query", self.sparql_query, json.dumps({"graphs": self.graphs, "endpoints": self.endpoints}), + False, + self.session["user"]["admin"] and any([attrib.get("form") for attrib in self.graph_state["attr"]]) if (self.graph_state and self.graph_state.get("attr")) else False, False ), get_id=True) @@ -484,16 +500,20 @@ def publish_query(self, public, admin=False): """Set public to True or False, and template to True if public is True""" database = Database(self.app, self.session) - # If query is set to public, template have to be True + # If query is set to public, template or form (if available) have to be True sql_substr = '' if admin and self.session['user']['admin']: sql_var = (public, self.id) where_query = "" + # Should not happen else: sql_var = (public, self.id, self.session["user"]["id"]) where_query = "AND user_id=?" if public: - sql_substr = 'template=?,' + if self.has_form_attr and not self.template: + sql_substr = 'form=?,' + else: + sql_substr = 'template=?,' sql_var = (public,) + sql_var query = ''' @@ -507,13 +527,17 @@ def publish_query(self, public, admin=False): database.execute_sql_query(query, sql_var) def template_query(self, template): - """Set template to True or False, and public to False if template is False""" + """Set template to True or False, and public to False if template and form are False""" database = Database(self.app, self.session) - # If query is set to public, template have to be True sql_substr = '' sql_var = (template, self.session["user"]["id"], self.id) - if not template: + + if template and self.form: + sql_substr = 'form=?,' + sql_var = (False, template, self.session["user"]["id"], self.id) + + if not (template or self.form): sql_substr = 'public=?,' sql_var = (template, template, self.session["user"]["id"], self.id) @@ -526,6 +550,32 @@ def template_query(self, template): database.execute_sql_query(query, sql_var) + def form_query(self, form): + """Set form to True or False, Set Template to False if True, public to False if template and form are False""" + database = Database(self.app, self.session) + if not self.has_form_attr: + raise Exception("This query does not has any form template attribute") + + sql_substr = '' + sql_var = (form, self.session["user"]["id"], self.id) + + if form and self.template: + sql_substr = 'template=?,' + sql_var = (False, form, self.session["user"]["id"], self.id) + + if not (form or self.template): + sql_substr = 'public=?,' + sql_var = (form, form, self.session["user"]["id"], self.id) + + query = ''' + UPDATE results SET + {} + form=? + WHERE user_id=? AND id=? + '''.format(sql_substr) + + database.execute_sql_query(query, sql_var) + def update_description(self, description): """Change the result description""" database = Database(self.app, self.session) @@ -542,6 +592,22 @@ def update_description(self, description): self.id )) + def update_graph(self, newGraph): + """Change the result description""" + database = Database(self.app, self.session) + + query = ''' + UPDATE results SET + graph_state=? + WHERE user_id=? AND id=? + ''' + + database.execute_sql_query(query, ( + json.dumps(newGraph), + self.session["user"]["id"], + self.id + )) + def send2galaxy(self, file2send): """Send files to Galaxy""" if file2send == "result": diff --git a/askomics/libaskomics/ResultsHandler.py b/askomics/libaskomics/ResultsHandler.py index 03b0bec2..8b9fe5ec 100644 --- a/askomics/libaskomics/ResultsHandler.py +++ b/askomics/libaskomics/ResultsHandler.py @@ -49,7 +49,7 @@ def get_files_info(self): database = Database(self.app, self.session) query = ''' - SELECT id, status, path, start, end, graph_state, nrows, error, public, template, description, size, sparql_query, traceback + SELECT id, status, path, start, end, graph_state, nrows, error, public, template, description, size, sparql_query, traceback, has_form_attr, form FROM results WHERE user_id = ? ''' @@ -79,7 +79,9 @@ def get_files_info(self): 'description': row[10], 'size': row[11], 'sparqlQuery': row[12], - 'traceback': row[13] + 'traceback': row[13], + 'has_form_attr': row[14], + 'form': row[15] }) return files @@ -94,16 +96,51 @@ def get_public_queries(self): """ database = Database(self.app, self.session) - where_substring = "" - sql_var = (True, ) + where_substring = "WHERE template = ? and public = ?" + sql_var = (True, True,) + if "user" in self.session: + where_substring = "WHERE template = ? and (public = ? or user_id = ?)" + sql_var = (True, True, self.session["user"]["id"]) + + query = ''' + SELECT id, description, public + FROM results + {} + '''.format(where_substring) + + rows = database.execute_sql_query(query, sql_var) + + queries = [] + + for row in rows: + queries.append({ + "id": row[0], + "description": row[1], + "public": row[2] + }) + + return queries + + def get_public_form_queries(self): + """Get id and description of published form queries + + Returns + ------- + List + List of published form queries (id and description) + """ + database = Database(self.app, self.session) + + where_substring = "WHERE form = ? and public = ?" + sql_var = (True, True,) if "user" in self.session: - where_substring = " or (template = ? and user_id = ?)" + where_substring = "WHERE form = ? and (public = ? or user_id = ?)" sql_var = (True, True, self.session["user"]["id"]) query = ''' SELECT id, description, public FROM results - WHERE public = ?{} + {} '''.format(where_substring) rows = database.execute_sql_query(query, sql_var) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 2d071644..dfa27095 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -804,7 +804,7 @@ def replace_variables_in_triples(self, var_to_replace): var_target = tpl_var[1] for i, triple_dict in enumerate(self.triples): for key, value in triple_dict.items(): - if key not in ["optional", "nested", "nested_start", "nested_end"]: + if key not in ["optional", "nested", "nested_start", "nested_end", "form"]: self.triples[i][key] = value.replace(var_source, var_target) for i, select in enumerate(self.selects): self.selects[i] = select.replace(var_source, var_target) @@ -831,7 +831,7 @@ def replace_variables_in_blocks(self, var_to_replace): # Iterate over triples for ntriple, triple_dict in enumerate(sblock["triples"]): for key, value in triple_dict.items(): - if key not in ["optional", "nested", "nested_start", "nested_end"]: + if key not in ["optional", "nested", "nested_start", "nested_end", "form"]: self.triples_blocks[nblock]["sblocks"][nsblock]["triples"][ntriple][key] = value.replace(var_source, var_target) for i, filtr in enumerate(sblock["filters"]): diff --git a/askomics/react/src/routes.jsx b/askomics/react/src/routes.jsx index 92c2970f..6555c070 100644 --- a/askomics/react/src/routes.jsx +++ b/askomics/react/src/routes.jsx @@ -15,6 +15,8 @@ import PasswordReset from './routes/login/passwordreset' import Account from './routes/account/account' import Admin from './routes/admin/admin' import Sparql from './routes/sparql/sparql' +import FormQuery from './routes/form/query' +import FormEditQuery from './routes/form_edit/query' import Query from './routes/query/query' import Results from './routes/results/results' import AskoNavbar from './navbar' @@ -112,6 +114,8 @@ export default class Routes extends Component { ( this.setState(p)} />)} /> ( this.setState(p)} />)} /> + + ()} /> }/> ()} /> diff --git a/askomics/react/src/routes/ask/ask.jsx b/askomics/react/src/routes/ask/ask.jsx index 1a1b87cf..e788fef5 100644 --- a/askomics/react/src/routes/ask/ask.jsx +++ b/askomics/react/src/routes/ask/ask.jsx @@ -23,11 +23,13 @@ export default class Ask extends Component { selected: null, startSession: false, publicQueries: [], + publicFormQueries: [], modalGalaxy: false, showGalaxyButton: false, dropdownOpen: false, selectedEndpoint: [], - frontMessage: "" + frontMessage: "", + redirectFormBuilder: false } this.utils = new Utils() this.cancelRequest @@ -35,6 +37,7 @@ export default class Ask extends Component { this.handleStart = this.handleStart.bind(this) this.handleFilter = this.handleFilter.bind(this) this.handleClickTemplateQuery = this.handleClickTemplateQuery.bind(this) + this.handleClickTemplateFormQuery = this.handleClickTemplateFormQuery.bind(this) this.toggleDropDown = this.toggleDropDown.bind(this) this.toggleModalGalaxy = this.toggleModalGalaxy.bind(this) this.clickOnEndpoint = this.clickOnEndpoint.bind(this) @@ -88,6 +91,7 @@ export default class Ask extends Component { selected: false })), publicQueries: response.data.publicQueries, + publicFormQueries: response.data.publicFormQueries, startSessionWithExemple: false }) }) @@ -165,6 +169,29 @@ export default class Ask extends Component { }) } + handleClickTemplateFormQuery (event) { + let requestUrl = '/api/results/graphstate' + let data = { fileId: event.target.id, formated: false } + axios.post(requestUrl, data, {baseURL: this.props.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) }) + .then(response => { + console.log(requestUrl, response.data) + // set state of resultsPreview + this.setState({ + redirectFormBuilder: true, + graphState: response.data.graphState + }) + }) + .catch(error => { + console.log(error, error.response.data.errorMessage) + this.setState({ + error: true, + errorMessage: error.response.data.errorMessage, + status: error.response.status, + waiting: false + }) + }) + } + toggleModalGalaxy (event) { this.setState({ modalGalaxy: !this.state.modalGalaxy @@ -254,6 +281,17 @@ export default class Ask extends Component { }} /> } + let redirectFormBuilder + if (this.state.redirectFormBuilder) { + redirectFormBuilder = + } + let errorDiv if (this.state.error) { @@ -267,6 +305,7 @@ export default class Ask extends Component { } let templateQueries + let templateFormQueries let emptyPrivate if (!this.state.waiting && this.state.publicQueries.length > 0) { templateQueries = ( @@ -292,6 +331,30 @@ export default class Ask extends Component { ) } + if (!this.state.waiting && this.state.publicFormQueries.length > 0) { + templateFormQueries = ( +
+

Or start with a simplified form:

+ + {this.state.publicFormQueries.map(query => { + if (query.public == 0) { + emptyPrivate =
+ return {query.description} + } + })} +
+ {emptyPrivate} + + {this.state.publicFormQueries.map(query => { + if (query.public == 1) { + return {query.description} + } + })} + +
+ ) + } + let galaxyImport if (!this.state.waiting && this.state.showGalaxyButton) { galaxyImport = ( @@ -408,6 +471,7 @@ export default class Ask extends Component { {redirectQueryBuilder} {redirectLogin} {redirectSparqlEditor} + {redirectFormBuilder} {HtmlFrontMessage} @@ -417,6 +481,7 @@ export default class Ask extends Component { {galaxyForm} + {templateFormQueries} {templateQueries} diff --git a/askomics/react/src/routes/form/attribute.jsx b/askomics/react/src/routes/form/attribute.jsx new file mode 100644 index 00000000..7b59e403 --- /dev/null +++ b/askomics/react/src/routes/form/attribute.jsx @@ -0,0 +1,486 @@ +import React, { Component} from 'react' +import axios from 'axios' +import { Input, FormGroup, CustomInput, FormFeedback } from 'reactstrap' +import { Redirect } from 'react-router-dom' +import DatePicker from "react-datepicker"; +import ErrorDiv from '../error/error' +import WaitingDiv from '../../components/waiting' +import update from 'react-addons-update' +import PropTypes from 'prop-types' +import Utils from '../../classes/utils' + +export default class AttributeBox extends Component { + constructor (props) { + super(props) + this.utils = new Utils() + this.state = {} + + this.toggleVisibility = this.props.toggleVisibility.bind(this) + this.handleNegative = this.props.handleNegative.bind(this) + this.toggleOptional = this.props.toggleOptional.bind(this) + this.toggleExclude = this.props.toggleExclude.bind(this) + this.handleFilterType = this.props.handleFilterType.bind(this) + this.handleFilterValue = this.props.handleFilterValue.bind(this) + this.handleFilterCategory = this.props.handleFilterCategory.bind(this) + this.handleFilterNumericSign = this.props.handleFilterNumericSign.bind(this) + this.handleFilterNumericValue = this.props.handleFilterNumericValue.bind(this) + this.handleFilterDateValue = this.props.handleFilterDateValue.bind(this) + this.toggleLinkAttribute = this.props.toggleLinkAttribute.bind(this) + this.handleChangeLink = this.props.handleChangeLink.bind(this) + this.toggleAddNumFilter = this.props.toggleAddNumFilter.bind(this) + this.toggleAddDateFilter = this.props.toggleAddDateFilter.bind(this) + this.handleDateFilter = this.props.handleDateFilter.bind(this) + } + + renderLinker () { + let options = [] + + this.props.graph.nodes.map(node => { + if (!node.suggested) { + options.push() + this.props.graph.attr.map(attr => { + if (attr.id != this.props.attribute.id && attr.nodeId == node.id && attr.type == this.props.attribute.type) { + options.push() + } + }) + } + }) + + return ( + + + {options.map(opt => { + return opt + })} + + ) + } + + checkUnvalidUri (value) { + if (value == "") { + return false + } else { + if (value.includes(":")) { + return false + } else { + return !this.utils.isUrl(value) + } + } + } + + 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 = ( + + + + + +
+ + {Object.keys(selected_sign).map(type => { + return + })} + + + + Please filter with a valid URI or CURIE +
+ ) + } + + return ( +
+ +
+ + +
+ {form} +
+ ) + } + + renderText () { + + let eyeIcon = 'attr-icon fas fa-eye-slash inactive' + if (this.props.attribute.visible) { + eyeIcon = 'attr-icon fas fa-eye' + } + + let optionalIcon = 'attr-icon fas fa-question-circle inactive' + if (this.props.attribute.optional) { + optionalIcon = 'attr-icon fas fa-question-circle' + } + + let negativIcon = 'attr-icon fas fa-not-equal inactive' + if (this.props.attribute.negative) { + negativIcon = 'attr-icon fas fa-not-equal' + } + + let linkIcon = 'attr-icon fas fa-unlink inactive' + if (this.props.attribute.linked) { + linkIcon = 'attr-icon fas fa-link' + } + + let selected = { + 'exact': false, + 'regexp': false + } + + let selected_sign = { + '=': !this.props.attribute.negative, + "≠": this.props.attribute.negative + } + + selected[this.props.attribute.filterType] = true + + let form + + if (this.props.attribute.linked) { + form = this.renderLinker() + } else { + form = ( + + + + + + +
+ + {Object.keys(selected).map(type => { + return + })} + + + + {Object.keys(selected_sign).map(type => { + return + })} + + + +
+ ) + } + + return ( +
+ +
+ + {this.props.attribute.uri == "rdf:type" || this.props.attribute.uri == "rdfs:label" ? : } + +
+ {form} +
+ ) + } + + renderNumeric () { + + let eyeIcon = 'attr-icon fas fa-eye-slash inactive' + if (this.props.attribute.visible) { + eyeIcon = 'attr-icon fas fa-eye' + } + + let optionalIcon = 'attr-icon fas fa-question-circle inactive' + if (this.props.attribute.optional) { + optionalIcon = 'attr-icon fas fa-question-circle' + } + + let linkIcon = 'attr-icon fas fa-unlink inactive' + if (this.props.attribute.linked) { + linkIcon = 'attr-icon fas fa-link' + } + + let sign_display = { + '=': '=', + '<': '<', + '<=': '≤', + '>': '>', + '>=': '≥', + '!=': '≠' + } + + let form + let numberOfFilters = this.props.attribute.filters.length - 1 + + if (this.props.attribute.linked) { + form = this.renderLinker() + } else { + form = ( + + {this.props.attribute.filters.map((filter, index) => { + return ( + + + + + ) + })} +
+ + {Object.keys(sign_display).map(sign => { + return + })} + + +
+ + {index == numberOfFilters ? : <>} +
+
+ ) + } + + return ( +
+ +
+ + + +
+ {form} +
+ ) + } + + renderCategory () { + + let eyeIcon = 'attr-icon fas fa-eye-slash inactive' + if (this.props.attribute.visible) { + eyeIcon = 'attr-icon fas fa-eye' + } + + let optionalIcon = 'attr-icon fas fa-question-circle inactive' + if (this.props.attribute.optional) { + optionalIcon = 'attr-icon fas fa-question-circle' + } + + let linkIcon = 'attr-icon fas fa-unlink inactive' + if (this.props.attribute.linked) { + linkIcon = 'attr-icon fas fa-link' + } + + let excludeIcon = 'attr-icon fas fa-ban inactive' + if (this.props.attribute.exclude) { + excludeIcon = 'attr-icon fas fa-ban' + } + + let form + + if (this.props.attribute.linked) { + form = this.renderLinker() + } else { + form = ( + + + {this.props.attribute.filterValues.map(value => { + let selected = this.props.attribute.filterSelectedValues.includes(value.uri) + return () + })} + + + ) + } + + return ( +
+ +
+ + + + +
+ {form} +
+ ) + } + + renderBoolean () { + + let eyeIcon = 'attr-icon fas fa-eye-slash inactive' + if (this.props.attribute.visible) { + eyeIcon = 'attr-icon fas fa-eye' + } + + let optionalIcon = 'attr-icon fas fa-question-circle inactive' + if (this.props.attribute.optional) { + optionalIcon = 'attr-icon fas fa-question-circle' + } + + let linkIcon = 'attr-icon fas fa-unlink inactive' + if (this.props.attribute.linked) { + linkIcon = 'attr-icon fas fa-link' + } + + let form + + if (this.props.attribute.linked) { + form = this.renderLinker() + } else { + form = ( + + + + + + + ) + } + + return ( +
+ +
+ + + +
+ {form} +
+ ) + } + + renderDate () { + + let eyeIcon = 'attr-icon fas fa-eye-slash inactive' + if (this.props.attribute.visible) { + eyeIcon = 'attr-icon fas fa-eye' + } + + let optionalIcon = 'attr-icon fas fa-question-circle inactive' + if (this.props.attribute.optional) { + optionalIcon = 'attr-icon fas fa-question-circle' + } + + let linkIcon = 'attr-icon fas fa-unlink inactive' + if (this.props.attribute.linked) { + linkIcon = 'attr-icon fas fa-link' + } + + let sign_display = { + '=': '=', + '<': '<', + '<=': '≤', + '>': '>', + '>=': '≥', + '!=': '≠' + } + let form + let numberOfFilters = this.props.attribute.filters.length - 1 + + if (this.props.attribute.linked) { + form = this.renderLinker() + } else { + form = ( + + {this.props.attribute.filters.map((filter, index) => { + return ( + + + + + ) + })} +
+ + {Object.keys(sign_display).map(sign => { + return + })} + + +
+ { + event.target = {value:date, id: this.props.attribute.id, dataset:{index: index}}; + this.handleFilterDateValue(event) + }} /> + {index == numberOfFilters ? : <>} +
+
+ ) + } + + return ( +
+ +
+ + + +
+ {form} +
+ ) + } + + + render () { + let box = null + if (this.props.attribute.type == 'text' || this.props.attribute.type == 'uri') { + box = this.renderText() + } + if (this.props.attribute.type == 'decimal') { + box = this.renderNumeric() + } + if (this.props.attribute.type == 'category') { + box = this.renderCategory() + } + if (this.props.attribute.type == 'boolean') { + box = this.renderBoolean() + } + if (this.props.attribute.type == 'date') { + box = this.renderDate() + } + return box + } +} + +AttributeBox.propTypes = { + handleNegative: PropTypes.func, + toggleVisibility: PropTypes.func, + toggleOptional: PropTypes.func, + toggleExclude: PropTypes.func, + toggleAddNumFilter: PropTypes.func, + handleFilterType: PropTypes.func, + handleFilterValue: PropTypes.func, + handleFilterCategory: PropTypes.func, + handleFilterNumericSign: PropTypes.func, + handleFilterNumericValue: PropTypes.func, + toggleLinkAttribute: PropTypes.func, + handleChangeLink: PropTypes.func, + toggleAddDateFilter: PropTypes.func, + handleFilterDateValue: PropTypes.func, + handleDateFilter: PropTypes.func, + attribute: PropTypes.object, + graph: PropTypes.object, +} diff --git a/askomics/react/src/routes/form/entity.jsx b/askomics/react/src/routes/form/entity.jsx new file mode 100644 index 00000000..739678ac --- /dev/null +++ b/askomics/react/src/routes/form/entity.jsx @@ -0,0 +1,35 @@ +import React, { Component} from 'react' +import axios from 'axios' +import { Input, FormGroup, CustomInput, FormFeedback } from 'reactstrap' +import { Redirect } from 'react-router-dom' +import DatePicker from "react-datepicker"; +import ErrorDiv from '../error/error' +import WaitingDiv from '../../components/waiting' +import update from 'react-addons-update' +import PropTypes from 'prop-types' +import Utils from '../../classes/utils' + +export default class Entity extends Component { + constructor (props) { + super(props) + this.utils = new Utils() + this.state = {} + } + + + render () { + let entity = this.props.entity + let attribute_boxes = this.props.attribute_boxes + return( +
+

{entity}

+ {attribute_boxes} +
+ ) + } +} + +Entity.propTypes = { + attribute_boxes: PropTypes.list, + entity: PropTypes.string, +} diff --git a/askomics/react/src/routes/form/query.jsx b/askomics/react/src/routes/form/query.jsx new file mode 100644 index 00000000..3b0fe52d --- /dev/null +++ b/askomics/react/src/routes/form/query.jsx @@ -0,0 +1,530 @@ +import React, { Component } from 'react' +import axios from 'axios' +import { Alert, Button, 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 AttributeBox from './attribute' +import Entity from './entity' +import ResultsTable from '../sparql/resultstable' +import PropTypes from 'prop-types' +import Utils from '../../classes/utils' + +export default class FormQuery extends Component { + + constructor (props) { + super(props) + this.utils = new Utils() + this.state = { + config: this.props.location.state.config, + startpoint: this.props.location.state.startpoint, + abstraction: [], + graphState: { + nodes: [], + links: [], + attr: [] + }, + resultsPreview: [], + headerPreview: [], + waiting: true, + error: false, + errorMessage: null, + saveTick: false, + + // save query icons + disableSave: false, + saveIcon: "play", + + // Preview icons + disablePreview: false, + previewIcon: "table" + } + + this.graphState = { + nodes: [], + links: [], + attr: [] + } + + this.idNumber = 0 + this.specialNodeIdNumber = 0 + this.previousSelected = null + this.currentSelected = null + this.cancelRequest + + this.handlePreview = this.handlePreview.bind(this) + this.handleQuery = this.handleQuery.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 + } + + toggleVisibility (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.visible = !attr.visible + if (!attr.visible) { + attr.optional = false + } + } + }) + this.updateGraphState() + } + + toggleExclude (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.exclude = !attr.exclude + if (attr.exclude) { + attr.visible = true + } + } + }) + this.updateGraphState() + } + + toggleOptional (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.optional = !attr.optional + if (attr.optional) { + attr.visible = true + } + } + }) + this.updateGraphState() + } + + toggleFormAttribute (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.form = !attr.form + } + }) + this.updateGraphState() + } + + handleNegative (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.negative = event.target.value == '=' ? false : true + } + }) + this.updateGraphState() + } + + handleFilterType (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filterType = event.target.value + } + }) + this.updateGraphState() + } + + handleFilterValue (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filterValue = event.target.value + } + }) + this.updateGraphState() + } + + handleFilterCategory (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filterSelectedValues = [...event.target.selectedOptions].map(o => o.value) + } + }) + this.updateGraphState() + } + + handleFilterNumericSign (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filters.map((filter, index) => { + if (index == event.target.dataset.index) { + filter.filterSign = event.target.value + } + }) + } + }) + this.updateGraphState() + } + + toggleAddNumFilter (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filters.push({ + filterValue: "", + filterSign: "=" + }) + } + }) + this.updateGraphState() + } + + handleFilterNumericValue (event) { + if (!isNaN(event.target.value)) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filters.map((filter, index) => { + if (index == event.target.dataset.index) { + filter.filterValue = event.target.value + } + }) + } + }) + this.updateGraphState() + } + } + + handleDateFilter (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filters.map((filter, index) => { + if (index == event.target.dataset.index) { + filter.filterSign = event.target.value + } + }) + } + }) + this.updateGraphState() + } + + toggleAddDateFilter (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filters.push({ + filterValue: null, + filterSign: "=" + }) + } + }) + this.updateGraphState() + } + + // This is a pain, but JS will auto convert time to UTC + // And datepicker use the local timezone + // So without this, the day sent will be wrong + fixTimezoneOffset (date){ + if(!date){return null}; + return new Date(date.getTime() - (date.getTimezoneOffset() * 60000)); + } + + + handleFilterDateValue (event) { + if (!isNaN(event.target.value)) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filters.map((filter, index) => { + if (index == event.target.dataset.index) { + filter.filterValue = this.fixTimezoneOffset(event.target.value) + } + }) + } + }) + this.updateGraphState() + } + } + + count_displayed_attributes() { + return this.graphState.attr.map(attr => { + return attr.visible ? 1 : 0 + }).reduce((a, b) => a + b) + } + + getAttributeType (typeUri) { + // FIXME: don't hardcode uri + if (typeUri == 'http://www.w3.org/2001/XMLSchema#decimal') { + return 'decimal' + } + if (typeUri == this.state.config.namespaceInternal + 'AskomicsCategory') { + return 'category' + } + if (typeUri == 'http://www.w3.org/2001/XMLSchema#string') { + return 'text' + } + if (typeUri == "http://www.w3.org/2001/XMLSchema#boolean") { + return "boolean" + } + if (typeUri == "http://www.w3.org/2001/XMLSchema#date") { + return "date" + } + } + + updateGraphState (waiting=this.state.waiting) { + this.setState({ + graphState: this.graphState, + previewIcon: "table", + resultsPreview: [], + headerPreview: [], + disableSave: false, + disablePreview: false, + saveIcon: "play", + waiting: waiting + }) + } + + // Preview results and Launch query buttons ------- + + handlePreview (event) { + let requestUrl = '/api/query/preview' + let data = { + graphState: this.graphState + } + this.setState({ + disablePreview: true, + previewIcon: "spinner" + }) + + // display an error message if user don't display attribute to avoid the virtuoso SPARQL error + if (this.count_displayed_attributes() == 0) { + this.setState({ + error: true, + errorMessage: ["No attribute are displayed. Use eye icon to display at least one attribute", ], + disablePreview: false, + previewIcon: "times text-error" + }) + return + } + + axios.post(requestUrl, data, { baseURL: this.state.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) }) + .then(response => { + console.log(requestUrl, response.data) + this.setState({ + resultsPreview: response.data.resultsPreview, + headerPreview: response.data.headerPreview, + waiting: false, + error: false, + previewIcon: "check text-success" + }) + }) + .catch(error => { + console.log(error, error.response.data.errorMessage) + this.setState({ + error: true, + errorMessage: error.response.data.errorMessage, + status: error.response.status, + disablePreview: false, + previewIcon: "times text-error" + }) + }) + } + + handleQuery (event) { + let requestUrl = '/api/query/save_result' + let data = { + graphState: this.graphState + } + + // display an error message if user don't display attribute to avoid the virtuoso SPARQL error + if (this.count_displayed_attributes() == 0) { + this.setState({ + error: true, + errorMessage: ["No attribute are displayed. Use eye icon to display at least one attribute", ], + disableSave: false, + saveIcon: "times text-error" + }) + return + } + + axios.post(requestUrl, data, { baseURL: this.state.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) }) + .then(response => { + console.log(requestUrl, response.data) + this.setState({ + saveIcon: "check text-success", + disableSave: true + }) + }).catch(error => { + console.log(error, error.response.data.errorMessage) + this.setState({ + error: true, + errorMessage: error.response.data.errorMessage, + status: error.response.status, + saveIcon: "times text-error", + disableSave: false + }) + }) + } + + // ------------------------------------------------ + + componentDidMount () { + if (!this.props.waitForStart) { + let requestUrl = '/api/query/abstraction' + axios.get(requestUrl, { baseURL: this.state.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) }) + .then(response => { + console.log(requestUrl, response.data) + this.setState({ + waiting: false, + abstraction: response.data.abstraction, + diskSpace: response.data.diskSpace, + exceededQuota: this.state.config.user.quota > 0 && response.data.diskSpace >= this.state.config.user.quota, + }) + }) + .catch(error => { + console.log(error, error.response.data.errorMessage) + this.setState({ + error: true, + errorMessage: error.response.data.errorMessage, + status: error.response.status + }) + }).then(response => { + this.graphState = this.props.location.state.graphState + console.log(this.props.location.state.graphState) + this.updateGraphState() + this.setState({ waiting: false }) + }) + } + } + + componentWillUnmount () { + if (!this.props.waitForStart) { + this.cancelRequest() + } + } + + render () { + // login page redirection + let redirectLogin + if (this.state.status == 401) { + redirectLogin = + } + + // error div + let errorDiv + if (this.state.error) { + errorDiv = ( +
+ + {this.state.errorMessage} + +
+ ) + } + + // Warning disk space + let warningDiskSpace + if (this.state.exceededQuota) { + warningDiskSpace = ( +
+ + Your files (uploaded files and results) take {this.utils.humanFileSize(this.state.diskSpace, true)} of space + (you have {this.utils.humanFileSize(this.state.config.user.quota, true)} allowed). + Please delete some before save queries or contact an admin to increase your quota + +
+ ) + } + + + let AttributeBoxes + let Entities = [] + let previewButton + let launchQueryButton + let entityMap = new Map() + + 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.get(attribute.nodeId).attributes.push( + this.handleChangeLink(p)} + toggleVisibility={p => this.toggleVisibility(p)} + toggleExclude={p => this.toggleExclude(p)} + handleNegative={p => this.handleNegative(p)} + toggleOptional={p => this.toggleOptional(p)} + handleFilterType={p => this.handleFilterType(p)} + handleFilterValue={p => this.handleFilterValue(p)} + handleFilterCategory={p => this.handleFilterCategory(p)} + handleFilterNumericSign={p => this.handleFilterNumericSign(p)} + handleFilterNumericValue={p => this.handleFilterNumericValue(p)} + toggleLinkAttribute={p => this.toggleLinkAttribute(p)} + toggleAddNumFilter={p => this.toggleAddNumFilter(p)} + toggleAddDateFilter={p => this.toggleAddDateFilter(p)} + handleFilterDateValue={p => this.handleFilterDateValue(p)} + handleDateFilter={p => this.handleDateFilter(p)} + /> + ) + } + }) + + entityMap.forEach((value, key) => { + Entities.push( + + ) + }) + + // buttons + let previewIcon = + if (this.state.previewIcon == "spinner") { + previewIcon = + } + previewButton = + if (this.state.config.logged) { + launchQueryButton = + } + } + + // preview + let resultsTable + if (this.state.headerPreview.length > 0) { + resultsTable = ( + + ) + } + + return ( +
+ {redirectLogin} +

Query Builder

+
+ +
+ + +
+ {Entities} +
+ +
+ {warningDiskSpace} +
+ + {previewButton} + {launchQueryButton} + +

+
+ {resultsTable} +
+ +
+ ) + } +} + +FormQuery.propTypes = { + location: PropTypes.object, + waitForStart: PropTypes.bool +} diff --git a/askomics/react/src/routes/form_edit/attribute.jsx b/askomics/react/src/routes/form_edit/attribute.jsx new file mode 100644 index 00000000..f7c95455 --- /dev/null +++ b/askomics/react/src/routes/form_edit/attribute.jsx @@ -0,0 +1,489 @@ +import React, { Component} from 'react' +import axios from 'axios' +import { Input, FormGroup, CustomInput, FormFeedback } from 'reactstrap' +import { Redirect } from 'react-router-dom' +import DatePicker from "react-datepicker"; +import ErrorDiv from '../error/error' +import WaitingDiv from '../../components/waiting' +import update from 'react-addons-update' +import PropTypes from 'prop-types' +import Utils from '../../classes/utils' + +export default class AttributeBox extends Component { + constructor (props) { + super(props) + this.utils = new Utils() + this.state = {} + + this.toggleVisibility = this.props.toggleVisibility.bind(this) + this.handleNegative = this.props.handleNegative.bind(this) + this.toggleOptional = this.props.toggleOptional.bind(this) + this.toggleExclude = this.props.toggleExclude.bind(this) + this.handleFilterType = this.props.handleFilterType.bind(this) + this.handleFilterValue = this.props.handleFilterValue.bind(this) + this.handleFilterCategory = this.props.handleFilterCategory.bind(this) + this.handleFilterNumericSign = this.props.handleFilterNumericSign.bind(this) + this.handleFilterNumericValue = this.props.handleFilterNumericValue.bind(this) + this.handleFilterDateValue = this.props.handleFilterDateValue.bind(this) + this.toggleLinkAttribute = this.props.toggleLinkAttribute.bind(this) + this.handleChangeLink = this.props.handleChangeLink.bind(this) + this.toggleAddNumFilter = this.props.toggleAddNumFilter.bind(this) + this.toggleAddDateFilter = this.props.toggleAddDateFilter.bind(this) + this.handleDateFilter = this.props.handleDateFilter.bind(this) + } + + renderLinker () { + let options = [] + + this.props.graph.nodes.map(node => { + if (!node.suggested) { + options.push() + this.props.graph.attr.map(attr => { + if (attr.id != this.props.attribute.id && attr.nodeId == node.id && attr.type == this.props.attribute.type) { + options.push() + } + }) + } + }) + + return ( + + + {options.map(opt => { + return opt + })} + + ) + } + + checkUnvalidUri (value) { + if (value == "") { + return false + } else { + if (value.includes(":")) { + return false + } else { + return !this.utils.isUrl(value) + } + } + } + + 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 = ( + + + + + +
+ + {Object.keys(selected_sign).map(type => { + return + })} + + + + Please filter with a valid URI or CURIE +
+ ) + } + + return ( +
+ +
+ + +
+ {form} +
+ ) + } + + renderText () { + + let eyeIcon = 'attr-icon fas fa-eye-slash inactive' + if (this.props.attribute.visible) { + eyeIcon = 'attr-icon fas fa-eye' + } + + let optionalIcon = 'attr-icon fas fa-question-circle inactive' + if (this.props.attribute.optional) { + optionalIcon = 'attr-icon fas fa-question-circle' + } + + let negativIcon = 'attr-icon fas fa-not-equal inactive' + if (this.props.attribute.negative) { + negativIcon = 'attr-icon fas fa-not-equal' + } + + let linkIcon = 'attr-icon fas fa-unlink inactive' + if (this.props.attribute.linked) { + linkIcon = 'attr-icon fas fa-link' + } + + let selected = { + 'exact': false, + 'regexp': false + } + + let selected_sign = { + '=': !this.props.attribute.negative, + "≠": this.props.attribute.negative + } + + selected[this.props.attribute.filterType] = true + + let label = this.props.attribute.displayLabel ? this.props.attribute.displayLabel : this.props.attribute.label + + let form + + if (this.props.attribute.linked) { + form = this.renderLinker() + } else { + form = ( + + + + + + +
+ + {Object.keys(selected).map(type => { + return + })} + + + + {Object.keys(selected_sign).map(type => { + return + })} + + + +
+ ) + } + + return ( +
+ +
+ + {this.props.attribute.uri == "rdf:type" || this.props.attribute.uri == "rdfs:label" ? : } + +
+ {form} +
+ ) + } + + renderNumeric () { + + let eyeIcon = 'attr-icon fas fa-eye-slash inactive' + if (this.props.attribute.visible) { + eyeIcon = 'attr-icon fas fa-eye' + } + + let optionalIcon = 'attr-icon fas fa-question-circle inactive' + if (this.props.attribute.optional) { + optionalIcon = 'attr-icon fas fa-question-circle' + } + + let linkIcon = 'attr-icon fas fa-unlink inactive' + if (this.props.attribute.linked) { + linkIcon = 'attr-icon fas fa-link' + } + + let sign_display = { + '=': '=', + '<': '<', + '<=': '≤', + '>': '>', + '>=': '≥', + '!=': '≠' + } + + let form + let numberOfFilters = this.props.attribute.filters.length - 1 + + if (this.props.attribute.linked) { + form = this.renderLinker() + } else { + form = ( + + {this.props.attribute.filters.map((filter, index) => { + return ( + + + + + ) + })} +
+ + {Object.keys(sign_display).map(sign => { + return + })} + + +
+ + {index == numberOfFilters ? : <>} +
+
+ ) + } + + return ( +
+ +
+ + + +
+ {form} +
+ ) + } + + renderCategory () { + + let eyeIcon = 'attr-icon fas fa-eye-slash inactive' + if (this.props.attribute.visible) { + eyeIcon = 'attr-icon fas fa-eye' + } + + let optionalIcon = 'attr-icon fas fa-question-circle inactive' + if (this.props.attribute.optional) { + optionalIcon = 'attr-icon fas fa-question-circle' + } + + let linkIcon = 'attr-icon fas fa-unlink inactive' + if (this.props.attribute.linked) { + linkIcon = 'attr-icon fas fa-link' + } + + let excludeIcon = 'attr-icon fas fa-ban inactive' + if (this.props.attribute.exclude) { + excludeIcon = 'attr-icon fas fa-ban' + } + + let form + + if (this.props.attribute.linked) { + form = this.renderLinker() + } else { + form = ( + + + {this.props.attribute.filterValues.map(value => { + let selected = this.props.attribute.filterSelectedValues.includes(value.uri) + return () + })} + + + ) + } + + return ( +
+ +
+ + + + +
+ {form} +
+ ) + } + + renderBoolean () { + + let eyeIcon = 'attr-icon fas fa-eye-slash inactive' + if (this.props.attribute.visible) { + eyeIcon = 'attr-icon fas fa-eye' + } + + let optionalIcon = 'attr-icon fas fa-question-circle inactive' + if (this.props.attribute.optional) { + optionalIcon = 'attr-icon fas fa-question-circle' + } + + let linkIcon = 'attr-icon fas fa-unlink inactive' + if (this.props.attribute.linked) { + linkIcon = 'attr-icon fas fa-link' + } + + let form + + if (this.props.attribute.linked) { + form = this.renderLinker() + } else { + form = ( + + + + + + + ) + } + + return ( +
+ +
+ + + +
+ {form} +
+ ) + } + + renderDate () { + + let eyeIcon = 'attr-icon fas fa-eye-slash inactive' + if (this.props.attribute.visible) { + eyeIcon = 'attr-icon fas fa-eye' + } + + let optionalIcon = 'attr-icon fas fa-question-circle inactive' + if (this.props.attribute.optional) { + optionalIcon = 'attr-icon fas fa-question-circle' + } + + let linkIcon = 'attr-icon fas fa-unlink inactive' + if (this.props.attribute.linked) { + linkIcon = 'attr-icon fas fa-link' + } + + let sign_display = { + '=': '=', + '<': '<', + '<=': '≤', + '>': '>', + '>=': '≥', + '!=': '≠' + } + let form + let numberOfFilters = this.props.attribute.filters.length - 1 + + if (this.props.attribute.linked) { + form = this.renderLinker() + } else { + form = ( + + {this.props.attribute.filters.map((filter, index) => { + return ( + + + + + ) + })} +
+ + {Object.keys(sign_display).map(sign => { + return + })} + + +
+ { + event.target = {value:date, id: this.props.attribute.id, dataset:{index: index}}; + this.handleFilterDateValue(event) + }} /> + {index == numberOfFilters ? : <>} +
+
+ ) + } + + return ( +
+ +
+ + + +
+ {form} +
+ ) + } + + + render () { + let box = null + if (this.props.attribute.type == 'text' || this.props.attribute.type == 'uri') { + box = this.renderText() + } + if (this.props.attribute.type == 'decimal') { + box = this.renderNumeric() + } + if (this.props.attribute.type == 'category') { + box = this.renderCategory() + } + if (this.props.attribute.type == 'boolean') { + box = this.renderBoolean() + } + if (this.props.attribute.type == 'date') { + box = this.renderDate() + } + return box + } +} + +AttributeBox.propTypes = { + handleNegative: PropTypes.func, + toggleVisibility: PropTypes.func, + toggleOptional: PropTypes.func, + toggleExclude: PropTypes.func, + toggleAddNumFilter: PropTypes.func, + handleFilterType: PropTypes.func, + handleFilterValue: PropTypes.func, + handleFilterCategory: PropTypes.func, + handleFilterNumericSign: PropTypes.func, + handleFilterNumericValue: PropTypes.func, + toggleLinkAttribute: PropTypes.func, + handleChangeLink: PropTypes.func, + toggleAddDateFilter: PropTypes.func, + handleFilterDateValue: PropTypes.func, + handleDateFilter: PropTypes.func, + attribute: PropTypes.object, + graph: PropTypes.object, + setAttributeName: PropTypes.func +} diff --git a/askomics/react/src/routes/form_edit/entity.jsx b/askomics/react/src/routes/form_edit/entity.jsx new file mode 100644 index 00000000..ecc15ab1 --- /dev/null +++ b/askomics/react/src/routes/form_edit/entity.jsx @@ -0,0 +1,41 @@ +import React, { Component} from 'react' +import axios from 'axios' +import { Input, FormGroup, CustomInput, FormFeedback, Label } from 'reactstrap' +import { Redirect } from 'react-router-dom' +import DatePicker from "react-datepicker"; +import ErrorDiv from '../error/error' +import WaitingDiv from '../../components/waiting' +import update from 'react-addons-update' +import PropTypes from 'prop-types' +import Utils from '../../classes/utils' + +export default class Entity extends Component { + constructor (props) { + super(props) + this.utils = new Utils() + this.state = {} + } + + + render () { + let entity_id = this.props.entity_id + let entity = this.props.entity + let attribute_boxes = this.props.attribute_boxes + return( +
+ + + + + {attribute_boxes} +
+ ) + } +} + +Entity.propTypes = { + setEntityName: PropTypes.func, + attribute_boxes: PropTypes.list, + entity: PropTypes.string, + entity_id: PropTypes.number, +} diff --git a/askomics/react/src/routes/form_edit/query.jsx b/askomics/react/src/routes/form_edit/query.jsx new file mode 100644 index 00000000..88aeeb44 --- /dev/null +++ b/askomics/react/src/routes/form_edit/query.jsx @@ -0,0 +1,477 @@ +import React, { Component } from 'react' +import axios from 'axios' +import { Alert, Button, 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 AttributeBox from './attribute' +import Entity from './entity' +import ResultsTable from '../sparql/resultstable' +import PropTypes from 'prop-types' +import Utils from '../../classes/utils' + +export default class FormEditQuery extends Component { + + constructor (props) { + super(props) + this.utils = new Utils() + this.state = { + config: this.props.location.state.config, + startpoint: this.props.location.state.startpoint, + abstraction: [], + graphState: { + nodes: [], + links: [], + attr: [] + }, + formId: null, + resultsPreview: [], + headerPreview: [], + waiting: true, + error: false, + errorMessage: null, + saveTick: false, + + // Preview icons + disableSave: false, + saveIcon: "play", + } + + this.graphState = { + nodes: [], + links: [], + attr: [] + } + + this.idNumber = 0 + this.specialNodeIdNumber = 0 + this.previousSelected = null + this.currentSelected = null + this.cancelRequest + + this.handleSave = this.handleSave.bind(this) + + } + + toggleVisibility (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.visible = !attr.visible + if (!attr.visible) { + attr.optional = false + } + } + }) + this.updateGraphState() + } + + toggleExclude (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.exclude = !attr.exclude + if (attr.exclude) { + attr.visible = true + } + } + }) + this.updateGraphState() + } + + toggleOptional (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.optional = !attr.optional + if (attr.optional) { + attr.visible = true + } + } + }) + this.updateGraphState() + } + + handleNegative (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.negative = event.target.value == '=' ? false : true + } + }) + this.updateGraphState() + } + + handleFilterType (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filterType = event.target.value + } + }) + this.updateGraphState() + } + + handleFilterValue (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filterValue = event.target.value + } + }) + this.updateGraphState() + } + + handleFilterCategory (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filterSelectedValues = [...event.target.selectedOptions].map(o => o.value) + } + }) + this.updateGraphState() + } + + handleFilterNumericSign (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filters.map((filter, index) => { + if (index == event.target.dataset.index) { + filter.filterSign = event.target.value + } + }) + } + }) + this.updateGraphState() + } + + toggleAddNumFilter (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filters.push({ + filterValue: "", + filterSign: "=" + }) + } + }) + this.updateGraphState() + } + + handleFilterNumericValue (event) { + if (!isNaN(event.target.value)) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filters.map((filter, index) => { + if (index == event.target.dataset.index) { + filter.filterValue = event.target.value + } + }) + } + }) + this.updateGraphState() + } + } + + handleDateFilter (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filters.map((filter, index) => { + if (index == event.target.dataset.index) { + filter.filterSign = event.target.value + } + }) + } + }) + this.updateGraphState() + } + + toggleAddDateFilter (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filters.push({ + filterValue: null, + filterSign: "=" + }) + } + }) + this.updateGraphState() + } + + // This is a pain, but JS will auto convert time to UTC + // And datepicker use the local timezone + // So without this, the day sent will be wrong + fixTimezoneOffset (date){ + if(!date){return null}; + return new Date(date.getTime() - (date.getTimezoneOffset() * 60000)); + } + + setEntityName(event){ + this.graphState.attr.map(attr => { + if (attr.nodeId == event.target.id) { + attr.entityDisplayLabel = event.target.value + } + }) + this.updateGraphState() + } + + setAttributeName(event){ + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.displayLabel = event.target.value + } + }) + this.updateGraphState() + } + + handleFilterDateValue (event) { + if (!isNaN(event.target.value)) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.filters.map((filter, index) => { + if (index == event.target.dataset.index) { + filter.filterValue = this.fixTimezoneOffset(event.target.value) + } + }) + } + }) + this.updateGraphState() + } + } + + count_displayed_attributes() { + return this.graphState.attr.map(attr => { + return attr.visible ? 1 : 0 + }).reduce((a, b) => a + b) + } + + getAttributeType (typeUri) { + // FIXME: don't hardcode uri + if (typeUri == 'http://www.w3.org/2001/XMLSchema#decimal') { + return 'decimal' + } + if (typeUri == this.state.config.namespaceInternal + 'AskomicsCategory') { + return 'category' + } + if (typeUri == 'http://www.w3.org/2001/XMLSchema#string') { + return 'text' + } + if (typeUri == "http://www.w3.org/2001/XMLSchema#boolean") { + return "boolean" + } + if (typeUri == "http://www.w3.org/2001/XMLSchema#date") { + return "date" + } + } + + updateGraphState (waiting=this.state.waiting) { + this.setState({ + graphState: this.graphState, + previewIcon: "table", + resultsPreview: [], + headerPreview: [], + disableSave: false, + saveIcon: "play", + waiting: waiting + }) + } + + // Preview results and Launch query buttons ------- + handleSave (event) { + let requestUrl = '/api/results/save_form' + let data = { + graphState: this.graphState, + formId: this.formId + } + + // display an error message if user don't display attribute to avoid the virtuoso SPARQL error + if (this.count_displayed_attributes() == 0) { + this.setState({ + error: true, + errorMessage: ["No attribute are displayed. Use eye icon to display at least one attribute", ], + disableSave: false, + saveIcon: "times text-error" + }) + return + } + + axios.post(requestUrl, data, { baseURL: this.state.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) }) + .then(response => { + console.log(requestUrl, response.data) + this.setState({ + saveIcon: "check text-success", + disableSave: true + }) + }).catch(error => { + console.log(error, error.response.data.errorMessage) + this.setState({ + error: true, + errorMessage: error.response.data.errorMessage, + status: error.response.status, + saveIcon: "times text-error", + disableSave: false + }) + }) + } + + // ------------------------------------------------ + + componentDidMount () { + if (!this.props.waitForStart) { + if (! (this.props.location.state.formId && this.props.location.state.graphState)){ + redirectLogin = + } + + let requestUrl = '/api/query/abstraction' + axios.get(requestUrl, { baseURL: this.state.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) }) + .then(response => { + this.setState({ + waiting: false, + abstraction: response.data.abstraction, + diskSpace: response.data.diskSpace, + exceededQuota: this.state.config.user.quota > 0 && response.data.diskSpace >= this.state.config.user.quota, + }) + }) + .catch(error => { + console.log(error, error.response.data.errorMessage) + this.setState({ + error: true, + errorMessage: error.response.data.errorMessage, + status: error.response.status + }) + }).then(response => { + this.formId = this.props.location.state.formId + this.graphState = this.props.location.state.graphState + this.updateGraphState() + this.setState({ waiting: false }) + }) + } + } + + componentWillUnmount () { + if (!this.props.waitForStart) { + this.cancelRequest() + } + } + + render () { + // login page redirection + let redirectLogin + if (this.state.status == 401) { + redirectLogin = + } + + // error div + let errorDiv + if (this.state.error) { + errorDiv = ( +
+ + {this.state.errorMessage} + +
+ ) + } + + // Warning disk space + let warningDiskSpace + if (this.state.exceededQuota) { + warningDiskSpace = ( +
+ + Your files (uploaded files and results) take {this.utils.humanFileSize(this.state.diskSpace, true)} of space + (you have {this.utils.humanFileSize(this.state.config.user.quota, true)} allowed). + Please delete some before save queries or contact an admin to increase your quota + +
+ ) + } + + + let AttributeBoxes + let Entities = [] + let previewButton + let entityMap = new Map() + + 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.get(attribute.nodeId).attributes.push( + this.handleChangeLink(p)} + toggleVisibility={p => this.toggleVisibility(p)} + toggleExclude={p => this.toggleExclude(p)} + handleNegative={p => this.handleNegative(p)} + toggleOptional={p => this.toggleOptional(p)} + handleFilterType={p => this.handleFilterType(p)} + handleFilterValue={p => this.handleFilterValue(p)} + handleFilterCategory={p => this.handleFilterCategory(p)} + handleFilterNumericSign={p => this.handleFilterNumericSign(p)} + handleFilterNumericValue={p => this.handleFilterNumericValue(p)} + toggleLinkAttribute={p => this.toggleLinkAttribute(p)} + toggleAddNumFilter={p => this.toggleAddNumFilter(p)} + toggleAddDateFilter={p => this.toggleAddDateFilter(p)} + handleFilterDateValue={p => this.handleFilterDateValue(p)} + handleDateFilter={p => this.handleDateFilter(p)} + setAttributeName={p => this.setAttributeName(p)} + /> + ) + } + }) + } + + entityMap.forEach((value, key) => { + Entities.push( + this.setEntityName(p)} + entity_id={key} + entity={value.entity_label} + attribute_boxes={value.attributes} + /> + ) + }) + + // buttons + + + let saveButton = + + // preview + let resultsTable + if (this.state.headerPreview.length > 0) { + resultsTable = ( + + ) + } + + return ( +
+ {redirectLogin} +

Query Builder

+
+ +
+ + +
+ {Entities} +
+ +
+ {warningDiskSpace} +
+ + {saveButton} + +

+
+ {resultsTable} +
+ +
+ ) + } +} + +FormEditQuery.propTypes = { + location: PropTypes.object, + waitForStart: PropTypes.bool +} diff --git a/askomics/react/src/routes/query/attribute.jsx b/askomics/react/src/routes/query/attribute.jsx index de2b3417..c26e8d4a 100644 --- a/askomics/react/src/routes/query/attribute.jsx +++ b/askomics/react/src/routes/query/attribute.jsx @@ -18,6 +18,7 @@ export default class AttributeBox extends Component { this.toggleVisibility = this.props.toggleVisibility.bind(this) this.handleNegative = this.props.handleNegative.bind(this) + this.toggleFormAttribute = this.props.toggleFormAttribute.bind(this) this.toggleOptional = this.props.toggleOptional.bind(this) this.toggleExclude = this.props.toggleExclude.bind(this) this.handleFilterType = this.props.handleFilterType.bind(this) @@ -81,6 +82,12 @@ 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' @@ -124,6 +131,7 @@ export default class AttributeBox extends Component {
+ {this.props.config.user.admin ? : } : }
@@ -133,6 +141,12 @@ export default class AttributeBox extends Component { } renderText () { + + 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' @@ -199,6 +213,7 @@ export default class AttributeBox extends Component {
+ {this.props.config.user.admin ? : } {this.props.attribute.uri == "rdf:type" || this.props.attribute.uri == "rdfs:label" ? : } @@ -209,6 +224,12 @@ export default class AttributeBox extends Component { } renderNumeric () { + + 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' @@ -268,6 +289,7 @@ export default class AttributeBox extends Component {
+ {this.props.config.user.admin ? : } @@ -278,6 +300,12 @@ export default class AttributeBox extends Component { } renderCategory () { + + 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' @@ -319,6 +347,7 @@ export default class AttributeBox extends Component {
+ {this.props.config.user.admin ? : } @@ -330,6 +359,12 @@ export default class AttributeBox extends Component { } renderBoolean () { + + 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' @@ -364,6 +399,7 @@ export default class AttributeBox extends Component {
+ {this.props.config.user.admin ? : } @@ -374,6 +410,12 @@ export default class AttributeBox extends Component { } renderDate () { + + 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' @@ -400,8 +442,6 @@ export default class AttributeBox extends Component { let form let numberOfFilters = this.props.attribute.filters.length - 1 - - if (this.props.attribute.linked) { form = this.renderLinker() } else { @@ -419,9 +459,9 @@ export default class AttributeBox extends Component {
-
+ {this.props.config.user.admin ? : } @@ -478,6 +519,7 @@ AttributeBox.propTypes = { handleNegative: PropTypes.func, toggleVisibility: PropTypes.func, toggleOptional: PropTypes.func, + toggleFormAttribute: PropTypes.func, toggleExclude: PropTypes.func, toggleAddNumFilter: PropTypes.func, handleFilterType: PropTypes.func, @@ -492,4 +534,5 @@ AttributeBox.propTypes = { handleDateFilter: PropTypes.func, attribute: PropTypes.object, graph: PropTypes.object, + config: PropTypes.object } diff --git a/askomics/react/src/routes/query/query.jsx b/askomics/react/src/routes/query/query.jsx index c1a45459..66624e0b 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/query.jsx @@ -262,6 +262,7 @@ export default class Query extends Component { filterType: 'exact', filterValue: '', optional: false, + form: false, negative: false, linked: false, linkedWith: null @@ -284,6 +285,7 @@ export default class Query extends Component { filterType: 'exact', filterValue: '', optional: false, + form: false, negative: false, linked: false, linkedWith: null @@ -302,11 +304,14 @@ export default class Query extends Component { humanNodeId: this.getHumanIdFromId(nodeId), uri: attr.uri, label: attr.label, + displayLabel: attr.displayLabel ? attr.displayLabel : attr.label, entityLabel: this.getLabel(nodeUri), + entityDisplayLabel: attr.entityDisplayLabel ? attr.entityDisplayLabel : this.getLabel(nodeUri), entityUris: this.getEntityUris(attr.uri), type: attributeType, faldo: attr.faldo, optional: false, + form: false, negative: false, linked: false, linkedWith: null @@ -1015,6 +1020,15 @@ export default class Query extends Component { this.updateGraphState() } + toggleFormAttribute (event) { + this.graphState.attr.map(attr => { + if (attr.id == event.target.id) { + attr.form = !attr.form + } + }) + this.updateGraphState() + } + handleNegative (event) { this.graphState.attr.map(attr => { if (attr.id == event.target.id) { @@ -1124,7 +1138,6 @@ export default class Query extends Component { return new Date(date.getTime() - (date.getTimezoneOffset() * 60000)); } - handleFilterDateValue (event) { if (!isNaN(event.target.value)) { this.graphState.attr.map(attr => { @@ -1445,6 +1458,7 @@ export default class Query extends Component { toggleExclude={p => this.toggleExclude(p)} handleNegative={p => this.handleNegative(p)} toggleOptional={p => this.toggleOptional(p)} + toggleFormAttribute={p => this.toggleFormAttribute(p)} handleFilterType={p => this.handleFilterType(p)} handleFilterValue={p => this.handleFilterValue(p)} handleFilterCategory={p => this.handleFilterCategory(p)} @@ -1455,6 +1469,7 @@ export default class Query extends Component { toggleAddDateFilter={p => this.toggleAddDateFilter(p)} handleFilterDateValue={p => this.handleFilterDateValue(p)} handleDateFilter={p => this.handleDateFilter(p)} + config={this.state.config} /> ) } diff --git a/askomics/react/src/routes/results/resultsfilestable.jsx b/askomics/react/src/routes/results/resultsfilestable.jsx index 6b07f9fc..3bde0dd2 100644 --- a/askomics/react/src/routes/results/resultsfilestable.jsx +++ b/askomics/react/src/routes/results/resultsfilestable.jsx @@ -19,6 +19,7 @@ export default class ResultsFilesTable extends Component { super(props) this.state = { redirectQueryBuilder: false, + redirectForm: false, graphState: [], modal: false, idToPublish: null, @@ -35,10 +36,12 @@ export default class ResultsFilesTable extends Component { this.handlePreview = this.handlePreview.bind(this) this.handleDownload = this.handleDownload.bind(this) this.handleRedo = this.handleRedo.bind(this) + this.handleForm = this.handleForm.bind(this) this.handleEditQuery = this.handleEditQuery.bind(this) this.handleSendToGalaxy = this.handleSendToGalaxy.bind(this) this.togglePublicQuery = this.togglePublicQuery.bind(this) this.toggleTemplateQuery = this.toggleTemplateQuery.bind(this) + this.toggleFormTemplateQuery = this.toggleFormTemplateQuery.bind(this) this.handleClickError = this.handleClickError.bind(this) this.toggleModalTraceback = this.toggleModalTraceback.bind(this) } @@ -136,6 +139,32 @@ export default class ResultsFilesTable extends Component { }) } + handleForm(event) { + // request api to get a preview of file + let requestUrl = '/api/results/graphstate' + let fileId = event.target.id + let data = { fileId: fileId, formated: false } + axios.post(requestUrl, data, {baseURL: this.props.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) }) + .then(response => { + console.log(requestUrl, response.data) + // set state of resultsPreview + this.setState({ + redirectForm: true, + graphState: response.data.graphState, + idToPublish: fileId + }) + }) + .catch(error => { + console.log(error, error.response.data.errorMessage) + this.setState({ + error: true, + errorMessage: error.response.data.errorMessage, + status: error.response.status, + waiting: false + }) + }) + } + handleEditQuery (event) { let requestUrl = '/api/results/sparqlquery' let data = { fileId: event.target.id } @@ -195,6 +224,15 @@ export default class ResultsFilesTable extends Component { }) } + toggleFormTemplateQuery(event) { + this.setState({ + idToFormTemplate: parseInt(event.target.id.replace("form-template-", "")), + newFormTemplateStatus: event.target.value == 1 ? false : true + }, () => { + this.form() + }) + } + publish() { let requestUrl = '/api/results/publish' let data = { @@ -248,6 +286,32 @@ export default class ResultsFilesTable extends Component { }) } + form() { + let requestUrl = '/api/results/form' + let data = { + id: this.state.idToFormTemplate, + form: this.state.newFormTemplateStatus + } + axios.post(requestUrl, data, {baseURL: this.props.config.proxyPath, cancelToken: new axios.CancelToken((c) => { this.cancelRequest = c }) }) + .then(response => { + this.setState({ + idToPublish: null + }) + this.props.setStateResults({ + results: response.data.files, + waiting: false + }) + }) + .catch(error => { + this.setState({ + error: true, + errorMessage: error.response.data.errorMessage, + status: error.response.status, + waiting: false + }) + }) + } + saveNewDescription (oldValue, newValue, row) { if (newValue === oldValue) {return} @@ -310,6 +374,18 @@ export default class ResultsFilesTable extends Component { }} /> } + let redirectForm + if (this.state.redirectForm) { + redirectForm = + } + let redirectQueryBuilder if (this.state.redirectQueryBuilder) { redirectQueryBuilder = { + return ( + +
+ +
+
+ ) + }, + editable: false }, { dataField: 'public', text: 'Public', @@ -429,6 +520,7 @@ export default class ResultsFilesTable extends Component { + {this.props.config.user.admin === 1 ? : } {this.props.config.user.galaxy ? : null} @@ -459,7 +551,7 @@ export default class ResultsFilesTable extends Component { return (
- {redirectQueryBuilder}{redirectSparqlEditor} + {redirectQueryBuilder}{redirectSparqlEditor}{redirectForm} unpublic it + data_public = {"id": result_info["id"], "public": True} + data_form = {"id": result_info["id"], "form": False} + + with open("tests/results/results_form.json", "r") as file: + file_content = file.read() + raw_results = file_content.replace("###START###", str(result_info["start"])) + raw_results = raw_results.replace("###END###", str(result_info["end"])) + raw_results = raw_results.replace("###EXECTIME###", str(int(result_info["end"] - result_info["start"]))) + raw_results = raw_results.replace("###ID###", str(result_info["id"])) + raw_results = raw_results.replace("###PATH###", str(result_info["path"])) + raw_results = raw_results.replace("###SIZE###", str(result_info["size"])) + raw_results = raw_results.replace("###PUBLIC###", str(0)) + raw_results = raw_results.replace("###TEMPLATE###", str(0)) + raw_results = raw_results.replace("###FORM###", str(0)) + raw_results = raw_results.replace("###HAS_FORM_ATTR###", str(1)) + raw_results = raw_results.replace("###DESC###", "Query") + + expected = json.loads(raw_results) + del expected["triplestoreMaxRows"] + + client.client.post("/api/results/public", json=data_public) + response = client.client.post("/api/results/form", json=data_form) + + assert response.status_code == 200 + assert self.equal_objects(response.json, expected) + + # If template is on and form is toggled, un-toggle template + data_template = {"id": result_info["id"], "template": True} + client.client.post("/api/results/template", json=data_template) + response = client.client.post("/api/results/form", json=data) + + with open("tests/results/results_form.json", "r") as file: + file_content = file.read() + raw_results = file_content.replace("###START###", str(result_info["start"])) + raw_results = raw_results.replace("###END###", str(result_info["end"])) + raw_results = raw_results.replace("###EXECTIME###", str(int(result_info["end"] - result_info["start"]))) + raw_results = raw_results.replace("###ID###", str(result_info["id"])) + raw_results = raw_results.replace("###PATH###", str(result_info["path"])) + raw_results = raw_results.replace("###SIZE###", str(result_info["size"])) + raw_results = raw_results.replace("###PUBLIC###", str(0)) + raw_results = raw_results.replace("###TEMPLATE###", str(0)) + raw_results = raw_results.replace("###FORM###", str(1)) + raw_results = raw_results.replace("###HAS_FORM_ATTR###", str(1)) + raw_results = raw_results.replace("###DESC###", "Query") + + expected = json.loads(raw_results) + del expected["triplestoreMaxRows"] + + assert response.status_code == 200 + assert self.equal_objects(response.json, expected) + + def test_form_no_attr(self, client): + """test /api/results/form route""" + client.create_two_users() + client.log_user("jdoe") + client.upload_and_integrate() + result_info = client.create_result() + + data = {"id": result_info["id"], "form": True} + response = client.client.post("/api/results/form", json=data) + + expected = { + 'files': [], + 'error': True, + 'errorMessage': 'Failed to create form template query: \nThis query does not has any form template attribute' + } + + assert response.status_code == 500 + assert response.json == expected + + def test_form_non_admin(self, client): + """test /api/results/form route""" + client.create_two_users() + client.log_user("jsmith") + client.upload_and_integrate() + result_info = client.create_result(has_form=True) + + data = {"id": result_info["id"], "form": True} + + response = client.client.post("/api/results/form", json=data) + + assert response.status_code == 401 + def test_publish(self, client): """test /api/results/publish route""" client.create_two_users() @@ -268,6 +459,8 @@ def test_publish(self, client): raw_results = raw_results.replace("###SIZE###", str(result_info["size"])) raw_results = raw_results.replace("###PUBLIC###", str(1)) raw_results = raw_results.replace("###TEMPLATE###", str(1)) + raw_results = raw_results.replace("###FORM###", str(0)) + raw_results = raw_results.replace("###HAS_FORM_ATTR###", str(0)) raw_results = raw_results.replace("###DESC###", "Query") expected = json.loads(raw_results) @@ -290,6 +483,8 @@ def test_publish(self, client): raw_results = raw_results.replace("###SIZE###", str(result_info["size"])) raw_results = raw_results.replace("###PUBLIC###", str(0)) raw_results = raw_results.replace("###TEMPLATE###", str(1)) + raw_results = raw_results.replace("###FORM###", str(0)) + raw_results = raw_results.replace("###HAS_FORM_ATTR###", str(0)) raw_results = raw_results.replace("###DESC###", "Query") expected = json.loads(raw_results) @@ -300,6 +495,34 @@ def test_publish(self, client): assert response.status_code == 200 assert response.json == expected + def test_update_form(self, client): + """test /api/results/save_form route""" + client.create_two_users() + client.log_user("jdoe") + client.upload_and_integrate() + result_info = client.create_result(has_form=True) + + with open("tests/data/graphState_simple_query_form_modified.json", "r") as file: + file_content = file.read() + body = json.loads(file_content) + data = {"formId": result_info["id"], "graphState": body} + + response = client.client.post("/api/results/save_form", json=data) + + assert response.status_code == 200 + + response = client.client.get('/api/results') + assert response.status_code == 200 + + res = json.loads(response.json["files"][0]["graphState"]) + + assert self.equal_objects(res, body) + + client.log_user("jsmith") + response = client.client.post("/api/results/save_form", json=data) + + assert response.status_code == 401 + def test_send2galaxy(self, client): """test /api/results/send2galaxy route""" client.create_two_users() From fd5edb0c6ec3fcd0a0f186148a770a65cf8b74e8 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 10 Jun 2021 17:46:06 +0200 Subject: [PATCH 054/318] Release 4.3.0 (#231) --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ package-lock.json | 2 +- package.json | 2 +- setup.py | 2 +- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7330494..e50fbb2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This changelog was started for release 4.2.0. +## [4.3.0] - 2021-06-10 + +### Added + +- Added 'Date' entity type, with associated Date picker in UI +- Added API-key authentication for most endpoints. The api key should be passed with the header "X-API-KEY". +- Added CLI (using token-auth) (https://github.com/askomics/askoclics). Still a WIP, with the python package 'askoclics'. +- "Not" filter for categories +- URI management in first column (and link column). Manage both full URI and CURIE. Check #223 for details. +- 'Forms' : Minimal templates (users only access a basic form for modifying parameters, and not the graph) Restricted to admins. Form creators can customized entities and attributes display names to improve usability. + +### Changed + +- Faldo entity "Strand" now default to "faldo:BothStrandPosition" when the value is empty. The label will be "unknown/both" for CSV. For GFF and BED, "." will be "faldo:BothStrandPosition" instead of being ignored. +- If one of the column name of a CSV file is empty, raise an Exception. +- Now return the created result id (instead of celery task id) in the sparql query endpoint +- Now return the created file id (instead of celery task id) in the create file endpoint +- Fixed Flask version to < 2.0.0 due to compatibility issues + + +### Fixed + +- Fixed the console restriction to admin/users (was not fully functional) +- Fixed an issue with spaces (and other characters) in URIs +- Fixed an issue with "Optional" button when using categories (and faldo entities) (either wrong values or nothing showing up) (Cf Changed category) +- Fixed table ordering in results for numerical values (they were managed as strings) +- Fixed UNION and MINUS blocks +- Fixed an issue with Faldo "same strand" (clicking on the link between Faldo nodes) +- Fixed Node/Link filter issue when using values with caps. + +### Security + +- Bump hosted-git-info from 2.8.8 to 2.8.9 + ## [4.2.2] - 2021-06-09 ### Fixed diff --git a/package-lock.json b/package-lock.json index 5b16cf66..0082a991 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "AskOmics", - "version": "4.2.2", + "version": "4.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 163e8419..e1faebdc 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "ontology" ], "name": "AskOmics", - "version": "4.2.2", + "version": "4.3.0", "description": "Visual SPARQL query builder", "author": "Xavier Garnier", "license": "AGPL-3.0", diff --git a/setup.py b/setup.py index 8d793f05..fe529d1e 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='askomics', - version='4.2.2', + version='4.3.0', description=''' AskOmics is a visual SPARQL query interface supporting both intuitive data integration and querying while shielding the user from most of the From 63f9a12c1a537e8c3cbeda01787cf7e91e38c555 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Jun 2021 18:27:25 +0000 Subject: [PATCH 055/318] Bump postcss from 7.0.35 to 7.0.36 Bumps [postcss](https://github.com/postcss/postcss) from 7.0.35 to 7.0.36. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/7.0.35...7.0.36) --- updated-dependencies: - dependency-name: postcss dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0082a991..b459b680 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8204,9 +8204,9 @@ "dev": true }, "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", "requires": { "chalk": "^2.4.2", "source-map": "^0.6.1", From 5939f429d17cadfae2d7ed3a74e34d99aeaa3091 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 15 Jun 2021 23:01:48 +0200 Subject: [PATCH 056/318] Fix Category issue --- askomics/libaskomics/SparqlQuery.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index dfa27095..d3cbfa78 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -1280,20 +1280,17 @@ def build_query_from_json(self, preview=False, for_editor=False): # values if attribute["filterSelectedValues"] != [] and not attribute["optional"] and not attribute["linked"]: uri_val_list = [] - for value in attribute["filterSelectedValues"]: - if attribute["faldo"] and attribute["faldo"].endswith("faldoStrand"): - value_var = faldo_strand - uri_val_list.append("<{}>".format(value)) + if attribute["faldo"] and attribute["faldo"].endswith("faldoStrand"): + value_var = faldo_strand + else: + value_var = category_value_uri + uri_val_list = ["<{}>".format(value) for value in attribute["filterSelectedValues"] + if uri_val_list: + if attribute["exclude"]: + filter_string = "FILTER ( {} NOT IN ( {} ) ) .".format(value_var, " ,".join(uri_val_list)) + self.store_filter(filter_string, block_id, sblock_id, pblock_ids) else: - value_var = category_value_uri - uri_val_list.append("<{}>".format(value)) - - if uri_val_list: - if attribute["exclude"]: - filter_string = "FILTER ( {} NOT IN ( {} ) ) .".format(value_var, " ,".join(uri_val_list)) - self.store_filter(filter_string, block_id, sblock_id, pblock_ids) - else: - self.store_value("VALUES {} {{ {} }}".format(value_var, ' '.join(uri_val_list)), block_id, sblock_id, pblock_ids) + self.store_value("VALUES {} {{ {} }}".format(value_var, ' '.join(uri_val_list)), block_id, sblock_id, pblock_ids) if attribute["linked"]: var_2 = self.format_sparql_variable("{}{}_{}Category".format( From 3c332beb3335c7239b8195a4f8527a0b90e1bf03 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 15 Jun 2021 23:02:56 +0200 Subject: [PATCH 057/318] Typo --- askomics/libaskomics/SparqlQuery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index d3cbfa78..ae098b87 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -1284,7 +1284,7 @@ def build_query_from_json(self, preview=False, for_editor=False): value_var = faldo_strand else: value_var = category_value_uri - uri_val_list = ["<{}>".format(value) for value in attribute["filterSelectedValues"] + uri_val_list = ["<{}>".format(value) for value in attribute["filterSelectedValues"]] if uri_val_list: if attribute["exclude"]: filter_string = "FILTER ( {} NOT IN ( {} ) ) .".format(value_var, " ,".join(uri_val_list)) From 1e8c616ee74fb9dd1327574dedec779397c96fa7 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 15 Jun 2021 23:28:16 +0200 Subject: [PATCH 058/318] Fix Gff import issue (parents) --- askomics/libaskomics/GffFile.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index 70804fc7..4c327f01 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -165,6 +165,13 @@ def generate_rdf_content(self): for rec in GFF.parse(handle, limit_info=limit, target_lines=1): + feature_dict = {} + for feature in rec.features: + if feature.id: + feature_dict[feature.id] = feature.type + elif "ID" in feature.qualifiers.keys(): + feature_dict[feature.qualifiers["ID"][0]] = feature.type + # Percent row_number += 1 self.graph_chunk.percent = row_number * 100 / total_lines @@ -300,17 +307,27 @@ def generate_rdf_content(self): for value in qualifier_value: if qualifier_key in ("Parent", "Derives_from"): + if len(value.split(":")) == 1: + # The entity is not in the value, try to detect it + related_type = value + related_qualifier_key = qualifier_key + if value in feature_dict: + related_type = feature_dict[value] + else: + related_type = value.split(":")[0] + related_qualifier_key = qualifier_key + "_" + related_type + relation = self.namespace_data[self.format_uri(qualifier_key)] attribute = self.namespace_data[self.format_uri(self.format_gff_entity(value))] - if (feature.type, qualifier_key) not in attribute_list: - attribute_list.append((feature.type, qualifier_key)) + if (feature.type, related_qualifier_key) not in attribute_list: + attribute_list.append((feature.type, related_qualifier_key)) self.attribute_abstraction.append({ "uri": self.namespace_data[self.format_uri(qualifier_key)], "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(value.split(":")[0])] + "range": self.namespace_data[self.format_uri(related_type)] }) else: From 7ce833d373f7f8f1f7dd40f9bab5a9c1e0af44ab Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 15 Jun 2021 23:31:58 +0200 Subject: [PATCH 059/318] Issue --- askomics/libaskomics/GffFile.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index 4c327f01..8a580208 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -309,14 +309,16 @@ def generate_rdf_content(self): if qualifier_key in ("Parent", "Derives_from"): if len(value.split(":")) == 1: # The entity is not in the value, try to detect it - related_type = value - related_qualifier_key = qualifier_key if value in feature_dict: related_type = feature_dict[value] + + else: + continue else: related_type = value.split(":")[0] related_qualifier_key = qualifier_key + "_" + related_type + related_qualifier_key = qualifier_key + "_" + related_type relation = self.namespace_data[self.format_uri(qualifier_key)] attribute = self.namespace_data[self.format_uri(self.format_gff_entity(value))] From d71f8640ec6a14985602eebd69a0c01d233486b7 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 15 Jun 2021 23:33:26 +0200 Subject: [PATCH 060/318] Issue2 --- askomics/libaskomics/GffFile.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index 8a580208..e9710a0b 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -311,12 +311,10 @@ def generate_rdf_content(self): # The entity is not in the value, try to detect it if value in feature_dict: related_type = feature_dict[value] - else: continue else: related_type = value.split(":")[0] - related_qualifier_key = qualifier_key + "_" + related_type related_qualifier_key = qualifier_key + "_" + related_type relation = self.namespace_data[self.format_uri(qualifier_key)] From a3c674ff2f535c6c80da6712df00cd53d472aca6 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 15 Jun 2021 23:53:32 +0200 Subject: [PATCH 061/318] Format entity --- askomics/libaskomics/GffFile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index e9710a0b..d69d9da6 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -168,9 +168,9 @@ def generate_rdf_content(self): feature_dict = {} for feature in rec.features: if feature.id: - feature_dict[feature.id] = feature.type + feature_dict[self.format_gff_entity(feature.id)] = feature.type elif "ID" in feature.qualifiers.keys(): - feature_dict[feature.qualifiers["ID"][0]] = feature.type + feature_dict[self.format_gff_entity(feature.qualifiers["ID"][0])] = feature.type # Percent row_number += 1 From f8f2d9262e50d9bab18c009bb5636d49c0d4ee20 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 16 Jun 2021 07:31:09 +0000 Subject: [PATCH 062/318] Fix logic to avoid loop --- askomics/libaskomics/GffFile.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index d69d9da6..1b525ca4 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -162,16 +162,11 @@ def generate_rdf_content(self): total_lines = sum(1 for line in open(self.path)) row_number = 0 + feature_dict = {} + delayed_link = [] for rec in GFF.parse(handle, limit_info=limit, target_lines=1): - feature_dict = {} - for feature in rec.features: - if feature.id: - feature_dict[self.format_gff_entity(feature.id)] = feature.type - elif "ID" in feature.qualifiers.keys(): - feature_dict[self.format_gff_entity(feature.qualifiers["ID"][0])] = feature.type - # Percent row_number += 1 self.graph_chunk.percent = row_number * 100 / total_lines @@ -203,9 +198,11 @@ def generate_rdf_content(self): else: entity = self.namespace_entity[self.format_uri(self.format_gff_entity(feature.qualifiers["ID"][0]))] entity_label = self.format_gff_entity(feature.qualifiers["ID"][0]) + feature_dict[self.format_gff_entity(feature.qualifiers["ID"][0])] = feature.type else: entity = self.namespace_entity[self.format_uri(self.format_gff_entity(feature.id))] entity_label = self.format_gff_entity(feature.id) + feature_dict[self.format_gff_entity(feature.id)] = feature.type self.graph_chunk.add((entity, rdflib.RDF.type, entity_type)) self.graph_chunk.add((entity, rdflib.RDFS.label, rdflib.Literal(entity_label))) @@ -312,7 +309,16 @@ def generate_rdf_content(self): if value in feature_dict: related_type = feature_dict[value] else: - continue + # Do this later + delayed_link.append( + { + "uri": self.namespace_data[self.format_uri(qualifier_key)], + "label": rdflib.Literal(qualifier_key), + "type": [rdflib.OWL.ObjectProperty, self.namespace_internal[self.format_uri("AskomicsRelation")]], + "domain": entity_type, + "range": value + } + ) else: related_type = value.split(":")[0] @@ -346,6 +352,11 @@ def generate_rdf_content(self): self.graph_chunk.add((entity, relation, attribute)) + for link in delayed_link: + if link["range"] in feature_dict: + link['range'] = feature_dict[link['range']] + self.attribute_abstraction.append(link) + location = BNode() begin = BNode() end = BNode() From 35ea5c96597bfc398bf77eaebd6902db54fbd604 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 16 Jun 2021 07:51:27 +0000 Subject: [PATCH 063/318] Lint --- askomics/libaskomics/GffFile.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index 1b525ca4..6187fc16 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -310,15 +310,13 @@ def generate_rdf_content(self): related_type = feature_dict[value] else: # Do this later - delayed_link.append( - { + delayed_link.append({ "uri": self.namespace_data[self.format_uri(qualifier_key)], "label": rdflib.Literal(qualifier_key), "type": [rdflib.OWL.ObjectProperty, self.namespace_internal[self.format_uri("AskomicsRelation")]], "domain": entity_type, "range": value - } - ) + }) else: related_type = value.split(":")[0] From 27ddba7c74686dd324c48c0e58c6d02fd1fbcf9a Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 16 Jun 2021 11:16:37 +0200 Subject: [PATCH 064/318] Move logic after --- askomics/libaskomics/GffFile.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index 6187fc16..85080027 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -302,12 +302,14 @@ def generate_rdf_content(self): for qualifier_key, qualifier_value in feature.qualifiers.items(): for value in qualifier_value: + skip = False if qualifier_key in ("Parent", "Derives_from"): if len(value.split(":")) == 1: # The entity is not in the value, try to detect it if value in feature_dict: related_type = feature_dict[value] + related_qualifier_key = qualifier_key + "_" + related_type else: # Do this later delayed_link.append({ @@ -315,16 +317,19 @@ 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": value + "range": value, + "qualifier_key": qualifier_key, + "feature_type": feature.type }) + skip = True else: related_type = value.split(":")[0] + related_qualifier_key = qualifier_key + "_" + related_type - related_qualifier_key = qualifier_key + "_" + related_type relation = self.namespace_data[self.format_uri(qualifier_key)] attribute = self.namespace_data[self.format_uri(self.format_gff_entity(value))] - if (feature.type, related_qualifier_key) not in attribute_list: + if not skip and (feature.type, related_qualifier_key) not in attribute_list: attribute_list.append((feature.type, related_qualifier_key)) self.attribute_abstraction.append({ "uri": self.namespace_data[self.format_uri(qualifier_key)], @@ -350,11 +355,6 @@ def generate_rdf_content(self): self.graph_chunk.add((entity, relation, attribute)) - for link in delayed_link: - if link["range"] in feature_dict: - link['range'] = feature_dict[link['range']] - self.attribute_abstraction.append(link) - location = BNode() begin = BNode() end = BNode() @@ -389,3 +389,13 @@ def generate_rdf_content(self): self.graph_chunk.add((entity, self.namespace_internal["includeInReference"], block_reference)) yield + + # Add missing abstractions + for link in delayed_link: + if link["range"] in feature_dict: + entity_type = feature_dict[link['range']] + qualifier_key = link.pop("qualifier_key") + "_" + entity_type + feature_type = link.pop("feature_type") + if (feature_type, related_qualifier_key) not in attribute_list: + link['range'] = entity_type + self.attribute_abstraction.append(link) From fa1ebfa4d6048eba3c8a768515ba707006feb3e7 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 16 Jun 2021 09:50:00 +0000 Subject: [PATCH 065/318] Fixes --- askomics/libaskomics/GffFile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index 85080027..102691b4 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -394,8 +394,8 @@ def generate_rdf_content(self): for link in delayed_link: if link["range"] in feature_dict: entity_type = feature_dict[link['range']] - qualifier_key = link.pop("qualifier_key") + "_" + entity_type + related_qualifier_key = link.pop("qualifier_key") + "_" + entity_type feature_type = link.pop("feature_type") if (feature_type, related_qualifier_key) not in attribute_list: - link['range'] = entity_type + link['range'] = self.namespace_data[self.format_uri(entity_type)] self.attribute_abstraction.append(link) From f099e85c7b955166a9eaa813e1f343e85fd039e9 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 16 Jun 2021 11:56:06 +0200 Subject: [PATCH 066/318] Changelog and release --- CHANGELOG.md | 9 ++++++++- package-lock.json | 2 +- package.json | 2 +- setup.py | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e50fbb2a..aaac2785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This changelog was started for release 4.2.0. +## [4.3.1] - 2021-06-16 + +### Fixed + +- Fixed an issue with categories +- Fixed an issue with GFF import + + ## [4.3.0] - 2021-06-10 ### Added @@ -26,7 +34,6 @@ This changelog was started for release 4.2.0. - Now return the created file id (instead of celery task id) in the create file endpoint - Fixed Flask version to < 2.0.0 due to compatibility issues - ### Fixed - Fixed the console restriction to admin/users (was not fully functional) diff --git a/package-lock.json b/package-lock.json index 0082a991..4da746c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "AskOmics", - "version": "4.3.0", + "version": "4.3.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e1faebdc..7f18fcc9 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "ontology" ], "name": "AskOmics", - "version": "4.3.0", + "version": "4.3.1", "description": "Visual SPARQL query builder", "author": "Xavier Garnier", "license": "AGPL-3.0", diff --git a/setup.py b/setup.py index fe529d1e..158a2a43 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='askomics', - version='4.3.0', + version='4.3.1', description=''' AskOmics is a visual SPARQL query interface supporting both intuitive data integration and querying while shielding the user from most of the From cbae26d74588764c31c7493c66c27f4cebb7fa43 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 16 Jun 2021 14:18:07 +0200 Subject: [PATCH 067/318] Tests --- test-data/gene.gff3 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/gene.gff3 b/test-data/gene.gff3 index e4b799fb..db501ae5 100644 --- a/test-data/gene.gff3 +++ b/test-data/gene.gff3 @@ -5,8 +5,8 @@ #!genome-date 2010-09 #!genome-build-accession GCA_000001735.1 #!genebuild-last-updated 2010-09 +1 tair transcript 3631 5899 . + . ID=transcript:AT1G01010.1;Parent=AT1G01010;Name=ANAC001;biotype=protein_coding;transcript_id=AT1G01010.1 1 tair gene 3631 5899 . + . ID=gene:AT1G01010;Name=NAC001;biotype=protein_coding;description=NAC domain-containing protein 1 [Source:UniProtKB/Swiss-Prot%3BAcc:Q0WV96];gene_id=AT1G01010;logic_name=tair -1 tair transcript 3631 5899 . + . ID=transcript:AT1G01010.1;Parent=gene:AT1G01010;Name=ANAC001;biotype=protein_coding;transcript_id=AT1G01010.1 1 tair five_prime_UTR 3631 3759 . + . Parent=transcript:AT1G01010.1 1 tair exon 3631 3913 . + . Parent=transcript:AT1G01010.1;Name=AT1G01010.1.exon1;constitutive=1;ensembl_end_phase=1;ensembl_phase=-1;exon_id=AT1G01010.1.exon1;rank=1 1 tair CDS 3760 3913 . + 0 ID=CDS:AT1G01010.1;Parent=transcript:AT1G01010.1;protein_id=AT1G01010.1 From e74817b6a21f67c19c6f567a069fd41756fa2954 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 16 Jun 2021 14:32:10 +0200 Subject: [PATCH 068/318] Revert for now --- test-data/gene.gff3 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/gene.gff3 b/test-data/gene.gff3 index db501ae5..e4b799fb 100644 --- a/test-data/gene.gff3 +++ b/test-data/gene.gff3 @@ -5,8 +5,8 @@ #!genome-date 2010-09 #!genome-build-accession GCA_000001735.1 #!genebuild-last-updated 2010-09 -1 tair transcript 3631 5899 . + . ID=transcript:AT1G01010.1;Parent=AT1G01010;Name=ANAC001;biotype=protein_coding;transcript_id=AT1G01010.1 1 tair gene 3631 5899 . + . ID=gene:AT1G01010;Name=NAC001;biotype=protein_coding;description=NAC domain-containing protein 1 [Source:UniProtKB/Swiss-Prot%3BAcc:Q0WV96];gene_id=AT1G01010;logic_name=tair +1 tair transcript 3631 5899 . + . ID=transcript:AT1G01010.1;Parent=gene:AT1G01010;Name=ANAC001;biotype=protein_coding;transcript_id=AT1G01010.1 1 tair five_prime_UTR 3631 3759 . + . Parent=transcript:AT1G01010.1 1 tair exon 3631 3913 . + . Parent=transcript:AT1G01010.1;Name=AT1G01010.1.exon1;constitutive=1;ensembl_end_phase=1;ensembl_phase=-1;exon_id=AT1G01010.1.exon1;rank=1 1 tair CDS 3760 3913 . + 0 ID=CDS:AT1G01010.1;Parent=transcript:AT1G01010.1;protein_id=AT1G01010.1 From e5f902ece4193df923c32ac8660d7bc17a586c5b Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 16 Jun 2021 14:59:30 +0200 Subject: [PATCH 069/318] Fix #244 #245 (#246) Fix #244 #245 (#246) --- CHANGELOG.md | 9 ++++++- askomics/libaskomics/GffFile.py | 42 ++++++++++++++++++++++++++--- askomics/libaskomics/SparqlQuery.py | 23 +++++++--------- package-lock.json | 2 +- package.json | 2 +- setup.py | 2 +- 6 files changed, 60 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e50fbb2a..aaac2785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This changelog was started for release 4.2.0. +## [4.3.1] - 2021-06-16 + +### Fixed + +- Fixed an issue with categories +- Fixed an issue with GFF import + + ## [4.3.0] - 2021-06-10 ### Added @@ -26,7 +34,6 @@ This changelog was started for release 4.2.0. - Now return the created file id (instead of celery task id) in the create file endpoint - Fixed Flask version to < 2.0.0 due to compatibility issues - ### Fixed - Fixed the console restriction to admin/users (was not fully functional) diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index 70804fc7..102691b4 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -162,6 +162,8 @@ def generate_rdf_content(self): total_lines = sum(1 for line in open(self.path)) row_number = 0 + feature_dict = {} + delayed_link = [] for rec in GFF.parse(handle, limit_info=limit, target_lines=1): @@ -196,9 +198,11 @@ def generate_rdf_content(self): else: entity = self.namespace_entity[self.format_uri(self.format_gff_entity(feature.qualifiers["ID"][0]))] entity_label = self.format_gff_entity(feature.qualifiers["ID"][0]) + feature_dict[self.format_gff_entity(feature.qualifiers["ID"][0])] = feature.type else: entity = self.namespace_entity[self.format_uri(self.format_gff_entity(feature.id))] entity_label = self.format_gff_entity(feature.id) + feature_dict[self.format_gff_entity(feature.id)] = feature.type self.graph_chunk.add((entity, rdflib.RDF.type, entity_type)) self.graph_chunk.add((entity, rdflib.RDFS.label, rdflib.Literal(entity_label))) @@ -298,19 +302,41 @@ def generate_rdf_content(self): for qualifier_key, qualifier_value in feature.qualifiers.items(): for value in qualifier_value: + skip = False if qualifier_key in ("Parent", "Derives_from"): + if len(value.split(":")) == 1: + # The entity is not in the value, try to detect it + if value in feature_dict: + related_type = feature_dict[value] + related_qualifier_key = qualifier_key + "_" + related_type + else: + # Do this later + delayed_link.append({ + "uri": self.namespace_data[self.format_uri(qualifier_key)], + "label": rdflib.Literal(qualifier_key), + "type": [rdflib.OWL.ObjectProperty, self.namespace_internal[self.format_uri("AskomicsRelation")]], + "domain": entity_type, + "range": value, + "qualifier_key": qualifier_key, + "feature_type": feature.type + }) + skip = True + else: + related_type = value.split(":")[0] + related_qualifier_key = qualifier_key + "_" + related_type + relation = self.namespace_data[self.format_uri(qualifier_key)] attribute = self.namespace_data[self.format_uri(self.format_gff_entity(value))] - if (feature.type, qualifier_key) not in attribute_list: - attribute_list.append((feature.type, qualifier_key)) + if not skip and (feature.type, related_qualifier_key) not in attribute_list: + attribute_list.append((feature.type, related_qualifier_key)) self.attribute_abstraction.append({ "uri": self.namespace_data[self.format_uri(qualifier_key)], "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(value.split(":")[0])] + "range": self.namespace_data[self.format_uri(related_type)] }) else: @@ -363,3 +389,13 @@ def generate_rdf_content(self): self.graph_chunk.add((entity, self.namespace_internal["includeInReference"], block_reference)) yield + + # Add missing abstractions + for link in delayed_link: + if link["range"] in feature_dict: + entity_type = feature_dict[link['range']] + related_qualifier_key = link.pop("qualifier_key") + "_" + entity_type + feature_type = link.pop("feature_type") + if (feature_type, related_qualifier_key) not in attribute_list: + link['range'] = self.namespace_data[self.format_uri(entity_type)] + self.attribute_abstraction.append(link) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index dfa27095..ae098b87 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -1280,20 +1280,17 @@ def build_query_from_json(self, preview=False, for_editor=False): # values if attribute["filterSelectedValues"] != [] and not attribute["optional"] and not attribute["linked"]: uri_val_list = [] - for value in attribute["filterSelectedValues"]: - if attribute["faldo"] and attribute["faldo"].endswith("faldoStrand"): - value_var = faldo_strand - uri_val_list.append("<{}>".format(value)) + if attribute["faldo"] and attribute["faldo"].endswith("faldoStrand"): + value_var = faldo_strand + else: + value_var = category_value_uri + uri_val_list = ["<{}>".format(value) for value in attribute["filterSelectedValues"]] + if uri_val_list: + if attribute["exclude"]: + filter_string = "FILTER ( {} NOT IN ( {} ) ) .".format(value_var, " ,".join(uri_val_list)) + self.store_filter(filter_string, block_id, sblock_id, pblock_ids) else: - value_var = category_value_uri - uri_val_list.append("<{}>".format(value)) - - if uri_val_list: - if attribute["exclude"]: - filter_string = "FILTER ( {} NOT IN ( {} ) ) .".format(value_var, " ,".join(uri_val_list)) - self.store_filter(filter_string, block_id, sblock_id, pblock_ids) - else: - self.store_value("VALUES {} {{ {} }}".format(value_var, ' '.join(uri_val_list)), block_id, sblock_id, pblock_ids) + self.store_value("VALUES {} {{ {} }}".format(value_var, ' '.join(uri_val_list)), block_id, sblock_id, pblock_ids) if attribute["linked"]: var_2 = self.format_sparql_variable("{}{}_{}Category".format( diff --git a/package-lock.json b/package-lock.json index 0082a991..4da746c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "AskOmics", - "version": "4.3.0", + "version": "4.3.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e1faebdc..7f18fcc9 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "ontology" ], "name": "AskOmics", - "version": "4.3.0", + "version": "4.3.1", "description": "Visual SPARQL query builder", "author": "Xavier Garnier", "license": "AGPL-3.0", diff --git a/setup.py b/setup.py index fe529d1e..158a2a43 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='askomics', - version='4.3.0', + version='4.3.1', description=''' AskOmics is a visual SPARQL query interface supporting both intuitive data integration and querying while shielding the user from most of the From 393c067a36ba8908c885f6c319eb198d5ea912c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jun 2021 18:50:05 +0000 Subject: [PATCH 070/318] Bump prismjs from 1.23.0 to 1.24.0 Bumps [prismjs](https://github.com/PrismJS/prism) from 1.23.0 to 1.24.0. - [Release notes](https://github.com/PrismJS/prism/releases) - [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md) - [Commits](https://github.com/PrismJS/prism/compare/v1.23.0...v1.24.0) --- updated-dependencies: - dependency-name: prismjs dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 19 +++++++++++++------ package.json | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4da746c6..44ccf4aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8327,12 +8327,9 @@ "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==" }, "prismjs": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz", - "integrity": "sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA==", - "requires": { - "clipboard": "^2.0.0" - } + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.24.0.tgz", + "integrity": "sha512-SqV5GRsNqnzCL8k5dfAjCNhUrF3pR0A9lTDSCUZeh/LIshheXJEaP0hwLz2t4XHivd2J/v2HR+gRnigzeKe3cQ==" }, "private": { "version": "0.1.8", @@ -8988,6 +8985,16 @@ "hastscript": "^6.0.0", "parse-entities": "^2.0.0", "prismjs": "~1.23.0" + }, + "dependencies": { + "prismjs": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz", + "integrity": "sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA==", + "requires": { + "clipboard": "^2.0.0" + } + } } }, "regenerate": { diff --git a/package.json b/package.json index 7f18fcc9..796274f0 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "immutability-helper": "^3.1.1", "js-file-download": "^0.4.12", "pretty-time": "^1.1.0", - "prismjs": "^1.23.0", + "prismjs": "^1.24.0", "qs": "^6.9.4", "react": "^16.13.1", "react-ace": "^9.1.3", From 65298b78db489c20e19df91251bf0af9de4aaccf Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 13 Jul 2021 14:25:10 +0200 Subject: [PATCH 071/318] Fix form issue with label & uris (#256) * Fix form --- askomics/react/src/routes/form/attribute.jsx | 31 +++++++++++-------- askomics/react/src/routes/form/query.jsx | 3 +- .../react/src/routes/form_edit/attribute.jsx | 29 ++++++++++------- askomics/react/src/routes/form_edit/query.jsx | 4 +-- askomics/react/src/routes/query/attribute.jsx | 2 +- askomics/react/src/routes/query/query.jsx | 4 +++ 6 files changed, 43 insertions(+), 30 deletions(-) diff --git a/askomics/react/src/routes/form/attribute.jsx b/askomics/react/src/routes/form/attribute.jsx index 7b59e403..07cf5108 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 = [] @@ -110,9 +121,8 @@ export default class AttributeBox extends Component { return (
- +
-
{form} @@ -186,9 +196,8 @@ export default class AttributeBox extends Component { return (
- +
- {this.props.attribute.uri == "rdf:type" || this.props.attribute.uri == "rdfs:label" ? : }
@@ -256,9 +265,8 @@ export default class AttributeBox extends Component { return (
- +
-
@@ -308,9 +316,8 @@ export default class AttributeBox extends Component { return (
- -
- + +
@@ -354,9 +361,8 @@ export default class AttributeBox extends Component { return (
- +
-
@@ -432,9 +438,8 @@ export default class AttributeBox extends Component { return (
- +
-
diff --git a/askomics/react/src/routes/form/query.jsx b/askomics/react/src/routes/form/query.jsx index 3b0fe52d..6189148f 100644 --- a/askomics/react/src/routes/form/query.jsx +++ b/askomics/react/src/routes/form/query.jsx @@ -383,7 +383,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 }) }) @@ -440,7 +439,7 @@ export default class FormQuery extends Component { 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( { + let code = char.charCodeAt() + newStr += String.fromCharCode(code + 8272) + }) + return newStr + } + renderLinker () { let options = [] @@ -110,9 +121,8 @@ export default class AttributeBox extends Component { return (
- +
-
{form} @@ -188,9 +198,8 @@ export default class AttributeBox extends Component { return (
- +
- {this.props.attribute.uri == "rdf:type" || this.props.attribute.uri == "rdfs:label" ? : }
@@ -258,9 +267,8 @@ export default class AttributeBox extends Component { return (
- +
-
@@ -310,9 +318,8 @@ export default class AttributeBox extends Component { return (
- +
- @@ -356,9 +363,8 @@ export default class AttributeBox extends Component { return (
- +
-
@@ -434,9 +440,8 @@ 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..67f4cea4 100644 --- a/askomics/react/src/routes/form_edit/query.jsx +++ b/askomics/react/src/routes/form_edit/query.jsx @@ -390,7 +390,7 @@ export default class FormEditQuery extends Component { 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( {redirectLogin} -

Query Builder

+

Form editor



diff --git a/askomics/react/src/routes/query/attribute.jsx b/askomics/react/src/routes/query/attribute.jsx index c26e8d4a..fd114943 100644 --- a/askomics/react/src/routes/query/attribute.jsx +++ b/askomics/react/src/routes/query/attribute.jsx @@ -131,7 +131,7 @@ export default class AttributeBox extends Component {
- {this.props.config.user.admin ? : } : } + {this.props.config.user.admin ? : }
diff --git a/askomics/react/src/routes/query/query.jsx b/askomics/react/src/routes/query/query.jsx index 66624e0b..b2f59083 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/query.jsx @@ -255,7 +255,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, @@ -278,7 +280,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, From b40e67676629c8454e918f55ceefac5be2e874ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Aug 2021 19:38:47 +0000 Subject: [PATCH 072/318] Bump tar from 6.1.0 to 6.1.3 Bumps [tar](https://github.com/npm/node-tar) from 6.1.0 to 6.1.3. - [Release notes](https://github.com/npm/node-tar/releases) - [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/npm/node-tar/compare/v6.1.0...v6.1.3) --- updated-dependencies: - dependency-name: tar dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index aaa650b8..5e80c18a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10020,9 +10020,9 @@ "dev": true }, "tar": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", - "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.3.tgz", + "integrity": "sha512-3rUqwucgVZXTeyJyL2jqtUau8/8r54SioM1xj3AmTX3HnWQdj2AydfJ2qYYayPyIIznSplcvU9mhBb7dR2XF3w==", "dev": true, "requires": { "chownr": "^2.0.0", From fc2df187058e649e7625b3df94ec83eba79af9c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Aug 2021 17:22:00 +0000 Subject: [PATCH 073/318] Bump @npmcli/git from 2.0.6 to 2.1.0 Bumps [@npmcli/git](https://github.com/npm/git) from 2.0.6 to 2.1.0. - [Release notes](https://github.com/npm/git/releases) - [Commits](https://github.com/npm/git/compare/v2.0.6...v2.1.0) --- updated-dependencies: - dependency-name: "@npmcli/git" dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index aaa650b8..9b4d6238 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1416,19 +1416,18 @@ "dev": true }, "@npmcli/git": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-2.0.6.tgz", - "integrity": "sha512-a1MnTfeRPBaKbFY07fd+6HugY1WAkKJzdiJvlRub/9o5xz2F/JtPacZZapx5zRJUQFIzSL677vmTSxEcDMrDbg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-2.1.0.tgz", + "integrity": "sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw==", "dev": true, "requires": { - "@npmcli/promise-spawn": "^1.1.0", + "@npmcli/promise-spawn": "^1.3.2", "lru-cache": "^6.0.0", - "mkdirp": "^1.0.3", - "npm-pick-manifest": "^6.0.0", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^6.1.1", "promise-inflight": "^1.0.1", "promise-retry": "^2.0.1", - "semver": "^7.3.2", - "unique-filename": "^1.1.1", + "semver": "^7.3.5", "which": "^2.0.2" }, "dependencies": { From 57b0cd3f6a1157be72d8a84351b2eb942c4cb31b Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 12 Aug 2021 13:39:00 +0200 Subject: [PATCH 074/318] Remove "Remote endpoint" form for non-ttl file (#263) Fix #254 --- askomics/api/file.py | 5 +++-- askomics/react/src/routes/integration/bedpreview.jsx | 1 + askomics/react/src/routes/integration/csvtable.jsx | 1 + askomics/react/src/routes/integration/gffpreview.jsx | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/askomics/api/file.py b/askomics/api/file.py index 62e9587e..663d0cfb 100644 --- a/askomics/api/file.py +++ b/askomics/api/file.py @@ -7,6 +7,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) @@ -318,8 +319,8 @@ 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["customUri"] = data["customUri"] if (data.get("customUri") and not isinstance(file, RdfFile)) else None dataset_info = { "celery_id": None, diff --git a/askomics/react/src/routes/integration/bedpreview.jsx b/askomics/react/src/routes/integration/bedpreview.jsx index 803ba86c..acbad90e 100644 --- a/askomics/react/src/routes/integration/bedpreview.jsx +++ b/askomics/react/src/routes/integration/bedpreview.jsx @@ -116,6 +116,7 @@ export default class BedPreview extends Component {
this.handleChangeUri(p)} handleChangeEndpoint={p => this.handleChangeEndpoint(p)} customUri={this.state.customUri} diff --git a/askomics/react/src/routes/integration/csvtable.jsx b/askomics/react/src/routes/integration/csvtable.jsx index f8c0547e..7ff737af 100644 --- a/askomics/react/src/routes/integration/csvtable.jsx +++ b/askomics/react/src/routes/integration/csvtable.jsx @@ -236,6 +236,7 @@ export default class CsvTable extends Component {
this.handleChangeUri(p)} handleChangeEndpoint={p => this.handleChangeEndpoint(p)} customUri={this.state.customUri} diff --git a/askomics/react/src/routes/integration/gffpreview.jsx b/askomics/react/src/routes/integration/gffpreview.jsx index 077ccf90..7a3bdddb 100644 --- a/askomics/react/src/routes/integration/gffpreview.jsx +++ b/askomics/react/src/routes/integration/gffpreview.jsx @@ -122,6 +122,7 @@ export default class GffPreview extends Component {
this.handleChangeUri(p)} handleChangeEndpoint={p => this.handleChangeEndpoint(p)} customUri={this.state.customUri} From fea683af79866985a28868fc909a37d4ec6efaaf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Aug 2021 13:39:29 +0200 Subject: [PATCH 075/318] Bump path-parse from 1.0.6 to 1.0.7 (#262) Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7. - [Release notes](https://github.com/jbgutierrez/path-parse/releases) - [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7) --- updated-dependencies: - dependency-name: path-parse dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56d33cab..ca575d54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8115,9 +8115,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "path-to-regexp": { From 2cae385d5fabeea2263c052672843b394f6edd5d Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 12 Aug 2021 13:46:11 +0200 Subject: [PATCH 076/318] Add 'scaff' to autodetect for reference type --- askomics/libaskomics/CsvFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 299a2965..28a4e675 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -192,7 +192,7 @@ def guess_column_type(self, values, header_index): return "general_relation" special_types = { - 'reference': ('chr', 'ref'), + 'reference': ('chr', 'ref', 'scaff'), 'strand': ('strand', ), 'start': ('start', 'begin'), 'end': ('end', 'stop'), From d35ed2ba8db8fe1f589357b4f1590ddb37acfa7a Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 13 Aug 2021 11:06:50 +0200 Subject: [PATCH 077/318] "Label" column type (#265) Fixes #221 --- askomics/api/file.py | 2 +- askomics/libaskomics/CsvFile.py | 23 ++++++++++++- .../react/src/routes/integration/csvtable.jsx | 32 +++++++++++++++++++ test-data/transcripts.tsv | 22 ++++++------- tests/__init__.py | 3 ++ tests/conftest.py | 2 +- tests/results/data.json | 2 +- tests/results/preview.json | 22 ++++++------- tests/results/preview_files.json | 32 +++++++++++++------ tests/results/sparql_preview.json | 22 ++++++------- tests/test_api_admin.py | 2 +- tests/test_api_data.py | 2 +- tests/test_api_file.py | 6 ++-- tests/test_api_query.py | 6 ++-- 14 files changed, 123 insertions(+), 55 deletions(-) diff --git a/askomics/api/file.py b/askomics/api/file.py index 663d0cfb..94f5377d 100644 --- a/askomics/api/file.py +++ b/askomics/api/file.py @@ -394,7 +394,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/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 28a4e675..ab8defd1 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -191,6 +191,10 @@ 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', 'scaff'), 'strand': ('strand', ), @@ -401,6 +405,10 @@ def set_rdf_abstraction(self): if index == 0: continue + # Skip label for second column + if self.columns_type[index] == "label" and index == 1: + continue + # Relation if self.columns_type[index] in ('general_relation', 'symetric_relation'): symetric_relation = True if self.columns_type[index] == 'symetric_relation' else False @@ -489,6 +497,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 +514,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 +541,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 diff --git a/askomics/react/src/routes/integration/csvtable.jsx b/askomics/react/src/routes/integration/csvtable.jsx index 7ff737af..81929947 100644 --- a/askomics/react/src/routes/integration/csvtable.jsx +++ b/askomics/react/src/routes/integration/csvtable.jsx @@ -91,6 +91,38 @@ export default class CsvTable extends Component { ) } + if (colIndex == 1) { + return ( +
+ + {colInput} + + + + + + + + + + + + + + + + + + + + + + + +
+ ) + } + return (
diff --git a/test-data/transcripts.tsv b/test-data/transcripts.tsv index 6117b805..87b82988 100644 --- a/test-data/transcripts.tsv +++ b/test-data/transcripts.tsv @@ -1,11 +1,11 @@ -transcript taxon featureName chromosomeName start end featureType strand biotype description date -AT3G10490 Arabidopsis_thaliana ANAC052 At3 3267835 3270883 gene plus protein_coding NAC_domain_containing_protein_52_[Source:TAIR%3BAcc:AT3G10490] 01/01/2000 -AT3G13660 Arabidopsis_thaliana DIR22 At3 4464908 4465586 gene plus protein_coding Dirigent_protein_22_[Source:UniProtKB/Swiss-Prot%3BAcc:Q66GI2] 02/01/2000 -AT3G51470 Arabidopsis_thaliana na At3 19097787 19099275 gene minus protein_coding Probable_protein_phosphatase_2C_47_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9SD02] 03/01/2000 -AT3G10460 Arabidopsis_thaliana na At3 3255800 3256439 gene plus protein_coding Plant_self-incompatibility_protein_S1_family_[Source:TAIR%3BAcc:AT3G10460] 04/01/2000 -AT3G22640 Arabidopsis_thaliana PAP85 At3 8011724 8013902 gene minus protein_coding cupin_family_protein_[Source:TAIR%3BAcc:AT3G22640] 05/01/2000 -AT1G33615 Arabidopsis_thaliana na At1 12193325 12194374 ncRNA_gene minus ncRNA other_RNA_[Source:TAIR%3BAcc:AT1G33615] 06/01/2000 -AT5G41905 Arabidopsis_thaliana MIR166E At5 16775524 16775658 miRNA_gene minus miRNA MIR166/MIR166E%3B_miRNA_[Source:TAIR%3BAcc:AT5G41905] 07/01/2000 -AT1G57800 Arabidopsis_thaliana ORTH3 At1 21408623 21412283 gene minus protein_coding E3_ubiquitin-protein_ligase_ORTHRUS_3_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9FVS2] 08/01/2000 -AT1G49500 Arabidopsis_thaliana na At1 18321295 18322284 gene minus protein_coding unknown_protein%3B_FUNCTIONS_IN:_molecular_function_unknown%3B_INVOLVED_IN:_biological_process_unknown%3B_LOCATED_IN:_endomembrane_system%3B_EXPRESSED_IN:_19_plant_structures%3B_EXPRESSED_DURING:_10_growth_stages%3B_BEST_Arabidopsis_thaliana_protein_match_is:_u_/.../_protein_(TAIR:AT3G19030.1)%3B_Has_24_Blast_hits_to_24_proteins_in_2_species:_Archae_-_0%3B_Bacteria_-_0%3B_Metazoa_-_0%3B_Fungi_-_0%3B_Plants_-_24%3B_Viruses_-_0%3B_Other_Eukaryotes_-_0_(source:_NCBI_BLink)._[Source:TAIR%3BAcc:AT1G49500] 09/01/2000 -AT5G35334 Arabidopsis_thaliana na At5 13537917 13538984 gene plus transposable_element transposable_element_gene_[Source:TAIR%3BAcc:AT5G35334] 10/01/2000 +transcript label taxon featureName chromosomeName start end featureType strand biotype description date +AT3G10490 label_AT3G10490 Arabidopsis_thaliana ANAC052 At3 3267835 3270883 gene plus protein_coding NAC_domain_containing_protein_52_[Source:TAIR%3BAcc:AT3G10490] 01/01/2000 +AT3G13660 label_AT3G13660 Arabidopsis_thaliana DIR22 At3 4464908 4465586 gene plus protein_coding Dirigent_protein_22_[Source:UniProtKB/Swiss-Prot%3BAcc:Q66GI2] 02/01/2000 +AT3G51470 label_AT3G51470 Arabidopsis_thaliana na At3 19097787 19099275 gene minus protein_coding Probable_protein_phosphatase_2C_47_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9SD02] 03/01/2000 +AT3G10460 label_AT3G10460 Arabidopsis_thaliana na At3 3255800 3256439 gene plus protein_coding Plant_self-incompatibility_protein_S1_family_[Source:TAIR%3BAcc:AT3G10460] 04/01/2000 +AT3G22640 label_AT3G22640 Arabidopsis_thaliana PAP85 At3 8011724 8013902 gene minus protein_coding cupin_family_protein_[Source:TAIR%3BAcc:AT3G22640] 05/01/2000 +AT1G33615 label_AT1G33615 Arabidopsis_thaliana na At1 12193325 12194374 ncRNA_gene minus ncRNA other_RNA_[Source:TAIR%3BAcc:AT1G33615] 06/01/2000 +AT5G41905 label_AT5G41905 Arabidopsis_thaliana MIR166E At5 16775524 16775658 miRNA_gene minus miRNA MIR166/MIR166E%3B_miRNA_[Source:TAIR%3BAcc:AT5G41905] 07/01/2000 +AT1G57800 label_AT1G57800 Arabidopsis_thaliana ORTH3 At1 21408623 21412283 gene minus protein_coding E3_ubiquitin-protein_ligase_ORTHRUS_3_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9FVS2] 08/01/2000 +AT1G49500 label_AT1G49500 Arabidopsis_thaliana na At1 18321295 18322284 gene minus protein_coding unknown_protein%3B_FUNCTIONS_IN:_molecular_function_unknown%3B_INVOLVED_IN:_biological_process_unknown%3B_LOCATED_IN:_endomembrane_system%3B_EXPRESSED_IN:_19_plant_structures%3B_EXPRESSED_DURING:_10_growth_stages%3B_BEST_Arabidopsis_thaliana_protein_match_is:_u_/.../_protein_(TAIR:AT3G19030.1)%3B_Has_24_Blast_hits_to_24_proteins_in_2_species:_Archae_-_0%3B_Bacteria_-_0%3B_Metazoa_-_0%3B_Fungi_-_0%3B_Plants_-_24%3B_Viruses_-_0%3B_Other_Eukaryotes_-_0_(source:_NCBI_BLink)._[Source:TAIR%3BAcc:AT1G49500] 09/01/2000 +AT5G35334 label_AT5G35334 Arabidopsis_thaliana na At5 13537917 13538984 gene plus transposable_element transposable_element_gene_[Source:TAIR%3BAcc:AT5G35334] 10/01/2000 diff --git a/tests/__init__.py b/tests/__init__.py index 87a99a2f..7a75d2e3 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -8,4 +8,7 @@ class AskomicsTestCase(): def equal_objects(obj1, obj2): """Compare 2 objects""" ddiff = DeepDiff(obj1, obj2, ignore_order=True, report_repetition=True) + if not ddiff == {}: + print(ddiff) + return True if ddiff == {} else False diff --git a/tests/conftest.py b/tests/conftest.py index 0ddcb52b..5394e6bd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -369,7 +369,7 @@ def upload_and_integrate(self): # integrate int_transcripts = self.integrate_file({ "id": 1, - "columns_type": ["start_entity", "category", "text", "reference", "start", "end", "category", "strand", "text", "text", "date"] + "columns_type": ["start_entity", "label", "category", "text", "reference", "start", "end", "category", "strand", "text", "text", "date"] }) int_de = self.integrate_file({ diff --git a/tests/results/data.json b/tests/results/data.json index 4d92ef58..fe526205 100644 --- a/tests/results/data.json +++ b/tests/results/data.json @@ -5,7 +5,7 @@ "predicat": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" }, { - "object": "AT3G10490", + "object": "label_AT3G10490", "predicat": "http://www.w3.org/2000/01/rdf-schema#label" }, { diff --git a/tests/results/preview.json b/tests/results/preview.json index a26074f0..a83d515b 100644 --- a/tests/results/preview.json +++ b/tests/results/preview.json @@ -7,37 +7,37 @@ "id": 1, "preview": [ { - "transcript1_Label": "AT1G57800" + "transcript1_Label": "label_AT1G57800" }, { - "transcript1_Label": "AT5G35334" + "transcript1_Label": "label_AT5G35334" }, { - "transcript1_Label": "AT3G10460" + "transcript1_Label": "label_AT3G10460" }, { - "transcript1_Label": "AT1G49500" + "transcript1_Label": "label_AT1G49500" }, { - "transcript1_Label": "AT3G10490" + "transcript1_Label": "label_AT3G10490" }, { - "transcript1_Label": "AT3G51470" + "transcript1_Label": "label_AT3G51470" }, { - "transcript1_Label": "AT5G41905" + "transcript1_Label": "label_AT5G41905" }, { - "transcript1_Label": "AT1G33615" + "transcript1_Label": "label_AT1G33615" }, { - "transcript1_Label": "AT3G22640" + "transcript1_Label": "label_AT3G22640" }, { - "transcript1_Label": "AT3G13660" + "transcript1_Label": "label_AT3G13660" }, { "transcript1_Label": "AT1G01010.1" } ] -} \ No newline at end of file +} diff --git a/tests/results/preview_files.json b/tests/results/preview_files.json index e38881d1..98336370 100644 --- a/tests/results/preview_files.json +++ b/tests/results/preview_files.json @@ -6,6 +6,7 @@ "data": { "columns_type": [ "start_entity", + "label", "text", "text", "reference", @@ -29,7 +30,8 @@ "strand": "plus", "taxon": "Arabidopsis_thaliana", "transcript": "AT3G10490", - "date": "01/01/2000" + "date": "01/01/2000", + "label": "label_AT3G10490" }, { "biotype": "protein_coding", @@ -42,7 +44,8 @@ "strand": "plus", "taxon": "Arabidopsis_thaliana", "transcript": "AT3G13660", - "date": "02/01/2000" + "date": "02/01/2000", + "label": "label_AT3G13660" }, { "biotype": "protein_coding", @@ -55,7 +58,8 @@ "strand": "minus", "taxon": "Arabidopsis_thaliana", "transcript": "AT3G51470", - "date": "03/01/2000" + "date": "03/01/2000", + "label": "label_AT3G51470" }, { "biotype": "protein_coding", @@ -68,7 +72,8 @@ "strand": "plus", "taxon": "Arabidopsis_thaliana", "transcript": "AT3G10460", - "date": "04/01/2000" + "date": "04/01/2000", + "label": "label_AT3G10460" }, { "biotype": "protein_coding", @@ -81,7 +86,8 @@ "strand": "minus", "taxon": "Arabidopsis_thaliana", "transcript": "AT3G22640", - "date": "05/01/2000" + "date": "05/01/2000", + "label": "label_AT3G22640" }, { "biotype": "ncRNA", @@ -94,7 +100,8 @@ "strand": "minus", "taxon": "Arabidopsis_thaliana", "transcript": "AT1G33615", - "date": "06/01/2000" + "date": "06/01/2000", + "label": "label_AT1G33615" }, { "biotype": "miRNA", @@ -107,7 +114,8 @@ "strand": "minus", "taxon": "Arabidopsis_thaliana", "transcript": "AT5G41905", - "date": "07/01/2000" + "date": "07/01/2000", + "label": "label_AT5G41905" }, { "biotype": "protein_coding", @@ -120,7 +128,8 @@ "strand": "minus", "taxon": "Arabidopsis_thaliana", "transcript": "AT1G57800", - "date": "08/01/2000" + "date": "08/01/2000", + "label": "label_AT1G57800" }, { "biotype": "protein_coding", @@ -133,7 +142,8 @@ "strand": "minus", "taxon": "Arabidopsis_thaliana", "transcript": "AT1G49500", - "date": "09/01/2000" + "date": "09/01/2000", + "label": "label_AT1G49500" }, { "biotype": "transposable_element", @@ -146,11 +156,13 @@ "strand": "plus", "taxon": "Arabidopsis_thaliana", "transcript": "AT5G35334", - "date": "10/01/2000" + "date": "10/01/2000", + "label": "label_AT5G35334" } ], "header": [ "transcript", + "label", "taxon", "featureName", "chromosomeName", diff --git a/tests/results/sparql_preview.json b/tests/results/sparql_preview.json index b372209c..e508fa85 100644 --- a/tests/results/sparql_preview.json +++ b/tests/results/sparql_preview.json @@ -1,37 +1,37 @@ { "data": [ { - "transcript1_Label": "AT3G13660" + "transcript1_Label": "label_AT3G13660" }, { - "transcript1_Label": "AT3G10460" + "transcript1_Label": "label_AT3G10460" }, { - "transcript1_Label": "AT3G51470" + "transcript1_Label": "label_AT3G51470" }, { - "transcript1_Label": "AT5G35334" + "transcript1_Label": "label_AT5G35334" }, { - "transcript1_Label": "AT3G10490" + "transcript1_Label": "label_AT3G10490" }, { - "transcript1_Label": "AT3G22640" + "transcript1_Label": "label_AT3G22640" }, { - "transcript1_Label": "AT1G57800" + "transcript1_Label": "label_AT1G57800" }, { - "transcript1_Label": "AT1G49500" + "transcript1_Label": "label_AT1G49500" }, { - "transcript1_Label": "AT1G33615" + "transcript1_Label": "label_AT1G33615" }, { - "transcript1_Label": "AT5G41905" + "transcript1_Label": "label_AT5G41905" } ], "header": [ "transcript1_Label" ] -} \ No newline at end of file +} diff --git a/tests/test_api_admin.py b/tests/test_api_admin.py index efaf3030..4fef7c93 100644 --- a/tests/test_api_admin.py +++ b/tests/test_api_admin.py @@ -68,7 +68,7 @@ def test_get_files(self, client): 'date': info["transcripts"]["upload"]["file_date"], 'id': 1, 'name': 'transcripts.tsv', - 'size': 2102, + 'size': 2268, 'type': 'csv/tsv', 'user': 'jsmith' diff --git a/tests/test_api_data.py b/tests/test_api_data.py index 9693f11c..92d25e26 100644 --- a/tests/test_api_data.py +++ b/tests/test_api_data.py @@ -63,7 +63,7 @@ def test_public_access(self, client): client.integrate_file({ "id": 1, - "columns_type": ["start_entity", "category", "text", "reference", "start", "end", "category", "strand", "text", "text", "date"] + "columns_type": ["start_entity", "label", "category", "text", "reference", "start", "end", "category", "strand", "text", "text", "date"] }, public=True) with open("tests/results/data.json", "r") as file: diff --git a/tests/test_api_file.py b/tests/test_api_file.py index e27468d8..f5510b05 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -25,7 +25,7 @@ def test_get_files(self, client): 'date': info["transcripts"]["upload"]["file_date"], 'id': 1, 'name': 'transcripts.tsv', - 'size': 2102, + 'size': 2268, 'type': 'csv/tsv' }, { 'date': info["de"]["upload"]["file_date"], @@ -73,7 +73,7 @@ def test_get_files(self, client): 'date': info["transcripts"]["upload"]["file_date"], 'id': 1, 'name': 'transcripts.tsv', - 'size': 2102, + 'size': 2268, 'type': 'csv/tsv' }] } @@ -105,7 +105,7 @@ def test_edit_file(self, client): 'date': info["transcripts"]["upload"]["file_date"], 'id': 1, 'name': 'new name.tsv', - 'size': 2102, + 'size': 2268, 'type': 'csv/tsv' }, { 'date': info["de"]["upload"]["file_date"], diff --git a/tests/test_api_query.py b/tests/test_api_query.py index 9a1cdc54..7a863d1b 100644 --- a/tests/test_api_query.py +++ b/tests/test_api_query.py @@ -153,7 +153,7 @@ def test_get_preview(self, client): client.log_user("jdoe") response = client.client.post('/api/query/preview', json=data) - expected = {'error': False, 'errorMessage': '', 'headerPreview': ['transcript1_Label'], 'resultsPreview': [{'transcript1_Label': 'AT1G57800'}, {'transcript1_Label': 'AT5G35334'}, {'transcript1_Label': 'AT3G10460'}, {'transcript1_Label': 'AT1G49500'}, {'transcript1_Label': 'AT3G10490'}, {'transcript1_Label': 'AT3G51470'}, {'transcript1_Label': 'AT5G41905'}, {'transcript1_Label': 'AT1G33615'}, {'transcript1_Label': 'AT3G22640'}, {'transcript1_Label': 'AT3G13660'}, {'transcript1_Label': 'AT1G01010.1'}]} + expected = {'error': False, 'errorMessage': '', 'headerPreview': ['transcript1_Label'], 'resultsPreview': [{'transcript1_Label': 'label_AT1G57800'}, {'transcript1_Label': 'label_AT5G35334'}, {'transcript1_Label': 'label_AT3G10460'}, {'transcript1_Label': 'label_AT1G49500'}, {'transcript1_Label': 'label_AT3G10490'}, {'transcript1_Label': 'label_AT3G51470'}, {'transcript1_Label': 'label_AT5G41905'}, {'transcript1_Label': 'label_AT1G33615'}, {'transcript1_Label': 'label_AT3G22640'}, {'transcript1_Label': 'label_AT3G13660'}, {'transcript1_Label': 'AT1G01010.1'}]} # print(response.json) @@ -171,7 +171,7 @@ def test_get_preview(self, client): "graphState": json_query, } response = client.client.post('/api/query/preview', json=data) - expected = {'error': False, 'errorMessage': '', 'headerPreview': ['transcript1_Label'], 'resultsPreview': [{'transcript1_Label': 'AT3G10490'}]} + expected = {'error': False, 'errorMessage': '', 'headerPreview': ['transcript1_Label'], 'resultsPreview': [{'transcript1_Label': 'label_AT3G10490'}]} assert response.status_code == 200 assert self.equal_objects(response.json, expected) @@ -197,7 +197,7 @@ def test_get_preview(self, client): "graphState": json_query, } response = client.client.post('/api/query/preview', json=data) - expected = {'error': False, 'errorMessage': '', 'headerPreview': ['transcript1_Label'], 'resultsPreview': [{'transcript1_Label': 'AT3G10490'}]} + expected = {'error': False, 'errorMessage': '', 'headerPreview': ['transcript1_Label'], 'resultsPreview': [{'transcript1_Label': 'label_AT3G10490'}]} assert response.status_code == 200 assert self.equal_objects(response.json, expected) From c455714eeef1464a2ca4ae56998ae3b54df0733f Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 23 Aug 2021 10:00:57 +0200 Subject: [PATCH 078/318] Add button to hide faldo relations (#269) --- .../react/src/routes/query/graphfilters.js | 8 +++-- askomics/react/src/routes/query/query.jsx | 34 ++++++++++++++++--- 2 files changed, 34 insertions(+), 8 deletions(-) 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/query.jsx b/askomics/react/src/routes/query/query.jsx index b2f59083..9ea51258 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/query.jsx @@ -1,6 +1,6 @@ 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' @@ -40,7 +40,7 @@ export default class Query extends Component { // Preview icons disablePreview: false, - previewIcon: "table" + previewIcon: "table", } this.graphState = { @@ -50,6 +50,8 @@ export default class Query extends Component { } this.divHeight = 650 + this.showFaldo = true; + this.idNumber = 0 this.specialNodeIdNumber = 0 @@ -62,6 +64,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() { @@ -570,7 +573,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() @@ -987,6 +990,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 => { @@ -1444,6 +1458,7 @@ export default class Query extends Component { let AttributeBoxes let linkView let previewButton + let faldoButton let launchQueryButton let removeButton let graphFilters @@ -1535,6 +1550,12 @@ export default class Query extends Component { ) } + faldoButton = ( +
+ +
+ ) + // Filters graphFilters = ( Query Builder
- + {graphFilters} - + + {faldoButton} + + {removeButton} From 78535cf9c6d9fadc2d9a8620abb4edb97118f840 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Aug 2021 16:37:45 +0000 Subject: [PATCH 079/318] Bump tar from 6.1.3 to 6.1.11 Bumps [tar](https://github.com/npm/node-tar) from 6.1.3 to 6.1.11. - [Release notes](https://github.com/npm/node-tar/releases) - [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/npm/node-tar/compare/v6.1.3...v6.1.11) --- updated-dependencies: - dependency-name: tar dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca575d54..59add023 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10019,9 +10019,9 @@ "dev": true }, "tar": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.3.tgz", - "integrity": "sha512-3rUqwucgVZXTeyJyL2jqtUau8/8r54SioM1xj3AmTX3HnWQdj2AydfJ2qYYayPyIIznSplcvU9mhBb7dR2XF3w==", + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", "dev": true, "requires": { "chownr": "^2.0.0", From 67107ffe0ee62f877575829ee5c438a123a0a642 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 8 Sep 2021 14:44:57 +0200 Subject: [PATCH 080/318] Fixes #248 (Overhaul the integration of relations) (#268) Fixes #248 --- askomics/libaskomics/CsvFile.py | 21 ++++++++++++--- askomics/libaskomics/GffFile.py | 30 ++++++++++++++++----- askomics/libaskomics/TriplestoreExplorer.py | 25 +++++++++-------- 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index ab8defd1..e7ecd448 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -418,7 +418,23 @@ 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) + 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))) + 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 # Category elif self.columns_type[index] in ('category', 'reference', 'strand'): @@ -460,9 +476,6 @@ def set_rdf_abstraction(self): 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)) # Faldo: if self.faldo_entity: diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index 102691b4..1d39c2f4 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -109,11 +109,25 @@ def set_rdf_abstraction_domain_knowledge(self): 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"])) + # New way of storing relations (starting from 4.4.0) + if attribute.get("relation"): + blank = BNode() + 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: + 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"])) # Domain Knowledge if "values" in attribute.keys(): @@ -319,7 +333,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 +351,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/TriplestoreExplorer.py b/askomics/libaskomics/TriplestoreExplorer.py index 50557c6c..0773ceaf 100644 --- a/askomics/libaskomics/TriplestoreExplorer.py +++ b/askomics/libaskomics/TriplestoreExplorer.py @@ -513,24 +513,26 @@ 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 ?node ?node_type ?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 ?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 +544,16 @@ 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"]) + if "node" in result: + # Retrocompatibility + property_uri = result.get("property_uri", result["node"]) + 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"], From 0aa6f6d32498a547062aa5c0511d5a22899da54a Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 16 Sep 2021 11:55:18 +0200 Subject: [PATCH 081/318] Fix #274 (#275) target="_blank" on links --- askomics/react/src/routes/data/data.jsx | 8 ++++---- askomics/react/src/routes/integration/csvtable.jsx | 2 +- askomics/react/src/routes/sparql/resultstable.jsx | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/askomics/react/src/routes/data/data.jsx b/askomics/react/src/routes/data/data.jsx index 1b23cbc2..28f49547 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) @@ -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}

-
+
{ 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) } 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 }, From b01893ce5d5a36d881d61cdc10904a0046f13865 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 16 Sep 2021 17:27:31 +0200 Subject: [PATCH 082/318] Temp --- askomics/api/file.py | 15 +++++++++++++-- askomics/libaskomics/Database.py | 11 +++++++++++ askomics/libaskomics/FilesHandler.py | 9 ++++++--- askomics/tasks.py | 15 +++++++++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/askomics/api/file.py b/askomics/api/file.py index 94f5377d..3bc5d2da 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 @@ -189,8 +190,18 @@ def upload_url(): }), 400 try: - files = FilesHandler(current_app, session) - files.download_url(data["url"]) + 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 = r.headers.get('Content-length') + disk_space + if session["user"]["quota"] > 0 and 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({ diff --git a/askomics/libaskomics/Database.py b/askomics/libaskomics/Database.py index 5afadeae..fdb11840 100644 --- a/askomics/libaskomics/Database.py +++ b/askomics/libaskomics/Database.py @@ -350,6 +350,17 @@ def create_files_table(self): ''' self.execute_sql_query(query) + query = ''' + ALTER TABLE files + ADD status text NULL + DEFAULT(null) + ''' + + try: + self.execute_sql_query(query) + except Exception: + pass + def create_abstraction_table(self): """Create abstraction table""" query = """ diff --git a/askomics/libaskomics/FilesHandler.py b/askomics/libaskomics/FilesHandler.py index 54bc495c..64d36c9e 100644 --- a/askomics/libaskomics/FilesHandler.py +++ b/askomics/libaskomics/FilesHandler.py @@ -203,7 +203,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"): """Store the file info in the database Parameters @@ -216,6 +216,8 @@ 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) """ file_path = "{}/{}".format(self.upload_path, file_name) @@ -228,6 +230,7 @@ def store_file_info_in_db(self, name, filetype, file_name, size): ?, ?, ?, + ?, ? ) ''' @@ -248,7 +251,7 @@ 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)) + database.execute_sql_query(query, (self.session['user']['id'], name, filetype, file_path, size, self.date, status)) def persist_chunk(self, chunk_info): """Persist a file by chunk. Store info in db if the chunk is the last @@ -316,7 +319,7 @@ def download_url(self, url): file.write(req.content) # insert in db - self.store_file_info_in_db(name, "", file_name, os.path.getsize(path)) + self.store_file_info_in_db(name, "", file_name, os.path.getsize(path), "downloading") def get_type(self, file_ext): """Get files type, based on extension diff --git a/askomics/tasks.py b/askomics/tasks.py index e0059151..0bdefaed 100644 --- a/askomics/tasks.py +++ b/askomics/tasks.py @@ -288,3 +288,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) From 51bd1e644429654aabd4938b66e625a16c20dc97 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 17 Sep 2021 15:03:42 +0200 Subject: [PATCH 083/318] Temp(2) --- askomics/libaskomics/Database.py | 15 ++- askomics/libaskomics/FilesHandler.py | 99 ++++++++++++++++--- .../react/src/routes/upload/filestable.jsx | 14 +++ tests/test_api_admin.py | 15 ++- tests/test_api_file.py | 57 +++++++---- 5 files changed, 158 insertions(+), 42 deletions(-) diff --git a/askomics/libaskomics/Database.py b/askomics/libaskomics/Database.py index fdb11840..db12c179 100644 --- a/askomics/libaskomics/Database.py +++ b/askomics/libaskomics/Database.py @@ -352,8 +352,19 @@ def create_files_table(self): query = ''' ALTER TABLE files - ADD status text NULL - DEFAULT(null) + 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: diff --git a/askomics/libaskomics/FilesHandler.py b/askomics/libaskomics/FilesHandler.py index 64d36c9e..0cd2a93b 100644 --- a/askomics/libaskomics/FilesHandler.py +++ b/askomics/libaskomics/FilesHandler.py @@ -89,7 +89,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 +101,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 +112,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 +126,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 +143,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 +158,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 +205,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, status="available"): + 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 @@ -218,6 +220,10 @@ def store_file_info_in_db(self, name, filetype, file_name, size, status="availab Size of file status: string Status of the file (downloading, available, unavailable) + Returns + ------- + str + file id """ file_path = "{}/{}".format(self.upload_path, file_name) @@ -231,6 +237,7 @@ def store_file_info_in_db(self, name, filetype, file_name, size, status="availab ?, ?, ?, + ?, ? ) ''' @@ -251,7 +258,55 @@ def store_file_info_in_db(self, name, filetype, file_name, size, status="availab self.date = int(time.time()) - database.execute_sql_query(query, (self.session['user']['id'], name, filetype, file_path, size, self.date, status)) + database.execute_sql_query(query, (self.session['user']['id'], name, filetype, file_path, size, self.date, status, task_id)) + return database.lastrowid + + def update_file_info(self, file_id, size="", 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 or status or task_id): + return + + query_vars = [file_id] + database = Database(self.app, self.session) + + size_query = "" + status_query = "" + task_query = "" + + if size: + size_query = "size=?" + query_vars.append(size) + + if status: + size_query = "status=?" + query_vars.append(status) + + if task_id: + task_query = "task_id=?" + query_vars.append(task_id) + + query = ''' + UPDATE files SET + {}, + {}, + {}, + WHERE id=? + '''.format(size_query, status_query, task_query) + + database.execute_sql_query(query, query_vars) def persist_chunk(self, chunk_info): """Persist a file by chunk. Store info in db if the chunk is the last @@ -300,7 +355,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 @@ -312,14 +367,26 @@ 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), "downloading") + 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 + if count == 100: + 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/react/src/routes/upload/filestable.jsx b/askomics/react/src/routes/upload/filestable.jsx index 7b399c19..3a50dd27 100644 --- a/askomics/react/src/routes/upload/filestable.jsx +++ b/askomics/react/src/routes/upload/filestable.jsx @@ -88,6 +88,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/tests/test_api_admin.py b/tests/test_api_admin.py index 4fef7c93..a5805619 100644 --- a/tests/test_api_admin.py +++ b/tests/test_api_admin.py @@ -70,7 +70,8 @@ def test_get_files(self, client): 'name': 'transcripts.tsv', 'size': 2268, 'type': 'csv/tsv', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }, { 'date': info["de"]["upload"]["file_date"], @@ -78,7 +79,8 @@ def test_get_files(self, client): 'name': 'de.tsv', 'size': 819, 'type': 'csv/tsv', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }, { 'date': info["qtl"]["upload"]["file_date"], @@ -86,7 +88,8 @@ def test_get_files(self, client): 'name': 'qtl.tsv', 'size': 99, 'type': 'csv/tsv', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }, { 'date': info["gene"]["upload"]["file_date"], @@ -94,7 +97,8 @@ def test_get_files(self, client): 'name': 'gene.gff3', 'size': 2267, 'type': 'gff/gff3', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], @@ -102,7 +106,8 @@ def test_get_files(self, client): 'name': 'gene.bed', 'size': 689, 'type': 'bed', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }] } diff --git a/tests/test_api_file.py b/tests/test_api_file.py index f5510b05..d48c383f 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -26,31 +26,36 @@ def test_get_files(self, client): 'id': 1, 'name': 'transcripts.tsv', 'size': 2268, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["de"]["upload"]["file_date"], 'id': 2, 'name': 'de.tsv', 'size': 819, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["qtl"]["upload"]["file_date"], 'id': 3, 'name': 'qtl.tsv', 'size': 99, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["gene"]["upload"]["file_date"], 'id': 4, 'name': 'gene.gff3', 'size': 2267, - 'type': 'gff/gff3' + 'type': 'gff/gff3', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], 'id': 5, 'name': 'gene.bed', 'size': 689, - 'type': 'bed' + 'type': 'bed', + 'status': 'available' }] } @@ -74,7 +79,8 @@ def test_get_files(self, client): 'id': 1, 'name': 'transcripts.tsv', 'size': 2268, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }] } @@ -84,7 +90,8 @@ def test_get_files(self, client): 'diskSpace': client.get_size_occupied_by_user(), 'error': False, 'errorMessage': '', - 'files': [] + 'files': [], + 'status': 'available' } def test_edit_file(self, client): @@ -106,31 +113,36 @@ def test_edit_file(self, client): 'id': 1, 'name': 'new name.tsv', 'size': 2268, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["de"]["upload"]["file_date"], 'id': 2, 'name': 'de.tsv', 'size': 819, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["qtl"]["upload"]["file_date"], 'id': 3, 'name': 'qtl.tsv', 'size': 99, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["gene"]["upload"]["file_date"], 'id': 4, 'name': 'gene.gff3', 'size': 2267, - 'type': 'gff/gff3' + 'type': 'gff/gff3', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], 'id': 5, 'name': 'gene.bed', 'size': 689, - 'type': 'bed' + 'type': 'bed', + 'status': 'available' }] } @@ -427,25 +439,29 @@ def test_delete_files(self, client): 'id': 2, 'name': 'de.tsv', 'size': 819, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["qtl"]["upload"]["file_date"], 'id': 3, 'name': 'qtl.tsv', 'size': 99, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["gene"]["upload"]["file_date"], 'id': 4, 'name': 'gene.gff3', 'size': 2267, - 'type': 'gff/gff3' + 'type': 'gff/gff3', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], 'id': 5, 'name': 'gene.bed', 'size': 689, - 'type': 'bed' + 'type': 'bed', + 'status': 'available' }] } @@ -459,19 +475,22 @@ def test_delete_files(self, client): 'id': 3, 'name': 'qtl.tsv', 'size': 99, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["gene"]["upload"]["file_date"], 'id': 4, 'name': 'gene.gff3', 'size': 2267, - 'type': 'gff/gff3' + 'type': 'gff/gff3', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], 'id': 5, 'name': 'gene.bed', 'size': 689, - 'type': 'bed' + 'type': 'bed', + 'status': 'available' }] } From 2c6677e2781a68bae2e9677dcb6a47384722d92e Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 17 Sep 2021 15:22:59 +0200 Subject: [PATCH 084/318] Test? --- askomics/libaskomics/Database.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/askomics/libaskomics/Database.py b/askomics/libaskomics/Database.py index db12c179..7ff4b59c 100644 --- a/askomics/libaskomics/Database.py +++ b/askomics/libaskomics/Database.py @@ -358,7 +358,8 @@ def create_files_table(self): try: self.execute_sql_query(query) - except Exception: + except Exception as e: + raise e pass query = ''' @@ -369,7 +370,8 @@ def create_files_table(self): try: self.execute_sql_query(query) - except Exception: + except Exception as e: + raise e pass def create_abstraction_table(self): From e7db535ce71c450487e342ebf497b5e621b00ccf Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 17 Sep 2021 14:12:52 +0000 Subject: [PATCH 085/318] Tmp(3) --- askomics/api/file.py | 2 +- askomics/libaskomics/Database.py | 8 +++----- askomics/libaskomics/FilesHandler.py | 3 +-- tests/test_api_admin.py | 9 ++++++--- tests/test_api_file.py | 4 +++- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/askomics/api/file.py b/askomics/api/file.py index 3bc5d2da..caf3b2b3 100644 --- a/askomics/api/file.py +++ b/askomics/api/file.py @@ -193,7 +193,7 @@ def upload_url(): 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 = r.headers.get('Content-length') + disk_space + total_size = int(r.headers.get('Content-length')) + disk_space if session["user"]["quota"] > 0 and total_size >= session["user"]["quota"]: return jsonify({ 'errorMessage': "File will exceed quota", diff --git a/askomics/libaskomics/Database.py b/askomics/libaskomics/Database.py index 7ff4b59c..16b138f0 100644 --- a/askomics/libaskomics/Database.py +++ b/askomics/libaskomics/Database.py @@ -353,13 +353,12 @@ def create_files_table(self): query = ''' ALTER TABLE files ADD status text NOT NULL - DEFAULT("available") + DEFAULT('available') ''' try: self.execute_sql_query(query) - except Exception as e: - raise e + except Exception: pass query = ''' @@ -370,8 +369,7 @@ def create_files_table(self): try: self.execute_sql_query(query) - except Exception as e: - raise e + except Exception: pass def create_abstraction_table(self): diff --git a/askomics/libaskomics/FilesHandler.py b/askomics/libaskomics/FilesHandler.py index 0cd2a93b..4f868e37 100644 --- a/askomics/libaskomics/FilesHandler.py +++ b/askomics/libaskomics/FilesHandler.py @@ -258,8 +258,7 @@ def store_file_info_in_db(self, name, filetype, file_name, size, status="availab self.date = int(time.time()) - database.execute_sql_query(query, (self.session['user']['id'], name, filetype, file_path, size, self.date, status, task_id)) - return database.lastrowid + 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="", status="", task_id=""): """Update file size and status diff --git a/tests/test_api_admin.py b/tests/test_api_admin.py index a5805619..db190f14 100644 --- a/tests/test_api_admin.py +++ b/tests/test_api_admin.py @@ -442,7 +442,8 @@ def test_delete_files(self, client): 'name': 'qtl.tsv', 'size': 99, 'type': 'csv/tsv', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }, { 'date': info["gene"]["upload"]["file_date"], @@ -450,7 +451,8 @@ def test_delete_files(self, client): 'name': 'gene.gff3', 'size': 2267, 'type': 'gff/gff3', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], @@ -458,7 +460,8 @@ def test_delete_files(self, client): 'name': 'gene.bed', 'size': 689, 'type': 'bed', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }] } diff --git a/tests/test_api_file.py b/tests/test_api_file.py index d48c383f..97401a20 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -1,6 +1,7 @@ import json import os import random +import time from . import AskomicsTestCase @@ -91,7 +92,6 @@ def test_get_files(self, client): 'error': False, 'errorMessage': '', 'files': [], - 'status': 'available' } def test_edit_file(self, client): @@ -333,6 +333,8 @@ def test_upload_url(self, client): "errorMessage": "" } + time.sleep(10) + response = client.client.get("/api/files") assert response.status_code == 200 assert len(response.json["files"]) == 1 From 341ccd8496e53a5da2ece624e674cfad4f53e10b Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 17 Sep 2021 15:43:36 +0000 Subject: [PATCH 086/318] typos --- askomics/libaskomics/FilesHandler.py | 27 +++++++++++-------- .../react/src/routes/upload/filestable.jsx | 1 + askomics/tasks.py | 2 +- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/askomics/libaskomics/FilesHandler.py b/askomics/libaskomics/FilesHandler.py index 4f868e37..6084b87a 100644 --- a/askomics/libaskomics/FilesHandler.py +++ b/askomics/libaskomics/FilesHandler.py @@ -260,7 +260,7 @@ def store_file_info_in_db(self, name, filetype, file_name, size, status="availab 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="", status="", task_id=""): + def update_file_info(self, file_id, size=None, status="", task_id=""): """Update file size and status Parameters @@ -275,37 +275,40 @@ def update_file_info(self, file_id, size="", status="", task_id=""): Current task id """ - if not (size or status or task_id): + if not (size is not None or status or task_id): return - query_vars = [file_id] + query_vars = [] database = Database(self.app, self.session) size_query = "" status_query = "" task_query = "" - if size: - size_query = "size=?" + # 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: - size_query = "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, query_vars) + 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 @@ -367,6 +370,7 @@ def download_url(self, url, task_id): 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 try: with requests.get(url, stream=True) as r: @@ -384,7 +388,8 @@ def download_url(self, url, task_id): # Update final value self.update_file_info(file_id, size=os.path.getsize(path), status="available") - except Exception: + except Exception as e: + raise e self.update_file_info(file_id, size=os.path.getsize(path), status="error") def get_type(self, file_ext): diff --git a/askomics/react/src/routes/upload/filestable.jsx b/askomics/react/src/routes/upload/filestable.jsx index 3a50dd27..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' diff --git a/askomics/tasks.py b/askomics/tasks.py index 0bdefaed..e0f3b1c9 100644 --- a/askomics/tasks.py +++ b/askomics/tasks.py @@ -302,4 +302,4 @@ def download_file(self, session, url): New user """ files = FilesHandler(app, session) - files.download_url(url) + files.download_url(url, download_file.request.id) From 451b5c4b1d0a73e2a2627c3a716f4fffe280dfc7 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 20 Sep 2021 10:33:59 +0200 Subject: [PATCH 087/318] Test tests --- tests/conftest.py | 18 ++++++++++++++++++ tests/test_api_file.py | 27 +++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5394e6bd..fb30c607 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -273,6 +273,24 @@ def upload_file(self, file_path): "file_date": filedate } + def upload_file_url(self, file_url): + """Summary + + Parameters + ---------- + file_path : TYPE + Description + + Returns + ------- + TYPE + Description + """ + + files = FilesHandler(self.app, self.session) + files.download_url(file_url, "1") + return files.date + def integrate_file(self, info, public=False): """Summary diff --git a/tests/test_api_file.py b/tests/test_api_file.py index 97401a20..59356245 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -1,7 +1,6 @@ import json import os import random -import time from . import AskomicsTestCase @@ -94,6 +93,29 @@ def test_get_files(self, client): 'files': [], } + def test_get_files_upload(self, client): + """test the /api/files route after an url upload""" + client.create_two_users() + client.log_user("jdoe") + date = client.upload_file_url("https://raw.githubusercontent.com/askomics/demo-data/master/Example/gene.tsv") + + response = client.client.get('/api/files') + + assert response.status_code == 200 + assert response.json == { + 'diskSpace': client.get_size_occupied_by_user(), + 'error': False, + 'errorMessage': '', + 'files': [{ + 'date': date, + 'id': 1, + 'name': 'gene.tsv', + 'size': 369, + 'type': 'csv/tsv', + 'status': 'available' + }] + } + def test_edit_file(self, client): """Test /api/files/editname route""" client.create_two_users() @@ -333,11 +355,8 @@ def test_upload_url(self, client): "errorMessage": "" } - time.sleep(10) - response = client.client.get("/api/files") assert response.status_code == 200 - assert len(response.json["files"]) == 1 def test_get_preview(self, client): """Test /api/files/preview route""" From e64f934d917c9f6da25bd7ffcb00aa5aabcc7ea1 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 20 Sep 2021 12:19:48 +0200 Subject: [PATCH 088/318] Check quota during upload --- askomics/api/file.py | 19 ++++++++++--------- askomics/libaskomics/FilesHandler.py | 9 +++++++-- .../react/src/routes/upload/uploadurlform.jsx | 6 +++--- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/askomics/api/file.py b/askomics/api/file.py index caf3b2b3..45be76a0 100644 --- a/askomics/api/file.py +++ b/askomics/api/file.py @@ -190,15 +190,16 @@ def upload_url(): }), 400 try: - 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 session["user"]["quota"] > 0 and total_size >= session["user"]["quota"]: - return jsonify({ - 'errorMessage': "File will exceed quota", - "error": True - }), 400 + 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"])) diff --git a/askomics/libaskomics/FilesHandler.py b/askomics/libaskomics/FilesHandler.py index 6084b87a..ae5e8bb8 100644 --- a/askomics/libaskomics/FilesHandler.py +++ b/askomics/libaskomics/FilesHandler.py @@ -379,17 +379,22 @@ def download_url(self, url, task_id): 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 as e: - raise e + except Exception: self.update_file_info(file_id, size=os.path.getsize(path), status="error") def get_type(self, file_ext): 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}
- +
From c729fe83148cc5f87f130f93d5125b6f5f0432fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Sep 2021 20:59:00 +0000 Subject: [PATCH 089/318] Bump prismjs from 1.24.0 to 1.25.0 Bumps [prismjs](https://github.com/PrismJS/prism) from 1.24.0 to 1.25.0. - [Release notes](https://github.com/PrismJS/prism/releases) - [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md) - [Commits](https://github.com/PrismJS/prism/compare/v1.24.0...v1.25.0) --- updated-dependencies: - dependency-name: prismjs dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59add023..36ea5d52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8326,9 +8326,9 @@ "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==" }, "prismjs": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.24.0.tgz", - "integrity": "sha512-SqV5GRsNqnzCL8k5dfAjCNhUrF3pR0A9lTDSCUZeh/LIshheXJEaP0hwLz2t4XHivd2J/v2HR+gRnigzeKe3cQ==" + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz", + "integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==" }, "private": { "version": "0.1.8", diff --git a/package.json b/package.json index 796274f0..f5f0a32e 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "immutability-helper": "^3.1.1", "js-file-download": "^0.4.12", "pretty-time": "^1.1.0", - "prismjs": "^1.24.0", + "prismjs": "^1.25.0", "qs": "^6.9.4", "react": "^16.13.1", "react-ace": "^9.1.3", From 8c64eec4e0c58ab34306c47e766516a8944b1d0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Sep 2021 14:22:02 +0000 Subject: [PATCH 090/318] Bump axios from 0.21.1 to 0.21.2 Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.2. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.2) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 36ea5d52..520b88ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2228,11 +2228,11 @@ "dev": true }, "axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.2.tgz", + "integrity": "sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==", "requires": { - "follow-redirects": "^1.10.0" + "follow-redirects": "^1.14.0" } }, "babel-code-frame": { @@ -5341,9 +5341,9 @@ } }, "follow-redirects": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", - "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==" + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==" }, "for-in": { "version": "1.0.2", diff --git a/package.json b/package.json index f5f0a32e..54b3bbe7 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@fortawesome/free-brands-svg-icons": "^5.14.0", "@fortawesome/free-regular-svg-icons": "^5.14.0", "@sentry/browser": "^5.24.2", - "axios": "^0.21.1", + "axios": "^0.21.2", "babel-preset-env": "^1.7.0", "bootstrap": "^4.5.2", "css-loader": "^4.3.0", From 9d3a7d8aabf676f33fd41a3d30e2df5c1a1b356d Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 27 Sep 2021 10:40:04 +0200 Subject: [PATCH 091/318] Test fix FALDO --- askomics/api/data.py | 9 +--- askomics/libaskomics/SparqlQuery.py | 70 +++++++++++++++++++++++++ askomics/react/src/routes/data/data.jsx | 2 +- 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/askomics/api/data.py b/askomics/api/data.py index 2e92d875..32b1cc2d 100644 --- a/askomics/api/data.py +++ b/askomics/api/data.py @@ -40,14 +40,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/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index ae098b87..b0ba68f0 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -444,6 +444,76 @@ def set_graphs_and_endpoints(self, entities=None, graphs=None, endpoints=None): 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 ?predicat ?object ?faldo_value ?faldo_uri + WHERE { + ?URI ?predicat ?object . + ?URI a ?entitytype . + + FILTER(! STRSTARTS(STR(?predicat), 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 . + }} + + VALUES ?predicat {faldo:location} + }} + VALUES ?URI {{{}}} + }} + '''.format(uri) + + federated = self.is_federated() + replace_froms = self.replace_froms() + + 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: + formated_data.append({ + 'predicate': row['faldo_uri '] if row['faldo_uri'] else row['predicate'], + 'object': row['faldo_value'] if row['faldo_value'] else row['object'], + }) + + return formated_data + def format_sparql_variable(self, name): """Format a name into a sparql variable by remove spacial char and add a ? diff --git a/askomics/react/src/routes/data/data.jsx b/askomics/react/src/routes/data/data.jsx index 28f49547..18b41092 100644 --- a/askomics/react/src/routes/data/data.jsx +++ b/askomics/react/src/routes/data/data.jsx @@ -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) => { From 3d08445712531e7cde24667f63b30b034f4851ba Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 27 Sep 2021 09:31:14 +0000 Subject: [PATCH 092/318] Fixes --- askomics/libaskomics/SparqlQuery.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index b0ba68f0..a37d8aa2 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -458,12 +458,12 @@ def get_uri_parameters(self, uri, endpoints): The corresponding parameters """ raw_query = ''' - SELECT DISTINCT ?predicat ?object ?faldo_value ?faldo_uri - WHERE { - ?URI ?predicat ?object . + SELECT DISTINCT ?predicate ?object ?faldo_value ?faldo_uri + WHERE {{ + ?URI ?predicate ?object . ?URI a ?entitytype . - FILTER(! STRSTARTS(STR(?predicat), STR(askomics:))) + FILTER(! STRSTARTS(STR(?predicate), STR(askomics:))) OPTIONAL {{ ?faldo_uri rdfs:domain ?entitytype . @@ -491,7 +491,7 @@ def get_uri_parameters(self, uri, endpoints): ?faldo_uri rdf:type askomics:faldoStrand . }} - VALUES ?predicat {faldo:location} + VALUES ?predicate {{faldo:location}} }} VALUES ?URI {{{}}} }} @@ -500,6 +500,8 @@ def get_uri_parameters(self, uri, endpoints): 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) @@ -508,8 +510,8 @@ def get_uri_parameters(self, uri, endpoints): formated_data = [] for row in data: formated_data.append({ - 'predicate': row['faldo_uri '] if row['faldo_uri'] else row['predicate'], - 'object': row['faldo_value'] if row['faldo_value'] else row['object'], + 'predicate': row['faldo_uri'] if row.get('faldo_uri') else row['predicate'], + 'object': row['faldo_value'] if row.get('faldo_value') else row['object'], }) return formated_data From 7a31f3f9b8c6d6b1c293716701e10b1e139a529b Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 27 Sep 2021 15:31:06 +0200 Subject: [PATCH 093/318] Fix strand values for GFF and BED --- askomics/libaskomics/BedFile.py | 10 +++++++--- askomics/libaskomics/GffFile.py | 11 +++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/askomics/libaskomics/BedFile.py b/askomics/libaskomics/BedFile.py index 7e55a7be..88440756 100644 --- a/askomics/libaskomics/BedFile.py +++ b/askomics/libaskomics/BedFile.py @@ -218,6 +218,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 +227,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 +236,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 +245,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/GffFile.py b/askomics/libaskomics/GffFile.py index 1d39c2f4..d807f6b0 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -196,7 +196,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(): @@ -286,6 +286,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"] = {"-", } @@ -293,6 +294,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"] = {".", } @@ -300,16 +302,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) From f546453908a652cbfdba6985e6bf88963a0beec8 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 27 Sep 2021 16:09:51 +0200 Subject: [PATCH 094/318] Fix tests? --- askomics/api/data.py | 1 - tests/results/abstraction.json | 8 ----- tests/results/data.json | 54 +++++++++++++++++----------------- tests/test_api_data.py | 6 ---- 4 files changed, 27 insertions(+), 42 deletions(-) diff --git a/askomics/api/data.py b/askomics/api/data.py index 32b1cc2d..4710b18b 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) diff --git a/tests/results/abstraction.json b/tests/results/abstraction.json index e29df978..0f7f16a5 100644 --- a/tests/results/abstraction.json +++ b/tests/results/abstraction.json @@ -42,10 +42,6 @@ { "label": "-", "uri": "http://askomics.org/test/data/-" - }, - { - "label": ".", - "uri": "http://askomics.org/test/data/." } ], "entityUri": "http://askomics.org/test/data/gene", @@ -76,10 +72,6 @@ { "label": "-", "uri": "http://askomics.org/test/data/-" - }, - { - "label": ".", - "uri": "http://askomics.org/test/data/." } ], "entityUri": "http://askomics.org/test/data/transcript", diff --git a/tests/results/data.json b/tests/results/data.json index fe526205..fe294d76 100644 --- a/tests/results/data.json +++ b/tests/results/data.json @@ -1,54 +1,54 @@ { - "data": [ + "data":[ { - "object": "http://askomics.org/test/data/transcript", - "predicat": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" + "object":"3267835", + "predicate":"http://askomics.org/test/data/start" }, { - "object": "label_AT3G10490", - "predicat": "http://www.w3.org/2000/01/rdf-schema#label" + "object":"3270883", + "predicate":"http://askomics.org/test/data/end" }, { - "object": "protein_coding", - "predicat": "http://askomics.org/test/data/biotype" + "object":"At3", + "predicate":"http://askomics.org/test/data/chromosomeName" }, { - "object": "NAC_domain_containing_protein_52_[Source:TAIR%3BAcc:AT3G10490]", - "predicat": "http://askomics.org/test/data/description" + "object":"plus", + "predicate":"http://askomics.org/test/data/strand" }, { - "object": "ANAC052", - "predicat": "http://askomics.org/test/data/featureName" + "object":"protein_coding", + "predicate":"http://askomics.org/test/data/biotype" }, { - "object": "http://askomics.org/test/data/gene", - "predicat": "http://askomics.org/test/data/featureType" + "object":"http://askomics.org/test/data/gene", + "predicate":"http://askomics.org/test/data/featureType" }, { - "object": "http://askomics.org/test/data/Arabidopsis_thaliana", - "predicat": "http://askomics.org/test/data/taxon" + "object":"label_AT3G10490", + "predicate":"http://www.w3.org/2000/01/rdf-schema#label" }, { - "object": "326", - "predicat": "http://askomics.org/test/internal/includeIn" + "object":"2000-01-01", + "predicate":"http://askomics.org/test/data/date" }, { - "object": "327", - "predicat": "http://askomics.org/test/internal/includeIn" + "object":"http://askomics.org/test/data/transcript", + "predicate":"http://www.w3.org/1999/02/22-rdf-syntax-ns#type" }, { - "object": "http://askomics.org/test/data/At3_326", - "predicat": "http://askomics.org/test/internal/includeInReference" + "object":"ANAC052", + "predicate":"http://askomics.org/test/data/featureName" }, { - "object": "http://askomics.org/test/data/At3_327", - "predicat": "http://askomics.org/test/internal/includeInReference" + "object":"http://askomics.org/test/data/Arabidopsis_thaliana", + "predicate":"http://askomics.org/test/data/taxon" }, { - "object": "2000-01-01", - "predicat": "http://askomics.org/test/data/date" + "object":"NAC_domain_containing_protein_52_[Source:TAIR%3BAcc:AT3G10490]", + "predicate":"http://askomics.org/test/data/description" } ], - "error": false, - "errorMessage": "" + "error":false, + "errorMessage":"" } diff --git a/tests/test_api_data.py b/tests/test_api_data.py index 92d25e26..ab137572 100644 --- a/tests/test_api_data.py +++ b/tests/test_api_data.py @@ -18,9 +18,6 @@ def test_get_uri(self, client): response = client.client.get('/api/data/AT3G10490') - # Remove this dict since the node value seems to change (dependant on load order maybe?) - response.json['data'] = [val for val in response.json['data'] if not val['predicat'] == "http://biohackathon.org/resource/faldo/location"] - assert response.status_code == 200 assert self.equal_objects(response.json, expected) @@ -73,8 +70,5 @@ def test_public_access(self, client): client.logout() response = client.client.get('/api/data/AT3G10490') - # Remove this dict since the node value seems to change (dependant on load order maybe?) - response.json['data'] = [val for val in response.json['data'] if not val['predicat'] == "http://biohackathon.org/resource/faldo/location"] - assert response.status_code == 200 assert self.equal_objects(response.json, expected) From f91d6849c40467ee18ad5dfd57b966a19b77d2a1 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 27 Sep 2021 17:11:57 +0200 Subject: [PATCH 095/318] Tests (2) --- tests/results/data_full.json | 62 +++++++++++++++++++ tests/results/{data.json => data_public.json} | 0 tests/test_api_data.py | 4 +- 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 tests/results/data_full.json rename tests/results/{data.json => data_public.json} (100%) diff --git a/tests/results/data_full.json b/tests/results/data_full.json new file mode 100644 index 00000000..4bded089 --- /dev/null +++ b/tests/results/data_full.json @@ -0,0 +1,62 @@ +{ + "data":[ + { + "object":"3267835", + "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/start" + }, + { + "object":"3270883", + "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/end" + }, + { + "object":"At3", + "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/chromosomeName" + }, + { + "object":"At3", + "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/reference" + }, + { + "object":"plus", + "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/strand" + }, + { + "object":"+", + "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/strand" + }, + { + "object":"protein_coding", + "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/biotype" + }, + { + "object":"https://test-192-168-100-87.vm.openstack.genouest.org/data/gene", + "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/featureType" + }, + { + "object":"label_AT3G10490", + "predicate":"http://www.w3.org/2000/01/rdf-schema#label" + }, + { + "object":"2000-01-01", + "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/date" + }, + { + "object":"https://test-192-168-100-87.vm.openstack.genouest.org/data/transcript", + "predicate":"http://www.w3.org/1999/02/22-rdf-syntax-ns#type" + }, + { + "object":"ANAC052", + "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/featureName" + }, + { + "object":"https://test-192-168-100-87.vm.openstack.genouest.org/data/Arabidopsis_thaliana", + "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/taxon" + }, + { + "object":"NAC_domain_containing_protein_52_[Source:TAIR%3BAcc:AT3G10490]", + "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/description" + } + ], + "error":false, + "errorMessage":"" +} diff --git a/tests/results/data.json b/tests/results/data_public.json similarity index 100% rename from tests/results/data.json rename to tests/results/data_public.json diff --git a/tests/test_api_data.py b/tests/test_api_data.py index ab137572..f7569042 100644 --- a/tests/test_api_data.py +++ b/tests/test_api_data.py @@ -12,7 +12,7 @@ def test_get_uri(self, client): client.log_user("jdoe") client.upload_and_integrate() - with open("tests/results/data.json", "r") as file: + with open("tests/results/data_full.json", "r") as file: file_content = file.read() expected = json.loads(file_content) @@ -63,7 +63,7 @@ def test_public_access(self, client): "columns_type": ["start_entity", "label", "category", "text", "reference", "start", "end", "category", "strand", "text", "text", "date"] }, public=True) - with open("tests/results/data.json", "r") as file: + with open("tests/results/data_public.json", "r") as file: file_content = file.read() expected = json.loads(file_content) From f8ba8a2a49f454097fecf79dc9d2b5df4aa1e59d Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 27 Sep 2021 17:16:32 +0200 Subject: [PATCH 096/318] Typo --- tests/results/data_full.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/results/data_full.json b/tests/results/data_full.json index 4bded089..9b8b8473 100644 --- a/tests/results/data_full.json +++ b/tests/results/data_full.json @@ -2,35 +2,35 @@ "data":[ { "object":"3267835", - "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/start" + "predicate":"http://askomics.org/test/data/start" }, { "object":"3270883", - "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/end" + "predicate":"http://askomics.org/test/data/end" }, { "object":"At3", - "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/chromosomeName" + "predicate":"http://askomics.org/test/data/chromosomeName" }, { "object":"At3", - "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/reference" + "predicate":"http://askomics.org/test/data/reference" }, { "object":"plus", - "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/strand" + "predicate":"http://askomics.org/test/data/strand" }, { "object":"+", - "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/strand" + "predicate":"http://askomics.org/test/data/strand" }, { "object":"protein_coding", - "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/biotype" + "predicate":"http://askomics.org/test/data/biotype" }, { - "object":"https://test-192-168-100-87.vm.openstack.genouest.org/data/gene", - "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/featureType" + "object":"http://askomics.org/test/data/gene", + "predicate":"http://askomics.org/test/data/featureType" }, { "object":"label_AT3G10490", @@ -38,23 +38,23 @@ }, { "object":"2000-01-01", - "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/date" + "predicate":"http://askomics.org/test/data/date" }, { - "object":"https://test-192-168-100-87.vm.openstack.genouest.org/data/transcript", + "object":"http://askomics.org/test/data/transcript", "predicate":"http://www.w3.org/1999/02/22-rdf-syntax-ns#type" }, { "object":"ANAC052", - "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/featureName" + "predicate":"http://askomics.org/test/data/featureName" }, { - "object":"https://test-192-168-100-87.vm.openstack.genouest.org/data/Arabidopsis_thaliana", - "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/taxon" + "object":"http://askomics.org/test/data/Arabidopsis_thaliana", + "predicate":"http://askomics.org/test/data/taxon" }, { "object":"NAC_domain_containing_protein_52_[Source:TAIR%3BAcc:AT3G10490]", - "predicate":"https://test-192-168-100-87.vm.openstack.genouest.org/data/description" + "predicate":"http://askomics.org/test/data/description" } ], "error":false, From cb00c31d8b1c30adfb93fb9fc6f4462f12d7343f Mon Sep 17 00:00:00 2001 From: root Date: Tue, 28 Sep 2021 17:53:40 +0000 Subject: [PATCH 097/318] Test coverage --- test-data/gene.bed | 2 +- test-data/gene.gff3 | 2 ++ test-data/transcripts.tsv | 2 +- tests/results/abstraction.json | 16 ++++++++++++++++ tests/results/preview.json | 6 ++++++ tests/results/preview_files.json | 2 +- tests/results/results.json | 2 +- tests/results/results_admin.json | 2 +- tests/results/results_form.json | 2 +- tests/test_api_admin.py | 6 +++--- tests/test_api_file.py | 14 +++++++------- tests/test_api_query.py | 2 +- 12 files changed, 41 insertions(+), 17 deletions(-) diff --git a/test-data/gene.bed b/test-data/gene.bed index ebfc540a..ffd3c788 100644 --- a/test-data/gene.bed +++ b/test-data/gene.bed @@ -8,4 +8,4 @@ track name="TilingArray" description="TilingArray demonstration" visibility=2 us 7 127477031 127478198 Neg2 400 - 127477031 127478198 0,0,255 7 127478198 127479365 Neg3 300 - 127478198 127479365 0,0,255 7 127479365 127480532 Pos5 1000 + 127479365 127480532 255,0,0 -7 127480532 127481699 Neg4 0 - 127480532 127481699 0,0,255 +7 127480532 127481699 Neg4 0 . 127480532 127481699 0,0,255 diff --git a/test-data/gene.gff3 b/test-data/gene.gff3 index e4b799fb..23c36b1c 100644 --- a/test-data/gene.gff3 +++ b/test-data/gene.gff3 @@ -7,6 +7,8 @@ #!genebuild-last-updated 2010-09 1 tair gene 3631 5899 . + . ID=gene:AT1G01010;Name=NAC001;biotype=protein_coding;description=NAC domain-containing protein 1 [Source:UniProtKB/Swiss-Prot%3BAcc:Q0WV96];gene_id=AT1G01010;logic_name=tair 1 tair transcript 3631 5899 . + . ID=transcript:AT1G01010.1;Parent=gene:AT1G01010;Name=ANAC001;biotype=protein_coding;transcript_id=AT1G01010.1 +1 tair transcript 3632 5900 . - . ID=transcript:AT1G01010.2;Parent=gene:AT1G01010;Name=ANAC002;biotype=protein_coding;transcript_id=AT1G01010.2 +1 tair transcript 3633 5901 . . . ID=transcript:AT1G01010.3;Parent=gene:AT1G01010;Name=ANAC003;biotype=protein_coding;transcript_id=AT1G01010.3 1 tair five_prime_UTR 3631 3759 . + . Parent=transcript:AT1G01010.1 1 tair exon 3631 3913 . + . Parent=transcript:AT1G01010.1;Name=AT1G01010.1.exon1;constitutive=1;ensembl_end_phase=1;ensembl_phase=-1;exon_id=AT1G01010.1.exon1;rank=1 1 tair CDS 3760 3913 . + 0 ID=CDS:AT1G01010.1;Parent=transcript:AT1G01010.1;protein_id=AT1G01010.1 diff --git a/test-data/transcripts.tsv b/test-data/transcripts.tsv index 87b82988..f32ec275 100644 --- a/test-data/transcripts.tsv +++ b/test-data/transcripts.tsv @@ -8,4 +8,4 @@ AT1G33615 label_AT1G33615 Arabidopsis_thaliana na At1 12193325 12194374 ncRNA_ge AT5G41905 label_AT5G41905 Arabidopsis_thaliana MIR166E At5 16775524 16775658 miRNA_gene minus miRNA MIR166/MIR166E%3B_miRNA_[Source:TAIR%3BAcc:AT5G41905] 07/01/2000 AT1G57800 label_AT1G57800 Arabidopsis_thaliana ORTH3 At1 21408623 21412283 gene minus protein_coding E3_ubiquitin-protein_ligase_ORTHRUS_3_[Source:UniProtKB/Swiss-Prot%3BAcc:Q9FVS2] 08/01/2000 AT1G49500 label_AT1G49500 Arabidopsis_thaliana na At1 18321295 18322284 gene minus protein_coding unknown_protein%3B_FUNCTIONS_IN:_molecular_function_unknown%3B_INVOLVED_IN:_biological_process_unknown%3B_LOCATED_IN:_endomembrane_system%3B_EXPRESSED_IN:_19_plant_structures%3B_EXPRESSED_DURING:_10_growth_stages%3B_BEST_Arabidopsis_thaliana_protein_match_is:_u_/.../_protein_(TAIR:AT3G19030.1)%3B_Has_24_Blast_hits_to_24_proteins_in_2_species:_Archae_-_0%3B_Bacteria_-_0%3B_Metazoa_-_0%3B_Fungi_-_0%3B_Plants_-_24%3B_Viruses_-_0%3B_Other_Eukaryotes_-_0_(source:_NCBI_BLink)._[Source:TAIR%3BAcc:AT1G49500] 09/01/2000 -AT5G35334 label_AT5G35334 Arabidopsis_thaliana na At5 13537917 13538984 gene plus transposable_element transposable_element_gene_[Source:TAIR%3BAcc:AT5G35334] 10/01/2000 +AT5G35334 label_AT5G35334 Arabidopsis_thaliana na At5 13537917 13538984 gene transposable_element transposable_element_gene_[Source:TAIR%3BAcc:AT5G35334] 10/01/2000 diff --git a/tests/results/abstraction.json b/tests/results/abstraction.json index 0f7f16a5..4aa59849 100644 --- a/tests/results/abstraction.json +++ b/tests/results/abstraction.json @@ -35,6 +35,10 @@ "label": "plus", "uri": "http://askomics.org/test/data/plus" }, + { + "label": "unknown/both", + "uri": "http://askomics.org/test/data/unknown/both" + }, { "label": "+", "uri": "http://askomics.org/test/data/%2B" @@ -42,6 +46,10 @@ { "label": "-", "uri": "http://askomics.org/test/data/-" + }, + { + "label": ".", + "uri": "http://askomics.org/test/data/." } ], "entityUri": "http://askomics.org/test/data/gene", @@ -65,6 +73,10 @@ "label": "plus", "uri": "http://askomics.org/test/data/plus" }, + { + "label": "unknown/both", + "uri": "http://askomics.org/test/data/unknown/both" + }, { "label": "+", "uri": "http://askomics.org/test/data/%2B" @@ -72,6 +84,10 @@ { "label": "-", "uri": "http://askomics.org/test/data/-" + }, + { + "label": ".", + "uri": "http://askomics.org/test/data/." } ], "entityUri": "http://askomics.org/test/data/transcript", diff --git a/tests/results/preview.json b/tests/results/preview.json index a83d515b..8aaa4dc6 100644 --- a/tests/results/preview.json +++ b/tests/results/preview.json @@ -38,6 +38,12 @@ }, { "transcript1_Label": "AT1G01010.1" + }, + { + "transcript1_Label": "AT1G01010.2" + }, + { + "transcript1_Label": "AT1G01010.3" } ] } diff --git a/tests/results/preview_files.json b/tests/results/preview_files.json index 98336370..beeb18c4 100644 --- a/tests/results/preview_files.json +++ b/tests/results/preview_files.json @@ -153,7 +153,7 @@ "featureName": "na", "featureType": "gene", "start": "13537917", - "strand": "plus", + "strand": "", "taxon": "Arabidopsis_thaliana", "transcript": "AT5G35334", "date": "10/01/2000", diff --git a/tests/results/results.json b/tests/results/results.json index 978cd972..e561abd1 100644 --- a/tests/results/results.json +++ b/tests/results/results.json @@ -9,7 +9,7 @@ "execTime": ###EXECTIME###, "graphState": "{\"nodes\": [{\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 1, \"humanId\": 1, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": true, \"suggested\": false, \"__indexColor\": \"#ec0001\", \"index\": 0, \"x\": 2.4610529943331394, \"y\": 2.226874027134668, \"vx\": 2.353770443806525e-10, \"vy\": 1.892184783500829e-10}, {\"uri\": \"http://askomics.org/test/data/gene\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, null, \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", \"urn:sparql:askomics_test:1_jdoe:gene.bed_1590576352\", null], \"id\": 18, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"gene\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#d80002\", \"index\": 1, \"x\": -18.936663966428853, \"y\": 27.854539518654068, \"vx\": 3.0435888926329303e-10, \"vy\": 2.455944547131252e-10}, {\"uri\": \"http://askomics.org/test/data/DifferentialExpression\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, null, null, \"urn:sparql:askomics_test:1_jdoe:de.tsv_1590570405\"], \"id\": 20, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"DifferentialExpression\", \"faldo\": false, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#c40003\", \"index\": 2, \"x\": -5.504944105164966, \"y\": -30.053414398937772, \"vx\": 1.6289496823647462e-10, \"vy\": 1.9886120976258667e-10}, {\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 23, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#b00004\", \"index\": 3, \"x\": 24.169971227492816, \"y\": 27.218300246800673, \"vx\": 3.366634402979017e-10, \"vy\": 1.1297513262220989e-10}, {\"uri\": \"http://askomics.org/test/data/QTL\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, \"urn:sparql:askomics_test:1_jdoe:qtl.tsv_1590570413\", null, null], \"id\": 25, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"QTL\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#9c0005\", \"index\": 4, \"x\": -29.503539254383465, \"y\": -7.095712074145055, \"vx\": 2.1843706072578676e-10, \"vy\": 2.560853024904153e-10}, {\"uri\": \"http://askomics.org/test/data/gene\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, null, \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", \"urn:sparql:askomics_test:1_jdoe:gene.bed_1590576352\", null], \"id\": 27, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"gene\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#880006\", \"index\": 5, \"x\": 27.314123105593524, \"y\": -20.150587318393672, \"vx\": 1.8446839255154e-10, \"vy\": 1.1018030967605363e-10}], \"links\": [{\"uri\": \"http://askomics.org/test/data/Parent\", \"type\": \"link\", \"sameStrand\": true, \"sameRef\": true, \"id\": 19, \"label\": \"Parent\", \"source\": {\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 1, \"humanId\": 1, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": true, \"suggested\": false, \"__indexColor\": \"#ec0001\", \"index\": 0, \"x\": 2.4610529943331394, \"y\": 2.226874027134668, \"vx\": 2.353770443806525e-10, \"vy\": 1.892184783500829e-10}, \"target\": {\"uri\": \"http://askomics.org/test/data/gene\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, null, \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", \"urn:sparql:askomics_test:1_jdoe:gene.bed_1590576352\", null], \"id\": 18, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"gene\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#d80002\", \"index\": 1, \"x\": -18.936663966428853, \"y\": 27.854539518654068, \"vx\": 3.0435888926329303e-10, \"vy\": 2.455944547131252e-10}, \"selected\": false, \"suggested\": true, \"directed\": true, \"__indexColor\": \"#740007\", \"__controlPoints\": null, \"index\": 0}, {\"uri\": \"http://askomics.org/test/data/concern\", \"type\": \"link\", \"sameStrand\": false, \"sameRef\": false, \"id\": 22, \"label\": \"concern\", \"source\": {\"uri\": \"http://askomics.org/test/data/DifferentialExpression\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, null, null, \"urn:sparql:askomics_test:1_jdoe:de.tsv_1590570405\"], \"id\": 20, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"DifferentialExpression\", \"faldo\": false, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#c40003\", \"index\": 2, \"x\": -5.504944105164966, \"y\": -30.053414398937772, \"vx\": 1.6289496823647462e-10, \"vy\": 1.9886120976258667e-10}, \"target\": {\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 1, \"humanId\": 1, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": true, \"suggested\": false, \"__indexColor\": \"#ec0001\", \"index\": 0, \"x\": 2.4610529943331394, \"y\": 2.226874027134668, \"vx\": 2.353770443806525e-10, \"vy\": 1.892184783500829e-10}, \"selected\": false, \"suggested\": true, \"directed\": true, \"__indexColor\": \"#600008\", \"__controlPoints\": null, \"index\": 1}, {\"uri\": \"included_in\", \"type\": \"posLink\", \"id\": 24, \"sameStrand\": true, \"sameRef\": true, \"strict\": true, \"label\": \"Included in\", \"source\": {\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 1, \"humanId\": 1, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": true, \"suggested\": false, \"__indexColor\": \"#ec0001\", \"index\": 0, \"x\": 2.4610529943331394, \"y\": 2.226874027134668, \"vx\": 2.353770443806525e-10, \"vy\": 1.892184783500829e-10}, \"target\": {\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 23, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#b00004\", \"index\": 3, \"x\": 24.169971227492816, \"y\": 27.218300246800673, \"vx\": 3.366634402979017e-10, \"vy\": 1.1297513262220989e-10}, \"selected\": false, \"suggested\": true, \"directed\": true, \"__indexColor\": \"#4c0009\", \"__controlPoints\": null, \"index\": 2}, {\"uri\": \"included_in\", \"type\": \"posLink\", \"id\": 26, \"sameStrand\": false, \"sameRef\": true, \"strict\": true, \"label\": \"Included in\", \"source\": {\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 1, \"humanId\": 1, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": true, \"suggested\": false, \"__indexColor\": \"#ec0001\", \"index\": 0, \"x\": 2.4610529943331394, \"y\": 2.226874027134668, \"vx\": 2.353770443806525e-10, \"vy\": 1.892184783500829e-10}, \"target\": {\"uri\": \"http://askomics.org/test/data/QTL\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, \"urn:sparql:askomics_test:1_jdoe:qtl.tsv_1590570413\", null, null], \"id\": 25, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"QTL\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#9c0005\", \"index\": 4, \"x\": -29.503539254383465, \"y\": -7.095712074145055, \"vx\": 2.1843706072578676e-10, \"vy\": 2.560853024904153e-10}, \"selected\": false, \"suggested\": true, \"directed\": true, \"__indexColor\": \"#38000a\", \"__controlPoints\": null, \"index\": 3}, {\"uri\": \"included_in\", \"type\": \"posLink\", \"id\": 28, \"sameStrand\": true, \"sameRef\": true, \"strict\": true, \"label\": \"Included in\", \"source\": {\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 1, \"humanId\": 1, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": true, \"suggested\": false, \"__indexColor\": \"#ec0001\", \"index\": 0, \"x\": 2.4610529943331394, \"y\": 2.226874027134668, \"vx\": 2.353770443806525e-10, \"vy\": 1.892184783500829e-10}, \"target\": {\"uri\": \"http://askomics.org/test/data/gene\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, null, \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", \"urn:sparql:askomics_test:1_jdoe:gene.bed_1590576352\", null], \"id\": 27, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"gene\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#880006\", \"index\": 5, \"x\": 27.314123105593524, \"y\": -20.150587318393672, \"vx\": 1.8446839255154e-10, \"vy\": 1.1018030967605363e-10}, \"selected\": false, \"suggested\": true, \"directed\": true, \"__indexColor\": \"#24000b\", \"__controlPoints\": null, \"index\": 4}], \"attr\": [{\"id\": 2, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"rdf:type\", \"label\": \"Uri\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"uri\", \"faldo\": false, \"filterType\": \"exact\", \"filterValue\": \"\", \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null}, {\"id\": 3, \"visible\": true, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"rdfs:label\", \"label\": \"Label\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"text\", \"faldo\": false, \"filterType\": \"exact\", \"filterValue\": \"\", \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null}, {\"id\": 4, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/strand\", \"label\": \"strand\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"category\", \"faldo\": \"http://askomics.org/internal/faldoStrand\", \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterValues\": [{\"label\": \"plus\", \"uri\": \"http://askomics.org/test/data/plus\"}, {\"label\": \"minus\", \"uri\": \"http://askomics.org/test/data/minus\"}, {\"label\": \"-\", \"uri\": \"http://askomics.org/test/data/-\"}, {\"label\": \"+\", \"uri\": \"http://askomics.org/test/data/%2B\"}], \"filterSelectedValues\": []}, {\"id\": 5, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/reference\", \"label\": \"reference\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"category\", \"faldo\": \"http://askomics.org/internal/faldoReference\", \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterValues\": [{\"label\": \"1\", \"uri\": \"http://askomics.org/test/data/1\"}], \"filterSelectedValues\": []}, {\"id\": 6, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/chromosomeName\", \"label\": \"chromosomeName\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"category\", \"faldo\": \"http://askomics.org/internal/faldoReference\", \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterValues\": [{\"label\": \"At3\", \"uri\": \"http://askomics.org/test/data/At3\"}, {\"label\": \"At5\", \"uri\": \"http://askomics.org/test/data/At5\"}, {\"label\": \"At1\", \"uri\": \"http://askomics.org/test/data/At1\"}], \"filterSelectedValues\": []}, {\"id\": 7, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/taxon\", \"label\": \"taxon\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"category\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterValues\": [{\"label\": \"Arabidopsis_thaliana\", \"uri\": \"http://askomics.org/test/data/Arabidopsis_thaliana\"}], \"filterSelectedValues\": []}, {\"id\": 8, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/featureType\", \"label\": \"featureType\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"category\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterValues\": [{\"label\": \"gene\", \"uri\": \"http://askomics.org/test/data/gene\"}, {\"label\": \"miRNA_gene\", \"uri\": \"http://askomics.org/test/data/miRNA_gene\"}, {\"label\": \"ncRNA_gene\", \"uri\": \"http://askomics.org/test/data/ncRNA_gene\"}], \"filterSelectedValues\": []}, {\"id\": 9, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/biotype\", \"label\": \"biotype\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"category\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterValues\": [{\"label\": \"miRNA\", \"uri\": \"http://askomics.org/test/data/miRNA\"}, {\"label\": \"ncRNA\", \"uri\": \"http://askomics.org/test/data/ncRNA\"}, {\"label\": \"protein_coding\", \"uri\": \"http://askomics.org/test/data/protein_coding\"}, {\"label\": \"transposable_element\", \"uri\": \"http://askomics.org/test/data/transposable_element\"}], \"filterSelectedValues\": []}, {\"id\": 10, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/start\", \"label\": \"start\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"decimal\", \"faldo\": \"http://askomics.org/internal/faldoStart\", \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filters\": [{\"filterValue\": \"\", \"filterSign\": \"=\"}]}, {\"id\": 11, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/end\", \"label\": \"end\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"decimal\", \"faldo\": \"http://askomics.org/internal/faldoEnd\", \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filters\": [{\"filterValue\": \"\", \"filterSign\": \"=\"}]}, {\"id\": 12, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/description\", \"label\": \"description\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"text\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterType\": \"exact\", \"filterValue\": \"\"}, {\"id\": 13, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/featureName\", \"label\": \"featureName\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"text\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterType\": \"exact\", \"filterValue\": \"\"}, {\"id\": 14, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/ID\", \"label\": \"ID\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"text\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterType\": \"exact\", \"filterValue\": \"\"}, {\"id\": 15, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/Name\", \"label\": \"Name\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"text\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterType\": \"exact\", \"filterValue\": \"\"}, {\"id\": 16, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/source\", \"label\": \"source\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"text\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterType\": \"exact\", \"filterValue\": \"\"}, {\"id\": 17, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/transcript_id\", \"label\": \"transcript_id\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"text\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterType\": \"exact\", \"filterValue\": \"\"}]}", "id": ###ID###, - "nrows": 11, + "nrows": 13, "path": "###PATH###", "public": ###PUBLIC###, "size": ###SIZE###, diff --git a/tests/results/results_admin.json b/tests/results/results_admin.json index a21b0c64..34cceb45 100644 --- a/tests/results/results_admin.json +++ b/tests/results/results_admin.json @@ -7,7 +7,7 @@ "end": ###END###, "execTime": ###EXECTIME###, "id": ###ID###, - "nrows": 11, + "nrows": 13, "public": ###PUBLIC###, "size": ###SIZE###, "start": ###START###, diff --git a/tests/results/results_form.json b/tests/results/results_form.json index 5c4cb14a..8d9bcc64 100644 --- a/tests/results/results_form.json +++ b/tests/results/results_form.json @@ -11,7 +11,7 @@ "graphState": "{\"nodes\": [{\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 1, \"humanId\": 1, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": true, \"suggested\": false, \"__indexColor\": \"#ec0001\", \"index\": 0, \"x\": 2.4610529943331394, \"y\": 2.226874027134668, \"vx\": 2.353770443806525e-10, \"vy\": 1.892184783500829e-10}, {\"uri\": \"http://askomics.org/test/data/gene\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, null, \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", \"urn:sparql:askomics_test:1_jdoe:gene.bed_1590576352\", null], \"id\": 18, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"gene\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#d80002\", \"index\": 1, \"x\": -18.936663966428853, \"y\": 27.854539518654068, \"vx\": 3.0435888926329303e-10, \"vy\": 2.455944547131252e-10}, {\"uri\": \"http://askomics.org/test/data/DifferentialExpression\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, null, null, \"urn:sparql:askomics_test:1_jdoe:de.tsv_1590570405\"], \"id\": 20, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"DifferentialExpression\", \"faldo\": false, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#c40003\", \"index\": 2, \"x\": -5.504944105164966, \"y\": -30.053414398937772, \"vx\": 1.6289496823647462e-10, \"vy\": 1.9886120976258667e-10}, {\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 23, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#b00004\", \"index\": 3, \"x\": 24.169971227492816, \"y\": 27.218300246800673, \"vx\": 3.366634402979017e-10, \"vy\": 1.1297513262220989e-10}, {\"uri\": \"http://askomics.org/test/data/QTL\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, \"urn:sparql:askomics_test:1_jdoe:qtl.tsv_1590570413\", null, null], \"id\": 25, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"QTL\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#9c0005\", \"index\": 4, \"x\": -29.503539254383465, \"y\": -7.095712074145055, \"vx\": 2.1843706072578676e-10, \"vy\": 2.560853024904153e-10}, {\"uri\": \"http://askomics.org/test/data/gene\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, null, \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", \"urn:sparql:askomics_test:1_jdoe:gene.bed_1590576352\", null], \"id\": 27, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"gene\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#880006\", \"index\": 5, \"x\": 27.314123105593524, \"y\": -20.150587318393672, \"vx\": 1.8446839255154e-10, \"vy\": 1.1018030967605363e-10}], \"links\": [{\"uri\": \"http://askomics.org/test/data/Parent\", \"type\": \"link\", \"sameStrand\": true, \"sameRef\": true, \"id\": 19, \"label\": \"Parent\", \"source\": {\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 1, \"humanId\": 1, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": true, \"suggested\": false, \"__indexColor\": \"#ec0001\", \"index\": 0, \"x\": 2.4610529943331394, \"y\": 2.226874027134668, \"vx\": 2.353770443806525e-10, \"vy\": 1.892184783500829e-10}, \"target\": {\"uri\": \"http://askomics.org/test/data/gene\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, null, \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", \"urn:sparql:askomics_test:1_jdoe:gene.bed_1590576352\", null], \"id\": 18, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"gene\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#d80002\", \"index\": 1, \"x\": -18.936663966428853, \"y\": 27.854539518654068, \"vx\": 3.0435888926329303e-10, \"vy\": 2.455944547131252e-10}, \"selected\": false, \"suggested\": true, \"directed\": true, \"__indexColor\": \"#740007\", \"__controlPoints\": null, \"index\": 0}, {\"uri\": \"http://askomics.org/test/data/concern\", \"type\": \"link\", \"sameStrand\": false, \"sameRef\": false, \"id\": 22, \"label\": \"concern\", \"source\": {\"uri\": \"http://askomics.org/test/data/DifferentialExpression\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, null, null, \"urn:sparql:askomics_test:1_jdoe:de.tsv_1590570405\"], \"id\": 20, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"DifferentialExpression\", \"faldo\": false, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#c40003\", \"index\": 2, \"x\": -5.504944105164966, \"y\": -30.053414398937772, \"vx\": 1.6289496823647462e-10, \"vy\": 1.9886120976258667e-10}, \"target\": {\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 1, \"humanId\": 1, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": true, \"suggested\": false, \"__indexColor\": \"#ec0001\", \"index\": 0, \"x\": 2.4610529943331394, \"y\": 2.226874027134668, \"vx\": 2.353770443806525e-10, \"vy\": 1.892184783500829e-10}, \"selected\": false, \"suggested\": true, \"directed\": true, \"__indexColor\": \"#600008\", \"__controlPoints\": null, \"index\": 1}, {\"uri\": \"included_in\", \"type\": \"posLink\", \"id\": 24, \"sameStrand\": true, \"sameRef\": true, \"strict\": true, \"label\": \"Included in\", \"source\": {\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 1, \"humanId\": 1, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": true, \"suggested\": false, \"__indexColor\": \"#ec0001\", \"index\": 0, \"x\": 2.4610529943331394, \"y\": 2.226874027134668, \"vx\": 2.353770443806525e-10, \"vy\": 1.892184783500829e-10}, \"target\": {\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 23, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#b00004\", \"index\": 3, \"x\": 24.169971227492816, \"y\": 27.218300246800673, \"vx\": 3.366634402979017e-10, \"vy\": 1.1297513262220989e-10}, \"selected\": false, \"suggested\": true, \"directed\": true, \"__indexColor\": \"#4c0009\", \"__controlPoints\": null, \"index\": 2}, {\"uri\": \"included_in\", \"type\": \"posLink\", \"id\": 26, \"sameStrand\": false, \"sameRef\": true, \"strict\": true, \"label\": \"Included in\", \"source\": {\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 1, \"humanId\": 1, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": true, \"suggested\": false, \"__indexColor\": \"#ec0001\", \"index\": 0, \"x\": 2.4610529943331394, \"y\": 2.226874027134668, \"vx\": 2.353770443806525e-10, \"vy\": 1.892184783500829e-10}, \"target\": {\"uri\": \"http://askomics.org/test/data/QTL\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, \"urn:sparql:askomics_test:1_jdoe:qtl.tsv_1590570413\", null, null], \"id\": 25, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"QTL\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#9c0005\", \"index\": 4, \"x\": -29.503539254383465, \"y\": -7.095712074145055, \"vx\": 2.1843706072578676e-10, \"vy\": 2.560853024904153e-10}, \"selected\": false, \"suggested\": true, \"directed\": true, \"__indexColor\": \"#38000a\", \"__controlPoints\": null, \"index\": 3}, {\"uri\": \"included_in\", \"type\": \"posLink\", \"id\": 28, \"sameStrand\": true, \"sameRef\": true, \"strict\": true, \"label\": \"Included in\", \"source\": {\"uri\": \"http://askomics.org/test/data/transcript\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [\"urn:sparql:askomics_test:1_jdoe:transcripts.tsv_1590570429\", \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", null, null, null], \"id\": 1, \"humanId\": 1, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"transcript\", \"faldo\": true, \"selected\": true, \"suggested\": false, \"__indexColor\": \"#ec0001\", \"index\": 0, \"x\": 2.4610529943331394, \"y\": 2.226874027134668, \"vx\": 2.353770443806525e-10, \"vy\": 1.892184783500829e-10}, \"target\": {\"uri\": \"http://askomics.org/test/data/gene\", \"type\": \"node\", \"filterNode\": \"\", \"filterLink\": \"\", \"graphs\": [null, null, \"urn:sparql:askomics_test:1_jdoe:gene.gff3_1590576352\", \"urn:sparql:askomics_test:1_jdoe:gene.bed_1590576352\", null], \"id\": 27, \"humanId\": null, \"specialNodeId\": null, \"specialNodeGroupId\": null, \"specialPreviousIds\": [null, null], \"label\": \"gene\", \"faldo\": true, \"selected\": false, \"suggested\": true, \"__indexColor\": \"#880006\", \"index\": 5, \"x\": 27.314123105593524, \"y\": -20.150587318393672, \"vx\": 1.8446839255154e-10, \"vy\": 1.1018030967605363e-10}, \"selected\": false, \"suggested\": true, \"directed\": true, \"__indexColor\": \"#24000b\", \"__controlPoints\": null, \"index\": 4}], \"attr\": [{\"id\": 2, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"rdf:type\", \"label\": \"Uri\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"uri\", \"faldo\": false, \"filterType\": \"exact\", \"filterValue\": \"\", \"optional\": false, \"negative\": false, \"linked\": false, \"form\": true, \"linkedWith\": null}, {\"id\": 3, \"visible\": true, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"rdfs:label\", \"label\": \"Label\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"text\", \"faldo\": false, \"filterType\": \"exact\", \"filterValue\": \"\", \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null}, {\"id\": 4, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/strand\", \"label\": \"strand\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"category\", \"faldo\": \"http://askomics.org/internal/faldoStrand\", \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterValues\": [{\"label\": \"plus\", \"uri\": \"http://askomics.org/test/data/plus\"}, {\"label\": \"minus\", \"uri\": \"http://askomics.org/test/data/minus\"}, {\"label\": \"-\", \"uri\": \"http://askomics.org/test/data/-\"}, {\"label\": \"+\", \"uri\": \"http://askomics.org/test/data/%2B\"}], \"filterSelectedValues\": []}, {\"id\": 5, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/reference\", \"label\": \"reference\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"category\", \"faldo\": \"http://askomics.org/internal/faldoReference\", \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterValues\": [{\"label\": \"1\", \"uri\": \"http://askomics.org/test/data/1\"}], \"filterSelectedValues\": []}, {\"id\": 6, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/chromosomeName\", \"label\": \"chromosomeName\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"category\", \"faldo\": \"http://askomics.org/internal/faldoReference\", \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterValues\": [{\"label\": \"At3\", \"uri\": \"http://askomics.org/test/data/At3\"}, {\"label\": \"At5\", \"uri\": \"http://askomics.org/test/data/At5\"}, {\"label\": \"At1\", \"uri\": \"http://askomics.org/test/data/At1\"}], \"filterSelectedValues\": []}, {\"id\": 7, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/taxon\", \"label\": \"taxon\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"category\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterValues\": [{\"label\": \"Arabidopsis_thaliana\", \"uri\": \"http://askomics.org/test/data/Arabidopsis_thaliana\"}], \"filterSelectedValues\": []}, {\"id\": 8, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/featureType\", \"label\": \"featureType\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"category\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterValues\": [{\"label\": \"gene\", \"uri\": \"http://askomics.org/test/data/gene\"}, {\"label\": \"miRNA_gene\", \"uri\": \"http://askomics.org/test/data/miRNA_gene\"}, {\"label\": \"ncRNA_gene\", \"uri\": \"http://askomics.org/test/data/ncRNA_gene\"}], \"filterSelectedValues\": []}, {\"id\": 9, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/biotype\", \"label\": \"biotype\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"category\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterValues\": [{\"label\": \"miRNA\", \"uri\": \"http://askomics.org/test/data/miRNA\"}, {\"label\": \"ncRNA\", \"uri\": \"http://askomics.org/test/data/ncRNA\"}, {\"label\": \"protein_coding\", \"uri\": \"http://askomics.org/test/data/protein_coding\"}, {\"label\": \"transposable_element\", \"uri\": \"http://askomics.org/test/data/transposable_element\"}], \"filterSelectedValues\": []}, {\"id\": 10, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/start\", \"label\": \"start\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"decimal\", \"faldo\": \"http://askomics.org/internal/faldoStart\", \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filters\": [{\"filterValue\": \"\", \"filterSign\": \"=\"}]}, {\"id\": 11, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/end\", \"label\": \"end\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"decimal\", \"faldo\": \"http://askomics.org/internal/faldoEnd\", \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filters\": [{\"filterValue\": \"\", \"filterSign\": \"=\"}]}, {\"id\": 12, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/description\", \"label\": \"description\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"text\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterType\": \"exact\", \"filterValue\": \"\"}, {\"id\": 13, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/featureName\", \"label\": \"featureName\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"text\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterType\": \"exact\", \"filterValue\": \"\"}, {\"id\": 14, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/ID\", \"label\": \"ID\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"text\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterType\": \"exact\", \"filterValue\": \"\"}, {\"id\": 15, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/Name\", \"label\": \"Name\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"text\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterType\": \"exact\", \"filterValue\": \"\"}, {\"id\": 16, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/source\", \"label\": \"source\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"text\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterType\": \"exact\", \"filterValue\": \"\"}, {\"id\": 17, \"visible\": false, \"nodeId\": 1, \"humanNodeId\": 1, \"uri\": \"http://askomics.org/test/data/transcript_id\", \"label\": \"transcript_id\", \"entityLabel\": \"transcript\", \"entityUris\": [\"http://askomics.org/test/data/transcript\"], \"type\": \"text\", \"faldo\": null, \"optional\": false, \"negative\": false, \"linked\": false, \"linkedWith\": null, \"filterType\": \"exact\", \"filterValue\": \"\"}]}", "has_form_attr": ###HAS_FORM_ATTR###, "id": ###ID###, - "nrows": 11, + "nrows": 13, "path": "###PATH###", "public": ###PUBLIC###, "size": ###SIZE###, diff --git a/tests/test_api_admin.py b/tests/test_api_admin.py index 4fef7c93..1a31748d 100644 --- a/tests/test_api_admin.py +++ b/tests/test_api_admin.py @@ -68,7 +68,7 @@ def test_get_files(self, client): 'date': info["transcripts"]["upload"]["file_date"], 'id': 1, 'name': 'transcripts.tsv', - 'size': 2268, + 'size': 2264, 'type': 'csv/tsv', 'user': 'jsmith' @@ -92,7 +92,7 @@ def test_get_files(self, client): 'date': info["gene"]["upload"]["file_date"], 'id': 4, 'name': 'gene.gff3', - 'size': 2267, + 'size': 2555, 'type': 'gff/gff3', 'user': 'jsmith' @@ -443,7 +443,7 @@ def test_delete_files(self, client): 'date': info["gene"]["upload"]["file_date"], 'id': 4, 'name': 'gene.gff3', - 'size': 2267, + 'size': 2555, 'type': 'gff/gff3', 'user': 'jsmith' diff --git a/tests/test_api_file.py b/tests/test_api_file.py index f5510b05..a91bbdeb 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -25,7 +25,7 @@ def test_get_files(self, client): 'date': info["transcripts"]["upload"]["file_date"], 'id': 1, 'name': 'transcripts.tsv', - 'size': 2268, + 'size': 2264, 'type': 'csv/tsv' }, { 'date': info["de"]["upload"]["file_date"], @@ -43,7 +43,7 @@ def test_get_files(self, client): 'date': info["gene"]["upload"]["file_date"], 'id': 4, 'name': 'gene.gff3', - 'size': 2267, + 'size': 2555, 'type': 'gff/gff3' }, { 'date': info["bed"]["upload"]["file_date"], @@ -73,7 +73,7 @@ def test_get_files(self, client): 'date': info["transcripts"]["upload"]["file_date"], 'id': 1, 'name': 'transcripts.tsv', - 'size': 2268, + 'size': 2264, 'type': 'csv/tsv' }] } @@ -105,7 +105,7 @@ def test_edit_file(self, client): 'date': info["transcripts"]["upload"]["file_date"], 'id': 1, 'name': 'new name.tsv', - 'size': 2268, + 'size': 2264, 'type': 'csv/tsv' }, { 'date': info["de"]["upload"]["file_date"], @@ -123,7 +123,7 @@ def test_edit_file(self, client): 'date': info["gene"]["upload"]["file_date"], 'id': 4, 'name': 'gene.gff3', - 'size': 2267, + 'size': 2555, 'type': 'gff/gff3' }, { 'date': info["bed"]["upload"]["file_date"], @@ -438,7 +438,7 @@ def test_delete_files(self, client): 'date': info["gene"]["upload"]["file_date"], 'id': 4, 'name': 'gene.gff3', - 'size': 2267, + 'size': 2555, 'type': 'gff/gff3' }, { 'date': info["bed"]["upload"]["file_date"], @@ -464,7 +464,7 @@ def test_delete_files(self, client): 'date': info["gene"]["upload"]["file_date"], 'id': 4, 'name': 'gene.gff3', - 'size': 2267, + 'size': 2555, 'type': 'gff/gff3' }, { 'date': info["bed"]["upload"]["file_date"], diff --git a/tests/test_api_query.py b/tests/test_api_query.py index 7a863d1b..68b9b486 100644 --- a/tests/test_api_query.py +++ b/tests/test_api_query.py @@ -153,7 +153,7 @@ def test_get_preview(self, client): client.log_user("jdoe") response = client.client.post('/api/query/preview', json=data) - expected = {'error': False, 'errorMessage': '', 'headerPreview': ['transcript1_Label'], 'resultsPreview': [{'transcript1_Label': 'label_AT1G57800'}, {'transcript1_Label': 'label_AT5G35334'}, {'transcript1_Label': 'label_AT3G10460'}, {'transcript1_Label': 'label_AT1G49500'}, {'transcript1_Label': 'label_AT3G10490'}, {'transcript1_Label': 'label_AT3G51470'}, {'transcript1_Label': 'label_AT5G41905'}, {'transcript1_Label': 'label_AT1G33615'}, {'transcript1_Label': 'label_AT3G22640'}, {'transcript1_Label': 'label_AT3G13660'}, {'transcript1_Label': 'AT1G01010.1'}]} + expected = {'error': False, 'errorMessage': '', 'headerPreview': ['transcript1_Label'], 'resultsPreview': [{'transcript1_Label': 'label_AT1G57800'}, {'transcript1_Label': 'label_AT5G35334'}, {'transcript1_Label': 'label_AT3G10460'}, {'transcript1_Label': 'label_AT1G49500'}, {'transcript1_Label': 'label_AT3G10490'}, {'transcript1_Label': 'label_AT3G51470'}, {'transcript1_Label': 'label_AT5G41905'}, {'transcript1_Label': 'label_AT1G33615'}, {'transcript1_Label': 'label_AT3G22640'}, {'transcript1_Label': 'label_AT3G13660'}, {'transcript1_Label': 'AT1G01010.1'}, {'transcript1_Label': 'AT1G01010.2'}, {'transcript1_Label': 'AT1G01010.3'}]} # print(response.json) From a2ee25ba2507ef2a42de7734bd8032b79d3a63c2 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 4 Oct 2021 14:23:11 +0200 Subject: [PATCH 098/318] Fix tests --- tests/test_api_file.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_api_file.py b/tests/test_api_file.py index ad531760..15b4cf5e 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -46,7 +46,8 @@ def test_get_files(self, client): 'id': 4, 'name': 'gene.gff3', 'size': 2555, - 'type': 'gff/gff3' + 'type': 'gff/gff3', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], 'id': 5, @@ -132,7 +133,8 @@ def test_edit_file(self, client): 'id': 1, 'name': 'new name.tsv', 'size': 2264, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["de"]["upload"]["file_date"], 'id': 2, @@ -152,7 +154,8 @@ def test_edit_file(self, client): 'id': 4, 'name': 'gene.gff3', 'size': 2555, - 'type': 'gff/gff3' + 'type': 'gff/gff3', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], 'id': 5, @@ -469,7 +472,8 @@ def test_delete_files(self, client): 'id': 4, 'name': 'gene.gff3', 'size': 2555, - 'type': 'gff/gff3' + 'type': 'gff/gff3', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], 'id': 5, From 18a183e3d63f61d0020e0efb77d892338f58ad0e Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 4 Oct 2021 14:37:50 +0200 Subject: [PATCH 099/318] Fix tests (2) --- tests/test_api_file.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_api_file.py b/tests/test_api_file.py index 15b4cf5e..d5a347a8 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -26,7 +26,8 @@ def test_get_files(self, client): 'id': 1, 'name': 'transcripts.tsv', 'size': 2264, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["de"]["upload"]["file_date"], 'id': 2, @@ -501,7 +502,8 @@ def test_delete_files(self, client): 'id': 4, 'name': 'gene.gff3', 'size': 2555, - 'type': 'gff/gff3' + 'type': 'gff/gff3', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], 'id': 5, From d81fcceb21aa35ae667cf34b96f3be5a8ea4b6d6 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 4 Oct 2021 14:55:26 +0200 Subject: [PATCH 100/318] ... --- tests/test_api_file.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_api_file.py b/tests/test_api_file.py index d5a347a8..829ce1b1 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -79,7 +79,8 @@ def test_get_files(self, client): 'id': 1, 'name': 'transcripts.tsv', 'size': 2264, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }] } From f68a387a133df9d4127ac3158acfad4085335f4a Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 4 Oct 2021 15:20:49 +0200 Subject: [PATCH 101/318] [WIP] Fix #186 : Remote upload in celery task (#283) Fix #186 --- askomics/api/file.py | 16 ++- askomics/libaskomics/Database.py | 22 ++++ askomics/libaskomics/FilesHandler.py | 109 +++++++++++++++--- .../react/src/routes/upload/filestable.jsx | 15 +++ .../react/src/routes/upload/uploadurlform.jsx | 6 +- askomics/tasks.py | 15 +++ tests/conftest.py | 18 +++ tests/test_api_admin.py | 24 ++-- tests/test_api_file.py | 80 +++++++++---- 9 files changed, 257 insertions(+), 48 deletions(-) diff --git a/askomics/api/file.py b/askomics/api/file.py index 94f5377d..45be76a0 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 @@ -189,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({ diff --git a/askomics/libaskomics/Database.py b/askomics/libaskomics/Database.py index 5afadeae..16b138f0 100644 --- a/askomics/libaskomics/Database.py +++ b/askomics/libaskomics/Database.py @@ -350,6 +350,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 = """ diff --git a/askomics/libaskomics/FilesHandler.py b/askomics/libaskomics/FilesHandler.py index 54bc495c..ae5e8bb8 100644 --- a/askomics/libaskomics/FilesHandler.py +++ b/askomics/libaskomics/FilesHandler.py @@ -89,7 +89,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 +101,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 +112,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 +126,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 +143,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 +158,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 +205,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 +218,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 +236,8 @@ def store_file_info_in_db(self, name, filetype, file_name, size): ?, ?, ?, + ?, + ?, ? ) ''' @@ -248,7 +258,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 +357,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 +369,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/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}
- +
diff --git a/askomics/tasks.py b/askomics/tasks.py index e0059151..e0f3b1c9 100644 --- a/askomics/tasks.py +++ b/askomics/tasks.py @@ -288,3 +288,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/tests/conftest.py b/tests/conftest.py index 5394e6bd..fb30c607 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -273,6 +273,24 @@ def upload_file(self, file_path): "file_date": filedate } + def upload_file_url(self, file_url): + """Summary + + Parameters + ---------- + file_path : TYPE + Description + + Returns + ------- + TYPE + Description + """ + + files = FilesHandler(self.app, self.session) + files.download_url(file_url, "1") + return files.date + def integrate_file(self, info, public=False): """Summary diff --git a/tests/test_api_admin.py b/tests/test_api_admin.py index 1a31748d..a6adf370 100644 --- a/tests/test_api_admin.py +++ b/tests/test_api_admin.py @@ -70,7 +70,8 @@ def test_get_files(self, client): 'name': 'transcripts.tsv', 'size': 2264, 'type': 'csv/tsv', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }, { 'date': info["de"]["upload"]["file_date"], @@ -78,7 +79,8 @@ def test_get_files(self, client): 'name': 'de.tsv', 'size': 819, 'type': 'csv/tsv', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }, { 'date': info["qtl"]["upload"]["file_date"], @@ -86,7 +88,8 @@ def test_get_files(self, client): 'name': 'qtl.tsv', 'size': 99, 'type': 'csv/tsv', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }, { 'date': info["gene"]["upload"]["file_date"], @@ -94,7 +97,8 @@ def test_get_files(self, client): 'name': 'gene.gff3', 'size': 2555, 'type': 'gff/gff3', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], @@ -102,7 +106,8 @@ def test_get_files(self, client): 'name': 'gene.bed', 'size': 689, 'type': 'bed', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }] } @@ -437,7 +442,8 @@ def test_delete_files(self, client): 'name': 'qtl.tsv', 'size': 99, 'type': 'csv/tsv', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }, { 'date': info["gene"]["upload"]["file_date"], @@ -445,7 +451,8 @@ def test_delete_files(self, client): 'name': 'gene.gff3', 'size': 2555, 'type': 'gff/gff3', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], @@ -453,7 +460,8 @@ def test_delete_files(self, client): 'name': 'gene.bed', 'size': 689, 'type': 'bed', - 'user': 'jsmith' + 'user': 'jsmith', + 'status': 'available' }] } diff --git a/tests/test_api_file.py b/tests/test_api_file.py index a91bbdeb..829ce1b1 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -26,31 +26,36 @@ def test_get_files(self, client): 'id': 1, 'name': 'transcripts.tsv', 'size': 2264, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["de"]["upload"]["file_date"], 'id': 2, 'name': 'de.tsv', 'size': 819, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["qtl"]["upload"]["file_date"], 'id': 3, 'name': 'qtl.tsv', 'size': 99, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["gene"]["upload"]["file_date"], 'id': 4, 'name': 'gene.gff3', 'size': 2555, - 'type': 'gff/gff3' + 'type': 'gff/gff3', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], 'id': 5, 'name': 'gene.bed', 'size': 689, - 'type': 'bed' + 'type': 'bed', + 'status': 'available' }] } @@ -74,7 +79,8 @@ def test_get_files(self, client): 'id': 1, 'name': 'transcripts.tsv', 'size': 2264, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }] } @@ -84,7 +90,30 @@ def test_get_files(self, client): 'diskSpace': client.get_size_occupied_by_user(), 'error': False, 'errorMessage': '', - 'files': [] + 'files': [], + } + + def test_get_files_upload(self, client): + """test the /api/files route after an url upload""" + client.create_two_users() + client.log_user("jdoe") + date = client.upload_file_url("https://raw.githubusercontent.com/askomics/demo-data/master/Example/gene.tsv") + + response = client.client.get('/api/files') + + assert response.status_code == 200 + assert response.json == { + 'diskSpace': client.get_size_occupied_by_user(), + 'error': False, + 'errorMessage': '', + 'files': [{ + 'date': date, + 'id': 1, + 'name': 'gene.tsv', + 'size': 369, + 'type': 'csv/tsv', + 'status': 'available' + }] } def test_edit_file(self, client): @@ -106,31 +135,36 @@ def test_edit_file(self, client): 'id': 1, 'name': 'new name.tsv', 'size': 2264, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["de"]["upload"]["file_date"], 'id': 2, 'name': 'de.tsv', 'size': 819, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["qtl"]["upload"]["file_date"], 'id': 3, 'name': 'qtl.tsv', 'size': 99, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["gene"]["upload"]["file_date"], 'id': 4, 'name': 'gene.gff3', 'size': 2555, - 'type': 'gff/gff3' + 'type': 'gff/gff3', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], 'id': 5, 'name': 'gene.bed', 'size': 689, - 'type': 'bed' + 'type': 'bed', + 'status': 'available' }] } @@ -323,7 +357,6 @@ def test_upload_url(self, client): response = client.client.get("/api/files") assert response.status_code == 200 - assert len(response.json["files"]) == 1 def test_get_preview(self, client): """Test /api/files/preview route""" @@ -427,25 +460,29 @@ def test_delete_files(self, client): 'id': 2, 'name': 'de.tsv', 'size': 819, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["qtl"]["upload"]["file_date"], 'id': 3, 'name': 'qtl.tsv', 'size': 99, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["gene"]["upload"]["file_date"], 'id': 4, 'name': 'gene.gff3', 'size': 2555, - 'type': 'gff/gff3' + 'type': 'gff/gff3', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], 'id': 5, 'name': 'gene.bed', 'size': 689, - 'type': 'bed' + 'type': 'bed', + 'status': 'available' }] } @@ -459,19 +496,22 @@ def test_delete_files(self, client): 'id': 3, 'name': 'qtl.tsv', 'size': 99, - 'type': 'csv/tsv' + 'type': 'csv/tsv', + 'status': 'available' }, { 'date': info["gene"]["upload"]["file_date"], 'id': 4, 'name': 'gene.gff3', 'size': 2555, - 'type': 'gff/gff3' + 'type': 'gff/gff3', + 'status': 'available' }, { 'date': info["bed"]["upload"]["file_date"], 'id': 5, 'name': 'gene.bed', 'size': 689, - 'type': 'bed' + 'type': 'bed', + 'status': 'available' }] } From 97c3f74dcc737a49695beb24ec83de5b31b4afd2 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 4 Oct 2021 16:15:50 +0200 Subject: [PATCH 102/318] Update documentation (#284) --- README.md | 1 + docs/abstraction.md | 6 +- docs/ci.md | 2 +- docs/cli.md | 13 ++ docs/configure.md | 2 +- docs/console.md | 51 ++++++ docs/contribute.md | 6 +- docs/data.md | 175 +++++++++++++++++- 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/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 | 16 +- docs/issues.md | 1 - docs/manage.md | 32 +++- docs/production-deployment.md | 8 +- docs/query.md | 299 +++++++++++++++++++++++++++++++ docs/requirements.txt | 1 + docs/results.md | 79 ++++++++ docs/style.css | 23 ++- docs/template.md | 98 ++++++++++ docs/tutorial.md | 115 +++++++----- mkdocs.yml | 16 +- 43 files changed, 905 insertions(+), 108 deletions(-) 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/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/query.md create mode 100644 docs/results.md create mode 100644 docs/template.md 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/docs/abstraction.md b/docs/abstraction.md index dec0278b..c61b3940 100644 --- a/docs/abstraction.md +++ b/docs/abstraction.md @@ -1,6 +1,6 @@ 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. # Namespaces @@ -19,7 +19,7 @@ PREFIX xsd: ```
-!!! info +!!! note "Info" Namespaces `:` and `askomics:` are defined in the AskOmics config file (`config/askomics.ini`) # Entity @@ -34,7 +34,7 @@ The entity is a class. In the query builder, it is represented with a graph node ```
-!!! 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. # Attributes 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..8d7e552f 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 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..477ee820 100644 --- a/docs/data.md +++ b/docs/data.md @@ -1,5 +1,176 @@ -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 do 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 label + +The values of the first column will also be transformed into the generated instances's label. + +* 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*. + +### 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 + +## 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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≈vxa6B0XR3}>@*U|0r>~@oZHP~3SDad zd7G@m)+duONqPn*mwB$(81~<7nkyTRwoU?!dL_UKe=z3~%*jx<3_8weksBBA)&xn{ z1PS0vfQ50ycLwA*jk6-U>F{4%O7f>OX>U1!tx&)dC{9P}VIWz5>-T66J0pt*BTc5x zm0gg;srC&5_78hs97q;QcIC88s=5vV^V@MD@#z^ehKvLBcus~*j|kLT7U|K+F1rm96KE`YdxOX(*{{u~vkss`$pEWzWgDIr>uR*pDBgUhfmjKE zQKDKGtpYYUI+LFc;P6US+mzg7 zVCE7MwQhQQ4d0BL_W^)kr3h0`O9WHwEL6_N`ztN@aAxy2PbTgC+ZVWgoD*T{IX60m z^MJ}~bbEMzb*ATgKEdEKEiMDl__ev4fTou0*~o3?&0$|Qli!fp=d(qE63`t`xVO6m zAaz`QvH@sXdINHH{Uu0Xl?~QMIMl8e8rv4?$rK`iSf5|m&aK?EYR4HDPv(@()uXN` zxf+E@ZJgDxR%VEcbrIZQ=m+pw%1r$3_qL>5Mhj72UY#Uf z>9aXR&+_yD>GlXsJ$)q_WM#6ms)Qj+ph_3r+P`2r)6@A~0k}OK02vq8j$LznxK+^w zC>HB9z0L?TpJQMv_&D6o@A*BZwvD)Q3^*5;rrlb3*ijZ00hRHqB2z3?j2!)zLXEe# zP(O1*jup*JvFqaZ%I!NW`PF3wB!?StAC83hlw9Ctq?!ewYC*to3*uXtA4?G!2@mBc zt=5chQEqGaz@k${nyC{=ay}>T20BvN84RtK@KEJ~M1In4#d!`NQ6=1{0-txVA~J>x zVI8B)yCUz!0fGz-ko2$N{blc=3$M%G-B(G(3!K{}6vbS$XY`k`Xojm^6&{F{S3 zc-3MW0L28EWqGzO16?ZySMF>3E~(Q2YnNZy3Tq@X6DNKdae{(3;Jm5Q{_=fixaoe1Uh`ot!)GS$?t3kL#vA(CK)` z_CUGGgl8iJbx{E8Z~LwOm9m<|$fJ=!A^l=p#cD56%V+5|;;v=8x@c>Lj^)a1pJd$L zcZA;@6CbKH1El9i_FQ6tAZ$mpRk$Fx~?NgVGi;=$S_^Q*EXS)eo+CgZ=PA zMJ1n54(LjYUtXr#+XCm`;-wziKA0h^`p%$zsH%Dwcr_s=e1coQXvd_~zYZ=&P`CDN zIm~yy8xH)Kj@10cV0&+Yxs~a~>eJkttX1_L&WSeZuj6lhysdJ#uX?RD?8HEZItB_{ zmL&+>05e7-S>KT(2YO5bsV$?B#29(#PT8)4^cf2i@U;{pvpnZjU$f%6zP#ZTPqkl;l&oxR9hNB*RhotEP+?;5)hWyqt@5T zUZ8yKPL34yw-jBkFa#rW!8gAaJiNK~zJ@)hOVgL|5Da}0
BfA^{LOFc8SVUMQv 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≷+ONvF&zP5)Syos+jtlyVa_L~{b8~YWko%{t zs}UgRIB5!CbV;DMz|y`p^GQf>EFQdIqFtiir^sCAAKI_aJ9LC*^lV+8v z6aic*(|R*U04`s;qoyDT| zQnRSCIPAU|)V!FE+G}iTMx!q_e~N?`4y@S;)!sqXJoIQbK>CHcRBC%OaJZe?{!UXq zyV4wrgbFU64X!#eI#@qn*T~8ADzOTl#Q_QHOe`=zq=B#>*wyU<3D?Kr*ESc$59(bv z5?f8$Xy%DUZ85_o1QurQq@NB4YR-Iv240mPM1+NsH<_Q+c$z$P(``P*%U5Na*)bv; z*P2Rtg5Sa+P(gMv_4FK`ur6L%<0`48xioZDr6jd4?gvD9c4S0Dmv$Fj}2pAR`kb%yD1Wi2)LL!USBGC^vfak^gyP zxqi+M6uhpVrhpr;a^Dj!4fp@{-HZR};j9}7K>si%{VV+655LU!Q^=^h3pDeOTX+BV zj-5FSCdxpvI|nEc+}wYQMt^Ee3_uSpg8mj6hKIEqW;KG2=zwDT%XGy*C8j!Gtf!Nc zAT7~O?0-reWd`mrUJsI`JqBJ6+$sd*j}%gZ0N@58&^_Q*BY7yg0tfWnlk<1}r^_^n z87X98IA52Lkg%NOgb9$Pr`)*==nvm|h8E;?{C7TfiVd$`94$i)2mT F{x8wH(A@w4 literal 0 HcmV?d00001 diff --git a/docs/img/csv_convert.png b/docs/img/csv_convert.png new file mode 100644 index 0000000000000000000000000000000000000000..d27ecb489dc613d7f5d8e906401311bbc74067d9 GIT binary patch literal 251893 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 literal 0 HcmV?d00001 diff --git a/docs/img/custom_nodes.png b/docs/img/custom_nodes.png new file mode 100644 index 0000000000000000000000000000000000000000..65429b9f4d17ebb7983a0b8833664376f534b21d GIT binary patch literal 5745 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 literal 11895 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 DJ|O$! diff --git a/docs/img/faldo.png b/docs/img/faldo.png new file mode 100644 index 0000000000000000000000000000000000000000..791f9076c26211e504af8fa0c9eb0dad3750ecb2 GIT binary patch literal 18666 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

RiG$ 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 literal 0 HcmV?d00001 diff --git a/docs/img/form_edit.png b/docs/img/form_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..c3c8cf67410eb46c873a92f1d62473d4e867c86d GIT binary patch literal 12392 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�(Am7p;+g=K^nb;UL`r5psLp>9iwYjlP6hiqIn# zqwBtuTH4yaU}KB3mcFJe0IJKxB%|GQ;?HQH{zJyYQEusH~g} z<#iFFQqF(qKDx|V2(&3G+Po&)NJD}Evh9`H@8x}f|I$=f z=it7%A^k(T{79<6u>8VD)X?Q477aEa3Lw7z#rt6w5lC5E7vvUC-DtN{L$v8NP&0Ze z7joEOi3+cTKCoZxmtuhThn$LdA9k;!Mn*Mf_kc^fQO z!w)aJWazjDx^KD%-2;*nw8CSd|Mi8O+Ow-WEMlT9R(K=m`x&xQiju|RM*jZ`&Yx6- literal 0 HcmV?d00001 diff --git a/docs/img/form_example.png b/docs/img/form_example.png new file mode 100644 index 0000000000000000000000000000000000000000..76bf4709858c9f0ded3a0e63c8c347a425a88408 GIT binary patch literal 12575 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 WUNF=!_zvAfKtfbjq)1rD^S=RIY^GiS literal 0 HcmV?d00001 diff --git a/docs/img/gff.png b/docs/img/gff.png index a12eac4915caf4bd57e99d2429549bd075a989b6..22872e7fc55e9b29d1c124160aed60e9719ba5e3 100644 GIT binary patch literal 2275 zcmb_eXEYlO7Y;Fkio|?TD@I%6MQl})P_=8stkqJhYP@!16|ovg6(v=pwohBMXuVd^ zCR(Gd-Ikh-5kb*Vgg4)>@6Y$|`|dgSKIb|2p6Aa!_c=Gq*5(Q?w-`470N^z@GqGo4 z1T#HB9Lx#{R{}GEEdphZ1~DTZ2IX?1#}KYK zRaoihr$ZZqR58RI1Uui6}#MRr8ux642tN+S?lr0h(f- zmzA}#_M*6AozOFr>T0Dhvl`P~5QQQP+0iY8u=4Qm1ZWc2&XFPM^E}e{B?JhH*2Pmb zJWc(yeLcI2))b80QT#93Xvu`0(ek;hyjqC15$lM?ECjB4w!qPU@3!HI#j&2Oz=f?& z9vz~Hfk8r3NWX%?+WMkIDhJ!3pyG>;JyPjR!gOA7# zFM~9k20<~~16w{zyf;itRbb5xQ`>kXFeStqki3N3|Fyz6$_WSwM7pfHryg=Gkr#Hx zre2|W1A>oLQN}B%hKco~SWMian@36F(Fz#Mp&oqO3lS0VviuKOiC%aXv}Z6RD2Nq+ z6V<4hGMq*6=S{-Y1D_#_yBO1kOED{l4mWUe9MUiM?}fU05sZZp|BTuc-S$QP@6l&m|@n;N!$3#pNb@6ZK1kVYz<*yy`0quMR6c?72&) z)1^Y>xs6icZ=`r&ec?Z-4TpxIjV|DNlav_-XPERU#54XgnAvJpkSUG%PRdlDf$jxs ztA^_M!r2w9ia-vfWm;~}g!eaR`wNsrAOgD`k~9ek2{jFkR3ees9kM2_uBM|y?(Qnv zw$5R>t1TKUVs!l-MN^6*UFziiK`$1lvVXcpf*_ zUp_f8kf~Rf4)bekZCzNO7__Yq`I$JWSYV+Z_d>vLcqch0$C?n`>XuU?wb@ty@oHst ztB^yQ>L!KQ~nJb!NPPhiDO(H9frCXNBZo@NB_?eEO7LV!ddf1mAWirN1&*uslY=y}Ng zV5(+eX%@qK7!?(@(8asfxfC58T~SfNuU`WGan43ekJ{^F(g>8xHG`b3GYKo_|M$`& zFxvSn5%)Vndyq!WE;t?$)dd$j^~To)j!79cmB5q4mv-*jYnRpz!F zL%k3A+IF(-+}*g_hk%2g1XAnYR9U|`B2yOx0E-Cr&3zPM&)|c}Ux1Rcv!Rf)tbO4J z)P?xqZ;$S-?`jPUec047ax?}qFQAG)3(275P=7M^3Awq%iIszglTPtbn+6mt_Dxco zDs#l*1AhOMbOy*FohNR)c5hA@_x6?@&w}nv?uT8HygJSzX4Uppp)MJL4keJYf|<_! zE0s0$lwMtY*rx#f^3Rd;i9WJmiWDl`T(qK4dA*sBGb1aL;E`3Cq|}GKS9oW*y7O4B z#Z9FTW$Yaif-gtNTUZdpe{G$nzPtY%IG$5bP|({}QlcWq&t(G3?H(bKIIK@!DP~zr z07PQi6R*kf%TR`InweX~Zzg`vjuWW9Np5CZ=i~EAcS6b*N8ztO{?)IG!2l300iX{b zuFfx34{oCe$}al7R#UcliJ~)xK>2nH#Eo9}??lO@VzKPmmKmqmPffbR;CwqHadxab2i84(u3HF5 zzTnWLg}GAZvkd3!6mEqi{a*E%X~#^JKU=PE+Mwx$g%|d}^8jOCN$Fp_7`}sMFExmR zpC?~eHXR?@;1LiIkW+a3q=sIm>+CV+Ki4uYXr`=^o3xsVObB$zqn0e+Uhjw?Rj}2z74cqjc z@zBD2$&A)ktv(cmzIyA%jg&fU?!k_ff81;>Sje~=4C#+O^IIQrW=Y1ZrlH}YF24U4 zb&9PKxV)}^3&90H=$~Y%0MRXh{~u@gFV=Fxi9v(IxEy;a%&!Gte%Z$4Im$cbAJ;A~ AJpcdz literal 2854 zcmds3X;c$g7Eap=D8tf#Fo3M$G6u+qEQUoy2m%5wElb2eBO+^PB$^Pm*w9LY1_bQT z>?)%ughUb`?9h!6b_5ba42!bJmJot0fh1&#K4;G9Kl68f%sEx{s_MS;?)ScT?{{z7 z70-(bhqMlXKp+KI7pH3=(EfDbJxp#N@chUPC<2Cq(JpWl2&DLa@BZdju%b2ybZE@g z$>DlzA#VbC@7Y|Q?CJ@3eR zsJw8M28LZ>@jeNqM&5XHyc%Oo!gV`BTpeO3AN(e=H zHBFopa?Tbt#gg<{=t<|3z7!L0n`r{LR)Zr1!B)V_dF5Y1fWg3^_!|)D=j2!!5J>sZ zG%)4-$v+J*^#5vLYl5c*!q*l!2x2nv8heD(HlNw68H`UUd@=10e&;u&W+bqvL zpenFkyCi`^tF9&@Iq0yS@Ca#LlB5EQ#lm169v=-rpf2&rEJ9o6ncDj718M5eO8N+DUAit7HRm&BRaG~Gt z?&|H#DQW#Px~usNU&I}&TE7Ii%x`q=pmyG-K2v3#tBMw=%4^_v2E0)7wqsR}sR8Gz z-X(>@U~|H>90>SSoOnJ{vT;mf3Yv85{sxJ`_@m?UToGnczBkvi#4?z@HlJ(aSA6#% zz$BHhJ=>`-m)$K>+o5$-HxqJ0%&;iE*q?v*vhx0;y}g?(MkKqzLN3|K@N~#ZJMibu z_!&F7~~O3xY8WKpOl(FS}7!wsf9)Chc+aKRz{Li zB3L`ebgUDQQ!R;_xd0315%_UCo9@`n#j+9^hv=31`Qo#`y4_Sg;ZbxpEL`$~|0Fg{ zONcW8&LPy_li!Xn8sMpkq27s}1(wKU{W{#}la|CChEi>xCu`8vcyF4iCx3;#U1Wn< zVubM;Hl|ML6l-lSDB)ZX>n(45qRbm2SSwXl6TfLeV^@Dz9<3Z1=NS?9plFm?71x9g zO^Yz&yPpPi!&$Y6K!@E&G|LtW8nJZuePLmt05buN9~kFyyFP%^xAO%%-1`u5Df9_Sas5eSlfDFn_4DwngM0z6AIx z*2bGAti3HG*v1b)b;z1_ODhLwMldlde@4ke;uD*o$LjrqgAnfUTr#)1b*;RjfS^(qS%z+UV_Q@Khh_yNnRD+k2S<=Y9b)Ngz4Q)@uSoba(Q{t#f&3q zPKIGUKvu0xG{&vhEudylU+gG}B2iFO(~BUw;&Onf-%s1h1RFE-kD9*$nd|ioyTxt$ z>05p??N!50iZAy{K4pA-JZd7&KW_2CCXb_M@9*a)8XMHZZU5@C#bT^4v4g~!Nxs$z zzN+SmzzWQ4=w4Ds%Tx;~fF6PL<=&qGBIv2|AnQUmKeMU)_5VZb&$qozB#2YOjH^<$Ra3fli5U|_k%#6`g+CDK>(iY>|qxp8-lk{ zHsac~2Go4U_lvK4x91_hNrlPVnNwB~oewp3nX_+4aHnw&5^hxxeI*YHmgr>Y+BKkg z4Y9T@JFhIth5<=v(n6A#N5#b4s&XtF_Au8n(eb`!O`HY7Q8)g! zfAV`ogiN_6kj5Al_QbD9go2+aSxO zunzs{>2%5XRvVyI^0EPwG~DOv#Vy9@=s?`3GKqZZdavF+*3YgM7P2i1C|PxN2II)t z_4%o$`=U-Ow@3a0Al2Ncw_z}t!|nsWC|xR2Bw{ifwZ8HIPk%hWDj=R_8^8K}?ozG^ zq@^j8JzQ4io-h34G58UIa7;BVzyz_j>PWR}s<}Hbjty9^XEvheoJ;G6LM2@i`#eFJ zy-uE8NL0#(C?b3^_G@L<(Vlb&xCig-<4p?hqwR%XigPE`>1cq0sg$!!lB{g7HWnr& zgaZ2dGUtSq6=t+A9%fycmL@8E>vszAIOnduSr_B$Bf zbu%bKa%wkNIyUHEoJ-+w_*)u}LzCW{YT02gEg=$jH2y`RQYf7h=-y|ps`eNfAKzH{ zGKL9MllZr59fKZ~Z{BcmVXXsLtb_8ClaPv=l3@-%Qq$Kr`cyx#!9vZD>Cc@{tjMh^ zA2*Wc2JNWg`jFQidC%OBlXo_MQI*0f0r8N=N{LG46&l8IYeIqaqhzX8B&q$}?siS| zWDJXZI~8ri%WaJxjrKFS;a(Wc z>hx$$M74Ly$iKc^pn@E^(@h;@W?sKeGVRiP`Z3{B^PT=CT5auS%~aFmb;MqMK4*Xr z0XA+7X%rSr6>hn-YlreUI39hZ3}DoF1Km4{kl0IGID3P7xj1fSO25S5s60Egl+Q)Cs~5oDq-lZ0seF4#vm z;KNJU<7IN==7v-_XfDm!Yh+a5nzSZ$ll+pBfU#G-2iWYz3@q`lO6`z?mID;UyDQ;c z(qM5j(ADTdV`kqws%2_q(lPu=W+@%!!2&e04Q_<8UFOhmhFP&v39@SJg?Vs&ef>nk z2A8un{CXL{{mD3c`?E7Z-M|B99R=W2+7^%hwRifjP1XO4K`OgCu3Tam%U=LU0J*|E Kok$mcNd7l2yGyeG diff --git a/docs/img/gff_preview.png b/docs/img/gff_preview.png index 90653374da51d7627430bda5a19fac5b90021748..eb8a8127aad707256be4ac68dc0d5e4074dd83ce 100644 GIT binary patch literal 11892 zcmd_QWl$Y$&@H-w;BJAS0YXA>2<{HS-Q6X)JHZ1%6Wk%VySqCC4Q|0TxSxmjeE0l4 zb^qO}OHshE_pX`g>0Z6o>i#A#D~^gpfCPa+P(MqEC_*66G~n+j1UT@MyCvlo{Cedm z^jR4Je0d=lhe9B4A)iGAmEBU0mR+^6cSzw+(R(ueGv(=QXvmqBYa)@t{2SnpTd&g# zoX0iUElZ1!d8xyY@#t|nTgA2BSoSKw;(mSm?)DMWaSMe4@vVh##`QsO_||b_^NvHp z@O8TLd&yiyL~`&&VUoP~<}LVR2q6F${9?95Q4I|Z9VlQ@Cj*B$efmM*@H@f({YA*x z8De8v&@)l4;m8QOFn%6W02AEkYhxz;imY@ z$@AbPA^T*(XXK?eM|Pu#uEXbZK=HUH3{7jk-cNh~;T(?&-~El1 z5NE2aoNqB997{v*n=w=*73-mn%IEl6E}btx`h7sHGnsOcI_A-0o#3VzYL%nfC+A}? zMwi2-2(9{)wdMQkii!$9BUt#FD~oSvM~gz_F~1tM-P~TyTF(bYPsq3UJfiwg-4-+#+z0s6TNSI;MS{kKc~amxI@>|8!i8n#wNxS|;1ty2j76uY7b-}fV6Pjw9)6pHO2(P>U$@p%E z#_{AI$-Q6cHH2d3^pPKLicel7&Z`Fo>xh}pn;b6H5^AxPg_IuEbCrH#V#17xQ3(D1 zy=%q$ex2JCCm4;SAda5pkU2OKKjQjiH9W5!+_G#2XZV!aShjr*oO+uR;>a(>ZdXNk z$y}^YrjLO~+^*m9d~Z&n)XH?b~tf8_f!8rCqx{`e7OF*rEb z;CaG#ywV)?2nAnhv}ii7u62-_5l)uCsDH``@&<8grLSjgtMF|q5?2Ie_;yaMoK)#w z2XcnzljPf)&Kz-H2!v#%^;X5d^@m`7(A%EDLCp3WIW6et(%`d8+N8}>~P?#|^WoCaL&=tVJ^ z@7htZ(Em)P=J?s4sxif6HtRH!&WYL`f+mKmHj~z_SlDCOZqk77@VRE>5gvUqe^r1? zzEHCl*GN37=c4e(M6sDG4sAhGIEOtmf}>Az0>yp#;In|AokD$0O;4zS@acgAzjO*S zX0B8mG(@}61C>U#>@^)89$v^9zwB%OfOxuOgQQjRkyJihN~w5ACUucsh5U8%cs&yn zKGT94g7QM0MUd#Zuh>aCufC-v62OLUKl0a4+B0_Cbg5iL#KaKGJzY+M9sVBbzmE%p z&K>!U?;&eux^Bo15m=lpl`qT=Cu=ryYT-HP>{}qXJ>L-@-j;FX=7vC&l$50L*+y>4S|zr5OdnpO+nM3+Y;Lvr!u%w~+2*gpn`K{qh<<0Q@cD8rvnyCnvr;gJz z@oaJ^zJwtfTHG9OCo#HQxKwYp4ALDYtog8yz;f4%3~E;PlgvP_e2dp5lHY!})Ns$> zRA8UEK|;CtsH=S`95=jKgS%T;-tb?CFUxJ%)(fsez0@-boxQju>8Ol88^(bl;`mik zke-N5o0TS103VqKQ?Knx)M4X_X9ccxz3u<*vqFlnTTlEt3pBe?akaJAmmA*9KGcyo zH1f<{ot5`Tt#ccYTh5h6A<>r=D7L=#xY`Q=SYVmU%A#j6c(m#E*eAo|c7p+t&la(l zzOzqNg;<-I1lhM41zz{drT=z+giMs%+OA)c`aV#$prJN@4?!n|l8+nuPQWB>^(U56 zDpRgJz3aYPJQAtgaU=EI;t$cLcfbC|E4@ikVtH@A-Y{HPNXt{UcSnCz;WF|}?&sut z`RUo$5DZe%44c8}1Qw|*7JgyK?nE1%;pff!{)X&V;Pel81Fr8UsHo`s5J`J=3qb%xWOkye}06rb1lw|>+=OTs}iHr+C-E|c*M)vsT_5T-y=01?H(6Bc4})A!B7AHz4^v7bgs)MBwhvf#27 zYB}&+9}x6~2s}MB%~`?t#l@KT?Cz2u;^%PnSL8gEhz6m2#$CmegqTkIOBxZ)u2+Aa?t8s9FCo7yQvin{|E}Dg zcnB&hPcR-gV8ife;*H#IHRsT3lN^1pSn<8jujruBi2H7l@z&a16AD^0$lUMG%lmg{ z>f6l;Gh%-Pu@Ql`H~g&+kH*lkzV1UksH8!-I6JzbGz~Rq@&enBJ$hc9^P?0%|;Fw4%MtNhZ9hFzWD3MU{A`w(Np*qGC}jH>Bf82 zXB}CmTFm)yp$#?!=7GuKMjldx+;VdBO{lUZYwP33#OHIORDN%)`>R8d+LK~vd05Dz z%b|ac5$xf76XK3+Mn|mZc?^RdAEaZ&`$^;YDer`@&Et~5$m_#fN=jH$#gB04hf#XI z8eydU_ee*S0AzG8x|0{H&N-fL)hs`Yi?uofX2HSD+4*M>(WG6f-M~$AEKTasA- zWxFmdjc7`z%^^2zzRiT9&h}3z=*D?T?>~$;{`&Q9snHWXAUG1EXjlx5kXmwcYb#Wh zPOISR+KW-_`&Dlih4>36a60*e=g>$5Vu;{Kc1th44&jKBcoM={R<6^p~e0iw+m7hk(8!LOnem#B zLTeSS8T^rH9EcDO2RnWt|M8AC?ft0RKbe~dhuOD9)`@(&TzXE}g>d`5t_JZQ{wBEB|v8{f?7IS2slo^HxJ>%ScgG%W}7*?-JZd4~8 zeVC1yUi*_c)lBKpF`U(b(3|6swnZ0v7pz%#Kp!KO|QmQ?~H|SFSq1cMkxXg z+r=t5IykAYTj_k3-7ro02`;W=z?j+T%j)aq>j$(X>EI5d{>e&jZUZ0vHTx_|HyOPUjGu&sWTq zG+bHwV?M1sV5)NyW)ccibcj&Cbrww>T3fQYHr8Ds8GyG&`)^F~IZxe<{7B(p*WCq~2Ikv?Vd?e~DyAQmOFG`IVPo73Ksf?K;` zBWVjETkrX?l>Gdqu(>x#)UK&dK>ku@DDPIc$~nKy4&pMMbQC z+oR#9p$J|#nN1{Cbq7krDeMfAI1G{bBY;g8RAx zfMDf-8jq$NMEN6~jHH=+HO}YtPJX)79%M!6%{dhLlUMZLaG4m}@X(cn$7Akw=WiRm zwLbVm<~VzOJ$>Xf7t8%h`LZzbkD0X=l=SaQHujM+UXQLHt0Jq$t7;6XxeSeM7W|=P zlOwTz;O%qum-j8Yog2xQNj~86q*s?vD)UEP;^BFL@+^F}flR0ZwK-vl@}v_U_Y2ZZ z!Kpc}of2eRp*AmzX9>|j@ii|yyX132qt*3+Nu%uM=8>AK&%C5Nbn1Z>!)oA(=8v~l ziG>EU-TZqPId1VPwF=SnW{{h_f4Du*aW0rQciJ&&fZ2L`)D;>AbomEL5pWo{>mK!jWgqjGQ@l{hb*bCz4deQ7?R7cjNvf z^rA!LGI)je6mw6v_LQ{S?a$A>W^}S?neP4L%5=C4R-a&^2aRu>X9y1GYYC5SYzoxs z(RdC#GVRv8{?yUQ00hp<%iCky9?r2>+;@bM3Fo_~N*Knx+(LxRpeHvih)*%Mi)swX z$qs;rr7@kDDkFNMR|OO0Lb;u9qrTL**|wA0?0LuiHRwf2OHT_sRav%S5%Oru<_|g&)B4Uxigr-r;&(r}a8y#*IPR=}4PM(sh-81d=pGkcFEI6Mhp`P71V!w(hq0+r_k$=oiTK(2;Cv`AbcJN;D>x4CQR%%*NG0yh-w_p)D8LhyW2 zSuTrt<;e4QtD_JJjvy#+a2e|XGo$^5nd$wf%WHl@aK6#{lZ(wFN_#J|)6iT?hAFb| z{>FJjLjy~3B1=HGTDkMdx6|4JdJdb@vXdY|m^*{#lja~EhJRYr)Nu=KMhT}$a>qlB9_aD($A}kP)42zHGu~*CUZW~-!tZ9`lF$v1zT>4< zc*yD1KM|OdJBZ?9sdjrIb%9;^rEnAK>I_qW_IQd%_dlfm9#$Mhe=Q-q-JzY| zto-Qn`=;X@_os(0=45O3C)BC~4lXX$j;!(JueM7ql$O)hdS$5-1@b!3aWRIyVcAOk zS6%m8KaHg`3N4A-=WXAx+1|+fZ1ZyW%u;8`Ii5u);p+ih910Rg)1)u(+?KRW`C)#+ zCmisQP2~xTm-9yfPLTy`W1&3grLp?O(9oZ&cKO8(p~690YV%avbxC~#ES!-PZg`~+ zazfKTK13%bXKQnEzw^EeJj4C~GjSXfqvSq#WF=nGw`}Zl{*di{w>2tF-J8Ec4S0C~QGCyx@9jI5R9LD4R4-i4oX@muJV<*{s@oMis`R^WmGoB_9Ea{wxnC7OZ zr`I-TTQrnPv|C^M6;bc@N~zd#d@962C72$cw4N(F3eIoNSiid@?q(~;e->O@YUPwMBnd%*)9Vx5uj%SD)-G;>pl?MW6 zFE2^=hv_}EPWJlNmhlp3_3XCGV#dZeVK^VZ*I8>CocX&WaQecr78PsOii?Tma?%Ug z4D5OTlz1nEH?T7~85$lwrMa_C@pZn^ianA@#yYdWwv9P{!YDvfhG|zc}dz{iqp`ksTX2xH~5+p#g@oS+7uk(en<8S8`|CN*x|M30=tH zSM2BfsMPec(_N`vO;TFAr+BVh;g`g^(=&>@?pVU~Tq3*e>b^=*JaLtSY<|CH4$hr0 z!Q9BmQd`>Ht951*$%B^Etp3qT{{Xuw=j|o;RPJ)>!2+#0n0VOVB<2jwb)`0*SlEQ& zGFo*|e)7=j4R!_%J~js%^@n(+CcpokIev?7iy9pGcj|JvKZ_YQG!v8Y;xI<2WWH%F z6vt9-_TDL*6_qSh&UUO%H=nCSzC$OWE*c)s;BVhWqb(&pS~_LDs{C5N-}CwNXG}@_ zY4V(O{gDR5L^h+k-81;RZJPRPOnKfz+EV%^S=q?01PZn*;=b=G(WXx`I=Gi)agw4i zt#q1I$}BIUXPq4D&7}xFw&2wH&gA^VtrQxBFQa*D`xVH-1O~m0t0Qg>pMO7R&)f*H z111<}(9=c8F`c{X=J4@p)}q}mERv+(qliNUtT0M1B4s=xiFn_msH+qye=c1?`ICkx zJl0s^BOXc2pA~02s?fUZbJMxjVsYhh3tD3-o0NiFdggN`S@w!0?uX9Hv&b(HBTOF|TSV4Fxy2y7mvu^0Ar;rb_gM0qF3Oy! z7`_2=6XJ9L`*X&+d9H}^XIBCIsxmx{U(st=2)$k-l7Nb{a-P#2R4Ui}Onv{C_~1o& zxkS%dt)?NFP5Pyvu$YrjR=w5Wd>=3IfoC>J9mDkLlko8C z(F)*u06WlN;8bCExtjaSmoKMl>%w1FitYpletbYLGF1wHD!C)LpU-!dJ6x#71Jj1Y zPp*8rEpCW-cxDfe*N$K2Ycv{eoKF`1ta<&t;^XtYV%Mxa@N+#`2@kebOW}7#)2y=y z0f(E1jruI+OGKq!nwiRjv4HHIu60sO!sfX#EfW&eS^N!{v6>@eW=5K`UFu1y-ceXB zCV{BL_4vE|P;S60~`IR;zK*WPe7hMrx#&2uB&|4jbZ6pNmqERdF9abxs z&#&X=^LYp*=2mLvZQ|hmmYx&)rxHq8#Wu02s{nCv8P$U%`pv>2(WKj$)Vp ztHga*q4>wKwU*1h8EoJScBio0+T9MRIPEXM z5Oddrm_Q*fH9tcZT@Sl}u>AgPk2PDWL%CHb5kn49=I6l9mljT2I0p6s>U;uC!n*z8 zpH{-Ulcfltd%rB#!H}-_3Rt_c_q2J2=rmt%xmko~0m-PHnkL`T1x>oS`H7g$^WZA& z#o;OUa6UUT;$gE{p?(#CO6Z0>Ui3RCs~=VV5=rxFxLhdOz|=Gt?($PP^w54CGOi+P zybAoc`1trPe0jejWo6z8!f5-#p<=c2AgipDlw(1XXw*)m%%Xo9tuAS&ZQji_%|Ske zpa7@uvfg*m>j?rCpj!Ib8eQMQ0#QmzsuU}BPwD-76#r8XV4n8WyDYM>X|tLq(}UPS zF^rCjbMyT`yciC4-Qx3E((-xVy;}%gIL!(W6+H^xqG7*2TZkYO>LGQyE15+%{D#$A zX|}_LLck0SD()??OD8!C3pIZ9CFb`JqyvU&Bo^5*z*Gb^5KpUx2KZ6eve*oDnR>M) ztXRaGkifuKK>v{M?-$PgvKI#axysHFf#ty3SD@^^0^96#y&E2+k0OoCD8Y=VkqN}F z`Um#*NOI(xY9D`X`jF=*NLFWy=F3$WHf9vGaaS9`6p*x^{hi0Jt6Mc}T-{)4MgnZ* zf_JCtj<`^H5_TtmVa@9lcG8K^%4#J2QCUYH_y~_?i8^_9R4>YCv|zmK3LQ+!4ymZ7LH_VT+~6$- zh)njGg@eOl?ecP?3jFwPK>C546}7gWY*2s!PA68eDs6}cR}}PsEHJWEX|?>Ft<*Z3+wj^dE5C}6 ze~pX7U0Sv@n0&~nbX+tAL#GMNI;$O74x+Uvz&*x~VkC11GWhvHQLce0TO=vxugk~T zzw;eEuS`u%sVuF+J)C(NJ-1m>`7jGEQxj50!rOMVRof}rOLf1UwHdw8oM_418$dQo zy)`%1o>pta22pRv)XJ1GKSUDut$lq|SXm*t{r5KjM0jX%(P-;5i^E|;47M+ouUA)cfRzAmr^~=gBVUg-*Z=W9)Y&9{r zimGFK7c90#MXHF+UXAo($J{*F`@$0#m|QL-P)NDd6YKRSvDVka4ra^zT?zOneq{ju zb!s{B>f^_c>WzkPlG$v2&Hd{;e=rBrbIq^)a{5#8rJ7SSQD_i9=3J>~8faXB89P^? zTOfYU2kcJ={X5JqKEpnOl9EWNsYK2M_SaH>ew+5H^EUZ%pNoiy{LI2*&@=q~b1(FD zGU+}9f=2qX3lJXOohxNv{-=tQmX_A;{))I{wLwE-uH3eKY4{gZlhb}EKn+X+mp4jg zBYnwn?Z#~wdvrHlqC^NBuKot9! zET@~C_h>yl{3G@KFUF`33Sv5%7>tDW z7gh7WDgRJ3XhyT;IuT$JvF5w>7EF60=I7OMsMRr4iW;&#=f<+(y`EBVYHNGTthFYZ zp9NYoUP&i;-u|P+!op&7y^@~XUAw!JW3QkjC;uHJ{k}UKk0I#HmdevhQdaC{(~g6a z(=?p}=6`k*D%mkGF)`n;!qI?m8BgPvfWsICqY5E#C>GI)^#6+&>7wYrZ8>mubqSsB zPQvu`_aD?4B8NlUIXQ`yY92>RywezZ2Lkc<%&b5iH#CA36sX++JO%%HR|<7a&Apn9 zh6Z#5BrZfPEv<%TPh-v;5WI`$&-t-&;!z}-I?a*M690C!tH@)v1be2;dwN<-{);c% zRN<(k*MoRs(%&GoAV&Y%19ts`E8@_oXLGx4uqqPjM6Ce@2@=Xu;sV27++tOualU8i zxq7=Z#fRe)^dwqD-So>-ouuEV{1|={$UA{f;xJJLy+bphVIKa!bES-0PRv=ptf4~e z&QC?a5CQ|xekDP(w=s#?8b97A@>w$jmp*M)5L|7a%+87cSY)|yCELQj8l7FVgcB6vZ<`A!Z_2 zU&j>vq0%f%7p&-yv89n}#QZcEi{-Qp^>!fz_606l(YAA#(jHAx2UbjeKngdQMJbAIv z8-`R)8X~?nQ#@zB)Ig@Fs2Cj^3k@z)lYhEr+4DNkDOWNcr?h*a>Fr9Jck%yo$|Y0L z<%ZduI&LpbmVPk8fPw&UTN_($zPWd=iLTSyk;P}WgBUYwBp-MHcMFmU2;-iO9I@SZ z5^j>5{z$yh>e@%u{1#Ia;+HZdxM*Z^F0rq{wb&xVOZjl!YOdJ~@A3mGgTjQ!8 zr#Y1d2?I!Br-og-wt+x9{0rT_V z=gy(}%X9rpAK;HGfLrmMIEkR*rp7$dK1ZC4iRlpwmlZ4+pzO}UpTDN{kITiiR4xyW zmdfJr+4w2X`vWxf@A!N;=XmsD={#7|#Rp##6V-3%?qR`ZP)`qHTawKO?*DSMQEEqD zlwJ-%>MH@uiP&h2sLAe-?AUPRmR6pa#Kg?Xz;E)-KjVG8J=P!ZuZzn1Jg>T{)8RGm zYkDG5>H}F|?k)wPkdaGPYuG45Hae|4ZoTUo89^bOD`hg) z1F_xZmjJc@?=LZc6JaoCNz2Hj1)eA_E|ljzlEs)_gYn1X<>n|G9j@-aJ_5n~@eM_D z{5qS3OwZ$QFQ1kJp9bP;eYW%f0o}LcyxIo|hYG^o{I5XquO|+*vV@h7wlY5$ZtBh%|ov|0tN(N(bO_!|!~# zm3v8T1ngYJ!V%gSZF+Habp#S#55Jomw=~r+1`3eK$gu*IVdB4O)MOju@ET*Z|3UH~ zi+ZJ_U0zY}f4U>*HgRKd~ARd@e(r$P5n3mU>!w z$ct`n^}G~FQRT~a${$Y^Udw`^QX7-4i7Z`o(ceyOY;fUx1%NKw9T+@fVD$x|)eSAE z{r3IhKd_f25-hqsjs0K1o0i>xo!XA%*fh`#O+^r}|s0=G&-^?Dav zAOV9kKILag1E416$1I*J!_JkGmF@ev_sLn;76>cZR1R1GAr#^;)vV($LBLO2DCz0< z4NTI|-IX_brw%5=bvCPCZ*HRg=)xKs7(m%F$hmpCXM^;8sB%K&Ihg6ai%1dK1BARx zyEPbSklmUUfb7YZ4=gtnd_K_t|2lTm{;-aPOs4{TmuMMp8fFsaQ31)-IsG$T=`0F=?V;wNe)U za>(s9ZLVabR!94C5iPLcPMZB+1K<@}7p`~zN+*B?$n4ZyN83z>fLLU* z1hAY_3munBMS3 zvJ7T-7Ax)gj)xo?zz~qcLM+}_-z_Ahe;Pb>^}eAe1?>$Pk0HzAhuCSsQ6%*1*9aU= z98*VFn3xJ(6Ee-cgo6kROlyny5=)goI>4Yq47#oxS>&U0$yIw zx@{gobXxVBU>5+aEoxX1f+enwm$k3vXCE+Sp<90Bad?LTh_3nX2F#I>5&6|&Y$$m2 z1C>ir@W}tj?Zn~+2_XgveOn7psIWL-g9xmh{ZOwk{uuj1sbavE^8p48Y54MR7PlJ? z^;NXbU|4Q$?rv*D1gm5MH7qtZwt;4rP|)f2k(NT!=_!J8ktT6mk!nbD2%hP9S_ogO zhyK$1q=iks@9K&jm2Fhvlo=T(C+8>&JQ|4*(9%G-AVXfREL~iD6WGmrK5XEsex6@Y zX(*^%x=p);$c1lrJ%HuYiE2GxA~fkE-lkijb|kzy zTna|q>=ObWp?mdvK%Xq=8R8_*rVf*UB9C+?WV0BJ`tIQmhK<>63HkDdN;zVzTcas> z@Mu+8bl_px{LUE-D?*Z>m+Sm>)@DAJR~x;3vNPFr@P9g-2vr;sWG|rvpW3(Wp+KRP z;H9N}eZ22#)8+&mo3`7eNbQ~kJi_jINQt|Y7l3y&Rivob*0_L4x z!C=S4>Iq=3gsVqF3D^~S_x^pC>okHw2Q&o8GsA~@1u*>*(tK+cvc%5H+PkaJ{h|xN zqRS^~Zu>3{^~4k-V8~?e!J`ukURtgq=d$j%Cr#y()6=7Y;plMX12(vyK9pnBpNWZ~ zl@v-aufYV9$>xFzoG9!!)E~X7f4O-PgfDv%Qt_tRO^OBbf-j@=X`8>GqTTKe-!!=% zkpG-5cm@jv-?QuLh#Z0Dpu*b&Y%(F=%f`4E;|U9EgURDYek7F(cC-Jx81Vo5yeI*k z^-J|l%mVtutN(i!Snag`rz{_Au?Ut30aw9UfhXcYp`oE+v6z;pDJ?AgxKM8u;J6du zOpyPwrCgOEI}VrpZZ{%eN4rK?&7o5Jk*Asr>&IsHy6W7YM;6hElQrzh12V ezpP|G!=NE|5Vs?k?SM6M$Y)Vmkuo8D|NjLQhu&WR literal 11601 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 LSxBL%&X@lNRrN5* diff --git a/docs/img/minus.png b/docs/img/minus.png new file mode 100644 index 0000000000000000000000000000000000000000..84030a3cc6377535025656df77e37e7d2043f37d GIT binary patch literal 13218 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

&O8# zd)RX)_>udXTUITp7{I|^ia?1*jTsD0U7PE3=5J{(1cX(yarIm0AV z+T$c~`70Fk89rNB7oD+p!E$0y-s;IP@C6kvXsG|paL4P8neIvR06HAvW;dq5CN!MR zCYnyxfLVRv@6-5_r-}*O0d*75&y>Vn0X9CI6%Q;D-{c*g9VgK9L8eL7-vA77UCqU! zAt^s^r?H8qdg!dItmJ?GoTliV7;h%8r1?E)D8>)N+M~wF4*_z zj`I}9i1#KwvSUf+yNo-f8XX%mzW2rPdx}8H&u|L;k)J20LtF1IZN>d>-Q9_O05Z8d_x+KEMi|`vNVNR&k`db*k=FFQ8}14} zHFR_+Oj%)0PG<`Y$@(hiB=k(eMpoFlOF@-XxiP_}y>D+2Bhy`>j`#`f0N}vGw=N#F zJXk+;$@f_nN(2si*7woFE!zTmGT3=R$8jYb$V5vGs$HfWL${`H#^ewuTs~lFo%_M{ zxu9Um<+zM_%^3TvqL#6fy=AoTWKEvSwp8dvJ z9ok~b{Ry124`Q+PN`#DMKHC-m#Wjm?@*74IakZ6#;~(6M4Wq88s>^)L&I$3A@#wvV#bkwrEe}X8|jv#(DEShVoF$nZj+P%gL-)nS}TEbkp>0GD6QAc z=iXP`G&_5N%F}3-YmA<|K+@Qk7N@+znw|$mPr%&+qX{V zN-e{j*S^7q>zYOBhf(g=UbsK`?mJ{j+mNh6KjK#bZ_ZNaE03i!Tf~v{*Xm%t`U4zj zlH3jV0V}$Z7e-2<;54`0@hXTcc7`li|L#PzgzJb5_!c?OZe}wOAbI};Uuu4+&B^W} zOLY(yLxk3k7L3(+hoKG2n(-XnmQozo?EchEB>+{u)ov<9%|lM%SrY^AFMu4Izccy8 z#iFXS>?rrBgf@v-bp%DsiNY4v9=+}x9v=Sm0RQ6ibg4D5hU&e31@f^fDBH}+ysND( z`i-F2j_~_O8s(J3G)2i73O+l+V~qjKMAo)HhldBIom^ZZH=~+Y2lFK2r8vd!N3C|* z+EK`e2nRBB2BV|A)!*H-d!X7;^PLm7HG{+t>Z@3jJlEqT`sT2h62mN(^yUq_ES>mi zr}+hCSE8!ZZcFDV3_<;r`IAmp)(q zjpq{iw2ita89b&*W|C;_)_?HBijv%VJpF2!x@$|coQt9cZ~%Ilr%pDO|m4kQMKh7vUKb3dy4A1(EY zd}Nl2{NOe_Ud+^SJrh?NIfe=>d+7{Ri@dyM9&j!9$7rN$)9wgHYP6&ZXUiHzQ;ywTl=h{MGJFra5gH{M^PXsW#Fu?ug=(WCq)4^OW$Fcamt627%fQc{;Y`}x z{-hcN4W6!z2VmQ(ii%k^)h;+lfF+8SBfW@hA&v)|!Q&vf&l}0T?B(Tkm2`fVIg;MY zXdrf6GrdRyQ_w2bzM~+KKhsanqgh4HUO>h3>~B%k^|BvaGfJZ6#iEaR_baUX#==;n zm*cDN`xs$v&(zyEf;g+O?IM|*;)?WzY!9JCepZxh%9FOCWrR^(#fx&b9R=hqvlGW* z?}9>+m%u-af^}0?eV3waXM5dMik#9CCBqAQzCW!+$u1Z00#88iX!_FSY(xFMn2@Us_X2f+tg?D9WnFJ<+*W69Vt2!Z>d%J@6QP|d=@`L zDuglj?&Zw+g}O2ODzTadwj0R)7m?E9h+cl9#c zVT8ygA^wMoaQX#-LV7eU;S&~nzgQLw`EBc&Pc#hZ0^EhLIZNi`n=I5J29!>1u65yz zGZB@JB0{u!W3E4IH($Mxb?ugL;TZSEnY2$>HXiv_pU%oFBv6bB?7wE6xWiGmGnha~ z1d2&OuB19R!9eA$=EOs-TwI)Jg7^**1x1n3znNiv^>`D<1&IsHn3Z-yLA$_x0;&k) z{sjhUS6=b+ib2{5lVOSSqVm? z`?T;XnszbjV0$_)q}*?(_x9gTj}V&u5(>$!zcROH9 zhNB3wnQk;U!kiKcs3vo0w)KphW{Omq4Z3qi{39%`Ikx#H1-65;S3=Xp(J|D^u5Z4k zscAZWJ>>A~3q~P_4&293 z)-SsfsF4N=9@I^^;2;Odx(1?Vq9o;}oGhDoEOxFP3rtz`vEeoHd$Yi-x)YLaWj1@f)YeVVA84#TEeq)i<9cg4^9H> zx#xlc=GR;9;H7abJZNQ(Oz_Un$cXl%K2kU9BL5_|Qt`(LV@pqyHL2a!?}mzAJZei7 z>X>Q!Hd4(eeiu>dY}`QWZuY5n^C6qGb2DZIr_k@#Pxxj;!Zzrp5}h`JMJ3owuTjhvR6pC~)`6zO8(UdvP8*F@RVn9k)1g8nGU8*O4`LKkfQr-wqA{o}LCCKy=*DJ&xI!yJnY3Z|0U9vOQ=$Hu-}*fY?Cu744`2^e8Sq!*@pQFz@%v~S)>nXK%WZD*ix?JCf`qC zsOSGTjxk%x(k%B=z`n~F` zkvVzdAGWh=h)7W!i=D&L|lYFJT>R+q87^Vm~K2#d2T()_)3 zOQ=e+6kp&?I&bZ2&xV|tT5xvz^a#Y78_0V@%1f3vR!nWBnv^cku*)_J*Y-j$6NvRV zj~Elf17t>?F;G+|6ab{jSx_ zcJdXA$%dZrLx$ykV&OLkLKQ4E3au6E`{Ba}l+-a=(mnk}D`7*XOCRMC2@!1Ctc9FE zmmTfn5_ALQs}8uPyL9`qxVpNjW~Fs^02*QC!TuwN+WNzHy)fYT1V7RPqy;rSQRkX^ zv>8xftw?zY^i1*QN?YgMF_X2PemKaxTVLn|0_ZJp$pn0`3ebb12BeaG5Amu6kzrm(q*Bfw*47;BMaXFs3-0VPj4YJP z0Yx!_D;5tPgk@udW3TEjDagya=~!Fy@HOusZkjc+gxDUI z4AWHiG}${h=Ln`#`a<(!-HN3N=~{~1)?dDZCn`Zhk8FJAODHqdhD#?d&R+BYYi$&1 zoalxkf0Mwde`$|}Gm{i!)FmzQxv-?n&KJ65K%xP7-Nfq28uuJ-2SL{03#2|9Mn`#) zj`JI@9M3p{Sqt z9`ip?P##U`N(y(F1~mKK?A-a;k%znSkMZ$u5uATRV)q|L=Fts*Bc|VdcH`mEy4u=g zh93L%0K!u$c>0URUhn_kf9w{)$aaNOXKNhmo+gSi11KhFYDpR}^OpRb-T%{F*~S0= z9rgW-Q)`O;591}Dt=~RW+W)}?G$93{`*dF_*)bSP;dpw(`($vZ=m81CY;WsA*}zNg zOk0qX3R-q=>XB5C#_Q0sM@ZBA!+y*07_!867p7(J;1t63UF@d@`vt5q`4w{zP!S;b z@96;rDEITz0|4MVuKoN(N=j?1iR%L-$tem^|v3W#ZS}Y5~yXa-InWn(g$cXEx2EsQFrpoT?Ma zeAx~&(ELejuUzRZd5g+}V0n3Yprz{Un@|Z0j2sBsDdKahd&_-nc%_B!Z@pPe zK*}=?SRO@_dnZ<32MRc<!nbAkeSp=;Y%OxLWsr0|2Xy`~kqk zmFC+L(0T01UUX`Ivnq*ZH(Z)9;DW(LXErpg&3_oZRKAcbnl%)anom>)@YM!$5{>y! z`*eH#x-am?D{E_?i;958G(m4&d7y_YBO_yIXoycs(Pj{yiVyiUdTctyf2X*&ESx~w zjWT)f--k?>+xPXRV6H{N+uPfzbzTU@5?8|8#hh>8>U~V_m|N^)wM>qyUA=F-vnj2lmmMc*bS|D|v8ArE^dpxDIYa{HDb`uK8#W7V!u_&R-=>!5oib+}?hC`~$Y z=Q!=NuFTmurl}t+zdd;}^{NaoxDGBN;p;}qrQc#zM&1?A#@gr8dt_5~y^yBId(+=S zlpbeA0M(2y8cheo2tWy=}*{BwyS@a#X| zzeE)4?e$+0-X=xXZ`bZ0^23KbhWGu|t~ep10JJN9Y6}Xp{iCXYcEy@;f8uElMr~eR z0sfi7cRd&5Xp@B7e;{`qX3XB6X(5bijubA5oOb(}C?^;^ybR-fUKc8n`Tj!O#0vb{ z6`^rW?8HrOXlS?+)nFdd$kH-_*x=z>?^u67W~2PIsYxAjyx{Za7peGWa1+K>VE60f z^qQniHJy62>c3*xjW-o=KJ9p`6xsYe@U5YSDe~$E4A^m!*$Pq_Ity7KF^(B0DJ{_D z23NjXiqu^)Qx~Vp0zwZU?_@u6Nv&PS9+;|OlMwwoJ~*u3<|ZL1rP>u=_HNcXh0@&I z9PB{w!xL6&w^}T5p7vY|4UiM7mwG8XlJV|?0Q~*MSDl_q-6P7Vlu?}Hw9lpdSmm)y zbBWc*nm?B9=u!_4t)%PAELmx&+SupVf?GQ|2`}2Tra7LEXT zm8X2<-FIG)obHPsB*Tb3BZ2V8!{O`x%RQ5Gx+P&A3<5Z33&r+c*- z^3*c`kh^^+C$TRmC@>C8*h5{(cYNqOs>%3sVd3j*U%6-Pk%!f`-|Y!CXW|n$g$Qp7 z37KwHWl%ZssUuW-Pq>@s2rN9Z4f#}|KZiKfT-@u;>%Hf6{1)FD(#0dUa=x9~LEGNb z`pU}%T*}^@s`uee;8gPyx@Iu8q!B75CFK&V2y7u!fDf9BApbfn@?2BW9VmTahtcmJ zXIa#EPlW@IZ0ykgyDE?aVOR3M6lNIvSwB-39k(xawEQFUJBC`Yp| zHti@lXwt#0cx6_s$tz@>WT=?`jD`g!RvHD=U#0XZJu}B+@WRhaCkUB-{36tk;{utr zXzF#X_(!{bf|;kqw==8ml*khsD>`lohQB;$jl!rx}t$#Z_Y<_0EtbD0v3aC;8 zLQ#>?1@6lve7M!2LI7YjdWhci=dloSq;Und`6TcJtPAkmxPQxwdg<%Hd5*s!r|SIl z>2-luK%Dti6q;R@;W!Mqm>}K%Y;6~yNOboxfMODw=ItuBL+>?CqD6L=f~VCTamkTaSfC9ggc^cpOmL2&XqIyWu)-V81+C zxqkf}7XS-kx^oDT&*A!qGi1FD*S3 zVMEQ8LP08ZX@9$n=7(C8-`nEYWPqmSizIc7Z&!>7tgC^{P6M3k5l~M`#l*eX9>aV+ zp312$=$gpGHx|IgDtsjsxuq60^*n)K5@?TXZv;vokrp3}0hLRP5N=nt3p*v}Emx+G zrk1$F#HHjZqIvb2f9JRB%wiV8f`W2x+@rk-#tV1Zs- z1lwf@Ak@kKtrY<{PvioWr#9A7bgoCZ@37!~>bBAqrf*MKw+k(a@V)~&A-gRn=f`N; zKrMB^bHXplK93d4eo}=$+MW%`K_xMKD4?@JE-K0(kG^U)p zED1H&WdIPv^TefKG(mS9m=*f-1Imo|x^ul|vtn{`G8>FmGT)h7=o`>3i@uQv6qSaY4p!Y@)VEJ+YWKH@BtS!1Tq3zKnSz&=Y>8gPbrQ(aE)i$3;*xYw1 zTj^No`^5y}&GWfOYZ)8TZ;)xI)@dhvrYt_NQ;ve|5$4(1KM{l@`2Mrxazn>*t&Lt#B}TrhUrs$=Xdm+?s|1!d@Y zi!Id&#N}5<-Y`4@G}~7EBGR(A&E`6eJG+|Z;`coOt4$Vm6y~qYpQ+i&qIJl(7cJj~ z08jS`UMcJ~Z(V|L|G*brBuC`8-U%I+#91g-Chr?Zr!cq0MYX0QpS3!M8B=;->!NZI z07tfHRlk?#_PVm(oJ9T`d-DT=#GohOv&^Vrjy~QNv&l5PzfAiv--^iE>!BN z-HB-sF>hOuqFr49U|=bUj_zm5g}GTk4BvQQF9h)0#(B`o9q{P1Hzf(wiPLT>iZ2W z2EHjVB@l64>)s)-bCvPcs!D`#Lfa(p$ogzY92-ABe>#jr#sh)1)4`>@2b2DOUIwt`P`2ptS_W?#(qfzN6gQh%a~b;PQS>$cVtA`QMu}?bqIAaf z!_{b4?#C4ZPlYlSe0q=1G{QPQL2FjYQ#fSiFc}GXIQPHNc9wBf zuIsv|yOC}X5NScWQ>09~8>xYG3(}1!-5{WZbjP4k2?^5 zh;5ngySbqFdM5;w{k2*yTuA4vVkc~OID7g)vVt$BG=#mU;dmWfA<=Gb3x^}gu0K9B z>oj{$|JoAhD*5ayk2(uuh|nD4U0;y^Ik!8G&-Y^A*%qbd*k7LeGnx{^@DJcQsQLQ( zN{yzk(yzvnzB+%dE((+suj^EUObz?5Jxk!7Q`fB!@Ma~X0yVS1M!DUZNNvH@4~mF_ zRz&PHSeWGKMfLgXtns=D&>~&bfOG+uX4P?FI?2_ti1>N1-p};T%j#M0uP>+9rq|%f z%D+04ZRM(5u}XjQtEirz+BDTomLM|>&z!&XU@f~B69JPlRZ&lJ9JeIo94<0xoDtvd zeR;=vrwJX~kBY@8>kH7M-GoihOE?E=iHHs~yIqCrYoujmWgYtzWNaR-NMtimbcdrm z1%FV63w=k(tYTqpjpgC(R;$fSJDsimOraCYo)5(O=y_<9Ny^U?y>JFHA`}DR5>|Eso{c=r@q^v2jSax351F<-PLOT8vmRw zP3>uN5o&%A&uT_DX9&^+*T9_%nk=>JMR}S7XD{PR$+3^lR9fGrui#}%jQk_!K8Z3B z6)Eq2lvw?A0L)S`J~gv!TDE?a2(eP?j^+|2qAoR z#~tanln#kl)#jk=x;^G}i(7E_k(;IY`MBNhXGnk?mP6`gBz9=CKviSRyZ)zX59O{r zyzvT+k)U5i+0(WMjGuU`>rU^O3-*=IDYQ7H>3P+e$Djczz>X)gv81>)C1hB-^H+tG zj&RvXR2&L!+i*7?Ga2~=5?cgN+HA+m7^lP0!CjjR&!$x+wFc7KVBBS&@wJZcas4)F zl~@6_6uOq$Bd5mz65K*{mtOB_S4;8ygs z8{rc43^VG}hnFO24@_w3n8EgI+$qx1(}w&J<>=_B#8OvrM(aNByiU-^{37hJQe;P& zzU1IqZ|coNR35Lx`2nVHcYr2w!2b~BtbqN70imp&$;|%OmwT=OUEpT}y5B_kwB>Mq z*SA!WPa}y6s^W)m$o!f~sk)OliqK&x2uWozEi`q9LseFgffyR!D*=#^T8(vUSh4|j9sq$VNx z&WHU^^HdUt8bOEA`rLbU=X(nO9*rsM)d7=+x?!J8=ydx=q_?=!(1^g0s6XCPk%onTyRG7K=1G3Tnuv*=Pex2l!uVkmAR&yWmhM5R#4h10F zdt2KcU~4=fxDP}O6RN*^4uO9XKa8y}xto5FDPNgQyXx>oKlyvC!Ve$j)d{vycww&J zZtXvmIvKVcD+_iUX1LePW#K5)3l|MBEt{}M(%{NvO8c8>m&4(a9S9l z{cutI0`wkkZ*I7|I5Ztk8z6SOc$rmI&ihLWYe9~#(B2T&UDTGk(|(mK{WoZr&5uDN zCG78JSZTmVa$F~1qm1-c`sESeNUcBbOINS6or!(75$aNz+H7WYSh7(r#NP*wwXeB0 zV!ES7Dklq__Ea4F+}krnvqMY&$Q(x-%G5Y*@$JLewKX|3v_H|^XK!U)-Q>OYZ@50U zDm2;1QPx;6C*V7qs&&wJTP9k>H5@J09m-X%HxI1-n#j6E`b`z)Tnu`JFP>$1Xm^Ru zjxj>2^W=5cz2V9bP(%a$#8JZuJH*y!XMDnyps{$ZK(~_(C+sIrslfhn zfL>|aZAo}Ndxx(58{>_xGx)r)>#Fd%;RB3ccU2@Rwx7TZ%k-p1W4HHwM>(8Y#JmXX zZ1Bf6LO44>Y1?r;YSgl}t?>tG8}RW3%S%DgsKz z`nozL3#~U|!XPYz;FArUzwtM3n(#J8{b{14?F~SuJH<4Yf+@fM-~6Qt^K)f&brYwk zosUMD{!>b<>-EZ3cATj_sAn`qHZ2Gj_Yd%Be*OHH-}EG!D2!fgkjdM&J!OEwh=9B3 z)wJE-f^4Ta*UKzrq-#0f6X%h>$)kup(09eTai)BBu~|4j+Q^m@ReQ7x0V&VGdyo|w zm^TF>!iV&%L)CMI{EXTLVXA5T^QXHiRNE8>Eq{et;GE-NuD;#vd;tP{L7eeQ#Gnek z(%Q^DQ2de}sZnnwlM{-^x)m!!BxsJW1T($of3E0H$k68~-wvp9JOq!O5JS{(2%~Ir zGB%fQSN~1PZ2rM$v3SMi(_5ZDa*NWQN(GDj;X^J~X49X!lC(B8WsJ0k>)O2q8jb&+ z6ZLaQ6{4`Pu+@tfxys3%0pdWi-~+AyIO3fwinz5*W{&pd2mUrfFNTdzH@F93rVh0# zPjEVtnuDbalBO#%<32KwXt~z}Cl~8!6fz7&ZL#dfKk5N-kZ)Rpj%t{S^!?4vq z59B1K&fT%S)8O_4IH^70(cC#ho-NKuj#z>1Wpw z@Bi5=&J%@Lf&3-X_?O|d4wV5WW}Bog2eReqN`O@osl$~x!8M^ASGJSdG(_|9 z+E1YQMC9iu06$KG!?Ro4!OexL?Jfn8u5nrw9FJn=Z(TvjL~Hz2@g~C1QHb&Ghvcrd z`8E4azhb?f(4jl%t^!d{a13XLkTI9Ym&ST{FO1=VVBFKkuz0D(cyJHTs$t_OuK4u6 zDl2U_;(^(v*l!Q1g&n_H4`Ge)KD3-V>#1?y*4?l9@Xlt{$5VIx!kVbiE+P=(ZD+`V z=9J=9k+mQ9$ut1ZP4UK&3~cw{BHK=C`sFRj+H!P27YkQ_{evQ99h*j9yqb;pi#Mjh zzx+K5$Gv@L0q=qjT59fI7IB=7qy%FJ1{QwjGVh9{ba$uhj9=|@8(qBZi|A73w4uh2 zzoFxR{P|`0w!Lpvb)|oiVR~eru#4Wck+57vGw0Qk$)qXa1ZHnIFpydnY6U$s;3lK% z87M>GtM=_9ZkdUE?K%o#MB(;2o8wTSYDlK!t)WxtxpTK`yX!*r!BEGHQQuckLSbQ;+8}j076L8l{E_BrBU0&C+SD%&p&TXiaFaq@Jg!(*9eL$M0Ro^)v6m7M_9&&zI~(m` zZ-64KTKHWX&^P~6+vRC)!>a)WU}*6>M@YNMS}sHQ2bH)trIdlJPSX?$=YsW2Lt>!3?lgX9 zsfp=Kdr@Cssnw`4n=r5+Zr>v$P9p0OQIC?owq%DEbH%tDknto1@EYqFLpx@0i1F=$ z9?mfi<*D+=P#X0mKd5%f59q}>8PT>Yvs6hDz#rkD?J*McI_Di~VQDIpd(qLcLuazo zjbBBjsV6YT2O>jgrPXcli?0R;86T|vcz%#kSyff0Yw(>1DWi+t6e;T|9fA}j9<29U z{`jwwwKX!|VmWV5wUo!5_DhXDl~_2;-&A`>xgg?h+ZV$v)(u)j(hm&hW5b7LX8O?B z$fcsWC-PBokn(Y9jW!<5;NWa^tXNrs>~`6!8sv$vYGYY@K5Y7$eoT4>q^(vNxH@H7 zS>%bgdL!}+2#W%}e<0*d5 zSrPtCE7c}L%U5=t+9#=Y{d;QM`VdV7qQTx%|ke=LrxAe_OvA1{u<)#TL- z_~y2R-LfFn5V))yDC>ToTz%~zi!4{lHI1mu097(_^j_&9BOJDR=33UCBzYYn?!Shy zx@F*@!GMt=WkLPT;K9S502GFie&cfY3eu`D=4gLSwoMdZ?*^@)T)Peb=l)`HR?rsSDaWZghRmU}g=4;>G! z%Xs8-ir~(mqQMr!w^W@DL?8POVwXqKBBaMVS|_9dr6c$>V!y;>7QS8PImk-zN=gNL-n_H9eT2n< z(4|ZB($2pgZHy{^%djN$u7(VvsJnnP2!$A$#_21NTle4e9)iESy1C>vk5V><1VKIb zHYj)|Ok=4{A^<^xtyf5YGJm9G(Q=z%u@5w&fPOn6i2~3ZE{DF8Hj9<*^AO(hXO2Ze z#=`7L=wHLYZJ?U-aPsNsq&phPjDwm56cHpm$1^JhG5yuUSF@aBr@qhId8Z}D5{ZZk zHb*x{YBjT3$V5Y4v9#sThv2mcpFo06v);XX+6YG}iy<80ceG>oaPC7tw9}N&+8`w~ zv?e=X-CwZzIgPm%e3)^4H0tHSdyY95-)RQ6`zup4+ar21Dx|%x?jhF^`;WZW(CDo| z4qe*s#$|o!1E25wg9`0(vxpFK3Xnw+AS8Tc-iTgcks|QT?%5ZKUwvhNU9@uN=~7`` z7uWE@q@m9tT#BHmn7bAD5f~$?on;TBg&=b{q}y(|n`ABOYQ+I3X}!s`&W#~IbspoF z&iHrr7%(Eio}o;@Yv`n}LTs9(>m2j!fWMc|Hr+z958hbeC05JN7q|R1sn}%IauJ`Y ztG5Y*e0^u9*kqf&M^CM}rVK7$b#UMe&Z=Fi%&)H~Lh{&9b{Vjvg6#o$WeV}GIk2Py zC9i}c2_lcx%msh1Z>?n+Abw;Fuw_6?`I%Z(3Vpy8HR||l{-XwTZ+Tek9&&ZW|0?%RP*C+3f* zQGU@6RM2p7K5$`3b0S+mD%^T8`lM1QrrBPT-S&z7T@0?}VBE;lY1Ay_yTa6W4oywXzr5j~!1ytpLq7|I^gfE3>0c=Usl>ON*awFH?@Hkx-2bI~U`rL7@w zxY6Gze9!8Mb7>;Y!kGoZh3G?NNI?-lun0+H)VCx4!ZuY^pijkDJMcg&nMFbsJP~>+ zS~Hh`P<@Jn-F6rniqSN%KWXfb=n{pGxv6c-WE!6oRLxf;+Ca0c1`cyThZKz1P31*l~z%1ZlFS@IK)A| zS|0gWA=eU-MMx?2T-_`K6iqofNOTa}`fu01ZJKH#mmPHBvJw9K%l;_|ILY6!s#^!3wlZ_L%i;1Eh)7mcR+bWdqZ7qc zt(zOytbx!?Op7H5n8j@A++OhnG$+@E(^w<*FOaaFPFb`!QoP!VkdV6=yLoUfg+)cC z{BlWYUPAdl=qVy1BJjl>FCAI?DiF*_eIlC03kfg%=JnwCVZ;wK--DAocyZ*#PO(z! zr(u%jtfAs;r`YC`bW9)puz%F*cFx{`tgo=kwaseJH$%bk$Z+G$4l)e_9KaCq$Sp6M z%f<=++1go6;Hw2(0^ofGR~Xv8I^b` zze;W1RY;nw#qEc~^0WjZze_oEk?K<=I9Ef=!F9kg2HePBv85-!WX~pN;PEg#N`~7$ zP42U+Sul`zEcw>$8OsZ+m^!4rNkDx5)13d1oMSK1Rvz;C($^9i_kDfd+ zGCCyf)ZrXjWCESx#=uhwk_f35`nr%S|LK87yxy&|<%+)cHQynOm96v6)3&`6XOb_B z)Lq`TfHA_HI!jZ#4Y)`Lo|k@u$j8L}Y_$T-k5@nG`dEzT1@plq+*vQ6Yu;?P1Cka1 zpMM}EQAyy>62NiP9A$bjO4YV^+4^OYztPU$6@%%D7q3e0lR_srQm-O^3c2yJ==iYdsAz-ey9D?@OEooqoJ6f*g`j zGPI{)mX0K#CIpOdL>#bbMDy3rAyDmiJ_R_Is=J|(8Ke}S8KlprzmAO`{^@YEZ2?<7=t`ZRXc*U#Y~#J%9d2T9X=;-_IF02Cph`bnD;lUac4pIn@aRWmdTSeLGXDM76+MP9_G{z{i387(vu}SuBTowCRoJmv> z(NM4Z-_Ft#y?4AW(o>WYvP2iH2Sc?erA=JWj1vXNFuCZ)~glEOk> z(7sRhx#k-!Xf34SanFajsG@>&e#vYt*KK#;t3^{`?LJ)}!IK5*>D0*|I&rY)ZEX-z z6_3)hz`H#Wry3j%kySV`rfG$_X@ARO4q7e$^1lrOo#mR7N685w!l`TgW<&Vpfd7wc z;V78={C6`bARN%s03U21@AvP?-+U2uo#O)m3c=nd<0h+sZXe&0&?vv{oqTN_-4w8@ zA&{iOsJgHIuKE5~0SNz-%DLtu_%cqheG~Fea1iv0U-Z!&ib4c4@C zZI>WT!{qY3-CLm0|J|kh;Q9rCwAG8geo*&0mN}Ue9XMWU9JhZ6`{=FLxv8oZv(9{X zzD^1mXe;jjF(ptgr^c3Cb>^rW2EKw6T~0Mq?KK|N-m0;yg-%R?JI=+@f%x+ym6P3w z4cHfbdjyJ3pR3V6rIZhy?yyViCXbaE!kSLO*#h};3=C>H;mF^s5GXFa>vXaj^~0m1z|a=bk6B0a z#Y#n>qPjYJt}O`HnDOm9_MOd5-c?En`xApF`Mf#SDgy1bsX-+v^0Tlu!|Df#lWCU= zB@vp}6TXW-cwE+F6BiW>~y0NyqyU_AkzWeIj@?6Z`~GxQ#}&dK*9Oxv2Mk=`>=l!798 zuFeN%X6~y#TrT#Owzm}M{7(~aI)yz)nMMjVjV}>9fhlLAeSw6J?B{enOy_a;W?;XZ zr$D)_xQeD^`87fe;V?MtU#aEJR%+V=9t_12ds@qFA~U*k*0LBodYn$}=|4yRCfk!B&I3V=XHhfSy(L27wa z--liONE{D**wRvbQ!-KaXS%v4Pp1GUKu<3cucoSsuXFv!4|14B6dE?Wv9>1h0N}lV z8|>&=E8q;^-2=U_%~g4g5{1M`{xTM|H-u%K;)N;YCQ(8`BC%OgzMnx zjxhM6Ntz`Of=ZlOShayHZRaNeG@?7S;XdvzJ2T6iJHRAoFwZchbvn!`5{rMAb(FpY zFa({zO!v;;VRM;S#nV=K{ZHQ3vtTAyyLpIyKdcG;d& zr!JJPT0GrbbdAd>SyM;Dweng3y9YESuPHT^cm9mLgFyH3$qN6+_sgv*3*!TQGFvQ8 z>t!8inrdpH@-r|C?gQByia2094Zy;35@=ILU%fuVGDO|QeD)V+tHAWxerBLUbgdTe zX{B=81autoUqqFEgqOSA4beSj@D4x16yQ3Nbp=!+t-2v^WghgU&a$&yc&#%J z2$Pgh$8GW~)0Ys%H0>P*<+{4Lxuq}#k_9#kfJbR~CDY*T0-mGFfRqSDi02(k zl=Ldt^jJ+gLW!>Kx z|Ee=Jf+&+U&HEE@GH8JyfPf{U)ZLknhG>!xGTL;N!gnjV5Wj~428KYx z)$OMV`Wlub-4BqMBS>zE=xA$ytv&$~uP3sQ*SVuB;5GRgkNP_R_o@t7ejq9-uP_=mQhCa-x_yY zG1EQ~7`67}0hep#*ZBHTHHjghe3NxZ8tA4bTD5fs7#|EIO#_63lyw}$MGlfq@`B8e z*xR5UsWr*I9-{#wQsqDh5nc(s!qe<1t?bz<3L zv9tMQs&SJYoVe6N0(q{-Oh|p$C6csUa!GN8V@OC0 zz=NPh?fK+w^wlmF4O6K#44%X#PxyDS``Cw>3uCDwZJ<0)mY_~(4a7Z3$nft0ma7-v zd8o+M$gt6|rtHHJg%i^^R>$E*BV!PvbQi|X5Rv$LfOpxjB1+?I{C#rE&;Dq56U09D zx!UOamIt7ts~+rN@O7!p`9OZ{Abunh{=ux9%Da8F^KN$F~hCa*@fm`%NNdg#C#J9F0hP zJP0;6HU5=hG)oZ(p~4-VGZTHvcR0Lpxu?CmpUiQ#vJe1XRk$cumLlp*A0yl9x~)8& zHLP2^!;qEra-X8oU_OXd-Rp~Sv8erFLZSjm_x6Nt@KX`HzEb1IG~@f`ot^ zQurd7ibtRsY2c9nPD&1y=ypE_j1*wJ0!RVV7(WM-1uGJ(kx)86J5buRrk80aDv$x{ zUCo23}r>WpJ7I-Me=o)|@Sp2AmiSvQY@YD{Z2jg=T0)j3nhSVdu{v zS$%6DLXmz&8HxPNaBBfx;mlDjGF5x=EfVm7_vA-ks9wR>3*}f%UioywS8PjBH zsF6}qlO-O>qUD_RekWcNDDbm~Bf?%>XIyZO3r=Bfq%!Dl*$SVPZ{KQ7W8orger&}$ z`bZzm!7w@SW&h(|0B8#yzWJPjEeSh1rp*+bD?`>o(4pls{?B<;DA)M&P5*;Vs#(81 zmRc+s-jJr`6izAs+9So3FSru&1f|S>8b1f(kMqrIpqu;kg4iHcd zZx)!TbwCXp9~lWuYrCSIUlzVh$XR<#td#?}Z`^ZlXfzY~eP8$w2-`3C1Fzn|Em#J~ zgy~y%lm!VU#}bmi#QRsX12qnWX!HyzL>)U;bDM=2e&;e7@;sqUsf_fiA-1Rx5|tzg zB*G;I(3YYcohhHqcbxDafjt~w!kf109A#iCO}+T}ePo+YQUTU<&Qpf#dkPuN0bgDa z3oFZ4h9Eu`hOted&%^OzTU(hQzjqdtcnPl4q=;yFvhDE|r0vGvRFk9SSc=*ujKv~T z)7fhpeA>mRyh~}hqTM3(l4Eltuga#S=e9)NC8AAOvsB?Y`J!Wc0M;6!hCA1QJyi9u zh>9Alv(F6v1=RF86&1Gvg%o=WjNW9)Yet*5XUdYt0{+e%!PM7Nk4!T|=1aiCJk_^v6T|V{4aAh9>NQZBbQR zlv?^T%TqnBOjzOfyGhS&WrB<_%}6~L0DETdmFJooQj_6}Mdu$Y-D@qq_$#VkJhro1 z6eE}1OSRmlK`!b(3=TcKRsm}I&MTxmlqTTHb&zK0p-9+U4mu~}s=+Lq$H$Z~pq+i( z7QCQ_<2x4L>0Ap>GJ})*_G+o=!-tS}WCBIB?x3TeRwT`1yC-4EWJH4d_EyII@r;t? z==9Lg=ckISIPKO(lnnSeotS6NNeX8jenkDHQC^hn<0#8vf?@Ixn2-ztd zK9Q}lg@u!vN3!PT%saL`a0?cM4H#8dK(WlS83^C<6ES&)Vclo1e;40XL=Zc(p&`W{ z&LJQz@EV2l$%=dQny#~9q(C(TnFIBzGZMd)@%&qPO-P7ay~i0v2QDvo=t`WCit+3# zrI;A?g@w1w_ekCuNCCQJ&L1jrfpVc{ISQhD6o?We0#JV9uPPfnY&aEdji-l|dnYdl zy;r@>@#T}}wh*XRWv15aUW>GaiR==*+?cSr%XGm<#V^SX)kr4}>oQ!k>&3#FB?}%9 zk+yhSNB1gFGNX-!@B;~j;KZ%)exP!B>z|{Crw9t4tKBv2n^t-QmGCZ~v-ckA(ATX( ztI86=iFdW;liEP*vl~OSr4j;!DiC^f(Q6=Y3PUYht^FFF7J)kzouF*ZV_gssS59H% zR*^+;7Ie{@wakL2U?OX|dxBZ;+b9cfI zLE&2jdkxU>bF3ag_qPR{{-PaKUXs8c8ZH>+AWXn#+`)LXg|fpRO5woE>-aPJ`rA2T zPEh?@_d=>c@jVm->nwrA(U>61MbA(+(9zvy}p=N)DhZ8KRN$Q}U( z`_Z%9XkC&SSsCIWJtd_>!6tR(dWrBnDhI|O;eN2Yh#ciTFfd>!j%L*T%_iO>#lZ)}umQnQ6eirY`*K)MrBW}9bIU?Wy!UjBP#RPdwV7yOT)P+|XL@Q3^vivJfL+Ti+r(UU1)h}{Dk z9~hwnKz>KSP6)7&z}EaX53F|#%`-}|P-o2={$*VEC%}>neoZjf-#;xG!UQM|nZ+)5 z+hEk$5r!!cQSA+ZvBPwPT#mqm9mnYgX4X-nofpDBz3pp;qvlS(XXGePde#+pq-=LY zEDGt^8cj87H|KqJ7!e|Da}ge;YYT>eU1q^HIEEo;T~|j7finv13sa~!_J!M@Dnm9#KeeId z+%}xfctPm+Z^(7F4gZkq0$;t4QEwk*g1L^3ntonibkZOVN>b)nUY^7W5YmS>O#}#l zq(1piyr5<*YIQJwMg^A&=0mPmn%#d7jK(LY&WKAuLv#o==TEoGb-6}$Ac>b#@CPqH zkod;(`)p<7*V+!w(JJflm!PU3OdBgi*-aDL4uk?V zoqEUe%SukNu6HQENbQf3^OLG4PZ{>i`~5EHDb0&^eWL-zl>vrn!NND}nLO&%zAqe} z7X*^w!8~+Xzbk)M7!y)D-8?842-6yzDQjG+sLRMHcmr50H(JoJ(M5oOZfF47BV+Fs zU_KVO0S4;s&P!OE6gP3jwfIR#{# z8sFVkGF3FqVWwBQx(M7K-tqG5+*>u0HuhNUqXN-HF!2M_iC8lLFZA)#C!mdF97Vnc zkY}(10Sy<66Guy1o6HyHbASFk0`Nooe&ITtoGQJ%yv`3TkrpPvy*y9@gp>WB23ub? zdY-Q{qv@9z_<|svc4{h&x+lu_i@JBbfy+kq@;Oj2da|j%A=>nv-4*M3{Lw%;P*DA& zgs18l%j^2Bzp{Ci-oySAF0+P~Teo6kdzbVQ(39(qB?F2*T;x{d)FE6z4X>R zTt7l+RL&y_5lq`B*0wo2YNKGz$6NwF8$dATBdOuvi!y!|Cg^gq4QvnidXFC)UN(YW zGMy)I=RIitht#Iye6k%9%N)AeE7^UVl)0A~3ii=I?5BWoBw&vqSJ4@Z4a)2%R)m1O zON+F9zT9N;k10J~!jF zMDgcxRec7uTtE#a-@QG3rS_s-bBe0(ljfL=K~7@EGnncPvc6E#_)%+y#32dhTH&r%&nhsmH6#u*0iB zM#JR{Fkp#20FwuxpK&w>1SFm(5MH&gOf=fhO`$HX;}+Dxq+luzeOp$bSO5?KVLu%} zq)Vol&x44-HR>0kPW$;FKCpV)hXg+;&qc0L*kVA^9(n^*DG0Z>PVjCzzj}Y*(g!Ha z_qp9Sd_XOK`p$U~TI(`ZV)%NMjq+p1N<;BBnp9^Ljr^y8>5wE@px0ICy||joK10({ zuN?Ww3lxrS?LXH(QfF=~vubfsQL2#BUHLO`NAm#S6)>BHNlxl7xBCBscGrug#`%IG z`wY;(w3vQ5JKfodYrrlxH9V|K1XdxIzw8$QPfQGl_qDYijY798!PTN^y^@d~Ol?Bu zG#XS)Kvkvv{G}lTag`H_^;&sb6JSXdQ4w)F*`*#4VeNG@_PnT2GT2gBJTb%;f5xbAp&byfOu$X(pCiBH)K z(k!vc*WnfY#h|+&-ig)4xb~pI|N9dytuIf>4EgfAxq_-D#`mV&M6pV7_>T%i2HH2F zw2S3%q+R#J?Kc&=7_B)~3zTUonDWh^1TwQcLzXs5TEN#1l6%{SQl=szP7db_5y{mt z_3#T=pIpdDoO801f80hS_ZgJb#X3hUQMJeI1IAw3v~Cjd7dZ`B_QV|Lr$4?o>BDZw ze|q4bB?G|A?Z7<9Z=lUQ2ymeJj~^TkE#hlDdD3bT0uIKEy&V^T`Gq*4HuIIvzt(c- z^S$ZFe8R>DZ((*Cu>}MBLaZ_dvr6>Dm6*Tn=kyqgG|Gi}wPBSL;$I%rmA6)#CmTkz zHR@9|Bp{%(QEl0%8fZAxeoK={>fCwFqJ0pq)8NW|{?P)Z#_5jrOAo82FS&`mWhI*9 zkiux-RN}7|kiZbg2qh13?i~0Go0e|8mqia_v*DLZU*}bv0lBoJgH)wbAOsfg~|NZB>dfQ zaH(y$Sxy6IYehwcfvXS3-VGN!G}N?D51yb{9{e z76u29A}FqYfG6m>+r}9Dnm1w=^=a<+EdsCSNQQ*;IyLPQV-FK8a5Uxw3|3;KKgxP8 zoJOE@AS8SckQ39yP-&n6;A?AR%9^e_w2@(X$jZ=2&W)TKLh$W2Q5VoztPFUVBPvt27ljEs5NO6 z;wPt##G%|xHd03sM~cu#1~$t{f=$RwIwUolj2!xv?u>meVAFoEi2m52H<*Ob$=A3O6KBut52#70ZB5il4`cg*2(xzpSg2CVY`a#6R=0YhWG~3-@LHv+bk@{>$lP#0lo&f?sT;SzH{zxghA^6 z$#bcM(2F(kxkMyFJWy0FyVYx60b$l)vX+2L(;g0UB!!fBVRLAng^CDCm9~J@`sAUM z$35$xihy4Q2?~>#6d>11hUtr$9l@l5>H*MM(gqZ z?LzI5Dn)|~OzVB1gI=A5gK!TU)fYdBVev-$kMRYut}D`+wuwN6mE6<`*-w*|o0zB@ zopTg?xwH$qp7mC}LDZPU-Zd6`Qq&$6jl<^yT{N_1zXJc6H@lwzJDLAsJW}!p==|R2 z<>f*OSdu#jo)fY+XmK5k|M&ENZ#c;A_a*j|_Ktr4Zr_illE+N8#SIkzB&+U|wg(@p zs)*&t5k#E@R9bHNHddFLYX=2C=K`)u)Y|Xkg3ep+q-G3i-oVGue2u3eG%=Vap;ZEz z8fy1H4(jC5FX_22Vd?1A``tGPqmk+ zA0-~mTqK;{7s>`Ip1CevJA?>RDUbCAhgHsZ6p;G)^y1?~_*WkDAH%5<1}@o6TUcZy zwgBu&xArAtVLF_*1z~ADMrl>L}4yay%)BUY#6R;FOW@bXuiskLE!%HrUV?PnUNIX*;6C3dsbphyM#Q-U64 zJco9VaXcZjYESA<>OK_jHE@>Er~l3eAu|RJ18^m%>W~unwtTn^js&oRd)HwC!k^^O z=5heV+;#SibfDjMoR&4F^$8h@s{U+?BAzfZ!0DJFer`Kb@qd37*1WEUCuy?L#kC2w`nt` zPJK+Sm`(yqcad}(lMG(*+p{ZN_}N={6Ho|W`Y_cL9yHXi{JRw57f@TvNFq-mf4seeX*qYNb?1=SxL;#Z_{z`Sb$aqds8P)rZ&Ec@8?W&@!tqCl}$4 zRkSXqcKOY0rq~Y9{3knX5EjrYQrj0FKJ3X&zP-5h%Eo9kQwx}>mBMF)a5tifCp~{% z@byhWKDCWyW}+N}X}*&Z8+*FhTD;==m2o{s?($qFV#mP>luE?$8xpuf|I?I_Ukb)2 z{pM>hjcPf=89)p{5b*vgj3@OU904Wq&ObQPH!8?|IXB0@KV*!suWh+@V8GNO35MEh^m(+`V>XWJ9$2hMgiddlQZ9I~@8 zX9W}g{%1T^8b}jjijTZPU$Se7i6tNlc(3|Jnh;js3o61^MU_ice^$89=^!EGv8pVx zkSr{{;5aAH-&$oeiHBOGrlHY2TQRar^C&s^_kWuD zYs|Kw-Zshywo{-_ z7_v(Ki(l|d8b$kW4uk@K3piK*T^02Auly&Hz>}BPmuErL=ZnBrXMTBQX=j%%?lS`$ z7b<$oq0H3Gktv9hC@2cX6biG9DQKgl8OJGPAJ8bq7u4!Fe-3u!#K!E^(aIOh_XR5;bkr6cq_Xqd`9KR`{ z0l|@4fb#fo&Vd)ir9mDrpUj~+zOSzjymDTz$GSknv9R2`mV+N=77~mFsN)qG0YgET zSgGS^1yIzwmGqLtIp7*rOP_;^KD5s*bka1Q%ZA>^n}866F*$};uRdn-{*!GoIOT@P z5V*_qE@*J#d;md}kVS{>)a>d6z>>iXEkM$N15K?UmzMS;m%4L3c0PU`DcXBJ8(MF- zlGwSf<+PEso`Nf2>jz*C+yY!W!nX|c_tu$viXkmrRQ5A4j%;(#rd<umz3iIh0HG1Cv}uxcUn+o#8I~*Q8$x_c2iEj{``)u# zg^nU`(gYyj`_CkY<|~Rpwwr^vs_+6#6$Z0p$kJ1xlgMgPYPRvZt=`i6tIXW^*2@>q zlLda?fqGYy*HeGCOm)1k#ZL{u`=iIYnH&i&*~a;YpPAE1ePE6czyt9%jNO}UoCGw$ ztX2NO)@I-;^u2{ysAa!3*t`&`?r48&+9=Zn1IVSdA))OTjUfJ_C&1kdQ-)q@v1ORljvCeRmiZJ4y*Y@JOpv+ zdW7kC>%tjm7;(f0VWEqIreSVMb*uD68qUX4nu{b-Sodz+(ZM*QcJRFA!(EZG`@hC> zCTGA1=ScsaiPP4~M7|>?xcjxF|9BKcV6j&HALF@763esg%1!{-;hUKUOl+3U@CJ)9 zXK&UeMc9SI`}GR0@7CaN_{iBZasZ-FEs%pGYr5*hK*|`wyvOz33?Z`$6ih63bZ3X< zpihB+X$>}6M=#2)0mOQdfM87C90g5@0wRvOo6-mnH_+HPIr*>CqH=(X&R9gWM8K93 z*O02F(Av$zqdD;wTOV+chKqmSDpMJ=LqnJkH(!JJSP@QIHKjZisn5>#u-=v4z!lKb zt_{rVZjnX>a;dMd>skh2!ecT0`P7h@0kENI>XZ<$itx-X)q5u+{u)oOq76Zu|78Nm zSB9&vLo#~8YH3?z(#Hs2`7pcNdq5Wq1}oH*^+dJWD_rIS`^>QvZn%L6ox!P?^JW65 z!%R)RuU7&)VT0>GvWO4&hi3D^6TSzxjGP>QyO5ZJMvs#u(#BeB2VNcvp+G^_=ey1& z0j|O$ME|gY=Kla_{mn0j&pij!>mSv@Xs+VHfSmjo9mdyaMl~@Q%rOto6Yl~1h7Pt= zZDx)NRTxpeo2f$lt+hcaxzBCj1Rh}pw=@V}Dxd$gwYteGY4wu|Ohw<#Ou^lFQOZ60 z9yDSdi%~I%u0bclWC!9#`%m71kt2~1IGa$2GliK4&__i~C9xT%_%I^VT^H=BwWASQ z+uR=*^o`q0k&(d(%J4@%2rWr4>?O&|V%ucC&b1F|(nYGd+owwZ;!|CnJ78|%!Q<#6 z4?v(a*d%gXX%%zo@3V~FHBSGYe>Rqmb5{*ubxY`oy&wA(E+q5@>G_t;orvr*$24;E z)IN~~NbtMkVwm~xIqM%EY^FfcnB9R>6s_W1i*jKpMMnLZ;@1Gs`^cs;_5)Y>Tm8we zqqpvpiRFTjV9Je)4i*`JQ6*W}+VV;Mr{UxOMrC+_CTB+)e%`$1%>KTvj)BtIBPBwb zhqk9y0iQxVHFdoVOw}uyH2l6P1O`JPQ1&$eHahsu&utaHF6|XagG6N~;_C`&BY6n_ z?D`3&*qTz3SRvN5d5Qlu><(aLLuP7U9hI1*vyf!sTZvp{jz7}VQ(+)BiY;2j6_`!2>S_CzwR}@!~yZJ;qtLNA+IcDj5`+%7!pY%V|I4fZEYEZqz%k*tVwW{ zhV&Z?Bf#GQAhB5HqFj@Jd$;>GV4hD=An-t)tt}!?+*i=NZ2MUlI1Fp3X8cjOU2M7O zt;b4QPA>>yZ*uII1?Tp;GydG&VaTB>eHxmNcxD(FFl4u3VW4%OO&ulekU;9PqmnK* zO^EPq;dDLFc<6v1!{y|s+&JcVeQQdxn0q|X)1NrYQjW9%Lvf)Hlw&C*E=E8Ql?-6b z_~5lTE4_CIJduil(_z%hA5Zg-Gejmva7#qmIKuCMR$4`|aAS}G|=jm*2M zqLg2%U~Qet7y5>TeC%9;L9$?<9L${ zz{Zg#n!c!tJVNyBj5+J<>;zCXkk}Uh7UaId1FGktu% zPUF#t_FCe2kd!_NTFcW*c{wzv)NUOiN`sLIbsVohwACxT z*_@LN!dRm7#lNrK-t&RP46P6z{L+t$ah%!dsDj98#?Z!k6Vd54)tP`Ly>8m=dt+>` zl+STJlI7=Oa3}=)^T|;HaFlqtoT&0)eZO`6UsSxKes>DyW|>v*xcrMrFz(Fneha`O zYr4*nUXC32)%WB8N)ig$?eBIAV*jvP7-s{W!$qEK8Ud-3CbwmUG4V_!Qt05{DZPFP zgm3!WB!X$??X~J0R&94g!IgZa_e(_R`5SnUv6diYKg3^9W+3p=qVmaO|JUY~DxGmX zh)ObuaiPpxVS7hAjG(PA5-SZvLiO$kbk_K?WgN0A5`g6UneIPHzUiL<$u~f-O3+dA z(T)&D-5yX}IavFSEvkmiG`f}k+V%r(fJE$ib>>QE-$_eePtVQ@0b^g^xxiL|lET7H zuEd926kvLaK&2aqp`5#|nIv_Cx4izjW0((YfKJw)RKk#f3w*JLG&(zez&EQ)=qp@pFrdu?%VrLw?C2 zAy<&m`_>6vm2Mklfn=u_uzaL!B+@y?7sIBS1VTL&76$_^&#afKyr@ckrO-jMrElo9IBXM9hELfm&A@;=bq@!9&GQH0Y&ma;Ow-&c}3!d{{B`Vf8fW&TJ^=iPs<< z&Fp!0H}5Md09_;5M3Z2* zTg42dVC07h4^SR&qKtGF($Z3Q}K;}pdDbuzw;5LB;1PMRubY~ z{1m>!HxB`ko{9q4S>e@HAnGkjA1{Sb9~Kc->H&JF|7PTK-TD`?_5o;9NbFt_16n}W zQJKz9+qL))Bl=twhw(1qy(ZIZEATW6=lL7*fSmi^A`e**Y!LQ~8vEFopUe-(m|ixX ztKDYoN6}pr*`lp2EI>dCH&zK`16dwD+;-*7|3xig5iCq>HZPRi=zi{I0Ldf+7m3*N zhsax?>@W&HjB;KDV#3TgI8(%RB24m^kTNH2gLMi&N6?MMCaMGrZQT(#?x?nJ`GrwP zX3m93@_e@^l51lWzt;4O^ne!h0Dg-~rhxHml@+h=U6(NJAT^u;rY} z2CpR-y*pB+|LZkrf>R4zm=l|UMBy}tdU-K@FdA+?>~Ux2M?5y=u6ljBc!bOYNPEe0 z&LZ@bJdvyYJa-#hY=A$f>DiIEn~hs!Xde!&KF2-yZs{MUP=|oz%(A0sDpB+cF3BYv zyiUP9_+sc-pfST|LDa$mf?3FABA`YuQ~g$l#{;on3Fs+KYWTIcA*@HDDMtNI=ELW? zp1>*?wI$411rPJ;9D&E-aVl5)cE{Q;df-1Sm??|eQO_$)LSZ~tjV}WEj=l1HdLI?X zTbBgOwuV4Atii|?tN;z5O(N+XtW7(+?dVCj^RFy+J2B4tX9~X56%~{PEX+xSU#yA4rsxn z9*fB6Z9%YiIts5VRORs3<{8Ui|H-y)xr2uPWLw|e0QD+#0f;dm{jJcJ7@4UQH3p?Uo8|vzolc$k(@|ZtS_d(DEo0 zMRX(lQKLnLW|#@#&2!0s7xgSyZSMY9gCkRsc*bcmkK(9AFAoPe zXt5hK7dt~fN!PNn=*FUkhw=_Z7;vv=*30&;6A>0Dvs)^zBsn?|o8bqLz7C8AEhy5E zh;uN7<9Ac&AbiTKy)GbEU1>2sLuGDUckm7dEG%%yF5#@<{5rp)8-HH}e5z>FOdzSW zPyS;{b$VmG&|5E<`rt=chhZIp9C_8%glRIUnRDU^)bFAtUQ$?6Mq1w(Mq8S!i@cEC znLFD72$y^;#RuZv_X9*n(`#M*P^XR7uu9+`k4jtd55Cd!jItyq&TuYC#Jfnw{}WH; z$-TKB(Yc{ur$J_sOIDPmZWZu2LH4D@5ixZfXs!_9EgWxE4KjPA(XMCt#I%#AQr8&yXs z@ksQ?Xg$bJQ*{oL8MYetx?_y~+}cukN5x$eRU9l~L3bjiQUWqfR2v6Oa+tQR8`IA1 z!vft~257+kYrM)q5U3>&dnS-Y&a<*}{oaXE*x-7uJ3tGN=rXTX8bC8ICP_Y(@va7b z3K7F_`0@q*w)KJMn@=G}BZMaW4msTTB;kEG-|bi?(5p*U&>yD}S{hNiU@1QR$Z zc{aFM{$h)lUcWs-p_p^wVgt-R=4^hH-Nm;-Aas-~^`D$jG}ZJ}5s!)k9L)cLCmvQR z5)$ok${!Od&A)>7wz6`fgq_d)GX`&`QOc!!B#mkyi6sC{AoUJAm-H;+!XWwN9E#lq z|Ji&BFIO6UU5}#xog_>z=k+jlmxu5aNalhJipMcpMP&aW4Ad8Brsl73D^D7+U0WfL zp0C}Uq}0m`{E(vS;_ZgM!GUm(yY1p0cUM!E^$VI?%XILstpNeAmL3qOvh^R}nhc*v zB=tWmyk6YB^R$7zMI<01YS2d-PPcWO?OxE;`(twI@a(kI%ReVsm=wFMqOZ+XTEw-! z+1`143R>DriEVMw%c~SLKJVJ{;XE)32RTy;U^YJxD041X)`X ze-P6qd6)Xb>j-L?gR?)ZCI0k5&_bbNC`f({ayD84MFQ#XjA>vNA@dd3Cea>3UqV5z zu4W+1)zuXwUgr#4A+ayebcq7OY+l{DsI#oIf=W7gJKKMGdpw`xf`)!GnhM2c$5$-8 zWva!J&+73GX+VSDhw|UN`oQVVlJkyJNf+*LCu`udoxKCNtY}}sSEI52BB@J=0Z4`5 z-;oO7YyrR&fT;ZcfUy353f0hHe{);d;n_IENk$NqiMTVa_)oKn8yY6>mV+tklY@c;Da6o^NoG|dgsnvlkRdB#up#} zXepr2-a`$dje7s(;oMpaXmDs5#dlWB)DTRCt^h8pYw67O^%uP8A!VRn4zL~9%~4t> zyMlwi*<(=c=Y>hVwPaodOp5mBp+h#qf*{mN6$E5a^LLd32J$_M_`A3`SQ1P!wh}nT z_C6CKl+%ub$@Lo|@;=s?IRqq1r(yUphsZILo5v8%r$Tg95#UtD%GbMR+aY~)u@1Ts zz(7KIZ+RTw-Gen4H{!Pt@;kSX6n6dP#pxB?a1Ev>_@EQf$|s33&>229d6(%0kU#| zzFkx{=;%y>Y?^B0BjnT|E<@;~v_vGpG}wK)O;!mxK=daVa{@^{Ac!vjJjhfM(S~s_ zLi4DORS|8GiYg$JL(2`a)aRhDzhLiP(x+>?{9ifzwZI(7TA1(OZ-2a<<@UW? zQ3iPFC*S9waRA{an2&=0O;}y}nb-dpsHdz=Fxt*~K*QJsFcE+mr?H6WJU@twN5lKG zeX1il^I(PgQUqk{f{4kZ5O2^d#(Xs?UbNiSk92_!K zR?Ze7l`aQ3Bq#yFx0Rs@P3*}5>ZLG=6>2%(187dYT#I}L97qE^ER56$yF%{$gS9_1 z&g7rP=vZBS#kd+9NspI+5`>CDaTlbd))wx05e0k{fKsmi0>ou(`n&`1PEZgg9g@YQ|KH*Hka0PWFR_CN>D)skEtpDFqP?D$M8 zE;87m4#ehY%f4q)&OpT&LGu8fs-+jN0Q40byL(%9M;=+Jer@d@S!HN@ySajc+fwq9 zjr=((EyC|UzF>0#b#R{X2tmYqrN)It+xfb&^CXaO{lA5Eo+(0MEiJwP1mEuvOM6(@ zm*t5iGN5}Hl_7zVi3jSzc}q}lT{ltqCD=@(?Bw`fqAjy(m$jXvwEgMQ+xZDZmxFHN z)d@=Ml`+ScuU{+l`kp;{6hZmu23l73MPfbb@i()`cKc=+5ubOj1*I`S^J(sVR^C9JNnZIdLIi7XmT`{0gK7_tfZ#ohp&*`ONlrpTt$ zsq5x*%a%4-exOoYyLjg8hpXKXzQBwjOck|fxPlnKOq>oN>=_H|_zr;4I*QoMYh;hu zf@T}ZDQi`gUaE$O@GKXTL8OKTYf2x=m_u! zM#aD(4np29cT{iP*255(hCqhvC4X7lH%AOVENvBr2Ine#tEmvdTK_7K^C_1*oL!gf z^lOt6#M`GC7}l=f1hSa70DTW|#0{kz?~RED#(osq#OLH+l?NGE)mjR1cs3k9x4=eF zH^hJhMf=Nc7yJNVu>q&%B^wu)WkDHcdMv)UpoF~|43@4|%;2jph&HWl8zG=Ye6G_4 zu%rVwRq@Pj;53EosYAZmd>jZaSNo<|XRZPg*dCZhbPX7y75&q(%w#M6K`1!0fsh(s zqd0^jMr-{%gl`)xWt9g{wKr(-2nqA`_KVT6t_FmZuYDeb@fWXa?Toha_4Wde%A;4m%oVvNc5=t=d__YoNnvP z-g73Q^E)1s{}Fr_MgKph1|9>gF!6th4SZsQ)e4dhynCm|#{p>qtO9>xdM{7lxFgWh zEpfEJB#m0Wj$WM3x$*pAnJV5BEiHL3oQS=JR==-BblE5IuRWkB!-oD`h!~$M0qV2j zD?oy;*IHRq^M>~8Ct8Y2`2h&8{XCsl*{80`5!H3AUv>A3k#2i;6xi0AA_A*)!(gu+ zfkZ-XLgVqG83frgw?EjqZa?5*hQ85<<)?S<$~kDh^$vhLx#E`l>xZw&qHC-8uOF>r zfaNYXl&l??yF%rv4Uelh!eVF2m-#&3KrZEKP zz^$tbQK7u8WTT@(*J)~4MIt+m#hF$ur%Ho%bjd3H@YShuA-01i(i36#eH!dVl3o0o z(@*5o9_tL)lRJQ9=74(+IY_j$rGvqit1!0E&1+l5`{u~)n~e7cA|oBbN0Xkgo}1C) zxb7kkzMA|bFOVmI8n!)y9n=zLC@u$A<7UwLrx5Bn9^?*SIz?iye`KWE?C-43M;HUGi) zCKq{vW)15j_$&~}iw9X^6UyFBSBH*}YJd#ZPQ&)HtI6*Mepd2$e3Q6rJNe@ZAXOX>uvmVLEiFftaO5dIJu62wtHn zWzXk{E5|s=pPHpg>+V!>#!(8(aTtZW)O|EuiwGS@6S`-^2dtm~jZy^|Ll$AZXwyVp zshgPkLzr%}>Re6k*A6tJR=2VrWcRN!Ph70t~Vgl9t|T7r-Av2L=Q~d z%xGA&wT+4dMdy_tuSl!Dqf?G-X7-C-0$0UU>4eEkjkQJ+iS zF8LNB0En9u!f!5)7-_pk{y`rso=*P+hcK{$y!slUX#{qiy(4yX}I98x-!J zf$tLt_se}Xtoogp{hJ9Ct{NSH2oUT?^gR`0Xs;wN`GJrboSAZQvnYLck`CeaKP4SY zb>gEY2M{@J%{S3`-XFHhJ=hJJNPM308kd&p)74;y{4lW}3L&%_E@!O@wmPejvP7Cu za*$XFBNE%n&vP@DeUtob!uns(d401|wND zHT@v?nCMQ>QE=!U-b&Aeu{(2;gfkgQ$g({T(d8L@(C}s_Y4<5=&(d1v&yHh|U}$Oa zY`Ql|>tGsxEDx6&TcvHb@(JFSpu-I+hjA1N@X8{c3wiB7%X4FZkRy*vWar_#LbTeu z(sKLO-UEXdTFcZfUJ7VwnL|qfamqdRSqMtK6tfbi4V=9O?WU*He4^CQIq3%IPxxC` zU>dlMApNzTFwl8jAj%NH<`|{lD(Rz3TfWCyI^>TW>;95KJ>dCAB1xf2(~;`gR{Qd@ z$rT<-sNz!=u^V@IY5ewzy7TIY#0@^G>DkR)O!Rqsq30R1g5@2h9GpmhWa7$zOrCH~ zYqRd2xK?9jwPDi^y!->m5-B|NR{JaH*Mo=+5E(*{*FKafh!|r%Y)&q0PiI0KDzM@! zHGrNEfQj&QT4O%7<7ap>O)WM1(!a`Ha~ppdJv0N7k&@y*;#OY0`$Pcs0g925Vn8cH zwLY9NCBW?kTm=VP*|S4|y4s#*cj1y^ODA&Vd;KWr@W!L6d2}lQ-#Xn*rrQ=lDHFcp z*ypquCdlR1^^PyN;bd^T%n;J%Xcs8BRPX?`%5nQCs?=C;bHYH#sG}wl8o@BkEBP12 zi0U$pFi)5tw!@tmsfeB9hRrCgkC}F$F5-X zBUPHbPv9;Pk|f6`8|l~#apb;m+yd9l?B|D*#2tFmAKuP8J-y3)^!>d7oB7S4S7%0jm7L@eAO@KrzdbxrYNN?nZE*ehq#6mf~ zt!oz0%Vg1`gG4)bSxu%g_DiSbm!q%Hde%_8?yum`!Colj4j~p22Hd1HIEK6X*hUzP z`6Issm-4~az*<{_SOAc-29zY>{{nu>02>qNOskBRs_i?}AuSNOy8ziL;NZo}t|3>| zc0!NvR#eZx0DBAjJ_v43<63$4nWA8BGxGB^#Mg4k!umU;BBpTL+Cuvs0B@WK;W6B_ z`s?VK$on{y;e6#0H!h-)hI*AYW%Js$R6}SonL1r1SC&ZxYbzh1ZWOyS3Co-@`+6;d zInkS>2g^YqbECe@tf(M*r^8@LtLTq?lN}4JBfvf(xIh1WKu-mICb0)!TWEb0x#ve( zI9%@vsx5*z!al6Lt_I1Gg&~XY?FpC&yx;E}YSn00Uk?t~2AB%MW@o>l4i^8vsaL$U zzw|&MhSLpia0X?4W9@n?*h!(sWi~V1T{zya)7gM{1y6MM9!Uy4rwxy6hq`ZYIiBUw z{%e<9TVQBjQ#LjArY(kw%{wC|vy;hK`mtN?Nrz&twm&X=ML6y%(Hph_>x?)}UQg6J z?TR^N51Z1E09QalbYY!5CVmw+%D7}*1OX)DzECX6^YcC)JF`H~eYN&JOL)4lJ`3l2 z<-t4QKRP~P5HO)u#0bxR;X>0As+yoaC&v$|Smk3^g+}+RXsawaHvy zzQw7gmhoTV%CRHDjaW<_d1%Bt!b|No_W0Fm-x4Tf-KNrG%Yh?U6cNX2Im+b4&yfeX zI#pFwp7>6*kMMu@*vJCCMecWR-x`C29NbipeG7C)K%_iKrV}9H7J>4w-1j;T z`;8a=)@tqqNdgWeFGVDc&SOzY+MSN)=jTNL|I)x5%m^Nu3FI2UDtDF_&vgVASu}^g zpT)oS5$gM^By0LtNfri9C6fj9sjpu>qqQ+&Eb0K!Frd(ia}A69>x5LNAsJeM@yHIG zRFB;5M0>GxUadROCgZV?wSAXB)?-9)#kQFQY!}*KN4VGokEg`1QB_L|3baF7znU1+ zokuN~MH)>{TzCy6yothLyCU$&sNZ1$1APwX2H794S=~v67vEHMZANP?pYqrkHhjRp z=-Mf+zw-kT?yTe4ot6g;yHDi#jC~zy(K`qNlyK%USa4r;r+st?11=)ad?!Hl6)tBS zf=E&Md>&24hE-(6lI$ztGRp3qR?Eo}@j|5I+0&=NCq#0KTu~zk6<&+%BHhZ%#F#PI zfYh4fbDMW+Y2kUiaII2*)j=R}_lhy#R9Hpi}k1dg-V#+@rCD%a=OfedVww!Z>DAUW|mE z#=MYPUR(6Lz(t{_QSxL1(|Hm=VyRY(Yy-Wbd}U47UBHZ+uYazexCLCw%S^h_Dcpae zbK2kRpF=bS^d3dbI41=^%$eg2cyEgRO+qzmUsyNo_%LXR<* zd}`j?w^!oBIK|?N(R;{x)K*hE=+3?BkednEzXWVb($<CcauIUM*CcJoJciar--r=I!moAHf<2DHdQl>#CMR<{bP%`Ifxdx1pYkzVbcCftf%ocA z@#%T1dKflkg^OKQv9hN}v;;+^zXGK9bnJy&g^&NY{us6YG)3~~Pz0?;vKrnGxKi4` z-%+M!Wn8&JE^aH&_S&E6TV(T#`8hwk9n*iP|4>8U{@067zWU&Q6JJ^tUwqCk;UCtG z<)MuGM(Jge6zXeQ0M}w1Z9h#*i_RCbBVmL3#!(?UqJ7=Oh7_@;3|^k342c966^#KP zHuwtj47AQEtTU|~8&{U)+E=emzvE{*z1z@G%bK;`sh%>ZZ9Y2^vDm35WSqcUSm)Z` z+v>`}epZ6wePHo(^8RU)CyAG{wuGgV+^w@^jbo7f(_Ow~7;_{(<(DsnjD@(=(oYyt zQVZ;*)a`ib*D^?M@}*yI!Ebby>Qu^gNFQUe$w)28oD*|Zz9AQfy{dXkAgi9NT~JcO ztP^rhl1NwICF28)e3eJq?g3RKl3M5j;1p;V4-P0|VPRcYfqSkKNgi_~_&`Y)7uo&? zUA^%H{ftaMGae_C$6PYW@dMTYlqp)C*bC{PWh?aVm7huE|J#>}f;dMYKd2y<&H|WM zSi5!0ga7u`;2YlkF(&=X?ueg1BNLy6$I;$TR8pSgfBV-v6q(fZ_0=oT54ad) zUqwV*G9_;*y=YCDnVbx+6h9i|!5?-+|IA{iUvn7q%c|tmv2dZ$wEO2xyCfRRjO6a= zKmn=x<%Izl=pxy_kA%)nMxFP?A&89LZQQ#z%G4Xv=(an*0kADIL+^LoL_-$rt;b1j zwmOIVJt`NCy6LlwvXS4YDXJ$}GoS9eey?fB+b}!BbtThvsC#$5p7Du;JhNGjA!Nh3 zCbvyt^xO9+ZW?XqBwkHze2zWz^GG;O@X-*rM0L z|0ajX=KAKami_ElaXJo`KC7Ej?CmjShql3uh=>Suh(iMdoojKvnF0qbQwF+s&P6j> z8jusEXV1P2rm4Mk#l;k#)N<7Zt}Snu!5XljFEVKU)Y@uzsGH}Ft7j4txsq3SYV&w&$r>B-Dkg%g0);A zw=**edLC~cjf|MmFA~p%C-@}vKVN`^UM^qD3cLFDEQZEx;fc8IvIc=gVG3^uCM;Q* zRxI?L0y|f~!Ges6%24wCq>p`M0lQJ|kMN*hyL+}Wa@!6|FDoSRk%Tc$38|6@?t}4@ zo9BmD1Z3y^lY8hWC?z*ZGuq#x6$n}yMF(A*+rjV!etlYJk3c9%_EfQhKVCYUD|$)U5AA>g*V4Td3X0Gp@IbLRo1zL+_ZTW%k03)y~xKbL>f z_KC`1rXW<2;b}jB86mp5x*$e2D)q+%wQ*pbf;sDEhP%^OA~b19kuR6MIX+Stt+h%w zY40lA+*xKb8oxI0lTJ*Rz;{V7;KKPpso7`fmmW8D^DH)n_ZqC6OAKzQ5zu`w8*uC~ zL*pMO5t#e#xX?&FSyQFx?tH7AUXDa=c zc%>W@d%-4D0PzrVNEVGgjl91>1@B>`%rQ;2ZUj8$w zx6|6;HDgr0?Q~0CpK{S-0$Ea@b8u9^%>FtO9=*9_dceZ+R&sr9?U8$Uib2ESlpZhu z3SOakUB3Si_+M>^?+4hf3b&w*&V3oFeW3c<@F4+SumX*L!l4QrkJ9pCH;Z46uet*R zgClaiagT)6#EhRoJ(26=GDpd9B=y4keS<AfS_zdFs7-Z;t8x|P)O_y99~YU0;giP*G(GvGD$Ue zd}R+m-;Uq|Z}E9+%tZnm(PNs^*uLv)+b<6n3~z{{3tY_Q86F_{K+EzjudY%!42L_E z)3`luZ+<1xnUjT}>JwJ21$Nava2L`J5}}+!Z*7S}^`{gTlU5o|J1VT|=^1Er&2vRs zBg8*ld8NidW<3A%j3E;N$^GZYdyEcjtZWV0)s=Hyq3dA&??aWgt4GbDW1i8k8gkXa zLnNm8L%S(F)Dzqo*WV*YKgl=L#vs7?7GT(-5qdoR0wGAW&-Q>V)xH(d{xgGzyUt?d zVc}B(V!!r*B-=jUOt1@f84Fl@2$y}la`fZB^5Z>u<>i9b;t)`%eEMDj6Mf`@;JCF& zul^O7MpG+C)FgOF-uYe91K6$(8Wkz;-FK=PYtF)SvH&IO)2B#lr(7Zo;+E|u8bpUi zX|W>u9{_chsJZjSVFM>?fpAT`ds<#KL%pKP;Qm z*-RO5+xjk`3P2VD`Bxpz-$;#6)I7b#gZZ;w7#FHtKR2^TRZc;cmg=rcr^lG5=0*5Hx} zvv;e-!iaDTT%aLO+%XM(ETFDx@d7eEIXO9+)2$kTq-;4`7&|S;O%CBJbE}$uk41{v z5;Rk57--~|*53b>^Xo%)F0TmmH<8`z1jtW7>QbCUM70rNpyvdx!QoH_KRxsq)z{I9 zbMGy#4D8`(-1OpN4zCp*pL@(SR)~@NBLNRcpvjzOGx5=rXtvY(vKW#*ZtCIjY?~+Y zqwjh`mWIMg`yW5nU_xXXCu&rvA@9)ICgf88b@ttlIKfZ5=J?QD$5%m}z}$JUu#hD( z@%~?THJzUsmbSL<8Jm26dD`%-AB~8Eng&nCKx+Yl>Dw}4{CN@i!d-EuJbqKkla zx}PMF#IRk5L3aOnYRmV?sur}zEqQMQoA_+=bZF?=ua)Mti8ImdtVjGc>T2y5hHtm_ zF{BSR#B;ivgOPD;UW0!2c(DHi9BIk~gE?}h&-)uHZJK_&YQjch?RK>(X`G|Duv~X$ zzFhsK>mdwFl+d|m-shP;T&WXpxjclb5a*!lkyAyH0`?{XIoRgCS>k>M9YlKmxpn6l zsx`I6&g9A7vsGc`0fxzM60mv48-R*DR^EZV_8upf*ok^iXgEVeWuByvI{#vfyjOgCVRC0i`;NR zI_Z$Cyp#qq@utfi-k}!&goMIl0b(5CKZLKTgq%0C+`e+dFIMA7lEYSTgZM0*(#8hv z(`smJ`Ipg;9%hmsu4nj}ktN~uL<{b1&$PDHbv$mbv2vPc*fHmhzo)jLmzW`bV|g&T zoD!oFGyVPahbD5f5ivgNuTJayk<2&D;3%H&1ra&~j2eI4Xe-><27yLaW9jI#%b^M; zHfFY;GH8T?Slh@g|7X8{^QY5DUu!N!mr%=fm@FknMj$xREHr&I=E(iGcpG% zZKvSk{-%InP!RTG*Bs2@ns~o6T)RXC#)ztX9MAWr(spTOVWSKsln?d^01i! zyk=xPH4CFHQjg8IC|g@w;2|LJDIu=>0!_D#7gd-zXyHQn{>Xr>>%nq&t9krlgTy;4 ztc+aW0~2K*ugDm|>NNC(oLlrKvn%n{Z5+#1ot^$0sF-BqA`maeZTaLxP!1tqNKhz| zOUhKZXP)e@XV{OEhfC2}wo(Lxt)~D2PQ-l_b?WxbEt<+JmXh2A9{-SuFR*WsG{&{s zRYyWXLKRBk1hy!~^UHNZ-rDroXsC071w&XOUk}Whuf7YHwjOxdIqgK~=NPVBxlUKQ z@sF=^=;-N?o@S@Om=&>?e%q5XTN!XbIix0b0r}F>pX7IWwQ%cuv@T^MU7JMU82?#& z^!)5IxteXwFKjU8>+i*wgxns1%Y#ooqK%&3;E~iaJKvJT0MzwimvBlz4K8H8J;l(s zzZKrLAXiu;51!C5?<|gnN9>xUX5!Y@$;EBIaEkTy)Zsylvh`EAIi_QLXQ7)P=|y;< z7e5}W#x8ywI>WYvRa#pvwr_HGe+W@l%6J&q9c4S)Y(qV3zm97|Qptv{uDXFl#G#A- z%)#4YX_TPW)>jHTo6OMU^r){1&Hjd_z>p%Iu!M4N1`n}3Uy3tZc5NQ|)edl^UTB7J z&8k1_5{N~_?dDIYSv@{bw|q~&?c3EH7{YIWXfv1Z8n{$dQv(W!NYdr0`H#~a!_nj% zEw%JDp%L$>`4pyW2E(Y-G{08C&1I&bm8JYB@4nwjz2M!+>RZ2cloeHM?+Pj3I-*oS z{#aO=bhj1sdVb#uv;0eINoryNnqh`$y^;@1mrhhz{nnGlVPsbWxoy7rD<;z9_cYyq zVGK$lz(*I^!x~mG@$c%c{t6%EAITy_qi;X*w*ESu!*^i=rSa+Qo9U8IpAvnf(Ho{p zjvgY*L3r$!d#%wsHdNf%s23D^3-&|!-qxlwY&yptK%Hw#fh+=UGdEUshk9zmP!KH|bM*->u>cD7Ja17%Na{kk%4 z(D(F$nk)SAp5~OGXT9a#e1%K|Y$QhCZ!8MqXAVHqXh;}p z>;uD6xiW<=)8|mxiChD;+_!-<{#Yw6136!b+S<=2PhP=!xqJ_XG3TNEUU+dW$QT$< zRW;1cKL2Bs@ytyS9KL37QpF^_pCT!bPL_b9-^}2|Fy8(4R8WAk&z9hsYj4-J?SRKh z?5-Um;>rL=E3Ya#4^J9;0}na%IT<3oY-{!Rt^_ABr3Vll)|P}Cbv(#}K;9l~M^quo zp&z*?s8Wkz_~T%B4G;ExH}M=^wl%nHjdpV;#Zrj|iUk&j>oey&GCPPN2obx^f_BCx zeL-QNsgKU9Ym-LsNYSi({~k%sN>UQumA3D8pB{hBS#mUy*VUe&ZnZajFhhc;gUT25 z{Tw-9GlJ%y%?Nr9?#IXbaxF@v&lvPV7Hyck9zY~bx+>qE`(kwsexk8dlexT%o(}yU z@KQndm5{tcqr|;&EAv*=^`^s>#Y~|l2GG}El0dZxaZpC~N$gql5;n0;RUkla-@o$D z!cnsgjqwra;j0zZiD1SRnczQkl3v8rVlR)jq`k_c(fX$_?himc0hvW36x4+XTvN)Ja9qkI{}F2P^UF)Ccf zP!6AjFU$Q+UIV%o_E0t{(HAA%mbVJ%d_tpG>0YdTe$6?lvqd94nZlfp(ZRPv_ayT& zk#U%DiFUJ+voyZUNhRaFP{~aC%!w`I=%JFijDc=btnUAm#|Wxq=FBh}y4fv2m|CD0k0YV)crez^K$WUmDag zVZYTetN!+|>gp3jIesA8L%E)dsC7(6edPr|WHa9eo`jozvNBH9rD zZ8*sX_-Q$&z8I8OEr^weu8!&|JK{HvgqPh{c%Q@bog@lhb#zCQ#(CdbNS|8A z735^n#ifu2njQ*}3a!dEY*=NQUv&Ry{qx#|5|Ps!a)^i5iQ>C1j!#VGpg)v{^!w&9 zSO`87ro<6DmjLF>+p%$fOe=!JY{zAac*=gfeFw{_7m>O*R3xj^LA7hKjplPS&F400 z1vf7aQ6@&+Sf!^Bqo?~Ek5}mZu~m}#`NowwrV)7Brd|AADF?{GFW&A&L385 zM2<&;hTV7-`hLK@{JAP!Zg^~zz7HRfc;xA5wmz7@y{08G>&unmUz&P)%vQf5#5+|) z|G_W7%E>T6=6;8gsumMjd)f!dsW#98eoXR4u#dmJ)JBnJ@jC)nh}&|^qTZPtwY zv>9+dx@t%_;Z-!-(D4+#`p;KxEh=D7`WDMDQ-<3x8hHC?eTWlQ5W=N3jH z2d(DAmUnPR+hg+i^zFfDD+#%+tvN~Kl%{YJZq%UNy=vpt>$B(kcy9c`5^ZHWpId}I zgT%#<5CXw0310!hrx)o6CWH9d!*5Wiy^X;7maLIEZ;Dv*7McvTXJ3?AXWb?U--$Pg z7RBFmD>F1&vPN*7AY~cWMN&2McfZFiBSOh0f&MwJWTxIJ-(EK$ej>qXV_5X|S_D&Y zHPq0CUhedL8*?}NL`KWvc4kr>Jm^pHdKE|v3dD_b@J0l6QMohcH#ddsg#+uG)iN1H z`<K4#eXHNU8eV%9vvLrw&2d2kw`TpEfbeVflE$qP*+6{E~ zoWtl%25}%sQsXZey&~ug^rL<%WKq=ot1GL%5DbeZ0gr!@^*f9LvLIh3zVWDTp2-JV zKgS^i1D}J@SS>j(VIAUU!5 z)_|nH%_8|HAQ72J^iRY<4)a7M8VPW8Pp*%S>e^_{KnDp#;sIPK@!KCDL)Sksj6X4k z|6idJe|;s{eujUJ!e2N>n#|wfhqPmrr_Y`ZWnIk!MGjH|Gft!HPCDo?eMl4aa@EmE z+YXO=D)$HU0w%&n>hEtQ-B)pTcHW+?w!S=;-*n9FYq9Ph8L6Fk{I%4OqtTS^^@H{|3IvN!bo`}fD(O`zYaTVu0Svt4<5B zn)>T|&jkO0%Zzr%lK`wK%_ExuHok*DP{usrh#ex`0v-hzFqL zXm2v(KmYZkA`?~q4rnpV`9LWtDS>lU?uEh5(5rF>IBkg-NCksBNBCj!n!%cw(bmRU7 zepR6TeN%w0Ev%v<@c@_u;=f)7-ZkOZ7b6yr?%H?%_6-QineIS4a>mEUrzp@3>T=oT zYU?R(lK(oB|LR9f@wYSHqw=zm19_%_pciXU{gTk6C#JEszCO*$g!bPpqZi)LWBK#Q z3pjb8sT%PU8@RLEal?Uj$^N@31n7rSofm`aPH_O05}T@VB2auQl8G8bbD<4=S)W)k=2J9N3aypmem8MOj`+hX7m0i-kF zg5U;%oFy4PcY>2+s?^YL!AD$@%tpb6?j1K3J6{#y^@}N%Gr#?DweR`@9~XC;Y~@=E z$SFvYzb$xcoR}lJUML==4J2-6*t_~U5O6858jGj)Xfbr-8lGU$9E|_SsB`P@B)_ey zcz5&!=PW(Xd##hvE0q;}xZED4H^zT?!iz6wCZe+_%_l~snSWknS&4y(a|*xgKsTn4t>iA>$1MmyjGm2;K} z;iRLF4D&*wq`lvR*~DCtzq>RQI5X#V(pp$QPF?m5H~9ef)UvT8jv5rP4=EqTWz1{%1m=%-ybB^X>~>A(u*}Ds!6*{v`7L z(7is;P?qCkWv`*+9X6mpt#6p~N)fw!n*?Vff;8314`1(Ho|Mow#b54n0iQHY^6esr+HWWoAYK)_cRtb+Pxbkb9RFkccM zmMv!n@qzMn7G+7wKSiY%hHWd`lxO%v$J-t;DU4RP>B6segks?%L7NXr=#4)sP8Pf{ zD-F@$XtLZe?Q#<_TWX59TxbfWqs-yTyX`=VIF#N{F+-EQyX}xl{z(={gkO*WxQ-7N zfGj)Su<6#ZU9sL(++Av^oW5XifYhYx6ny*PY@4?S^|vdZc4NilYIv(}t;`>}^phgP z`#`O*ZZ=!cOarbEFmhre__Jrd*Ux2zP#-sq!_s#OvAJTfRJQhyLj6r?@d2hkT$7!( zm&}FF)ZFzvIMeReK#u9u=Na9Rte58HUX53Ad*aRa{rI5*O@h`K<8wEZ9Fj3=)Ln=9 z;=_K6hy>auzr3M6)WK%k=E+!$G9R1rW2dLaez0=nGohHLb5$|KsWi8)zS-kOuCkdy zr;NHqcr;mnjEs|kM7Gqek^z3*GLM`$R7O6T0}N-VN8KyaeFKap!o}wWp6+C(ZE5e7 z26h|BwR%YoRH7HFM-PAG3lO;_)+{f#C*z--v`M(BsO)4~RDRYXMA!=$79nG_KKKol z9&DOONI4s*sNlBQ{?>S}kN;rCV(xZ7OkA(Un{;lfpI@@jWo6j}4Fz}1fb#wVOiWamd_4(Ncr^yifS3*>;-rghzPjGoJKwh1j&9q#jh;eF&P%J<`w>mt5 zH;MIC?@e`}J9S|#AlIKQ50WH_K{c2zgKvFIN9tj+K5^3O5mCcsDku6{tW1#`ZbTXK+!Xa`_?hq{vqA4LOPB2M2<6HwFz#(7Y`JS}uPSm>c|+uOLC9Y6!l13EhCe3YLcxe(6wjqu{^@Mz(0$LB){GC3 z?zLrz@66nQElZ;Qp}2`Yk}?u&5)U!XrAuyMrczmAdfk#@JqS`IPKg^4T7hz};6xibYDeDFP3xy&XrVz)Y^lh- z9EMwq3qGQxHjypyrO|7^v$(g69yO*Y|I(K8f76neVbDVCB28#--4QP zZQME3Iyp;`YBqjor6>r%G4U)G9zD8BU zEnkHT=|x4WzeH|q91D6u;$Dh$>OK)kh?p{09;H=ae~SS~JsNMOO(LM1Eu>%0uq?8Z3j~Wb-T_jQWWQYb<$bvPfP#B?{KP?4?9>O_ zB4JNeU|qVXdnHhR0J!X@84UJ z4qHb)VK7B>f5_SQcFr#7MJL4nXRMY3(W8Xr^Jr34gz&7;i)lG}M>-vMOD$$(9nahp(5 z@%CP4pFUr#&%CSvlNde~=q4Z;NamC}g|P6$KiB3o+{GbsRhNXfsMbJEph5fjaf+Mw zZ0E?FTZJj=LM)GoA-1|Aw$Q-9gaNmzCV`hsd9UN?{tc3k#mHsNGk1E4?TIz!->O>f z4?a*hO%@yVzK%yo8Ha9$HkL`0L{(bNCYVO}-}sjZO)-!3REL-J?kzx~epZi5t4@9U ze$}Iqk+wul#lgz<3C)kq{GB%x+zO1Zw)yB z9`YI8GvX*x+=ji9{*||P?ZGVl_~ERQVgZ5FMlR#y+l-VFvt)#bLaMayI_mA>%KJ(_ zl)V5peB$spVsN3%u)2niUDx%d`HTc2XPdZ>tNL#2!pOIZh;Yf7c@8XWnx3Kn>hjf4 zD(i50X*~fR ziZu6yV0NqF%S#5vN@Sa?{||L<8I|?6zHL(iHyu*a-HoIYlG0((Eg+3ZH%PZgBM1m6 z(%sz+A|P?oA>9q{yj`xfw|hVT@%D#jjP+%Wp~$c1ysqm!&U4P|5ZwI?c8ko^%7uHT zQt*7ikH70`_$S`P6Uro$z&j>={4R*=XO2>iTviJGB<9hT7R-(Lwq@#>j>lqBx;>{v z8_fXNRTPP`>R5MDx*POf;N|W#v&r~GE^VlG^)hSVTIi$aIK6!MDBDc28!wPLKnLB* z&Z^kzdSGDvZY6bCj6A^`DZ>W>%WJ0}Vmfl81EGx)@|`|R69_1Dy$lO?>&3Ua!T)SV z#`ER2`7k%F^^&2j@jf&;oABEo&wOGYu}47fX(^u=MS_?|z8Mx44Dn$uAbCRET;J;= zdDwb{N8DPTU6fHqmnJRO<7#1CWZ`rdV?Ynos*2)2i8$I_M=mjF=oP$dwzWEoyO`xd z2z~6@lW*>Hry3K?_SV5SQ+-WoYS!_$w+#`JiiSZjGQT01@HbE-bBkB{BDNxI>b;@* zR?^{H3KH()tdS^&QXcpc6uvl*Qtzl4z|$GD#C}a>PcEUpa`Z>^2k+0m|JP`!vKQq! zmAaFDpWHc&lps=O0h^pCBZYy;HnWwVG7(XhG5N)u^{~e0hd3-aPk4HFYU^%3vE~OK zdN<_|uoiB~Pw@+)O%LikgPZpE4ha$;=T@AK2*PG z+bThd?wFax&t7}uF#CRN8?mqS$3|phQt{Ds!qOXMprWQXYnmjol1j|oHz{GI4mQIO zSHd;EAlhSce-xjvFwbNu&NjWFOf&i+wep94&uoJuQ;iAa5+%ZO*9ZuMcr& zf~tif=|zd7PK*mI6ufC4OB3?4KQ8k&TFr|rayJ5t!aP{z(HRMjoQ_5Io6`4j{l>pP zf8H80Ibre9ZGo62faPAqi&w;LxS)UHxd?!BbJxn~!~plN8k5LRb)`4F+alws;L`eWjfOOD=(W(1KoM0$G~ zu<0@p;~_y<=r`CD^%Y!*kMyg3&>Zqab`bq-l^ze*L)=lNt(Zu+{N$RW${})cJwjUR z+nDh8UwL>YX;QZa_q-!0LBY1CnNoe(8prwTgVOr-2txjO1QT%hZeS}dR<>B@;mK?e zrU5G~E9v)?`L#y|hYBviFLFh+r{$fau9H;SP%jub^tYt!ZdX|tKjc>LZz`#d?2q$& z{A@r}B?O^7q4WPXZpT*nc^D-`O?0s7^g~%+aKWjhZ>MH>#pC-wDz$08C|OjPfdT-z zl0d3;zhs&{meVOE8c=x)>8dO%PLHtN_q!kxoX7$O_oQf+pyuy;+DF zb(-~3WFS9P>emhS|K|pCfCcCrTgR2YEk9)eKf8k_q{5Ex8(Hg;!yyXlW4WYWb(1%T zyvr{p>4sB2%S`jW^BiYb-j&K3{~!?;JFUtbASWp5RCkV7^3mh{qrsZ{)&$b_j^b{? zNQH>PnPh=tnVFP>ulEPAo=_6Rkuzt8GeuYWJqY7=$b{3_ySm&Rdu+d=Z`0I2(orBi z9q}&HI_lPQsLEwa{!+!Z?f8QLZtH@`PPuGCgl8uc?U(Cj_-u)>3Id`On-1z=3}NZv ziuq|zGGr%{AC}ZtTaA0)a3E1go@+zADu=NN)Fv4R*ID=~Vr?qA8HQk2-cjV;x6OM} z;CYBNdFSsN-Dkd^!e}I@uj<2BjqUjCiCi4xP{z({nivZ1M#^0-xfq#?gC69}O+b*& z!2Lte+i}0F*~j|b8$260ziZ()=fCFwT5E5&O)45d$Bk$QnlwW6cVWDoay&nEU@q@| zcz_g}-SCDC&MN}EYQmp69`335BHMQk>s2RwCkb&YKW5PKNZFHYZN47mV)gntBu?M+ zEG6?rteU(Hug7-Mr$?fACpaY^rI_pa(_zic=+`^V73)R?0)a<35FHdpDIpARDd`7f zt7wXB-OVdb&jylO=S4EBQG?TX3O_+^f_;Pg~+unKDV=SLD!!thghg;LG%?= z_a~PI@xsl~YODIws~UsKOdowo#aOn8;voGH!qQTZST|?@!|B!5eIF8ZaG2OiE z4bA=ZQzKb{z`BDla|NKUwu09t_SXXDTKo@=dqu0VCh_e9kAi_09ra&mW$Gmlw&EDY~r$g+eMJ=~*js?s#I<9Uo9Xw!Pw^yet0sj2pGg zJY7x#6*MXgCjzX9J0j)b@Z*XQvu;|JKs$5`u z{mHh5R1IN&CY?@M#_5qoC0@Ht9w^=-)Z@?^tOki&L2HKb{(I*3%e;&PH4#G5Wdgi9 zb>#M}J6uof-a%Mub{B~0BwP{xwS!D~M%SLBD{5qrZ|yo3>$5N@FTRhIMZ`N~ddJAR zNS>}tCvz`|FGm! zB{2Fb@)&JvGSK+BSe&%`_;ahJcrTQqNhb%@d%@XqhK*J~gNp_pfc-~y7-2=EFsWso zj>{_Y_o7Ln^VpxUJouk6Udqksh;s>Uk(fdp9!Em?lj%X z_E)dhUeFSUR+@dEvJA`Kt0NVcZ+wk@ueX|o=7PbGgEjbcr`HAI!iC}{t)7kg({#%E zoYQK-oSWWEx_)Vlw8v>Lbk{S!?zI zVn6z=k=gq+Hb}r>7YuRh@6ZB~QCuYBzN*}A$d*avs;#e1=gMJ@RyQ;eWw6C{EgN$HG z8Lj}Umi+yjb=Qj5AGU2RKK^`pM4WBG!S>*Qb14biE5=OQp8NEkfkon1Fk`7aymKem zcX0V>dzQJFg@R3&2%dsFGTZy-Td}ehc{|C;wdaS}&~udsWFejtxJYsv*Rj$@+d^o+ zS}@onRPHird-VFI+(fbkM_Nf~SZ)}!+Xl8j9xdu%o}YW@LJ6nRGRra%a>cvspKoB> z$f}L#mx@TM=#tltFZ*Mfgz3f4GQW_}FPSDAm#Rv(#W(P8V5`$O`#wmbR)ShfSWhWl zVi>WN&$%7^NQD}HNol;;I-O>*!lzMWzket(=Sud}jt94L1WOU_#iKg7#taO(rE7|S z*~CE(ZKDbyt&II+)7ALkgRC5L6f>!${stOmb^Tij93d(JGmMnYx2LRiN^odfQo;%E z+2`t5m+0qP$kquSb4xvc`o3l@i*Ql~^hCaBnP#qRS`gi>xKxSVv!e*9z9pLbM5VhK z@*+O3asR|N_C&}9@cxnsb-c}`=nrL>d-HtXsZtGw#%|Nom*-8t`%>J?B>C^sn)d~$%5VQ&WczLZqK&#>xO-<9AWp{;&k6}47lR*GXztR7 zh>B;0%m2H?mI?Ebm0~rzr9Crh5Ge>Z1w|?N879XOR-gb4)xX(<(v;IR0|1EOSW1ejx?ZAuK3i+G0%KYL`SD9iQKEDB?#FU{g z=uZRv^h#7(tRk+Q4lXU%tZgk`CCoCr$mmPB(y1u|m)i|s|EoB(AOE`ju)7)i(B;>L z5d2pOOo!MQ0z^aIIwz&{8`K6opdGKSt^&!`FxNMn)Y%pwVA%ixdl?+d-etHQbjg5> zf?Sku>sM;0E&+!9kL^7@@LA>MZGkTkfS@dc`q2PH{dLVfSoObD1*tz&1@eDa6`mEi zk?sSKTw6nf*6-{Zh_BQPoSadRMVj{{ejGlWXpwSfT63utcic;GCj_W{;Q&U-5g3MP zZTp~tgjIJDmgY_{%l0W_)OInD&5zr=IA@5|t!6U(eRX8t z7$3sHjqyL0l6cZ{@6Rh;sNjYX2;QXe4+$LVNNst zxAQWb`Om+t&=>-aAI>HNnmTyqeH12LIN&{4Phomd{7!-+!u8yU(I0fyGsMt#Fj#M2 z2BxckfBi7-|N3E1^Pf?#l$sVPoM}~BO&x5N4GqfqVj`LT`mw@Vb^4`*ZZCMNP+Pzg zCTTzE?C6-x$fc4>q!8Sfu~G@xP9Y{E{>mUD=R`K#JNMNe#@Oo%6ffKf9c65GlVVX`49N zueJh_K(N%%et*6>Bb(R)Dg_KIW{fJH-sjp&&zcz3{$380{&e5ioG}3ZF3{A8dZP6* zuhzlQT(JnGc&60vOMF^}5}3ajQ~So1lh z_ocSM(Y&22x4Km~-%VMmm%i6KFVDD!JpJqQjk9X~gRFLF7EsgB;00)G_BmiX@CX#2 zqk*k+6Sz)?gB}5Ak7}_2qgav}@Q^5|ZQaZ>Iq70Uk<8Q+iG*Ef+Q3E5lwIilW7eGt z`D21^QfHf)lk*bLIk@Eb3jhiWw_QBt6m~_*+;#j7U1LTOoHH+RPS##EgVAqAP?3j7mWM36 zQDMF<=>!6VM^)rVxz}2qpnt8M*YOI*yN_tC- zCtwEG%z;sb{X(|i&WT>94mLEvOY+I@g+c3oEJ(tpxo@a)jS+$+ z=aBNuCk9}_dL;?(_AVIU#Pn+q(|C|wW%TW_!Xv@%_3N{Y``YoxW9kIgK)LEOj481$ z$`$EDIuC>Ez-STo32ZZhT^di$pAJ%trZIcknBMTm{}`i9*o}37liEu1ZzpvKHxM{Q zlt084|3-IAF#qdMwCtWzRD(5%vWQu5-0d+8^Md_HFG9Qo+u%t5v1^AzlIv;#Dhcx; zXR}Q;s5t$8i1Ilk|M^D=1}mNW&-UJVUoElx^X_Q=ul93rW&dU8{{1WbUj?WBzr-v| znDuX}nndiz2HwBRw<0IOC@eu;iZM(PIjRd!ahl^ei?IWkuFF#J2 zu)kiQ(`D!gHtL*2xO$z|L1#Trby2Y6ilLbx~J-h<^ zI|JnAkxLC)JW@}A_ekEw3jb4B%8-uN2IFp{kY!jvnLiWa3&1@~4Q~7l<*CK}D@wp* zR6yJ3lvp7nZ`;(cy9EZb zi|Ya2(pM#)PZb2Efz&ZpWi`d3X6)0_*jQ#;#Bx5!o+rpp$3J%TZK8~m3hxafmm zm`H@=Tf#4#@v!Cr2HVthRnYga0gpn>#WtZi0-<2waPCaQj8U!D1&AkTxcS=?4XvNox#vhSYO=u&~e%t2!f_v(Y6Sq&>1#0sK@J<8|pGI zqe8rNH=67|It`XxHXd7l7oKMM@b-}5OI4KQ)ku`hPK}37CLS&iCv9%XPD%zmhVkQ8 z_~GQ2_Sz)ltGWyqL#l^>~C%No*5P|)#)q2XT#Ci9_ zb3IKRUEP6skdy^O&a?Z6hRhUovUzGcfn?11QWTT5%4>lpL>}z=N)41TMEq9KOfSNn zXEeF()hm@wMhK@NYwmv2yZwwjDDLXB?Ew0)8 znP&Lyi~+3EyDv)IU`ARpIK^j)-dy_h4@NU~a3D-cR7ON*DYmW7a7(X}K_-CA6E(FM zDbK@!atrLC57+2Yk>t?!oKWMks=FUU=)N*#12^y9H6C+mQxCHIVm0i!eeul zlXz>ht@47w%FhEOI6%&TI2-}mUimY5m(v^l<#cCg{)c3>r4$$ATZt#^9wG39Nw*d) zG7xt!~v6gF-&ZPP(*%zXNUHzD^iNfy2mvR7s*uC!{C(;hpM5}4vt_4a}eOn-we|2`d zv0K~u{o1W~*YWXOOU12kVGv^e4MLBHCyFAKFI?kK>&}{Je`j(u3JSUhvnx46#hwPo z`k7B2^fn8~t3I)!<*u}BN)#T3s_8mnPpr1@)`Ze4TKRX6I^W{si5D-VFc6k|G=dL* z_81fIJs^aM9{@lM7IIMuOVwCtO%M?gNt&N;!J8}c=%{8%b&(Cv_p2cw@&;+`fn|z` zTof&`lxdM=yuL~*Dnnx`LsG7p)yka(UYy%_h^f5#s(RFhVZ2?OgnuDra4GnuS4k=W zD`C701V!LXQw>^O;!8k_IfJGoztQs9wy7t5v=PjB57HiblX%VO(ELA^!J>lj8Rxo= zc+EsBK(_Iez%sJm3IzYIQUKv(gS!A0@P=?K?u&4hKjEqyLiGzj9fd-pFep9t;B%z( zr_Jcr->#*ncPY2*0$A2^uPYlQ zjlc&RJqBBtM1L<#LQ5%md3iq1P%6GwRzAx#qIutQ|AJFzKII-*e%RF{8$)~xIjr_7 zB4f21!!R!x!XIKBe&`ZjoZO2PQnG(mZ7$|AtT)vXyrtaY0Y*(gyEVd+F+X`1OfN7a z2bFT^N$ceEjDbsB41(THK9%Jo*;8@UD9??{P^{VFK-zPG7EMyimbQrJ`Ly0OP_64_gk zkZ%`jH|2=0uEmOUw1KCfL~^*_r==|=HU@-Ov>*ZQZU1NfdAYRp=dWK)?)eK<2r1x5 z->=A{`oe4|CTKgVG^{B^@s_cNypNHk|LZi;s2rSL3kcix~n%8S4 zvLFMeMzV(;T2V0Llphr_qRnghY`kr(7@Zr;QX*~ro*jKA(#uYONA`Aq-1Bj|uwQCj z#QTGV_;u^s;-5{g2s8>n6>@QBu%Mwfk!+&Ay4nIC`mx#8Oj^1FSPT+|*`gLxGM^trql32+* zofw#Nx!~7iA$BTcd?CDv-EcS&thkzjnj~P@tS0zfkd|za!t;(ht60s`R%T+&22V2o zBPDS=nmD_Rrlrl6#@Ys69OI!H!Pn*-08Vq{&Bs<%x-d#sDKu7eti8OX{Gp2a)@!kj za;`NxkR#1^7rEi3ULzweD3>t2|3Czr3JD$%La@tT;O zDPeP$_vFNEOl=Z2#&@z;CP6%yn)_h|U8m_g+l>c{aHHSiLq5*Aog_nSET9{~JegL0 z$XqOaqSE=9_dzlXuD=stP{}OJ2_yN+g)U;|@(3+?=D)98g5~<|QoKa?^82!?%qQ*w zx^K`4*(O60{inYu^{Aoqp`YUE6%Rh$09iq7FSydj3^V!_llgGDSLg5mRRgj_hE+kV z$ai4a3sH|FV zCB;tE@fBrC#hR8k)29(aSNBb|o47SKVr^x(?lxU=_p0L14fTX(AY`ahLGa#y z?yQ)e2HaDM_^8u#lSjOW{9J4%As4HpH`2S46kw$8vu&s{8zk|lFO|Vur(R$0h`!gE z-rAtzhs?(3gECZ_SblAsygNvwe6^`P2tKRyT0J4T7P6LOXAI`8B~jI^-6*(sv8ydR z!)4QWg@?_*Hk}0GBve-NoA`YtbH?a1z@~_N_hz%uzN!Sb-+NWjI7-n<Dv7k2LZgby2v3 zzwn-pXw%JcQ4yJO3y>J4`5KC8QWEq$>&ymbXxamsyjtNyMBm$mQb7au`EV~MP3Sql ze3s_S>hQP@4~>1joC&MUh<{gRgY{$Z_hYaf9yFz-q|ozYsVB|&B9|et2?}M39#3H< z?^m{jTayK2D(9f$Q79u{Ufk2yhoOOKe3iON3Z&}x&~~71`Fm||Sbjn|X8#(O^ZWLh-em9d}me#PfkEt4uBnHT-i!;M3)yt9-jCo>gbit zfeCk+GtU@Ce{CSETn5IY;j+U=w3RU80}Zc^v+*bC_k zno{h(8dw`k*2yPO~cgkpH_wKxcH$*&$ zz?k9=u=U4ppKgSVeREnux^JNR097bUQd4s(D@s|`s29YXh1C%CpzQH;1vK=v|!8wRRk}(;-5u9wu>fuAMitfOL{|26&>Z?0^XiKs1T+xNlumE z3WIwR1f%D^`aSmZ!0)QOqFv_ga*)RH-VKkYfnLE)RwMxh$!bQoP zp6a`Moj?i{{EOW8^p5IZQ#}VZ zm0^pL)r(yb@+&LZI9L3f&q)L+e81jrY-p31)%p*R&S?X3VwBN{+az>1&!zPb@j!_Q=J&=8q5vW1cS)!fyHCw2mJB2 zG~a{KNSYZn($SCK9KmfZqCz&@Qm#|-?M^_k^x*Z?E5yJjNc2Tag}lmW?%yG@aj(7S zn=5HSG+1*~Fn!g?F+o@Buj32^1GMo!56?Ey|}iY?lg> z`9HMY5zY$Ph}uiT>gq?`S4t$p^vf97xE$i&tB{E%j(NwRTh?nO1*=5FNeK|%sb{LM zu7FW!fSf)Q>HLdra|$wnuYs|Ffx%#^zAI~=^F{-@_h@<6Q?b&b zr}2?RS620TsC6`2P@w{v2iznB&P`|GIJM3{p8*LpK-1O;m>-3Oh3Q~xKbhgYgZsvI zRf5MF;o6V~h~_ffGUG7N^cs4YMnE9p3I+_7x}GLiXRED` z4%xBkM6ZA1WtU9!Jt%Yhoc7dG4UD&=Uo=8+J}vXN9VzXx(&gQUaB;8BR8<1he8v$>IajT5dR8L$QiZq|~UGYY5`>mHUj|Y_w9l^|#1P^GOcG zCup6-G_Z&PzZi2xe!+ms3&1$p5&nokC+rvC)c~g*n0x0Uk`6sQ-2%mA|GoH0gXeCS zci7LnI|t0qA-navY2lK&ZJ@7NL_*YGPPNx{?*cus3A|6Kuc7mIM+)n6r>QrOz|_g zm+9a$qncT5B?ER*7Z7u*Dmfxi=5X>%dBwbL_EwN6;y^9nB;a|L9$c&pgp%TQKpdGF>i+YDRs2n7%14SA; z3(XG3=ywQ4LNK4a{GGJQ?M8=Xqm_Ir`QlSQpy^OXxsh{!Q5c37G;P zhwudgGkW@mgb30iuUCwDxcy-ie0?C+&LyLW3mzhzwL$b@gcTO_x2=*QmL?d&@TtCJ z$P*)_X_(wz?+7dwa7SB$JV1~NS8*?w+Au0KeuXYJ2Vz1*wRK-HW(?=fpE39dNKUs# z@%(zwMR(2_&IX>_WDN-Q*S8`ypJ`YbZg0g-yZ=&>ok~V5m5FHA8{XL^ARz>+9}G6$ zfaFvu$%QJB5xfGd1u#Rm;S$QtXYE~=`K|AgHbNe3fwQ--H*(V%Qn)3ud4wKy*N42e zoM;`!JW#Sj_C-=Z=UZN*y!c?b*0*yPjO-F_*C{%|yl%fhT3V2~(i*)fx3CaQju|Me zsHVM!WG7(Gl<`Je&pKfC0W-X@cX$&5@T2h_zydcv8cF_O!Z*c6MoI1dYWhke#IDe& z$M>nXZ`|2K-PqG(1Ik=dxO#x8khl-Mx)fWV<;3g~ zA(R?N<8!b@20@TLX=ook4|%T$4C_3?!p*gySH1 z37;b&7+Q5$Ne=Z+cE|i0honxdJCmBzI2IsElu%8h2;0J#L+BEG>E09?jZwU_hn>r0zo7PqwFH zkhgh3u!06xs0BPuL)et_OZzV1zbW>_2L*ARm|$;|qX8FediCjZnkaLTEdt4ng=tT0 zP*F80Q{`JqQ>jnn%_#!5GD`j)_*UYO?0N$bERoD(T53Uqc8d4SZE83GH+(ru|Jz3{ z?dXwV!)m)|?0{8v#Qj}QW)kqUr16k6^r=RRToNDropBE>$*rg>7jTU2_ z;8=Sj=++!T7diX8i{x2K=|+XQkZn+plmzn)@IcD4U0R<`J9j})8>sA`^k`K(c7TkI zeoF?hm-=9#%V}$q8aY&jWnmbAxQ2)SMVf$P+7;9;JDxza^K217SWTukohbPi6JDKf z1|XaZk0R5|d=~$L!J#~i7VCfg6KMONH?V^Wt;qAUDOOtX4JtHRT+UJCB-!1 zPdPcA^;)CD!+VQioK$m{AMs%UT;THm2#l6tA9TQOj5RPtqYtR%OrH@br)$5dx?x)v z$@-ZB2ShfS(Qn_{j|J=f&UoP|KLfeBhy6c*8`E!#)zlt`UF%xrm~?~_%{UmT{&}{ z3wL$L#}%QarukA4@Sr(1V2THo(-SgMyc7$!pPN%eI3cyZNWkp!BXZmOa>)9UEVD5L zX;{E88^EzsF|AwMBHntL>T$Dg1ZY(HJ+xYl{W)gVDU$|~WAe_59|!GfsKRt?`S1TF zQls1*gN-RhT+93FMNE5a2{EgGo-n>`IdK zkm2IZr`y)IKZJ&M0Li7fX_fB+QuOoYM6vc|#cFHT4(vl}2fulq(+wtKHbO|3Au-Kd z&Epf*RwS|+4^IHD@GlFp{XVcD|I5c5U6Iq>cxLj^w)JIVg$B^1#W`T((a^ugqkyZ> z(vs(F3*&4oHtmZqrwHO~y267HvL&YTn+mEY=^(P)ge0j*?ic7+%VnxJh8Sz?%4S<% zku@$2hK9Vu5du!$r`Rfzrr0x%yoyx9mYs4PV|bu#^-!7ShUk0C04ITCe1h?oM0i$o zAYY*>hFYf&K*Hl^-#CLQEIDms$caD76x+DU16TVo2AjUdvtJ*N>9==vu;IP@zf?t^ z`g;BN_wNm12^|X*K>(s+)?k3W5+IRh@<<6znL?CjrwdQHhC0Kf?SQ)z`}V!Hm>pb` zal4f5RB46HjbBTns8zmgD?_&ccPYQ?-A+G{FLd_HxjSf3TWpBv714$;u>~{WlF@=d zQX$h{%}Q?{Z8QicfMpZ_ z>l;|tcQ?iuKX?VQiooDLm{srTQ~FEwv!B;`SqKuzW=s&3&>3Kv<-Pnw01Em*!wUu% zK`oV5<`?85crE4hYicq$ape2H*B)orO&v0p*PU&5y#43FTKM%~={C11KLpY};qRHN zaDY{Kjm=l{OZ#t=L4ZmR#>+x?OTGj@hbMi)@ z1_Hmw+;ngP%upjyU=N|CS9q&1dFn(o$I0I>ivS}wXdJX)wQKD!TegZEKYQlQ`&`V~x567OZ`NE_8f6|5S3M zZjc_yf{ZcpmwlE?)eyU^sE%dDlq3?~YCd_TpP?43q$S3QUJcE-e0ovGt=^BUrDIY< zo0VCeESW4m(XaFDrGoLEPOQZJ2&@_8x$i3~Ho^W(+Y|t!^374{wm!1UMKN3@u3{dk(-E| z>b=_#cy%?$Yep<}Pv=<9SS4zaRkwS_V;Cjj^-`&+V7$b<2oYhQ<(BV(dOF_GoYNP-!^aH(m#nXtSV30#ZsZdlUBfnG zQh#6_UG&|PqTT2=J02Of+gbsqqKA3wyXbgj!37)CkG~(erYiDbh@29FdJxHi=XY>5$YTL}IFl#e=dHn{c<&!-- z$Qf?QGQU+mjz0?-#fPj$NYP3?e7;6rJT6bjUhl^w^cS#0Arit zcO|6~u^{lHNTNAO>CAMP;tAOs2_vg{;1W;Mz55ysp%1vEN`d95zrj&`a*W9X#RLJ0 zgbj(s07DMGyVKB=s5CaCrb*DFgGwq+>QC(kM0638&}>c=C1l3{Wll6zgxutT0nvLa zeU0`$P*l)~0ER1#;Q2E?D(WsQC+8|cYXRlZ%7^YIvOaUra-+BSJ&5$MiTU(!Dk^tg z>%sz%qsMm3paS-bW&0q(qr}$FV|*Gs+WSk{)h>(dQ@H6=P^rV7H^I=`@9DaEUSlBj z+F2MBB*KHcGTE-jw0Wh}R1c*PCVbnRXDl#uQS@jlZCgp+U|q57!J2&F0jm!b-xVKb zuC;sxQav`Zi}lU+$fF-tR4AKBL8XSLJQqGw<3*}~c;9xA-5_#ajH{?Ey+MSM9V90X z2+POqe@rmdpi(;$(qiobE$7RrZV}d5pD!PoHdu6^L2~DFQOI~Z7Z!7rl<$pP7T97w z+E`;{tmKAen|evH)nE>UEL<>rJ-&*#Z<@js9wRQCDCt5P`0jgv4cHLz;(%lV$%P5q z*os>zT&c`yK}S!g3%%<3IY3xjC*|}(6rQsE=Z`os7M}PO`ijzYUL?!UAM}qVs{Q5h zTtB56OPfHQs3=fjKYJB#aP*d6=y^0ZC{2S>{eUfk9K<2f@ggQ7jt-(t5CMf$mU)QS zi+SOP)II1^3s3In?*f+E)TgR&gMu;XPxCAfVWAi_`+Wv+UiPqm;n({R&H&7=GVs44 zc#!aRhWGFj=bv-jU8wVuBmZy|ZH3jav6a)AF-ZT<*O1emI6uxM(;=&<1^U92hKRdT$cxm8mmJ zcmN?Q!C&+FCzz{n;W4Qf?hE?fq<@)u+aXm=|+jCSwCRypf2^65?z35j9; zPE=SLIum`a+5)RnPu71AHx=*V*BagqCH8j%&{DK=g%W0(XlaPt0==cDegYSy-W}KY zMA+yG>snXoNNUnyFYPv1zX$}s4pu1$zG-ExRH0*H4R(GQ#+3M;oX*O(n+l3_?Y!!c z5wlD1`b`_ZQz0`dr^@ABiHS!bJSUdsj@jeMRu$vUW2KBIoka`5r*8WMBAR#oSj_KH z8%gdvEfcIQiCA3TK{CAq+5Z@@mv21{d?K;x&ddw=x?rbw?h%5$paTEU&?d~P75=MA zd)U)dOYotBC-PM9RV2M`KJL~yHEt=7*q_;*N41X%0_T}_Sg4)4)ij}kimoo8{FFo| z#VfDbcXG)<07??UR=^k89MIh>sel&_NlyEK>h&Gi|LL7T!>^DgIltMF$gCAr&L zQIZwtJ-n9NZha9=t2#Q8$GZpiopqjrb*mn-mW7Q)_T*&6&UH5dz!8E@6`pUFXdWSS zCfv-Q0Mnd)t|)<3x>0daQB5%p5$9nGCzb^(NBpehe6ldin1(#TECvbRt_BClY)H>V zU7GX?z-RJ30nt<7wGP_7Vq|FR7@UsVs+61Nq}@rGuMYv_rotLH4vdHYkGuS?|BQjh z?|;g?{C`4ylT-+D`d-TWy=Ml;qKHy&-k+rT3?@)+9rxE2|0*K-(HPXZ=a zF#7%vQ`A4i{r_g{`b3b~2iH|_x*6bZW9>Lz=k0G5^gSCqKjMX;?48M`Q)ym*3aF~b z5xv@zv#^EQ)2CKd1GNp;F*TGmsD2LhK_K*8#Mo>b@Aj+R6Vyy^#)|_{Yt8 zxq}3&QreCY$ZhBu7_=nVg%??1Pcu8={5?HYDHVX%l_e50WPtDC@$nXVu#_$( z5JjAJ=h6}qoc9bu02%jHPXSk`-6P}4#Xh~mTrEVcAbv)2Q0g7kJ6BvspA>GaZ3sT-4#dN z(|?9#i=%7$T#X>;AVgk`*OHJ18)}~m5&?BpHz? zPlhEiSVCiQ|1}Ew*SIY_9y!L7&=?%A_RpX#>hcogQ;}BBaEN?0Gx|;O+*a{ek=vX9vf1WpLAi8n+jmdB{-e8+>R4?hs({oZ3C{=VD>A zB%(DXoCjv43G7Q{onF1fP$^iX;QP|g#E_UgwE@7k1F6MS?CRs^V>=eFpKpn_J;Iwm zB-|y`6tv9*cQ1|*O*>lbZTNRr{Jh43Sdx&!6cXJu3bOUu|F~orQ!nKHFQ#4s+J14o zN&mnK@Qlb5R{&v+a4#>op7-*!`gt8(Uu@_P8!rN^`)i^g^LJya+|OVP#o`WjxR1mm zn!L5Fb7F#m?g}HdrL=g;+^;k2dKAGLaA+}Zh!&OaCh3*SfL%%QLzz?<9l zY@{@QVl-R{+>_s*gGp=#%j;~MT^h(nH+UJg2t$itNYY0GP z*fRX;O9g?o!%ah;igi|gK1fF`h5t=Ty`0`>^Ss6r(el7| z6wdZkOTR(Cl6T1lWo+ky`$M(W4in%Ks9OG+QoYYt;XHGFBC;Mfr5!5cv4AaV4%^$P zK1je4{}y`NPBUf_l>0quZv;3O;3MQZ!`)bN6}JUFh)G}4K$ezTWh}Ky9<>Ug=HuLq zcA3BRCQ9q?BOS!Eka_+bjFe3ff7gr{MQ=X?(S6U=wy8dpio*9v_!*@#c%xjn_H1Bp zPZEZmCd1*5KA6*gqqh3LiQg|=K3RpLg457E<}&C&6d%liy_N6`Xvd_18!7V(gJQg2 zBgsPye#9O!(E`VyA%);}}>v z)+#_p6ZrmFBbUkn90LJjO@SnTzP9#pXYwtY(dA6d>r&fp%m9uT{~S6N7S<##AG}TB z;Kta_?f}j`O}!IDOX3A&zmV5sUCfoh)be^&qaV#5zUKTyK}%RfMZnB@p`_8Mjs-?OtM&lBSx}1$um3)}o%HK@0s;bVhQ&`^ zUyFe^sv};u^ewu+OgayI%X|wC8i_L1VsL9nY7B@oYj$^!A)hdcqDTE1TsCd}XlSD| zf>f1A!zCNq_|FjX*P`4}+p5 z{WGdcVwmA6GF;3LqD&vifDXTbbHVx9lf)o4B$cG^AK?w=-;F#d+7FiE-umhnF3dRk zAld%!Ajc0xl5w&4KUr`fT>%kJ7vuDJSk0*&!CTdY>s-SoipD=$yM#%OH3@Q zj*B+c;s$EI0qX<)k#WRwF5zyrJk6Ud_tY31X=_R6LoxO&`Hzw7mPNZ%+Z%AxV_y*$ zQzIiF8!$-*0hROoJaaqzlwdbcMo3L-=a`+ph{)YSot7z(uJwA&K?J#ygndpSstqoP zO7g1oVa%Kf&+vGMW&1P&6Wrpa_hD=R0ll-24Hwz3b8Km*fo00q_Ejo~v6aFE!n_`< z{w7n+O5NoAQppJdY#+_fRRP2_Pr^dY71!k0I1RI)2#z_{;82BrD3g|AV@>45}+# z*R^p8F2UX112ecoumDLQ1ovRU-Q6uAK=2>|BDlM|LxQ^#+@15zboX9s?Ooq@&d+nI z`d3$1nlQ#3^BvD~-`DkG!S}(tT*P8bngL$r0K&-gUAuZnMb`e!Stp^s9jKj9@m?KOJHh(tz7tvJxiV=H3Fb3Xc7(W&DR0{pfd5B3+c z2Wd73aZ`qVa_V3aaImBMv74wjpELu2zD3ZOI+E6z!nGr(1ZFnHy|2b6torT-OJEnn6S*Lm`*ALXW#@8ujp z9Jv&Na~e(tru0iJ@cw8QKcahzX0dMcA|qnSl9gf{bD6Iv;%rj&P`~3j6;g z?OYav;sJL+5Og0wo2X0(yr0g&vc%M0NDL&X`B5D-KPfj`Hf$jj1(EH`D86d5H={L; z$6sPYy*;0yQc6~tgPxq^oKmx;jVU%33$%%#+fUT;gp=dlOb0+Y&T6Tt*)P6gMLQ%m z5SxH?aG}A;{kJ-Tl~WVws~i&Vdecb=btDoJdgg%`QKs<4;`@r~oZ0M=S1iN8}J6`*P z(-sK78z#9_J@cOBa4Je`eB8ALs@_qs>q3?|FG>u|T|;kIC&wo4L2Bbwk`6nmjl~;% zCSK*Nl@+TlM_pZVZJ6FmN}d^Nv4_VUAQ2z$lE6(hIs?;Ub{ZO~xTWC;A_>?fg+q{$Py>1}u14gloWCilFU^Msz~vnJP0sXl-aJs(8f+5Q zSlj~WM4qyt{sU@gu{BZY4^Gkf!>A>>vO}t_1+Z*2O1isj;YHIR%g}d=7+vdfjE)qY z@y(;L|4BQ3Wbgz_%R|bJ4G*TNAsm^2QXB?rzst0L_u?uZYaB4e8Q%wowy;>Sv$Soi z@+a$sar*)|SZ!H#0ynWko zNX9U<{;LSwX*Yg31CuL6QQ3zP;^Sn`CUxQ;cQ|CQ^?}?UZliw`&h}YbIntJmwX|?1>}}@f`@Ez}rcPRfR=^aS+CV5@jTP}(Cqg@k z_#Sk(q&sy^EVV8f<=T{nLAe9bXk)(Y0Fj5FWQ)=cQGSWuX$?qHka1f(Ibwhe^xp?1 z%!SzWyi@1`Pgka2(2B?YwK#1A0UEy3Y?M3bS>bFGwSW#p$-@#NpLJT#mQL*R(6(Kt zDE0fJCVMa>!wrRJ4bT^4Flcs9jeL-|o!i2ctU@23J%7(#Fh#Dk;e^4JpICy3Wv@er zSz-~7#B5>1(6wfa#n`g)Sr2_|O%{*1{Ua4_n~jFo(aIu^{1)Wo$CnHEw2&YDSXl=E zee$x6d8!l4h|$tXSk3DfU#k30JzODI3yFI#2R7{ExoHzH_QD=yNEVzT8wXJYH#3Xn z<^okfFy5<+gs#~^s!X#ImOYtsKOD&iT`-Af;!s){7VV1aRM~T#@)DG zM{Ry;W_~VRN+)vw%kU>0+Ol}*BMO_ma7d^W9VlbZ=PQglKk63xezeE00+}?>t$_Uy z@_|iqsl2y=R8ku)o)5W5^QXI>NwIeGZgvSQ z7{pP9$?qDC=)6$TXb@NW&HYWPdfrTj8X+DVHJKUrSLUnR0E!vawutvHa)r@*fhPWa zV-qU!Td|iVRO{>OxTMI?yKplCfz7RdV=&3=_9N+H9O!#247#^t;5r^4jW~e1$A&lM zuutw$6T1J{$eW6EQDbD$v;p!=5Nr9X?J=bRfECpKP9>eY{863trgERvr9^?r4_Q94 zv((u=?^1TgvXRj8fjXk;VEb~N+_A*##WXkoMCxEP-pvME zeW#LCJou@3!l*=qwOTS`i^QPa;@Q3)2?Tu86J8h@%9i(&XEx$3FFd1N(ol}fcqFmm zr4A6Y)Dw$fR*{#tSwneP=C+i>-~IMMT1I{o>RGP_-< z_Mkm@7-hzcZ*@?t;5;xkuJuq&Je9HXsDMj7v)iJ;@39#JMZ94n_P* z_6Q@qbtk~`RGo?oA*k6SMx|+h-ak9!ZNa($9ZK)AdA8F9fZ~#ee!J-WsR@5Ld$jiSuUSDHnqLHQr%&mXju1x#R!ZpV>WOMWCR#M?q%u zLm0z3dPWd%)y>(%fZRlYW@EhA^LsgUNeu^sD|Fdcu%i0gqAN9OnDH~0>ID*>xsTw&rnorPIIj^d=+oo>>L5NEpyRWa0!rQ5?w^fVqm%jxam-NUI^7kjz zfklXm^6g;0(GZF0xDfQ`n!j`0Z~?1y^cgHsQT>39*O!}%p2O3o9X{x-J*`?%4*{Ss zK_U~Uwkcf}w2UcJFlLavhc9gyJR#_Z#e-F1J>=Z?~g zvFkrdzJWRVbHUpm&=f)2A|2<}C8H>yaE;^dfdJWw^4jJm?GbPxAhbTSl3)q0{UqD) zVXWVCt7=w|wP+^vKX!?lADtJqh4ZUs{#7p4sYItLA?6#WcOe&yK02T+|vg|6n}#)t!v zptXETLb>^aq)|>OaO#mgp*dkNm>x<#+QNG-oB=B3zRmf$qNhvFN`8m#+^5P;6?%?X zu636t7I*<$;~?r!>Wc^3m*7$GBpcif{Q|9qQq9S(bpPB3o_4ZyePKI*K~!Z!nvTEq z?f`|csneLfsJkH?N6VH5VaNhNFh^dl1`c0bt+YI7#(!U~j@+{dr|x!{oB#po@n_4r zvi_ESuvbFyY=)Ooyb8TGmlJ{VZgkM9df=QpJAdd zS`pRE#S|5%@pO6-!cq{mOH)zCNNX$&d);vOfxm0CZqDcO! zU(Fs83+pTfJ+VrN_M#v2DB_ic+YY;@Lu2f76(m#i+X%I#)8|78nDGsx-}S81)Nfqj zK20y690GMSg}3HDYIcJPX?ZeFni6xBH}fcR{Q)2h88%9wjxlZW{_0JWR%UF=f?5Be zt@-S&@dp})k6cxtScpa}n=j@zKZqR>+1x^Xo)Q)$ie@Y#%UxSrBys6RyPS!MXthD` zIaMW5?+5T59zZ3;^=_^^wB)3OguL=3F}mU=KnCLw+uZrsTRM;3-Dwd48Ag^c%BzlX zml+)#sY9WAV=8XkhN}%qda_Spv^#s%i?Sq(@#tQW-Od7o>SajRYy+Y4Zp@#_iTEyM z&#^E?U)4OWPKv))3FY*%lV5uC_G)}-Il=9<>D)S+4SU$#MEf}ymrCc}5jg-j#~-q- z@K{TJxlO;!kurc&M7zIJaHHb_tlS)dEhq45w^!ju$e%FJ&>`?1A$^GI%?Otl9ue=< z;|e0iWv#KVHrZ1_AUBW}W6}0-+xP)^`b`CArY0LuL9 zg-Vv~axOgQ94n_Z$yZ>WzNxy!lB}TePZtqmu>;tMSXo$r*9uAf{&usRj-TN_7t5Qq zaBh+4N6O+ggGn;|2etX-*)%;v`NwM9_P7tl(gzultGUZROd4~bO*LHI|BOpe(m-PO;x$N|EhRNmT?;A%E&FMFwEcs>JfmI9S z1-chI!AzG5-J+TA>FBl&>S9tx2DnRF2+q98{qlwEmS8U0Vmtuf0SDeHg3_u)0=BJr z{0NMt3K9+?uaP+TpaM#cgc?wJ@oD|MC%Fk4*`Y+;@1@ss}-~LKWmq`i! zQ1o>^CeSY=Z#9D}9>`8jXo1W5&?>S^oCFYYg)j$e0Q!QTXXv*VkYQ{L+BO`?N=XH7 zKd~M!v|q@n`~2+TDE6DdOHamCDqo6^K;uowARWR++JP?=(L0z!!2ZT*dyIarh5i>- z(s6rK}V5@uHt-#odu&?58d1QDn*K&2YKcj0Z37^23UD`9XJ ziWH*)t%&406(io8dEpS%WWf9cCDZ{$vZ*ieW?vq+(3F+$ z*jhXlJ$?LacCAX02&kH84-1jw?Ub_2DnJOu5-FpzfV{Z@`?O4aWE&gxET=IS0mEvh z&;M8PB*rr7vj7$I7CcH+>)`Gr>@z>#Gh|B%e4UE&G&Q zY%+jvW7^8?=B{OIsBvqO`auEniy63vV*q2n@c{shO-b_CY0l8 z1F^5Qig2*tgYE9-M!#~>{$C6JU|os*_u?>0tE}&7a7Id?*aREsJk{x3_h*&2jV|c1 z1x%dTaCET@xVav7h1gOjadNpi&>0M7$Fg|X4K4^>uOj9*O2%tgKGV8< zOZnnr0vv(w+XBo;5lCXCwMMwkBkneZ$1h#JwYoM)m<@S-{rXh_H*d*P1XMl5Z8e+i zWmN%7F{{1J$sQ__#i_C*a`VO;H0SWdOVtzxN9DMxHHm1{Wg?i8O>Q@Ui{M_yE}Ps4 zbKBa9yDvn=dmo&?C2C>yanZZhJ-utocuheOH71Y8XD(QE z@8mO+21jaJxLgr(E+1sM-Esel9D18x8{PTYaF=Fuop6U zPe=%rFqZ7hM%N6Nrg-w-ubP{J#EKuUdq@#V8G&i=T8OO?K>q>w*h0N4rz^b_%Lm$} zDpEe%0N+)FLVPchXMaKwqWZNJGA{XB5U-kBIfqEGe`O+Emow_>Ky*LFtHj^BQU830 zjniDC=0;P5huqb7VdRLFIfP;u7lpy5xA4uCLdRz4U>>=U8^_!10_Lw>w|nR28@MQb z7GA(rgXqCuvFB_$sr01UMi)LBmJv4%Jm|nq4G8 z)=%5JFl70ME;ckYNQGkd?prNCaK&Xk81C8!=pXEasCnTrofP}tSFx?S>7b#H{CbxkiF4($!2d-{&>N3Xu(Jw8MdFCA42ZZyI`n*K z`obdA9rzQ9?a+z3Ka3}L85+tOcPPes(qk)uC%Pl6-;^mh^@Hty9M)oG&{C5>-0URW z()tqX`5m&eM0E1w=bM)$ZjF#6q(j*z^#<48eQ#VMe(Dp3tw?YXJZv==`@(r}hf9Ei z#cjmsH;CTecR|tyU~p2r7`jm)Fb#rFhodmzxUReEJ-UR^3l?8WX3Wu!B*5wjK(-K z#%a0E+oZ0yZ#Hn5zS`N@2`{Fyd288;0M~%bL+r}X68XvdMI{*$-XbO<((`(Xx3DjS zZq>a%dNaGpc>ey4_Nc3t=*-8ex|?S2AVc=%p?TVFrNHkuKtkUgy78_0V_=JqxWV)sf5{a2mhtb*Ool zePK$;Q4Qh{9q!y(J<*)C047OKg$TP@mhkVVwFLR`mX@RBs*7zQ$!{l1(de{ zrh)Yv9p<(V$d)_Y1uS#IX6cB@s%i=}g@-J1R8893ab)m@>4X{u-~CX?dSPzB?VoAqsp|3kJ!HJvaLV+@e?#w%5Y ze-=IjcA2}uxM0viStgl}8>8o;5aY1wjc34hMlA1n)ecf9`}Ka+?_WjL6fJ1zfKYaX zR$KX+Q1wd@p2puZ&MtBY*9_-*iTQhMoPhe)YyWRo0R+9FS?>@3ZifZ`UuY-@`i}|j z|26eB>BQr8-PAc#1ae-*DhVte zg+TP9Y{lHLJr!-6y-k|wikY%3XVY&qZtKyylK_Z!=oQ;jokIb6@u#=@t0NI4qIM&(IJ9*pZbf}TAhCm`?`#uE>@0y>@&AKd>AL7mD4l%^ zaX(%=@O`*Jrl`qeHXll37J`z+aH8B%{kf-I&%f`=+dH*2)7*n&4Sibnx4M*VASwP> zz};e?zc^j3DWNadSxQ}cM7+EI&dXa1AlA)Ejo>Au``$$=X8N}BZ6W>%80p@$2C9;M z8Vg$ugoYbdL&sIM(7sm!X8aTiXk-(gYeNZpky8CtHa*CL9ES8HAo>g{4AbXN!DU9x zvTc)JPbh>EB=sM4O+IYUqvpT8I(^aQ`ikO10k`;{2#zRuWq7fvsCi=mX<;rA@7z*; z29Mc)hS|_Bq3@$Edoxb}xAOFfs>xy{)%x*z|DH)4H^}O7Mg84%3tj?o&}qwp(N6#$ zLBEq~JH)2j*P~|P?(sLPvPvSOW(DMo&z<0Dn*`sw45dQ&&wTJN0c`O4`bE#%bb+I@ zmKG2K*^S|)VO|l(0?gSJ&@0=r5DK}xv%Q-JwE%sr^JlwfCdos=IH$@}WT00Qx}8ep zhF$TPaFwNt=|?i8H{(?|H&i7;pSA)FaKvJ=AxJ0N$Jd?m96hBYrmgfTt_m(IU=DNWB~-w733hE5Ko{Pf&FH#UF)Ox6sa zJy&!yF(xtV_oY|YSdFM7siUS&gCL6Rtnre}b4h|BEclw?J1x0VX=xrMBNG{@W=Ad0qwlTV;{Wk86N$y_XbnuAnJK!81_p_1l0cdo@> zMLL1eGE*zCu^AaH6(L&;`ox@GA0qP2Pd4uGHJlkyeeq2PYB&kSPO7{tczXwJP@GtR zQTK)R?3P?Y{XKX%VR+g&~API^S{{ZPGn+c#@?J zmJmrqL*4|?JhQ&367LxIw_6M<%!z4M9EG`Uk4|98W46kHfQCg+;p;5+9dbXZ&Tc_sG=Pzkfm0eI5hH1Vt4|S2RaKk z^j-X`-kc%?=%hU(Yz6)p=<8e06oFo@gqC@n(Eb;KdT-FJ;>i&_*VY1O#m%FCNirs zmc>KY1-=jfEP`;KSRlE7qCFlM%iD65UHR-M2hZ6H!s9%+(!?zy0JMLynJQ!l9(3Ip zpZJ?G-t`aBNBOhdsN2B^U6AQjF}$ajUXB;@aaY&DTu9Xe;iNdE6`h9BBlcQl0Hi3E zo7<;jK};cu2d|RRgPk+W?$OyRqQh-TcZZ=vc+un$9Uv&_d_s7r68y!E2YI<#`u({( zh8>OPM|zWWUC-HCZV%5OaKuWk7hwsr0bMJv9cJUS|Geb@q8!X7Mc>Ue;u4{PfSv3^ zFj`u?m6NI;RmSOUPL!j^=P0AV9cCA9;!_xk-(fj=sup$~TFY&&g))D&0cA$Ev~ekp zmBo$CFlDisnQ4;d#P68j+P;AZ)!k-zk~#NLMi93Y&71aCQdDrNIcJr057p6wQI3{ZQ(;Q%iM>OA2UngcI?Gi-c9syS|yl@q{4_ z>>%i%(o;vs<)#U5y4nCwQNenuk=?!Tueo&QT}V(k+~;89vVaK^!CtVtV2LISgbbcR zNLCc4gmO#<^+(tEK}x-(H`d3gygbQh4004RrA6+wDJJMi&ESvGkA#{zA-*Z8O0uyO zmYy66ty?S*ynTAsM5pkv0kr0vL8_#Ii}rWwR^hjHc9OfuNO_|WFFc;m{iW9X4YPNL z@A;%e`e2Y$;36EsyZs?8BW%Zo=!VWsLysBah7 zkD_JrtDdsR^ynjE{Hvw-FE6}*4@jTw&)2)2?@mLl4@*I)tNQMyM*s{-C!++eGyKB| zpAjoJiaMeHGAu22J=kOg?-Z)Nc>Br(Fc450svS~V^Cjc&a~2AQ_1O2Tzyt-Pf7o*csxOz-X1 z9N;QJ9j)TbWAKr(qtUATTu-WO$XVwP57t*PMFQopT+wZEInedqsn_53d=J>C;DLm}^lGMs(u~ z&Ex|xh`29u)M1M52kaN6R|G%n4=;@jz%wwy;di`Trl2bS?-rS6a*c0azi2x}q9px8Cc zx71<2isN|u+_iLVAYwl%gFo||8*pDXv=g;1abHL++`1=0h2?XS_E&aQlkVmctWWgql(a}mU9lWpAK^~(6vev;XwyL+{IlTPJjAk!2q?UTgK9GplkjtYN zV}N(-jY?$gDVaUbT3)mw#ViwDY}t7747vP|m>@nTG3^$Pr@xKfQ)8%S_&B2)4T#|Z z$q-fFNy-UF{FvJ-kKwdJ;PLc%IbGs*3BryW@P?X#9L{z3XoM>FJXqXr-yIELPAVcU zCth?h{AVK*vKxoA2-2-eAbHm6xNP<6}gNy+M^FacZW}Q3PwQSM>0xFX4e=mA3^axa&?_r3Rm-ejsG^tcWM;O__C4&pjs0U+ZusB6aJ0 zLP((ChC%|rQU7${Q;J|KzY#X)@@K2QPbJZ<&&ATWY0XUQo&9*}Ub7gtz^nxl@nr?k zN;OM?M&r!lPdYWJ#kL^l!6i@=-F{MT(MXyf=ueC^$N=qyol}5^(y4tE%5oDbCtK}N z$l)PqO8=bq2;zq6%t2fIw!o?zrH>Fxa1Vb$vSlfeM`z8ge!?*F1ADFzTBA44f=f;? zljj465j5mg|8^uz8kZ+OUidNgR?jXPTlh#w!b(D1F|@bObr6?WV4$iz)~pnqq$$$LRt6(F*qG#o@)XC%Fbq=^ z!SvQqeJKN}r!H^?y@UD*)KkStvn$EOD&wU=3}B*>%piZsNzZNGx2Sf#7 z$l;&nq-R@%Yy&zOP}(X!_#wVX0TQrJ7)Yc)q)#}nK_L=Bs!Iubxd=gAOa@$FmSyVq zR}9znDUivfmaom?Df^0#%Z!PFZW!uPuwGf;b4-qwbS4OI*)_FFY)bYQUJUx{0T!7X zg%K~H_bjrePm&+-ZWLSJ)7t2J;~;aD5VDnYHu1Sx1uXShaz1@4w2IyYpRD;Q{It;m zBF$OrUmsAkULjq33KbB}onNuJ4A^476!PFxy0*IR>`#5?8VdQyKsuOZD@6vS$X@-zOo{290wEi+!JEWR9r>Q9eQ%m7;4gF zbZm0*G%)lkwdc8RP4qJpwG9G4=J zfwq}QIPp3$_W(_GS^8AjP5ABTXDBEsChuLeLMI5OP&(|Y17fQ2q@ z2=U9Gmh3Kh2wflHM2?u!Xk(qO1|C8OhaK-@aG_EThHy{omC)Yg%U7w?-Nsyqss5xSc zg_2!9?0}NU@9#C&v8Fwxf%EfFt>>8W@WI(D(G-pEb~Nr;;KT9Sfs`d)!sdMxh;s%B z?-6RFPZTud2WlXDW`RT8zy&^3b5G}3(OC25&B!+TgqAluK`50?>eH#k!n1%khr?IU zG1EhsD8#Df8q+{!Lm<8fPa=^k46q_4u4_W4z{L@NZh5DuBGpoSr;dd3 zmHeGwn1ExG?4hVFin|`E9cSA(r2vy?xfi876Q zl8Ye&T8uzsMeJH24MFXeVEJ&PI*K}GP!frRwdrd#%N<2tzzyMybokU_ryP&=aoR%q zs$?NEzsrSwOkEV3{D3knKjRWlF!X}4#^hF>?79f~Vwj5t_93pOBAQ@2b=?q2w#~Q) z7VsCE`%m2k?6nu39V%szWezPZmjVatIP4eHhm*{qO4be4mN$1)M!n&#M6-wVdxy^x zkCjw{?LHYF0YvpE+wE0X{*=Fesxd@{~N2cp;m>RGji2h8JdEZ4vcho4v( ziqe7mdmK3ZI~8!Yk)iMXP$NfjATkzBm^@i$^6L_gTDXaHpl##?8Wp|Hun3hnq(EKl zcPE_d$DaG!-wR59@Q^ljXhS#3&-ee?DF5aLb2%_b93Dc zg~$jvXD~?5nq81D;T{!INC!?)4rL9aw9)^nnQXwue`i(_^ygAys%+hbU3h;aH*g!_ zvMVc_%!_^P&=&CV?vR6n;Q;1a%WXZ;X$U`Sq`IN%@`MC@@_d9*>M&XI&u6zr^R&RwjXigkLq8ZT@bzFNQ)^n59_B{W-JvytEv$2oHR>`ohFVrN8XbcyNwCas7#9Bh( z#*&9srZGAbOR-LI*Ar2|q2nma7EBlqKJhYP1+YCH%ZmTh0$o3o$Fvb^<${O7~j2WeHI5Q)UdxA?!f zhYnw#G~pJl3I?qiKTi?gOKX&kLfWu_?Thy%wr&1RDx(i9NX;z&%CgJ7~?#m`0~73A!wL?(Jh6c3@%eYShw-;e9pnqE2T z9im#ozMLlVq{{Y}2Ik+?PY_>YS;h$4m6h=7xGsX^t=pluAbs{~$NN!n8pl;$Zi#_L zT-;NfCK_hDZEx%GT9}fvI+aNLN^~iSb zsQ7hwe$7#4qZ@c^ku5t??}wVr&Yy#r5EwN$ry_Exs?K)GEp;k_}mc+oU3^hhs;`wlY z_eXn32FrrNC%ov6H)NHD`7)m_t}mZjj8Ybd#wLEA*?>RkL4Jb#^(d$^PRLtheE&CS zZM0g7#EU<%^Sr)45~|9P;MQ0u7(QQjI-l=fyMg^IcJeq|hP8DB$Yn>l5k?sH6rKtNR=bspqT$6AC?s5aVroB}I1O~1CVz?Tce5p@%c7E04`J=flKq8978F@ zwalO)#h$*m!H)4!rjiC~BZjcgb|LZrl-R4$`VxQL1AG1?o5`5$zi*Rd(VJAqwe&ca zscV9ot{J*eoYc~Ne%|G$zxYi(*(<Lofcqajh%#G#QZA#YrMTN`Pc7OvooE)E*Ulh|1JJmnW9P!>B zT6D)z1LT9UNB^}xOi)9cXszHWyUx~XonZa=;)~#K6sDYy8P^`0-fpyEu6oLpqUYL2 z0Is~v^&){vl0dbDelHuV6bb;8{-4mvsv=@K+9FPuSFffc+gk~KqzUK!sCn(eY8-f< zm>6>F<=I#VT@j;G9Ps$Jhc=-WiRH(j%Ai7P`R@n+-`L5GY%&5M$yvQ$tvpEoWaB+= zJ{LmOrvz2}Pe7{rt+4P}TkXK-u5utheY!b5Oh?wTO6eV7a+ZXmT9Rv_!)D&=PP+jB0UR z&|+QwJurN)dfROF?sZ+MP8)1R#l?W9)uL|mElN0ZJ!WRSH?tFr+~@P!S~nW9rjtw% zmH)LkMpLTbVrQH&84{M?4k=7O`F=(BbPZJ3dz$e7+d{F={y$O4ADY5Jwx-rGGmr(~ zAE_;^tcVX-L0u=o2^z#i*-%4Z{@)WQ0X`rC>mdG)>CUdCIh#{$GIXmc91I{g*VoB~ zU(*|)PvwRNVAMX|Dp-i>Cu7pmU1{r zJO0m!>`48EO6@jS^lkxrlgUB=xTQu$jm2)FKR7s8taJW7@acJJBsksOc9GgvR9z>3 zsfqZ61f;hg>KymFCaKhX(yaEb^D}+ls;h$~4(1Zmh=7DY_qOntqr;V=rjt#27MW>X zSK4E+F{2mef6K~IRAWoSg851=dj)J>}jH&_*bdF`Pk(D*MR5$UQK9IOStqk6rK66N`DjT8#!WbE}-T00Y+0X_9>=i z7=z2!+dv$qWgMAJWfORu%}(%Z^v|)|4ZC4zpg-8%_1gN3%w0f+TGvr#x6sk%ibXRx zke8X+jDLy#6P7H#7#RtS^8BMakP~gpNftYTF*J2$p!LM3mTbdzTgoN*sB}e;(>!pB z7Pulp?c+k*RGt9)*4(F}#v#R^>A&KRcZNn-+E2D!SZp4!H4^yZM#t>lNi8Am9|9Zf zqO^qt^I18ezL#NtBKusT_cjw7oAgSh)uaLhVBvM(X9fV1bG0i%s8Tu)SPi}`#L8>O zKK1HO2hS^Nhp0=2uM`;}eH1m@8(^3$XeH~~2~Zd2w#;udU8LKXWut9ZqVJFmCm$zh zg7IKQ3puMstoB_-kX(w=6$8H1sXO=dM(QE^74UX&Wt$D(%ScJJi|_mtPCg37B=^{> zq*sc&n|n|neMqTY*0D+o>c*YFyMxO2DwwSqGR&QnEU<>%xWgBO4B{$E{L`6_z_P*^40 zj79!tgW6>V;~Jk5-M5bUEW!LS<(c+tyyXHrqbZ8BCe*jRST;;SN4!V#Sb{g#^ez>b z3|mg}H`$_hgy88VpVJf`)qdfTJ@7XVfjf=Ld`Ra6N%~!D5`>PcYwETA?oIz zkT*Xu1gsr87dI$y6!)L}FW7+sI(8agspU@=)NX0813m0|^hBnuFqB}woNK65Lgb6j zayC#T9*K1QS%NP8_J?H>2yJpDU7-e_J0~(xHsF-o-4CHK(CcRduXo^ge8%w$F?zXU zeAupuK7$MdDGt7VeecbzO1CR%L7x^t5<(tM^%yfDh*9Ppv`lq3_6RHs+PS>(gy)XB z%l|4n59LHf^{gIRdv?c8;ved3b$jI7f=9gh!9wGUFG^kKV8a5`O+#W9%1}r^TLZ^F zYt=Wn+3_zTQ5b3at;m!u3jmAPxqW?8T&IW*0QgYkdf$g%{H(w+x;Mtb&Gqigvjh)( zf)(7t7WVJwDamy22N<~^)WvfTtzT~E(~~@aAx{4cW7li*aBMp@*?Rg#s$!%i5^_;h z{-l6E>2E->#;$c(R|r=a1@LG9{#JF}l|(G{27OGZDNmBnF5(_6_B&jFrc)lZbY4p3 zAgD%0C0#JMe94Zk9V*4Me|HTDE-!+0IN3|803MGby+%uLizPokmUiuL!YAYEp=9jj zM5Y+??2m<4j<=S;d-e$JPmK6WUZMA1!ybs;iPqZ!liv~00*o|ojev(Xd)iAsUHR}+ zh=Pmn$sll5AHLQv)qRX3mq1;x_TAdqxiScT$&Y(w76M~D5Ctoj>~~k@`*0sg$eD6< zb-5b*6w&Q|@9q?$3OKJmT+s-^(v&SkMMN*jSmHoH@2eFhWpp(7j2(qM^P5HMDzA-& z^-vLbH|KX0el+YN?lGaM{u;z64Lr*}a(Torcs2r0i}>)$euc?}XgCS=rQg<9{99G}H1IbgL47h9anQfaOJ z?pN@oIuNB|HQ%$*&xx@NA@477@ZdE#yIgosuox$EvLH$8vIU31 zKnk#U;a|xrRKc)p;X`Qw0M{)KMpTOcd@>GU3eL;f!u-GRUCuf(&@{APDWqNZ_i4{H{{NV9=zCs*r zwcVmZ?#h$K)7M;4#1nPUT>RT=M#5ND@XT`I#BSOS%vN}bR;j0p|QL2IHP7_wUB zUqW|;&|R?Ci*`K>G>N&_HX=x_+}02U_O=(60+gs5b0$ zV6o|?g1o1UhE<)(;rFwY7LfED^0}4u>AqBK?*g44+PYiokQGVd2*2OIp#t6`G@*7t zzTlhu1qtAKStHQ%i|4k&fEL~FkRGXKWXaL@SS{pIpoX?sd3I{v=Vv50gsbLs6f2z% zdzQx3KiV4+J~jrmdu67 z(syE4vj1p+A6`($7cFI_;gTO%QFROBeUF7{y(BTb9e3LdOX?-8`z)c&y+bt(8H2Ju zW(oD(Fl~#}FIO3)8$;y}v%Pa_yfBJy7kaqhDmz|cg8;bytCGwgrAtEOs>YJ{V=yZ5 z04pkc^2&!vsXau}*Zsu|i}YX1`T8eQ8?pfjnM23N6)CCZkzhA362qGQoaUB7nvw{c)&b#2h zpr#kd>I9(8QgZhvM14O)o1zL7edKoB}v|WPtbNB!?w6#IDbrWGv0^Gs|H8j zc)vazZ+0?f#o!H7MkVhF65#o%^{nqTtVmtd~h5XyPa7pm(r~yBqVlNhH%FKRNYo! zwimawHJU@_yczoLvO!4Aj;4Zcsq*}|pOYIS%%LtZk-#Jdjm&&`sYA_rSYYv%c6^G@ z)%m&C1^k>ZC?iji&jc<*d~6U*yGK@$NZB)eBBDWJLs;vQ9ZNJrz0gD^WR=$}nHd$J z9M>;&K825fW0P^q)y0BU&+q|x-O&oSg7$pIi(dC6x3Uvzv>Fxj*^F%zpPB%ak% z`IxZWWukIRA9VLk;Lah4jFO6y5Tm_^0norYuo6#d*D4|8hD7XXJ*2W~ayvQ4knySQ zgmH}}rUf~1glF6FL~Etsv$rJ5aq_})QT7|NQX_>whi>NI68|DR6h}!?lv3Cggff?t z&)2+RlGp=-;;orvYPtW03Dhp9Zf$KXtqpX1V1W7kUg{7jTH6M2n!Fb9=3H=wZ@$lc z)$<5<<(pf=7XFkJYUA-bR_y!|Acza%gKl!&S*s}*gLwzEqorU8|1rgh2#P_CQhw1*JgM8u%J=3LlVu?~wSotNlIH1wF&<3V~{AK05# z1iLYTTeUm!H?hfG^%Y7+YGhW-x+N?^YWLmE64P z=jj~`<8<9E%=NnqWI1qj)U^gLQzM0?4(Cq@*+7Z=Z-xMT#TvM=2hn&ym{RU6ed9@q zp|a4d9nsVsAjDDWwMm5<&&@1|UsrjZpdTwwjZjki8%QE?xlZo~h8+s>WUx6hPixX! zMiABGWTzEnWx-f99Ly-Co|lOH=>6hr7sq;k>!#c~eU2F(9bWoO!PidcyJ@(xgb)F# z*X{DB7^(;IPI&TJK5H=Zy*VUzC7}(qt7M)XUk={LvymIs@*7B>v-Zj~0z}zv6m!!- zsi*IH-60N%c3yTxC%-hs43KGtv-(1Wi?xJOQV?cUrrjWdLm8hOEV^T|gmVgzd~{OZC4;JU7t91cw+pTGQ(aI!mS6RrL^6i|CB&cg?L;R=lmI7x zjjo}RmL+YV7Q`o!o=wj`Ajjhp9qa$Gtx-dQlzx|fw&NaqEIHNlMMR6GDBDT*#nB49GCb>{p1mtG{Q}f50RD)8wb#neZ;0#=~21 zZ{|IHeo58vTnauzqGAwc6GB8fEF2CJ&!sqYUa&zUYywvD#Gl`Z8xS648ooYYO8zut zuzG`^gJdLkEh=u4oix`{Rfd+93?TQ$Si(JytyHJr2NEs@NY<&&m?_l|rX5&ZaXM^t z(mu^A!`{XZg5(}Q@;a=!;Xgh;IuHK)=OJPo`3dNYj8)_h0i0m&-li>--S(_G&~+iG z)*6-*VM4M^>QDx2T<$Nfz^LoJZ~(1?43mP>p*R`ldqpLO764~gCKL0~$;NH1Tf=S` zeAW^$jOwT#NKroQaW^p6;3kBL>)77u-^7_NnWpUyD5wBSKB#9cWa~g9vXY(;chKfS zhn4ilZP#9<;8~0n5GwTb@i$|+G8kNWxyx0qM`jDsamtupHo7xiP4U1}-J0VSomqkY zF#fqw_pS-;nK+qZS$*Ths@U$TWOy8uKJn$dNOZvYPjL9Ws}WFRM8YeRlGuNdzoejWa9;on$C2b}W_ETN-M9f( zGd?6;G!>JCacjp1IOZAYPinX=tVW|+prCp;%Pj{(KBDtjRLPY-tgjAGc#y;Y3rhN^)m znbgPI zJH)MmPHUNKddpn*0Ox6~2j3k5t<6RT1`6kdnU9h8m(Oml9-41M zwrydXPf+0BedD0pW2L~nXF)8eZ35`tOzI#TmX1iTGHkl*sixRjZP-LSWNS_FDd@UM z;GudfdUY7L0h-u`w-E8uYB*b1@p|@jwY#*YwQQC{!o%N7Ioa0*H;>U)kA5gt36XXH z!18*?0s&p+JF+{OABitgUJNz@Cw13safK=D_K#|E4xe*~VT{$6OCPZuE+n>*XhN>5 z8!jn!5*joYYE2z^0DUm>+&C(Y=V}C~5V3AZpD}v#v3DPG4AjDA^AGl8D<9-)>3Hud z+yOdI{O#HAzaj^n!Tby;sY|SpwFXkW1^%U-ujEVj_6og%_Lxe=ax zO6e~-I23Os?vH8@4&#$pamJcS%&*^g?C2T{^no|y=pdlEzM0siPg??}5YNXG(dboR z`t4}%sd3E2_xcyaVL;=mIK>A^?H_^mqCj7g1c|Hs&lAwQJa_iP>^fA`XXmAI9=%$9 z-m-&YS_^#s0q5J3P;F7m0fNwUZd@eqJ@tHhqsAByR`5gBjqZQ5l3vR{Z1RltF=AX? z8XcXIz`!6x^mWd($(qQvle!z36pT_P(**n+@}Cr+boN zpqyBUk|N8+hn`38VdIrWJ73?w)gzZ>Z)2{HffPyb;Syh{yf zn4OLM;1sim!}B(T^bvVT4ACR*I?4B;_oWqaM+wNb@4y(5yXfp?4?3~aR)il)Zbpx6 zdE`RgV`M-Ky@l91LuPLoEuBx?8fVc-PRVVVra_4>mtN_l!_@?wc!%qo)F_ywkJ+n` zVdLbDr2$(VUD1o@San0G@x2<)3e6#Iub@bp*P@GVbyTN&3#HWho7BDK~1wXmVd;}SS0BmSS0F@q|=d7RlxJl{fq-T ziLg6(Wb>_~@|;#QmqB$Y%tg+H@GjKz^$09NWgucCcgpn5*@E6{=C$4ok>F7Xe{sAZ zBR68+-ZXF(+kiy7pb)ZI%-+Zv*Kxr~~Dx~AMs^Fa$y6a&0jsW&T#zTH9=!8#S0B_C~ z7(e2IWMBcy6<`=A`G$5K*NEL!`xEWXMH(urnI-2;>r6zloF2Nk~e=Xo_+{PNsP9-1t&qy*)3RGI082DVmva5L-aMtvvvvjD zpp0?}JfuF*8)3??Cs5Ou_1nzJ88u`nU|1Hgc(>F&zZO*Mp8BI?{$&+j3D5XawZ!e( zl7Nb@RMkEI22Xuu zmu4g$dKa3%R+Mj5G;5nJG*aas9^P!${c+(Y*;G(qNQ&fc5jZYCIHEdkVIolMu{EKB zR#2jaOjuk&k$DnTws5m_zCtudBXYN($ELw%X%25lOHO8>!9Z8TqoGLOoxPcS@iM98 zdwE0vYlZmX+@_ocv^;8dLK%cwTFy6Ani1j(sE-VFdROpbNNPgy@nI91Kj4Euf0rgd zU+PV@9Yp97zuR)aX(sBkg|2mYf{Mp@0C~Q`jC4cFTajR7wtlk?U4lm^A@LENxc@rn zkk9qVWy=Ur%XAJAnM5Ft_L;my4x~V$^I~U?Uz+{bKdLMD%O9U2L=p29@Hv!5n0h<- z=|Ic65Uf_8L0nyMX?#GcM; zH#9X#{Qj{vPxc|4gs11 zNc>kW64ZYB8=tF%9xovWnOn|k?(bZ|XzHH8gPb9e`FQjj`1$XgE$VvWw^ifcQpxkF z4~Ip!ny*Y3n_TiHYdx71FS#dw6$Q#QRq2*F4vj9Fim(bwdveUNwRr8-d87<+dVqym z28|U)!S62uw(}z@K7Srsar9jdmDOSbq1+gQHtLUfI&ao`uH4tRhOwA7jx@C$S83*U zJbCA9s=Bo0%W3-90RAUn(YtP~dF1ril!kE69C_9+v*4LciC!!s%IV*-b(Ze!<)sxS z1pDJPokj91=wvb1R8<9RX4+U1Y#R*3CCB>gzL_4-3|o9Y#tn$-wuy)8GXp@=Zt-~;IUC9x zchY)oU`}5n53psC5-&u2XTqC^b4EO4pD6+oac;72yQqQWO1vaWJ^ykP*`_xDh`;Gl zv*@OM5LG>s?JA%T>K_5cCP*6Wf$5IzK_`pE+^snu83u_TEa)J~WZHwZYBXv32(UNO zL<+1@6IxZO@5C34x@rCTd_F5kM9(0{rr*+5v5(HldB;EaD6>b;3e(#-4q|bfjs3Kd zt=TT4*Ry8_fAr4-_15MWC?xJ`>wD`5QdQjH^XA%7A3GkmkR;9>WhkwWR)>C^D_;u0 zYycg{`ojlx?#|UUKYybq7nY*v$!{#^O;V!o?%;3vR412kud?-yIva~h>GK$xyZK}G z^`=LXUyTBz^cqCKF5#iwt?ZlW(6&M!?P4J>LU&FocSU5~GSoPOo z)|Uj<=G!>*_h7XR=i$^vD0|;KU(MCtD8|S?7XLAok!$5Shq)JsK4q33GTgS%Bx~e| zWGjW@WeQZiEqMN)9h?ZkNto8z03AtPe)jQXEfIE#$T-(7nZR(hrmZj~`cw%p~r%WXs zG8z%R@q&NxJBx}op&Xw!+kGe=j-A?T=bQDpo1nDk`sUPYlc1OAR*&il@t6{6ep2TP+utG9_kb%ZO-9tjKXbW&zJNq(+0gG~ht}hSttu zLZ0=YZLRPHZ=v?pF1&9E?i2w5X@WpKn$H0Zl?eG>$vkt^O6t3LADRe}R7SVfs}ZNp zzWcL!X2HzbH)QVKZ8w28mVi}4x8Ja)I)f2k2KR(h`8^~2+F<+ljeOAyx8o8U{Q$?e zl7@3J-5-`Qm$ru#dP>wqUnaJ=WvDV~WNHU>@MVD~OM`ve!gVLV!yxqGs6kA6!L@L* zz@uR_60t6X{W@dfNnL(94Q%FVLlzIEc-p*0*#{Zb zNIOH_^hFNQ#YS3ohKNv_M`NJX8h~sKA4U=z*&L!NS%XU+MOl|<(Yur50I&YYIuY)w zjQ@Efz>=i!E8Tk~50foe(=wGm^r~HdjFqMT*Y^f!9m@O(D1Pd~3k-k0bICaT{2A?_N+ztjhjBDV3?rQPz|g|?h86D`K#yJPf9Rnp>_L8fd>k)!vaJXAEeu7bRy8%9Zlni! z_0V2@TI-F-yI5!0+uO5t{V>=+c)#%}1H(pqVxu4*J$)AeGU#HsMb-Q5r+ld%^wLQj z(v#>B9hIHm#%s2kfvACd6HTsbAt75P27%iIrjt&rbVb^}S?zHubi31T1!1jPio&97 z70PE?Jq@JK8U@A9Gfv=HL;_AqPF1KlXftWiB5VvQz8mA^od;NB@~?Ux zoFoE5c^B=M=NQHr3MjAQYl##1oxJ@ifsI{Drfpln0yvXFcEU`g<2WD6No3>)>#lqxd!hc=#U(_BeBnG!;* zP{=r$nKyhMI67#p=V~hx15)^W_6_y*$W;XdzRwuD_~c|!@vNre*+|T4M((K^4g8Q( ze`SIoH)g1`Yz{DsIuF(TF$<`;db4iUP>OT563S<6LOhcmM(05dzx3uE^70T2}&vvltRihCB zE7lWgh(q53?aR;S2t6u|pjzi#Cats9i~tJw#NRdd2my?GIbr*#pP4Bi^}yhpqmNf4 z^5HtDfph+B6H=0%5&%E40ysY6@Kos4RZtr*0s_&G0(9I(4Aj+bj6#+$!Y&qvGuCX-Z?o=tMsmgv=tDOh=+Yg&g z8N4b@U9NcOCGG>-Xzy%aW-oiwvdijB=5!2aVs4NFAVJc9NU-5kvX&o#g_e}ai|;O+ znrS%$t~V+f3)Bgfh<*Wxjy&ri;L1QrXQXC+Xb5#XN~_64(GxKUu$E`5*yep`UgK3? zZVx#)ecY!8cJV9q8PnF1Pan}shJ5i>9zjIGs)HSGPXurz+Gn8J+LKw-YCg+27ran{ zMs8j%r(+Lh!0Ah06Cn59p~<(7CuKv7!5g-#(j{m)9M_VbHu?$9qExd zr-SiPRD>W}wdgZ_keU$p-q1FnD>47kl{%Y^+#c(oG*07ZmoiO?l?GpxecLWi7D`&~ zTU%Mv1BM1WZmF*?Qhb+rL;dViyr-gB1NO>aWv8r^@@07;9$SHDrJ>&CmXKmlB;d@P z(1|;STg8wNEC22iOn(h1Btfx@UR&=bs%##_{4a4I>VRB{My=8=rY0bE;xI^)wRxs3 z>T`7~-==ipIaGid8aaAX7HQ=8bcl2EB!g&9TqM|;RbQ%{j|PggKBH;5qcyC)KCA|G zvt$6gFx{dO5w3l=@`%zFY6D@6t9)6Z`!ui>(ZBS<%grx1R!%dzUf7ehd{J>9yTHBvd1A%#(HwOcIGH5 zSrj_D427HZsMX1fh-SJtOv#{jk9O+5cV3q6G+m`u>mAQ8H_z6M#UwH;C!yz+XnhW~ z$J{E(HbCl2r?>VdCyab^1AfcRq35*&AFP*3Y4n{;eg0ai=z#-hJ0UIu%bM>IZToUi zdWW24=JbtGw&(XMs3=Bny#No5O-!FmGN**4BL_@#%Cm{bqD``xn=EM}NBba2&P(LW zXVpmcgo9rd5;l!uOhUUN{o&(RlCyN;`t8^8(!$G)-IQu+F!i&=P3MdoZis1rOZau< zbH)B?-2?C$X{9I(i6ckN5)SkzrUBq+Pcn zY(0%4C^>wy*js@PdbX+Q_DpumoXN|pxvkdhG}^h$mK+a7ao@*=vSrP2cCMIfcpjvJ z_%cB{reybRphV83{DB-v#;E_{u=t^ob>S;%biy79s2G;x?8Kz0E+J+*Vi zAU^qy7lMO|x!%6*$@x-bT6Nwm#RMRmCegUi?1OXvZvhpEs!m*|+qG^>FDG*1l$duh zUQ;7iFkPtAY-`A5U%V5xa8#2>RJ|;S6`>T+x+x?r!@P^jX)G9t5E8I;XRxEmk!D3C z9`E!tU0Q(&z-^D?MD=U83P##7#aU?!tD8xNAj9=doh zPvku@=DqyfO&p4RQhw>uEx=ESkWcssiwheL5UX+0Fz%S%%1w{{qW&Y?&q)coo)))~ z+lrjC5xJ1}uVe47-VS4KD0y}wQbO8%TP}za;goCbl}{>d))emSA=D7`(dhO1W3tEE z9gAEC!u&dn4W?j%^~)(0z82}|dl^#J&Vcik`XA>js||`bHa~x^adi|xEA=zY5W86_ zacSokiD#y3y*Ya2Gw;5UrblfR&p5`K zuaup9PV~=S!QFosc*Qk*MUucVL4r<)KRZg)43v}M=?)GK@W9W+uMPg-KD_;P^5;p4 zdTcm4!1U_$I_ze5Fig?_9yIM&6lazqsWJ;E1N#)x5((8dhf!+%;Q?s$yY<{Wg@gwlr{hM3bVnD%i|m zS_$7WnL9q=2CP~_O>f5zZU=?Wo>u~Lo78IO730uugEVKCzF+)zAUo$g(iU>>YrOhX zuUI1>6VmmhJFPN~=2^47wN|^6!okB+n6SI9G96quSzka+rq`5K$`rmrM?H}n=9^604d(v{z>VVack z<~i~_d(PesxqyXWoHE5D+iw?Yqa+e@!!|*~bOE{|R{gwp_V`8v%FKF(DAlXS)n`%= zLa*vq;b6gwg;cYvyuG^pI()jS^M_PHpB65bU&A@`4JEr1W9hY%#TAw=jA<3JE3=sb z)LZ=uIic#x6I3!{JT|{m))X&KDI%(i=x(Eqg~PlTvIXZ}at} z#oliiy5C82>9g(J8b;!FSl<0yafFkWxWRrMAV*ZpI>hB#HJ18~eWAR422B({32#4A zaYphd4{x+O0#MjIkZkd-8fHKOdTbQ9&3x6ol`U1!PMqTi82U5T%S)L~Uwf}mzc-<1 z4yl*K=;g3YKase1F%k$0K@4$9^t=DweRt_eE7iLN3@>S}5I-Ij8FSup382V(kWJsZ&jgAY3{B6ssENE0 zX)kKQ8`hNo$;7*0REFGCBN5O@R%3Sv(O8Y%<;60I$;*m>(C4`&+|`7bmN!`CEkH!a z^3zHTEQ?i}EGkftgvDnJdKJm15^p`c3br~)_88op&)kLG*58eG$`8v@J^ddTci{)EH1|E+OqU`YnGqBeG3d?;bC#R8@hr7c$sRwgFxH%ytWVXz3J}PsCSX#nj zG3cws<)K7%q)%+CjE>5alFoeixq;BcvK_L)^X1ud(-e|(cvjJ94STsjj-Q>+(W^I2 z2}Q;1cx>6mh+6Hdnui5p@tRVa(F)EQIdRC3$$@BTMGMkay!gfDHX6Y_j z_VU5>wKlw_!4!8HeWm*PqlD4RTth84-E;&tn)lx6Z+D?x{$ou%ClO=m1t9Ye&Mr!b zg2faHxJGIQGUzTb>(%=8j0epzhryt1NTA=QBXf?{S3X)|+6hj{aZZoE_L8r%7?;jC z&z&N^9-ic<-p{0La|-|)m4S-+=}umFYwOq~B%}tL6~p(ge46kZKGcK!mhseAL-Eo* z+izy+FOfUvwV%tydt=pzw}JcUt_T)&gzm+!-LIu=j# z9lw=^n`Av*AH2?wsdeku-unWm2b*0vrl*%lh9ay;BXtdz(J$$7MR=f`it}g+n2WS; zb99ci4D`T+>HzEdhBqq|>AIcNqBQcx{W6h2y&;wPR5BJ;{flfq*Uq|h?WvsOZydhf zf1;xj>T$rVV8-H~TwzbIkUt{HVK5kwQ2>luWLGq$%2lyS`h3KT`Go}m!&?6{mHATo zW=_73X)jeHWcnG$mnmL6-Wp2o|DrSApnccwgQSX|l37s4M)Y~bg>9&ER7h+rAbwNC zsU!K*rQ;98LjxDVKLB&g`Inb$#=dB=f%?DnG=`%pfRZ?$C*?@H|rlh<|~;|TI{d) zjnd-@7wp2G3O7n_D=N-gG0ZcTOx|zMUS`(+K|9RmK|WUD=LW^&5pit;EP}6I@Q-%A zAdblwR{8GhAkJ(fE1L&Vtx!B;DK*aR7fP?X5usv^*UBB{AWtGFhS7TRU5qZUjPko} zsCq1rZFFcmcqrN+K8|a?d!sVi%}2zl`3}ZebI2D;QqJZjkVqhwr{H6Y03&|L znwsVv@o2M+@(-yQwZ40WiCAJV(1&XK2BQ6z1s1Pe%|oQvp3)s+s-io=DaH7gSN6w5 zLPpg&7x7kc=(vHUx!MU3Tfq?Ptb6!)kHTW^x!eRP%SKX5LrPl7{WOz-iD;`VR0xNz zR;C7ijTYSt_rsya*x$b-;8OA)XlLK|I4I)vB{*3#9QC73uh~(mz4P5PCpMY zDqWQ~yGX~}<}kPS)Dbea_)zt>A_W>*2*G{_+dRmDXp*lniB-8^&|SFh_gI!-V}z_m zafly@LP2_Wr(l;hGB6g^n%*RqfXn^nonpA4N}>gYQpq!+L|L?5ycqBCyU}{CjT@Cv z4&l&-)my1+EkM47c4x*Rsbi#FIKly7o6zAxF0UGLl36v$z0a68FyFLq$V3T2C(e%4 zNsVP%C~`&_`PsB5LY(8ovy;}`ALNwu{x1y7$1tBp>l_bl)CG&Fi>lS>jBBT9ipuSx z2rhpWUyCY6vwcti0_3fpe^QFzF94iFAbPONf&d-u$FP)KCj79QB3OZ3$msHB(ai|L zas2otGOj!)H-@ydM@O$LRie~x7E&%WHJ`#VWzi%a|Gpv=xZs(=jK%enc!A*AnA^hZM3aa=GBPUKrJl|PA5yK(+ z%X={wh%&+1h^vd4^(<3hQpIP}A|v;QVll=!Qit!Lrur2c1{<7a1UY_XRm$bv01l!5 zDd1pQ^0`xxGmtq$=d(6ER8191c0$M48Z$ept|~LzB}*mwKI31GY<<$ubCv0jvz3)54BzY=E|H?;VZuRO|pEpW@m%- z9;ANHjT2#0}+C6U=#Te`Hg*cvNP(5!71m*qNa}neH!+bcAMWH6;sXv zKpH@)pSasN^@MW-Je7t zsi-pCzx zFUhH(X|@+X*Raj#H06oAvfk4Mu7Z0`OLLJxH0eX@g#Po3-2v-(HlVTVzkDQ}Irt4o zb+rKaMEB+p#2dZjB*M$jM=F9P|4IS>xhNFI2`(;}EG|$v`HSGvMAY5iimsS5oF~kdbD%(UhIip0UUlj zlpgh5)k66n7TcY)&o|gj7<(^%by*gl*n0}1c>ZP|ILG--!)x$MBex9lt`OobC<_9~ z^Ziv03(vcaj!iI=~p zxkGMgIf=U`m)}t4IR6<(Yp@L_@Ru1HQX`QtFJ+>$tOI~7YuE4x^I$ElH)HZ+4Wmd3 zaWH-flm61b0+Fsm%7vO`Q*kgXO3%y%5QuzWLkdO`8QugokJAWmSf?p=M>BHBIg}sJ zS@recb=#oS>tY5*eQb3UK1Q>_F=5o?2_049{8;-Zm@n0{h)}|*soHxLsNbWUAD_Hp zMDK>iFcDA%M8@RgB;83KvxMwuX6o2p!?h}L3!~Ix(xMJuYI*pBLtDJge?$L{r*$;c0Gv0Kf5HgL1J2*tP-fL}n&sD%+u;i$0U#SFad~*Ema-Vb4qyY2K1oYQ zKn76=DA0kusf2}}+zqpL1vH8XmIGo7;4qc5xwJgDHk+o8JGqsa?p;=@OPfMNO$x2F zsUWgnhf5j9M@*ZHF6{kEZAJDIfA z;O3WWh*CKrF{8on6a{n*M7Oon)oGEFYlxq%0|(@tTlO0L(3cAT7+(OI&EP$kjP>iO z83!HpsTncx%T1jd{gK%7b%4o6x|3UELOy#mapSL$Ta5S$X_p^KOF92@iKvoCM!q{g zoSGI-N`uZbAud>INJfU}>9D4`bVm6D014%`;ujiw;j*+pS3hs_($?!0UECgPBOcD+ zbAye9oIq&f^itHzYy28X3m{v2d|$(}AjecLO+E%V8&)3VHh~vh&KBdw4yQnYhW>S*wrAvpOGwpvllRGad}R}Tk;pueLFo)W`@EQ3vx?!Bq$jx zgZSB$(cT!;mEqt3=0iCt24UWv>5PeX3fQ~AVmk>H8MlCwD(K{1k@XQdGtL zdr!aza5U9ZrasuoX%jMLWqipQ`ys16VTWW|{_m)El%ne7)Knk+e1~dRCE@iqBSWu& za$9Hf-#={}&80;ae}sB9d#m=+@1;oncl6P{0?p5lb-G-0!nRn@RE?)CcDf#d-c{U6Z% z|9=Re*#BEPsZzECtr`n0FE}+@rx2sjTYf>NcFp2SQxMor(0Ub`I|m2nE$EtElI5-S z{UZV|11G_oa?S;~m?e5(^?Qth^G%%hjSSyg@*MF|Q-JMk`R#CJ<^t6zy*ppGT6Y4?#Bbre*AKat80K1cB~3lL z!(9E9#T?FMNoPi`Yxx;ezW$C3UkjF2iD3k%+rApQnb|~UbXi~;U1hoxfKmt)|EWFV z%O2;~;^_Rd(tFJx-MuIBvnW*s`2JsZ6Zn)GKJl>afb+4H9RgUXLu>6PxbMr}2Qfzb zxq)MtNq=;+0t{mETRko-Uzu_x=;f+LJDpi^eZsBbI*y|!3wy^|H{^F1&~!n&%VdnE zt)5lh2S!*#=<`7j_96ZnOrnrYant7oLapXOi$$z$vq_%cm|@U7p;I|w+jgkndj4fS znMmQ%@X%6rn03Pl$8jz@crC`KW%q%!;a#BE1m}7OmE;E9lLbY7@`<9$BNVB;f&MKQ z`0%n;x#r+Y)7NoK3!P71x_GAj$$6gd497{_y}Vcobj_xpL?yBKy;C;-Rc4n?v#T9~ ztFEh`9nEhwB`cOq4fhtYp{GYrfbc(+Vm@eydAC7#amy5td0SQ)O1 zl`aQ#)_e~NoUlA@D<99x%E5SW`Q#a_*I2|MWYMqC1o6Hz5>6%MJM2@~nfXE74h<@u zckvQ?0UjP-6#ZI7t$29X2pceBbo#q4Z-TbXY6KN8LxFtWQb*~fOJ-!RFUPUMH<-Ko z*`afGEc*pVp^VilmjH9?rNNsx-v5>bn_fmO>#NnEMMrGe3iN=l1l7imCDzC1C2R^?daOPhRaOMq=!515{PuS(%qYj zSahobpAR&CU)UhDpI`MCQjGS;QtEqa{W%R^?^Pr8iRap{4r+IB7y4i=$xB7E%5~`P zl3G8=JA7HEru)vgM07JV4sec}&BDZ=TNqyU*1I*gE^PUkE|B``7A}1}17uY>_*!DM zRVVPBG3i;l@4S1#z7|WJhn;iywGLmkhlt5&lGkqU>B_>*&r(meS}7(T>cDe!!bXe7 zv##EZN^kM81m9qgS-L!g!F?CNR8C7Xyd#>nxr~Tr8*XVWXY)_}SIS_oEZsK{rI&vO zTI~-u?`*gG!ke}8aD*W-$v zLK8gc`3CO?+r9RwCk& zjSMAXxBNL%lXsDAvG%cqY`$QL0dOT09vYQgCQ+I#bt zt5|95=a~mFqIOa4Ptvz(d*ms_~VLq=J zppmu~6vRAgCY1YKHM`kXK*zA&`Fl&5 zAMD{vPcW(3B4^0!%7?o`~f@U|cWv#IP=_h*0mo7Q<&-(H*BZD~*2YHE!)- z6q+#0u+jt!2+##VH+EIT*iYY)SS~R<4uox+p}!6-RSmGPw4K}u(h|BtEIgA5Ne3@s zqm7)*qDFTIMP=(vA21GF=yAQvYvMwu94B@4K2f0aJ6rX``p?`Y2?=#4&pJqmCHIXe z^IMb2jh$G9n@m;hJ}tmJlpZZfJS!3ca^u09gSpKsC{|ZTsFJd94H;D6S0wH^=qku4Ksp zOE{_o*YAI`1}ax^a3*eEzhA9?y#%}@h5xPszij|6|KFt)^8Ywvn(F`mL;nX!aAzH4 z5>)^515gz?+^T>ZoUzDY)0~ao9J~jR{XLlq1oGwrp|TuEU)J>UPnlr^1Fm_aoNL^` z8K(_Q`08(!X^Pgy^>=h=s3xS+e{^7Z;;%F8i^{!b+diIRDlp@mIQW~O%y59m!|S#8 zNlK}ZeFruKztsI1gWEPRDE{@bWM9#++Wp&ZYT^I>odBW7Wz@^~eElv?3Lz9PnBG0? z|Nfi(v;&R0(=?y4Z+DV3gUo$H|Gi`it5B`^**sAV#bd6TkE#(F$F;R3Hqs>ZzdtnI za&aS6Z5f5#y1v(9?I^^LMhn>5CM1jsU9~`w`Ix7r@K@^`#{S{}YGJ6y6&)j=!ZDb8 zO}_U@Y*Nkz1+~lmHLmH@%Y6(t??s^BS#vBK@A96g?4V)Aqa3}u| znHb&UU1j*_iG*;+y<8g5rIROJC2p*4!JEe8?@;WZnfqBX#0AMToALMSrTJKdco067 zzeceYGE+oVPU+#7A}h8SRBAp?skkoPPR>3Ze_0LU#Mx>HiF)Ny0RI3HE#V~d0Isx( z>ZWC*`;AN7x*A9Bp0HaZ;&q|A)s?@(EqpabaQ*+mx_sUD3>+j7t$UrnwA0tQk)QLJ zNm!A8W#;`jo0+q@Klp{}RaLfyBH*ZAoHnfU{sg(nX6n=k+F z53sePv$@h&e~UjyekFVvcD8XYf`l&)@j>GzUtN!L*NkN>unz1!3K#>4-qY6aY1%SN zvRA*Wu(m2oBGbaZVjK0WvdH##rdIpEkZ0a`b2!kf?o?}T?znBPay;K>h%=~FRpz)$ z_(bx{=Pw0yZB-g*P~yh!O9Loq`>Ma4fP6`IrhPX;-=qwcwO`$UN($Galg;=C3#^^YIJSI%>;Yg$jY){Yo_Y5kI=(4w;yYOXJS=bpOQOe77`;6dhPg=HR}Epo9gG6Jeqqqp=4!5%;npTO zP>z5be8lo*+0$3o`*X~c+HfQuI1S+_fuaK?x4f_dyxhV<7spS)46o(&ZJ(fYHKVe= zMcb~9D$hW}r%W>KW=WpW3JG+`YGr$BtAIsjczIOg+k3R4OMyPS{?=v}0$GnxJQ$Rt z>u&ZU-Y=#BGu`_|a7bJ7>DNwvq_}pQdlc?CbkDh_} z=(d4o=lKd!cp|G1VyAn%&2W~pD+PQ}pxL~LcJof^LSEW^l zTUR#+o^)Boyr9u*yGiwdJ|w}%RZq3M(&nNS|RuRU$K2f0Cz)S+MtgDuZ{B;lo)F*53wHU zEF{VK&O(67JNhEzYM)FiEIK_kpk+fVr;=91oyTsk(R}Z$!eX(`F_R_c+2wj@i%xqi z_a>pQl}pR+9|8=^GF!dlgp&SgwnO6mcF;e@?HNF+N5bypakcH!i(DGIFD?}`A-Jze ziv>BS#`~;qO6Pmq#CXlXnj9l1CG?ysq1?{}MwY@?rY~3A;&XT7*U8iaA$Bv#xsSXczm{94ww;Q#UZr$p1pTA0Il~uv%TNm^z#aji&W?2T*IDd z;zyFV`mvH&1I%8|RXgCPcL*-UN(a_;QM~Ljiy1HNSqq59pIvwJEc$INU#eCR{xJDH z{m+@+T7lx`1r{~%P;|~?;@r)Bbm}NL{3_^ApC7C8!#f)wttO$`-62?2h1-nVUxhp@Aas4gn^$h*8hY6hUqJ8fvglcmV`XF(nXnd27U>I*iJgrSzj~>d zEPH;+KXz0Y*;qB3*!{6CO!nNz1(u78t(Xf}Od4rNC^$N1xLmuAl6`}{C!oI;3&ct5 zQIXTGS}G_+deQWYlRom*?RyF1#Sa4GLyv>hMb%}RvIfrKs-}(!i@5rR=2`2xv=|QWE zC+6DYkVFoSHgY#jooHB!snZSeV?1-urz3v?Wi8=2ytSK>TNzuip8k%I>W}fCKs0^M ze}q7@x=+m0p7ln`=HBj$TQMUF>IbUq^hZ)H)wsKgR2|Z>1=jOl_NjRjMgr#}(G4d{N837M7*rhX$R_-7<4!%HccC&WS=Ba6Gl<_E1U zIac<%S{cr&Y~+q62?9>t0g+OQat36(Y;OP8{B9W%KOFoz;=E>2bCHTrDF>2ZFo#;9 zjXW0_R4vu9($LeRoi|;W7Zm~Lt5mq_B|=6Q4#ZsiKZ$-T#AP>T(d48WPiEXZ;#N*6ntUTcD^|#gK>BaY2h@j@J{!b ztFG5;9UB`P6&3FwACxGkCuhjUh+fE!q_UqGOk@-&4yvsOePnu2>*ukdU~}2I?pSP{ z*x=F8s&Hbfvb?Q%dS^~0*|2g25$a=DO7qZoXjAOdmTz#U<;oGNs-1zK2QlH*nh&jV z_Wsj?l^U0r&YKhc>XhE&6vbg%4mST%C}ry7=k~7XRBO8pZ!RFvj{@t)J;z#dvOGUa zx%;PaWoNUCS&YbSOD`Aq1c>&5Kwvb+fnWj;#%Eq;1<@6~JbiHzubiW4Zz;3b!#;Dn z{KMxyHO0y|S8h99H*zY3c>c%CF5SMj<)zZQHBJMrUAi;Ka1$J{>w%j|VN|2+vo!kPl@33H`%9Xl{Wh8B zUubiBCnmF82e{bpG4oo}sXyf(akw73cg0$n&42&$*|iU)udc0~d3ed{kJDE>MX&jA zr{&0|wd_$X}w^e`r3%Tk{{`~L%*1soLt1XZQ&I&Lvc)I$ztaD0e0sy{j BnC<`o literal 0 HcmV?d00001 diff --git a/docs/img/startpoint.png b/docs/img/startpoint.png index 455d1bf1f6b1663ea4b1d58e273c2fc91a5d12a9..8cf932580a8c75161e8eb17744c91b320b79ec03 100644 GIT binary patch literal 15434 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 Y{2lL{WnN#OIKQ!yyqa9evo|093r@5pn*aa+ diff --git a/docs/img/template.png b/docs/img/template.png new file mode 100644 index 0000000000000000000000000000000000000000..73ce81e107027d5ac9dd6140e391057ac71a154f GIT binary patch literal 3700 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 a@gDAJ`Q9wx_;Hr`la!deXr-`W!2bYbZ%%js literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index 1ad07ce3..8ac1ab6b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,12 +1,22 @@ -AskOmics is a visual SPARQL query interface supporting both intuitive data integration and querying while shielding the user from most of the technical difficulties underlying RDF and SPARQL +AskOmics is a visual SPARQL query interface supporting both intuitive data integration and querying while shielding the user from most of the technical difficulties underlying RDF and SPARQL. + +AskOmics helps loading heterogeneous data (formatted as tabular files, GFF, BED or native RDF files) into an RDF triplestore, to be transparently and interactively queried through a user-friendly interface. + +AskOmics also support federated queries to external SPARQL endpoints. + +![AskOmics](img/askograph.png){: .center} - AskOmics Tutorials - [Overview tutorial](tutorial.md): How to use AskOmics with example data - [Prepare your data](data.md): How to format your own data for AskOmics + - [Building a query](query.md): Learn how the query building interface works + - [Results interface](results.md): In-depth guide of the results interface + - [Template & forms](template.md): An overview of the templates & forms functionalities + - [SPARQL console](console.md): How to interact with the provided SPARQL console + - [Command-line interface](cli.md): A python-based CLI for interacting with AskOmics - [Build an RDF abstraction](abstraction.md): Learn how to build an RDF abstraction for RDF data - [Perform federated queries](federation.md): How to query your own data with external resources - [Use AskOmics with Galaxy](galaxy.md): How to connect AskOmics with your Galaxy history - - [Contribute as a user](issues.md): How to contribute to the development of AskOmics as a user

- Administration @@ -18,5 +28,5 @@ AskOmics is a visual SPARQL query interface supporting both intuitive data integ - Developer documentation - [Deploy a development instance locally](dev-deployment.md) - [Contribute to AskOmics](contribute.md) - - [CI](ci.md): Test code with continuous integration + - [CI](ci.md): Test your code with continuous integration - [Contribute to doc](docs.md): Write documentation diff --git a/docs/issues.md b/docs/issues.md deleted file mode 100644 index 942e038f..00000000 --- a/docs/issues.md +++ /dev/null @@ -1 +0,0 @@ -If you have an idea for a feature to add, or an approach for a bugfix, the best way to communicate with AskOmics developers is [Github issues](https://github.com/askomics/flaskomics/issues). Browse through existing issues to see if one seems related, or open a new one. diff --git a/docs/manage.md b/docs/manage.md index 9bec4562..a808ad41 100644 --- a/docs/manage.md +++ b/docs/manage.md @@ -1,4 +1,4 @@ -# Command line interface +# Make commands Several commands are available to help manage your instance. These commands are available through `make` when launched from the same directory as the *Makefile*. (If you are running Askomics in a docker container, you will need to connect to it to launch these commands) @@ -19,3 +19,33 @@ In the latter case, you will need to run the command twice (once for each namesp - `make clear-cache` This will clear the abstraction cache, making sure your data is synchronized with the new namespaces. + +# Administrator panel + +Administrators have access to a specific panel in AskOmics. +This Admin tab can be found after clicking on *Your Name ▾*. + +## User management + +From the Admin tab, administrators are able to: + +- Create a new user account +- Manage existing user accounts + - Blocking an user account + - Setting an user as an administrator + - Updating an user's individual storage quota + - Deleting an user + +They will also be able to check the last time of activity of an user. + +## Files + +A list of all uploaded files is available. Administrators can delete a file at any time. + +## Datasets + +All currently stored datasets are available. Administrators can publish, unpublish, and delete them. + +## Forms / Templates + +A list of **public** forms and templates is available. Administator can unpublish them if need be. diff --git a/docs/production-deployment.md b/docs/production-deployment.md index adae676d..843e0f30 100644 --- a/docs/production-deployment.md +++ b/docs/production-deployment.md @@ -1,4 +1,4 @@ -In production, AskOmics is deployed with docker and docker-compose. We provide `docker-compose.yml` templates to deploy your instance. +In production, AskOmics is deployed using docker and docker-compose. `docker-compose.yml` templates are provided to deploy your own instance. # Prerequisites @@ -81,11 +81,11 @@ All properties defined in `askomics.ini` can be configured via the environment v !!! warning Change `ASKO_flask_secret_key` and `ASKO_askomics_password_salt` to random string -For more information about AskOmics configuration, see [configuration](configure.md) section. +For more information about AskOmics configuration, see the [configuration](configure.md) section. #### First user -environment variables can also be used to create a user into AskOmics at first start. For this, use `CREATE_USER=true` User information can be configured with the following environment variables: +Environment variables can also be used to create a user into AskOmics at first start. For this, use `CREATE_USER=true` User information can be configured with the following environment variables: - `USER_FIRST_NAME`: User first name (default: Ad) - `USER_LAST_NAME`: User last name (default: Min) @@ -96,4 +96,4 @@ environment variables can also be used to create a user into AskOmics at first s - `GALAXY_API_KEY`: Galaxy URL linked to the user (optional) - `GALAXY_URL`: User Galaxy API Key (optional) -The user will be created only if the users table of the database is empty. \ No newline at end of file +The user will be created only if the users table of the database is empty. diff --git a/docs/query.md b/docs/query.md new file mode 100644 index 00000000..3da6233d --- /dev/null +++ b/docs/query.md @@ -0,0 +1,299 @@ +AskOmics aims to provide a simple interface able to create complex queries on linked entities. +The query interface is customized based on available (both personal and public) integrated data. + + +# Starting point + +Any entity integrated with the *starting entity* type can be used to start a query. Other entities can still be queried through a linked entity. +The starting entity will start with its label already set to 'visible'. + +![Startpoints](img/startpoint.png){: .center} +Once the start entity is chosen, the query builder is displayed. + +On the left-side of the query builder is the entity graph. Nodes (circles) represent *entities* and links represent *relations* between entities. +The currently selected entity is surrounded by a red circle. Dotted links and entities are not yet instantiated. + +![Query builder: Differential Expression is the *selected* entity, GeneLink is a *suggested* entity](img/query_builder.png){: .center} + + +# Filtering on attributes + +The selected entity's attributes are shown as attribute boxes on the right of the graph. + +!!! note "Info" + By default, every instantiated entity (directly or through a relation) has its **label** attribute set to visible (though it can be toggled off). + +Various filters are available to further refine the query, with each attribute type having its own filter: + +- Entity URI, entity label, and String attribute type: exact match or regular expression (equal or not equal) +- Numeric attribute type, FALDO start, FALDO end: comparison operators +- Category attribute type, FALDO reference, FALDO strand: Value selection among a list +- Date attribute type: comparison operators with a date picker +- Boolean attribute type: "True" or "False" selector + +![Attribute boxes for an entity](img/attributes.png){: .center} + +!!! note "Info" + Due to the way SPARQL works, any result row with an empty value for **any** of its column will not be shown. You can force the display of these rows by using the button. + +!!! tip + For the Category type, you can Ctrl+Click to either deselect a value, or select multiple values. + +!!! tip + For the Numeric and Date types, you can add filters by clicking on the "+" button. + + +## Additional customization + +In addition to the filter, several customization options are available for each attribute box. Depending on the attribute type, not all options will be available. + +![Additional customization](img/attribute_box.png){: .center} + +From left to right : + +- : Mark the attribute as a **form** attribute. More information [here](template.md#forms). +- : Link this attribute to another (on a different entity or the same one). + - *This will only show rows where both attributes have the same value*. +- : Show all values for this attribute, including empty values. +- : Exclude one or more categories, instead of including. + - *Select where the attribute is not XXX*. +- : Display the attribute value in the results. + +# Filtering on related entities + +To query on a linked entity, simple click on a suggested node. The linked node will be surrounded in a red circle, and the list of attributes on the right-hand side will change to show the new node's attributes. + +!!! note "Info" + Linking entity A (after filtering on parameter A1) to entity B (filtering on parameter B1) in the interface create the following query : + + - *List all entities A who match parameter A1 , AND are linked to any entity B matching parameter B1* + +## Explicit relations + +Explicit relations between entities (defined by the "@" symbol in CSV files, and the *"Parents"/"Derives_from"* relations from GFF files) will appears between related entities. If the relation is a *symetric* relation, it will appear twice between entities. + +## FALDO relations + +All *FALDO* entities will be linked by an implicit *Included_in* relation. This relation is slightly different than *explicit* relations: it relies on the *FALDO* attributes of both entities for the query, instead of a direct link. + + FALDO entities are represented with a green circle and FALDO relations have a green arrow. + +!!! Tip + You can customize the relation by clicking on the *Included in* relation. + + ![Customizing the 'Included in' relation](img/faldo.png){: .center} + +!!! note "Info" + Entity A is *Included_in* Entity B means: + + - **Entity A Start > Entity B Start** *AND* **Entity B End < Entity B End.** + + By default, the inequalities are **Strict**, but it can be changed from the interface. + +!!! Tip + If both entities have a defined *Reference* and/or *Strand* attribute, you will be able to select the **Same reference** and/or **Same strand** options. (Both are selected by default if available) + +!!! Tip + You can **reverse** the relation (Entity B *Included_in* Entity A instead of the opposite) from the interface. + +!!! Warning + *Included_in* queries are **ressource-intensive**, especially if you have a lot of instances for each entity. + +## Filtering displayed relations + +If there are too many related entities displayed, it can be difficult to select the entity you wish to link to. It is possible to filter the displayed relations on either the name of the entity, or the name of the link. + +Simply type out the name you want to filter in either the "Filter links" or the "Filter nodes" fields on the top of the graph. + +![Filtering displayed relations](img/filters.png){: .center} + +!!! Tip + You can stop the *Included_in* relations from being displayed by toggling the *Show FALDO relations* button. + +## Removing instanciated relations + +At any point, you can remove an instanciated node **(and any node linked to it)**, by selecting the node you wish to remove, and using the **Remove Node** button at the top of the interface. + + +# MINUS and UNION subqueries + +**Minus** and **Union** nodes are an additional way of querying on relations. Both nodes are applied to an entity as an additional filter. + +- Minus nodes remove results based on a subquery + - *Show all genes except those that match XXX* +- Union nodes act as a "OR" between multiple subqueries + - *Show all genes that match either XXX or YYY* + +Both type of nodes can be integrated in a bigger query + +- *List genes linked to a mRNA, except mRNA linked to another entity* +- *List genes linked to a mRNA with either attribute A set to XXX, or attribute B set to YYY* + +!!! note "Info" + *Right-click* on any **non-instanciated** node, and select the type of node you wish to use. + + ![Creating a minus or union node](img/custom_nodes.png){: .center} + +!!! note "Info" + The entity affected by the special node is linked with the *Union* or *Minus* relation. + +!!! warning + To avoid displaying empty columns or duplicates in the results, you should disable the *label* display for entities in the sub-queries. + + +## MINUS nodes + +*Minus* nodes **remove** from the results any instance that match a specific subquery. + +- **Display all genes that are not linked to an mRNA** +- **Display all genes that are not linked to an mRNA whose reference is XXX** + +![List all genes, except those linked to a mRNA](img/minus.png){: .center} + + +!!! tip + It's currently the only way to query on the **lack** of relation between entities. + Such as: *List all instances of entity A who are not linked with any instance of entity B* + +!!! note "Info" + To add a minus node on the relation between entities A and B + + - Entity A is currently selected + - Right-clicking on entity B and selecting "MINUS" add a new node to the graph + - Entity B is automatically instanciated and linked to the new minus node + - The query is now: *List all instances of entity A, without instances linked to entity B* + - *Optional*: Disable the display of the label for entity B + +The generated SPARQL query will look like this: + +```turtle +# Listing all genes NOT related to an mRNA by the 'Parent' relation. + +SELECT DISTINCT ?gene1_Label +WHERE { + ?gene1_uri rdf:type . + ?gene1_uri rdfs:label ?gene1_Label . + { + MINUS { + ?mRNA31_uri ?gene1_uri . + ?mRNA31_uri rdf:type . + } + } +} +``` + +!!! tip + You can customize the sub-query further: instead of simply removing any instance linked to a specific entity, you can remove all instances linked to a specific entity whose attribute A is XXX. + + - I want to list all instances of Entity A with the attribute A1 + - But I want to exclude all instances of Entity A that are linked to any instance of Entity B with the attribute B1 + + To create this query: + + 1) Instantiate Entity A with attribute A1 + 2) Right-click on Entity B and select "Convert to MINUS node" + 3) Select entity B + 4) Select attribute B1 for entity B + +!!! note "Info" + You can create multiple *minus* nodes starting from the same entity. Instances that match **any** of the sub-queries will be removed. + + - I want to remove instances that match *condition A* + - I also want to remove instances that match *condition B* + + This is not the same as removing instances that match *condition A* **and** *condition B* + To do so, you will need to add conditions to an existing *minus* node instead of creating a new one. + +!!! Warning + While nested MINUS nodes are possible, the generated query might not be what you would expect. Make sure to check the generated SPARQL query in the results page if the results are not what you expected. + +## UNION node + +UNION nodes implement the conditional **OR**. + +- Display transcripts that are either + - Linked to a 3′-UTR + - Linked to a 5′-UTR + +![List all transcripts linked to either a 3′-UTR or a 5′-UTR](img/union.png){: .center} + +!!! note "Info" + Due to the way UNION nodes works, entities will appear once for each matching sub-query. This can lead to a duplication of results. + This can be solved by setting the label of the entities in the subquery to **Not visible** () + ![Duplicated results](img/union_duplicated.png){: .center} + +!!! Warning + UNION nodes will only behave as a UNION if there is more than one entity linked to it + +!!! note "Info" + To add an union node on the relations between entities A, B and C + + - Entity A is currently selected + - Right-clicking on entity B and selecting "UNION" add a new node to the graph + - Entity B is instanciated and linked to the new union node + - Click on Entity B and set the label to "Not visible" + - For now, the *union* node has no effect + - Click on the *union* node, and then click on entity C to instanciate it + - Set the label to "Not visible" + - The sparql query is now: *List all instances of entity A that are either linked to entity B or entity C* + + +The generated SPARQL query will look like this: + +```turtle +# List all genes with an ortholog who has either the attribute "organism" set to "Arabidopsis thaliana", or the "reference" attribute set to "Chra01" +# Two sub-queries linked with an UNION + +SELECT DISTINCT ?gene1_Label ?gene3_Label ?gene4_Label +WHERE { + ?gene1_uri rdf:type . + ?gene1_uri rdfs:label ?gene1_Label . + { + { + ?gene1_uri ?gene26_uri . + ?gene26_uri rdf:type . + ?gene26_uri rdfs:label ?gene3_Label . + ?gene26_uri faldo:location/faldo:begin/faldo:reference ?gene26_chromosomeCategory . + VALUES ?gene26_chromosomeCategory { } + } + UNION { + ?gene1_uri ?gene64_uri . + ?gene64_uri rdf:type . + ?gene64_uri rdfs:label ?gene4_Label . + ?gene64_uri ?gene64_organismCategory . + VALUES ?gene64_organismCategory { } + } + } +} +``` + +!!! tip + You can customize the sub-query further. + I want to list all instances of Entity A that are either: + + - Linked to entity B with attribute B1 + - Linked to entity C, itself linked to entity D + + +!!! note "Info" + Sub-sub-queries (entities linked to the UNION node) can be as complex as you want : + + - *I want instances of entity A either linked to entities B with attribute B1, or linked entities C linked with entity D* + +!!! Warning + Just as MINUS node, nested UNION nodes might not behave as you want. Make sure to check the generated SPARQL query in the results page if the results appear to be strange. + + +## Removing special nodes + +Much like a "normal" node, you can remove special nodes (and any node linked to it) at any time by selecting it, and using the **Remove Node** button. + + +# Launching queries + +Once you are satisfied with your query, you can either: + +- Preview the results (*at most 30 rows*) with Run & Preview +- Send a full query with Run & save + +In the case of a full query, you will be able to access the query results (and more) on the [results page](results.md) diff --git a/docs/requirements.txt b/docs/requirements.txt index 591f7c09..2dfb00fb 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,2 @@ mkdocs==1.0.4 +markdown-captions==2 diff --git a/docs/results.md b/docs/results.md new file mode 100644 index 00000000..438ce8fe --- /dev/null +++ b/docs/results.md @@ -0,0 +1,79 @@ +On the Results page, you will be able to see all your saved results (after using the Run & save button). Each row stores both the query and its results. + +# General information + +Result information can be found for each row : + +- Creation data: The creation time of this result +- Exec time: The running time of the linked query +- Status: Current status of the query + - Possible values are 'Success', 'Queued', 'Started', 'Failure' and 'Deleting' +- Rows: Number of rows in the result +- Size: Size of the result file + +## Description + +Each description can be customized by clicking on the field, and entering the desired value. You can use this to identify the query related to this result. + +!!! Warning + Don't forget to save your new description using **Enter** + +!!! note "Info" + The description will be displayed on the main page if you transform this query in a [template or form](template.md). + +# Templates and forms + +You can use the available toggle buttons if you wish to create a [template or form](template.md). + +!!! Warning + Form creation is restricted to administrators. The related query must also be a [form-able query](template.md#Forms). + +# Publication + +The 'Public' toggle is available if you are an administrator. If will automatically create a public form (if the result is form-able), or a template. They will be accessible to **all users** from the main page. + +!!! Tip + Make sure to set a custom description (and [customize your form](template.md#editing-the-form-display), if relevant) to help users understand your template/form. + +# Actions + +Several actions are also available for each result : + +## Preview + +Preview directly re-launch the related query, and print a preview of the results. +The preview will be shown under the results table. + +## Download + +Clicking on Download will let you download a CSV file containing the results. + +## Form + +Clicking on Form will let you customize the related form display. + +!!! Warning + Only available for administator and form-able results. + +## Redo + +Clicking on Redo will let you directly replay the query from the query interface. It will be in the exact same state as when you clicked on Run & save. + +!!! Warning + Only available results generated from the query interface. + +## Sparql + +Clicking on Sparql will redirect you to the [SPARQL console](console.md). You will be able to browse the SPARQL code generated by your query. + +!!! note "Info" + Depending on your AskOmics configuration, you might be able to directly customize the query and launch it from the console. + +![SPARQL query generated by AskOmics](img/sparql.png){: .center} + +# Deletion + +To delete one or more results, simply select them in the table, and use the "Delete" button at the bottom of the table. + +!!! Warning + This will delete any template or form generated from the result. diff --git a/docs/style.css b/docs/style.css index 686c23c6..7b96bac7 100644 --- a/docs/style.css +++ b/docs/style.css @@ -19,11 +19,15 @@ btn { } btn.white { + border: 2px solid #6c757d; background-color: white; - color: #5a6268; - padding-right: 5px; - padding-left: 5px; - border-radius: 5px; + color: #6c757d; + cursor: pointer; +} + +btn.white:hover { + color:white; + background: #6c757d; } badge { @@ -70,3 +74,14 @@ div.admonition.quote > p.admonition-title { color: #007bff; } +.center { + display: block; + max-width: 100%; + height: auto; + margin: auto; + float: none!important; + text-align: center; + font-style: italic; +} + +.fa { display:inline; } diff --git a/docs/template.md b/docs/template.md new file mode 100644 index 00000000..e9692bcb --- /dev/null +++ b/docs/template.md @@ -0,0 +1,98 @@ +One of the goal of AskOmics is to enable re-use and customization of complex queries shared by other users. +This functionality is available in two different ways. + +# Templates + +Templates are a simple way for yourself or another user to replay your queries. They will access a **copy** of the query graph, and be able to customize all parameters and relations (this includes linking to additional entities). + +Templates will appears on the right-side of the homepage. The description of the template can be customized to include more information regarding the query. + +!!! Warning + By default, your templates are private. To publicize them, you need administrator privileges. + +## Creating a template + +To create a template, first create the query you want, and Run & save. +Then, in the result page, you can toggle the 'template' attribute to create the template. + +## Editing the template description + +You can modify the template description the same way you would edit the result description. Simple click on the value of the 'Description' column, and enter your own description. + +!!! Warning + Don't forget to save with the **Enter** key. + +## Using the template + +On the AskOmics homepage, simply click on the template of interest to access the query interface. From there, you can simply interact with it as you would with a normal query, including saving the results. + +![Template display on the home page](img/template.png){: .center} + +!!! note "Info" + Any change to the query will not affect the template. + +## Removing the template + +To remove the template, you can either toggle the 'template' attribute back to *off*, or delete the result as a whole. + +# Forms + +Whereas templates allow users to completely replay your queries (including modifying the query graphs), forms aim to be a much simpler way to share your queries for users not familiar with the way AskOmics works. + +!!! Warning + Forms are restricted to administrators. + +When using a form, other users will only able to change the values for a set of parameters you will have selected beforehand. + +For instance, if your query is *List all users whose favorite color is red*, users will only be able to change the favorite color, before sending the query. + +![Example of a form: the user can only change the favorite color](img/form_example.png){: .center} + +In addition, you will be able to customize the form before sharing it, such as changing the description (much like templates), but also the name of each entity and attribute. + +Like templates, forms will appear on the right side of the homepage, with the chosen description. + +!!! Warning + Users will only be able to interact with *form attributes*. They will not be able to affect or change visibility for other attributes. + +!!! Warning + A given **Result** can be either a form or a template, but not both. + +## Creating a form + +To create a form, you will first to create a **form-ifiable** query. To do so, start with a regular query, and toggle the template ( ) button on all attributes of interest. Selected attributes will be modifiable in the form. + +After saving the query with Run & save, head to the *Results* page. +You will be able to toggle the 'Form' button, creating the form. + +!!! Warning + The form creation option is restricted to form-ifiable (meaning, with at least one selected attribute) queries. + +## Editing the form description + +You can modify the form description the same way you would edit the result (and template) description. Simple click on the value of the 'Description' column, and enter your own description. + +!!! Warning + Don't forget to save with the **Enter** key. + +## Editing the form display + +For each entity with a form attribute, the form will display both the entity name, and each attribute label (in addition to the input field). You can customize this display (for instance, changing the display entity name to something more readable, or changing an attribute label to make your query more explicit) + +To do so, simply click on Form to access the *form editing interface*. +You can then simply edit entities and attributes labels, and click on Save to save the new display. + +![Form editing interface](img/form_edit.png){: .center} + +!!! Warning + The query results will not be affected by these changes (the column name will still match the default attribute label). + +## Using the form + +On the AskOmics homepage, simply click on the form of interest to access the interface. + +![Form display on the home page](img/form.png){: .center} + +## Removing the form + +To remove the form, you can either toggle the 'form' attribute back to *off*, or delete the result as a whole. diff --git a/docs/tutorial.md b/docs/tutorial.md index 0f3f5e7c..0059027f 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1,16 +1,20 @@ AskOmics is a web software for data integration and query using the Semantic Web technologies. It helps users to convert multiple data sources (CSV/TSV files, GFF and BED annotation) into RDF triples, and perform complex queries using a user-friendly interface. -In this tutorial, we will learn the basics of AskOmics by analyses RNA-seq results. The data comes from a differential expression analysis and are provided for you. 4 files will be used in this tutorial: +In this tutorial, we will learn the basics of AskOmics by analysing RNA-seq results. The provided datasets come from a differential expression analysis. + +4 files will be used in this tutorial: - [Differentially expressed results file](https://zenodo.org/record/2529117/files/limma-voom_luminalpregnant-luminallactate): genes in rows, and 4 required columns: identifier (ENTREZID), gene symbol (SYMBOL), log fold change (logFC) and adjusted P values (adj.P.Val) - [Reference genome annotation file](https://zenodo.org/record/3601076/files/Mus_musculus.GRCm38.98.subset.gff3) in GFF format -- [Correspondence file between gene symbol and Ensembl id](https://zenodo.org/record/3601076/files/symbol-ensembl.tsv): TSV of two columns: symbol and the corresponding Ensembl id +- [Correspondence file between gene symbol and Ensembl id](https://zenodo.org/record/3601076/files/symbol-ensembl.tsv): TSV with two columns: symbol and the corresponding Ensembl id - [QTL file](https://zenodo.org/record/3601076/files/MGIBatchReport_Qtl_Subset.txt): QTL in row, with 5 required columns: identifier, chromosome, start, end and name -Throughout the guide, you will find Hands-on containing tutorial instruction to perform in order to get started with AskOmics. +Throughout the guide, you will find Hands-on sections containing instructions to perform in order to get started with AskOmics. To complete the tutorial, you will need an AskOmics instance. You can [install your own](production-deployment.md) or use this [public instance](https://use.askomics.org). +A Galaxy Training tutorial is also available [here](https://training.galaxyproject.org/training-material/topics/transcriptomics/tutorials/rna-seq-analysis-with-askomics-it/tutorial.html) + # Account creation and management @@ -22,18 +26,18 @@ AskOmics is a multi-user platform. To use it, you will need an account on the in Create your AskOmics account (or login with your existing one) -Once your are logged, you can use all the functionalities of AskOmics. +Once you are logged, you can use all the functionalities of AskOmics. ## Manage your account -To manage your account, use the Account management tab by clicking on Your Name ▾ on the navigation bar. +To manage your account, use the Account management tab by clicking on *Your Name ▾* on the navigation bar. -Uses the forms to change your personal information. +You can use the forms to change your personal information. # Data integration -AskOmics convert project specific data into RDF triples automatically. It can convert CSV/TSV, GFF and BED files. +AskOmics will convert project specific data into RDF triples automatically. It can convert CSV/TSV, GFF and BED files. !!! Hands-on Download the files for the tutorial using the following links:
@@ -55,9 +59,9 @@ You can upload files from your computer, or distant files using an URL. !!! Tip You can also copy files URL and use the URL button. -Uploaded files are displayed into the files table. Filenames can be change by clicking on it. +Uploaded files are displayed into the files table. Filenames can be changed by clicking on it. -![files_table](img/files_table.png "files_table") +![Files table](img/files_table.png "files_table"){: .center} Next step is to convert this files into RDF triples. This step is called *Integration*. Integration will produce a RDF description of your data: the *Abstraction*. @@ -67,18 +71,22 @@ Next step is to convert this files into RDF triples. This step is called *Integr ## Integration +Detailed information regarding the *Integration* step can be found [here](data.md). + The *integration* convert input files into RDF triples, and load them into an RDF triplestore. AskOmics can convert CSV/TSV, GFF3 and BED files. During the step of integration, AskOmics show a preview of each files. We can choose how the file will be integrated at this step. +More information about data integration is available [here](data.md) + ### GFF -GFF files contain genetic coordinate of entities. Each entities contained in the GFF file are displayed on the preview page. We can Select the entities that will be integrated. +GFF files contain genetic coordinate of entities. Each entity contained in the GFF file is displayed on the preview page. We can Select the entities that will be integrated. !!! Hands-on 1. Search for `Mus_musculus.GRCm38.98.subset.gff3 (preview)` 2. Select `gene` and `mRNA` 3. Integrate (Private dataset) - ![De results preview](img/gff_preview.png) + ![Integration interface for GFF files](img/gff_preview.png){: .center} ### CSV/TSV @@ -86,7 +94,7 @@ GFF files contain genetic coordinate of entities. Each entities contained in the The TSV preview show an HTML table representing the TSV file. During integration, AskOmics will convert the file using the header. -The first column of a TSV file will be the *entity* name. Other columns of the file will be *attributes* of the *entity*. *Labels* of the *entity* and *attributes* will be set by the header. This *labels* can be edited by clicking on it. +The first column of a TSV file will be the *entity* name. Other columns of the file will be *attributes* of the *entity*. *Labels* of the *entity* and *attributes* will be set by the header. The column names can be edited by clicking on it. Entity and attributes can have special types. The types are defined with the select below the header. An *entity* can be a *start entity* or an *entity*. A *start entity* mean that the entity may be used to start a query. @@ -95,6 +103,7 @@ Attributes can take the following types: - Numeric: if all the values are numeric - Text: if all the values are strings +- Date: if all the values are dates - Category: if there is a limited number of repeated values If the entity describe a locatable element on a genome: @@ -116,7 +125,7 @@ A columns can also be a relation between the *entity* to another. In this case, - Keep the other column names and set their types to *numeric* 3. Integrate (Private dataset) - ![De results preview](img/de_results_preview.png) + ![Integration interface for CSV files](img/de_results_preview.png){: .center} !!! Hands-on 1. Search for `symbol-ensembl.tsv (preview)` @@ -125,7 +134,7 @@ A columns can also be a relation between the *entity* to another. In this case, - change `ensembl` to `linkedTo@gene` and set type to *Directed relation* 3. Integrate (Private dataset) - ![Symbol to Ensembl preview](img/symbol_to_ensembl_preview.png) + ![Modifying the name and type of a column](img/symbol_to_ensembl_preview.png){: .center} !!! Hands-on 1. Search for `MGIBatchReport_Qtl_Subset.txt (preview)` @@ -136,7 +145,7 @@ A columns can also be a relation between the *entity* to another. In this case, - set `End` type to *End* 3. Integrate (Private dataset) - ![QTL preview](img/qtl_preview.png) + ![Preview of the QTL file](img/qtl_preview.png){: .center} ### Manage integrated datasets @@ -148,11 +157,11 @@ Integration can take some times depending on the file size. The Datasets page 2. Wait for all datasets to be *success* - ![dataset](img/datasets.png "Datasets table") + ![Table of datasets](img/datasets.png "Datasets table"){: .center} -The table show all integrated datasets. The *status* column show if the datasets is fully integrated or in the process of being integrated. +The table show all integrated datasets. The *status* column show if the datasets are fully integrated or in the process of being integrated. @@ -160,13 +169,15 @@ The table show all integrated datasets. The *status* column show if the datasets Once all the data of interest is integrated (converted to RDF graphs), its time to query them. Querying RDF data is done by using the SPARQL language. Fortunately, AskOmics provides a user-friendly interface to build SPARQL queries without having to learn the SPARQL language. +More information about the query building process is available [here](query.md) + ## Query builder overview ### Simple query The first step to build a query is to choose a start point for the query. -![ask](img/startpoint.png) +![List of startpoints](img/startpoint.png){: .center} !!! Hands-on @@ -178,19 +189,19 @@ The first step to build a query is to choose a start point for the query. Once the start entity is chosen, the query builder is displayed. -The query builder is composed of a graph. Nodes represents *entities* and links represents *relations* between entities. The selected entity is surrounded by a red circle. links and other entities are dotted and lighter because there are not instantiated. +The query builder is composed of a graph. Nodes represents *entities* and links represents *relations* between entities. The selected entity is surrounded by a red circle. Links and other entities are dotted and lighter because there are not instantiated. -![query builder](img/query_builder.png "Query builder, Differential Expression is the selected entity, GeneLink is a suggested entity") +![The query builder](img/query_builder.png "Query builder, Differential Expression is the selected entity, GeneLink is a suggested entity"){: .center} On the right, attributes of the selected entity are displayed as attribute boxes. Each boxes have an eye icon. Open eye mean the attribute will be displayed on the results. !!! Hands-on 1. Display `logFC` and `adj.P.val` by clicking on the eye icon - 2. Run & Preview + 2. Run & preview -![preview results](img/preview_results.png "Results preview") +![Preview of results](img/preview_results.png "Results preview"){: .center} - Run & Preview launch the query with a limit of 30 rows returned. We use this button to get an idea of the results returned. + Run & preview launch the query with a limit of 30 rows returned. We use this button to get an idea of the results returned. ### Filter on attributes @@ -200,38 +211,39 @@ Next query will search for all over-expressed genes. Genes are considered over-e !!! Hands-on 1. Filter `logFC` with `>` `2` 2. Filter `adj.P.val` with `≤` `0.05` - 2. Run & Preview + 2. Run & preview Results show only significantly over-expressed genes. ### Filter on relations -Now that we have our genes if interest, we will link these genes to the reference genome to get information about location. +Now that we have our genes if interest, we will link these genes to the reference genome to get information about the location. -To constraint on relation, we have to click on suggested nodes, linked to our entity of interest. +To constraint on a relation, we have to click on any suggested nodes linked to our entity of interest. !!! Hands-on 1. First, hide `Label`, `logFC` and `adj.P.val` of `Differential Expression` 2. Instantiate `GeneLink`, and hide `Label` 3. Instantiate `gene` - 2. Run & Preview + 2. Run & preview -Results now show the Ensembl id of our over-expressed genes. We have now access to all the information about the `gene` entity containing on the GFF file. for example, we can filter on chromosome and display chromosome and strand to get information about gene location. +Results now show the Ensembl id of our over-expressed genes. We have now access to all the information about the `gene` entity containing on the GFF file. For example, we can filter on chromosome and display chromosome and strand to get information about the gene location. !!! Hands-on 1. Show `reference` and `strand` using the eye icon 2. Filter `reference` to select `X` and `Y` chromosomes (use `ctrl`+`click` to multiple selection) - 2. Run & Preview + 2. Run & preview ### Use FALDO ontology to query on the position of elements on the genome. -The [FALDO](https://bioportal.bioontology.org/ontologies/FALDO) ontology describe sequence feature positions and regions. AskOmics use FALDO ontology to represent entity positions. GFF are using FALDO, as well as TSV entities with chromosome, strand, start and end. +The [FALDO](https://bioportal.bioontology.org/ontologies/FALDO) ontology describe sequence features's positions and regions. AskOmics use the FALDO ontology to represent entity positions. +All entities extracted from GFF and BED files use this ontology, in addition to any entity extracted from a CSV/TSV file with a reference, strand, start and end columns. -The FALDO ontology are used in AskOmics to perform special queries between 2 FALDO entities. These queries are: +The FALDO ontology is used in AskOmics to perform special queries between 2 FALDO entities. These queries are: -- Entity included in another entity -- Entity overlapping another one +- Entity is included in another entity +- Entity is overlapping another entity On the query builder interface, FALDO entities are represented with a green circle and FALDO relations have a green arrow. @@ -241,19 +253,19 @@ On the query builder interface, FALDO entities are represented with a green circ 3. Instantiate `QTL` 4. Click on the link between `gene` and `QTL` to edit the relation 5. check that the relation is `gene` `included in` `QTL` `on the same reference` with `strict` ticked - 7. Run & Preview + 7. Run & preview To go further, we can filter on `QTL` to refine the results. !!! Hands-on - 1. got back to the `QTL` node + 1. Go back to the `QTL` node 2. Show the `Name` attribute using the eye icon 3. Filter the name with a `regexp` with `growth` - 4. Run & Preview + 4. Run & preview -From now, our query is "All Genes that are over-expressed (logFC > 2 and FDR ≤ 0.05) and located on a QTL that are related to growth" This is the results that we are looking for. So we can save it. +From now, our query is "All Genes that are over-expressed (logFC > 2 and FDR ≤ 0.05) and located on a QTL that are related to growth" This is the results that we are looking for. We can now save it. !!! Hands-on 1. Run & save @@ -266,21 +278,36 @@ The results page store the saved queries. A table show some useful information a !!! Hands-on 1. Click on the name and enter `Over-expressed genes on a growth QTL` - 2. press `enter` key + 2. Press the `Enter` key - ![results table](img/results_table.png) + ![Table of results](img/results_table.png){: .center} -The **Action** column contain button to perform certain action: +The **Action** column contain buttons to perform certain action: -- Preview: show a results preview on the bottom of the table +- Preview: Show a results preview on the bottom of the table - Download: Download the results (TSV file) - Edit: Edit the query with the query builder -- SPARQL: edit the query with a SPARQL editor for advanced users +- SPARQL: Access the generated SPARQL query for the result + +For more information about the Results page, please head [here](results.md) !!! Hands-on - 1. Download the results file on your computer using Download button + 1. Download the results file on your computer using the Download button. +The Edit button can be used to simply replay the query after changing some parameters. + +!!! Hands-on + 1. Edit the query, and replace "growth" with another term of interest (such as "anxiety"). + 2. Preview the results + + +## Advanced queries + +Advanced queries, including *UNION* and *MINUS* SPARQL queries are also available to further your queries. +Please head [here](query.md#minus-and-union-nodes) for more information. # Conclusion -In this tutorial we have seen how to use AskOmics Interactive Tool to Build a complex SPARQL query to interrogate 4 different datasets and answer a biological question. \ No newline at end of file +In this tutorial we have seen how to use the AskOmics Interactive Tool, building a complex SPARQL query to interrogate 4 different datasets and answer a biological question. + +This tutorial was a brief overview of AskOmics's functionalities. Please check the other categories on the left-side for more information. diff --git a/mkdocs.yml b/mkdocs.yml index 3d88af34..f10f9490 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,7 +3,7 @@ repo_url: https://github.com/askomics/flaskomics edit_uri: edit/master/docs/ site_author: Xavier Garnier -theme: +theme: name: readthedocs highlightjs: true hljs_languages: @@ -12,6 +12,8 @@ theme: - javascript markdown_extensions: - admonition + - attr_list + - markdown_captions - toc: permalink: true toc_depth: 1 @@ -21,12 +23,16 @@ nav: - Home: index.md - AskOmics tutorials: - Overview tutorial: tutorial.md - - Prepare data: data.md + - Preparing data: data.md + - Building a query: query.md + - Managing results: results.md + - Templates & forms: template.md + - SPARQL console: console.md + - Command-line interface: cli.md - Build RDF Abstraction: abstraction.md - Federated queries: federation.md - AskOmics and Galaxy: galaxy.md - - Contribute: issues.md - - Admin : + - Administrator guide: - Deploy AskOmics: production-deployment.md - Configure: configure.md - Manage: manage.md @@ -34,7 +40,7 @@ nav: - Dev deployment: dev-deployment.md - Contribute: contribute.md - CI: ci.md - - Doc: docs.md + - Documentation: docs.md - Bug Tracker: https://github.com/askomics/flaskomics/issues - Project monitoring: https://github.com/askomics/flaskomics/projects From b6f1d22bb762a4b020c71e0875c67f7a4a9ebd14 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 5 Oct 2021 12:00:48 +0200 Subject: [PATCH 103/318] Add tooltips to buttons in the query interface (#285) Add tooltips --- askomics/react/src/routes/form/attribute.jsx | 75 +++----------- askomics/react/src/routes/form/query.jsx | 14 ++- .../react/src/routes/form_edit/attribute.jsx | 73 +++----------- askomics/react/src/routes/form_edit/query.jsx | 16 ++- askomics/react/src/routes/query/attribute.jsx | 98 ++++--------------- askomics/react/src/routes/query/query.jsx | 12 +++ docs/data.md | 2 +- package-lock.json | 16 +++ package.json | 1 + 9 files changed, 100 insertions(+), 207 deletions(-) diff --git a/askomics/react/src/routes/form/attribute.jsx b/askomics/react/src/routes/form/attribute.jsx index 07cf5108..7581a5da 100644 --- a/askomics/react/src/routes/form/attribute.jsx +++ b/askomics/react/src/routes/form/attribute.jsx @@ -79,57 +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 = ( - - - - - -
- - {Object.keys(selected_sign).map(type => { - return - })} - - - - Please filter with a valid URI or CURIE -
- ) - } - - return ( -

- ) - } - renderText () { let eyeIcon = 'attr-icon fas fa-eye-slash inactive' @@ -198,8 +147,8 @@ export default class AttributeBox extends Component {
- {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}
@@ -267,8 +216,8 @@ export default class AttributeBox extends Component {
- - + +
{form}
@@ -317,10 +266,10 @@ export default class AttributeBox extends Component { return (
-
- - - +
+ + +
{form}
@@ -363,8 +312,8 @@ export default class AttributeBox extends Component {
- - + +
{form}
@@ -440,8 +389,8 @@ export default class AttributeBox extends Component {
- - + +
{form}
diff --git a/askomics/react/src/routes/form/query.jsx b/askomics/react/src/routes/form/query.jsx index 6189148f..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 ------- @@ -434,6 +436,15 @@ 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 => { @@ -504,6 +515,7 @@ export default class FormQuery extends Component {
{Entities} + {tooltips}
diff --git a/askomics/react/src/routes/form_edit/attribute.jsx b/askomics/react/src/routes/form_edit/attribute.jsx index f73a45c4..eb85d041 100644 --- a/askomics/react/src/routes/form_edit/attribute.jsx +++ b/askomics/react/src/routes/form_edit/attribute.jsx @@ -79,57 +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 = ( - - - - - -
- - {Object.keys(selected_sign).map(type => { - return - })} - - - - Please filter with a valid URI or CURIE -
- ) - } - - return ( -
- -
- -
- {form} -
- ) - } - renderText () { let eyeIcon = 'attr-icon fas fa-eye-slash inactive' @@ -200,8 +149,8 @@ export default class AttributeBox extends Component {
- {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}
@@ -269,8 +218,8 @@ export default class AttributeBox extends Component {
- - + +
{form}
@@ -320,9 +269,9 @@ export default class AttributeBox extends Component {
- - - + + +
{form}
@@ -365,8 +314,8 @@ export default class AttributeBox extends Component {
- - + +
{form}
@@ -442,8 +391,8 @@ export default class AttributeBox extends Component {
- - + +
{form}
diff --git a/askomics/react/src/routes/form_edit/query.jsx b/askomics/react/src/routes/form_edit/query.jsx index 67f4cea4..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,6 +387,15 @@ 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 => { @@ -430,8 +441,8 @@ export default class FormEditQuery extends Component { }) // buttons - - + + let saveButton = // preview @@ -453,6 +464,7 @@ export default class FormEditQuery extends Component {
{Entities} + {tooltips}
diff --git a/askomics/react/src/routes/query/attribute.jsx b/askomics/react/src/routes/query/attribute.jsx index fd114943..b6cb21ca 100644 --- a/askomics/react/src/routes/query/attribute.jsx +++ b/askomics/react/src/routes/query/attribute.jsx @@ -3,6 +3,7 @@ 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' @@ -81,65 +82,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 = ( - - - - - -
- - {Object.keys(selected_sign).map(type => { - return - })} - - - - Please filter with a valid URI or CURIE -
- ) - } - - return ( -
- -
- {this.props.config.user.admin ? : } - - -
- {form} -
- ) - } - renderText () { let formIcon = 'attr-icon fas fa-bookmark inactive' @@ -213,10 +155,10 @@ export default class AttributeBox extends Component {
- {this.props.config.user.admin ? : } - - {this.props.attribute.uri == "rdf:type" || this.props.attribute.uri == "rdfs:label" ? : } - + {this.props.config.user.admin ? : } + + {this.props.attribute.uri == "rdf:type" || this.props.attribute.uri == "rdfs:label" ? : } +
{form}
@@ -289,10 +231,10 @@ export default class AttributeBox extends Component {
- {this.props.config.user.admin ? : } - - - + {this.props.config.user.admin ? : } + + +
{form}
@@ -347,11 +289,11 @@ export default class AttributeBox extends Component {
- {this.props.config.user.admin ? : } - - - - + {this.props.config.user.admin ? : } + + + +
{form}
@@ -399,10 +341,10 @@ export default class AttributeBox extends Component {
- {this.props.config.user.admin ? : } - - - + {this.props.config.user.admin ? : } + + +
{form}
@@ -485,8 +427,8 @@ export default class AttributeBox extends Component {
{this.props.config.user.admin ? : } - - + +
{form}
diff --git a/askomics/react/src/routes/query/query.jsx b/askomics/react/src/routes/query/query.jsx index 9ea51258..a516105a 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/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 Visualization from './visualization' import AttributeBox from './attribute' import LinkView from './linkview' @@ -866,6 +867,7 @@ export default class Query extends Component { waiting: waiting }) console.log(this.graphState) + ReactTooltip.rebuild(); } initGraph () { @@ -1462,6 +1464,15 @@ export default class Query extends Component { 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 @@ -1606,6 +1617,7 @@ export default class Query extends Component { {uriLabelBoxes} {AttributeBoxes} {linkView} + {tooltips}
diff --git a/docs/data.md b/docs/data.md index 477ee820..b06597b1 100644 --- a/docs/data.md +++ b/docs/data.md @@ -3,7 +3,7 @@ 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 do not delete related datasets. + Deleting files does not delete related datasets. # Data visibility diff --git a/package-lock.json b/package-lock.json index 520b88ae..567eb622 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8817,6 +8817,22 @@ "refractor": "^3.2.0" } }, + "react-tooltip": { + "version": "4.2.21", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-4.2.21.tgz", + "integrity": "sha512-zSLprMymBDowknr0KVDiJ05IjZn9mQhhg4PRsqln0OZtURAJ1snt1xi5daZfagsh6vfsziZrc9pErPTDY1ACig==", + "requires": { + "prop-types": "^15.7.2", + "uuid": "^7.0.3" + }, + "dependencies": { + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + } + } + }, "react-transition-group": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", diff --git a/package.json b/package.json index 54b3bbe7..479003cc 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "react-router-dom": "^5.2.0", "react-simple-code-editor": "^0.11.0", "react-syntax-highlighter": "^15.4.3", + "react-tooltip": "^4.2.21", "reactstrap": "^8.6.0", "style-loader": "^1.2.1", "url-loader": "^4.1.0" From 6ddea25a458e59e3acdf06c233714ebea75f2dc5 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 5 Oct 2021 15:05:31 +0200 Subject: [PATCH 104/318] Docs for abstration changes (#286) --- docs/abstraction.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/abstraction.md b/docs/abstraction.md index c61b3940..428367e6 100644 --- a/docs/abstraction.md +++ b/docs/abstraction.md @@ -16,6 +16,7 @@ PREFIX prov: PREFIX rdf: PREFIX rdfs: PREFIX xsd: +PREFIX dcat: ```
@@ -144,12 +145,16 @@ faldo:start and faldo:end are numeric attributes. # 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 `ÈntityTarget`, with the label *relation_example*, will be defined as follows: ```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 . +_:relation_node askomics:uri :RelationExample . +_:relation_node a askomics:AskomicsRelation . +_:relation_node a owl:ObjectProperty . +_:relation_node rdfs:label "relation_example" . +_:relation_node rdfs:domain :EntitySource . +_:relation_node rdfs:range :EntityTarget . +# Optional information for future-proofing +_:relation_node dcat:endpointURL . +_:relation_node dcat:dataset . ``` From 3973fd62d11cb7985cf8d3a4a4a80fae4317c472 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 5 Oct 2021 16:39:26 +0200 Subject: [PATCH 105/318] Changelog & doc --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ docs/data.md | 28 +++++++++++++++++----------- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6897dd9..a1574f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This changelog was started for release 4.2.0. +## Unreleased + +### 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) + +### 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) + +### Changed + +- Changed "Query builder" to "Form editor" in form editing interface +- Changed abstraction building method for relations. (Please refer to #248 and #268) +- Changed abstraction building method for 'strand': only add the required strand type, and not all three types (#277) +- Updated documentation + +### Removed + +- Removed "Remote endpoint" field for non-ttl file +- Removed "Custom_uri" field for ttl file + +### Security + +- Bump prismjs from 1.23.0 to 1.25.0 +- 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 ## [4.3.1] - 2021-06-16 diff --git a/docs/data.md b/docs/data.md index b06597b1..88d8253d 100644 --- a/docs/data.md +++ b/docs/data.md @@ -39,17 +39,6 @@ The first column of the file will manage the entity itself : the column name wil !!! Warning Unless you are trying to merge entities, make sure your URIs are unique across **both your personal and public datasets**. -### Entity label - -The values of the first column will also be transformed into the generated instances's label. - -* 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*. - ### 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. @@ -65,6 +54,23 @@ To setup inheritance, the **column name** needs to be formated as follows: !!! 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. From 6cbc3164509b4e41e2c6f25cf11465182124fb7e Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 5 Oct 2021 16:42:49 +0200 Subject: [PATCH 106/318] Changelog & doc (#287) --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ docs/data.md | 28 +++++++++++++++++----------- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6897dd9..a1574f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This changelog was started for release 4.2.0. +## Unreleased + +### 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) + +### 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) + +### Changed + +- Changed "Query builder" to "Form editor" in form editing interface +- Changed abstraction building method for relations. (Please refer to #248 and #268) +- Changed abstraction building method for 'strand': only add the required strand type, and not all three types (#277) +- Updated documentation + +### Removed + +- Removed "Remote endpoint" field for non-ttl file +- Removed "Custom_uri" field for ttl file + +### Security + +- Bump prismjs from 1.23.0 to 1.25.0 +- 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 ## [4.3.1] - 2021-06-16 diff --git a/docs/data.md b/docs/data.md index b06597b1..88d8253d 100644 --- a/docs/data.md +++ b/docs/data.md @@ -39,17 +39,6 @@ The first column of the file will manage the entity itself : the column name wil !!! Warning Unless you are trying to merge entities, make sure your URIs are unique across **both your personal and public datasets**. -### Entity label - -The values of the first column will also be transformed into the generated instances's label. - -* 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*. - ### 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. @@ -65,6 +54,23 @@ To setup inheritance, the **column name** needs to be formated as follows: !!! 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. From 66625c42964ebd235913728011b3505bb5bd9ecf Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Wed, 13 Oct 2021 11:42:36 +0200 Subject: [PATCH 107/318] Fix npm install on quay.io --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7b1a35bf..fb0ac6de 100644 --- a/Makefile +++ b/Makefile @@ -176,7 +176,7 @@ install-python: check-python install-js: check-npm @echo 'Installing javascript dependencies inside node_modules... ' - $(NPM) install --silent + $(NPM) install @echo ' Done' clean: clean-js clean-python From b995c2284c21c8db433070c4ec8bf3816fe8aabf Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Wed, 13 Oct 2021 11:47:12 +0200 Subject: [PATCH 108/318] try --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index fb0ac6de..6629c330 100644 --- a/Makefile +++ b/Makefile @@ -176,6 +176,7 @@ install-python: check-python install-js: check-npm @echo 'Installing javascript dependencies inside node_modules... ' + $(NPM) install -g npm $(NPM) install @echo ' Done' From 76f443e660cb47c575442499b038afaefe1f127f Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Wed, 13 Oct 2021 14:41:34 +0200 Subject: [PATCH 109/318] try new base image --- Makefile | 1 - docker/Dockerfile | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 6629c330..fb0ac6de 100644 --- a/Makefile +++ b/Makefile @@ -176,7 +176,6 @@ install-python: check-python install-js: check-npm @echo 'Installing javascript dependencies inside node_modules... ' - $(NPM) install -g npm $(NPM) install @echo ' Done' diff --git a/docker/Dockerfile b/docker/Dockerfile index c8b7ce9f..73d78fcb 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:update AS builder MAINTAINER "Xavier Garnier " COPY . /askomics From 500605ad01ef2454226ea08d08c165edcea5ca6f Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Wed, 13 Oct 2021 14:50:39 +0200 Subject: [PATCH 110/318] try --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index fb0ac6de..a6a3c4a5 100644 --- a/Makefile +++ b/Makefile @@ -176,6 +176,7 @@ install-python: check-python install-js: check-npm @echo 'Installing javascript dependencies inside node_modules... ' + $(NPM) cache verify $(NPM) install @echo ' Done' From bec4773b25c87e575d459836e81f4049f9433e68 Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Wed, 13 Oct 2021 14:56:44 +0200 Subject: [PATCH 111/318] base img --- Makefile | 1 - docker/Dockerfile | 2 +- docker/DockerfileAll | 4 ++-- docker/DockerfileCelery | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index a6a3c4a5..fb0ac6de 100644 --- a/Makefile +++ b/Makefile @@ -176,7 +176,6 @@ install-python: check-python install-js: check-npm @echo 'Installing javascript dependencies inside node_modules... ' - $(NPM) cache verify $(NPM) install @echo ' Done' diff --git a/docker/Dockerfile b/docker/Dockerfile index 73d78fcb..ebde4c78 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -7,7 +7,7 @@ WORKDIR /askomics RUN make clean-config fast-install build # Final image -FROM alpine:3.9 +FROM alpine:3.14 WORKDIR /askomics RUN apk add --no-cache make python3 bash git libc-dev libstdc++ nodejs nodejs-npm openldap-dev diff --git a/docker/DockerfileAll b/docker/DockerfileAll index fb9709cc..b4332355 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:update 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.14 ENV MODE="prod" \ NTASKS="5" \ diff --git a/docker/DockerfileCelery b/docker/DockerfileCelery index d2df8601..1f174820 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:update 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.14 WORKDIR /askomics RUN apk add --no-cache make python3 bash git libc-dev libstdc++ openldap-dev From 2cf93ac8bec9270858798718a4b6c9f6bdaa950b Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Wed, 13 Oct 2021 15:32:41 +0200 Subject: [PATCH 112/318] sync with base img --- Pipfile | 2 +- Pipfile.lock | 879 ++++++++++++++++++++++++++------------------------- 2 files changed, 446 insertions(+), 435 deletions(-) diff --git a/Pipfile b/Pipfile index 0dde7cdf..cd5a56d0 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 = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 6c8940c3..56c2411a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f208e36a35ade8b9d94b025ec7085c79f6b57dbd69ae9dd2b08c8b06418242fe" + "sha256": "d398feea9b583f305001058caf3d00d225702ce354469889db8ec34953b40471" }, "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:6e6f70639149612272a3b298a93ac50bba6f9ecece934f2a0ea86d4abde975da" ], "index": "pypi", - "version": "==0.6.6" + "version": "==0.6.7" }, "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": [ @@ -97,38 +98,42 @@ }, "celery": { "hashes": [ - "sha256:5e8d364e058554e83bbb116e8377d90c79be254785f357cb2cec026e79febe13", - "sha256:f4efebe6f8629b0da2b8e529424de376494f5b7a743c321c8a2ddc2b1414921c" + "sha256:8d9a3de9162965e97f8e8cc584c67aad83b3f7a267584fa47701ed11c3e0d4b0", + "sha256:9dab2170b4038f7bf10ef2861dbf486ddf1d20592290a1040f7b7a1259705d42" ], "index": "pypi", - "version": "==5.0.5" + "version": "==5.1.2" }, "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:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", + "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" ], - "version": "==4.0.0" + "markers": "python_version >= '3'", + "version": "==2.0.7" }, "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_version < '4' and python_full_version >= '3.6.2'", + "version": "==0.3.0" }, "click-plugins": { "hashes": [ @@ -139,10 +144,10 @@ }, "click-repl": { "hashes": [ - "sha256:9c4c3d022789cae912aad8a3f5e1d7c2cdd016ee1225b5212ad3e8691563cda5", - "sha256:b9f29d52abc4d6059f8e276132a111ab8d94980afe6a5432b9d996544afa95d5" + "sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b", + "sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8" ], - "version": "==0.1.6" + "version": "==0.2.0" }, "configparser": { "hashes": [ @@ -154,11 +159,11 @@ }, "deepdiff": { "hashes": [ - "sha256:dd79b81c2d84bfa33aa9d94d456b037b68daff6bb87b80dfaa1eca04da68b349", - "sha256:e054fed9dfe0d83d622921cbb3a3d0b3a6dd76acd2b6955433a0a2d35147774a" + "sha256:e3f1c3a375c7ea5ca69dba6f7920f9368658318ff1d8a496293c79481f48e649", + "sha256:ef3410ca31e059a9d10edfdff552245829835b3ecd03212dc5b533d45a6c3f57" ], "index": "pypi", - "version": "==5.5.0" + "version": "==5.6.0" }, "flask": { "hashes": [ @@ -181,15 +186,16 @@ "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005" ], + "markers": "python_version >= '3.4'", "version": "==4.0.7" }, "gitpython": { "hashes": [ - "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135", - "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e" + "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647", + "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5" ], "index": "pypi", - "version": "==3.1.17" + "version": "==3.1.24" }, "gunicorn": { "hashes": [ @@ -201,18 +207,11 @@ }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" - ], - "version": "==2.10" - }, - "importlib-metadata": { - "hashes": [ - "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", - "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "markers": "python_version < '3.8'", - "version": "==4.0.1" + "markers": "python_version >= '3'", + "version": "==3.3" }, "isodate": { "hashes": [ @@ -226,6 +225,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 +233,161 @@ "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:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d", + "sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a" ], - "version": "==5.0.2" + "markers": "python_version >= '3.6'", + "version": "==5.1.0" }, "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:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", + "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", + "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", + "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", + "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", + "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", + "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", + "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:09858463db6dd9f78b2a1a05c93f3b33d4f65975771e90d2cf7aadb7c2f66edf", + "sha256:209666ce9d4a817e8a4597cd475b71b4878a85fa4b8db41d79fdb4fdee01dde2", + "sha256:298156f4d3d46815eaf0fcf0a03f9625fc7631692bd1ad851517ab93c3168fc6", + "sha256:30fc68307c0155d2a75ad19844224be0f2c6f06572d958db4e2053f816b859ad", + "sha256:423216d8afc5923b15df86037c6053bf030d15cc9e3224206ef868c2d63dd6dc", + "sha256:426a00b68b0d21f2deb2ace3c6d677e611ad5a612d2c76494e24a562a930c254", + "sha256:466e682264b14982012887e90346d33435c984b7fead7b85e634903795c8fdb0", + "sha256:51a7b9db0a2941434cd930dacaafe0fc9da8f3d6157f9d12f761bbde93f46218", + "sha256:52a664323273c08f3b473548bf87c8145b7513afd63e4ebba8496ecd3853df13", + "sha256:550564024dc5ceee9421a86fc0fb378aa9d222d4d0f858f6669eff7410c89bef", + "sha256:5de64950137f3a50b76ce93556db392e8f1f954c2d8207f78a92d1f79aa9f737", + "sha256:640c1ccfd56724f2955c237b6ccce2e5b8607c3bc1cc51d3933b8c48d1da3723", + "sha256:7fdc7689daf3b845934d67cb221ba8d250fdca20ac0334fea32f7091b93f00d3", + "sha256:805459ad8baaf815883d0d6f86e45b3b0b67d823a8f3fa39b1ed9c45eaf5edf1", + "sha256:92a0ab128b07799dd5b9077a9af075a63467d03ebac6f8a93e6440abfea4120d", + "sha256:9f2dc79c093f6c5113718d3d90c283f11463d77daa4e83aeeac088ec6a0bda52", + "sha256:a5109345f5ce7ddb3840f5970de71c34a0ff7fceb133c9441283bb8250f532a3", + "sha256:a55e4d81c4260386f71d22294795c87609164e22b28ba0d435850fbdf82fc0c5", + "sha256:a9da45b748caad72ea4a4ed57e9cd382089f33c5ec330a804eb420a496fa760f", + "sha256:b160b9a99ecc6559d9e6d461b95c8eec21461b332f80267ad2c10394b9503496", + "sha256:b342064e647d099ca765f19672696ad50c953cac95b566af1492fd142283580f", + "sha256:b5e8590b9245803c849e09bae070a8e1ff444f45e3f0bed558dd722119eea724", + "sha256:bf75d5825ef47aa51d669b03ce635ecb84d69311e05eccea083f31c7570c9931", + "sha256:c01b59b33c7c3ba90744f2c695be571a3bd40ab2ba7f3d169ffa6db3cfba614f", + "sha256:d96a6a7d74af56feb11e9a443150216578ea07b7450f7c05df40eec90af7f4a7", + "sha256:dd0e3651d210068d13e18503d75aaa45656eef51ef0b261f891788589db2cc38", + "sha256:e167b9805de54367dcb2043519382be541117503ce99e3291cc9b41ca0a83557", + "sha256:e42029e184008a5fd3d819323345e25e2337b0ac7f5c135b7623308530209d57", + "sha256:f545c082eeb09ae678dd451a1b1dbf17babd8a0d7adea02897a76e639afca310", + "sha256:fde50062d67d805bc96f1a9ecc0d37bfc2a8f02b937d2c50824d186aa91f2419" + ], + "markers": "python_version < '3.11' and python_version >= '3.7'", + "version": "==1.21.2" }, "ordered-set": { "hashes": [ "sha256:ba93b2df055bca202116ec44b9bead3df33ea63a7d5827ff8e16738b97f33a95" ], + "markers": "python_version >= '3.5'", "version": "==4.0.2" }, "prompt-toolkit": { "hashes": [ - "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04", - "sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc" + "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c", + "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c" ], - "version": "==3.0.18" + "markers": "python_full_version >= '3.6.2'", + "version": "==3.0.20" }, "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" }, @@ -359,28 +403,35 @@ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "pysam": { "hashes": [ - "sha256:107eca9050d8140910b5ea5c5a9e66313e1155eb44cc180e10f48f9cb71e8095", - "sha256:3eb70111a5ed86cc7a048c9b2205087b1184093e6135f02ad7f144b206951452", - "sha256:9da29490c666a963e5a7f6f5114e86c9b36a8a6adc2227f5772bdc38c09d2d37", - "sha256:9e3597a49e4bc72c31199d6231018ad3034e08a8243b9f8086953afb2ab5a3af", - "sha256:a5a0fc1f0d724d0b7789341add26ba181ac009430021f0998f6083fb62432193", - "sha256:d428a9768691d5ea3c28cc52a949c920ae691aa4c110a8b7328dc4d165ef1ad6", - "sha256:f65659deadc4904984de24cb6f3878b6052cf504a2a85a50b80f2ff7939f96db" + "sha256:08f88fa4472e140e39c9ec3c01643563587a28e49c115ef9077295b0452342ac", + "sha256:097eedc82095ff52e020b6e0e099a171313e60083c68ad861ac474f325e2b7d0", + "sha256:18478ac02511d6171bce1891b503031eaf5e22b7a38cf62e00f3a070a4a37da2", + "sha256:2fd8fa6205ad893082ec0aa1992446e3d9c63bfe9f7a3e59d81a956780e5cc2a", + "sha256:4032361c424fb3b27a7da7ddeba0de8acb6c548b025bdc03c8ffc6b306d7ee9a", + "sha256:4d0a35dec9194bacbde0647ddf32ebf805a6724b0192758039c031fce849f01f", + "sha256:4f5188fda840fe51144b8c56c45af014174295c3f539a40333f9e270ca0d5e01", + "sha256:5d140da81ca42f474006f5cc0fd66647f1b08d559af7026bbe9f01fab029bffd", + "sha256:6f802073d625a6a9b47aaaed6ff6b08e90ec4ad79df271073452fa8c7a62c529", + "sha256:7caada03fbec2e18b8eb2c80aabf8834ea2ceb12b2aa1c0fa6d4ba42b60d83fa", + "sha256:c4a8099cf474067eaf5e270b5e71433a9ea0e97af90262143a655528eb756cd9", + "sha256:cb2c3ca9ff3b5c9694846254fea9f8697406a35769b11767a312e97ad5c8cedd", + "sha256:eb35399530188efe48ef136a043ba3acf9f538f1b11e946f3aaea9fd09c8cbde" ], "index": "pypi", - "version": "==0.16.0.1" + "version": "==0.17.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": [ @@ -391,18 +442,18 @@ }, "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": [ @@ -436,15 +487,16 @@ "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==5.4.1" }, "rdflib": { "hashes": [ - "sha256:78149dd49d385efec3b3adfbd61c87afaf1281c30d3fcaf1b323b34f603fb155", - "sha256:88208ea971a87886d60ae2b1a4b2cdc263527af0454c422118d43fe64b357877" + "sha256:6136ae056001474ee2aff5fc5b956e62a11c3a9c66bb0f3d9c0aaa5fbb56854e", + "sha256:b7642daac8cdad1ba157fecb236f5d1b2aa1de64e714dcee80d65e2b794d88a6" ], "index": "pypi", - "version": "==5.0.0" + "version": "==6.0.2" }, "redis": { "hashes": [ @@ -456,11 +508,11 @@ }, "requests": { "hashes": [ - "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", - "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" ], "index": "pypi", - "version": "==2.25.1" + "version": "==2.26.0" }, "requests-toolbelt": { "hashes": [ @@ -474,17 +526,18 @@ "flask" ], "hashes": [ - "sha256:c1227d38dca315ba35182373f129c3e2722e8ed999e52584e6aca7d287870739", - "sha256:c7d380a21281e15be3d9f67a3c4fbb4f800c481d88ff8d8931f39486dd7b4ada" + "sha256:b9844751e40710e84a457c5bc29b21c383ccb2b63d76eeaad72f7f1c808c8828", + "sha256:c091cc7115ff25fe3a0e410dbecd7a996f81a3f6137d2272daef32d6c3cfa6dc" ], "index": "pypi", - "version": "==1.1.0" + "version": "==1.4.3" }, "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": { @@ -492,11 +545,14 @@ "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182", "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2" ], + "markers": "python_version >= '3.5'", "version": "==4.0.0" }, "sparqlwrapper": { "hashes": [ + "sha256:17ec44b08b8ae2888c801066249f74fe328eec25d90203ce7eadaf82e64484c7", "sha256:357ee8a27bc910ea13d77836dbddd0b914991495b8cc1bf70676578155e962a8", + "sha256:8cf6c21126ed76edc85c5c232fd6f77b9f61f8ad1db90a7147cdde2104aff145", "sha256:c7f9c9d8ebb13428771bc3b6dee54197422507dcc3dea34e30d5dcfc53478dec", "sha256:d6a66b5b8cda141660e07aeb00472db077a98d22cb588c973209c7336850fb3c" ], @@ -505,32 +561,33 @@ }, "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:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", + "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", + "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" ], - "markers": "python_version < '3.8'", - "version": "==3.10.0.0" + "markers": "python_version < '3.10'", + "version": "==3.10.0.2" }, "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_version < '4'", + "version": "==1.26.7" }, "validate-email": { "hashes": [ @@ -544,30 +601,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": [ @@ -583,13 +647,6 @@ ], "index": "pypi", "version": "==0.16.1" - }, - "zipp": { - "hashes": [ - "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", - "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" - ], - "version": "==3.4.1" } }, "develop": { @@ -598,27 +655,30 @@ "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:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", + "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" ], - "version": "==4.0.0" + "markers": "python_version >= '3'", + "version": "==2.0.7" }, "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": { @@ -679,15 +739,16 @@ "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==5.5" }, "coveralls": { "hashes": [ - "sha256:7bd173b3425733661ba3063c88f180127cc2b20e9740686f86d2622b31b41385", - "sha256:cbb942ae5ef3d2b55388cb5b43e93a269544911535f1e750e1c656aef019ce60" + "sha256:15a987d9df877fff44cd81948c5806ffb6eafb757b3443f737888358e96156ee", + "sha256:aedfcc5296b788ebaf8ace8029376e5f102f67c53d1373f2e821515c15b36527" ], "index": "pypi", - "version": "==3.0.1" + "version": "==3.2.0" }, "docopt": { "hashes": [ @@ -697,32 +758,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:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15", + "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1" ], - "markers": "python_version < '3.8'", - "version": "==4.0.1" + "markers": "python_version >= '3.6'", + "version": "==4.8.1" }, "iniconfig": { "hashes": [ @@ -736,76 +799,76 @@ "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" ], + "markers": "python_version >= '3.6'", "version": "==3.3.4" }, "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:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", + "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", + "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", + "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", + "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", + "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", + "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.1" }, "mccabe": { "hashes": [ @@ -814,78 +877,93 @@ ], "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:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" ], - "version": "==20.9" + "markers": "python_version >= '3.6'", + "version": "==21.0" }, "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" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.10.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" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "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": "==3.0.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], "index": "pypi", - "version": "==2.12.0" + "version": "==2.8.2" }, "pyyaml": { "hashes": [ @@ -919,67 +997,31 @@ "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "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" + "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": { @@ -989,81 +1031,50 @@ ], "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" - }, - "typing-extensions": { + "urllib3": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", + "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" ], - "markers": "python_version < '3.8'", - "version": "==3.10.0.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.7" }, - "urllib3": { + "watchdog": { "hashes": [ - "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", - "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" + "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" } } } From 4185c7432b5ef9cc9ce61b3234fbb7394e88150f Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Wed, 13 Oct 2021 15:33:21 +0200 Subject: [PATCH 113/318] fix --- docker/Dockerfile | 2 +- docker/DockerfileAll | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ebde4c78..421691b6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -10,7 +10,7 @@ RUN make clean-config fast-install build FROM alpine:3.14 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 b4332355..cad2881c 100644 --- a/docker/DockerfileAll +++ b/docker/DockerfileAll @@ -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 From d32046b13e0107924e0b50962af7df1b6affbe3c Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Wed, 13 Oct 2021 15:47:19 +0200 Subject: [PATCH 114/318] catch errors --- Makefile | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index fb0ac6de..966c97bd 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,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 +94,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 @@ -126,32 +126,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 +161,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 + $(NPM) install || { echo "ERROR"; exit 1; } @echo ' Done' clean: clean-js clean-python From 3a1b6fd53e337ea4e55ac4a18e319f190c9ba287 Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Wed, 13 Oct 2021 15:52:23 +0200 Subject: [PATCH 115/318] newer python --- .github/workflows/lint_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint_test.yml b/.github/workflows/lint_test.yml index 1be124fb..bd3ba62d 100644 --- a/.github/workflows/lint_test.yml +++ b/.github/workflows/lint_test.yml @@ -33,7 +33,7 @@ jobs: uses: actions/checkout@v2 - uses: actions/setup-python@v1 with: - python-version: 3.6 + python-version: 3.7 - name: Update apt cache run: sudo apt-get update - name: Install python-ldap deps From 1930bf779ecb273e6f9a91b002670b660924cb92 Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Wed, 13 Oct 2021 15:52:41 +0200 Subject: [PATCH 116/318] newer python --- .github/workflows/lint_test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint_test.yml b/.github/workflows/lint_test.yml index bd3ba62d..c2058667 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.7 + python-version: 3.8 - name: Update apt cache run: sudo apt-get update - name: Install python-ldap deps From cf49f96693809c3fe5473cc4d1953ca9f938312e Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Wed, 13 Oct 2021 17:01:28 +0200 Subject: [PATCH 117/318] python3, no need to decode anymore --- askomics/libaskomics/Galaxy.py | 2 +- askomics/libaskomics/LdapAuth.py | 8 ++++---- askomics/libaskomics/SparqlQueryLauncher.py | 2 +- tests/test_api_file.py | 3 +-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/askomics/libaskomics/Galaxy.py b/askomics/libaskomics/Galaxy.py index d31ec271..8ade4368 100644 --- a/askomics/libaskomics/Galaxy.py +++ b/askomics/libaskomics/Galaxy.py @@ -156,6 +156,6 @@ def get_dataset_content(self, dataset_id): Content of the dataset """ galaxy_instance = galaxy.GalaxyInstance(self.url, self.apikey) - dataset = galaxy_instance.datasets.download_dataset(dataset_id).decode('utf-8') + dataset = galaxy_instance.datasets.download_dataset(dataset_id) return dataset diff --git a/askomics/libaskomics/LdapAuth.py b/askomics/libaskomics/LdapAuth.py index 28e606b5..65342392 100644 --- a/askomics/libaskomics/LdapAuth.py +++ b/askomics/libaskomics/LdapAuth.py @@ -92,10 +92,10 @@ def get_user(self, login): return { 'dn': ldap_user[0][0], - 'email': ldap_user[0][1][self.mail_attribute][0].decode(), - 'username': ldap_user[0][1][self.username_attribute][0].decode(), - 'fname': ldap_user[0][1][self.first_name_attribute][0].decode(), - 'lname': ldap_user[0][1][self.surname_attribute][0].decode() + 'email': ldap_user[0][1][self.mail_attribute][0], + 'username': ldap_user[0][1][self.username_attribute][0], + 'fname': ldap_user[0][1][self.first_name_attribute][0], + 'lname': ldap_user[0][1][self.surname_attribute][0] } def check_password(self, dn, password): diff --git a/askomics/libaskomics/SparqlQueryLauncher.py b/askomics/libaskomics/SparqlQueryLauncher.py index aae8cc51..b0094c6c 100644 --- a/askomics/libaskomics/SparqlQueryLauncher.py +++ b/askomics/libaskomics/SparqlQueryLauncher.py @@ -220,7 +220,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 {{ diff --git a/tests/test_api_file.py b/tests/test_api_file.py index 829ce1b1..2476f7db 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -600,5 +600,4 @@ def test_serve_file(self, client): response = client.client.get('/api/files/ttl/1/jdoe/{}'.format(filename)) assert response.status_code == 200 - # print(response.data.decode("utf-8")) - assert response.data.decode("utf-8") == content + assert response.data == content From 38f649cef3f6d4396d66c21c74fa5df8d2070551 Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Wed, 13 Oct 2021 17:23:33 +0200 Subject: [PATCH 118/318] ok, maybe we need some decode still --- askomics/libaskomics/Galaxy.py | 2 +- askomics/libaskomics/LdapAuth.py | 8 ++++---- tests/test_api_file.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/askomics/libaskomics/Galaxy.py b/askomics/libaskomics/Galaxy.py index 8ade4368..d31ec271 100644 --- a/askomics/libaskomics/Galaxy.py +++ b/askomics/libaskomics/Galaxy.py @@ -156,6 +156,6 @@ def get_dataset_content(self, dataset_id): Content of the dataset """ galaxy_instance = galaxy.GalaxyInstance(self.url, self.apikey) - dataset = galaxy_instance.datasets.download_dataset(dataset_id) + dataset = galaxy_instance.datasets.download_dataset(dataset_id).decode('utf-8') return dataset diff --git a/askomics/libaskomics/LdapAuth.py b/askomics/libaskomics/LdapAuth.py index 65342392..28e606b5 100644 --- a/askomics/libaskomics/LdapAuth.py +++ b/askomics/libaskomics/LdapAuth.py @@ -92,10 +92,10 @@ def get_user(self, login): return { 'dn': ldap_user[0][0], - 'email': ldap_user[0][1][self.mail_attribute][0], - 'username': ldap_user[0][1][self.username_attribute][0], - 'fname': ldap_user[0][1][self.first_name_attribute][0], - 'lname': ldap_user[0][1][self.surname_attribute][0] + 'email': ldap_user[0][1][self.mail_attribute][0].decode(), + 'username': ldap_user[0][1][self.username_attribute][0].decode(), + 'fname': ldap_user[0][1][self.first_name_attribute][0].decode(), + 'lname': ldap_user[0][1][self.surname_attribute][0].decode() } def check_password(self, dn, password): diff --git a/tests/test_api_file.py b/tests/test_api_file.py index 2476f7db..a3a0f708 100644 --- a/tests/test_api_file.py +++ b/tests/test_api_file.py @@ -600,4 +600,4 @@ def test_serve_file(self, client): response = client.client.get('/api/files/ttl/1/jdoe/{}'.format(filename)) assert response.status_code == 200 - assert response.data == content + assert response.data.decode("utf-8") == content From e02f41e2d704290b0b7fb24c5c6f190da1cf9262 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 15 Oct 2021 12:16:06 +0200 Subject: [PATCH 119/318] askomics:instancesHaveNoLabels in documentation --- docs/abstraction.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/abstraction.md b/docs/abstraction.md index 428367e6..ea7d8c7b 100644 --- a/docs/abstraction.md +++ b/docs/abstraction.md @@ -32,12 +32,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 . ```
!!! 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*. From 137b444a7056b141253eafd4351b3272ffa4d3ab Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 15 Oct 2021 12:17:19 +0200 Subject: [PATCH 120/318] askomics:instancesHaveNoLabels in documentation (#291) * askomics:instancesHaveNoLabels in documentation --- docs/abstraction.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/abstraction.md b/docs/abstraction.md index 428367e6..ea7d8c7b 100644 --- a/docs/abstraction.md +++ b/docs/abstraction.md @@ -32,12 +32,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 . ```
!!! 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*. From 212dff379e8eb86a3912413d50e90cbbac809a86 Mon Sep 17 00:00:00 2001 From: Anthony Bretaudeau Date: Fri, 15 Oct 2021 14:56:10 +0200 Subject: [PATCH 121/318] use the new tagges base image --- docker/Dockerfile | 2 +- docker/DockerfileAll | 2 +- docker/DockerfileCelery | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 421691b6..e3f38ef5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/askomics/flaskomics-base:update AS builder +FROM quay.io/askomics/flaskomics-base:4.0.0-alpine3.14 AS builder MAINTAINER "Xavier Garnier " COPY . /askomics diff --git a/docker/DockerfileAll b/docker/DockerfileAll index cad2881c..402d80bf 100644 --- a/docker/DockerfileAll +++ b/docker/DockerfileAll @@ -1,5 +1,5 @@ # Build AskOmics -FROM quay.io/askomics/flaskomics-base:update AS askomics_builder +FROM quay.io/askomics/flaskomics-base:4.0.0-alpine3.14 AS askomics_builder MAINTAINER "Xavier Garnier " COPY . /askomics diff --git a/docker/DockerfileCelery b/docker/DockerfileCelery index 1f174820..bd11c62e 100644 --- a/docker/DockerfileCelery +++ b/docker/DockerfileCelery @@ -1,4 +1,4 @@ -FROM quay.io/askomics/flaskomics-base:update AS builder +FROM quay.io/askomics/flaskomics-base:4.0.0-alpine3.14 AS builder MAINTAINER "Xavier Garnier " COPY . /askomics From 8295f2883ec6a9eb44f49cab9c5e83d19199f191 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 18 Oct 2021 14:35:29 +0200 Subject: [PATCH 122/318] Fix 234 & 292 (#293) * Test separation data-abstraction (+ duplicate) --- askomics/api/query.py | 11 +++++++---- askomics/api/sparql.py | 4 ++-- askomics/libaskomics/SparqlQuery.py | 18 ++++++++++++------ askomics/tasks.py | 10 +++------- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/askomics/api/query.py b/askomics/api/query.py index f5bfc8af..1de59ddd 100644 --- a/askomics/api/query.py +++ b/askomics/api/query.py @@ -125,7 +125,7 @@ def get_preview(): 'errorMessage': "Missing graphState parameter" }), 400 - query = SparqlQuery(current_app, session, data["graphState"]) + query = SparqlQuery(current_app, session, data["graphState"], get_graphs=False) query.build_query_from_json(preview=True, for_editor=False) endpoints = query.endpoints @@ -186,15 +186,18 @@ def save_result(): 'errorMessage': "Missing graphState parameter" }), 400 - query = SparqlQuery(current_app, session, data["graphState"]) + query = SparqlQuery(current_app, session, data["graphState"], get_graphs=False) query.build_query_from_json(preview=False, for_editor=False) + federated = query.is_federated() info = { - "graph_state": query.json, + "graph_state": data["graphState"], "query": query.sparql, "graphs": query.graphs, "endpoints": query.endpoints, - "celery_id": None + "federated": federated, + "celery_id": None, + "selects": query.selects, } result = Result(current_app, session, info) diff --git a/askomics/api/sparql.py b/askomics/api/sparql.py index c501ab92..8a75fcb1 100644 --- a/askomics/api/sparql.py +++ b/askomics/api/sparql.py @@ -116,7 +116,7 @@ def query(): }), 400 try: - query = SparqlQuery(current_app, session) + query = SparqlQuery(current_app, session, get_graphs=False) query.set_graphs_and_endpoints(graphs=graphs, endpoints=endpoints) @@ -209,7 +209,7 @@ def save_query(): }), 400 # Is query federated? - query = SparqlQuery(current_app, session) + query = SparqlQuery(current_app, session, get_graphs=False) query.set_graphs_and_endpoints(graphs=graphs, endpoints=endpoints) federated = query.is_federated() diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index a37d8aa2..75fb681d 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -18,7 +18,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=True): """init Parameters @@ -44,8 +44,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 @@ -413,12 +414,17 @@ def set_graphs_and_endpoints(self, entities=None, graphs=None, endpoints=None): query = ''' SELECT DISTINCT ?graph ?endpoint WHERE {{ + ?graph_abstraction askomics:public ?public . + ?graph_abstraction dc:creator ?creator . ?graph askomics:public ?public . ?graph dc:creator ?creator . - GRAPH ?graph {{ - ?graph prov:atLocation ?endpoint . + GRAPH ?graph_abstraction {{ + ?graph_abstraction prov:atLocation ?endpoint . ?entity_uri a askomics:entity . }} + GRAPH ?graph {{ + [] a ?entity_uri . + }} {} {} }} @@ -432,7 +438,7 @@ def set_graphs_and_endpoints(self, entities=None, graphs=None, endpoints=None): if not graphs or res["graph"] in graphs: self.graphs.append(res["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: diff --git a/askomics/tasks.py b/askomics/tasks.py index e0f3b1c9..281f0cc1 100644 --- a/askomics/tasks.py +++ b/askomics/tasks.py @@ -16,7 +16,6 @@ from askomics.libaskomics.FilesHandler import FilesHandler from askomics.libaskomics.LocalAuth import LocalAuth from askomics.libaskomics.Result import Result -from askomics.libaskomics.SparqlQuery import SparqlQuery from askomics.libaskomics.SparqlQueryLauncher import SparqlQueryLauncher @@ -156,15 +155,12 @@ def query(self, session, info): 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) + 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) From bf06552834cd3e743361815a1d83574fefa39703 Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 18 Oct 2021 16:13:41 +0200 Subject: [PATCH 123/318] Fix #294 (#295) * Fix console issue --- askomics/api/results.py | 4 +++- askomics/react/src/routes/ask/ask.jsx | 7 +++++-- askomics/react/src/routes/results/resultsfilestable.jsx | 3 ++- tests/results/sparql_and_graph.json | 3 ++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/askomics/api/results.py b/askomics/api/results.py index 854aeb08..97c394b3 100644 --- a/askomics/api/results.py +++ b/askomics/api/results.py @@ -166,6 +166,7 @@ def get_graph_and_sparql_query(): # Get all graphs and endpoint, and mark as selected the used one query = SparqlQuery(current_app, session) graphs, endpoints = query.get_graphs_and_endpoints(selected_graphs=graphs, selected_endpoints=endpoints) + console_enabled = can_access(session['user']) except Exception as e: traceback.print_exc(file=sys.stdout) @@ -186,7 +187,8 @@ def get_graph_and_sparql_query(): 'endpoints': endpoints, 'diskSpace': disk_space, 'error': False, - 'errorMessage': '' + 'errorMessage': '', + 'console_enabled': console_enabled }) 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/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/tests/results/sparql_and_graph.json b/tests/results/sparql_and_graph.json index 34caa773..cc485305 100644 --- a/tests/results/sparql_and_graph.json +++ b/tests/results/sparql_and_graph.json @@ -9,6 +9,7 @@ }, "error": false, "errorMessage": "", + "console_enabled": true, "graphState": { "attr": [ { @@ -435,4 +436,4 @@ } }, "sparqlQuery": null -} \ No newline at end of file +} From cc87876e5cfbea13834162c29463400e5073c6cc Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 27 Oct 2021 10:54:19 +0200 Subject: [PATCH 124/318] Fix date issues --- askomics/libaskomics/CsvFile.py | 4 ++-- askomics/libaskomics/File.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index e7ecd448..64d8aa37 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -299,7 +299,7 @@ def is_date(value): try: parser.parse(value, dayfirst=True).date() return True - except parser.ParserError: + except Exception: return False @property @@ -617,7 +617,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/File.py b/askomics/libaskomics/File.py index 24292fa8..b5cd4879 100644 --- a/askomics/libaskomics/File.py +++ b/askomics/libaskomics/File.py @@ -461,7 +461,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 +480,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 From 80044307019da12323edee529eab15b71b39fdaf Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 27 Oct 2021 14:58:59 +0200 Subject: [PATCH 125/318] Update tasks.py --- askomics/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askomics/tasks.py b/askomics/tasks.py index 281f0cc1..9d2bad0e 100644 --- a/askomics/tasks.py +++ b/askomics/tasks.py @@ -158,7 +158,7 @@ def query(self, session, info): headers = info["selects"] results = [] - if query.graphs: + if info["graphs"]: 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) From 8859bc1d5a74ab1a757aa165026c818ac8e81dc2 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 27 Oct 2021 19:32:44 +0200 Subject: [PATCH 126/318] Fix #296 (#297) --- askomics/libaskomics/CsvFile.py | 4 ++-- askomics/libaskomics/File.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index e7ecd448..64d8aa37 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -299,7 +299,7 @@ def is_date(value): try: parser.parse(value, dayfirst=True).date() return True - except parser.ParserError: + except Exception: return False @property @@ -617,7 +617,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/File.py b/askomics/libaskomics/File.py index 24292fa8..b5cd4879 100644 --- a/askomics/libaskomics/File.py +++ b/askomics/libaskomics/File.py @@ -461,7 +461,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 +480,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 From 4496ccfa8aacfaa242677c8b26a2874aa9f62130 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 27 Jan 2022 09:24:44 +0100 Subject: [PATCH 127/318] Use authenticated endpoint instead of public endpoint (#302) --- .github/workflows/lint_test.yml | 2 +- askomics/libaskomics/SparqlQueryLauncher.py | 9 ++++-- .../src/routes/datasets/datasetstable.jsx | 2 +- config/askomics.ini.template | 28 ++++++++++--------- config/askomics.test.ini | 28 ++++++++++--------- tests/results/abstraction.json | 8 +++--- tests/results/startpoints.json | 12 ++++---- 7 files changed, 49 insertions(+), 40 deletions(-) diff --git a/.github/workflows/lint_test.yml b/.github/workflows/lint_test.yml index c2058667..84ca6d05 100644 --- a/.github/workflows/lint_test.yml +++ b/.github/workflows/lint_test.yml @@ -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/askomics/libaskomics/SparqlQueryLauncher.py b/askomics/libaskomics/SparqlQueryLauncher.py index b0094c6c..6c250619 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 diff --git a/askomics/react/src/routes/datasets/datasetstable.jsx b/askomics/react/src/routes/datasets/datasetstable.jsx index 23cec291..5afa8e04 100644 --- a/askomics/react/src/routes/datasets/datasetstable.jsx +++ b/askomics/react/src/routes/datasets/datasetstable.jsx @@ -209,4 +209,4 @@ DatasetsTable.propTypes = { waiting: PropTypes.bool, datasets: PropTypes.object, config: PropTypes.object -} \ No newline at end of file +} diff --git a/config/askomics.ini.template b/config/askomics.ini.template index e22d88a2..9e8001ba 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,16 @@ 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 = [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 +98,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 diff --git a/config/askomics.test.ini b/config/askomics.test.ini index 5609a185..6165a1cd 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,16 @@ 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 = [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 +92,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 diff --git a/tests/results/abstraction.json b/tests/results/abstraction.json index 4aa59849..2c2eb213 100644 --- a/tests/results/abstraction.json +++ b/tests/results/abstraction.json @@ -561,7 +561,7 @@ "entities": [ { "endpoints": [ - "http://localhost:8891/sparql" + "http://localhost:8891/sparql-auth" ], "faldo": true, "graphs": [ @@ -575,7 +575,7 @@ }, { "endpoints": [ - "http://localhost:8891/sparql" + "http://localhost:8891/sparql-auth" ], "faldo": true, "graphs": [ @@ -589,7 +589,7 @@ }, { "endpoints": [ - "http://localhost:8891/sparql" + "http://localhost:8891/sparql-auth" ], "faldo": true, "graphs": [ @@ -602,7 +602,7 @@ }, { "endpoints": [ - "http://localhost:8891/sparql" + "http://localhost:8891/sparql-auth" ], "faldo": false, "graphs": [ diff --git a/tests/results/startpoints.json b/tests/results/startpoints.json index 58475efd..ba87cada 100644 --- a/tests/results/startpoints.json +++ b/tests/results/startpoints.json @@ -8,10 +8,10 @@ [{ "endpoints": [{ "name": "local", - "url": "http://localhost:8891/sparql" + "url": "http://localhost:8891/sparql-auth" }, { "name": "local", - "url": "http://localhost:8891/sparql" + "url": "http://localhost:8891/sparql-auth" }], "entity": "http://askomics.org/test/data/transcript", @@ -37,10 +37,10 @@ { "endpoints": [{ "name": "local", - "url": "http://localhost:8891/sparql" + "url": "http://localhost:8891/sparql-auth" }, { "name": "local", - "url": "http://localhost:8891/sparql" + "url": "http://localhost:8891/sparql-auth" }], "entity": "http://askomics.org/test/data/gene", @@ -70,7 +70,7 @@ { "endpoints": [{ "name": "local", - "url": "http://localhost:8891/sparql" + "url": "http://localhost:8891/sparql-auth" }], "entity": "http://askomics.org/test/data/DifferentialExpression", @@ -89,7 +89,7 @@ { "endpoints": [{ "name": "local", - "url": "http://localhost:8891/sparql" + "url": "http://localhost:8891/sparql-auth" }], "entity": "http://askomics.org/test/data/QTL", From cd3171a8ea7626afec87fd138a8a94b27cc24de4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Jan 2022 09:25:11 +0100 Subject: [PATCH 128/318] Bump follow-redirects from 1.14.4 to 1.14.7 (#299) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.4 to 1.14.7. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.4...v1.14.7) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 567eb622..f1b8dfe5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5341,9 +5341,9 @@ } }, "follow-redirects": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==" + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", + "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==" }, "for-in": { "version": "1.0.2", From 83a0244952e6f4a72cec94b3cc379ac657052cb4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Jan 2022 09:25:25 +0100 Subject: [PATCH 129/318] Bump python-ldap from 3.3.1 to 3.4.0 (#298) Bumps [python-ldap](https://github.com/python-ldap/python-ldap) from 3.3.1 to 3.4.0. - [Release notes](https://github.com/python-ldap/python-ldap/releases) - [Commits](https://github.com/python-ldap/python-ldap/compare/python-ldap-3.3.1...python-ldap-3.4.0) --- updated-dependencies: - dependency-name: python-ldap dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile.lock | 336 ++++++++++++++++++++++++++++----------------------- 1 file changed, 187 insertions(+), 149 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 56c2411a..10565d9a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -113,11 +113,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", - "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" + "sha256:735e240d9a8506778cd7a453d97e817e536bb1fc29f4f6961ce297b9c7a917b0", + "sha256:83fcdeb225499d6344c8f7f34684c2981270beacc32ede2e669e94f7fa544405" ], "markers": "python_version >= '3'", - "version": "==2.0.7" + "version": "==2.0.8" }, "click": { "hashes": [ @@ -183,11 +183,11 @@ }, "gitdb": { "hashes": [ - "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", - "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005" + "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd", + "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa" ], - "markers": "python_version >= '3.4'", - "version": "==4.0.7" + "markers": "python_version >= '3.6'", + "version": "==4.0.9" }, "gitpython": { "hashes": [ @@ -238,17 +238,18 @@ }, "kombu": { "hashes": [ - "sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d", - "sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a" + "sha256:0f5d0763fb916808f617b886697b2be28e6bc35026f08e679697fc814b48a608", + "sha256:d36f0cde6a18d9eb7b6b3aa62a59bfdff7f5724689850e447eca5be8efc9d501" ], - "markers": "python_version >= '3.6'", - "version": "==5.1.0" + "markers": "python_version >= '3.7'", + "version": "==5.2.2" }, "markupsafe": { "hashes": [ "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", @@ -256,6 +257,7 @@ "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", @@ -263,27 +265,36 @@ "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", @@ -291,10 +302,14 @@ "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", @@ -306,39 +321,39 @@ }, "numpy": { "hashes": [ - "sha256:09858463db6dd9f78b2a1a05c93f3b33d4f65975771e90d2cf7aadb7c2f66edf", - "sha256:209666ce9d4a817e8a4597cd475b71b4878a85fa4b8db41d79fdb4fdee01dde2", - "sha256:298156f4d3d46815eaf0fcf0a03f9625fc7631692bd1ad851517ab93c3168fc6", - "sha256:30fc68307c0155d2a75ad19844224be0f2c6f06572d958db4e2053f816b859ad", - "sha256:423216d8afc5923b15df86037c6053bf030d15cc9e3224206ef868c2d63dd6dc", - "sha256:426a00b68b0d21f2deb2ace3c6d677e611ad5a612d2c76494e24a562a930c254", - "sha256:466e682264b14982012887e90346d33435c984b7fead7b85e634903795c8fdb0", - "sha256:51a7b9db0a2941434cd930dacaafe0fc9da8f3d6157f9d12f761bbde93f46218", - "sha256:52a664323273c08f3b473548bf87c8145b7513afd63e4ebba8496ecd3853df13", - "sha256:550564024dc5ceee9421a86fc0fb378aa9d222d4d0f858f6669eff7410c89bef", - "sha256:5de64950137f3a50b76ce93556db392e8f1f954c2d8207f78a92d1f79aa9f737", - "sha256:640c1ccfd56724f2955c237b6ccce2e5b8607c3bc1cc51d3933b8c48d1da3723", - "sha256:7fdc7689daf3b845934d67cb221ba8d250fdca20ac0334fea32f7091b93f00d3", - "sha256:805459ad8baaf815883d0d6f86e45b3b0b67d823a8f3fa39b1ed9c45eaf5edf1", - "sha256:92a0ab128b07799dd5b9077a9af075a63467d03ebac6f8a93e6440abfea4120d", - "sha256:9f2dc79c093f6c5113718d3d90c283f11463d77daa4e83aeeac088ec6a0bda52", - "sha256:a5109345f5ce7ddb3840f5970de71c34a0ff7fceb133c9441283bb8250f532a3", - "sha256:a55e4d81c4260386f71d22294795c87609164e22b28ba0d435850fbdf82fc0c5", - "sha256:a9da45b748caad72ea4a4ed57e9cd382089f33c5ec330a804eb420a496fa760f", - "sha256:b160b9a99ecc6559d9e6d461b95c8eec21461b332f80267ad2c10394b9503496", - "sha256:b342064e647d099ca765f19672696ad50c953cac95b566af1492fd142283580f", - "sha256:b5e8590b9245803c849e09bae070a8e1ff444f45e3f0bed558dd722119eea724", - "sha256:bf75d5825ef47aa51d669b03ce635ecb84d69311e05eccea083f31c7570c9931", - "sha256:c01b59b33c7c3ba90744f2c695be571a3bd40ab2ba7f3d169ffa6db3cfba614f", - "sha256:d96a6a7d74af56feb11e9a443150216578ea07b7450f7c05df40eec90af7f4a7", - "sha256:dd0e3651d210068d13e18503d75aaa45656eef51ef0b261f891788589db2cc38", - "sha256:e167b9805de54367dcb2043519382be541117503ce99e3291cc9b41ca0a83557", - "sha256:e42029e184008a5fd3d819323345e25e2337b0ac7f5c135b7623308530209d57", - "sha256:f545c082eeb09ae678dd451a1b1dbf17babd8a0d7adea02897a76e639afca310", - "sha256:fde50062d67d805bc96f1a9ecc0d37bfc2a8f02b937d2c50824d186aa91f2419" + "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.2" + "version": "==1.21.4" }, "ordered-set": { "hashes": [ @@ -349,11 +364,11 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c", - "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c" + "sha256:5f29d62cb7a0ecacfa3d8ceea05a63cd22500543472d64298fc06ddda906b25d", + "sha256:7053aba00895473cb357819358ef33f11aa97e4ac83d38efb123e5649ceeecaf" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.20" + "version": "==3.0.23" }, "pyasn1": { "hashes": [ @@ -400,11 +415,11 @@ }, "pyparsing": { "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", + "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.7" + "markers": "python_version >= '3.6'", + "version": "==3.0.6" }, "pysam": { "hashes": [ @@ -435,10 +450,10 @@ }, "python-ldap": { "hashes": [ - "sha256:4711cacf013e298754abd70058ccc995758177fb425f1c2d30e71adfc1d00aa5" + "sha256:60464c8fc25e71e0fd40449a24eae482dcd0fb7fcf823e7de627a6525b3e0d12" ], "index": "pypi", - "version": "==3.3.1" + "version": "==3.4.0" }, "python-magic": { "hashes": [ @@ -457,38 +472,42 @@ }, "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" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "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": [ @@ -532,6 +551,14 @@ "index": "pypi", "version": "==1.4.3" }, + "setuptools": { + "hashes": [ + "sha256:b4c634615a0cf5b02cf83c7bedffc8da0ca439f00e79452699454da6fbd4153d", + "sha256:feb5ff19b354cde9efd2344ef6d5e79880ce4be643037641b49508bbb850d060" + ], + "markers": "python_version >= '3.6'", + "version": "==59.4.0" + }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", @@ -542,11 +569,11 @@ }, "smmap": { "hashes": [ - "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182", - "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2" + "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94", + "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936" ], - "markers": "python_version >= '3.5'", - "version": "==4.0.0" + "markers": "python_version >= '3.6'", + "version": "==5.0.0" }, "sparqlwrapper": { "hashes": [ @@ -572,15 +599,6 @@ "index": "pypi", "version": "==0.12.6" }, - "typing-extensions": { - "hashes": [ - "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", - "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", - "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" - ], - "markers": "python_version < '3.10'", - "version": "==3.10.0.2" - }, "urllib3": { "hashes": [ "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", @@ -667,11 +685,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", - "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" + "sha256:735e240d9a8506778cd7a453d97e817e536bb1fc29f4f6961ce297b9c7a917b0", + "sha256:83fcdeb225499d6344c8f7f34684c2981270beacc32ede2e669e94f7fa544405" ], "markers": "python_version >= '3'", - "version": "==2.0.7" + "version": "==2.0.8" }, "click": { "hashes": [ @@ -683,7 +701,7 @@ }, "coverage": { "extras": [ - "toml" + ], "hashes": [ "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", @@ -781,11 +799,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15", - "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1" + "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100", + "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb" ], "markers": "python_version >= '3.6'", - "version": "==4.8.1" + "version": "==4.8.2" }, "iniconfig": { "hashes": [ @@ -804,17 +822,18 @@ }, "markdown": { "hashes": [ - "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49", - "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c" + "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006", + "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3" ], "markers": "python_version >= '3.6'", - "version": "==3.3.4" + "version": "==3.3.6" }, "markupsafe": { "hashes": [ "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", @@ -822,6 +841,7 @@ "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", @@ -829,27 +849,36 @@ "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", @@ -857,10 +886,14 @@ "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", @@ -895,11 +928,11 @@ }, "packaging": { "hashes": [ - "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", - "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" ], "markers": "python_version >= '3.6'", - "version": "==21.0" + "version": "==21.3" }, "pluggy": { "hashes": [ @@ -911,11 +944,11 @@ }, "py": { "hashes": [ - "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", - "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "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": [ @@ -935,11 +968,11 @@ }, "pyparsing": { "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", + "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.7" + "markers": "python_version >= '3.6'", + "version": "==3.0.6" }, "pytest": { "hashes": [ @@ -967,38 +1000,42 @@ }, "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" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "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" }, "pyyaml-env-tag": { "hashes": [ @@ -1029,6 +1066,7 @@ "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" }, "urllib3": { From a07c8b7f41b1d08d9e48a4459892ab2d9bd544f6 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 27 Jan 2022 14:28:31 +0100 Subject: [PATCH 130/318] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1574f50..ef6d8420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ This changelog was started for release 4.2.0. - Changed abstraction building method for relations. (Please refer to #248 and #268) - 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 ### Removed From e6b944d018e89fb36467b635f002b36962dbc14c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 17:40:59 +0100 Subject: [PATCH 131/318] Bump follow-redirects from 1.14.7 to 1.14.8 (#304) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f1b8dfe5..5450fa45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5341,9 +5341,9 @@ } }, "follow-redirects": { - "version": "1.14.7", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", - "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==" + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" }, "for-in": { "version": "1.0.2", From cb8bed2c92e9a820969cb0422bc3e1f9a4e4d988 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 17:41:07 +0100 Subject: [PATCH 132/318] Bump mkdocs from 1.0.4 to 1.2.3 in /docs (#303) Bumps [mkdocs](https://github.com/mkdocs/mkdocs) from 1.0.4 to 1.2.3. - [Release notes](https://github.com/mkdocs/mkdocs/releases) - [Commits](https://github.com/mkdocs/mkdocs/compare/1.0.4...1.2.3) --- updated-dependencies: - dependency-name: mkdocs dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 2dfb00fb..100e7c46 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ -mkdocs==1.0.4 +mkdocs==1.2.3 markdown-captions==2 From ab1ec99302b1042d8ee73b5ea8ad1eb2be773604 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Feb 2022 11:31:12 +0100 Subject: [PATCH 133/318] Bump simple-get from 2.8.1 to 2.8.2 (#308) Bumps [simple-get](https://github.com/feross/simple-get) from 2.8.1 to 2.8.2. - [Release notes](https://github.com/feross/simple-get/releases) - [Commits](https://github.com/feross/simple-get/compare/v2.8.1...v2.8.2) --- updated-dependencies: - dependency-name: simple-get dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5450fa45..05d51ffe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9477,9 +9477,9 @@ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" }, "simple-get": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz", - "integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.2.tgz", + "integrity": "sha512-Ijd/rV5o+mSBBs4F/x9oDPtTx9Zb6X9brmnXvMW4J7IR15ngi9q5xxqWBKU744jTZiaXtxaPL7uHG6vtN8kUkw==", "requires": { "decompress-response": "^3.3.0", "once": "^1.3.1", From 314166cadc5248abfbcd6fa066c890c2a6a366ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Feb 2022 11:31:26 +0100 Subject: [PATCH 134/318] Bump ssri from 6.0.1 to 6.0.2 (#307) Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2. - [Release notes](https://github.com/npm/ssri/releases) - [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md) - [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2) --- updated-dependencies: - dependency-name: ssri dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 05d51ffe..0e6b9eca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11102,9 +11102,9 @@ } }, "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", + "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", "dev": true, "requires": { "figgy-pudding": "^3.5.1" From 87f2aa970a24ec5bb96cde288af4f2bc794f602c Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 8 Mar 2022 14:22:31 +0100 Subject: [PATCH 135/318] Update FilesHandler.py (#312) --- askomics/libaskomics/FilesHandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askomics/libaskomics/FilesHandler.py b/askomics/libaskomics/FilesHandler.py index ae5e8bb8..d3d1adfe 100644 --- a/askomics/libaskomics/FilesHandler.py +++ b/askomics/libaskomics/FilesHandler.py @@ -247,7 +247,7 @@ def store_file_info_in_db(self, name, filetype, file_name, size, status="availab 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" From 268b4898e9c7528a1aa79b8531b03a32a8fec23b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Mar 2022 14:22:48 +0100 Subject: [PATCH 136/318] Bump prismjs from 1.25.0 to 1.27.0 (#310) Bumps [prismjs](https://github.com/PrismJS/prism) from 1.25.0 to 1.27.0. - [Release notes](https://github.com/PrismJS/prism/releases) - [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md) - [Commits](https://github.com/PrismJS/prism/compare/v1.25.0...v1.27.0) --- updated-dependencies: - dependency-name: prismjs dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0e6b9eca..c824dea8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8326,9 +8326,9 @@ "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==" }, "prismjs": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz", - "integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==" + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==" }, "private": { "version": "0.1.8", diff --git a/package.json b/package.json index 479003cc..7df880d1 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "immutability-helper": "^3.1.1", "js-file-download": "^0.4.12", "pretty-time": "^1.1.0", - "prismjs": "^1.25.0", + "prismjs": "^1.27.0", "qs": "^6.9.4", "react": "^16.13.1", "react-ace": "^9.1.3", From da6c3fb718e08ec1ac33ce005dd3217f1a8205ce Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 8 Mar 2022 14:25:43 +0100 Subject: [PATCH 137/318] Update CHANGELOG.md --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef6d8420..9c13aebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ This changelog was started for release 4.2.0. - 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 ### Changed @@ -39,11 +40,16 @@ This changelog was started for release 4.2.0. ### Security -- Bump prismjs from 1.23.0 to 1.25.0 - 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 ## [4.3.1] - 2021-06-16 From 6c8f7ae722756c0c8e421918631c1f2d21787149 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 11 Mar 2022 21:39:19 +0100 Subject: [PATCH 138/318] Revert to alpine 13 to solve make issue (#315) * Update Dockerfile * Update DockerfileAll * Update DockerfileCelery --- docker/Dockerfile | 2 +- docker/DockerfileAll | 2 +- docker/DockerfileCelery | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index e3f38ef5..3e7c3de2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/askomics/flaskomics-base:4.0.0-alpine3.14 AS builder +FROM quay.io/askomics/flaskomics-base:4.0.0-alpine3.13 AS builder MAINTAINER "Xavier Garnier " COPY . /askomics diff --git a/docker/DockerfileAll b/docker/DockerfileAll index 402d80bf..fede02e6 100644 --- a/docker/DockerfileAll +++ b/docker/DockerfileAll @@ -1,5 +1,5 @@ # Build AskOmics -FROM quay.io/askomics/flaskomics-base:4.0.0-alpine3.14 AS askomics_builder +FROM quay.io/askomics/flaskomics-base:4.0.0-alpine3.13 AS askomics_builder MAINTAINER "Xavier Garnier " COPY . /askomics diff --git a/docker/DockerfileCelery b/docker/DockerfileCelery index bd11c62e..850d8121 100644 --- a/docker/DockerfileCelery +++ b/docker/DockerfileCelery @@ -1,4 +1,4 @@ -FROM quay.io/askomics/flaskomics-base:4.0.0-alpine3.14 AS builder +FROM quay.io/askomics/flaskomics-base:4.0.0-alpine3.13 AS builder MAINTAINER "Xavier Garnier " COPY . /askomics From 1a2251e8010786d5e953386e739c660299084d1d Mon Sep 17 00:00:00 2001 From: root Date: Fri, 11 Mar 2022 21:53:38 +0000 Subject: [PATCH 139/318] Actually change alpine --- docker/Dockerfile | 2 +- docker/DockerfileAll | 2 +- docker/DockerfileCelery | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 3e7c3de2..10c4902e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -7,7 +7,7 @@ WORKDIR /askomics RUN make clean-config fast-install build # Final image -FROM alpine:3.14 +FROM alpine:3.13 WORKDIR /askomics RUN apk add --no-cache make python3 bash git libc-dev libstdc++ nodejs-current npm openldap-dev diff --git a/docker/DockerfileAll b/docker/DockerfileAll index fede02e6..65224511 100644 --- a/docker/DockerfileAll +++ b/docker/DockerfileAll @@ -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.14 +FROM alpine:3.13 ENV MODE="prod" \ NTASKS="5" \ diff --git a/docker/DockerfileCelery b/docker/DockerfileCelery index 850d8121..db08157a 100644 --- a/docker/DockerfileCelery +++ b/docker/DockerfileCelery @@ -7,7 +7,7 @@ WORKDIR /askomics RUN make clean-config fast-install # Final image -FROM alpine:3.14 +FROM alpine:3.13 WORKDIR /askomics RUN apk add --no-cache make python3 bash git libc-dev libstdc++ openldap-dev From cd1d504329dc9ee366825671ef332bb5efb9ce5d Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 24 Mar 2022 12:07:42 +0100 Subject: [PATCH 140/318] Update RdfFile.py --- askomics/libaskomics/RdfFile.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/askomics/libaskomics/RdfFile.py b/askomics/libaskomics/RdfFile.py index 0954d91f..b1252d01 100644 --- a/askomics/libaskomics/RdfFile.py +++ b/askomics/libaskomics/RdfFile.py @@ -69,6 +69,12 @@ def get_preview(self): for x in range(1, 100): head += ttl_file.readline() + location = None + try: + location = self.get_location() + except Exception as e: + self.error_message = str(e) + return { 'type': self.type, 'id': self.id, @@ -77,7 +83,7 @@ def get_preview(self): 'error_message': self.error_message, 'data': { 'preview': head, - 'location': self.get_location() + 'location': location } } From 0701cf17ccdb0f842254d452189af6362419f957 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 10 Apr 2022 00:22:53 +0000 Subject: [PATCH 141/318] Bump minimist from 1.2.5 to 1.2.6 Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/substack/minimist/releases) - [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6) --- updated-dependencies: - dependency-name: minimist dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c824dea8..0b75625b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7113,9 +7113,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "minipass": { "version": "3.1.3", From bff4203fab33904bc67891904658860155fbc888 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 13 Apr 2022 14:17:30 +0000 Subject: [PATCH 142/318] Speedup startpoints --- askomics/libaskomics/TriplestoreExplorer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askomics/libaskomics/TriplestoreExplorer.py b/askomics/libaskomics/TriplestoreExplorer.py index 0773ceaf..51e6ade8 100644 --- a/askomics/libaskomics/TriplestoreExplorer.py +++ b/askomics/libaskomics/TriplestoreExplorer.py @@ -131,7 +131,7 @@ def get_startpoints(self): filter_user = " || ?creator = <{}>".format(self.session["user"]["username"]) query_launcher = SparqlQueryLauncher(self.app, self.session) - query_builder = SparqlQuery(self.app, self.session) + query_builder = SparqlQuery(self.app, self.session, get_graphs=False) query = ''' SELECT DISTINCT ?endpoint ?graph ?entity ?entity_label ?creator ?public From 49abf95ee12bec6469b6e2faa71d7d928e07c2ab Mon Sep 17 00:00:00 2001 From: root Date: Wed, 13 Apr 2022 15:14:06 +0000 Subject: [PATCH 143/318] Speedup graph query --- askomics/libaskomics/SparqlQuery.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 75fb681d..ed8af369 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -400,12 +400,10 @@ 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: From b7434d12e961e1915171a187f3d63febe6ddbaa8 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 13 Apr 2022 17:32:23 +0200 Subject: [PATCH 144/318] Update SparqlQuery.py --- askomics/libaskomics/SparqlQuery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index ed8af369..4346f574 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -403,7 +403,7 @@ def set_graphs_and_endpoints(self, entities=None, graphs=None, endpoints=None): filter_entity_string = '' if entities: substr = ",".join(["<{}>".format(entity) for entity in entities]) - filter_entity_string = 'FILTER (?entity_uri IN( ' + substr + ' ))' + filter_entity_string = 'FILTER (?entity_uri IN( ' + substr + ' ))' filter_public_string = 'FILTER (?public = )' if 'user' in self.session: From 3815535e31a4848b9040a7f59cbe32d9c784f3c0 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 14 Apr 2022 10:44:29 +0200 Subject: [PATCH 145/318] Update TriplestoreExplorer.py --- askomics/libaskomics/TriplestoreExplorer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/askomics/libaskomics/TriplestoreExplorer.py b/askomics/libaskomics/TriplestoreExplorer.py index 51e6ade8..2e807bda 100644 --- a/askomics/libaskomics/TriplestoreExplorer.py +++ b/askomics/libaskomics/TriplestoreExplorer.py @@ -318,7 +318,7 @@ def get_abstraction_entities(self): filter_user = " || ?creator = <{}>".format(self.session["user"]["username"]) query_launcher = SparqlQueryLauncher(self.app, self.session) - query_builder = SparqlQuery(self.app, self.session) + query_builder = SparqlQuery(self.app, self.session, get_graphs=False) query = ''' SELECT DISTINCT ?endpoint ?graph ?entity_uri ?entity_type ?entity_faldo ?entity_label ?have_no_label @@ -398,7 +398,7 @@ def get_abstraction_attributes(self): ) query_launcher = SparqlQueryLauncher(self.app, self.session) - query_builder = SparqlQuery(self.app, self.session) + query_builder = SparqlQuery(self.app, self.session, get_graphs=False) query = ''' SELECT DISTINCT ?graph ?entity_uri ?attribute_uri ?attribute_type ?attribute_faldo ?attribute_label ?attribute_range ?category_value_uri ?category_value_label @@ -510,7 +510,7 @@ def get_abstraction_relations(self): filter_user = " || ?creator = <{}>".format(self.session["user"]["username"]) query_launcher = SparqlQueryLauncher(self.app, self.session) - query_builder = SparqlQuery(self.app, self.session) + query_builder = SparqlQuery(self.app, self.session, get_graphs=False) query = ''' SELECT DISTINCT ?graph ?entity_uri ?entity_faldo ?entity_label ?node ?node_type ?attribute_uri ?attribute_faldo ?attribute_label ?attribute_range ?property_uri ?property_faldo ?property_label ?range_uri ?category_value_uri ?category_value_label From 4508fc90ab1faf91a644c2f289d9abeda0b858df Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 14 Apr 2022 10:54:35 +0200 Subject: [PATCH 146/318] 1/? --- askomics/libaskomics/TriplestoreExplorer.py | 71 +++++++++++++-------- askomics/tasks.py | 2 +- config/askomics.ini.template | 3 + config/askomics.test.ini | 2 + 4 files changed, 51 insertions(+), 27 deletions(-) diff --git a/askomics/libaskomics/TriplestoreExplorer.py b/askomics/libaskomics/TriplestoreExplorer.py index 0773ceaf..4e682f9a 100644 --- a/askomics/libaskomics/TriplestoreExplorer.py +++ b/askomics/libaskomics/TriplestoreExplorer.py @@ -127,8 +127,14 @@ def get_startpoints(self): Startpoints """ filter_user = "" - if self.logged_user(): - filter_user = " || ?creator = <{}>".format(self.session["user"]["username"]) + if not self.settings.get("single_tenant", False): + filter_user = ''' + FILTER ( + ?public = {} + ) + ''' + if self.logged_user(): + filter_user.format(" || ?creator = <{}>".format(self.session["user"]["username"])) query_launcher = SparqlQueryLauncher(self.app, self.session) query_builder = SparqlQuery(self.app, self.session) @@ -144,9 +150,7 @@ def get_startpoints(self): ?entity a askomics:startPoint . ?entity rdfs:label ?entity_label . }} - FILTER ( - ?public = {} - ) + {} }} '''.format(filter_user) @@ -208,12 +212,14 @@ def get_abstraction(self): """ insert, abstraction = self.get_cached_asbtraction() + single_tenant = self.settings.get("single_tenant", 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 +311,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 @@ -313,9 +319,16 @@ def get_abstraction_entities(self): list List of entities available """ + filter_user = "" - if self.logged_user(): - filter_user = " || ?creator = <{}>".format(self.session["user"]["username"]) + if not single_tenant: + filter_user = ''' + FILTER ( + ?public = {} + ) + ''' + if self.logged_user(): + filter_user.format(" || ?creator = <{}>".format(self.session["user"]["username"])) query_launcher = SparqlQueryLauncher(self.app, self.session) query_builder = SparqlQuery(self.app, self.session) @@ -338,9 +351,7 @@ def get_abstraction_entities(self): OPTIONAL {{ ?entity_uri rdfs:label ?entity_label . }} OPTIONAL {{ ?entity_uri askomics:instancesHaveNoLabels ?have_no_label . }} }} - FILTER ( - ?public = {} - ) + {} }} '''.format(filter_user) @@ -378,7 +389,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 @@ -387,8 +398,14 @@ def get_abstraction_attributes(self): AskOmics attributes """ filter_user = "" - if self.logged_user(): - filter_user = " || ?creator = <{}>".format(self.session["user"]["username"]) + if not single_tenant: + filter_user = ''' + FILTER ( + ?public = {} + ) + ''' + if self.logged_user(): + filter_user.format(" || ?creator = <{}>".format(self.session["user"]["username"])) litterals = ( "http://www.w3.org/2001/XMLSchema#string", @@ -429,9 +446,7 @@ def get_abstraction_attributes(self): }} UNION {{ ?attribute_uri rdfs:domain ?entity_uri . }} - FILTER ( - ?public = {} - ) + {} }} '''.format(filter_user) @@ -497,7 +512,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 @@ -506,8 +521,14 @@ def get_abstraction_relations(self): Relations """ filter_user = "" - if self.logged_user(): - filter_user = " || ?creator = <{}>".format(self.session["user"]["username"]) + if not single_tenant: + filter_user = ''' + FILTER ( + ?public = {} + ) + ''' + if self.logged_user(): + filter_user.format(" || ?creator = <{}>".format(self.session["user"]["username"])) query_launcher = SparqlQueryLauncher(self.app, self.session) query_builder = SparqlQuery(self.app, self.session) @@ -534,9 +555,7 @@ def get_abstraction_relations(self): }} UNION {{ ?node rdfs:domain ?entity_uri . }} - FILTER ( - ?public = {} - ) + {} }} '''.format(filter_user) diff --git a/askomics/tasks.py b/askomics/tasks.py index 9d2bad0e..c4337ff3 100644 --- a/askomics/tasks.py +++ b/askomics/tasks.py @@ -158,7 +158,7 @@ def query(self, session, info): headers = info["selects"] results = [] - if info["graphs"]: + if info["graphs"] or app.iniconfig.get("single_tenant", 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) diff --git a/config/askomics.ini.template b/config/askomics.ini.template index 9e8001ba..dd1070a9 100644 --- a/config/askomics.ini.template +++ b/config/askomics.ini.template @@ -124,6 +124,9 @@ preview_limit = 25 # Triplestore max rows limit # result_set_max_rows = 10000 +# Single tenant means all graphs are public +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 6165a1cd..f2afac84 100644 --- a/config/askomics.test.ini +++ b/config/askomics.test.ini @@ -118,6 +118,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 From add9dbab5aecf8b8dc431cb659aa30a04240d1dd Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 14 Apr 2022 14:37:31 +0200 Subject: [PATCH 147/318] Set get_graph to False as default --- askomics/api/data.py | 2 +- askomics/api/query.py | 4 ++-- askomics/api/results.py | 4 ++-- askomics/api/sparql.py | 6 +++--- askomics/libaskomics/SparqlQuery.py | 2 +- askomics/libaskomics/TriplestoreExplorer.py | 8 ++++---- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/askomics/api/data.py b/askomics/api/data.py index 4710b18b..f09b8695 100644 --- a/askomics/api/data.py +++ b/askomics/api/data.py @@ -25,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()] diff --git a/askomics/api/query.py b/askomics/api/query.py index 1de59ddd..9ea82e77 100644 --- a/askomics/api/query.py +++ b/askomics/api/query.py @@ -125,7 +125,7 @@ def get_preview(): 'errorMessage': "Missing graphState parameter" }), 400 - query = SparqlQuery(current_app, session, data["graphState"], get_graphs=False) + query = SparqlQuery(current_app, session, data["graphState"]) query.build_query_from_json(preview=True, for_editor=False) endpoints = query.endpoints @@ -186,7 +186,7 @@ def save_result(): 'errorMessage': "Missing graphState parameter" }), 400 - query = SparqlQuery(current_app, session, data["graphState"], get_graphs=False) + query = SparqlQuery(current_app, session, data["graphState"]) query.build_query_from_json(preview=False, for_editor=False) federated = query.is_federated() diff --git a/askomics/api/results.py b/askomics/api/results.py index 97c394b3..c0092f32 100644 --- a/askomics/api/results.py +++ b/askomics/api/results.py @@ -164,7 +164,7 @@ 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['user']) @@ -361,7 +361,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 8a75fcb1..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 @@ -116,7 +116,7 @@ def query(): }), 400 try: - query = SparqlQuery(current_app, session, get_graphs=False) + query = SparqlQuery(current_app, session) query.set_graphs_and_endpoints(graphs=graphs, endpoints=endpoints) @@ -209,7 +209,7 @@ def save_query(): }), 400 # Is query federated? - query = SparqlQuery(current_app, session, get_graphs=False) + query = SparqlQuery(current_app, session) query.set_graphs_and_endpoints(graphs=graphs, endpoints=endpoints) federated = query.is_federated() diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 4346f574..2476dd6b 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -18,7 +18,7 @@ class SparqlQuery(Params): all public graph """ - def __init__(self, app, session, json_query=None, get_graphs=True): + def __init__(self, app, session, json_query=None, get_graphs=False): """init Parameters diff --git a/askomics/libaskomics/TriplestoreExplorer.py b/askomics/libaskomics/TriplestoreExplorer.py index 2e807bda..0773ceaf 100644 --- a/askomics/libaskomics/TriplestoreExplorer.py +++ b/askomics/libaskomics/TriplestoreExplorer.py @@ -131,7 +131,7 @@ def get_startpoints(self): filter_user = " || ?creator = <{}>".format(self.session["user"]["username"]) query_launcher = SparqlQueryLauncher(self.app, self.session) - query_builder = SparqlQuery(self.app, self.session, get_graphs=False) + query_builder = SparqlQuery(self.app, self.session) query = ''' SELECT DISTINCT ?endpoint ?graph ?entity ?entity_label ?creator ?public @@ -318,7 +318,7 @@ def get_abstraction_entities(self): filter_user = " || ?creator = <{}>".format(self.session["user"]["username"]) query_launcher = SparqlQueryLauncher(self.app, self.session) - query_builder = SparqlQuery(self.app, self.session, get_graphs=False) + query_builder = SparqlQuery(self.app, self.session) query = ''' SELECT DISTINCT ?endpoint ?graph ?entity_uri ?entity_type ?entity_faldo ?entity_label ?have_no_label @@ -398,7 +398,7 @@ def get_abstraction_attributes(self): ) query_launcher = SparqlQueryLauncher(self.app, self.session) - query_builder = SparqlQuery(self.app, self.session, get_graphs=False) + query_builder = SparqlQuery(self.app, self.session) query = ''' SELECT DISTINCT ?graph ?entity_uri ?attribute_uri ?attribute_type ?attribute_faldo ?attribute_label ?attribute_range ?category_value_uri ?category_value_label @@ -510,7 +510,7 @@ def get_abstraction_relations(self): filter_user = " || ?creator = <{}>".format(self.session["user"]["username"]) query_launcher = SparqlQueryLauncher(self.app, self.session) - query_builder = SparqlQuery(self.app, self.session, get_graphs=False) + query_builder = SparqlQuery(self.app, self.session) query = ''' SELECT DISTINCT ?graph ?entity_uri ?entity_faldo ?entity_label ?node ?node_type ?attribute_uri ?attribute_faldo ?attribute_label ?attribute_range ?property_uri ?property_faldo ?property_label ?range_uri ?category_value_uri ?category_value_label From 2a9b96fc4dc5de674b602c06802d65f1fd74121b Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 14 Apr 2022 14:45:47 +0200 Subject: [PATCH 148/318] typo1 --- askomics/api/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askomics/api/data.py b/askomics/api/data.py index f09b8695..24a26cdd 100644 --- a/askomics/api/data.py +++ b/askomics/api/data.py @@ -25,7 +25,7 @@ def get_data(uri): """ try: - query = SparqlQuery(current_app, session, get_graphs=True)) + 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()] From 4cca82b502e2e6e70bad7c8c2f915543ef4e0d79 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 14 Apr 2022 15:18:46 +0200 Subject: [PATCH 149/318] move query logic to celery to speedup response --- askomics/api/query.py | 11 +---------- askomics/libaskomics/Result.py | 29 +++++++++++++++++++++++++++++ askomics/tasks.py | 12 ++++++++++++ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/askomics/api/query.py b/askomics/api/query.py index 9ea82e77..45dd8c6d 100644 --- a/askomics/api/query.py +++ b/askomics/api/query.py @@ -186,18 +186,9 @@ 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) - federated = query.is_federated() - info = { "graph_state": data["graphState"], - "query": query.sparql, - "graphs": query.graphs, - "endpoints": query.endpoints, - "federated": federated, - "celery_id": None, - "selects": query.selects, + "celery_id": None } result = Result(current_app, session, info) diff --git a/askomics/libaskomics/Result.py b/askomics/libaskomics/Result.py index 07e74ca5..14367629 100644 --- a/askomics/libaskomics/Result.py +++ b/askomics/libaskomics/Result.py @@ -389,6 +389,35 @@ def save_in_db(self): return self.id + def populate_db(self, sparql_query, 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 + sparql_query=? + graphs_and_endpoints=? + WHERE user_id=? AND id=? + ''' + + variables = [ + sparql_query, + 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 diff --git a/askomics/tasks.py b/askomics/tasks.py index 9d2bad0e..86db1eaa 100644 --- a/askomics/tasks.py +++ b/askomics/tasks.py @@ -16,6 +16,7 @@ from askomics.libaskomics.FilesHandler import FilesHandler from askomics.libaskomics.LocalAuth import LocalAuth from askomics.libaskomics.Result import Result +from askomics.libaskomics.Sparql import SparqlQuery from askomics.libaskomics.SparqlQueryLauncher import SparqlQueryLauncher @@ -150,6 +151,17 @@ 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["graphState"]) + query.build_query_from_json(preview=False, for_editor=False) + federated = query.is_federated() + result.populate_db(query.sparql, 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) From befde35b008a16178068f2e1e6fc304795b3517d Mon Sep 17 00:00:00 2001 From: root Date: Thu, 14 Apr 2022 14:14:10 +0000 Subject: [PATCH 150/318] Typi --- askomics/libaskomics/Result.py | 6 ++---- askomics/tasks.py | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/askomics/libaskomics/Result.py b/askomics/libaskomics/Result.py index 14367629..d48f7725 100644 --- a/askomics/libaskomics/Result.py +++ b/askomics/libaskomics/Result.py @@ -389,7 +389,7 @@ def save_in_db(self): return self.id - def populate_db(self, sparql_query, graphs, endpoints): + def populate_db(self, graphs, endpoints): """Update status of results in db Parameters @@ -404,13 +404,11 @@ def populate_db(self, sparql_query, graphs, endpoints): query = ''' UPDATE results SET - sparql_query=? graphs_and_endpoints=? WHERE user_id=? AND id=? ''' variables = [ - sparql_query, json.dumps({"graphs": graphs, "endpoints": endpoints}), self.session["user"]["id"], self.id @@ -500,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/tasks.py b/askomics/tasks.py index 86db1eaa..00a67d90 100644 --- a/askomics/tasks.py +++ b/askomics/tasks.py @@ -16,7 +16,7 @@ from askomics.libaskomics.FilesHandler import FilesHandler from askomics.libaskomics.LocalAuth import LocalAuth from askomics.libaskomics.Result import Result -from askomics.libaskomics.Sparql import SparqlQuery +from askomics.libaskomics.SparqlQuery import SparqlQuery from askomics.libaskomics.SparqlQueryLauncher import SparqlQueryLauncher @@ -151,10 +151,10 @@ 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["graphState"]) + 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.sparql, query.graphs, query.endpoints) + result.populate_db(query.graphs, query.endpoints) info["query"] = query.sparql info["graphs"] = query.graphs From 71a58ce944e53d40caac0105887662627b1cd3f9 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 4 May 2022 15:09:00 +0200 Subject: [PATCH 151/318] Api --- askomics/api/datasets.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/askomics/api/datasets.py b/askomics/api/datasets.py index bceb125e..9cf0b152 100644 --- a/askomics/api/datasets.py +++ b/askomics/api/datasets.py @@ -119,6 +119,14 @@ def toogle_public(): error: True if error, else False errorMessage: the error message of error, else an empty string """ + + if current_app.iniconfig.get("single_tenant", False): + return jsonify({ + 'files': [], + 'error': True, + 'errorMessage': 'Cannot change dataset public status: \nSingle tenant mode' + }), 401 + data = request.get_json() if not (data and data.get("id")): return jsonify({ From ae95eac52365760889d424f16c89551c0295dbaa Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 5 May 2022 10:48:24 +0200 Subject: [PATCH 152/318] Fix config --- askomics/api/datasets.py | 2 +- askomics/libaskomics/SparqlQuery.py | 2 +- askomics/libaskomics/TriplestoreExplorer.py | 4 ++-- askomics/tasks.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/askomics/api/datasets.py b/askomics/api/datasets.py index 9cf0b152..758616f2 100644 --- a/askomics/api/datasets.py +++ b/askomics/api/datasets.py @@ -120,7 +120,7 @@ def toogle_public(): errorMessage: the error message of error, else an empty string """ - if current_app.iniconfig.get("single_tenant", False): + if current_app.iniconfig.get("askomics", "single_tenant", fallback=False): return jsonify({ 'files': [], 'error': True, diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index 2476dd6b..e54e8361 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -1376,7 +1376,7 @@ 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.get("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() diff --git a/askomics/libaskomics/TriplestoreExplorer.py b/askomics/libaskomics/TriplestoreExplorer.py index 4e682f9a..327d4d76 100644 --- a/askomics/libaskomics/TriplestoreExplorer.py +++ b/askomics/libaskomics/TriplestoreExplorer.py @@ -127,7 +127,7 @@ def get_startpoints(self): Startpoints """ filter_user = "" - if not self.settings.get("single_tenant", False): + if not self.settings.get("askomics", "single_tenant", fallback=False): filter_user = ''' FILTER ( ?public = {} @@ -212,7 +212,7 @@ def get_abstraction(self): """ insert, abstraction = self.get_cached_asbtraction() - single_tenant = self.settings.get("single_tenant", False) + single_tenant = self.settings.get("askomics", "single_tenant", fallback=False) # No abstraction entry in database, create it if not abstraction: diff --git a/askomics/tasks.py b/askomics/tasks.py index 61eb37a3..cba7a733 100644 --- a/askomics/tasks.py +++ b/askomics/tasks.py @@ -170,7 +170,7 @@ def query(self, session, info): headers = info["selects"] results = [] - if info["graphs"] or app.iniconfig.get("single_tenant", False): + if info["graphs"] or app.iniconfig.get("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) From 1ec62ab777003e9b4260c4a897aee94551913719 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 5 May 2022 11:13:15 +0200 Subject: [PATCH 153/318] fix --- askomics/libaskomics/TriplestoreExplorer.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/askomics/libaskomics/TriplestoreExplorer.py b/askomics/libaskomics/TriplestoreExplorer.py index 327d4d76..7147c3d0 100644 --- a/askomics/libaskomics/TriplestoreExplorer.py +++ b/askomics/libaskomics/TriplestoreExplorer.py @@ -128,13 +128,15 @@ def get_startpoints(self): """ filter_user = "" if not self.settings.get("askomics", "single_tenant", fallback=False): + substring = "" filter_user = ''' FILTER ( ?public = {} ) ''' if self.logged_user(): - filter_user.format(" || ?creator = <{}>".format(self.session["user"]["username"])) + substring = " || ?creator = <{}>".format(self.session["user"]["username"]) + filter_user.format(substring) query_launcher = SparqlQueryLauncher(self.app, self.session) query_builder = SparqlQuery(self.app, self.session) @@ -322,13 +324,15 @@ def get_abstraction_entities(self, single_tenant=False): filter_user = "" if not single_tenant: + substring = "" filter_user = ''' FILTER ( ?public = {} ) ''' if self.logged_user(): - filter_user.format(" || ?creator = <{}>".format(self.session["user"]["username"])) + substring = " || ?creator = <{}>".format(self.session["user"]["username"]) + filter_user.format(substring) query_launcher = SparqlQueryLauncher(self.app, self.session) query_builder = SparqlQuery(self.app, self.session) @@ -399,13 +403,15 @@ def get_abstraction_attributes(self, single_tenant=False): """ filter_user = "" if not single_tenant: + substring = "" filter_user = ''' FILTER ( ?public = {} ) ''' if self.logged_user(): - filter_user.format(" || ?creator = <{}>".format(self.session["user"]["username"])) + substring = " || ?creator = <{}>".format(self.session["user"]["username"]) + filter_user.format(substring) litterals = ( "http://www.w3.org/2001/XMLSchema#string", @@ -522,13 +528,15 @@ def get_abstraction_relations(self, single_tenant=False): """ filter_user = "" if not single_tenant: + substring = "" filter_user = ''' FILTER ( ?public = {} ) ''' if self.logged_user(): - filter_user.format(" || ?creator = <{}>".format(self.session["user"]["username"])) + substring = " || ?creator = <{}>".format(self.session["user"]["username"]) + filter_user.format(substring) query_launcher = SparqlQueryLauncher(self.app, self.session) query_builder = SparqlQuery(self.app, self.session) From e2763ac581ced54aaaaaab589573dbd11528b26e Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 5 May 2022 11:44:35 +0200 Subject: [PATCH 154/318] rollback --- askomics/libaskomics/TriplestoreExplorer.py | 65 +++++++-------------- askomics/tasks.py | 2 +- 2 files changed, 21 insertions(+), 46 deletions(-) diff --git a/askomics/libaskomics/TriplestoreExplorer.py b/askomics/libaskomics/TriplestoreExplorer.py index 7147c3d0..d39ce7e2 100644 --- a/askomics/libaskomics/TriplestoreExplorer.py +++ b/askomics/libaskomics/TriplestoreExplorer.py @@ -127,16 +127,8 @@ def get_startpoints(self): Startpoints """ filter_user = "" - if not self.settings.get("askomics", "single_tenant", fallback=False): - substring = "" - filter_user = ''' - FILTER ( - ?public = {} - ) - ''' - if self.logged_user(): - substring = " || ?creator = <{}>".format(self.session["user"]["username"]) - filter_user.format(substring) + if self.logged_user(): + filter_user = " || ?creator = <{}>".format(self.session["user"]["username"]) query_launcher = SparqlQueryLauncher(self.app, self.session) query_builder = SparqlQuery(self.app, self.session) @@ -152,7 +144,9 @@ def get_startpoints(self): ?entity a askomics:startPoint . ?entity rdfs:label ?entity_label . }} - {} + FILTER ( + ?public = {} + ) }} '''.format(filter_user) @@ -321,18 +315,9 @@ def get_abstraction_entities(self, single_tenant=False): list List of entities available """ - filter_user = "" - if not single_tenant: - substring = "" - filter_user = ''' - FILTER ( - ?public = {} - ) - ''' - if self.logged_user(): - substring = " || ?creator = <{}>".format(self.session["user"]["username"]) - filter_user.format(substring) + if self.logged_user(): + filter_user = " || ?creator = <{}>".format(self.session["user"]["username"]) query_launcher = SparqlQueryLauncher(self.app, self.session) query_builder = SparqlQuery(self.app, self.session) @@ -355,7 +340,9 @@ def get_abstraction_entities(self, single_tenant=False): OPTIONAL {{ ?entity_uri rdfs:label ?entity_label . }} OPTIONAL {{ ?entity_uri askomics:instancesHaveNoLabels ?have_no_label . }} }} - {} + FILTER ( + ?public = {} + ) }} '''.format(filter_user) @@ -402,16 +389,8 @@ def get_abstraction_attributes(self, single_tenant=False): AskOmics attributes """ filter_user = "" - if not single_tenant: - substring = "" - filter_user = ''' - FILTER ( - ?public = {} - ) - ''' - if self.logged_user(): - substring = " || ?creator = <{}>".format(self.session["user"]["username"]) - filter_user.format(substring) + if self.logged_user(): + filter_user = " || ?creator = <{}>".format(self.session["user"]["username"]) litterals = ( "http://www.w3.org/2001/XMLSchema#string", @@ -452,7 +431,9 @@ def get_abstraction_attributes(self, single_tenant=False): }} UNION {{ ?attribute_uri rdfs:domain ?entity_uri . }} - {} + FILTER ( + ?public = {} + ) }} '''.format(filter_user) @@ -527,16 +508,8 @@ def get_abstraction_relations(self, single_tenant=False): Relations """ filter_user = "" - if not single_tenant: - substring = "" - filter_user = ''' - FILTER ( - ?public = {} - ) - ''' - if self.logged_user(): - substring = " || ?creator = <{}>".format(self.session["user"]["username"]) - filter_user.format(substring) + if self.logged_user(): + filter_user = " || ?creator = <{}>".format(self.session["user"]["username"]) query_launcher = SparqlQueryLauncher(self.app, self.session) query_builder = SparqlQuery(self.app, self.session) @@ -563,7 +536,9 @@ def get_abstraction_relations(self, single_tenant=False): }} UNION {{ ?node rdfs:domain ?entity_uri . }} - {} + FILTER ( + ?public = {} + ) }} '''.format(filter_user) diff --git a/askomics/tasks.py b/askomics/tasks.py index cba7a733..820cecc1 100644 --- a/askomics/tasks.py +++ b/askomics/tasks.py @@ -47,7 +47,7 @@ def integrate(self, session, data, host_url): files_handler = FilesHandler(app, session, host_url=host_url, external_endpoint=data["externalEndpoint"], custom_uri=data["customUri"]) 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 app.iniconfig.get("askomics", "single_tenant", fallback=False) for file in files_handler.files: From 3ae8d0b802b9693091c304d7d1fd31e599079b96 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 5 May 2022 13:47:51 +0200 Subject: [PATCH 155/318] Remove limitation for admin --- askomics/api/datasets.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/askomics/api/datasets.py b/askomics/api/datasets.py index 758616f2..b31e510c 100644 --- a/askomics/api/datasets.py +++ b/askomics/api/datasets.py @@ -120,13 +120,6 @@ def toogle_public(): errorMessage: the error message of error, else an empty string """ - if current_app.iniconfig.get("askomics", "single_tenant", fallback=False): - return jsonify({ - 'files': [], - 'error': True, - 'errorMessage': 'Cannot change dataset public status: \nSingle tenant mode' - }), 401 - data = request.get_json() if not (data and data.get("id")): return jsonify({ From 1961b68b0104bde39efe5a7d56c2be338a295e92 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 5 May 2022 14:38:33 +0000 Subject: [PATCH 156/318] Fixes --- askomics/api/file.py | 2 +- askomics/tasks.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/askomics/api/file.py b/askomics/api/file.py index 45be76a0..7705f4e9 100644 --- a/askomics/api/file.py +++ b/askomics/api/file.py @@ -339,7 +339,7 @@ def integrate(): "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) } dataset = Dataset(current_app, session, dataset_info) diff --git a/askomics/tasks.py b/askomics/tasks.py index 820cecc1..408abbc8 100644 --- a/askomics/tasks.py +++ b/askomics/tasks.py @@ -47,7 +47,8 @@ def integrate(self, session, data, host_url): files_handler = FilesHandler(app, session, host_url=host_url, external_endpoint=data["externalEndpoint"], custom_uri=data["customUri"]) files_handler.handle_files([data["fileId"], ]) - public = data.get("public", False) if session["user"]["admin"] else app.iniconfig.get("askomics", "single_tenant", fallback=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: @@ -170,7 +171,7 @@ def query(self, session, info): headers = info["selects"] results = [] - if info["graphs"] or app.iniconfig.get("askomics", "single_tenant", fallback=False): + 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) From eda77fdd992093462df68b3c26dff1ab3dbda165 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 5 May 2022 14:39:10 +0000 Subject: [PATCH 157/318] Fix2 --- askomics/libaskomics/SparqlQuery.py | 2 +- askomics/libaskomics/SparqlQueryLauncher.py | 1 + askomics/libaskomics/TriplestoreExplorer.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index e54e8361..d977c719 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -1376,7 +1376,7 @@ def build_query_from_json(self, preview=False, for_editor=False): )) var_to_replace.append((category_value_uri, var_2)) - from_string = "" if self.settings.get("askomics", "single_tenant", fallback=False) else 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() diff --git a/askomics/libaskomics/SparqlQueryLauncher.py b/askomics/libaskomics/SparqlQueryLauncher.py index 6c250619..0907402e 100644 --- a/askomics/libaskomics/SparqlQueryLauncher.py +++ b/askomics/libaskomics/SparqlQueryLauncher.py @@ -294,6 +294,7 @@ def execute_query(self, query, disable_log=False, isql_api=False): # Debug if self.settings.getboolean('askomics', 'debug'): + print(query) self.log.debug("Launch {} query on {} ({})".format("ISQL" if use_isql else "SPARQL", self.triplestore, self.url_endpoint)) self.log.debug(query) diff --git a/askomics/libaskomics/TriplestoreExplorer.py b/askomics/libaskomics/TriplestoreExplorer.py index d39ce7e2..7aadaf2b 100644 --- a/askomics/libaskomics/TriplestoreExplorer.py +++ b/askomics/libaskomics/TriplestoreExplorer.py @@ -208,7 +208,7 @@ def get_abstraction(self): """ insert, abstraction = self.get_cached_asbtraction() - single_tenant = self.settings.get("askomics", "single_tenant", fallback=False) + single_tenant = self.settings.getboolean("askomics", "single_tenant", fallback=False) # No abstraction entry in database, create it if not abstraction: From 39b003bf904e32c81548b5507311671c465ee2e2 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 6 May 2022 11:07:14 +0200 Subject: [PATCH 158/318] React stuff --- askomics/api/start.py | 3 ++- askomics/react/src/routes.jsx | 3 ++- askomics/react/src/routes/integration/bedpreview.jsx | 7 +++++-- askomics/react/src/routes/integration/csvtable.jsx | 7 +++++-- askomics/react/src/routes/integration/gffpreview.jsx | 6 +++++- askomics/react/src/routes/integration/rdfpreview.jsx | 7 +++++-- askomics/tasks.py | 1 - tests/test_api.py | 3 ++- 8 files changed, 26 insertions(+), 11 deletions(-) diff --git a/askomics/api/start.py b/askomics/api/start.py index eca8f91b..442314f8 100644 --- a/askomics/api/start.py +++ b/askomics/api/start.py @@ -79,7 +79,8 @@ def start(): "namespaceInternal": current_app.iniconfig.get('triplestore', 'namespace_internal'), "proxyPath": proxy_path, "user": {}, - "logged": False + "logged": False, + "singleTenant": current_app.iniconfig.getboolean('askomics', 'single_tenant', fallback=False) } json = { diff --git a/askomics/react/src/routes.jsx b/askomics/react/src/routes.jsx index 6555c070..28fa4d3c 100644 --- a/askomics/react/src/routes.jsx +++ b/askomics/react/src/routes.jsx @@ -44,7 +44,8 @@ export default class Routes extends Component { gitUrl: null, disableIntegration: null, namespaceData: null, - namespaceInternal: null + namespaceInternal: null, + singleTenant: false } } this.cancelRequest diff --git a/askomics/react/src/routes/integration/bedpreview.jsx b/askomics/react/src/routes/integration/bedpreview.jsx index acbad90e..86385b58 100644 --- a/askomics/react/src/routes/integration/bedpreview.jsx +++ b/askomics/react/src/routes/integration/bedpreview.jsx @@ -91,7 +91,10 @@ export default class BedPreview extends Component { if (this.state.publicTick) { publicIcon = } - + let privateButton + if (this.props.config.user.admin || !this.props.config.singleTenant){ + privateButton = + } let publicButton if (this.props.config.user.admin) { publicButton = @@ -124,7 +127,7 @@ export default class BedPreview extends Component {
- + {privateButton} {publicButton}
diff --git a/askomics/react/src/routes/integration/csvtable.jsx b/askomics/react/src/routes/integration/csvtable.jsx index efee1315..6320c9cb 100644 --- a/askomics/react/src/routes/integration/csvtable.jsx +++ b/askomics/react/src/routes/integration/csvtable.jsx @@ -240,7 +240,10 @@ export default class CsvTable extends Component { if (this.state.publicTick) { publicIcon = } - + let privateButton + if (this.props.config.user.admin || !this.props.config.singleTenant){ + privateButton = + } let publicButton if (this.props.config.user.admin) { publicButton = @@ -276,7 +279,7 @@ export default class CsvTable extends Component {
- + {privateButton} {publicButton}
diff --git a/askomics/react/src/routes/integration/gffpreview.jsx b/askomics/react/src/routes/integration/gffpreview.jsx index 7a3bdddb..23db590b 100644 --- a/askomics/react/src/routes/integration/gffpreview.jsx +++ b/askomics/react/src/routes/integration/gffpreview.jsx @@ -100,6 +100,10 @@ export default class GffPreview extends Component { if (this.state.publicTick) { publicIcon = } + let privateButton + if (this.props.config.user.admin || !this.props.config.singleTenant){ + privateButton = + } let publicButton if (this.props.config.user.admin) { publicButton = @@ -130,7 +134,7 @@ export default class GffPreview extends Component {
- + {privateButton} {publicButton}
diff --git a/askomics/react/src/routes/integration/rdfpreview.jsx b/askomics/react/src/routes/integration/rdfpreview.jsx index 4d62c6e5..c4894d14 100644 --- a/askomics/react/src/routes/integration/rdfpreview.jsx +++ b/askomics/react/src/routes/integration/rdfpreview.jsx @@ -107,7 +107,10 @@ export default class RdfPreview extends Component { if (this.state.publicTick) { publicIcon = } - + let privateButton + if (this.props.config.user.admin || !this.props.config.singleTenant){ + privateButton = + } let publicButton if (this.props.config.user.admin) { publicButton = @@ -144,7 +147,7 @@ export default class RdfPreview extends Component {
- + {privateButton} {publicButton}
diff --git a/askomics/tasks.py b/askomics/tasks.py index 408abbc8..285cd6a8 100644 --- a/askomics/tasks.py +++ b/askomics/tasks.py @@ -47,7 +47,6 @@ def integrate(self, session, data, host_url): files_handler = FilesHandler(app, session, host_url=host_url, external_endpoint=data["externalEndpoint"], custom_uri=data["customUri"]) files_handler.handle_files([data["fileId"], ]) - 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: diff --git a/tests/test_api.py b/tests/test_api.py index 7e00b700..1fad464a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -42,7 +42,8 @@ def test_start(self, client): "namespaceInternal": client.get_config('triplestore', 'namespace_internal'), "proxyPath": "/", "user": {}, - "logged": False + "logged": False, + "singleTenant": False } response = client.client.get('/api/start') assert response.status_code == 200 From aae80d51beea27a0efc27b55f055fa8b3e13703a Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 6 May 2022 09:26:52 +0000 Subject: [PATCH 159/318] typo --- askomics/react/src/routes/integration/bedpreview.jsx | 2 +- askomics/react/src/routes/integration/csvtable.jsx | 2 +- askomics/react/src/routes/integration/gffpreview.jsx | 2 +- askomics/react/src/routes/integration/rdfpreview.jsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/askomics/react/src/routes/integration/bedpreview.jsx b/askomics/react/src/routes/integration/bedpreview.jsx index 86385b58..90e72d19 100644 --- a/askomics/react/src/routes/integration/bedpreview.jsx +++ b/askomics/react/src/routes/integration/bedpreview.jsx @@ -96,7 +96,7 @@ export default class BedPreview extends Component { privateButton = } let publicButton - if (this.props.config.user.admin) { + if (this.props.config.user.admin || this.props.config.singleTenant) { publicButton = } diff --git a/askomics/react/src/routes/integration/csvtable.jsx b/askomics/react/src/routes/integration/csvtable.jsx index 6320c9cb..cb766fff 100644 --- a/askomics/react/src/routes/integration/csvtable.jsx +++ b/askomics/react/src/routes/integration/csvtable.jsx @@ -245,7 +245,7 @@ export default class CsvTable extends Component { privateButton = } let publicButton - if (this.props.config.user.admin) { + if (this.props.config.user.admin || this.props.config.singleTenant) { publicButton = } diff --git a/askomics/react/src/routes/integration/gffpreview.jsx b/askomics/react/src/routes/integration/gffpreview.jsx index 23db590b..0fafbbef 100644 --- a/askomics/react/src/routes/integration/gffpreview.jsx +++ b/askomics/react/src/routes/integration/gffpreview.jsx @@ -105,7 +105,7 @@ export default class GffPreview extends Component { privateButton = } let publicButton - if (this.props.config.user.admin) { + if (this.props.config.user.admin || this.props.config.singleTenant) { publicButton = } diff --git a/askomics/react/src/routes/integration/rdfpreview.jsx b/askomics/react/src/routes/integration/rdfpreview.jsx index c4894d14..5dbbd4b9 100644 --- a/askomics/react/src/routes/integration/rdfpreview.jsx +++ b/askomics/react/src/routes/integration/rdfpreview.jsx @@ -112,7 +112,7 @@ export default class RdfPreview extends Component { privateButton = } let publicButton - if (this.props.config.user.admin) { + if (this.props.config.user.admin || this.props.config.singleTenant) { publicButton = } From d7832ddfb36ee9e4ddc911ad2b5781a52a389785 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 6 May 2022 11:56:36 +0200 Subject: [PATCH 160/318] Changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c13aebe..a872b0f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ This changelog was started for release 4.2.0. - 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 +- Added 'single tenant' mode ### Changed @@ -46,7 +47,7 @@ This changelog was started for release 4.2.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 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 From 25be9a41c80513d4d682f1e04d31d8028df56a7d Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 6 May 2022 11:59:23 +0200 Subject: [PATCH 161/318] typo --- askomics/libaskomics/SparqlQueryLauncher.py | 1 - 1 file changed, 1 deletion(-) diff --git a/askomics/libaskomics/SparqlQueryLauncher.py b/askomics/libaskomics/SparqlQueryLauncher.py index 0907402e..6c250619 100644 --- a/askomics/libaskomics/SparqlQueryLauncher.py +++ b/askomics/libaskomics/SparqlQueryLauncher.py @@ -294,7 +294,6 @@ def execute_query(self, query, disable_log=False, isql_api=False): # Debug if self.settings.getboolean('askomics', 'debug'): - print(query) self.log.debug("Launch {} query on {} ({})".format("ISQL" if use_isql else "SPARQL", self.triplestore, self.url_endpoint)) self.log.debug(query) From cdc4f3b0892aeea771523075ed941ea2361819c4 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 10 May 2022 10:31:25 +0200 Subject: [PATCH 162/318] Fix #321 : Add blank nodes to attribute definition (#324) Fix #321 --- CHANGELOG.md | 3 ++ askomics/libaskomics/BedFile.py | 18 +++++-- askomics/libaskomics/CsvFile.py | 23 ++++++--- askomics/libaskomics/GffFile.py | 19 +++++--- askomics/libaskomics/SparqlQuery.py | 21 +++++++-- askomics/libaskomics/TriplestoreExplorer.py | 37 ++++++++------- tests/results/abstraction.json | 52 ++++----------------- 7 files changed, 91 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a872b0f4..8dd0e971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,15 +24,18 @@ This changelog was started for release 4.2.0. - 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 ### Changed - Changed "Query builder" to "Form editor" in form editing interface - Changed abstraction building method for relations. (Please refer to #248 and #268) +- Changed abstraction building method for attributes. (Please refer to #321 and #324) - 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 diff --git a/askomics/libaskomics/BedFile.py b/askomics/libaskomics/BedFile.py index 88440756..24ebf6da 100644 --- a/askomics/libaskomics/BedFile.py +++ b/askomics/libaskomics/BedFile.py @@ -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 diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 64d8aa37..08f368eb 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -396,6 +396,8 @@ 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'])) + attribute_blanks = {} + # Attributes and relations for index, attribute_name in enumerate(self.header): @@ -409,6 +411,7 @@ def set_rdf_abstraction(self): 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 @@ -420,7 +423,7 @@ def set_rdf_abstraction(self): 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"])) @@ -442,7 +445,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'): @@ -472,16 +475,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)) + 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 diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index d807f6b0..28341bab 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -108,10 +108,12 @@ 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))) + attribute_blanks = {} + for attribute in self.attribute_abstraction: + blank = BNode() # New way of storing relations (starting from 4.4.0) if attribute.get("relation"): - blank = BNode() 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)) @@ -123,12 +125,15 @@ def set_rdf_abstraction_domain_knowledge(self): 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((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"]: @@ -143,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) diff --git a/askomics/libaskomics/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index d977c719..b7931cf5 100644 --- a/askomics/libaskomics/SparqlQuery.py +++ b/askomics/libaskomics/SparqlQuery.py @@ -462,7 +462,7 @@ def get_uri_parameters(self, uri, endpoints): The corresponding parameters """ raw_query = ''' - SELECT DISTINCT ?predicate ?object ?faldo_value ?faldo_uri + SELECT DISTINCT ?predicate ?object ?faldo_value ?faldo_relation WHERE {{ ?URI ?predicate ?object . ?URI a ?entitytype . @@ -495,9 +495,14 @@ def get_uri_parameters(self, uri, endpoints): ?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) @@ -513,9 +518,19 @@ def get_uri_parameters(self, uri, endpoints): 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': row['faldo_uri'] if row.get('faldo_uri') else row['predicate'], - 'object': row['faldo_value'] if row.get('faldo_value') else row['object'], + 'predicate': predicate, + 'object': object, }) return formated_data diff --git a/askomics/libaskomics/TriplestoreExplorer.py b/askomics/libaskomics/TriplestoreExplorer.py index 7aadaf2b..9a832f37 100644 --- a/askomics/libaskomics/TriplestoreExplorer.py +++ b/askomics/libaskomics/TriplestoreExplorer.py @@ -409,13 +409,16 @@ def get_abstraction_attributes(self, single_tenant=False): ?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 ?attribute_uri}} + BIND( IF(isBlank(?node),?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) @@ -426,10 +429,10 @@ def get_abstraction_attributes(self, single_tenant=False): }} # 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 = {} @@ -443,13 +446,13 @@ def get_abstraction_attributes(self, single_tenant=False): 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"], @@ -467,12 +470,12 @@ def get_abstraction_attributes(self, single_tenant=False): 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"], @@ -515,7 +518,7 @@ def get_abstraction_relations(self, single_tenant=False): query_builder = SparqlQuery(self.app, self.session) query = ''' - SELECT DISTINCT ?graph ?entity_uri ?entity_faldo ?entity_label ?node ?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 . @@ -528,6 +531,7 @@ def get_abstraction_relations(self, single_tenant=False): ?node rdfs:range ?range_uri . # Retrocompatibility OPTIONAL {{?node askomics:uri ?property_uri}} + BIND( IF(isBlank(?node), ?property_uri, ?node) as ?property_uri) }} # Relation of entity (or motherclass of entity) {{ @@ -548,9 +552,8 @@ def get_abstraction_relations(self, single_tenant=False): relations = [] for result in data: # Relation - if "node" in result: - # Retrocompatibility - property_uri = result.get("property_uri", result["node"]) + if "property_uri" in result: + 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) diff --git a/tests/results/abstraction.json b/tests/results/abstraction.json index 2c2eb213..b24c244d 100644 --- a/tests/results/abstraction.json +++ b/tests/results/abstraction.json @@ -27,18 +27,6 @@ }, { "categories": [ - { - "label": "minus", - "uri": "http://askomics.org/test/data/minus" - }, - { - "label": "plus", - "uri": "http://askomics.org/test/data/plus" - }, - { - "label": "unknown/both", - "uri": "http://askomics.org/test/data/unknown/both" - }, { "label": "+", "uri": "http://askomics.org/test/data/%2B" @@ -55,7 +43,6 @@ "entityUri": "http://askomics.org/test/data/gene", "faldo": "http://askomics.org/test/internal/faldoStrand", "graphs": [ - "urn:sparql:askomics_test:1_jdoe:transcripts.tsv_###TRANSCRIPTS_TIMESTAMP###", "urn:sparql:askomics_test:1_jdoe:gene.bed_###BED_TIMESTAMP###", "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###" ], @@ -73,7 +60,7 @@ "label": "plus", "uri": "http://askomics.org/test/data/plus" }, - { + { "label": "unknown/both", "uri": "http://askomics.org/test/data/unknown/both" }, @@ -94,8 +81,7 @@ "faldo": "http://askomics.org/test/internal/faldoStrand", "graphs": [ "urn:sparql:askomics_test:1_jdoe:transcripts.tsv_###TRANSCRIPTS_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:gene.bed_###BED_TIMESTAMP###" + "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###" ], "label": "strand", "type": "http://askomics.org/test/internal/AskomicsCategory", @@ -127,17 +113,12 @@ { "label": "1", "uri": "http://askomics.org/test/data/1" - }, - { - "label": "7", - "uri": "http://askomics.org/test/data/7" } ], "entityUri": "http://askomics.org/test/data/transcript", "faldo": "http://askomics.org/test/internal/faldoReference", "graphs": [ - "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:gene.bed_###BED_TIMESTAMP###" + "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###" ], "label": "reference", "type": "http://askomics.org/test/internal/AskomicsCategory", @@ -148,8 +129,6 @@ "entityUri": "http://askomics.org/test/data/gene", "faldo": "http://askomics.org/test/internal/faldoStart", "graphs": [ - "urn:sparql:askomics_test:1_jdoe:transcripts.tsv_###TRANSCRIPTS_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:qtl.tsv_###QTL_TIMESTAMP###", "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###", "urn:sparql:askomics_test:1_jdoe:gene.bed_###BED_TIMESTAMP###" ], @@ -162,10 +141,7 @@ "entityUri": "http://askomics.org/test/data/QTL", "faldo": "http://askomics.org/test/internal/faldoStart", "graphs": [ - "urn:sparql:askomics_test:1_jdoe:transcripts.tsv_###TRANSCRIPTS_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:qtl.tsv_###QTL_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:gene.bed_###BED_TIMESTAMP###" + "urn:sparql:askomics_test:1_jdoe:qtl.tsv_###QTL_TIMESTAMP###" ], "label": "start", "type": "http://www.w3.org/2001/XMLSchema#decimal", @@ -177,9 +153,7 @@ "faldo": "http://askomics.org/test/internal/faldoStart", "graphs": [ "urn:sparql:askomics_test:1_jdoe:transcripts.tsv_###TRANSCRIPTS_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:qtl.tsv_###QTL_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:gene.bed_###BED_TIMESTAMP###" + "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###" ], "label": "start", "type": "http://www.w3.org/2001/XMLSchema#decimal", @@ -191,9 +165,7 @@ "faldo": "http://askomics.org/test/internal/faldoEnd", "graphs": [ "urn:sparql:askomics_test:1_jdoe:transcripts.tsv_###TRANSCRIPTS_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:qtl.tsv_###QTL_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:gene.bed_###BED_TIMESTAMP###" + "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###" ], "label": "end", "type": "http://www.w3.org/2001/XMLSchema#decimal", @@ -204,8 +176,6 @@ "entityUri": "http://askomics.org/test/data/gene", "faldo": "http://askomics.org/test/internal/faldoEnd", "graphs": [ - "urn:sparql:askomics_test:1_jdoe:transcripts.tsv_###TRANSCRIPTS_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:qtl.tsv_###QTL_TIMESTAMP###", "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###", "urn:sparql:askomics_test:1_jdoe:gene.bed_###BED_TIMESTAMP###" ], @@ -218,10 +188,7 @@ "entityUri": "http://askomics.org/test/data/QTL", "faldo": "http://askomics.org/test/internal/faldoEnd", "graphs": [ - "urn:sparql:askomics_test:1_jdoe:transcripts.tsv_###TRANSCRIPTS_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:qtl.tsv_###QTL_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:gene.bed_###BED_TIMESTAMP###" + "urn:sparql:askomics_test:1_jdoe:qtl.tsv_###QTL_TIMESTAMP###" ], "label": "end", "type": "http://www.w3.org/2001/XMLSchema#decimal", @@ -284,7 +251,6 @@ "entityUri": "http://askomics.org/test/data/gene", "faldo": null, "graphs": [ - "urn:sparql:askomics_test:1_jdoe:transcripts.tsv_###TRANSCRIPTS_TIMESTAMP###", "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###" ], "label": "biotype", @@ -296,8 +262,7 @@ "entityUri": "http://askomics.org/test/data/transcript", "faldo": null, "graphs": [ - "urn:sparql:askomics_test:1_jdoe:transcripts.tsv_###TRANSCRIPTS_TIMESTAMP###", - "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###" + "urn:sparql:askomics_test:1_jdoe:transcripts.tsv_###TRANSCRIPTS_TIMESTAMP###" ], "label": "description", "type": "http://www.w3.org/2001/XMLSchema#string", @@ -308,7 +273,6 @@ "entityUri": "http://askomics.org/test/data/gene", "faldo": null, "graphs": [ - "urn:sparql:askomics_test:1_jdoe:transcripts.tsv_###TRANSCRIPTS_TIMESTAMP###", "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###" ], "label": "description", From 494a3a685bcd028d9cf4d96728950721316bb3cb Mon Sep 17 00:00:00 2001 From: root Date: Mon, 16 May 2022 14:32:20 +0000 Subject: [PATCH 163/318] Fix bind --- askomics/libaskomics/TriplestoreExplorer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/askomics/libaskomics/TriplestoreExplorer.py b/askomics/libaskomics/TriplestoreExplorer.py index 9a832f37..98d55026 100644 --- a/askomics/libaskomics/TriplestoreExplorer.py +++ b/askomics/libaskomics/TriplestoreExplorer.py @@ -414,8 +414,8 @@ def get_abstraction_attributes(self, single_tenant=False): ?node rdfs:label ?attribute_label . ?node rdfs:range ?attribute_range . # Retrocompatibility - OPTIONAL {{?node askomics:uri ?attribute_uri}} - BIND( IF(isBlank(?node),?attribute_uri, ?node) as ?attribute_uri ) + OPTIONAL {{?node askomics:uri ?new_attribute_uri}} + BIND( IF(isBlank(?node),?new_attribute_uri, ?node) as ?attribute_uri ) # Faldo OPTIONAL {{ ?node a ?attribute_faldo . @@ -530,8 +530,8 @@ def get_abstraction_relations(self, single_tenant=False): ?node rdfs:label ?property_label . ?node rdfs:range ?range_uri . # Retrocompatibility - OPTIONAL {{?node askomics:uri ?property_uri}} - BIND( IF(isBlank(?node), ?property_uri, ?node) as ?property_uri) + 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) {{ From 2aabba1cfe4ff604182d0194acc5eb6d0fb14ca5 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 17 May 2022 11:48:34 +0200 Subject: [PATCH 164/318] Fix empty category column --- askomics/libaskomics/CsvFile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 08f368eb..1b94c86b 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -361,8 +361,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]]: From 5919426397b0238a2cfa86e4fe36f34700b4eea8 Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 17 May 2022 11:51:22 +0200 Subject: [PATCH 165/318] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dd0e971..ce78b3d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This changelog was started for release 4.2.0. - 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) ### Added @@ -25,7 +26,7 @@ This changelog was started for release 4.2.0. - 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 +- Added 'single tenant' mode: Send queries to all graphs to speed up ### Changed @@ -54,6 +55,7 @@ This changelog was started for release 4.2.0. - 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 From 8b4ea876558534999a337f00285b9dbf2ae48e9c Mon Sep 17 00:00:00 2001 From: mboudet Date: Tue, 17 May 2022 14:27:26 +0200 Subject: [PATCH 166/318] Fix empty category column (#334) (#335) * Fix empty category column * Update CHANGELOG.md --- CHANGELOG.md | 4 +++- askomics/libaskomics/CsvFile.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dd0e971..ce78b3d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This changelog was started for release 4.2.0. - 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) ### Added @@ -25,7 +26,7 @@ This changelog was started for release 4.2.0. - 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 +- Added 'single tenant' mode: Send queries to all graphs to speed up ### Changed @@ -54,6 +55,7 @@ This changelog was started for release 4.2.0. - 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/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 08f368eb..1b94c86b 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -361,8 +361,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]]: From 884bf02cd7420b5b6650fe13ca6d666e7c4206bc Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 22 Jun 2022 12:10:09 +0200 Subject: [PATCH 167/318] Update mail message --- askomics/libaskomics/LocalAuth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/askomics/libaskomics/LocalAuth.py b/askomics/libaskomics/LocalAuth.py index 4759cedb..478e7c73 100644 --- a/askomics/libaskomics/LocalAuth.py +++ b/askomics/libaskomics/LocalAuth.py @@ -1046,13 +1046,13 @@ def send_mail_to_new_user(self, user): body = textwrap.dedent(""" Welcome {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, From 0204796dad09863575739d5bf6e1b9fd28bc486d Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 22 Jun 2022 12:15:24 +0200 Subject: [PATCH 168/318] Update LocalAuth.py --- askomics/libaskomics/LocalAuth.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/askomics/libaskomics/LocalAuth.py b/askomics/libaskomics/LocalAuth.py index 478e7c73..b4f2bd33 100644 --- a/askomics/libaskomics/LocalAuth.py +++ b/askomics/libaskomics/LocalAuth.py @@ -1044,7 +1044,7 @@ 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}! An account with this email adress was created by the administrators of {url}. @@ -1104,16 +1104,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. + + If you did not initiate this request, feel free to ignore this message. - But don’t worry! You can use the following link to reset your password: + You can use the following link to reset your password: {url}/password_reset?token={token} - If you don’t use this link within 3 hours, it will expire. To get a new password reset link, visit {url}/password_reset + 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( From 4e11906b636c9bcfc9a8c9326df87025fb22ec77 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 22 Jun 2022 12:35:44 +0200 Subject: [PATCH 169/318] Update LocalAuth.py --- askomics/libaskomics/LocalAuth.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/askomics/libaskomics/LocalAuth.py b/askomics/libaskomics/LocalAuth.py index b4f2bd33..a1bf0c4f 100644 --- a/askomics/libaskomics/LocalAuth.py +++ b/askomics/libaskomics/LocalAuth.py @@ -1059,7 +1059,6 @@ def send_mail_to_new_user(self, user): The AskOmics Team """.format( username=user["username"], - email=user["email"], url=self.settings.get('askomics', 'instance_url'), token=token )) @@ -1105,7 +1104,7 @@ def send_reset_link(self, login): Dear {user}, A password reset request has been received for your {url} account. - + If you did not initiate this request, feel free to ignore this message. You can use the following link to reset your password: From 42a92c3cb9f0c8a9a888345c930be1719107cc87 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 29 Jun 2022 16:14:08 +0200 Subject: [PATCH 170/318] Onotolgies integration (#353) Ontology management --- CHANGELOG.md | 1 + Pipfile | 2 +- Pipfile.lock | 312 ++++++++++----- askomics/api/admin.py | 310 +++++++++++++++ askomics/api/datasets.py | 6 + askomics/api/file.py | 6 +- askomics/api/ontology.py | 59 +++ askomics/api/start.py | 8 +- askomics/app.py | 4 +- askomics/libaskomics/BedFile.py | 4 +- askomics/libaskomics/CsvFile.py | 40 +- askomics/libaskomics/Database.py | 93 +++++ askomics/libaskomics/Dataset.py | 61 ++- askomics/libaskomics/DatasetsHandler.py | 10 +- askomics/libaskomics/File.py | 6 +- askomics/libaskomics/FilesHandler.py | 11 +- askomics/libaskomics/GffFile.py | 4 +- askomics/libaskomics/OntologyManager.py | 275 +++++++++++++ askomics/libaskomics/PrefixManager.py | 82 +++- askomics/libaskomics/RdfFile.py | 29 +- askomics/libaskomics/RdfGraph.py | 1 + askomics/libaskomics/SparqlQuery.py | 145 ++++++- askomics/libaskomics/SparqlQueryLauncher.py | 4 +- askomics/libaskomics/TriplestoreExplorer.py | 3 +- .../react/src/components/autocomplete.jsx | 147 +++++++ askomics/react/src/navbar.jsx | 4 +- askomics/react/src/routes.jsx | 8 +- 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 ++++++++++++ .../src/routes/datasets/datasetstable.jsx | 2 +- .../routes/integration/advancedoptions.jsx | 10 +- .../src/routes/integration/bedpreview.jsx | 13 +- .../react/src/routes/integration/csvtable.jsx | 30 +- .../src/routes/integration/gffpreview.jsx | 21 +- .../src/routes/integration/integration.jsx | 5 +- .../src/routes/integration/rdfpreview.jsx | 14 +- askomics/react/src/routes/query/attribute.jsx | 52 ++- .../react/src/routes/query/ontolinkview.jsx | 46 +++ askomics/react/src/routes/query/query.jsx | 113 +++++- .../react/src/routes/query/visualization.jsx | 2 +- askomics/static/css/askomics.css | 35 +- askomics/tasks.py | 2 +- config/askomics.ini.template | 8 + config/askomics.test.ini | 3 + package-lock.json | 52 +++ package.json | 1 + test-data/agro_min.ttl | 70 ++++ tests/conftest.py | 75 +++- tests/results/abstraction.json | 4 + tests/results/init.json | 2 +- tests/results/preview_files.json | 2 +- tests/results/preview_malformed_files.json | 2 +- tests/results/sparql_query.json | 2 +- tests/test_api.py | 4 +- tests/test_api_admin.py | 217 ++++++++++- tests/test_api_datasets.py | 30 +- tests/test_api_ontology.py | 73 ++++ 59 files changed, 2927 insertions(+), 225 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 test-data/agro_min.ttl create mode 100644 tests/test_api_ontology.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ce78b3d6..90aae6b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ This changelog was started for release 4.2.0. - 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 ### Changed diff --git a/Pipfile b/Pipfile index cd5a56d0..6509b8eb 100644 --- a/Pipfile +++ b/Pipfile @@ -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 10565d9a..ef4d555b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d398feea9b583f305001058caf3d00d225702ce354469889db8ec34953b40471" + "sha256": "5d768dd0f5c397f0380f7a594b66aed5e23d923674edfa3c62b4e47f5ce3e81e" }, "pipfile-spec": 6, "requires": {}, @@ -32,10 +32,10 @@ }, "bcbio-gff": { "hashes": [ - "sha256:6e6f70639149612272a3b298a93ac50bba6f9ecece934f2a0ea86d4abde975da" + "sha256:34dfa970e14f4533dc63c0a5512b7b5221e4a06449e6aaa344162ed5fdd7a1de" ], "index": "pypi", - "version": "==0.6.7" + "version": "==0.6.9" }, "billiard": { "hashes": [ @@ -96,13 +96,21 @@ ], "version": "==2.49.0" }, + "cached-property": { + "hashes": [ + "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130", + "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0" + ], + "markers": "python_version < '3.8'", + "version": "==1.5.2" + }, "celery": { "hashes": [ - "sha256:8d9a3de9162965e97f8e8cc584c67aad83b3f7a267584fa47701ed11c3e0d4b0", - "sha256:9dab2170b4038f7bf10ef2861dbf486ddf1d20592290a1040f7b7a1259705d42" + "sha256:5e8d364e058554e83bbb116e8377d90c79be254785f357cb2cec026e79febe13", + "sha256:f4efebe6f8629b0da2b8e529424de376494f5b7a743c321c8a2ddc2b1414921c" ], "index": "pypi", - "version": "==5.1.2" + "version": "==5.0.5" }, "certifi": { "hashes": [ @@ -113,11 +121,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:735e240d9a8506778cd7a453d97e817e536bb1fc29f4f6961ce297b9c7a917b0", - "sha256:83fcdeb225499d6344c8f7f34684c2981270beacc32ede2e669e94f7fa544405" + "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", + "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" ], "markers": "python_version >= '3'", - "version": "==2.0.8" + "version": "==2.0.9" }, "click": { "hashes": [ @@ -132,7 +140,7 @@ "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667", "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035" ], - "markers": "python_version < '4' and python_full_version >= '3.6.2'", + "markers": "python_full_version >= '3.6.2' and python_full_version < '4.0.0'", "version": "==0.3.0" }, "click-plugins": { @@ -151,11 +159,11 @@ }, "configparser": { "hashes": [ - "sha256:85d5de102cfe6d14a5172676f09d19c465ce63d6019cf0a4ef13385fc535e828", - "sha256:af59f2cdd7efbdd5d111c1976ecd0b82db9066653362f0962d7bf1d3ab89a1fa" + "sha256:1b35798fdf1713f1c3139016cfcbc461f09edbf099d1fb658d4b7479fcaa3daa", + "sha256:e8b39238fb6f0153a069aa253d349467c3c4737934f253ef6abac5fe0eca1e5d" ], "index": "pypi", - "version": "==5.0.2" + "version": "==5.2.0" }, "deepdiff": { "hashes": [ @@ -165,6 +173,14 @@ "index": "pypi", "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": [ "sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196", @@ -213,6 +229,14 @@ "markers": "python_version >= '3'", "version": "==3.3" }, + "importlib-metadata": { + "hashes": [ + "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100", + "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb" + ], + "markers": "python_version < '3.8'", + "version": "==4.8.2" + }, "isodate": { "hashes": [ "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", @@ -423,22 +447,25 @@ }, "pysam": { "hashes": [ - "sha256:08f88fa4472e140e39c9ec3c01643563587a28e49c115ef9077295b0452342ac", - "sha256:097eedc82095ff52e020b6e0e099a171313e60083c68ad861ac474f325e2b7d0", - "sha256:18478ac02511d6171bce1891b503031eaf5e22b7a38cf62e00f3a070a4a37da2", - "sha256:2fd8fa6205ad893082ec0aa1992446e3d9c63bfe9f7a3e59d81a956780e5cc2a", - "sha256:4032361c424fb3b27a7da7ddeba0de8acb6c548b025bdc03c8ffc6b306d7ee9a", - "sha256:4d0a35dec9194bacbde0647ddf32ebf805a6724b0192758039c031fce849f01f", - "sha256:4f5188fda840fe51144b8c56c45af014174295c3f539a40333f9e270ca0d5e01", - "sha256:5d140da81ca42f474006f5cc0fd66647f1b08d559af7026bbe9f01fab029bffd", - "sha256:6f802073d625a6a9b47aaaed6ff6b08e90ec4ad79df271073452fa8c7a62c529", - "sha256:7caada03fbec2e18b8eb2c80aabf8834ea2ceb12b2aa1c0fa6d4ba42b60d83fa", - "sha256:c4a8099cf474067eaf5e270b5e71433a9ea0e97af90262143a655528eb756cd9", - "sha256:cb2c3ca9ff3b5c9694846254fea9f8697406a35769b11767a312e97ad5c8cedd", - "sha256:eb35399530188efe48ef136a043ba3acf9f538f1b11e946f3aaea9fd09c8cbde" + "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.17.0" + "version": "==0.18.0" }, "python-dateutil": { "hashes": [ @@ -519,11 +546,11 @@ }, "redis": { "hashes": [ - "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", - "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" + "sha256:c8481cf414474e3497ec7971a1ba9b998c8efad0f0d289a009a5bbef040894f9", + "sha256:ccf692811f2c1fc7a92b466aa2599e4a6d2d73d5f736a2c70be600657c0da34a" ], "index": "pypi", - "version": "==3.5.3" + "version": "==4.0.2" }, "requests": { "hashes": [ @@ -545,11 +572,19 @@ "flask" ], "hashes": [ - "sha256:b9844751e40710e84a457c5bc29b21c383ccb2b63d76eeaad72f7f1c808c8828", - "sha256:c091cc7115ff25fe3a0e410dbecd7a996f81a3f6137d2272daef32d6c3cfa6dc" + "sha256:0db297ab32e095705c20f742c3a5dac62fe15c4318681884053d0898e5abb2f6", + "sha256:789a11a87ca02491896e121efdd64e8fd93327b69e8f2f7d42f03e2569648e88" ], "index": "pypi", - "version": "==1.4.3" + "version": "==1.5.0" + }, + "setuptools": { + "hashes": [ + "sha256:6d10741ff20b89cd8c6a536ee9dc90d3002dec0226c78fb98605bfb9ef8a7adf", + "sha256:d144f85102f999444d06f9c0e8c737fd0194f10f2f7e5fdb77573f6e2fa4fad0" + ], + "markers": "python_version >= '3.6'", + "version": "==59.5.0" }, "setuptools": { "hashes": [ @@ -599,12 +634,20 @@ "index": "pypi", "version": "==0.12.6" }, + "typing-extensions": { + "hashes": [ + "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", + "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" + ], + "markers": "python_version < '3.10'", + "version": "==4.0.1" + }, "urllib3": { "hashes": [ "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_version < '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": { @@ -665,6 +708,71 @@ ], "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:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", + "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" + ], + "markers": "python_version >= '3.6'", + "version": "==3.6.0" } }, "develop": { @@ -685,11 +793,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:735e240d9a8506778cd7a453d97e817e536bb1fc29f4f6961ce297b9c7a917b0", - "sha256:83fcdeb225499d6344c8f7f34684c2981270beacc32ede2e669e94f7fa544405" + "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", + "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" ], "markers": "python_version >= '3'", - "version": "==2.0.8" + "version": "==2.0.9" }, "click": { "hashes": [ @@ -704,69 +812,64 @@ ], "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" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==5.5" + "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:15a987d9df877fff44cd81948c5806ffb6eafb757b3443f737888358e96156ee", - "sha256:aedfcc5296b788ebaf8ace8029376e5f102f67c53d1373f2e821515c15b36527" + "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea", + "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026" ], "index": "pypi", - "version": "==3.2.0" + "version": "==3.3.1" }, "docopt": { "hashes": [ @@ -802,7 +905,7 @@ "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100", "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb" ], - "markers": "python_version >= '3.6'", + "markers": "python_version < '3.8'", "version": "==4.8.2" }, "iniconfig": { @@ -1069,12 +1172,27 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, + "tomli": { + "hashes": [ + "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee", + "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade" + ], + "version": "==1.2.2" + }, + "typing-extensions": { + "hashes": [ + "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", + "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" + ], + "markers": "python_version < '3.10'", + "version": "==4.0.1" + }, "urllib3": { "hashes": [ "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_version < '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" }, "watchdog": { 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/datasets.py b/askomics/api/datasets.py index b31e510c..665a0490 100644 --- a/askomics/api/datasets.py +++ b/askomics/api/datasets.py @@ -136,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 7705f4e9..5d224030 100644 --- a/askomics/api/file.py +++ b/askomics/api/file.py @@ -237,6 +237,7 @@ def get_preview(): }), 400 try: + files_handler = FilesHandler(current_app, session) files_handler.handle_files(data['filesId']) @@ -332,6 +333,7 @@ def integrate(): for file in files_handler.files: 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 = { @@ -342,8 +344,10 @@ def integrate(): "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)) 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/start.py b/askomics/api/start.py index 442314f8..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, @@ -80,7 +84,9 @@ def start(): "proxyPath": proxy_path, "user": {}, "logged": False, - "singleTenant": current_app.iniconfig.getboolean('askomics', 'single_tenant', fallback=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 24ebf6da..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 = {} diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 1b94c86b..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") @@ -395,6 +396,9 @@ 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 @@ -438,6 +442,28 @@ def set_rdf_abstraction(self): 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'): attribute = self.rdfize(attribute_name) @@ -501,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) @@ -578,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) diff --git a/askomics/libaskomics/Database.py b/askomics/libaskomics/Database.py index 16b138f0..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 = ''' @@ -383,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 b5cd4879..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))) diff --git a/askomics/libaskomics/FilesHandler.py b/askomics/libaskomics/FilesHandler.py index d3d1adfe..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 diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index 28341bab..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 = [] diff --git a/askomics/libaskomics/OntologyManager.py b/askomics/libaskomics/OntologyManager.py new file mode 100644 index 00000000..c988f959 --- /dev/null +++ b/askomics/libaskomics/OntologyManager.py @@ -0,0 +1,275 @@ +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" + } + + r = requests.get(base_url, params=arguments) + + data = [] + + if not r.status_code == 200: + return data + + res = r.json() + if res['response']['docs']: + data = [term['label'] for term in res['response']['docs']] + + 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 b1252d01..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 @@ -71,7 +80,7 @@ def get_preview(self): location = None try: - location = self.get_location() + location, remote_graph = self.get_location_and_remote_graph() except Exception as e: self.error_message = str(e) @@ -83,13 +92,15 @@ def get_preview(self): 'error_message': self.error_message, 'data': { 'preview': head, - 'location': 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/SparqlQuery.py b/askomics/libaskomics/SparqlQuery.py index b7931cf5..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 @@ -35,6 +37,7 @@ def __init__(self, app, session, json_query=None, get_graphs=False): self.graphs = [] self.endpoints = [] + self.remote_graphs = defaultdict(list) self.selects = [] self.federated = False @@ -68,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 @@ -276,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 @@ -296,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') @@ -311,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'): @@ -375,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 @@ -392,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 @@ -410,7 +442,7 @@ def set_graphs_and_endpoints(self, entities=None, graphs=None, endpoints=None): 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 . @@ -418,11 +450,15 @@ def set_graphs_and_endpoints(self, entities=None, graphs=None, endpoints=None): ?graph dc:creator ?creator . GRAPH ?graph_abstraction {{ ?graph_abstraction prov:atLocation ?endpoint . - ?entity_uri a askomics:entity . + OPTIONAL {{?graph_abstraction dcat:Dataset ?remote_graph .}} + ?entity_uri a ?askomics_type . }} GRAPH ?graph {{ - [] a ?entity_uri . + {{ [] a ?entity_uri . }} + UNION + {{ ?entity_uri a ?askomics_type . }} }} + VALUES ?askomics_type {{askomics:entity askomics:ontology}} {} {} }} @@ -432,9 +468,15 @@ 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 federated query engine if res["endpoint"] == self.settings.get('triplestore', 'endpoint') and self.local_endpoint_f is not None: @@ -444,6 +486,8 @@ 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 @@ -535,6 +579,50 @@ def get_uri_parameters(self, uri, endpoints): 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 ? @@ -964,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 = [] @@ -981,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) @@ -1100,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, @@ -1122,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"]) @@ -1131,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) @@ -1205,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"]) @@ -1394,6 +1510,7 @@ def build_query_from_json(self, preview=False, for_editor=False): 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) @@ -1423,6 +1540,7 @@ def build_query_from_json(self, preview=False, for_editor=False): query = """ {endpoints} {federated} +{remote_graphs} SELECT DISTINCT {selects} WHERE {{ @@ -1434,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 6c250619..64fe2802 100644 --- a/askomics/libaskomics/SparqlQueryLauncher.py +++ b/askomics/libaskomics/SparqlQueryLauncher.py @@ -310,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 98d55026..ced8a54e 100644 --- a/askomics/libaskomics/TriplestoreExplorer.py +++ b/askomics/libaskomics/TriplestoreExplorer.py @@ -330,7 +330,7 @@ def get_abstraction_entities(self, single_tenant=False): 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 . @@ -364,6 +364,7 @@ def get_abstraction_entities(self, single_tenant=False): "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"]], } 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 28fa4d3c..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' @@ -45,7 +47,9 @@ export default class Routes extends Component { disableIntegration: null, namespaceData: null, namespaceInternal: null, - singleTenant: false + ontologies: [], + singleTenant: false, + autocompleteMaxResults: 10 } } this.cancelRequest @@ -114,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

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + {this.state.datasets.map(dataset => { + if (dataset.public == 1 && dataset.status == "success"){ + return + } + })} + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ +
+
+ + +
+ ) + } +} + +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

+
+
+ + + + + + + + + + + + + + + +
+ +
+
+
+
+ +
+
+ + +
+ ) + } +} + +Prefixes.propTypes = { + waitForStart: PropTypes.bool, + config: PropTypes.object +} diff --git a/askomics/react/src/routes/datasets/datasetstable.jsx b/askomics/react/src/routes/datasets/datasetstable.jsx index 5afa8e04..ae1f8723 100644 --- a/askomics/react/src/routes/datasets/datasetstable.jsx +++ b/askomics/react/src/routes/datasets/datasetstable.jsx @@ -118,7 +118,7 @@ export default class DatasetsTable extends Component { return (
- +
) 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 { +
@@ -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 90e72d19..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 = @@ -122,6 +131,8 @@ export default class BedPreview extends Component { hideDistantEndpoint={true} handleChangeUri={p => this.handleChangeUri(p)} handleChangeEndpoint={p => this.handleChangeEndpoint(p)} + handleChangeExternalGraph={p => this.handleChangeExternalGraph(p)} + externalGraph={this.state.externalGraph} customUri={this.state.customUri} />
diff --git a/askomics/react/src/routes/integration/csvtable.jsx b/askomics/react/src/routes/integration/csvtable.jsx index cb766fff..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,18 @@ export default class CsvTable extends Component { ) } + let ontoInput + + if (this.props.ontologies.length > 0){ + ontoInput = ( + + {this.props.ontologies.map(onto => { + return + })} + + ) + } + if (colIndex == 1) { return (
@@ -117,6 +130,7 @@ export default class CsvTable extends Component { + {ontoInput}
@@ -145,6 +159,7 @@ export default class CsvTable extends Component { + {ontoInput}
@@ -199,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 } } }) @@ -274,6 +297,8 @@ export default class CsvTable extends Component { hideDistantEndpoint={true} handleChangeUri={p => this.handleChangeUri(p)} handleChangeEndpoint={p => this.handleChangeEndpoint(p)} + handleChangeExternalGraph={p => this.handleChangeExternalGraph(p)} + externalGraph={this.state.externalGraph} customUri={this.state.customUri} />
@@ -300,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 0fafbbef..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 = @@ -129,6 +146,8 @@ export default class GffPreview extends Component { hideDistantEndpoint={true} handleChangeUri={p => this.handleChangeUri(p)} handleChangeEndpoint={p => this.handleChangeEndpoint(p)} + handleChangeExternalGraph={p => this.handleChangeExternalGraph(p)} + externalGraph={this.state.externalGraph} customUri={this.state.customUri} />
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 5dbbd4b9..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" @@ -142,6 +152,8 @@ 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)} />
diff --git a/askomics/react/src/routes/query/attribute.jsx b/askomics/react/src/routes/query/attribute.jsx index b6cb21ca..7f37204f 100644 --- a/askomics/react/src/routes/query/attribute.jsx +++ b/askomics/react/src/routes/query/attribute.jsx @@ -10,6 +10,7 @@ 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) { @@ -33,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) { @@ -46,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 = [] @@ -123,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 { @@ -144,7 +183,7 @@ export default class AttributeBox extends Component { - + {input} @@ -154,12 +193,7 @@ export default class AttributeBox extends Component { return (
-
- {this.props.config.user.admin ? : } - - {this.props.attribute.uri == "rdf:type" || this.props.attribute.uri == "rdfs:label" ? : } - -
+ {attrIcons} {form}
) @@ -476,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/ontolinkview.jsx b/askomics/react/src/routes/query/ontolinkview.jsx new file mode 100644 index 00000000..b50dfb27 --- /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 ...

+ + + + + +
+ + + + + + + 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 a516105a..aa0c48f6 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/query.jsx @@ -9,6 +9,7 @@ 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' @@ -42,6 +43,7 @@ export default class Query extends Component { // Preview icons disablePreview: false, previewIcon: "table", + ontologies: this.props.location.state.config.ontologies } this.graphState = { @@ -212,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) @@ -244,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 @@ -271,7 +302,8 @@ export default class Query extends Component { form: false, negative: false, linked: false, - linkedWith: null + linkedWith: null, + ontology: isOnto }) } @@ -482,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() @@ -506,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: isOnto ? false : 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() @@ -565,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 } @@ -593,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({ @@ -608,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 } @@ -634,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, @@ -644,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, @@ -661,7 +703,7 @@ export default class Query extends Component { target: node1.id, selected: false, suggested: false, - directed: true + directed: link.direct, } } }) @@ -728,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 @@ -1289,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"] = "Children of" + labels["http://www.w3.org/2000/01/rdf-schema#subClassOf*"] = "Descendants of" + labels["^http://www.w3.org/2000/01/rdf-schema#subClassOf"] = "Parents of" + labels["^http://www.w3.org/2000/01/rdf-schema#subClassOf*"] = "Ancestors of" + return labels[uri] + } + // ------------------------------------------------ // Preview results and Launch query buttons ------- @@ -1458,6 +1521,7 @@ export default class Query extends Component { let visualizationDiv let uriLabelBoxes let AttributeBoxes + let isOnto let linkView let previewButton let faldoButton @@ -1477,6 +1541,7 @@ export default class Query extends Component { 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 ( @@ -1500,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} /> ) } @@ -1528,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) 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/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 285cd6a8..ee048d3e 100644 --- a/askomics/tasks.py +++ b/askomics/tasks.py @@ -44,7 +44,7 @@ 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) or app.iniconfig.getboolean("askomics", "single_tenant", fallback=False) diff --git a/config/askomics.ini.template b/config/askomics.ini.template index dd1070a9..d861b8fe 100644 --- a/config/askomics.ini.template +++ b/config/askomics.ini.template @@ -83,6 +83,10 @@ ldap_mail_attribute = mail #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 @@ -125,8 +129,12 @@ preview_limit = 25 # result_set_max_rows = 10000 # Single tenant means all graphs are public +# All queries are launched on all graphes (speedup queries) single_tenant=False +# Max results returned for autocompletion +autocomplete_max_results = 10 + [federation] # Query engine can be corese or fedx #query_engine = corese diff --git a/config/askomics.test.ini b/config/askomics.test.ini index f2afac84..6d9bd17c 100644 --- a/config/askomics.test.ini +++ b/config/askomics.test.ini @@ -76,6 +76,7 @@ ldap_surname_attribute = sn ldap_mail_attribute = mail #ldap_password_reset_link = #ldap_account_link = +autocomplete_max_results = 20 [triplestore] # name of the triplestore, can be virtuoso or fuseki @@ -132,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/package-lock.json b/package-lock.json index 0b75625b..04f514fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4547,6 +4547,11 @@ "is-symbol": "^1.0.2" } }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -5513,6 +5518,11 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-input-selection": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/get-input-selection/-/get-input-selection-1.1.4.tgz", + "integrity": "sha512-o3rv95OOpoHznujIEwZljNhUM9efW/gZsIKCQtTrjRU4PkneVpDvxNBmC7kXC4519lZYT95DKcdj0A5f9GZkKg==" + }, "get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -8625,6 +8635,18 @@ "object-assign": "^4.1.0" } }, + "react-autosuggest": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-autosuggest/-/react-autosuggest-10.1.0.tgz", + "integrity": "sha512-/azBHmc6z/31s/lBf6irxPf/7eejQdR0IqnZUzjdSibtlS8+Rw/R79pgDAo6Ft5QqCUTyEQ+f0FhL+1olDQ8OA==", + "requires": { + "es6-promise": "^4.2.8", + "prop-types": "^15.7.2", + "react-themeable": "^1.1.0", + "section-iterator": "^2.0.0", + "shallow-equal": "^1.2.1" + } + }, "react-bootstrap-table-next": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/react-bootstrap-table-next/-/react-bootstrap-table-next-4.0.3.tgz", @@ -8817,6 +8839,21 @@ "refractor": "^3.2.0" } }, + "react-themeable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/react-themeable/-/react-themeable-1.1.0.tgz", + "integrity": "sha512-kl5tQ8K+r9IdQXZd8WLa+xxYN04lLnJXRVhHfdgwsUJr/SlKJxIejoc9z9obEkx1mdqbTw1ry43fxEUwyD9u7w==", + "requires": { + "object-assign": "^3.0.0" + }, + "dependencies": { + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==" + } + } + }, "react-tooltip": { "version": "4.2.21", "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-4.2.21.tgz", @@ -9352,6 +9389,11 @@ "ajv-keywords": "^3.5.2" } }, + "section-iterator": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/section-iterator/-/section-iterator-2.0.0.tgz", + "integrity": "sha512-xvTNwcbeDayXotnV32zLb3duQsP+4XosHpb/F+tu6VzEZFmIjzPdNk6/O+QOOx5XTh08KL2ufdXeCO33p380pQ==" + }, "select": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", @@ -9440,6 +9482,11 @@ "safe-buffer": "^5.0.1" } }, + "shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10118,6 +10165,11 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "textarea-caret": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz", + "integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==" + }, "three": { "version": "0.126.1", "resolved": "https://registry.npmjs.org/three/-/three-0.126.1.tgz", diff --git a/package.json b/package.json index 7df880d1..fb733689 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "qs": "^6.9.4", "react": "^16.13.1", "react-ace": "^9.1.3", + "react-autosuggest": "^10.1.0", "react-addons-update": "^15.6.3", "react-bootstrap-table-next": "^4.0.3", "react-bootstrap-table2-editor": "^1.4.0", diff --git a/test-data/agro_min.ttl b/test-data/agro_min.ttl new file mode 100644 index 00000000..4f895879 --- /dev/null +++ b/test-data/agro_min.ttl @@ -0,0 +1,70 @@ +@prefix askomics: . +@prefix owl: . +@prefix rdf: . +@prefix rdfs: . +@prefix xml: . +@prefix xsd: . +@prefix ns1: . + + + a askomics:ontology ; + a owl:ontology ; + rdfs:label "AGRO". + +[] a owl:ObjectProperty ; + a askomics:AskomicsRelation ; + askomics:uri rdfs:subClassOf ; + rdfs:label "subClassOf" ; + rdfs:domain ; + rdfs:range . + + + a owl:Class ; + rdfs:label "desuckering" ; + rdfs:subClassOf . + + a owl:Class ; + rdfs:label "irrigation water source role" ; + rdfs:subClassOf . + + a owl:Class ; + rdfs:label "irrigation water quantity" ; + rdfs:subClassOf , + . + + a owl:Class ; + rdfs:label "reduced tillage process" ; + rdfs:subClassOf . + + a owl:Class ; + rdfs:label "laser land levelling process" ; + rdfs:subClassOf . + + a owl:Class ; + rdfs:label "chemical pest control process" ; + rdfs:subClassOf . + + a owl:Class ; + rdfs:label "no-till" ; + rdfs:subClassOf , + . + + a owl:Class ; + rdfs:label "puddling process" ; + rdfs:subClassOf . + + a owl:Class ; + rdfs:label "mulch-till" ; + rdfs:subClassOf . + + a owl:Class ; + rdfs:label "ridge-till" ; + rdfs:subClassOf . + + a owl:Class ; + rdfs:label "strip-till" ; + rdfs:subClassOf . + + a owl:Class ; + rdfs:label "aerial application" ; + rdfs:subClassOf . diff --git a/tests/conftest.py b/tests/conftest.py index fb30c607..999d0248 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,8 @@ from askomics.libaskomics.FilesHandler import FilesHandler from askomics.libaskomics.FilesUtils import FilesUtils from askomics.libaskomics.LocalAuth import LocalAuth +from askomics.libaskomics.PrefixManager import PrefixManager +from askomics.libaskomics.OntologyManager import OntologyManager from askomics.libaskomics.SparqlQueryLauncher import SparqlQueryLauncher from askomics.libaskomics.Start import Start from askomics.libaskomics.Result import Result @@ -249,7 +251,8 @@ def upload_file(self, file_path): ".tsv": "text/tab-separated-values", ".csv": "text/tab-separated-values", ".gff3": "null", - ".bed": "null" + ".bed": "null", + ".ttl": "rdf/ttl" } with open(file_path, 'r') as file_content: @@ -291,7 +294,7 @@ def upload_file_url(self, file_url): files.download_url(file_url, "1") return files.date - def integrate_file(self, info, public=False): + def integrate_file(self, info, public=False, set_graph=False, endpoint=""): """Summary Parameters @@ -307,6 +310,9 @@ def integrate_file(self, info, public=False): files_handler = FilesHandler(self.app, self.session) files_handler.handle_files([info["id"], ]) + # TODO: Fix this. Why do we need the virtuoso url? + endpoint = endpoint or "http://virtuoso:8890/sparql" + for file in files_handler.files: dataset_info = { @@ -318,7 +324,7 @@ def integrate_file(self, info, public=False): } dataset = Dataset(self.app, self.session, dataset_info) - dataset.save_in_db() + dataset.save_in_db(endpoint, set_graph=set_graph) if file.type == "csv/tsv": file.integrate(dataset.id, info["columns_type"], public=public) @@ -326,7 +332,8 @@ def integrate_file(self, info, public=False): file.integrate(dataset.id, info["entities"], public=public) elif file.type == "bed": file.integrate(dataset.id, info["entity_name"], public=public) - + elif file.type in ('rdf/ttl', 'rdf/xml', 'rdf/nt'): + file.integrate(public=public) # done dataset.update_in_db("success") dataset.set_info_from_db() @@ -334,7 +341,9 @@ def integrate_file(self, info, public=False): return { "timestamp": file.timestamp, "start": dataset.start, - "end": dataset.end + "end": dataset.end, + "graph": dataset.graph_name, + "endpoint": dataset.endpoint } def upload(self): @@ -369,7 +378,7 @@ def upload(self): } } - def upload_and_integrate(self): + def upload_and_integrate(self, set_graph=False): """Summary Returns @@ -388,27 +397,27 @@ def upload_and_integrate(self): int_transcripts = self.integrate_file({ "id": 1, "columns_type": ["start_entity", "label", "category", "text", "reference", "start", "end", "category", "strand", "text", "text", "date"] - }) + }, set_graph=set_graph) int_de = self.integrate_file({ "id": 2, "columns_type": ["start_entity", "directed", "numeric", "numeric", "numeric", "text", "numeric", "numeric", "numeric", "numeric"] - }) + }, set_graph=set_graph) int_qtl = self.integrate_file({ "id": 3, "columns_type": ["start_entity", "ref", "start", "end"] - }) + }, set_graph=set_graph) int_gff = self.integrate_file({ "id": 4, "entities": ["gene", "transcript"] - }) + }, set_graph=set_graph) int_bed = self.integrate_file({ "id": 5, "entity_name": "gene" - }) + }, set_graph=set_graph) return { "transcripts": { @@ -443,6 +452,31 @@ def upload_and_integrate(self): } } + def upload_and_integrate_ontology(self): + """Summary + + Returns + ------- + TYPE + Description + """ + # upload + up_ontology = self.upload_file("test-data/agro_min.ttl") + + # integrate + int_ontology = self.integrate_file({ + "id": 1, + }, set_graph=True, endpoint="http://localhost:8891/sparql-auth") + + return { + "upload": up_ontology, + "timestamp": int_ontology["timestamp"], + "start": int_ontology["start"], + "end": int_ontology["end"], + "graph": int_ontology["graph"], + "endpoint": int_ontology["endpoint"] + } + def create_result(self, has_form=False): """Create a result entry in db @@ -496,6 +530,13 @@ def create_result(self, has_form=False): "size": file_size } + def publicize_dataset(self, dataset_id, public=True): + """Publicize a result""" + + dataset_info = {"id": dataset_id} + result = Dataset(self.app, self.session, dataset_info) + result.toggle_public(public) + def publicize_result(self, result_id, public=True): """Publicize a result""" @@ -574,6 +615,18 @@ def delete_galaxy_history(self): galaxy = GalaxyInstance(self.gurl, self.gkey) galaxy.histories.delete_history(self.galaxy_history["id"], purge=True) + def create_prefix(self): + """Create custom prefix""" + pm = PrefixManager(self.app, self.session) + pm.add_custom_prefix("OBO", "http://purl.obolibrary.org/obo/") + + def create_ontology(self): + """Create ontology""" + data = self.upload_and_integrate_ontology() + om = OntologyManager(self.app, self.session) + om.add_ontology("AgrO ontology", "http://purl.obolibrary.org/obo/agro.owl", "AGRO", 1, data["graph"], data['endpoint'], type="local") + return data["graph"], data["endpoint"] + @staticmethod def get_random_string(number): """return a random string of n character diff --git a/tests/results/abstraction.json b/tests/results/abstraction.json index b24c244d..470c1c59 100644 --- a/tests/results/abstraction.json +++ b/tests/results/abstraction.json @@ -535,6 +535,7 @@ "instancesHaveLabels": true, "label": "transcript", "type": "node", + "ontology": false, "uri": "http://askomics.org/test/data/transcript" }, { @@ -549,6 +550,7 @@ "instancesHaveLabels": true, "label": "gene", "type": "node", + "ontology": false, "uri": "http://askomics.org/test/data/gene" }, { @@ -562,6 +564,7 @@ "instancesHaveLabels": true, "label": "QTL", "type": "node", + "ontology": false, "uri": "http://askomics.org/test/data/QTL" }, { @@ -575,6 +578,7 @@ "instancesHaveLabels": true, "label": "DifferentialExpression", "type": "node", + "ontology": false, "uri": "http://askomics.org/test/data/DifferentialExpression" } ], diff --git a/tests/results/init.json b/tests/results/init.json index d557420b..0f3054bf 100644 --- a/tests/results/init.json +++ b/tests/results/init.json @@ -1,5 +1,5 @@ { - "defaultQuery": "PREFIX : \nPREFIX askomics: \nPREFIX dc: \nPREFIX faldo: \nPREFIX owl: \nPREFIX prov: \nPREFIX rdf: \nPREFIX rdfs: \nPREFIX xsd: \n\nSELECT DISTINCT ?s ?p ?o\nWHERE {\n ?s ?p ?o\n}\n", + "defaultQuery": "PREFIX : \nPREFIX askomics: \nPREFIX dc: \nPREFIX dcat: \nPREFIX faldo: \nPREFIX owl: \nPREFIX prov: \nPREFIX rdf: \nPREFIX rdfs: \nPREFIX skos: \nPREFIX xsd: \n\nSELECT DISTINCT ?s ?p ?o\nWHERE {\n ?s ?p ?o\n}\n", "diskSpace": ###SIZE###, "console_enabled": true, "endpoints": { diff --git a/tests/results/preview_files.json b/tests/results/preview_files.json index beeb18c4..6497c378 100644 --- a/tests/results/preview_files.json +++ b/tests/results/preview_files.json @@ -181,5 +181,5 @@ "error": false, "error_message": "" } - ] +] } diff --git a/tests/results/preview_malformed_files.json b/tests/results/preview_malformed_files.json index 2035e650..44312c87 100644 --- a/tests/results/preview_malformed_files.json +++ b/tests/results/preview_malformed_files.json @@ -22,5 +22,5 @@ "error": true, "error_message": "Malformated CSV/TSV (Empty column in header)" } - ] +] } diff --git a/tests/results/sparql_query.json b/tests/results/sparql_query.json index d815b914..0bfe4a08 100644 --- a/tests/results/sparql_query.json +++ b/tests/results/sparql_query.json @@ -37,5 +37,5 @@ "uri": "urn:sparql:askomics_test:1_jdoe:gene.gff3_###GFF_TIMESTAMP###" } }, - "query": "PREFIX : \nPREFIX askomics: \nPREFIX dc: \nPREFIX faldo: \nPREFIX owl: \nPREFIX prov: \nPREFIX rdf: \nPREFIX rdfs: \nPREFIX xsd: \n\nSELECT DISTINCT ?transcript1_Label\nWHERE {\n ?transcript1_uri rdf:type .\n ?transcript1_uri rdfs:label ?transcript1_Label .\n\n\n\n}\n" + "query": "PREFIX : \nPREFIX askomics: \nPREFIX dc: \nPREFIX dcat: \nPREFIX faldo: \nPREFIX owl: \nPREFIX prov: \nPREFIX rdf: \nPREFIX rdfs: \nPREFIX skos: \nPREFIX xsd: \n\nSELECT DISTINCT ?transcript1_Label\nWHERE {\n ?transcript1_uri rdf:type .\n ?transcript1_uri rdfs:label ?transcript1_Label .\n\n\n\n}\n" } diff --git a/tests/test_api.py b/tests/test_api.py index 1fad464a..81a8bb23 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -43,7 +43,9 @@ def test_start(self, client): "proxyPath": "/", "user": {}, "logged": False, - "singleTenant": False + "ontologies": [], + "singleTenant": False, + "autocompleteMaxResults": 20 } response = client.client.get('/api/start') assert response.status_code == 200 diff --git a/tests/test_api_admin.py b/tests/test_api_admin.py index a6adf370..30b6cbba 100644 --- a/tests/test_api_admin.py +++ b/tests/test_api_admin.py @@ -139,7 +139,8 @@ def test_get_datasets(self, client): 'traceback': None, 'percent': 100.0, 'exec_time': info["transcripts"]["end"] - info["transcripts"]["start"], - 'user': 'jsmith' + 'user': 'jsmith', + 'ontology': False }, { 'end': info["de"]["end"], 'error_message': '', @@ -152,7 +153,8 @@ def test_get_datasets(self, client): 'traceback': None, 'percent': 100.0, 'exec_time': info["de"]["end"] - info["de"]["start"], - 'user': 'jsmith' + 'user': 'jsmith', + 'ontology': False }, { 'end': info["qtl"]["end"], 'error_message': '', @@ -165,7 +167,8 @@ def test_get_datasets(self, client): 'traceback': None, 'percent': 100.0, 'exec_time': info["qtl"]["end"] - info["qtl"]["start"], - 'user': 'jsmith' + 'user': 'jsmith', + 'ontology': False }, { 'end': info["gff"]["end"], 'error_message': '', @@ -178,7 +181,8 @@ def test_get_datasets(self, client): 'traceback': None, 'percent': 100.0, 'exec_time': info["gff"]["end"] - info["gff"]["start"], - 'user': 'jsmith' + 'user': 'jsmith', + 'ontology': False }, { 'end': info["bed"]["end"], 'error_message': '', @@ -191,7 +195,8 @@ def test_get_datasets(self, client): 'traceback': None, 'percent': 100.0, 'exec_time': info["bed"]["end"] - info["bed"]["start"], - 'user': 'jsmith' + 'user': 'jsmith', + 'ontology': False }], 'error': False, 'errorMessage': '' @@ -491,3 +496,205 @@ def test_delete_datasets(self, client): assert response.json["datasets"][0]["status"] == "queued" assert response.json["datasets"][1]["status"] == "queued" assert response.json["datasets"][2]["status"] == "queued" + + def test_view_custom_prefixes(self, client): + """test /api/admin/getprefixes route""" + client.create_two_users() + client.log_user("jsmith") + + response = client.client.get('/api/admin/getprefixes') + assert response.status_code == 401 + + client.log_user("jdoe") + + expected_empty = { + "error": False, + "errorMessage": "", + "prefixes": [] + } + + response = client.client.get('/api/admin/getprefixes') + assert response.status_code == 200 + assert response.json == expected_empty + + client.create_prefix() + + response = client.client.get('/api/admin/getprefixes') + + expected = { + "error": False, + "errorMessage": "", + "prefixes": [{ + "id": 1, + "namespace": "http://purl.obolibrary.org/obo/", + "prefix": "OBO" + }] + } + + assert response.status_code == 200 + assert response.json == expected + + def test_add_custom_prefix(self, client): + """test /api/admin/addprefix route""" + client.create_two_users() + client.log_user("jsmith") + + data = {"prefix": "OBO", "namespace": "http://purl.obolibrary.org/obo/"} + + response = client.client.post('/api/admin/addprefix', json=data) + assert response.status_code == 401 + + client.log_user("jdoe") + + response = client.client.post('/api/admin/addprefix', json=data) + + expected = { + "error": False, + "errorMessage": "", + "prefixes": [{ + "id": 1, + "namespace": "http://purl.obolibrary.org/obo/", + "prefix": "OBO" + }] + } + + assert response.status_code == 200 + assert response.json == expected + + def test_delete_custom_prefix(self, client): + """test /api/admin/delete_prefixes route""" + client.create_two_users() + client.log_user("jsmith") + + data = {"prefixesIdToDelete": [1]} + + response = client.client.post('/api/admin/delete_prefixes', json=data) + assert response.status_code == 401 + + client.log_user("jdoe") + client.create_prefix() + + response = client.client.post('/api/admin/delete_prefixes', json=data) + + expected = { + "error": False, + "errorMessage": "", + "prefixes": [] + } + + assert response.status_code == 200 + assert response.json == expected + + def test_view_ontologies(self, client): + """test /api/admin/getontologies route""" + client.create_two_users() + client.log_user("jsmith") + + response = client.client.get('/api/admin/getontologies') + assert response.status_code == 401 + + client.log_user("jdoe") + + expected_empty = { + "error": False, + "errorMessage": "", + "ontologies": [] + } + + response = client.client.get('/api/admin/getontologies') + assert response.status_code == 200 + assert response.json == expected_empty + + graph, endpoint = client.create_ontology() + + response = client.client.get('/api/admin/getontologies') + + expected = { + "error": False, + "errorMessage": "", + "ontologies": [{ + "id": 1, + "name": "AgrO ontology", + "uri": "http://purl.obolibrary.org/obo/agro.owl", + "short_name": "AGRO", + "type": "local", + "dataset_id": 1, + "dataset_name": "agro_min.ttl", + "graph": graph, + "endpoint": endpoint, + "remote_graph": None, + "label_uri": "rdfs:label" + }] + } + + assert response.status_code == 200 + assert response.json == expected + + def test_add_ontology(self, client): + """test /api/admin/addontology route""" + client.create_two_users() + client.log_user("jsmith") + + data = {"shortName": "AGRO", "uri": "http://purl.obolibrary.org/obo/agro.owl", "name": "AgrO ontology", "type": "local", "datasetId": 1, "labelUri": "rdfs:label"} + + response = client.client.post('/api/admin/addontology', json=data) + assert response.status_code == 401 + + client.log_user("jdoe") + graph_data = client.upload_and_integrate_ontology() + graph = graph_data["graph"] + endpoint = graph_data["endpoint"] + + response = client.client.post('/api/admin/addontology', json=data) + + # Dataset is not public + assert response.status_code == 400 + assert response.json['errorMessage'] == "Invalid dataset id" + + client.publicize_dataset(1, True) + response = client.client.post('/api/admin/addontology', json=data) + + expected = { + "error": False, + "errorMessage": "", + "ontologies": [{ + "id": 1, + "name": "AgrO ontology", + "uri": "http://purl.obolibrary.org/obo/agro.owl", + "short_name": "AGRO", + "type": "local", + "dataset_id": 1, + "dataset_name": "agro_min.ttl", + "label_uri": "rdfs:label", + "graph": graph, + "endpoint": endpoint, + "remote_graph": None + }] + } + + assert response.status_code == 200 + assert response.json == expected + + def test_delete_ontologies(self, client): + """test /api/admin/delete_ontologies route""" + client.create_two_users() + client.log_user("jsmith") + + data = {"ontologiesIdToDelete": [1]} + + response = client.client.post('/api/admin/delete_ontologies', json=data) + assert response.status_code == 401 + + client.log_user("jdoe") + client.create_ontology() + + response = client.client.post('/api/admin/delete_ontologies', json=data) + + expected = { + "error": False, + "errorMessage": "", + "ontologies": [] + } + + assert response.status_code == 200 + assert response.json == expected diff --git a/tests/test_api_datasets.py b/tests/test_api_datasets.py index 3e35faed..ba5dba74 100644 --- a/tests/test_api_datasets.py +++ b/tests/test_api_datasets.py @@ -24,7 +24,8 @@ def test_get_datasets(self, client): 'status': 'success', 'traceback': None, 'percent': 100.0, - 'exec_time': info["transcripts"]["end"] - info["transcripts"]["start"] + 'exec_time': info["transcripts"]["end"] - info["transcripts"]["start"], + 'ontology': False }, { 'end': info["de"]["end"], 'error_message': '', @@ -36,7 +37,8 @@ def test_get_datasets(self, client): 'status': 'success', 'traceback': None, 'percent': 100.0, - 'exec_time': info["de"]["end"] - info["de"]["start"] + 'exec_time': info["de"]["end"] - info["de"]["start"], + 'ontology': False }, { 'end': info["qtl"]["end"], 'error_message': '', @@ -48,7 +50,8 @@ def test_get_datasets(self, client): 'status': 'success', 'traceback': None, 'percent': 100.0, - 'exec_time': info["qtl"]["end"] - info["qtl"]["start"] + 'exec_time': info["qtl"]["end"] - info["qtl"]["start"], + 'ontology': False }, { 'end': info["gff"]["end"], 'error_message': '', @@ -60,7 +63,8 @@ def test_get_datasets(self, client): 'status': 'success', 'traceback': None, 'percent': 100.0, - 'exec_time': info["gff"]["end"] - info["gff"]["start"] + 'exec_time': info["gff"]["end"] - info["gff"]["start"], + 'ontology': False }, { 'end': info["bed"]["end"], 'error_message': '', @@ -72,7 +76,8 @@ def test_get_datasets(self, client): 'status': 'success', 'traceback': None, 'percent': 100.0, - 'exec_time': info["bed"]["end"] - info["bed"]["start"] + 'exec_time': info["bed"]["end"] - info["bed"]["start"], + 'ontology': False }], 'error': False, 'errorMessage': '' @@ -118,7 +123,8 @@ def test_toggle_public(self, client): 'status': 'success', 'traceback': None, 'percent': 100.0, - 'exec_time': info["transcripts"]["end"] - info["transcripts"]["start"] + 'exec_time': info["transcripts"]["end"] - info["transcripts"]["start"], + 'ontology': False }, { 'end': info["de"]["end"], 'error_message': '', @@ -130,7 +136,8 @@ def test_toggle_public(self, client): 'status': 'success', 'traceback': None, 'percent': 100.0, - 'exec_time': info["de"]["end"] - info["de"]["start"] + 'exec_time': info["de"]["end"] - info["de"]["start"], + 'ontology': False }, { 'end': info["qtl"]["end"], 'error_message': '', @@ -142,7 +149,8 @@ def test_toggle_public(self, client): 'status': 'success', 'traceback': None, 'percent': 100.0, - 'exec_time': info["qtl"]["end"] - info["qtl"]["start"] + 'exec_time': info["qtl"]["end"] - info["qtl"]["start"], + 'ontology': False }, { 'end': info["gff"]["end"], 'error_message': '', @@ -154,7 +162,8 @@ def test_toggle_public(self, client): 'status': 'success', 'traceback': None, 'percent': 100.0, - 'exec_time': info["gff"]["end"] - info["gff"]["start"] + 'exec_time': info["gff"]["end"] - info["gff"]["start"], + 'ontology': False }, { 'end': info["bed"]["end"], 'error_message': '', @@ -166,7 +175,8 @@ def test_toggle_public(self, client): 'status': 'success', 'traceback': None, 'percent': 100.0, - 'exec_time': info["bed"]["end"] - info["bed"]["start"] + 'exec_time': info["bed"]["end"] - info["bed"]["start"], + 'ontology': False }], 'error': False, 'errorMessage': '' diff --git a/tests/test_api_ontology.py b/tests/test_api_ontology.py new file mode 100644 index 00000000..c22a1821 --- /dev/null +++ b/tests/test_api_ontology.py @@ -0,0 +1,73 @@ +from . import AskomicsTestCase + + +class TestApiOntology(AskomicsTestCase): + """Test AskOmics API /api/ontology/""" + + def test_local_autocompletion_protected(self, client): + """ Test autocompletion on missing ontology""" + query = "blabla" + client.set_config("askomics", "protect_public", "true") + response = client.client.get('/api/ontology/AGRO/autocomplete?q={}'.format(query)) + + assert response.status_code == 401 + assert len(response.json["results"]) == 0 + + def test_local_autocompletion_missing_ontology(self, client): + """ Test autocompletion on missing ontology""" + query = "blabla" + response = client.client.get('/api/ontology/AGRO/autocomplete?q={}'.format(query)) + + assert response.status_code == 404 + assert len(response.json["results"]) == 0 + + def test_local_autocompletion(self, client): + """test /api/ontology/AGRO/autocomplete route""" + client.create_two_users() + client.log_user("jdoe") + + client.create_ontology() + + query = "blabla" + response = client.client.get('/api/ontology/AGRO/autocomplete?q={}'.format(query)) + + assert response.status_code == 200 + assert len(response.json["results"]) == 0 + assert response.json["results"] == [] + + query = "" + response = client.client.get('/api/ontology/AGRO/autocomplete?q={}'.format(query)) + + expected = [ + "desuckering", + "irrigation water source role", + "irrigation water quantity", + "reduced tillage process", + "laser land levelling process", + "chemical pest control process", + "no-till", + "puddling process", + "mulch-till", + "ridge-till", + "strip-till", + "aerial application" + ] + + assert response.status_code == 200 + assert len(response.json["results"]) == 12 + + # SPARQL order is not reliable, so we make sure to return everything + # If it fails, skip this + assert self.equal_objects(response.json["results"], expected) + + query = "irrigation" + response = client.client.get('/api/ontology/AGRO/autocomplete?q={}'.format(query)) + + expected = [ + "irrigation water source role", + "irrigation water quantity" + ] + + assert response.status_code == 200 + assert len(response.json["results"]) == 2 + assert self.equal_objects(response.json["results"], expected) From 4a0363733116a1363260edc4d56be62f5a7b69cd Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 29 Jun 2022 16:23:43 +0200 Subject: [PATCH 171/318] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce78b3d6..90aae6b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ This changelog was started for release 4.2.0. - 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 ### Changed From 733c85debd2e3a5eda6d3634e58e167f45c5deed Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 30 Jun 2022 14:36:56 +0200 Subject: [PATCH 172/318] Update doc & fix layout --- CHANGELOG.md | 2 + .../react/src/routes/query/ontolinkview.jsx | 2 +- askomics/react/src/routes/query/query.jsx | 12 +- config/askomics.ini.template | 3 - docs/abstraction.md | 169 +++++++++++++----- docs/configure.md | 3 +- 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 -> 14178 bytes docs/img/ontology_link.png | Bin 0 -> 13408 bytes docs/index.md | 1 + docs/manage.md | 13 ++ docs/ontologies.md | 60 +++++++ docs/prefixes.md | 11 ++ 14 files changed, 221 insertions(+), 55 deletions(-) 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/ontologies.md create mode 100644 docs/prefixes.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 90aae6b9..08563035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ This changelog was started for release 4.2.0. - 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 ### Changed diff --git a/askomics/react/src/routes/query/ontolinkview.jsx b/askomics/react/src/routes/query/ontolinkview.jsx index b50dfb27..99e5e435 100644 --- a/askomics/react/src/routes/query/ontolinkview.jsx +++ b/askomics/react/src/routes/query/ontolinkview.jsx @@ -31,7 +31,7 @@ export default class OntoLinkView extends Component { - a term +  a term
diff --git a/askomics/react/src/routes/query/query.jsx b/askomics/react/src/routes/query/query.jsx index aa0c48f6..ac4000de 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/query.jsx @@ -559,7 +559,7 @@ export default class Query extends Component { target: targetId, selected: false, suggested: true, - directed: isOnto ? false : true, + directed: true, }) incrementSpecialNodeGroupId ? specialNodeGroupId += 1 : specialNodeGroupId = specialNodeGroupId } @@ -703,7 +703,7 @@ export default class Query extends Component { target: node1.id, selected: false, suggested: false, - directed: link.direct, + directed: link.directed, } } }) @@ -1345,10 +1345,10 @@ export default class Query extends Component { getOntoLabel (uri) { let labels = {} - labels["http://www.w3.org/2000/01/rdf-schema#subClassOf"] = "Children of" - labels["http://www.w3.org/2000/01/rdf-schema#subClassOf*"] = "Descendants of" - labels["^http://www.w3.org/2000/01/rdf-schema#subClassOf"] = "Parents of" - labels["^http://www.w3.org/2000/01/rdf-schema#subClassOf*"] = "Ancestors of" + 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] } diff --git a/config/askomics.ini.template b/config/askomics.ini.template index d861b8fe..5e75e378 100644 --- a/config/askomics.ini.template +++ b/config/askomics.ini.template @@ -132,9 +132,6 @@ preview_limit = 25 # All queries are launched on all graphes (speedup queries) single_tenant=False -# Max results returned for autocompletion -autocomplete_max_results = 10 - [federation] # Query engine can be corese or fedx #query_engine = corese diff --git a/docs/abstraction.md b/docs/abstraction.md index ea7d8c7b..208ff7ad 100644 --- a/docs/abstraction.md +++ b/docs/abstraction.md @@ -2,6 +2,10 @@ During integration of TSV/CSV, GFF and BED files, AskOmics create RDF triples th 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 AskOmics use the following namespaces. @@ -10,11 +14,13 @@ 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: ``` @@ -50,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 @@ -70,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 . @@ -107,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 @@ -120,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 @@ -133,33 +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. To avoid overwriting information, relations are described using a blank node. The relation `:RelationExample`, linking `EntitySource` to `ÈntityTarget`, with the label *relation_example*, will be defined as follows: +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 -_:relation_node askomics:uri :RelationExample . -_:relation_node a askomics:AskomicsRelation . -_:relation_node a owl:ObjectProperty . -_:relation_node rdfs:label "relation_example" . -_:relation_node rdfs:domain :EntitySource . -_:relation_node rdfs:range :EntityTarget . +_: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 -_:relation_node dcat:endpointURL . -_:relation_node dcat:dataset . +_: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 +_:blank ns1:atLocation "https://my_remote_endpoint/sparql" . +# Optional: Set a specific graph for remote queries +_:blank dcat:Dataset . ``` diff --git a/docs/configure.md b/docs/configure.md index 8d7e552f..31132d21 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -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/img/ontology_autocomplete.png b/docs/img/ontology_autocomplete.png new file mode 100644 index 0000000000000000000000000000000000000000..c87c10787ca5aa50830d8e063d2ae468cc561c3e GIT binary patch literal 13559 zcmeHtXHb+|w`LO;=;QIw6Lf~hXenxph^hhw03B@&8I)D_I~Ey>UBxXoUt&InO`+wIJNx_0nElem(L zsCDk!!O@2v-;feq!eE2o;qJ!bTWfEE)q`8)?%fk6q2ZM*_hs0qwHqL$CBKd5INAN4 zoxLm%DHE%v4%^=hJo!?mrsnkZm?b|y^OEaoMf3O?xFFEW4KLW$kI-_o%gNr#TwB=Y zc+J4X1YD9KAt6EjYI{V4goGJ4NUocIFn-lZV_z)XAK3&Mv@$eID=I2NAUNV>4-O81 zP9Y&76M#g}ElENKy#DJQAEI8((zqMH3mb+gBnzbKL7qQ<4uQ1mj&lK>KHpKg5$Ln` zii9@0SmnJ2I@Oh)p5DT?$b8W3N1lVn_JnbYz=5M^M=o0ytHA+SIoFLGg^|L50A zlNL|}Hy78d=k#k1K>zMLN;hwQEN}Y2^#=1f{N&^W0)bRt?eQ_LBufU_OG->*l3p*&r z^cBNM0$YCkxUGKu-}>?J@`lV55d&@QR2gIuL8ZZd2%^AK%<^GZXqMZ*RgH4SZg<8UIsV+vR$dmhXveKWYoY| zFUcqGFl*il`q~=Z`T1cHnYQk#ZsvtM-hk4+Ow-P+y-)XzRZW{;P&h<06;04U5*toe zd%hQb`gGq@$sHCo)^qJj%lL`jYF*D38u)+kx_XuiX(FA*Ha#2Wkfx)P!4nqawc}4v zeaV^|E^mNcv%G2L+|V_dRCY5t6hn9XM2zkenCNZKeme0!#fBxRMA?3+)BNm?3Dy&= zaT?HO*JDv%eaNc6jaG*h-VH&+1Y!F16*`k55A~|g5a5uihpomN(k{@RaE6Q)I-fsvB6O$7@Zy z4V>vsS=?f+tFw0AomZFkt>&lq3I5!RG@p0u1=t`E)Fj8lr^byIJsc7;e4 zgKs?%F&z5eUiUzT@|=e@4raL0JNx1AcaMsR0GJV7GI^`1)YQd9HEcD}p$4qvp00Qqk`0w$-D zZUE+Edh<&CG)}(_4EO)o4;rRDq8E~T0?$0QXJ!b9iDw$U>4SaJ!VEvth0li54*)am zB#+S_CKkGy2@z2~YZ7E(V|{ww2%%|6)z2-i`Q}ZZfcrhma{wFTCE(`~`N6?%I$-D3 zoz;=;?n#K#4S!_e9z(>$GY8Kf>g`Nr$r-V`#M??IO>;!Ua;Al z1?i!72xS$mRv|19<$u z!JG`t9~T0&%ibdkwFeos!@Yt6>tR{w+q z?Xol9Vf#WOJ4P4wI^s!<^XPm!tkikkRk-=rdi(=E!I+qt)h|!L&=FN!6Qw)q2~;j- z@HM@^WzoB$$s1J8zxxLzv!@5Ro38kiNpNmA=qIn8xoT?mb-Kq+FfN-ej0Js)IX6Rg zx;P>YhEIlLzP=<3N_O8~K;_r2d5#P4kID&Njzx&+7)bE+!pX)q~Th2dTi_W}k z%0!(z?tALSP16Af-!0k=nOk}qt`*~6JtqmB%Uu-FZ#b7VADIQirP&Wr*7`cN%k>Ec z@d*!vG!>Opa$ihHp!(RJ=Q`qAMICwU(G1=^`4g?WZOg)M`F z(~OY5sYy-_?aJevcA@Asw??_0gf*?~h8=KQ=hwL&wsSfOUn>o>j#71f7EbNmNVOY7ctpp`eM zOiB+y2&WXvn*4~l9&NR|93yC1LuWgi0TOjd(KL&clA8Fn(fYg`#~wa+HpWA#u1el9 zB(*=DdtoF;sU^Gv_fTAXBkP4Yb!{T*?Po$v6xsRNF1PEcgV_>Er^`i`Ws=R<**AG3 zDAcS=iDWCU!Hx(3Ia8W395;k|AlV3|D$jPprXCYxou(UTShwIL)9F#N)PD?SA9u54 zm9k2YQzv959g~)hwUx=r2a{o@iL^WEgP&WObiG5W0VjLx;G(H`yky3|varUCqM*k4 z1_2qlgm)OyLqni&zJZeQ=UG(y~w;J+z*mTJR&2wM#9IwsKJ5YhnCZen`R)W#*)|(;+ zu;v$`a26qcYb~=hlR1RKwr z4s1;8Hn_Kac|2)&CXcquOc-s|&1FXLFi}>Pb1FPXb+g1cr}ym_Jpe;S`M_#lT)1fV zq0lMe#~%VC8!lL*n}DMUlGJQFe7~D}f{paJ?5a2e?H$D<7Bag$oy4WgcXpEXSI>LS zTYDHYO@gK-ppxZz$2%eSr%vC(Ex{IvKbevWM%Dj9O?~ zstHKF(L_;ayI^KU3fZHtrwiVduW0X8=pPsKP2dT>9V;#9u0cJWRIOPtIVSuO!MLe< z_T#kO*-!3?OBF`rJCa=$g?i|YIoDmfQ4?V0^!_}57L&;+^Ax<4UA{{JxZPa?cy3^j zr)K?LiYHz6vcly#-1fA0n*1p!OK!dZyYw31%tths-2l`adH?e5|1GDDHw+}yLcgO1 z7Z@Xe^M-1A1?Tu=H`~e^2n&(ds^np9*s$wb zOE*EgHxx?E8=+N?fe0h@P0RmU^}_quDM;Nd&}}SdrITqXN+(k*xS!vR!dW1U5aGN9 z6X}dWs43^3DReM^c5f+E@N7j@%-jU)qaUF8tu*}Sn|LXILgeJ+jEszQy0S3Rs0lao zOBt+o@;%NUZi=sM@!K#1+4Si~9YMn%9Y!|?N-AdTPkY+fnq0`++l@erAcgecoRPOp z&|$A^7-fy!X$N&;@>>jLLpz!l@mZ%j0$ZCoT=EtnVXiCH#w{Wzy|0CN-nUbK1!))M zXG)CsIdw5>V71QFIpi*J)l)4?tVhJ;F4r4x&E!VjN&Y~zMtM^~A3J%xWUfnD;SzZs zUMZipm1q~yD~_VZUob(0HmE&Foj{8O34 zO1+5ZK64R%GEIDrXM+6lZB^EX4LP1ilQR;VJ6z#AM?IuqWDHZm&Uxgg43EpTMO~@5`D#+!;@5rMI6B@ia$|w^aD=-O6TbqImstXdH zmB41C`2O?f&ya5=7P(19`KYIVbx8MDE%el)ZD09pkzjlR}&CQ%zFMXfUN- zZKLzagG!?H)4y+>5RgG*Wq7eQLk8vSWWAfZZOHlg*!0P7U;r zO5a4>VYu7>=`xE>+dnQYZh393duxRVv`a`J(bU8hB@!GOdNA?zRDY@&LX(DvDW8yG zPd?JhUdcmt(MmD`L-89pZxq7XCGU=P7?(CQq>N#G7Ww&<;RSJF2{7RB(~YpUaFk!ZzE@)#f?F8wjDQl=6l(PO}yGBmyz$TPCpjghFYKjpYKN4vN{_0t zfW?d5`Ov<1{+yq#rylVAl0?4{!YhZ5VaXW*2#gNhiHV6o(X`y$6~IEp>zi--)YjHg zdmPPFi&$G*hZv)h1qI}=0{-IRA9|R%(n)%FWS`K=6YruW<6JvD3)80qb5gt+*QwU&&YDUl-doG6? zx*E8m_Y#tL#D?auS1O+i^bx2E6cgc*6Cus599be(s@lWD@BGsTYWZ^`<2o9Pw%H(d z5|SL!yS4TBx6%{RlGlj}j3UsGJ>ee&zRcO*-Zv9}et?o4mKx+905mk^dFd`E*HdED z7D~zQaI`fe;!iZwVSZcltwaCa%s}aZXeJIh-Gs(fR*U^Jo^ParmnkW|qmcuH!R5iW zfgi?SHr!%sWwcP>ws_C&XlG~l=FRlM`dF3CM6JuNsi&vVS4R9R@Fk@2D4N&yXJqE) zf8|*{Zp}$p7uerDMy5G(K+j`XtWR?8pNUa^=6<%8Wd^6@R03o>FYw@Ny9sgX0S)6!@BhPk29lh%x2<4(vEB%pCk7i&O4RLMs*KwncCL zG3wiGg2Au;g?w;l9n6p906o0CkV{99gC9P>u-KPRt&V8Ls^PctfKdX#&S1^6EtMO9 zT=y4!_@#?E`Uh`T-O!?z*|p0d!g)%fY92H>6;+lU_^-dbU@yN;R}o!J#1l`@2Dx63 z_qPdKfuE49<3Hb`E_h`#<3&&!ddz|aEG)PUvYHORR!;Ovm=ZN1|Ea;={g1;Ia?a-1)h~jtA2!sV)wdG~&XQyD4qp_@RqK%mB!Ch4BHp!N#biFT+O39cmy zRpkV@299Ub@6k1hiwd5-J&;LWKG@et3{9*u(t?dU2O?is-5E{irkfNgS^r`g)qr71|*0R*QRQ=`M3ak7N-+~ z=I2RE2iFNRC=@_1FMfWHT!W{}b@aBdUML8C@LR0xW#^M9-F-iD3o%z%xWdw{dLP~K zfYL6!I8^T`Vm~qR-H?h*L*Cr{iA6xuUVh~%^!0FV=Y&9+LSQ1TM?yVJiKi;hLe!>~ zlL$gQ+Y%eK1~YpjMAHYG6CZs?rb_;=kGTc(k-4q|E@hSS zuZJDt-qmy~xw}yW@tFNpArKc*K-7N^FSyY9eXO3wyg#h%9O{Y~1Z-4HDd=(pJ##Y>rQ|N+V z>2^hFS`AfVI_VTrxdP$^np@$SE2Aq?G$=!T^H<@xX!5%ECqJjy^H)c|`&_T*Sc9+5 zWhYXsViXuT`wvX5zUr$D;(iSvue0Bbb++dR(TRbQvn zq0GF%<;Frfhop_}6r2_0Jw}xQ^03A!4iTKm(4KelB4wefQv*LKqb4eb^34Q3iVq@; ztZa|4Dz(iToX#EQ_h4rF+&}fyzt?-JT3 ziJzAgImnfC>%Fc!SoZ3q$eSvDy%23{t++2rol>Y*{(6W*ulntNHg50alRB-2S1}JY zwmK&Z3s)7?^CP39iY5yb7MdwHUSfLr#5NtK$tA2R@{vQ;bBF~8jqe1pp5;~1k7&PJ za@jc|pqcpxda`^>)Be~t$q4M1y#bZ58_(r)B=GK@ff6yX^gX>Pa~atu>i|XyGw^w`4n2*zApj>Y5=QRDZb0cJF&vRkFRBi+jY! zg@xo=8K-v+jKmvu!!~vT%(zrMh7*N#bxEFvpN5CF^z(M>?)gC-h2FT@HGYH8aEgvV z=)uh069kIcXannXpDAXWfTR+Nz(|hzLpKy<%n(mube0?%F;Kiv(KFOxiJiW;`_t~! zCZ-#po>glw;1U|lg64CHqTvNhUXc>BQFCnG@U}$}pGYCgy+T3Bie=H~@Nd;-rS|?9 zI$Wy9hUpXRm$dJ6($Cw_Wfk`d1oO&`>hEF0JNs69o_`R?4^CjPYL- zZ9Bi{l{X+cBoI&?8X5w4B~9D1L)xl%OJ)qZFPLb&+MdJ3?tC||H=#38=kYsxKE8*Z zbA%Or_DRr7(}f4|Glfgn+_Pp0_e+y2du5Q1Iv*Tv>W@`@+Eof|E%m=QTKqVKX+L1% z9R@QD1d=$OE3W^4^kR z_{DxFp)?;R?Q1?pe}&uTOX-augpRg2mhi2~dk?a3Adh}PvVH`3-N~+%g)b#dAu?>6 z>&wdsdgF2a>@#!|3h&H|oG_Zjq)GHjZdU70ho>BgZbQL`Mu|9#3y8UW)3imPwZ-(n)G{~+=2#8?nkmwq*07P%Ww3iR;r49fnU(G#kE0USSlF70uYRg)> z`nf2z;S}4l6ocn4kp9_amhd2?z7jvcpiA^RhGKAs`MY8`l!YGZ=x5iZZ${Qo50ak9 z5>!>T2d`-|JGbPHt_U>1o(>|3SEJQ=G>~kM37<^^Zu)+j4OrM+Uqabk=RLvRvddc!up%C=i!dCSb}+ zS}@i@+vH)}9DSo;>Q&po&LV3yr;Ov6Wl99;V?#qVG#AyN_Uk%}6eds5xDCAQdGMTCga=rHB?^=xe%u{2;=qk8x}6c ztNJztW_(zGn*T@y`M&qfJ0Q|){kGGVcE~(DF*X7|d}DF117O_JaBwLLuxEt}3L*)A zhs`Ukya5vf=5y~ixQat@ft;D(7l3gz+OYo_bgf_x3yszH;n#Ap(A)o-AB1uK9qO;p z*8+g#+<9M8;?F$XO*!@^qNz<@2Mkw)R-7P<(!yll0p<7-R=QWY~y?zej4;ken(ut?=fD z$?cX!xfw4RTQI&l{%A~PsEm4{IVQhE*8 z0fzCRV8Cfk4Qh$3l;7{uvLSxi!Xk^w0f%Y91-k!35hJdimD27NI`s#_(xMN2d@z0I z@2=Fk3|T$e>guuo$lo;tL-(W1 z>svo%sxqsnR@Lh`!IhwamDBjFT{Vw%O$q5H;s?LrF|V^`wc5yxL~0KV*k4|RY%Gs! zT^q^frOHlH*i6ROxPm|Y>;^iX8lyo_sH56LhCbp~HHv2TjIwjDv7#_1$o?@9gG9Y& z8v4n}7bN%>JhO;Vy4(K5;sH6UJn^kxC)sW_RT4xb-s?Cn}(gVpEXZ(S({EX757!QT*|W(XhU%#&y=_cw_ZR2N9fA z*^`8ElJwB~eMrTO+O3&)a}-oRfX(4|LWaZK4`y)vZ5`OGF7(_rIFPGf818rb3RKAZ zU9I(0Pa@XAOkRso&>Fips#Iqwkh%b4&nd!zw1peOSLVN_uUdnc{Nz^4N2+G@qz5gRYQAaHkjI zJ=}{Gi;JGQmR_&(1D?Ug(`CX%JBm>v&a>4Y3pR6rpw06uWtVB8s?UKUTzrZ_B#zBj z&+Ex~;#gY%c=1q>x!AmhBfCDRiCCry>fw3uY&+w{VOJzv{}DU8R?X&4hBPT3`;?wa z`I^v}YH#`&ArjqCxrbw+v9}iV$Ww2~3orL~i*4o~P4gEk!PGt-5F{17Q2% zh_e!4qREA-0tZXin!yqY6bii5CnxJ> z(p~p9msPZCJZ(;A`^7egZgmX~9(8<*=qJ;6D*oszl;s-)^NruD=z%m$4lxMnmxQ7{ zhX2|b06*z`KD9@FrHF-0u^n=6A)5(RRku9;?H;pASY@&_lIzoFSOA`fKC@X0Ms(p6 z0_qkj4)uRr06jc%y8s>$%+K8ja^g|(EYDf)>C}^8aL_Cl;Bhf!M+A04=y_A_Msr@) zirmYf0m>d}2URy6wrWC|NIVlkUdOoT?c08^EWiAzM`bBYTQn(58kK_EZ!AiSBA$vD zQ7@~@=oyaDx$$x99mYEgCkWksV0(5nQW{Z}U!G~@3zM#_uxEQPy;*GEtfECsd3%Y|!ZcB`Z%6J8dkK&QuyUu2?;|#6JH(yOhPSHH5~D(mSU3QQ1~--WZcVrWxr=gsByI$Oy2BgH z)w8XT5POcq=RVEw*rI$38qsClFNbudw)nU@pJ7^~5Ki5iECjT4`)(C3jp5xam?VGI z#;v@ej;rTjGtvNAB}&J|!_z|M{y(dbv?VrS0h}tI?G(S;g|Niof_9aSIi|d_(!mWx zC!VS)vF=c~KQaHY!|zI39w2RF!{(D&VI2O+LM=vGfo>#({t&l*$2Hc7#D^~vC`UmO z#vlv!GqRYE14|HjxV5b`L!wr1isfc4dO*V97RIY?ddx>6bcol(b9Qjsj~gb(!7L9K zbjhknt%+vhmNQTI$y0A0F`%Lj)${ja=Gj@0tm;yH-58;BlxhD4GhPE_j+b>113BQF z6XkrF!o4{fHn8cJo}RuZGuz&dcV7AI*)z$8r%ezV{VxCsAH~Ebr=C!5F(on(_wUk^ zv8UUp`59($fR}Vlk3z7u{olO@7bNw_zq9m4 z$O965^Y548WPi#c8TW&9PmmK-{RnC+sHxP z$;pW>{H=urTEZ}VSuL6bnr+(4Q z73s*Btipt5p7^1HtQEP)r0ml`QcBSNJ|&5Zn1~2=uJ`p<2{f9YpFBAg4@W~l^Pq%u z{LwJdl?x(@a~sJ9hJM>1i>ZO3pCuJcT;(2j#aP~=VF}9*e%&Z z2UEupt{ZJ3!R9JfMtP z?e11^k2(5h*IgyB`OcLrxXBN#_xlbx(i5YlB}k3YXt>x&tNSK0tS2TemLWce~#LrdRP%tU9oknKI5hO1X^NtELZ>ac}+5rX#W%c2b>1U zVrEbBDvJ<}g`Hw~S!UyTSwk9zp=#l!3hBu#hkNM+1OFz?&0mhQwlx8x&j`a-kYR$B zl*pc{TY1N`dpw1F7U7OHak=50Hy<^YGdfwxhHxX1g4>-|_!S|Y*hkl&?5$iBxRq=> z8ZNfn)L_1?f$Mt{S0_|xINaQ+gH*~5%aJ00tY0UVF+ONwflmx|E&W3kc6d43$c4>2 zCxcY{(5BzUGFf!C>vj%R-AEoYRZJhJQOX7?w`kNK_V4O>)}~2*o}V$+I5pTQqxkT? z@}<(6;Xp4zD8!oSjG7g@?TXPlgB53TZpJf~e<_Qh2lv$0HR ze-N`jAIB0ta>kO_LK$87xkMqKig<7xC1_Gx)&ku5{@ywsnoCW5J5o=Eaq>o8BimY@ zucmZJ_kwy_@BB2_--ZJ}$=9Z6%z^t*N98-KN`<@iMD0QG+V-=dkWt^~(pB(C!-5y} z`~CZ|5Wau2Ezta}KXY6W7uO0%uZoKQWjnqiEC0dIFRJ5@bzbp>f3SMR)d3IoesAUf zPxF7{(3&dbFE64Au1b1?N&NNp-wKeV);MM6La~)Jmj7t*Z)e+i3vjlavC0Aed~h&; zhh+0FXh@m?#ZJYMzsL(B5C8Si3akK-7p1s=z{9zOE>UySz%?fRqjVh>H*h`V`AxGC zCM&UABA#Snr~3O=0vt*QowhMZF*Q!wTG&obguQ$^pDn%7V{vVT(545Ks`F7Au`7=B z7Fl4g!^~po2{qF;aeb`%Olvape;Hh5s19blysTC;s7P;7C0ASf>A}Msn}}FN1;6at zY`)EQrEjpLR4u#s0W?4P5X`O8kJTc9K5EFAZ~`c5wyH!A9Cmcv6>PWXA5lw`tw9x=cD$-z{7vN7iXRnM*QhG_PCA1=Ut< zCe6z-CCNi4Bo}K+u`9;4y^x(BS5vizxOyh~+p;+Q-N46X^&Wu_-E?6T89G7S_bIx{ z8_c{Fzm0iIYAwEQX6!ComK7g5s37~PS|d5M7py&0{y?U?k44hp^sNh^7278g|pU^_@Iz|u6-r*#ZM_AMDsuPW@XyB@l+VI#{t2W zs^$}slB>RBMZE7P>b3i9Vk;GpPWLC$L&jLFZCNE@AKJ10gzc{FCCHO?P6ImgM{jMg zOD^Iur3}y#q;uA?<{Oo*cQg^`6E+3L+tH3q%ad}W);*Du@5!$X`S5A)w=_Q)HCg7~ zkIq6doAznH_>VCWaJs{1mPvn{E?Ry9Zfn9=AQ1j|w5wIz4M-Dm=A#_2b+atStPy3m zO9#8{ZsQz~-A=PCv~rBfRj`d0sXZInkHP&H2yMW6b1KOtEl*6bD=LwQ5Wj0sgk!|l zTYRi;-13X$5BQucbIM55Lgk%b1Frlau)v@Om3=2o)O0u9iUg`sE8O}5UM{~C53@0<3mc*){Qc0O@-2NnS+uH2*|1|(WLz{I;~M^`Jk4A z4~h~~O%hfe8%856iMy!6lqYiJs?#0E_*q+5l@0c-?i#NqO7&^is`dSopYWl82f$|k j%=G_=6UCC1JNJy6?%TkUeFq0#)q|wO_ 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 literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index 8ac1ab6b..455cedae 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,6 +16,7 @@ AskOmics also support federated queries to external SPARQL endpoints. - [Command-line interface](cli.md): A python-based CLI for interacting with AskOmics - [Build an RDF abstraction](abstraction.md): Learn how to build an RDF abstraction for RDF data - [Perform federated queries](federation.md): How to query your own data with external resources + - [Link your data to ontologies](ontologies.md): How to connect add ontologies to AskOmics, and connect your data - [Use AskOmics with Galaxy](galaxy.md): How to connect AskOmics with your Galaxy history

diff --git a/docs/manage.md b/docs/manage.md index a808ad41..254c13f1 100644 --- a/docs/manage.md +++ b/docs/manage.md @@ -20,6 +20,19 @@ In the latter case, you will need to run the command twice (once for each namesp - `make clear-cache` This will clear the abstraction cache, making sure your data is synchronized with the new namespaces. +# Single tenant mode + +Starting from release 4.4, the *Single tenant mode* is available through a configuration option. +In Virtuoso, aggregating multiples graphs (using several FROM clauses) can be very costly for big/numerous graphs. + +Single tenant mode send all queries on all stored graphs, thus speeding up the queries. This means that **all graphs are public, and can be queried by any user**. This affect starting points, abstractions, and query. + +!!! warning + Do not use *Single tenant mode* if you are storing sensitive data on AskOmics. + +!!! warning + *Single tenant mode* has no effect on federated queries + # Administrator panel Administrators have access to a specific panel in AskOmics. diff --git a/docs/ontologies.md b/docs/ontologies.md new file mode 100644 index 00000000..2ead6460 --- /dev/null +++ b/docs/ontologies.md @@ -0,0 +1,60 @@ +Starting for the 4.4 release, hierarchical ontologies (such as the NCBITAXON ontology) can be integrated in AskOmics. +This will allow users to query on an entity, or on its ancestors and descendants + +# Registering an ontology (admin-only) + +!!! warning + While not required for basic queries (and subClassOf queries), registering an ontology is required for enabling auto-completion, using non-default labels (ie: *skos:prefLabel*), and enable an integration shortcut for users. + + +First, make sure to have the [abstraction file](/abstraction/#ontologies) ready. Upload it to AskOmics, and integrate it. +Make sure *to set it public*. + +You can then head to the Ontologies tab. There, you will be able to create and delete ontologies. + +## Creating an ontology + +Parameters to create an ontology are as follows: + +* Ontology name: the full name of the ontology: will be displayed when as a column type when integrating CSV files. +* Ontology short name: the shortname of the ontology (ex: NCBITAXON). /!\ When using ols autocompleting, this needs to match an existing ols ontology +* Ontology uri: The ontology uri in your abstraction file +* Linked public dataset: The *public* dataset containing your classes (not necessarily your abstraction) +* Label uri: The label predicated your classes are using. Default to rdfs:label +* Autocomplete type: If local, autocomplete will work with a SPARQL query (local or federated). If OLS, it will be sent on the OLS endpoint. + +# Linking your data to an ontology + +This functionality will only work with CSV files. You will need to fill out a column with the terms uris. +If the ontology has been registered, you can directly select the ontology's column type. + +![Ontology selection](img/ontology_integration.png){: .center} + +Else, you will need to set the header as you would for a relation, using the ontology uri as the remote entity. + +Ex: `is organism@http://purl.bioontology.org/ontology/NCBITAXON` + +# Querying data using ontological terms + +If your entity is linked to an ontology, the ontology will appears as a related entity on the graph view. +From there, you will be able to directly print the linked term's attributes (label, or other) + +![Ontology graph](img/ontology_graph.png){: .center} + +If the ontology was registered (and an autocompletion type was selected), the label field will have autocompletion (starting after 3 characters). + +![Ontology autocompletion](img/ontology_autocomplete.png){: .center} + +## Querying on hierarchical relations + +You can also query on a related term, to build queries such as : + +* Give me all entities related to the children of this term +* Give me all entities related any ancestor of this term + +To do so, simply click on the linked ontology circle, fill out the required label (or other attribute), and click on the link between both ontologies to select the type of query (either *children of*, *descendants of*, *parents of*, *ancestors of*) + +![Ontology search](img/ontology_link.png){: .center} + +!!! warning + The relation goes from the second ontology circle to the first. Thus, to get the *children of* a specific term, you will need to select the *children of* relation, and select the label on the **second** circle diff --git a/docs/prefixes.md b/docs/prefixes.md new file mode 100644 index 00000000..7b2266ea --- /dev/null +++ b/docs/prefixes.md @@ -0,0 +1,11 @@ +Starting for the 4.4 release, custom prefixes can be added in the administration UI. +These prefixes can be used by non-admin users when integrating CSV files (for specifying URIs, for instance) + +# Registering a prefix (admin-only) + +You can then head to the Prefixes tab. There, you will be able to create and delete custom prefixes. + +## Creating a custom prefix + +Simply fill out the desired prefix (ex: *wikibase*), and namespace: (ex: *http://wikiba.se/ontology#*). +Users will be able to fill out data using the wikibase:XXX format. From 8d1e6a1035ba425f8b45e4d2e65c5100f5de95d2 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 30 Jun 2022 12:45:43 +0000 Subject: [PATCH 173/318] doc --- docs/index.md | 3 ++- mkdocs.yml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 455cedae..ed1b34b4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,14 +16,15 @@ AskOmics also support federated queries to external SPARQL endpoints. - [Command-line interface](cli.md): A python-based CLI for interacting with AskOmics - [Build an RDF abstraction](abstraction.md): Learn how to build an RDF abstraction for RDF data - [Perform federated queries](federation.md): How to query your own data with external resources - - [Link your data to ontologies](ontologies.md): How to connect add ontologies to AskOmics, and connect your data - [Use AskOmics with Galaxy](galaxy.md): How to connect AskOmics with your Galaxy history + - [Link your data to ontologies](ontologies.md): How to connect add ontologies to AskOmics, and connect your data

- Administration - [Deploy an instance](production-deployment.md): Deploy an AskOmics instance on your server - [Configuration](configure.md): Configure your instance - [Manage](manage.md): Manage your instance + - [Add custom prefixes](prefixes.md): How to add custom prefixes for your users

- Developer documentation diff --git a/mkdocs.yml b/mkdocs.yml index f10f9490..44953fd2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,10 +32,12 @@ nav: - Build RDF Abstraction: abstraction.md - Federated queries: federation.md - AskOmics and Galaxy: galaxy.md + - AskOmics and ontologies: ontologies.md - Administrator guide: - Deploy AskOmics: production-deployment.md - Configure: configure.md - Manage: manage.md + - Custo prefixes: prefixes.md - Developer guide: - Dev deployment: dev-deployment.md - Contribute: contribute.md From df3ea49bb1ec9e470fc3384758a7cbb37435d30f Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 30 Jun 2022 14:47:54 +0200 Subject: [PATCH 174/318] new image --- docs/img/ontology_integration.png | Bin 14178 -> 6304 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/img/ontology_integration.png b/docs/img/ontology_integration.png index 7e49d0fd085c5cdf819d04afacd3c4fc6311d0a1..bab9e9b0d25356b0f3a6c309af884b90e13404c5 100644 GIT binary patch literal 6304 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%0A`d--Z4^B1OwfuUDuajB+pw*iU89J4vQ~ z=f^0kXbLB}w!_`rYVA?xPym<8OF98c=gRCJHCOm$qw}g=!iky*Vt9T(5J(@J-enjxwF4^>TD+pmuG(?PPjR~=^iPc- zUVdv~EGFv}Au1e-7+JW77R5hI+U(ndjX?p41YkN4%W`+?f)$uy{{ zE{0MsIR1PvioPkfr*n%tF?L$GwuYXBG}E+&_#ZkJ=-$-9xBhqM65)G^(n~MT#)GC> zFiiEJ{D`aWfP#|)-7!o*hsmcvqg%q1M;OxqOM~^$tFfWFSW%~5;ml*%TkUPv|~PqEB|RP1ZK6bV126xa82#gxO5!YJzf zNuDO?bO?TcGH^z4A@qR7BY5;p+-T^YMfecw*Hj~>k(gc8hb8HYpEVpZPv$M^yslQ(H0U~P1#@6=g)VscWZWb<4OHGS8Yj{;f?QRBJ78&NW)IU@J)mCeT`O7hN|=gDY48EhExKXyyN%H z=Q9t9Eyo`)TCxAAHf*o>zspVC;|L}w)By&J#ey6?j@mP1!&-`ZVl}i9PnCVf!*R{mA)TaD@)D?vWW@%u1p?G#bEB%c(-2@zFJ#D|u zA=s8g>n;~(xOsUj7k{c&0wCc*dLhiGZlmXx%50D}esXNlNM;CywjwiRls#RNvpTfp zp4hSi(L&-^>GufMEmCG{5Lu# z5fdXyds2}7=KH+WGXCEn*c1|%Lbh?PV0U2;-q1ERJ^JKFCkCt3oM3zY_1Wq4#-aze zgV>mv+z)0EQ715-Ea&99#xWsL#opk-@Vsrlh{`E0t0|WdWvP0a-`dX6JQnubHzh6z z*j+u_=SaOWuUD%snd~3x!QN3Xu$nl`u)7>|{A%s)uilg3*M3`e=D&C6-^MdV1_~5s zf5`IsL^$#7`mCk-1WEp{F;_{3(Tvz>D9R;@0a2N_wfI=)X+3e?K|f*sA~tS6cHgd;q59SMftGl)q~OT)U(vb`jZIc%T?N)<0mBu~*X06A85(WplWz z22I>aRun|irru(G4$n-2zh>M34mZ_$^NbS>H2<#muS zlS%{<1I{pO3RE-W2zuhc=3C!Cx@IHL{myq$E833Myc6CZIG>Kvi9{qDp9m24?{kjWk&I~#G{8M1xXfHyu0bz-Mt6Hb z3VCq;<>nXMnq?kmNeUsZeL2oES~ARb+6j2jU@?B%0VZB%*e@$o=jtmixBcWW#x=G^ zg_78jpe9P&@JG>{3l>Ovu6c^Z*`j$Q;;yTMXt7ED?rc!lh7>}0D*1Dc6oxB5-W#I% zLzb8DB+=^Cm6s+2;QSuP1(AtVJpyqVcg)n~K68{#!?|dna?rqN>u(25*8Cc~1ki$Q zghhta1-BsJ$iAB{A&GX3wCdN{AY12ixHa2Ryfo~Y2*Hj8 zaYe=z1Zlh=&6|7li$#9G)~O~4>1$?aIX|8Jbb=m=ha#XV?`wM4%BbC;S~OPOMbcHt ziO7&vB)0mD{AF#DFF}?weTMq#xm7mKy`o#2I4)pTp;N$grM56?`Q?1ksW`-_q8Ue} zKERK{s}AT*^~D%RipZR-QrmO0J{6OuqpQ+JD&^?OfOOf^@iAh@%umnElZ&}BtzkRY ze3vD;-+M^Xs164Fo0VjxvBLbj>k5yM^K!RMy`@Ks%8aD#YsjXJVwtC1`HG#G-gb{m z_1N|5Ozoqm%dN&&y{=Mk1wUELz{RIaOD)~7p0Jo=)WfQ#v3B$ynrV{3tK7b>ysKf^ zCfFIL-85%54<`yjfNQ?#w0$3zBKbV`*S-jJG>zHM$@P3fE48qOZh{V*S-UzCV}&Q@ zqhIz3?ZusaCp4mNj&<#PCL~F#iF`=sVJKg%OqWZLaqQMbAd@^Xl(!yaBW{}f-(LfY zz(Ukfw7lOH?);5i%-2;G82eRgc)J;*TU%HT%-|F9o@)@W$rzU+p)`m4yUWKx$cyA| z*&9bJj#Z;Nr-N;)50sGg=@fZCS1({{iy$Kl#PW?&|KEVeA?bg!z^-8f$Pei=J+rov mI8>TyYH2=S*tu{1y%V4r&O8LkI=uZVz$wdX$kobNg8mETg#{@9 literal 14178 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 Date: Thu, 30 Jun 2022 14:59:33 +0200 Subject: [PATCH 175/318] Doc update --- docs/img/ontology_integration.png | Bin 6304 -> 6323 bytes docs/index.md | 2 +- docs/manage.md | 2 +- docs/ontologies.md | 2 +- docs/prefixes.md | 2 +- mkdocs.yml | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/img/ontology_integration.png b/docs/img/ontology_integration.png index bab9e9b0d25356b0f3a6c309af884b90e13404c5..e0fa51b91489695262df1ddb24bb8531a1f488ce 100644 GIT binary patch literal 6323 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%0A`d--Z4^B1OwfuUDuajB+pw*iU89J4vQ~ z=f^0kXbLB}w!_`rYVA?xPym<8OF98c=gRCJHCOm$qw}g=!iky*Vt9T(5J(@J-enjxwF4^>TD+pmuG(?PPjR~=^iPc- zUVdv~EGFv}Au1e-7+JW77R5hI+U(ndjX?p41YkN4%W`+?f)$uy{{ zE{0MsIR1PvioPkfr*n%tF?L$GwuYXBG}E+&_#ZkJ=-$-9xBhqM65)G^(n~MT#)GC> zFiiEJ{D`aWfP#|)-7!o*hsmcvqg%q1M;OxqOM~^$tFfWFSW%~5;ml*%TkUPv|~PqEB|RP1ZK6bV126xa82#gxO5!YJzf zNuDO?bO?TcGH^z4A@qR7BY5;p+-T^YMfecw*Hj~>k(gc8hb8HYpEVpZPv$M^yslQ(H0U~P1#@6=g)VscWZWb<4OHGS8Yj{;f?QRBJ78&NW)IU@J)mCeT`O7hN|=gDY48EhExKXyyN%H z=Q9t9Eyo`)TCxAAHf*o>zspVC;|L}w)By&J#ey6?j@mP1!&-`ZVl}i9PnCVf!*R{mA)TaD@)D?vWW@%u1p?G#bEB%c(-2@zFJ#D|u zA=s8g>n;~(xOsUj7k{c&0wCc*dLhiGZlmXx%50D}esXNlNM;CywjwiRls#RNvpTfp zp4hSi(L&-^>GufMEmCG{5Lu# z5fdXyds2}7=KH+WGXCEn*c1|%Lbh?PV0U2;-q1ERJ^JKFCkCt3oM3zY_1Wq4#-aze zgV>mv+z)0EQ715-Ea&99#xWsL#opk-@Vsrlh{`E0t0|WdWvP0a-`dX6JQnubHzh6z z*j+u_=SaOWuUD%snd~3x!QN3Xu$nl`u)7>|{A%s)uilg3*M3`e=D&C6-^MdV1_~5s zf5`IsL^$#7`mCk-1WEp{F;_{3(Tvz>D9R;@0a2N_wfI=)X+3e?K|f*sA~tS6cHgd;q59SMftGl)q~OT)U(vb`jZIc%T?N)<0mBu~*X06A85(WplWz z22I>aRun|irru(G4$n-2zh>M34mZ_$^NbS>H2<#muS zlS%{<1I{pO3RE-W2zuhc=3C!Cx@IHL{myq$E833Myc6CZIG>Kvi9{qDp9m24?{kjWk&I~#G{8M1xXfHyu0bz-Mt6Hb z3VCq;<>nXMnq?kmNeUsZeL2oES~ARb+6j2jU@?B%0VZB%*e@$o=jtmixBcWW#x=G^ zg_78jpe9P&@JG>{3l>Ovu6c^Z*`j$Q;;yTMXt7ED?rc!lh7>}0D*1Dc6oxB5-W#I% zLzb8DB+=^Cm6s+2;QSuP1(AtVJpyqVcg)n~K68{#!?|dna?rqN>u(25*8Cc~1ki$Q zghhta1-BsJ$iAB{A&GX3wCdN{AY12ixHa2Ryfo~Y2*Hj8 zaYe=z1Zlh=&6|7li$#9G)~O~4>1$?aIX|8Jbb=m=ha#XV?`wM4%BbC;S~OPOMbcHt ziO7&vB)0mD{AF#DFF}?weTMq#xm7mKy`o#2I4)pTp;N$grM56?`Q?1ksW`-_q8Ue} zKERK{s}AT*^~D%RipZR-QrmO0J{6OuqpQ+JD&^?OfOOf^@iAh@%umnElZ&}BtzkRY ze3vD;-+M^Xs164Fo0VjxvBLbj>k5yM^K!RMy`@Ks%8aD#YsjXJVwtC1`HG#G-gb{m z_1N|5Ozoqm%dN&&y{=Mk1wUELz{RIaOD)~7p0Jo=)WfQ#v3B$ynrV{3tK7b>ysKf^ zCfFIL-85%54<`yjfNQ?#w0$3zBKbV`*S-jJG>zHM$@P3fE48qOZh{V*S-UzCV}&Q@ zqhIz3?ZusaCp4mNj&<#PCL~F#iF`=sVJKg%OqWZLaqQMbAd@^Xl(!yaBW{}f-(LfY zz(Ukfw7lOH?);5i%-2;G82eRgc)J;*TU%HT%-|F9o@)@W$rzU+p)`m4yUWKx$cyA| z*&9bJj#Z;Nr-N;)50sGg=@fZCS1({{iy$Kl#PW?&|KEVeA?bg!z^-8f$Pei=J+rov mI8>TyYH2=S*tu{1y%V4r&O8LkI=uZVz$wdX$kobNg8mETg#{@9 diff --git a/docs/index.md b/docs/index.md index ed1b34b4..3a0de9a0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,7 +17,7 @@ AskOmics also support federated queries to external SPARQL endpoints. - [Build an RDF abstraction](abstraction.md): Learn how to build an RDF abstraction for RDF data - [Perform federated queries](federation.md): How to query your own data with external resources - [Use AskOmics with Galaxy](galaxy.md): How to connect AskOmics with your Galaxy history - - [Link your data to ontologies](ontologies.md): How to connect add ontologies to AskOmics, and connect your data + - [Link your data to ontologies](ontologies.md): How to add ontologies to AskOmics, and connect your own data

- Administration diff --git a/docs/manage.md b/docs/manage.md index 254c13f1..3751ff61 100644 --- a/docs/manage.md +++ b/docs/manage.md @@ -28,7 +28,7 @@ In Virtuoso, aggregating multiples graphs (using several FROM clauses) can be ve Single tenant mode send all queries on all stored graphs, thus speeding up the queries. This means that **all graphs are public, and can be queried by any user**. This affect starting points, abstractions, and query. !!! warning - Do not use *Single tenant mode* if you are storing sensitive data on AskOmics. + If you are storing sensitive data on AskOmics, make sure to disable anonymous access and account creation when using *Single tenant mode*. !!! warning *Single tenant mode* has no effect on federated queries diff --git a/docs/ontologies.md b/docs/ontologies.md index 2ead6460..fe06432b 100644 --- a/docs/ontologies.md +++ b/docs/ontologies.md @@ -10,7 +10,7 @@ This will allow users to query on an entity, or on its ancestors and descendants First, make sure to have the [abstraction file](/abstraction/#ontologies) ready. Upload it to AskOmics, and integrate it. Make sure *to set it public*. -You can then head to the Ontologies tab. There, you will be able to create and delete ontologies. +You can then head to Ontologies in the user tab. There, you will be able to create and delete ontologies. ## Creating an ontology diff --git a/docs/prefixes.md b/docs/prefixes.md index 7b2266ea..acec6b54 100644 --- a/docs/prefixes.md +++ b/docs/prefixes.md @@ -3,7 +3,7 @@ These prefixes can be used by non-admin users when integrating CSV files (for sp # Registering a prefix (admin-only) -You can then head to the Prefixes tab. There, you will be able to create and delete custom prefixes. +You can head to Prefixes in the user tab. There, you will be able to create and delete custom prefixes. ## Creating a custom prefix diff --git a/mkdocs.yml b/mkdocs.yml index 44953fd2..ae5b080b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,7 +37,7 @@ nav: - Deploy AskOmics: production-deployment.md - Configure: configure.md - Manage: manage.md - - Custo prefixes: prefixes.md + - Custom prefixes: prefixes.md - Developer guide: - Dev deployment: dev-deployment.md - Contribute: contribute.md From 921de8fd70cb3739a63110fb0009f8917204d5f2 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 30 Jun 2022 16:00:00 +0000 Subject: [PATCH 176/318] Support for multithread in web server --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 966c97bd..686933f4 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ FLASKOPTS= PYTESTOPTS= TESTFILE?=tests NTASKS?=1 +WORKERS?=1 HOST?=0.0.0.0 PORT?=5000 @@ -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 From abb18eb76dcddfe4e100d3c4066bd0a39b01c1fa Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 1 Jul 2022 10:34:42 +0200 Subject: [PATCH 177/318] Changelog & doc --- CHANGELOG.md | 1 + Makefile | 10 +++++----- askomics/static/about.html | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08563035..1c4835fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ This changelog was started for release 4.2.0. - 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 diff --git a/Makefile b/Makefile index 686933f4..12f74d0c 100644 --- a/Makefile +++ b/Makefile @@ -53,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' 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 From 1b1c22cf56ad39f95ab7199e5cfe228345329de7 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 1 Jul 2022 10:37:02 +0200 Subject: [PATCH 178/318] Timeout for ols query --- askomics/libaskomics/OntologyManager.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/askomics/libaskomics/OntologyManager.py b/askomics/libaskomics/OntologyManager.py index c988f959..c2fd543d 100644 --- a/askomics/libaskomics/OntologyManager.py +++ b/askomics/libaskomics/OntologyManager.py @@ -261,15 +261,19 @@ def autocomplete(self, ontology_uri, ontology_type, query_term, onto_short_name, "fieldList": "label" } - r = requests.get(base_url, params=arguments) + try: + r = requests.get(base_url, params=arguments, timeout=10) - data = [] + data = [] - if not r.status_code == 200: - return data + if not r.status_code == 200: + return data - res = r.json() - if res['response']['docs']: - data = [term['label'] for term in res['response']['docs']] + res = r.json() + if res['response']['docs']: + data = [term['label'] for term in res['response']['docs']] + + except requests.exceptions.Timeout: + data = [] return data From 10893fc4765f65a04070d3dbd4ce717856d4b605 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 1 Jul 2022 10:41:48 +0200 Subject: [PATCH 179/318] order --- askomics/libaskomics/OntologyManager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/askomics/libaskomics/OntologyManager.py b/askomics/libaskomics/OntologyManager.py index c2fd543d..eed16a87 100644 --- a/askomics/libaskomics/OntologyManager.py +++ b/askomics/libaskomics/OntologyManager.py @@ -261,11 +261,11 @@ def autocomplete(self, ontology_uri, ontology_type, query_term, onto_short_name, "fieldList": "label" } + data = [] + try: r = requests.get(base_url, params=arguments, timeout=10) - data = [] - if not r.status_code == 200: return data @@ -274,6 +274,6 @@ def autocomplete(self, ontology_uri, ontology_type, query_term, onto_short_name, data = [term['label'] for term in res['response']['docs']] except requests.exceptions.Timeout: - data = [] + pass return data From 4f6971dae8542ba0893937fc1b636d1a2cdb778e Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 1 Jul 2022 11:02:52 +0200 Subject: [PATCH 180/318] Update documentation & add multithread on web server (#354) --- CHANGELOG.md | 3 + Makefile | 13 +- askomics/libaskomics/OntologyManager.py | 18 +- .../react/src/routes/query/ontolinkview.jsx | 2 +- askomics/react/src/routes/query/query.jsx | 12 +- askomics/static/about.html | 2 +- config/askomics.ini.template | 3 - docs/abstraction.md | 169 +++++++++++++----- docs/configure.md | 3 +- 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/index.md | 2 + docs/manage.md | 13 ++ docs/ontologies.md | 60 +++++++ docs/prefixes.md | 11 ++ mkdocs.yml | 2 + 18 files changed, 244 insertions(+), 69 deletions(-) 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/ontologies.md create mode 100644 docs/prefixes.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 90aae6b9..1c4835fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ This changelog was started for release 4.2.0. - 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 diff --git a/Makefile b/Makefile index 966c97bd..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' @@ -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 diff --git a/askomics/libaskomics/OntologyManager.py b/askomics/libaskomics/OntologyManager.py index c988f959..eed16a87 100644 --- a/askomics/libaskomics/OntologyManager.py +++ b/askomics/libaskomics/OntologyManager.py @@ -261,15 +261,19 @@ def autocomplete(self, ontology_uri, ontology_type, query_term, onto_short_name, "fieldList": "label" } - r = requests.get(base_url, params=arguments) - data = [] - if not r.status_code == 200: - return 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']] - 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/react/src/routes/query/ontolinkview.jsx b/askomics/react/src/routes/query/ontolinkview.jsx index b50dfb27..99e5e435 100644 --- a/askomics/react/src/routes/query/ontolinkview.jsx +++ b/askomics/react/src/routes/query/ontolinkview.jsx @@ -31,7 +31,7 @@ export default class OntoLinkView extends Component { - a term +  a term
diff --git a/askomics/react/src/routes/query/query.jsx b/askomics/react/src/routes/query/query.jsx index aa0c48f6..ac4000de 100644 --- a/askomics/react/src/routes/query/query.jsx +++ b/askomics/react/src/routes/query/query.jsx @@ -559,7 +559,7 @@ export default class Query extends Component { target: targetId, selected: false, suggested: true, - directed: isOnto ? false : true, + directed: true, }) incrementSpecialNodeGroupId ? specialNodeGroupId += 1 : specialNodeGroupId = specialNodeGroupId } @@ -703,7 +703,7 @@ export default class Query extends Component { target: node1.id, selected: false, suggested: false, - directed: link.direct, + directed: link.directed, } } }) @@ -1345,10 +1345,10 @@ export default class Query extends Component { getOntoLabel (uri) { let labels = {} - labels["http://www.w3.org/2000/01/rdf-schema#subClassOf"] = "Children of" - labels["http://www.w3.org/2000/01/rdf-schema#subClassOf*"] = "Descendants of" - labels["^http://www.w3.org/2000/01/rdf-schema#subClassOf"] = "Parents of" - labels["^http://www.w3.org/2000/01/rdf-schema#subClassOf*"] = "Ancestors of" + 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] } 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/config/askomics.ini.template b/config/askomics.ini.template index d861b8fe..5e75e378 100644 --- a/config/askomics.ini.template +++ b/config/askomics.ini.template @@ -132,9 +132,6 @@ preview_limit = 25 # All queries are launched on all graphes (speedup queries) single_tenant=False -# Max results returned for autocompletion -autocomplete_max_results = 10 - [federation] # Query engine can be corese or fedx #query_engine = corese diff --git a/docs/abstraction.md b/docs/abstraction.md index ea7d8c7b..208ff7ad 100644 --- a/docs/abstraction.md +++ b/docs/abstraction.md @@ -2,6 +2,10 @@ During integration of TSV/CSV, GFF and BED files, AskOmics create RDF triples th 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 AskOmics use the following namespaces. @@ -10,11 +14,13 @@ 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: ``` @@ -50,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 @@ -70,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 . @@ -107,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 @@ -120,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 @@ -133,33 +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. To avoid overwriting information, relations are described using a blank node. The relation `:RelationExample`, linking `EntitySource` to `ÈntityTarget`, with the label *relation_example*, will be defined as follows: +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 -_:relation_node askomics:uri :RelationExample . -_:relation_node a askomics:AskomicsRelation . -_:relation_node a owl:ObjectProperty . -_:relation_node rdfs:label "relation_example" . -_:relation_node rdfs:domain :EntitySource . -_:relation_node rdfs:range :EntityTarget . +_: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 -_:relation_node dcat:endpointURL . -_:relation_node dcat:dataset . +_: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 +_:blank ns1:atLocation "https://my_remote_endpoint/sparql" . +# Optional: Set a specific graph for remote queries +_:blank dcat:Dataset . ``` diff --git a/docs/configure.md b/docs/configure.md index 8d7e552f..31132d21 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -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/img/ontology_autocomplete.png b/docs/img/ontology_autocomplete.png new file mode 100644 index 0000000000000000000000000000000000000000..c87c10787ca5aa50830d8e063d2ae468cc561c3e GIT binary patch literal 13559 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 j%=G_=6UCC1JNJy6?%TkUeFq0#)q|wO_ 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 literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index 8ac1ab6b..3a0de9a0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,12 +17,14 @@ AskOmics also support federated queries to external SPARQL endpoints. - [Build an RDF abstraction](abstraction.md): Learn how to build an RDF abstraction for RDF data - [Perform federated queries](federation.md): How to query your own data with external resources - [Use AskOmics with Galaxy](galaxy.md): How to connect AskOmics with your Galaxy history + - [Link your data to ontologies](ontologies.md): How to add ontologies to AskOmics, and connect your own data

- Administration - [Deploy an instance](production-deployment.md): Deploy an AskOmics instance on your server - [Configuration](configure.md): Configure your instance - [Manage](manage.md): Manage your instance + - [Add custom prefixes](prefixes.md): How to add custom prefixes for your users

- Developer documentation diff --git a/docs/manage.md b/docs/manage.md index a808ad41..3751ff61 100644 --- a/docs/manage.md +++ b/docs/manage.md @@ -20,6 +20,19 @@ In the latter case, you will need to run the command twice (once for each namesp - `make clear-cache` This will clear the abstraction cache, making sure your data is synchronized with the new namespaces. +# Single tenant mode + +Starting from release 4.4, the *Single tenant mode* is available through a configuration option. +In Virtuoso, aggregating multiples graphs (using several FROM clauses) can be very costly for big/numerous graphs. + +Single tenant mode send all queries on all stored graphs, thus speeding up the queries. This means that **all graphs are public, and can be queried by any user**. This affect starting points, abstractions, and query. + +!!! warning + If you are storing sensitive data on AskOmics, make sure to disable anonymous access and account creation when using *Single tenant mode*. + +!!! warning + *Single tenant mode* has no effect on federated queries + # Administrator panel Administrators have access to a specific panel in AskOmics. diff --git a/docs/ontologies.md b/docs/ontologies.md new file mode 100644 index 00000000..fe06432b --- /dev/null +++ b/docs/ontologies.md @@ -0,0 +1,60 @@ +Starting for the 4.4 release, hierarchical ontologies (such as the NCBITAXON ontology) can be integrated in AskOmics. +This will allow users to query on an entity, or on its ancestors and descendants + +# Registering an ontology (admin-only) + +!!! warning + While not required for basic queries (and subClassOf queries), registering an ontology is required for enabling auto-completion, using non-default labels (ie: *skos:prefLabel*), and enable an integration shortcut for users. + + +First, make sure to have the [abstraction file](/abstraction/#ontologies) ready. Upload it to AskOmics, and integrate it. +Make sure *to set it public*. + +You can then head to Ontologies in the user tab. There, you will be able to create and delete ontologies. + +## Creating an ontology + +Parameters to create an ontology are as follows: + +* Ontology name: the full name of the ontology: will be displayed when as a column type when integrating CSV files. +* Ontology short name: the shortname of the ontology (ex: NCBITAXON). /!\ When using ols autocompleting, this needs to match an existing ols ontology +* Ontology uri: The ontology uri in your abstraction file +* Linked public dataset: The *public* dataset containing your classes (not necessarily your abstraction) +* Label uri: The label predicated your classes are using. Default to rdfs:label +* Autocomplete type: If local, autocomplete will work with a SPARQL query (local or federated). If OLS, it will be sent on the OLS endpoint. + +# Linking your data to an ontology + +This functionality will only work with CSV files. You will need to fill out a column with the terms uris. +If the ontology has been registered, you can directly select the ontology's column type. + +![Ontology selection](img/ontology_integration.png){: .center} + +Else, you will need to set the header as you would for a relation, using the ontology uri as the remote entity. + +Ex: `is organism@http://purl.bioontology.org/ontology/NCBITAXON` + +# Querying data using ontological terms + +If your entity is linked to an ontology, the ontology will appears as a related entity on the graph view. +From there, you will be able to directly print the linked term's attributes (label, or other) + +![Ontology graph](img/ontology_graph.png){: .center} + +If the ontology was registered (and an autocompletion type was selected), the label field will have autocompletion (starting after 3 characters). + +![Ontology autocompletion](img/ontology_autocomplete.png){: .center} + +## Querying on hierarchical relations + +You can also query on a related term, to build queries such as : + +* Give me all entities related to the children of this term +* Give me all entities related any ancestor of this term + +To do so, simply click on the linked ontology circle, fill out the required label (or other attribute), and click on the link between both ontologies to select the type of query (either *children of*, *descendants of*, *parents of*, *ancestors of*) + +![Ontology search](img/ontology_link.png){: .center} + +!!! warning + The relation goes from the second ontology circle to the first. Thus, to get the *children of* a specific term, you will need to select the *children of* relation, and select the label on the **second** circle diff --git a/docs/prefixes.md b/docs/prefixes.md new file mode 100644 index 00000000..acec6b54 --- /dev/null +++ b/docs/prefixes.md @@ -0,0 +1,11 @@ +Starting for the 4.4 release, custom prefixes can be added in the administration UI. +These prefixes can be used by non-admin users when integrating CSV files (for specifying URIs, for instance) + +# Registering a prefix (admin-only) + +You can head to Prefixes in the user tab. There, you will be able to create and delete custom prefixes. + +## Creating a custom prefix + +Simply fill out the desired prefix (ex: *wikibase*), and namespace: (ex: *http://wikiba.se/ontology#*). +Users will be able to fill out data using the wikibase:XXX format. diff --git a/mkdocs.yml b/mkdocs.yml index f10f9490..ae5b080b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,10 +32,12 @@ nav: - Build RDF Abstraction: abstraction.md - Federated queries: federation.md - AskOmics and Galaxy: galaxy.md + - AskOmics and ontologies: ontologies.md - Administrator guide: - Deploy AskOmics: production-deployment.md - Configure: configure.md - Manage: manage.md + - Custom prefixes: prefixes.md - Developer guide: - Dev deployment: dev-deployment.md - Contribute: contribute.md From 2b5cd65d33294e208b26273c6fcd4d69a5dc5beb Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 1 Jul 2022 17:36:39 +0200 Subject: [PATCH 181/318] Fix anonymous access & prepare 4.4 --- CHANGELOG.md | 5 +++-- askomics/api/results.py | 4 +++- package-lock.json | 2 +- package.json | 2 +- setup.py | 6 +++--- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c4835fa..87c08296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ This changelog was started for release 4.2.0. - 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 @@ -35,8 +36,8 @@ This changelog was started for release 4.2.0. ### Changed - Changed "Query builder" to "Form editor" in form editing interface -- Changed abstraction building method for relations. (Please refer to #248 and #268) -- Changed abstraction building method for attributes. (Please refer to #321 and #324) +- 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 diff --git a/askomics/api/results.py b/askomics/api/results.py index c0092f32..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'] @@ -166,7 +168,7 @@ def get_graph_and_sparql_query(): # Get all graphs and endpoint, and mark as selected the used one 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['user']) + console_enabled = can_access(session.get('user')) except Exception as e: traceback.print_exc(file=sys.stdout) diff --git a/package-lock.json b/package-lock.json index 04f514fa..a8d78062 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "AskOmics", - "version": "4.3.1", + "version": "4.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fb733689..a22d4138 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "ontology" ], "name": "AskOmics", - "version": "4.3.1", + "version": "4.4.0", "description": "Visual SPARQL query builder", "author": "Xavier Garnier", "license": "AGPL-3.0", diff --git a/setup.py b/setup.py index 158a2a43..85cf2122 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='askomics', - version='4.3.1', + version='4.4.0', description=''' AskOmics is a visual SPARQL query interface supporting both intuitive data integration and querying while shielding the user from most of the @@ -17,8 +17,8 @@ "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], - maintainer='Xavier Garnier', - maintainer_email='xavier.garnier@irisa.fr', + maintainer='Mateo Boudet', + maintainer_email='mateo.boudet@inrae.fr', url='https://github.com/askomics/flaskomics', keyword='rdf sparql query data integration', packages=find_packages(), From dad81d526575e9732b7fae441c48cca57496c111 Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 1 Jul 2022 19:30:41 +0200 Subject: [PATCH 182/318] Fix anonymous access & prepare 4.4 (#355) --- CHANGELOG.md | 5 +++-- askomics/api/results.py | 4 +++- package-lock.json | 2 +- package.json | 2 +- setup.py | 6 +++--- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c4835fa..87c08296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ This changelog was started for release 4.2.0. - 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 @@ -35,8 +36,8 @@ This changelog was started for release 4.2.0. ### Changed - Changed "Query builder" to "Form editor" in form editing interface -- Changed abstraction building method for relations. (Please refer to #248 and #268) -- Changed abstraction building method for attributes. (Please refer to #321 and #324) +- 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 diff --git a/askomics/api/results.py b/askomics/api/results.py index c0092f32..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'] @@ -166,7 +168,7 @@ def get_graph_and_sparql_query(): # Get all graphs and endpoint, and mark as selected the used one 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['user']) + console_enabled = can_access(session.get('user')) except Exception as e: traceback.print_exc(file=sys.stdout) diff --git a/package-lock.json b/package-lock.json index 04f514fa..a8d78062 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "AskOmics", - "version": "4.3.1", + "version": "4.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fb733689..a22d4138 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "ontology" ], "name": "AskOmics", - "version": "4.3.1", + "version": "4.4.0", "description": "Visual SPARQL query builder", "author": "Xavier Garnier", "license": "AGPL-3.0", diff --git a/setup.py b/setup.py index 158a2a43..85cf2122 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='askomics', - version='4.3.1', + version='4.4.0', description=''' AskOmics is a visual SPARQL query interface supporting both intuitive data integration and querying while shielding the user from most of the @@ -17,8 +17,8 @@ "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], - maintainer='Xavier Garnier', - maintainer_email='xavier.garnier@irisa.fr', + maintainer='Mateo Boudet', + maintainer_email='mateo.boudet@inrae.fr', url='https://github.com/askomics/flaskomics', keyword='rdf sparql query data integration', packages=find_packages(), From f78bd4e2de81f6915595eb4cdf7621791010c3fa Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 1 Jul 2022 19:52:02 +0200 Subject: [PATCH 183/318] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87c08296..0766ab02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This changelog was started for release 4.2.0. -## Unreleased +## [4.4.0] - 2022-07-01 ### Fixed From 22733668ab4145f7c23178246f5d5552efe00b64 Mon Sep 17 00:00:00 2001 From: mboudet Date: Wed, 6 Jul 2022 14:36:33 +0200 Subject: [PATCH 184/318] fix requirement --- docs/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index 100e7c46..9796f78e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ mkdocs==1.2.3 markdown-captions==2 +jinja2==3.0.3 From a83b20cf4922d7fbf0f4f0911a06179d6fcbbfdc Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 7 Jul 2022 11:07:21 +0200 Subject: [PATCH 185/318] Try using blank node for category values --- askomics/libaskomics/BedFile.py | 5 +++-- askomics/libaskomics/CsvFile.py | 9 ++++++--- askomics/libaskomics/GffFile.py | 5 +++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/askomics/libaskomics/BedFile.py b/askomics/libaskomics/BedFile.py index 98802ada..ba84cefc 100644 --- a/askomics/libaskomics/BedFile.py +++ b/askomics/libaskomics/BedFile.py @@ -98,6 +98,7 @@ def set_rdf_abstraction_domain_knowledge(self): for attribute in self.attribute_abstraction: blank = BNode() + blank_category = BNode() for attr_type in attribute["type"]: self.graph_abstraction_dk.add((blank, rdflib.RDF.type, attr_type)) @@ -110,9 +111,9 @@ def set_rdf_abstraction_domain_knowledge(self): # Domain Knowledge if "values" in attribute.keys(): for value in attribute["values"]: - self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, self.namespace_data[self.format_uri("{}CategoryValue".format(attribute["label"]))])) + self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, blank_category)) self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDFS.label, rdflib.Literal(value))) - self.graph_abstraction_dk.add((self.namespace_data[self.format_uri("{}Category".format(attribute["label"]))], self.namespace_internal[self.format_uri("category")], self.namespace_data[self.format_uri(value)])) + self.graph_abstraction_dk.add((blank_category, self.namespace_internal[self.format_uri("category")], self.namespace_data[self.format_uri(value)])) if attribute["label"] == rdflib.Literal("strand"): self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, self.get_faldo_strand(value))) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index 821f00dd..b780985b 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -362,8 +362,8 @@ 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') and self.header[index] in self.category_values: - s = self.namespace_data["{}Category".format(self.format_uri(attribute, remove_space=True))] + if self.columns_type[index] in ('category', 'reference', 'strand') and self.header[index] in self.category_values and self.header[index] in self.category_blank: + s = self.category_blank[self.header[index]] p = self.namespace_internal["category"] for value in self.category_values[self.header[index]]: o = self.rdfize(value) @@ -400,6 +400,7 @@ def set_rdf_abstraction(self): for ontology in OntologyManager(self.app, self.session).list_ontologies(): available_ontologies[ontology['short_name']] = ontology['uri'] attribute_blanks = {} + self.category_blank = {} # Attributes and relations for index, attribute_name in enumerate(self.header): @@ -468,7 +469,9 @@ def set_rdf_abstraction(self): elif self.columns_type[index] in ('category', 'reference', 'strand'): attribute = self.rdfize(attribute_name) label = rdflib.Literal(attribute_name) - rdf_range = self.namespace_data["{}Category".format(self.format_uri(attribute_name, remove_space=True))] + category_blank = BNode() + self.category_blank[attribute_name] = category_blank + rdf_range = category_blank rdf_type = rdflib.OWL.ObjectProperty self.graph_abstraction_dk.add((blank, rdflib.RDF.type, self.namespace_internal["AskomicsCategory"])) diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index 7c413de7..2e746234 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -136,10 +136,11 @@ def set_rdf_abstraction_domain_knowledge(self): attribute_blanks[attribute["uri"]] = blank # Domain Knowledge if "values" in attribute.keys(): + blank_category = BNode() for value in attribute["values"]: - self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, self.namespace_data[self.format_uri("{}CategoryValue".format(attribute["label"]))])) + self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, blank_category)) self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDFS.label, rdflib.Literal(value))) - self.graph_abstraction_dk.add((self.namespace_data[self.format_uri("{}Category".format(attribute["label"]))], self.namespace_internal[self.format_uri("category")], self.namespace_data[self.format_uri(value)])) + self.graph_abstraction_dk.add((blank_category, self.namespace_internal[self.format_uri("category")], self.namespace_data[self.format_uri(value)])) if attribute["label"] == rdflib.Literal("strand"): self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, self.get_faldo_strand(value))) From 110b05075455f014023c8ac68c0503b221f48354 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 7 Jul 2022 15:54:54 +0200 Subject: [PATCH 186/318] Fix blank --- askomics/libaskomics/BedFile.py | 8 +++++--- askomics/libaskomics/GffFile.py | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/askomics/libaskomics/BedFile.py b/askomics/libaskomics/BedFile.py index ba84cefc..469cff51 100644 --- a/askomics/libaskomics/BedFile.py +++ b/askomics/libaskomics/BedFile.py @@ -98,7 +98,6 @@ def set_rdf_abstraction_domain_knowledge(self): for attribute in self.attribute_abstraction: blank = BNode() - blank_category = BNode() for attr_type in attribute["type"]: self.graph_abstraction_dk.add((blank, rdflib.RDF.type, attr_type)) @@ -110,6 +109,7 @@ def set_rdf_abstraction_domain_knowledge(self): attribute_blanks[attribute["uri"]] = blank # Domain Knowledge if "values" in attribute.keys(): + blank_category = attribute["range"] for value in attribute["values"]: self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, blank_category)) self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDFS.label, rdflib.Literal(value))) @@ -176,13 +176,14 @@ def generate_rdf_content(self): self.graph_chunk.add((entity, relation, attribute)) if "reference" not in attribute_list: + blank_category = BNode() attribute_list.append("reference") self.attribute_abstraction.append({ "uri": self.namespace_data[self.format_uri("reference")], "label": rdflib.Literal("reference"), "type": [self.namespace_internal[self.format_uri("AskomicsCategory")], rdflib.OWL.ObjectProperty], "domain": entity_type, - "range": self.namespace_data[self.format_uri("{}Category".format("reference"))], + "range": blank_category, "values": [feature.chrom] }) else: @@ -258,13 +259,14 @@ def generate_rdf_content(self): if strand: if ("strand", strand_type) not in attribute_list: + blank_category = BNode() 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"))], + "range": blank_category, "values": [strand_type] }) diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index 2e746234..b4afeb0a 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -136,7 +136,7 @@ def set_rdf_abstraction_domain_knowledge(self): attribute_blanks[attribute["uri"]] = blank # Domain Knowledge if "values" in attribute.keys(): - blank_category = BNode() + blank_category = attribute["range"] for value in attribute["values"]: self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, blank_category)) self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDFS.label, rdflib.Literal(value))) @@ -238,13 +238,14 @@ def generate_rdf_content(self): # self.graph_chunk.add((entity, relation, attribute)) if (feature.type, "reference") not in attribute_list: + blank_category = BNode() attribute_list.append((feature.type, "reference")) self.attribute_abstraction.append({ "uri": self.namespace_data[self.format_uri("reference")], "label": rdflib.Literal("reference"), "type": [self.namespace_internal[self.format_uri("AskomicsCategory")], rdflib.OWL.ObjectProperty], "domain": entity_type, - "range": self.namespace_data[self.format_uri("{}Category".format("reference"))], + "range": blank_category, "values": [rec.id] }) else: @@ -313,13 +314,14 @@ def generate_rdf_content(self): strand_type = "." if (feature.type, "strand", strand_type) not in attribute_list: + blank_category = BNode() 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"))], + "range": blank_category, "values": [strand_type] }) From 02a0545345a9240f5d0782ca4e7cb4529b2447e0 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 7 Jul 2022 17:04:30 +0200 Subject: [PATCH 187/318] doc --- docs/abstraction.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/abstraction.md b/docs/abstraction.md index 208ff7ad..d2ffb8e6 100644 --- a/docs/abstraction.md +++ b/docs/abstraction.md @@ -82,12 +82,12 @@ _: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 rdfs:range _:blank2 . _:blank askomics:uri :category_attribute_uri -:category_attributeCategory askomics:category :value_1 . -:category_attributeCategory askomics:category :value_2 . +_:blank2 askomics:category :value_1 . +_:blank2 askomics:category :value_2 . :value_1 rdf:type :category_attributeCategoryValue . :value_1 rdfs:label "value_1" . @@ -122,7 +122,7 @@ _: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 rdfs:range _:blank2. _:blank askomics:uri :reference_attribute ``` @@ -136,7 +136,7 @@ _: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 rdfs:range _:blank2. _:blank askomics:uri :strand_attribute ``` From 65cd23b7c2e23659802f1c85e7b9b0eaa33e4ceb Mon Sep 17 00:00:00 2001 From: mboudet Date: Fri, 8 Jul 2022 09:59:04 +0200 Subject: [PATCH 188/318] Fix? --- askomics/libaskomics/BedFile.py | 2 +- askomics/libaskomics/GffFile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/askomics/libaskomics/BedFile.py b/askomics/libaskomics/BedFile.py index 469cff51..4ce0982a 100644 --- a/askomics/libaskomics/BedFile.py +++ b/askomics/libaskomics/BedFile.py @@ -111,7 +111,7 @@ def set_rdf_abstraction_domain_knowledge(self): if "values" in attribute.keys(): blank_category = attribute["range"] for value in attribute["values"]: - self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, blank_category)) + self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, self.namespace_data[self.format_uri("{}CategoryValue".format(attribute["label"]))])) self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDFS.label, rdflib.Literal(value))) self.graph_abstraction_dk.add((blank_category, self.namespace_internal[self.format_uri("category")], self.namespace_data[self.format_uri(value)])) diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index b4afeb0a..825072b2 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -138,7 +138,7 @@ def set_rdf_abstraction_domain_knowledge(self): if "values" in attribute.keys(): blank_category = attribute["range"] for value in attribute["values"]: - self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, blank_category)) + self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, self.namespace_data[self.format_uri("{}CategoryValue".format(attribute["label"]))])) self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDFS.label, rdflib.Literal(value))) self.graph_abstraction_dk.add((blank_category, self.namespace_internal[self.format_uri("category")], self.namespace_data[self.format_uri(value)])) From d15426569367b9f35970de3f10d779e33e74ef0a Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 11 Jul 2022 10:36:52 +0200 Subject: [PATCH 189/318] 4.4.1 (#358) * prepare 4.4.1 --- CHANGELOG.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- setup.py | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0766ab02..6fe5cf65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ 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.1] - Unreleased + +### Changed + +- Using blank node for categories values +- Changed documentation for blank nodes + ## [4.4.0] - 2022-07-01 ### Fixed diff --git a/package-lock.json b/package-lock.json index a8d78062..58ca5f6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "AskOmics", - "version": "4.4.0", + "version": "4.4.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a22d4138..19e760c3 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "ontology" ], "name": "AskOmics", - "version": "4.4.0", + "version": "4.4.1", "description": "Visual SPARQL query builder", "author": "Xavier Garnier", "license": "AGPL-3.0", diff --git a/setup.py b/setup.py index 85cf2122..a51b9bf6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='askomics', - version='4.4.0', + version='4.4.1', description=''' AskOmics is a visual SPARQL query interface supporting both intuitive data integration and querying while shielding the user from most of the From 54c760c7b58c989e12984c0f94693736ed5e94cc Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 11 Jul 2022 16:08:29 +0200 Subject: [PATCH 190/318] Revert before blank node --- CHANGELOG.md | 9 +-------- askomics/libaskomics/BedFile.py | 9 +++------ askomics/libaskomics/CsvFile.py | 9 +++------ askomics/libaskomics/GffFile.py | 9 +++------ docs/abstraction.md | 10 +++++----- package-lock.json | 2 +- package.json | 2 +- setup.py | 2 +- 8 files changed, 18 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fe5cf65..87c08296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,14 +7,7 @@ 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.1] - Unreleased - -### Changed - -- Using blank node for categories values -- Changed documentation for blank nodes - -## [4.4.0] - 2022-07-01 +## Unreleased ### Fixed diff --git a/askomics/libaskomics/BedFile.py b/askomics/libaskomics/BedFile.py index 4ce0982a..98802ada 100644 --- a/askomics/libaskomics/BedFile.py +++ b/askomics/libaskomics/BedFile.py @@ -109,11 +109,10 @@ def set_rdf_abstraction_domain_knowledge(self): attribute_blanks[attribute["uri"]] = blank # Domain Knowledge if "values" in attribute.keys(): - blank_category = attribute["range"] for value in attribute["values"]: self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, self.namespace_data[self.format_uri("{}CategoryValue".format(attribute["label"]))])) self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDFS.label, rdflib.Literal(value))) - self.graph_abstraction_dk.add((blank_category, self.namespace_internal[self.format_uri("category")], self.namespace_data[self.format_uri(value)])) + self.graph_abstraction_dk.add((self.namespace_data[self.format_uri("{}Category".format(attribute["label"]))], self.namespace_internal[self.format_uri("category")], self.namespace_data[self.format_uri(value)])) if attribute["label"] == rdflib.Literal("strand"): self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, self.get_faldo_strand(value))) @@ -176,14 +175,13 @@ def generate_rdf_content(self): self.graph_chunk.add((entity, relation, attribute)) if "reference" not in attribute_list: - blank_category = BNode() attribute_list.append("reference") self.attribute_abstraction.append({ "uri": self.namespace_data[self.format_uri("reference")], "label": rdflib.Literal("reference"), "type": [self.namespace_internal[self.format_uri("AskomicsCategory")], rdflib.OWL.ObjectProperty], "domain": entity_type, - "range": blank_category, + "range": self.namespace_data[self.format_uri("{}Category".format("reference"))], "values": [feature.chrom] }) else: @@ -259,14 +257,13 @@ def generate_rdf_content(self): if strand: if ("strand", strand_type) not in attribute_list: - blank_category = BNode() 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": blank_category, + "range": self.namespace_data[self.format_uri("{}Category".format("strand"))], "values": [strand_type] }) diff --git a/askomics/libaskomics/CsvFile.py b/askomics/libaskomics/CsvFile.py index b780985b..821f00dd 100644 --- a/askomics/libaskomics/CsvFile.py +++ b/askomics/libaskomics/CsvFile.py @@ -362,8 +362,8 @@ 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') and self.header[index] in self.category_values and self.header[index] in self.category_blank: - s = self.category_blank[self.header[index]] + 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]]: o = self.rdfize(value) @@ -400,7 +400,6 @@ def set_rdf_abstraction(self): for ontology in OntologyManager(self.app, self.session).list_ontologies(): available_ontologies[ontology['short_name']] = ontology['uri'] attribute_blanks = {} - self.category_blank = {} # Attributes and relations for index, attribute_name in enumerate(self.header): @@ -469,9 +468,7 @@ def set_rdf_abstraction(self): elif self.columns_type[index] in ('category', 'reference', 'strand'): attribute = self.rdfize(attribute_name) label = rdflib.Literal(attribute_name) - category_blank = BNode() - self.category_blank[attribute_name] = category_blank - rdf_range = category_blank + 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((blank, rdflib.RDF.type, self.namespace_internal["AskomicsCategory"])) diff --git a/askomics/libaskomics/GffFile.py b/askomics/libaskomics/GffFile.py index 825072b2..7c413de7 100644 --- a/askomics/libaskomics/GffFile.py +++ b/askomics/libaskomics/GffFile.py @@ -136,11 +136,10 @@ def set_rdf_abstraction_domain_knowledge(self): attribute_blanks[attribute["uri"]] = blank # Domain Knowledge if "values" in attribute.keys(): - blank_category = attribute["range"] for value in attribute["values"]: self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, self.namespace_data[self.format_uri("{}CategoryValue".format(attribute["label"]))])) self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDFS.label, rdflib.Literal(value))) - self.graph_abstraction_dk.add((blank_category, self.namespace_internal[self.format_uri("category")], self.namespace_data[self.format_uri(value)])) + self.graph_abstraction_dk.add((self.namespace_data[self.format_uri("{}Category".format(attribute["label"]))], self.namespace_internal[self.format_uri("category")], self.namespace_data[self.format_uri(value)])) if attribute["label"] == rdflib.Literal("strand"): self.graph_abstraction_dk.add((self.namespace_data[self.format_uri(value)], rdflib.RDF.type, self.get_faldo_strand(value))) @@ -238,14 +237,13 @@ def generate_rdf_content(self): # self.graph_chunk.add((entity, relation, attribute)) if (feature.type, "reference") not in attribute_list: - blank_category = BNode() attribute_list.append((feature.type, "reference")) self.attribute_abstraction.append({ "uri": self.namespace_data[self.format_uri("reference")], "label": rdflib.Literal("reference"), "type": [self.namespace_internal[self.format_uri("AskomicsCategory")], rdflib.OWL.ObjectProperty], "domain": entity_type, - "range": blank_category, + "range": self.namespace_data[self.format_uri("{}Category".format("reference"))], "values": [rec.id] }) else: @@ -314,14 +312,13 @@ def generate_rdf_content(self): strand_type = "." if (feature.type, "strand", strand_type) not in attribute_list: - blank_category = BNode() 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": blank_category, + "range": self.namespace_data[self.format_uri("{}Category".format("strand"))], "values": [strand_type] }) diff --git a/docs/abstraction.md b/docs/abstraction.md index d2ffb8e6..208ff7ad 100644 --- a/docs/abstraction.md +++ b/docs/abstraction.md @@ -82,12 +82,12 @@ _:blank rdf:type owl:ObjectProperty . _:blank rdf:type askomics:AskomicsCategory . _:blank rdfs:label "category_attribute" . _:blank rdfs:domain :EntityName . -_:blank rdfs:range _:blank2 . +_:blank rdfs:range :category_attributeCategory . _:blank askomics:uri :category_attribute_uri -_:blank2 askomics:category :value_1 . -_:blank2 askomics:category :value_2 . +:category_attributeCategory askomics:category :value_1 . +:category_attributeCategory askomics:category :value_2 . :value_1 rdf:type :category_attributeCategoryValue . :value_1 rdfs:label "value_1" . @@ -122,7 +122,7 @@ _:blank rdf:type askomics:AskomicsCategory . _:blank rdf:type owl:ObjectProperty . _:blank rdfs:label "reference_attribute" . _:blank rdfs:domain :EntityName . -_:blank rdfs:range _:blank2. +_:blank rdfs:range :reference_attributeCategory. _:blank askomics:uri :reference_attribute ``` @@ -136,7 +136,7 @@ _:blank rdf:type askomics:AskomicsCategory . _:blank rdf:type owl:ObjectProperty . _:blank rdfs:label "strand_attribute" . _:blank rdfs:domain :EntityName . -_:blank rdfs:range _:blank2. +_:blank rdfs:range :strand_attributeCategory. _:blank askomics:uri :strand_attribute ``` diff --git a/package-lock.json b/package-lock.json index 58ca5f6c..a8d78062 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "AskOmics", - "version": "4.4.1", + "version": "4.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 19e760c3..a22d4138 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "ontology" ], "name": "AskOmics", - "version": "4.4.1", + "version": "4.4.0", "description": "Visual SPARQL query builder", "author": "Xavier Garnier", "license": "AGPL-3.0", diff --git a/setup.py b/setup.py index a51b9bf6..85cf2122 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='askomics', - version='4.4.1', + version='4.4.0', description=''' AskOmics is a visual SPARQL query interface supporting both intuitive data integration and querying while shielding the user from most of the From 1cedcce8d240c9ae0090c9bbe2704db597b1361d Mon Sep 17 00:00:00 2001 From: mboudet Date: Mon, 11 Jul 2022 16:15:11 +0200 Subject: [PATCH 191/318] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87c08296..0766ab02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This changelog was started for release 4.2.0. -## Unreleased +## [4.4.0] - 2022-07-01 ### Fixed From b8079ea045706017d166295f4d04e7f9d37a16e5 Mon Sep 17 00:00:00 2001 From: mboudet Date: Thu, 18 Aug 2022 17:28:09 +0200 Subject: [PATCH 192/318] Update configure.md --- docs/configure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configure.md b/docs/configure.md index 31132d21..ec17a616 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -52,6 +52,7 @@ All AskOmics configuration is set in `config/askomics.ini` files. When AskOmics - `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 + - `single_tenant` (bool): Enable [single tenant mode](/manage/#single-tenant-mode) - `virtuoso` @@ -73,7 +74,6 @@ 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` From 0f315abcab367dc73203505b176f96959e955f22 Mon Sep 17 00:00:00 2001 From: mboudet Date: Sat, 1 Oct 2022 14:33:11 +0200 Subject: [PATCH 193/318] Fix 360 (Contact page and front message) (#363) --- CHANGELOG.md | 12 ++++++++++++ askomics/api/file.py | 2 +- askomics/api/results.py | 2 +- askomics/api/start.py | 7 +++++++ askomics/libaskomics/FilesHandler.py | 2 +- askomics/libaskomics/Utils.py | 2 +- askomics/react/src/contact.jsx | 28 +++++++++++++++++++++++++++ askomics/react/src/navbar.jsx | 11 +++++++++++ askomics/react/src/routes.jsx | 9 +++++++++ askomics/react/src/routes/ask/ask.jsx | 18 +++++++---------- config/askomics.ini.template | 2 ++ config/askomics.test.ini | 1 + docs/configure.md | 2 ++ package-lock.json | 2 +- package.json | 2 +- setup.py | 2 +- tests/test_api.py | 7 +++++++ 17 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 askomics/react/src/contact.jsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 0766ab02..08100431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This changelog was started for release 4.2.0. +## [4.5.0] - Unreleased + +### Fixed + +- Fixed new linting + +### Changed + +- Added contact_message config option, displayed in a new 'Contact' page +- Added front_message config option, diplayed on the front page + + ## [4.4.0] - 2022-07-01 ### Fixed diff --git a/askomics/api/file.py b/askomics/api/file.py index 5d224030..6a92ebb3 100644 --- a/askomics/api/file.py +++ b/askomics/api/file.py @@ -397,7 +397,7 @@ def serve_file(path, user_id, username): username ) - return(send_from_directory(dir_path, path)) + return send_from_directory(dir_path, path) @file_bp.route('/api/files/columns', methods=['GET']) diff --git a/askomics/api/results.py b/askomics/api/results.py index d87f9e9e..0985a731 100644 --- a/askomics/api/results.py +++ b/askomics/api/results.py @@ -279,7 +279,7 @@ def download_result(): 'errorMessage': str(e) }), 500 - return(send_from_directory(dir_path, file_name)) + return send_from_directory(dir_path, file_name) @results_bp.route('/api/results/delete', methods=['POST']) diff --git a/askomics/api/start.py b/askomics/api/start.py index 910af044..372a9e93 100644 --- a/askomics/api/start.py +++ b/askomics/api/start.py @@ -45,6 +45,12 @@ def start(): except Exception: pass + contact_message = None + try: + contact_message = current_app.iniconfig.get('askomics', 'contact_message') + except Exception: + pass + # get proxy path proxy_path = "/" try: @@ -71,6 +77,7 @@ def start(): config = { "footerMessage": current_app.iniconfig.get('askomics', 'footer_message'), "frontMessage": front_message, + "contactMessage": contact_message, "version": get_distribution('askomics').version, "commit": sha, "gitUrl": current_app.iniconfig.get('askomics', 'github'), diff --git a/askomics/libaskomics/FilesHandler.py b/askomics/libaskomics/FilesHandler.py index 04254028..4bacfd17 100644 --- a/askomics/libaskomics/FilesHandler.py +++ b/askomics/libaskomics/FilesHandler.py @@ -356,7 +356,7 @@ def persist_chunk(self, chunk_info): self.delete_file_from_fs(file_path) except Exception: pass - raise(e) + raise e def download_url(self, url, task_id): """Download a file from an URL and insert info in database diff --git a/askomics/libaskomics/Utils.py b/askomics/libaskomics/Utils.py index 57eb14ee..bf0f6014 100644 --- a/askomics/libaskomics/Utils.py +++ b/askomics/libaskomics/Utils.py @@ -33,7 +33,7 @@ def redo_if_failure(logger, max_redo, sleep_time, call, *args): break except Exception as e: if i == max_redo: - raise(e) + raise e traceback.print_exc(file=sys.stdout) logger.debug("Fail to execute {}. Retrying in {} sec...".format(call.__name__, sleep_time)) time.sleep(sleep_time) diff --git a/askomics/react/src/contact.jsx b/askomics/react/src/contact.jsx new file mode 100644 index 00000000..2b214808 --- /dev/null +++ b/askomics/react/src/contact.jsx @@ -0,0 +1,28 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' +import { Collapse, Navbar, NavbarBrand, Nav, NavItem } from 'reactstrap' +import PropTypes from 'prop-types' +import Template from './components/template' + +export default class Contact extends Component { + constructor (props) { + super(props) + console.log("test") + } + + render () { + return ( +
+

Contact

+
+
+