Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Automated checks pages #2656

Merged
merged 7 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Expand All @@ -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
Expand All @@ -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*//'))
anitacaron marked this conversation as resolved.
Show resolved Hide resolved
if [ $(ID) == 1 ]; then export TITLE=$(1); \
elif [ $(ID) == 2 ]; then export TITLE=$(2); \
elif [ $(ID) == 3 ]; then export TITLE=$(3); \
Expand All @@ -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 $< \
Expand Down
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
---

Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.
"""
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Up @@ -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
Expand Up @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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'}

Expand Down Expand Up @@ -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.
Expand All @@ -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',
Expand Down
Loading
Loading