Skip to content

Commit

Permalink
Update Automated checks pages (#2656)
Browse files Browse the repository at this point in the history
* add fp_020.py script to makefile to be updated automatically from OBO-Dashboard

* add fp_20 to be updated automatically; fix bug to have ID

* update checks page auto

---------

Co-authored-by: Nico Matentzoglu <nicolas.matentzoglu@gmail.com>
anitacaron and matentzn authored Jan 21, 2025
1 parent fa4b255 commit 4de23bc
Showing 12 changed files with 305 additions and 203 deletions.
6 changes: 4 additions & 2 deletions principles/Makefile
Original file line number Diff line number Diff line change
@@ -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
@@ -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 $< \
45 changes: 21 additions & 24 deletions principles/checks/fp_001.md
Original file line number Diff line number Diff line change
@@ -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']
```
29 changes: 6 additions & 23 deletions principles/checks/fp_002.md
Original file line number Diff line number Diff line change
@@ -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'}
```
64 changes: 44 additions & 20 deletions principles/checks/fp_003.md
Original file line number Diff line number Diff line change
@@ -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,43 +208,52 @@ 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):
return 'WARN'
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',
140 changes: 114 additions & 26 deletions principles/checks/fp_004.md
Original file line number Diff line number Diff line change
@@ -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
```
61 changes: 19 additions & 42 deletions principles/checks/fp_006.md
Original file line number Diff line number Diff line change
@@ -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()}
```
73 changes: 47 additions & 26 deletions principles/checks/fp_007.md
Original file line number Diff line number Diff line change
@@ -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,53 +173,52 @@ 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:
Dict of label to property IRI
"""
# 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():
15 changes: 6 additions & 9 deletions principles/checks/fp_008.md
Original file line number Diff line number Diff line change
@@ -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'}
```
30 changes: 25 additions & 5 deletions principles/checks/fp_009.md
Original file line number Diff line number Diff line change
@@ -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'}
```
9 changes: 3 additions & 6 deletions principles/checks/fp_011.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 2 additions & 6 deletions principles/checks/fp_016.md
Original file line number Diff line number Diff line change
@@ -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() \
28 changes: 14 additions & 14 deletions principles/checks/fp_020.md
Original file line number Diff line number Diff line change
@@ -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'}
```

0 comments on commit 4de23bc

Please sign in to comment.