Skip to content

Commit eddf848

Browse files
authored
Merge pull request #414 from SynBioDex/offline-validation-conversion
Offline validation conversion
2 parents 127b92d + 41f306e commit eddf848

9 files changed

Lines changed: 341 additions & 104 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ If you get a permission error, try using the `--user` flag:
2525
pip install --user sbol2
2626
```
2727

28+
### Java dependency for offline validation/conversion
29+
30+
By default, pySBOL2 executes its validation and conversion with the online validator/converter.
31+
These can also be done offline using the Java JAR file packaged with the library.
32+
33+
Java requirements are: openjdk8 or later
34+
35+
Java location defaults to `/usr/bin/java`, and can be adjusted by setting the `JAVA_LOCATION` config option:
36+
37+
```
38+
Config.setOption(ConfigOptions.JAVA_LOCATION, '/my/java/path')
39+
```
40+
2841
## CODE EXAMPLE
2942

3043
This short example creates a Document, adds a ComponentDefinition

examples/CRISPR_example.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
setHomespace('http://sbols.org/CRISPR_Example')
44
Config.setOption(ConfigOptions.SBOL_TYPED_URIS, False)
5+
Config.setOption(ConfigOptions.VALIDATE_ONLINE, False)
56
version = '1.0'
67
doc = Document()
78

sbol2/config.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ class ConfigOptions(Enum):
1919
SBOL_TYPED_URIS = 'sbol_typed_uris'
2020
SERIALIZATION_FORMAT = 'serialization_format'
2121
VALIDATE = 'validate'
22+
VALIDATE_ONLINE = 'validate_online'
2223
VALIDATOR_URL = 'validator_url'
24+
JAVA_LOCATION = 'java_location'
2325
LANGUAGE = 'language'
2426
TEST_EQUALITY = 'test_equality'
2527
CHECK_URI_COMPLIANCE = 'check_uri_compliance'
@@ -43,7 +45,9 @@ class ConfigOptions(Enum):
4345
ConfigOptions.SBOL_TYPED_URIS.value: True,
4446
ConfigOptions.SERIALIZATION_FORMAT.value: 'sbol',
4547
ConfigOptions.VALIDATE.value: True,
48+
ConfigOptions.VALIDATE_ONLINE.value: True,
4649
ConfigOptions.VALIDATOR_URL.value: 'https://validator.sbolstandard.org/validate/',
50+
ConfigOptions.JAVA_LOCATION.value: '/usr/bin/java',
4751
ConfigOptions.LANGUAGE.value: 'SBOL2',
4852
ConfigOptions.TEST_EQUALITY.value: False,
4953
ConfigOptions.CHECK_URI_COMPLIANCE.value: False,
@@ -68,6 +72,7 @@ class ConfigOptions(Enum):
6872
ConfigOptions.SERIALIZATION_FORMAT.value: {'sbol', 'rdfxml',
6973
'json', 'ntriples'},
7074
ConfigOptions.VALIDATE.value: {True, False},
75+
ConfigOptions.VALIDATE_ONLINE.value: {True, False},
7176
ConfigOptions.LANGUAGE.value: {'SBOL2', 'FASTA', 'GenBank'},
7277
ConfigOptions.TEST_EQUALITY.value: {True, False},
7378
ConfigOptions.CHECK_URI_COMPLIANCE.value: {True, False},
@@ -182,8 +187,10 @@ def setOption(option, val):
182187
| sbol_compliant_uris | Enables autoconstruction of SBOL-compliant URIs from displayIds | True or False |
183188
| sbol_typed_uris | Include the SBOL type in SBOL-compliant URIs | True or False |
184189
| output_format | File format for serialization | True or False |
185-
| validate | Enable validation and conversion requests through the online validator | True or False |
186-
| validator_url | The http request endpoint for validation | A valid URL, set to<br>https://validator.sbolstandard.org/validate/ by default |
190+
| validate | Automatic validation | True or False |
191+
| validate_online | Use online (not local) validator for validation and conversion requests | True or False, defaults to True |
192+
| validator_url | The http request endpoint for online validation and conversion | A valid URL, set to<br>https://validator.sbolstandard.org/validate/ by default |
193+
| java_location | Path to Java executable for offline validation and conversion | A valid URL, set to<br>/usr/bin/java by default |
187194
| language | File format for conversion | SBOL2, SBOL1, FASTA, GenBank |
188195
| test_equality | Report differences between two files | True or False |
189196
| check_uri_compliance | If set to false, URIs in the file will not be checked for compliance<br>with the SBOL specification | True or False |

sbol2/document.py

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from .sequenceconstraint import SequenceConstraint
4545
from .toplevel import TopLevel
4646
from .uridict import URIDict
47+
from .validator import do_validation # local libSBOLj wrapper
4748

4849
import requests
4950

@@ -402,7 +403,7 @@ def write(self, filename):
402403
"""
403404
self.doc_serialize_rdf2xml(filename)
404405
# Optionally validate
405-
result = 'Validation disabled. To enable use of the online validation tool, use'
406+
result = 'Validation disabled. To enable use of validation, use'
406407
result += ' Config.setOption(ConfigOptions.VALIDATE, True)'
407408
if Config.getOption(ConfigOptions.VALIDATE):
408409
t_start = time.time()
@@ -822,7 +823,7 @@ def update_graph(self):
822823

823824
def validate(self):
824825
"""
825-
Run validation on this Document via the online validation tool.
826+
Run validation on this Document via the validation tool (locally or online, depending on configuration)
826827
827828
:return: A string containing a message with the validation results
828829
:rtype: str
@@ -1035,24 +1036,29 @@ def importFromFormat(self, input_path: str, overwrite=False):
10351036
contents = infile.read()
10361037
json_request['main_file'] = contents
10371038

1038-
headers = {
1039-
'Accept': 'application/json',
1040-
'Content-Type': 'application/json',
1041-
'charsets': 'utf-8'
1042-
}
1043-
1044-
validator_url = options[ConfigOptions.VALIDATOR_URL.value]
1045-
1046-
# Send the request to the online validation tool
1047-
response = requests.post(validator_url,
1048-
json=json_request,
1049-
headers=headers)
1050-
if response:
1051-
response = response.json()
1039+
validate_online = Config.getOption(ConfigOptions.VALIDATE_ONLINE)
1040+
if not validate_online:
1041+
response = do_validation(json_request)
10521042
else:
1053-
msg = 'Validation failure. HTTP post request failed with code {}: {}'
1054-
msg = msg.format(response.status_code, response.content)
1055-
raise SBOLError(SBOLErrorCode.SBOL_ERROR_BAD_HTTP_REQUEST, msg)
1043+
headers = {
1044+
'Accept': 'application/json',
1045+
'Content-Type': 'application/json',
1046+
'charsets': 'utf-8'
1047+
}
1048+
1049+
validator_url = options[ConfigOptions.VALIDATOR_URL.value]
1050+
1051+
# Send the request to the online validation tool
1052+
response = requests.post(validator_url,
1053+
json=json_request,
1054+
headers=headers)
1055+
if response:
1056+
response = response.json()
1057+
else:
1058+
msg = 'Validation failure. HTTP post request failed with code {}: {}'
1059+
msg = msg.format(response.status_code, response.content)
1060+
raise SBOLError(SBOLErrorCode.SBOL_ERROR_BAD_HTTP_REQUEST, msg)
1061+
10561062
if response['valid']:
10571063
self.appendString(response['result'], overwrite)
10581064
else:
@@ -1112,29 +1118,34 @@ def validate(doc: Document, options: Mapping[str, Any]):
11121118
"""
11131119
return_file_key = config.ConfigOptions.RETURN_FILE.value
11141120
validator_key = config.ConfigOptions.VALIDATOR_URL.value
1121+
validate_online = Config.getOption(ConfigOptions.VALIDATE_ONLINE)
11151122
json_request = _make_validation_request(options)
11161123
# We always want the return file
11171124
json_request[return_file_key] = options[return_file_key]
11181125
json_request['main_file'] = doc.writeString()
11191126

1120-
headers = {
1121-
'Accept': 'application/json',
1122-
'Content-Type': 'application/json',
1123-
'charsets': 'utf-8'
1124-
}
1127+
if not validate_online:
1128+
result = do_validation(json_request)
1129+
return result
1130+
else:
1131+
headers = {
1132+
'Accept': 'application/json',
1133+
'Content-Type': 'application/json',
1134+
'charsets': 'utf-8'
1135+
}
11251136

1126-
validator_url = options[validator_key]
1137+
validator_url = options[validator_key]
11271138

1128-
# Send the request to the online validation tool
1129-
response = requests.post(validator_url,
1130-
json=json_request,
1131-
headers=headers)
1132-
if response:
1133-
return response.json()
1134-
else:
1135-
msg = 'Validation failure. HTTP post request failed with code {}: {}'
1136-
msg = msg.format(response.status_code, response.content)
1137-
raise SBOLError(SBOLErrorCode.SBOL_ERROR_BAD_HTTP_REQUEST, msg)
1139+
# Send the request to the online validation tool
1140+
response = requests.post(validator_url,
1141+
json=json_request,
1142+
headers=headers)
1143+
if response:
1144+
return response.json()
1145+
else:
1146+
msg = 'Validation failure. HTTP post request failed with code {}: {}'
1147+
msg = msg.format(response.status_code, response.content)
1148+
raise SBOLError(SBOLErrorCode.SBOL_ERROR_BAD_HTTP_REQUEST, msg)
11381149

11391150

11401151
igem_assembly_scars = '''<?xml version="1.0" encoding="utf-8"?>

sbol2/libSBOLj.jar

8.84 MB
Binary file not shown.

sbol2/validator.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# SBOL Validator worker class
2+
# Written by Zach Zundel
3+
# zach.zundel@utah.edu
4+
# 08/13/2016
5+
# Imported from https://github.com/SynBioDex/SBOL-Validator
6+
import subprocess
7+
import tempfile
8+
import uuid
9+
import traceback
10+
import os
11+
12+
from sbol2 import Config, ConfigOptions
13+
14+
15+
class ValidationResult:
16+
def __init__(self, output_file, equality):
17+
self.check_equality = equality
18+
self.output_file = output_file
19+
self.valid = False
20+
self.errors = []
21+
22+
def digest_errors(self, output):
23+
self.errors = output.strip().split('\n')
24+
25+
def decipher(self, output, options):
26+
if self.check_equality:
27+
if "differ" in output or "not found in" in output:
28+
self.equal = False
29+
else:
30+
self.equal = True
31+
32+
if "Validation successful, no errors." not in output:
33+
self.valid = False
34+
self.digest_errors(output)
35+
else:
36+
self.digest_errors(output.strip(u"Validation successful, no errors."))
37+
self.valid = True
38+
39+
if options.return_file:
40+
with open(options.output_file, 'r') as file:
41+
self.result = file.read()
42+
43+
def broken_validation_request(self, command):
44+
self.valid = False
45+
self.errors = ["Something about your validation request is contradictory or poorly-formed!", " ".join(command)]
46+
47+
def json(self):
48+
return self.__dict__
49+
50+
51+
class ValidationRun:
52+
def __init__(self, options, validation_file, diff_file=None):
53+
self.options = options
54+
self.validation_file = validation_file
55+
self.diff_file = diff_file
56+
57+
def execute(self):
58+
result = ValidationResult(self.options.output_file, self.options.test_equality)
59+
60+
# Attempt to run command
61+
jar_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "libSBOLj.jar")
62+
command = self.options.command(jar_path, self.validation_file, self.diff_file)
63+
try:
64+
output = subprocess.check_output(command, universal_newlines=True, stderr=subprocess.STDOUT)
65+
result.decipher(output, self.options)
66+
except subprocess.CalledProcessError as exception:
67+
# If the command fails, the file is not valid.
68+
result.valid = False
69+
result.errors += [exception.output, ]
70+
except ValueError as ve:
71+
print(traceback.print_tb(ve.__traceback__))
72+
result.broken_validation_request(command)
73+
74+
return result.json()
75+
76+
77+
class ValidationOptions:
78+
language = "SBOL2"
79+
subset_uri = False
80+
fail_on_first_error = False
81+
provide_detailed_stack_trace = False
82+
check_uri_compliance = True
83+
check_completeness = True
84+
check_best_practices = False
85+
uri_prefix = False
86+
version = False
87+
insert_type = False
88+
test_equality = False
89+
return_file = True
90+
main_file_name = "main file"
91+
diff_file_name = "comparison file"
92+
93+
def __init__(self, return_file):
94+
self.return_file = return_file
95+
96+
def build(self, work_dir, data):
97+
for key, value in data.items():
98+
setattr(self, key, value)
99+
self.output_file = os.path.join(work_dir, str(uuid.uuid4()))
100+
101+
if self.language in ['SBOL1', 'SBOL2', 'SBML']:
102+
self.output_file = self.output_file + ".rdf"
103+
elif self.language == 'GFF3':
104+
self.output_file = self.output_file + '.gff'
105+
elif self.language == 'GenBank':
106+
self.output_file = self.output_file + '.gb'
107+
else:
108+
self.output_file = self.output_file + '.fasta'
109+
110+
def command(self, jar_path, validation_file, diff_file=None):
111+
java_location = Config.getOption(ConfigOptions.JAVA_LOCATION)
112+
command = [java_location, "-jar", jar_path, validation_file, "-o", self.output_file, "-l", self.language]
113+
114+
if self.test_equality and diff_file:
115+
command += ["-e", diff_file, "-mf", self.main_file_name, "-cf", self.diff_file_name]
116+
elif self.test_equality and not diff_file:
117+
raise ValueError
118+
119+
if self.subset_uri:
120+
command += ["-s", self.subset_uri]
121+
122+
if self.provide_detailed_stack_trace and not self.fail_on_first_error:
123+
raise ValueError
124+
125+
if self.fail_on_first_error:
126+
command += ["-f"]
127+
128+
if self.provide_detailed_stack_trace:
129+
command += ["-d"]
130+
131+
if not self.check_uri_compliance:
132+
command += ["-n"]
133+
134+
if not self.check_completeness:
135+
command += ["-i"]
136+
137+
if self.check_best_practices:
138+
command += ["-b"]
139+
140+
if self.uri_prefix:
141+
command += ["-p", self.uri_prefix]
142+
143+
if self.version:
144+
command += ["-v", self.version]
145+
146+
if self.insert_type:
147+
command += ["-t"]
148+
149+
return command
150+
151+
152+
def do_validation(json):
153+
"""
154+
Performs validation based on a json request
155+
"""
156+
with tempfile.TemporaryDirectory() as work_dir:
157+
options = ValidationOptions(json['return_file'])
158+
options.build(work_dir, json['options'])
159+
160+
main_filename = os.path.join(work_dir, str(uuid.uuid4()) + ".sbol")
161+
with open(main_filename, 'a+') as file:
162+
file.write(json["main_file"])
163+
164+
if json['options']['test_equality']:
165+
diff_filename = os.path.join(work_dir, str(uuid.uuid4()) + ".sbol")
166+
167+
with open(diff_filename, 'a+') as file:
168+
file.write(json["diff_file"])
169+
170+
run = ValidationRun(options, main_filename, diff_filename)
171+
else:
172+
run = ValidationRun(options, main_filename)
173+
174+
result = run.execute()
175+
return result

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
'urllib3',
3939
'packaging>=20.0'
4040
],
41+
package_data={'sbol2': ['libSBOLj.jar']},
4142
tests_require=[
4243
'pycodestyle>=2.6.0'
4344
])

0 commit comments

Comments
 (0)