From a5300b8216a9d93f0b7269bc9d097c1b7fe9ad6f Mon Sep 17 00:00:00 2001 From: Anita Caron Date: Sun, 10 Nov 2024 18:19:14 -0300 Subject: [PATCH 1/3] add fp_020.py script to makefile to be updated automatically from OBO-Dashboard --- principles/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/principles/Makefile b/principles/Makefile index 05391bb10..3dcdcd158 100644 --- a/principles/Makefile +++ b/principles/Makefile @@ -1,7 +1,7 @@ # Collect the list of query files for report. SCRIPTS := build/fp_001.py build/fp_002.py build/fp_003.py build/fp_004.py \ build/fp_005.py build/fp_006.py build/fp_007.py build/fp_008.py build/fp_009.py \ -build/fp_011.py build/fp_012.py build/fp_016.py +build/fp_011.py build/fp_012.py build/fp_016.py build/fp_020.py DOCS := $(foreach x, $(SCRIPTS), checks/$(notdir $(basename $(x))).md) # FP ID to the principle name From 3290f5917d6d3e4ea42605438a9f7f0c7dfd4650 Mon Sep 17 00:00:00 2001 From: Anita Caron Date: Mon, 25 Nov 2024 18:37:06 -0300 Subject: [PATCH 2/3] add fp_20 to be updated automatically; fix bug to have ID --- principles/Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/principles/Makefile b/principles/Makefile index 3dcdcd158..86f0ad9be 100644 --- a/principles/Makefile +++ b/principles/Makefile @@ -17,6 +17,7 @@ DOCS := $(foreach x, $(SCRIPTS), checks/$(notdir $(basename $(x))).md) 11 = 'Locus of Authority' 12 = 'Naming Conventions' 16 = Maintenance +20 = Responsiveness # Generate all check docs all: clean @@ -33,7 +34,7 @@ build/fp_%.py: | build # Build the MD page from the check script checks/fp_%.md: build/fp_%.py | $(SCRIPTS) checks - $(eval ID := $(subst 0,,$(subst fp_,,$(notdir $(basename $@))))) + $(eval ID := $(shell echo $(subst fp_,,$(notdir $(basename $@))) | sed 's/^0*//')) if [ $(ID) == 1 ]; then export TITLE=$(1); \ elif [ $(ID) == 2 ]; then export TITLE=$(2); \ elif [ $(ID) == 3 ]; then export TITLE=$(3); \ @@ -46,6 +47,7 @@ checks/fp_%.md: build/fp_%.py | $(SCRIPTS) checks elif [ $(ID) == 11 ]; then export TITLE=$(11); \ elif [ $(ID) == 12 ]; then export TITLE=$(12); \ elif [ $(ID) == 16 ]; then export TITLE=$(16); \ + elif [ $(ID) == 20 ]; then export TITLE=$(20); \ fi; \ echo "---\nlayout: check\nid: $(ID)\ntitle: $$TITLE Automated Check\n---\n" > $@ tail -n+3 $< \ From ff854d6a3ea32bc427232388146a1a04d287d7c5 Mon Sep 17 00:00:00 2001 From: Anita Caron Date: Mon, 25 Nov 2024 18:38:01 -0300 Subject: [PATCH 3/3] update checks page auto --- principles/checks/fp_001.md | 45 ++++++------ principles/checks/fp_002.md | 29 ++------ principles/checks/fp_003.md | 64 +++++++++++------ principles/checks/fp_004.md | 140 +++++++++++++++++++++++++++++------- principles/checks/fp_006.md | 61 +++++----------- principles/checks/fp_007.md | 73 ++++++++++++------- principles/checks/fp_008.md | 15 ++-- principles/checks/fp_009.md | 30 ++++++-- principles/checks/fp_011.md | 9 +-- principles/checks/fp_016.md | 8 +-- principles/checks/fp_020.md | 28 ++++---- 11 files changed, 301 insertions(+), 201 deletions(-) diff --git a/principles/checks/fp_001.md b/principles/checks/fp_001.md index 7dea596af..27bc92ae1 100644 --- a/principles/checks/fp_001.md +++ b/principles/checks/fp_001.md @@ -1,6 +1,6 @@ --- layout: check -id: fp_001 +id: 1 title: Open Automated Check --- @@ -39,11 +39,11 @@ See [Open Implementation](http://obofoundry.org/principles/fp-001-open.html#impl The registry data entry is validated with JSON schema using the [license schema](https://raw.githubusercontent.com/OBOFoundry/OBOFoundry.github.io/master/util/schema/license.json). The license schema ensures that a license entry is present and that the entry has a `url` and `label`. The license schema also checks that the license is one of the CC0 or CC-BY licenses. OWL API is then used to check the ontology as an `OWLOntology` object. Annotations on the ontology are retrieved and the `dcterms:license` property is found. The python script ensures that the correct `dcterms:license` property is used. The script compares this license to the registry license to ensure that they are the same. ```python -import jsonschema import dash_utils +import jsonschema -def is_open(ontology, data): +def is_open(ontology, data, schema): """Check FP 1 - Open. This method checks the following: @@ -63,7 +63,7 @@ def is_open(ontology, data): ERROR, WARN, INFO, or PASS string with optional message. """ - v = OpenValidator(ontology, data) + v = OpenValidator(ontology, data, schema) loadable = False if ontology: @@ -91,7 +91,7 @@ class OpenValidator(): license (None if missing) """ - def __init__(self, ontology, data): + def __init__(self, ontology, data, schema): """Instantiate an OpenValidator. Args: @@ -105,7 +105,7 @@ class OpenValidator(): self.is_open = None if self.registry_license is not None: - self.is_open = check_registry_license(data) + self.is_open = check_registry_license(data, schema) self.ontology_license = None self.correct_property = None @@ -134,18 +134,18 @@ class OpenValidator(): annotations = ontology.getAnnotations() license = dash_utils.get_ontology_annotation_value(annotations, license_prop) - bad_license = dash_utils.get_ontology_annotation_value( - annotations, bad_license_prop) + + bad_licenses = list(filter(None, [dash_utils.get_ontology_annotation_value(annotations, prop) for prop in bad_license_props])) if license: self.ontology_license = license self.correct_property = True - elif bad_license: - self.ontology_license = bad_license + elif len(bad_licenses) > 0: + self.ontology_license = bad_licenses[0] self.correct_property = False -def big_is_open(file, data): +def big_is_open(file, data, schema): """Check FP 1 - Open. This method checks the following: @@ -165,7 +165,7 @@ def big_is_open(file, data): ERROR, WARN, INFO, or PASS string with optional message. """ - v = BigOpenValidator(file, data) + v = BigOpenValidator(file, data, schema) return process_results(v.registry_license, v.ontology_license, v.is_open, @@ -188,7 +188,7 @@ class BigOpenValidator(): license (None if missing) """ - def __init__(self, file, data): + def __init__(self, file, data, schema): """Instantiate a BigOpenValidator. Args: @@ -202,7 +202,7 @@ class BigOpenValidator(): self.is_open = None if self.registry_license is not None: - self.is_open = check_registry_license(data) + self.is_open = check_registry_license(data, schema) self.ontology_license = None self.correct_property = None @@ -276,7 +276,7 @@ class BigOpenValidator(): # ---------- UTILITY METHODS ---------- # -def check_registry_license(data): +def check_registry_license(data, schema): """Use the JSON license schema to validate the registry data. This ensures that the license is present and one of the CC0 or CC-BY @@ -290,7 +290,7 @@ def check_registry_license(data): """ try: - jsonschema.validate(data, license_schema) + jsonschema.validate(data, schema) return True except jsonschema.exceptions.ValidationError as ve: return False @@ -304,7 +304,7 @@ def compare_licenses(registry_license, ontology_license): ontology_license (str): license URL from the ontology Return: - True if registry license matches ontology licences; + True if registry license matches ontology license; False if the licenses do not match; None if one or both licenses are missing. """ @@ -380,7 +380,7 @@ def process_results(registry_license, level = 'ERROR' issues.append(missing_ontology_license) - # matches_ontology = None if missing ontology licenese + # matches_ontology = None if missing ontology license if matches_ontology is False: level = 'ERROR' issues.append(no_match.format(ontology_license, registry_license)) @@ -395,11 +395,8 @@ def process_results(registry_license, return {'status': level, 'comment': ' '.join(issues)} -# correct dc license property namespace +# correct dc license property license_prop = 'http://purl.org/dc/terms/license' -# incorrect dc license property namespace -bad_license_prop = 'http://purl.org/dc/elements/1.1/license' - -# license JSON schema for registry validation -license_schema = dash_utils.load_schema('dependencies/license.json') +# incorrect dc license properties +bad_license_props = ['http://purl.org/dc/elements/1.1/license', 'http://purl.org/dc/elements/1.1/rights', 'http://purl.org/dc/terms/rights'] ``` diff --git a/principles/checks/fp_002.md b/principles/checks/fp_002.md index 0b7290d39..8c739f465 100644 --- a/principles/checks/fp_002.md +++ b/principles/checks/fp_002.md @@ -25,36 +25,19 @@ import dash_utils from dash_utils import format_msg -def is_common_format(ontology): +def is_common_format(syntax): """Check FP 2 - Common Format. Args: - ontology (OWLOntology): ontology object + syntax (str): the syntax as determined by ROBOT metrics Return: PASS if OWLOntology is not None, ERROR otherwise. """ - if ontology is None: - return {'status': 'ERROR', 'comment': 'Unable to load ontology'} - else: + if syntax is None: + return {'status': 'ERROR', 'comment': 'Unknown format'} + elif syntax == "RDF/XML Syntax": return {'status': 'PASS'} - - -def big_is_common_format(good_format): - """Check FP 2 - Common Format on large ontologies - - Args: - good_format (bool): True if ontology could be parsed by Jena - - Return: - PASS if good_format, ERROR otherwise. - """ - if good_format is None: - return {'status': 'ERROR', - 'comment': 'Unable to load ontology (may be too large)'} - elif good_format is False: - return {'status': 'ERROR', - 'comment': 'Unable to parse ontology'} else: - return {'status': 'PASS'} + return {'status': 'WARN', 'comment': f'OWL syntax ({syntax}), but should be RDF/XML'} ``` diff --git a/principles/checks/fp_003.md b/principles/checks/fp_003.md index a3980d512..9e16b6b09 100644 --- a/principles/checks/fp_003.md +++ b/principles/checks/fp_003.md @@ -33,20 +33,19 @@ The full OBO Foundry ID Policy can be found [here](http://www.obofoundry.org/id- All entity IRIs are retrieved from the ontology, excluding annotation properties. Annotation properties may use hashtags and words due to legacy OBO conversions for subset properties. All other IRIs are checked if they are in the ontology's namespace. If the IRI begins with the ontology namespace, the next character must be an underscore. If not, this is an error. The IRI is also compared to a regex pattern to check if the local ID after the underscore is numeric. If not, this is a warning. ```python -import dash_utils import os import re -from dash_utils import format_msg +import dash_utils iri_pattern = r'http:\/\/purl\.obolibrary\.org\/obo\/%s_[0-9]{1,9}' owl_deprecated = 'http://www.w3.org/2002/07/owl#deprecated' -error_msg = '{0} invalid IRIs' +error_msg = '{} invalid IRIs. The Ontology IRI is {}valid.' warn_msg = '{0} warnings on IRIs' -def has_valid_uris(robot_gateway, namespace, ontology): +def has_valid_uris(robot_gateway, namespace, ontology, ontology_dir): """Check FP 3 - URIs. This check ensures that all ontology entities follow NS_LOCALID. @@ -67,6 +66,7 @@ def has_valid_uris(robot_gateway, namespace, ontology): otherwise. """ if not ontology: + dash_utils.write_empty(os.path.join(ontology_dir, 'fp3.tsv'), ["Status", "Issue"]) return {'status': 'ERROR', 'comment': 'Unable to load ontology'} entities = robot_gateway.OntologyHelper.getEntities(ontology) @@ -95,10 +95,14 @@ def has_valid_uris(robot_gateway, namespace, ontology): elif check == 'WARN': warn.append(iri) - return save_invalid_uris(namespace, error, warn) + ontology_iri = dash_utils.get_ontology_iri(ontology) + + valid_iri = is_valid_ontology_iri(ontology_iri, namespace) + return save_invalid_uris(error, warn, ontology_dir, valid_iri) -def big_has_valid_uris(namespace, file): + +def big_has_valid_uris(namespace, file, ontology_dir): """Check FP 3 - URIs on a big ontology. This check ensures that all ontology entities follow NS_LOCALID. @@ -112,6 +116,7 @@ def big_has_valid_uris(namespace, file): Args: namespace (str): ontology ID file (str): path to ontology file + ontology_dir (str): Return: INFO if ontology IRIs cannot be parsed. ERROR if any errors, WARN if @@ -134,6 +139,7 @@ def big_has_valid_uris(namespace, file): if 'Ontology' and 'about' in line: if not owl and not rdf: # did not find OWL and RDF - end now + dash_utils.write_empty(os.path.join(ontology_dir, 'fp3.tsv'), ["Status", "Issue"]) return {'status': 'ERROR', 'comment': 'Unable to parse ontology'} @@ -171,11 +177,20 @@ def big_has_valid_uris(namespace, file): if not valid: # not valid ontology + dash_utils.write_empty(os.path.join(ontology_dir, 'fp3.tsv'), ["Status", "Issue"]) return {'status': 'ERROR', 'comment': 'Unable to parse ontology'} - return save_invalid_uris(namespace, error, warn) + return save_invalid_uris(error, warn, ontology_dir) + +def is_valid_ontology_iri(iri, namespace): + if iri: + if iri == 'http://purl.obolibrary.org/obo/{0}.owl'.format(namespace): + return True + if iri == 'http://purl.obolibrary.org/obo/{0}/{0}-base.owl'.format(namespace): + return True + return False def check_uri(namespace, iri): """Check if a given IRI is valid. @@ -193,7 +208,7 @@ def check_uri(namespace, iri): return True if iri.startswith(namespace): # all NS IRIs must follow NS_ - if not iri.startwith(namespace + '_'): + if not iri.startswith(namespace + '_'): return 'ERROR' # it is recommended to follow NS_NUMID elif not re.match(pattern, iri, re.IGNORECASE): @@ -201,35 +216,44 @@ def check_uri(namespace, iri): return True -def save_invalid_uris(ns, error, warn): +def save_invalid_uris(error, warn, ontology_dir, valid_ontology_iri = True): """Save invalid (error or warning) IRIs to a report file - (reports/dashboard/*/fp3.tsv). Args: - ns (str): ontology ID error (list): list of ERROR IRIs warn (list): list of WARN IRIs + ontology_dir (str): Return: ERROR or WARN with detailed message, or PASS if no errors or warnings. """ - if len(error) > 0 or len(warn) > 0: - file = 'build/dashboard/{0}/fp3.tsv'.format(ns) - with open(file, 'w+') as f: - for e in error: - f.write('ERROR\t{0}\n'.format(e)) - for w in warn: - f.write('WARN\t{0}\n'.format(w)) + # write a report (maybe empty) + file = os.path.join(ontology_dir, 'fp3.tsv') + + with open(file, 'w+') as f: + f.write('Status\tIssue\n') + for e in error: + f.write('ERROR\t{0}\n'.format(e)) + for w in warn: + f.write('WARN\t{0}\n'.format(w)) + + o_iri_msg="" + if not valid_ontology_iri: + o_iri_msg = "not " if len(error) > 0 and len(warn) > 0: return {'status': 'ERROR', 'file': 'fp3', - 'comment': ' '.join([error_msg.format(len(error)), + 'comment': ' '.join([error_msg.format(len(error), o_iri_msg), warn_msg.format(len(warn))])} elif len(error) > 0: return {'status': 'ERROR', 'file': 'fp3', - 'comment': error_msg.format(len(error))} + 'comment': error_msg.format(len(error), o_iri_msg)} + elif not valid_ontology_iri: + return {'status': 'ERROR', + 'file': 'fp3', + 'comment': error_msg.format(0, o_iri_msg)} elif len(warn) > 0: return {'status': 'ERROR', 'file': 'fp3', diff --git a/principles/checks/fp_004.md b/principles/checks/fp_004.md index 65a3e53a1..22d4ba281 100644 --- a/principles/checks/fp_004.md +++ b/principles/checks/fp_004.md @@ -32,13 +32,21 @@ Please be aware that the [Ontology Development Kit](https://github.com/INCATools The version IRI is retrieved from the ontology using OWL API. For very large ontologies, the RDF/XML ontology header is parsed to find the owl:versionIRI declaration. If found, the IRI is compared to a regex pattern to determine if it is in date format. If it is not in date format, a warning is issued. If the version IRI is not present, this is an error. ```python -import dash_utils import re +from typing import Optional +from urllib.parse import urlparse -from dash_utils import format_msg +import dash_utils +from lib import url_exists -# regex pattern to match dated version IRIs -pat = r'http:\/\/purl\.obolibrary\.org/obo/.*/([0-9]{4}-[0-9]{2}-[0-9]{2})/.*' +# regex pattern to match purl obolibrary url +pat = r'http:\/\/purl\.obolibrary\.org/obo/.*/.*/.*' +PATTERN = re.compile(pat) +#: Official regex for semantic versions from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +# Simplify semantic versions to accept only numbers with or without point, .eg 1.1 or 11 +SEMVER_PATTERN = re.compile(r"^(0|[1-9]\d*)(\.)?(0|[1-9]\d*)?((\.)?(0|[1-9]\d*))?$") +#: Regular expression for ISO 8601 compliant date in YYYY-MM-DD format +DATE_PATTERN = re.compile(r"^([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])$") # descriptions of issues bad_format = 'Version IRI \'{0}\' is not in recommended format' @@ -63,18 +71,22 @@ def has_versioning(ontology): # retrieve version IRI or None from ontology version_iri = dash_utils.get_version_iri(ontology) - if version_iri: - # compare version IRI to the regex pattern - search = re.search(pat, version_iri) - if search: - return {'status': 'PASS'} - else: - return {'status': 'WARN', - 'comment': bad_format.format(version_iri)} - else: + if not version_iri: return {'status': 'ERROR', 'comment': missing_version} - # TODO: check that versionIRI resolves + if not url_exists(version_iri): + return {"status": "ERROR", "comment": "Version IRI does not resolve"} + + iri_version_error_message = get_iri_version_error_message(version_iri) + if iri_version_error_message is not None: + return {"status": "ERROR", "comment": iri_version_error_message} + + # compare version IRI to the regex pattern + if not PATTERN.search(version_iri): + return {'status': 'WARN', + 'comment': bad_format.format(version_iri)} + + return {'status': 'PASS'} def big_has_versioning(file): @@ -93,17 +105,93 @@ def big_has_versioning(file): # may return empty string if version IRI is missing # or None if ontology cannot be parsed version_iri = dash_utils.get_big_version_iri(file) - - if version_iri and version_iri != '': - # compare version IRI to the regex pattern - search = re.search(pat, version_iri) - if search: - return {'status': 'PASS'} - else: - return {'status': 'WARN', - 'comment': bad_format.format(version_iri)} - elif version_iri == '': - return {'status': 'ERROR', 'comment': missing_version} - else: + if version_iri is None: return {'status': 'ERROR', 'comment': 'Unable to parse ontology'} + if version_iri == "": + return {'status': 'ERROR', 'comment': missing_version} + if not url_exists(version_iri): + return {"status": "ERROR", "comment": "Version IRI does not resolve"} + # compare version IRI to the regex pattern + if not PATTERN.search(version_iri): + return {'status': 'WARN', + 'comment': bad_format.format(version_iri)} + + return {'status': 'PASS'} + +def contains_semver(iri: str) -> bool: + """Return if the IRI contains a semantic version substring. + + >>> contains_semver("https://example.org/1.0.0/ontology.owl") + True + >>> contains_semver("https://example.org/1.0/ontology.owl") + True + >>> contains_semver("https://example.org/2022-01-01/1.0.0/ontology.owl") + True + >>> contains_semver("https://example.org/ontology.owl") + False + >>> contains_semver("https://example.org/2022-01-01/ontology.owl") + False + >>> contains_semver("http://purl.obolibrary.org/obo/chebi/223/chebi.owl") + True + >>> contains_semver("http://purl.obolibrary.org/obo/pr/68.0/pr.owl") + True + """ + return _match_any_part(iri, SEMVER_PATTERN) + + +def contains_date(iri: str) -> bool: + """Return if the IRI contains a date substring. + + >>> contains_date("https://example.org/1.0.0/ontology.owl") + False + >>> contains_date("https://example.org/1.0/ontology.owl") + False + >>> contains_date("https://example.org/2022-01-01/1.0.0/ontology.owl") + True + >>> contains_date("https://example.org/ontology.owl") + False + >>> contains_date("https://example.org/2022-01-01/ontology.owl") + True + >>> contains_date("http://purl.obolibrary.org/obo/chebi/223/chebi.owl") + False + >>> contains_date("http://purl.obolibrary.org/obo/pr/68.0/pr.owl") + False + """ + return _match_any_part(iri, DATE_PATTERN) + + +def _match_any_part(iri, pattern): + parse_result = urlparse(iri) + return any( + bool(pattern.match(part)) + for part in parse_result.path.split("/") + ) + + +def get_iri_version_error_message(version_iri: str) -> Optional[str]: + """Check a version IRI has exactly one of a semantic version or ISO 8601 date (YYYY-MM-DD) in it. + + >>> get_iri_version_error_message("https://example.org/2022-01-01/ontology.owl") + None + >>> get_iri_version_error_message("https://example.org/1.0.0/ontology.owl") + None + >>> get_iri_version_error_message("https://example.org/1.0/ontology.owl") + None + >>> get_iri_version_error_message("http://purl.obolibrary.org/obo/chebi/223/chebi.owl") + None + >>> get_iri_version_error_message("http://purl.obolibrary.org/obo/pr/68.0/pr.owl") + None + >>> get_iri_version_error_message("https://obofoundry.org") + 'Version IRI has neither a semantic version nor a date' + >>> get_iri_version_error_message("https://example.org/2022-01-01/1.0.0/ontology.owl") + 'Version IRI should not contain both a semantic version and date' + """ + matches_semver = contains_semver(version_iri) + matches_date = contains_date(version_iri) + if matches_date and matches_semver: + return "Version IRI should not contain both a semantic version and date" + if not matches_date and not matches_semver: + return "Version IRI has neither a semantic version nor a date" + # None means it's all gucci + return None ``` diff --git a/principles/checks/fp_006.md b/principles/checks/fp_006.md index a94871f88..4a4c79966 100644 --- a/principles/checks/fp_006.md +++ b/principles/checks/fp_006.md @@ -28,16 +28,23 @@ If a term has more than one defintion, combine the two definitions. Alternativel Add an [`IAO:0000115` (definition)](http://purl.obolibrary.org/obo/IAO_0000115) annotation to each term that is missing a definition. For help writing good definitions, see [Textual Definitions Recommendations](http://obofoundry.org/principles/fp-006-textual-definitions.html#recommendation). -For adding defintions in bulk, check out [ROBOT template](http://robot.obolibrary.org/template). +For adding definitions in bulk, check out [ROBOT template](http://robot.obolibrary.org/template). ### Implementation -[ROBOT report](http://robot.obolibrary.org/report) is run over the ontology. A count of violations for each of the following checks is retrieved from the report object: [duplicate definition](http://robot.obolibrary.org/report_queries/duplicate_definition), [multiple definitions](http://robot.obolibrary.org/report_queries/multiple_definitions), and [missing definition](http://robot.obolibrary.org/report_queries/missing_definition). If there are any duplicate or multiple defintions, it is an error. If there are missing definitions, it is a warning. +[ROBOT report](http://robot.obolibrary.org/report) is run over the ontology. A count of violations for each of the following checks is retrieved from the report object: [duplicate definition](http://robot.obolibrary.org/report_queries/duplicate_definition), [multiple definitions](http://robot.obolibrary.org/report_queries/multiple_definitions), and [missing definition](http://robot.obolibrary.org/report_queries/missing_definition). If there are any duplicate or multiple definitions, it is an error. If there are missing definitions, it is a warning. +Note: Even a single duplicate or multiple definition will result in an error status, while missing definitions will only trigger a warning status regardless of count. ```python import dash_utils from dash_utils import format_msg +# violation messages +DUPLICATE_MSG = '{0} duplicate definitions.' +MULTIPLE_MSG = '{0} multiple definitions.' +MISSING_MSG = '{0} missing definitions.' +HELP_MSG = 'See ROBOT Report for details.' + def has_valid_definitions(report): """Check fp 6 - textual definitions. @@ -60,47 +67,17 @@ def has_valid_definitions(report): # warn level violation missing = report.getViolationCount('missing_definition') - if duplicates > 0 and multiples > 0 and missing > 0: - return {'status': 'ERROR', - 'comment': ' '.join([duplicate_msg.format(duplicates), - multiple_msg.format(multiples), - missing_msg.format(missing), - help_msg])} - elif duplicates > 0 and multiples > 0: - return {'status': 'ERROR', - 'comment': ' '.join([duplicate_msg.format(duplicates), - multiple_msg.format(multiples), - help_msg])} - elif duplicates > 0 and missing > 0: - return {'status': 'ERROR', - 'comment': ' '.join([duplicate_msg.format(duplicates), - missing_msg.format(missing), - help_msg])} - elif multiples > 0 and missing > 0: - return {'status': 'ERROR', - 'comment': ' '.join([multiple_msg.format(multiples), - missing_msg.format(missing), - help_msg])} - elif duplicates > 0: - return {'status': 'ERROR', - 'comment': ' '.join([duplicate_msg.format(duplicates), - help_msg])} - elif multiples > 0: - return {'status': 'ERROR', - 'comment': ' '.join([multiple_msg.format(missing), - help_msg])} - elif missing > 0: - return {'status': 'WARN', - 'comment': ' '.join([missing_msg.format(missing), - help_msg])} - else: - # no violations found + if not duplicates > 0 and not multiples > 0 and not missing > 0: return {'status': 'PASS'} + if not missing == 0 and not duplicates > 0 and not multiples > 0: + return {'status': 'WARN', + 'comment': ' '.join([MISSING_MSG.format(missing), HELP_MSG])} + comment = [f'{DUPLICATE_MSG.format(duplicates) if duplicates > 0 else ""}', + f'{MULTIPLE_MSG.format(multiples) if multiples > 0 else ""}', + f'{MISSING_MSG.format(missing) if missing > 0 else ""}', + HELP_MSG] -# violation messages -duplicate_msg = '{0} duplicate definitions.' -multiple_msg = '{0} multiple definitions.' -missing_msg = '{0} missing definitions.' -help_msg = 'See ROBOT Report for details.' + return {'status': 'ERROR', + 'comment': ' '.join(comment).strip()} ``` diff --git a/principles/checks/fp_007.md b/principles/checks/fp_007.md index 7b512d565..a704285cd 100644 --- a/principles/checks/fp_007.md +++ b/principles/checks/fp_007.md @@ -1,6 +1,6 @@ --- layout: check -id: fp_007 +id: 7 title: Relations Automated Check --- @@ -30,11 +30,12 @@ Review your non-RO properties to see if any can be replaced with an RO property The object and data properties from the ontology are compared to existing RO properties. If any labels match existing RO properties, but do not use the correct RO IRI, this is an error. Any non-RO properties (no label match and do not use an RO IRI) will be listed as INFO messages. ```python +import csv import os import unicodedata +from io import TextIOWrapper import dash_utils -from dash_utils import format_msg owl_deprecated = 'http://www.w3.org/2002/07/owl#deprecated' @@ -43,7 +44,7 @@ ro_match = '{0} labels match RO labels.' non_ro = '{0} non-RO properties used.' -def has_valid_relations(namespace, ontology, ro_props): +def has_valid_relations(namespace, ontology, ro_props, ontology_dir): """Check fp 7 - relations. Retrieve all non-obsolete properties from the ontology. Compare their @@ -57,28 +58,49 @@ def has_valid_relations(namespace, ontology, ro_props): namespace (str): ontology ID ontology (OWLOntology): ontology object ro_props (dict): map of RO property label to IRI + ontology_dir (str): Return: PASS or violation level with optional help message """ if ontology is None: + dash_utils.write_empty(os.path.join(ontology_dir, 'fp7.tsv'), ["IRI", "Label", "Issue"]) return {'status': 'ERROR', 'comment': 'Unable to load ontology'} # ignore RO if namespace == 'ro': + dash_utils.write_empty(os.path.join(ontology_dir, 'fp7.tsv'), ["IRI", "Label", "Issue"]) return {'status': 'PASS'} - props = get_properties(namespace, ontology) + props = get_properties(ontology) # get results (PASS, INFO, or ERROR) - return check_properties(namespace, props, ro_props) + return check_properties(props, ro_props, ontology_dir) -def get_properties(namespace, ontology): +def get_ro_properties(ro_file): + """ + :param TextIOWrapper ro_file: CSV file containing RO IRIs and labels + :return: dict of label to property IRI + """ + ro_props = {} + try: + reader = csv.reader(ro_file, delimiter=',') + # Skip headers + next(reader) + for row in reader: + iri = row[0] + label = normalize_label(row[1]) + ro_props[label] = iri + finally: + ro_file.close() + return ro_props + + +def get_properties(ontology): """Create a map of normalized property label to property IRI. Args: - namespace (str): ontology ID ontology (OWLOntology): ontology object Return: @@ -114,7 +136,8 @@ def get_properties(namespace, ontology): for dp in ontology.getDataPropertiesInSignature(): dp_iri = dp.getIRI() obsolete = False - for ann in ontology.getAnnotationAssertionAxioms(op_iri): + normal = None + for ann in ontology.getAnnotationAssertionAxioms(dp_iri): ann_prop = ann.getProperty() if ann_prop.isLabel(): # find the label @@ -136,7 +159,7 @@ def get_properties(namespace, ontology): return props -def big_has_valid_relations(namespace, file, ro_props): +def big_has_valid_relations(namespace, file, ro_props, ontology_dir): """Check fp 7 - relations - on large ontologies. Retrieve all non-obsolete properties from the ontology. Compare their @@ -150,29 +173,31 @@ def big_has_valid_relations(namespace, file, ro_props): namespace (str): ontology ID file (str): path to ontology file ro_props (dict): map of RO property label to IRI + ontology_dir (str): Return: PASS or violation level with optional help message """ if not os.path.isfile(file): + dash_utils.write_empty(os.path.join(ontology_dir, 'fp7.tsv'), ["IRI","Label","Issue"]) return {'status': 'ERROR', 'comment': 'Unable to find ontology file'} # ignore RO if namespace == 'ro': + dash_utils.write_empty(os.path.join(ontology_dir, 'fp7.tsv'), ["IRI","Label","Issue"]) return {'status': 'PASS'} - props = big_get_properties(namespace, file) + props = big_get_properties(file) # get results (PASS, INFO, or ERROR) - return check_properties(namespace, props, ro_props) + return check_properties(props, ro_props, ontology_dir) -def big_get_properties(namespace, file): +def big_get_properties(file): """Create a map of normalized property label to property IRI for large ontologies by parsing RDF/XML. Args: - namespace (str): ontology ID file (str): path to ontology file Return: @@ -180,23 +205,20 @@ def big_get_properties(namespace, file): """ # TODO: handle different prefixes props = {} - label = None - prefixes = True with open(file, 'r') as f: p_iri = None for line in f: if 'owl:ObjectProperty rdf:about' in line: try: p_iri = dash_utils.get_resource_value(line) - except Exception as e: + except Exception: print('Unable to get IRI from line: ' + line) elif p_iri and 'rdfs:label' in line: - label = None try: label = dash_utils.get_literal_value(line) normal = normalize_label(label) props[normal] = p_iri - except Exception as e: + except Exception: # continue on to the next line # might be a line break between (like RNAO) print('Unable to get label from line: ' + line) @@ -221,13 +243,13 @@ def normalize_label(s): return unicodedata.normalize('NFC', clean) -def check_properties(namespace, props, ro_props): +def check_properties(props, ro_props, ontology_dir): """Compare the properties from an ontology to the RO properties. Args: - namespace (str): ontology ID props (dict): map of ontology property label to IRI ro_props (dict): map of RO property label to IRI + ontology_dir (str): Return: PASS or violation level with optional help message @@ -259,9 +281,8 @@ def check_properties(namespace, props, ro_props): # delete the property map to free up memory del props - # maybe save a report file - if len(same_label) > 0 or len(not_ro) > 0: - save_invalid_relations(namespace, ro_props, same_label, not_ro) + # save a report file (maybe empty) + save_invalid_relations(ro_props, same_label, not_ro, ontology_dir) # return the results if len(same_label) > 0 and len(not_ro) > 0: @@ -281,18 +302,18 @@ def check_properties(namespace, props, ro_props): return {'status': 'PASS'} -def save_invalid_relations(namespace, ro_props, same_label, not_ro): +def save_invalid_relations(ro_props, same_label, not_ro, ontology_dir): """Save any violations to a TSV file in the reports directory. Args: - namespace (str): ontology ID ro_props (dict): map of RO property label to IRI same_label (dict): map of property label to IRI that matches RO property label with a different IRI not_ro (dict): map of property label to IRI that does not have an RO IRI + ontology_dir (str): """ - file = 'build/dashboard/{0}/fp7.tsv'.format(namespace) + file = os.path.join(ontology_dir, 'fp7.tsv') with open(file, 'w+') as f: f.write('IRI\tLabel\tIssue\n') for iri, label in same_label.items(): diff --git a/principles/checks/fp_008.md b/principles/checks/fp_008.md index 93aa92ecc..ae1ab2181 100644 --- a/principles/checks/fp_008.md +++ b/principles/checks/fp_008.md @@ -38,10 +38,11 @@ description: An integrated ontology for the description of life-science and clin ### Implementation -The registry data is checked for 'homepage' and 'description' entries. If either is missing, this is an error. If the homepage is present, the URL is checked to see if it resolves (does not return an HTTP status of greater than 400). If the URL does not resolve, this is also an error. +The registry data is checked for 'homepage' and 'description' entries. If either is missing, this is an error. If the homepage is present, the URL is checked to see if it returns a successful HTTP status code (200-299) rather than an error code (400+). If the URL does not resolve, this is also an error. + ```python -import requests +from lib import url_exists def has_documentation(data): @@ -77,13 +78,9 @@ def has_documentation(data): 'comment': 'Missing description'} # check if URL resolves - try: - request = requests.get(home) - except Exception as e: - return {'status': 'ERROR', - 'comment': 'homepage URL ({0}) does not resolve'.format(home)} - if request.status_code > 400: + if not url_exists(home): return {'status': 'ERROR', - 'comment': 'homepage URL ({0}) does not resolve'.format(home)} + 'comment': 'Homepage URL ({0}) does not resolve'.format(home)} + return {'status': 'PASS'} ``` diff --git a/principles/checks/fp_009.md b/principles/checks/fp_009.md index adfc5e9a6..c654b4297 100644 --- a/principles/checks/fp_009.md +++ b/principles/checks/fp_009.md @@ -10,12 +10,23 @@ Discussion on this check can be [found here](https://github.com/OBOFoundry/OBOFo ### Requirements -1. The ontology **must** have usages. +1. The ontology **must** have a tracker. +2. The ontology **must** have usages. ### Fixes First, read the [FAQ](http://obofoundry.github.io/faq/how-do-i-edit-metadata.html) on how to edit the metadata for your ontology. +#### Adding a Tracker + +If you do not already have a version control repository that has an [Issues Tracker](https://help.github.com/en/github/managing-your-work-on-github/about-issues), create one. We recommend creating a [GitHub Repository](https://help.github.com/en/github/getting-started-with-github/create-a-repo). To do this, you will need to [create a GitHub account](https://github.com/join) if you do not already have one. + +Once you have a version control repository, add the following to your [metadata file](https://github.com/OBOFoundry/OBOFoundry.github.io/tree/master/ontology) (replacing with the link to your repository's issue tracker): + +``` +tracker: https://github.com/DiseaseOntology/HumanDiseaseOntology/issues +``` + #### Adding Usages Determine what other groups are using your ontology and how they are using it. Then, add the following to your [metadata file](https://github.com/OBOFoundry/OBOFoundry.github.io/tree/master/ontology) (replacing with the correct group name, link, and description): @@ -29,11 +40,11 @@ usages: description: Human genes and mouse homology associated with nail diseases (description of specific example) ``` -You may have multiple exampels for each user, and mulitple users under the `usages` tag. +You may have multiple examples for each user, and multiple users under the `usages` tag. ### Implementation -The registry data is checked for 'usage' entries. If they are missing, this is an error. +The registry data is checked for 'tracker' and 'usage' entries. If either is missing, this is an error. The tracker should be a valid URL to an issue tracking system, and usages should contain at least one user with a valid URL and description. ```python import dash_utils @@ -50,14 +61,23 @@ def has_users(data): Return: PASS or ERROR with optional help message """ + if 'tracker' in data: + tracker = data['tracker'] + else: + tracker = None if 'usages' in data: usages = data['usages'] - # TODO: usages should have a valid user that resolves + # TODO: usages should have a valid user that resovles # and a description else: usages = None - if usages is None: + # tracker is required? + if tracker is None and usages is None: + return {'status': 'ERROR', 'comment': 'Missing tracker and usages'} + elif tracker is None: + return {'status': 'ERROR', 'comment': 'Missing tracker'} + elif usages is None: return {'status': 'ERROR', 'comment': 'Missing usages'} return {'status': 'PASS'} ``` diff --git a/principles/checks/fp_011.md b/principles/checks/fp_011.md index 0f3eb8d68..61a6e55ea 100644 --- a/principles/checks/fp_011.md +++ b/principles/checks/fp_011.md @@ -16,7 +16,7 @@ Discussion on this check can be [found here](https://github.com/OBOFoundry/OBOFo First, read the [FAQ](http://obofoundry.github.io/faq/how-do-i-edit-metadata.html) on how to edit the metadata for your ontology. -Next, determine who the point person for your ontology project is. This _must not_ be a mailing list. If this person does not already have a GitHub account, we request that they [create one](https://github.com/join). Then, add the following to your [metadata file](https://github.com/OBOFoundry/OBOFoundry.github.io/tree/master/ontology) (replacing with the correct email, name, and GitHub username): +Next, determine who the point person for your ontology project is. This *must not* be a mailing list. If this person does not already have a GitHub account, we request that they [create one](https://github.com/join). Then, add the following to your [metadata file](https://github.com/OBOFoundry/OBOFoundry.github.io/tree/master/ontology) (replacing with the correct email, name, and GitHub username): ``` contact: @@ -30,15 +30,12 @@ contact: The registry data entry is validated with JSON schema using the [contact schema](https://raw.githubusercontent.com/OBOFoundry/OBOFoundry.github.io/master/util/schema/contact.json). The contact schema ensures that a contact entry is present and that the entry has a name and email address. ```python -import jsonschema - import dash_utils +import jsonschema from dash_utils import format_msg -contact_schema = dash_utils.load_schema('dependencies/contact.json') - -def has_contact(data): +def has_contact(data, contact_schema): """Check fp 11 - locus of authority. Check if the registry data contains a valid contract entry. diff --git a/principles/checks/fp_016.md b/principles/checks/fp_016.md index 82689b2b9..21cef36cf 100644 --- a/principles/checks/fp_016.md +++ b/principles/checks/fp_016.md @@ -103,12 +103,8 @@ def check_version_iri(version_iri): version_date = datetime.datetime( int(splits[0]), int(splits[1]), int(splits[2])) - # check 3 years (error) - three_years_ago = datetime.datetime.now() \ - - datetime.timedelta(days=3*365) - if version_date < three_years_ago: - return {'status': 'ERROR', - 'comment': old_version_msg.format(date, 'three')} + # Dropping the severity to 2 years old as WARN decided on the 2023-07-25 OFOC call + # Check issue https://github.com/OBOFoundry/OBO-Dashboard/issues/94 # check 2 years (warn) two_years_ago = datetime.datetime.now() \ diff --git a/principles/checks/fp_020.md b/principles/checks/fp_020.md index 64efa910f..4994c618d 100644 --- a/principles/checks/fp_020.md +++ b/principles/checks/fp_020.md @@ -36,22 +36,22 @@ from dash_utils import format_msg def is_responsive(data): - """Check fp 20 - responsiveness. - If the ontology has an active issue tracker, PASS. + """Check fp 20 - responsiveness. + If the ontology has an active issue tracker, PASS. - Args: - data (dict): ontology registry data from YAML file + Args: + data (dict): ontology registry data from YAML file - Return: - PASS or ERROR with optional help message - """ - if 'tracker' in data: - tracker = data['tracker'] - else: - tracker = None + Return: + PASS or ERROR with optional help message + """ + if 'tracker' in data: + tracker = data['tracker'] + else: + tracker = None - if tracker is None: - return {'status': 'ERROR', 'comment': 'Missing tracker'} + if tracker is None: + return {'status': 'ERROR', 'comment': 'Missing tracker'} - return {'status': 'PASS'} + return {'status': 'PASS'} ```