dwc/build/build.py

326 lines
12 KiB
Python
Raw Permalink Normal View History

#
2021-07-30 15:56:42 +00:00
# Author: S. Van Hoey
# Contributors: John Wieczorek
#
# Build script for tdwg dwc handling
#
2021-07-30 15:56:42 +00:00
__version__ = '2021-07-30T-03:00'
import io
2017-12-06 19:20:31 +00:00
import os
import re
import csv
2017-09-30 15:13:14 +00:00
import sys
2017-10-01 01:55:07 +00:00
import codecs
from urllib import request
2017-12-06 19:20:31 +00:00
from jinja2 import FileSystemLoader, Environment
2017-09-30 14:49:25 +00:00
NAMESPACES = {
'http://rs.tdwg.org/dwc/iri/' : 'dwciri',
'http://rs.tdwg.org/dwc/terms/' : 'dwc',
2021-07-30 15:56:42 +00:00
'http://rs.tdwg.org/chrono/terms/' : 'chrono',
'http://purl.org/dc/elements/1.1/' : 'dc',
'http://purl.org/dc/terms/' : 'dcterms',
'http://rs.tdwg.org/dwc/terms/attributes/' : 'tdwgutility'}
class ProvidedTermsError(Exception):
2017-10-01 14:08:14 +00:00
"""inconsistency in the available terms Error"""
pass
2017-10-01 14:08:14 +00:00
2017-09-30 14:48:27 +00:00
class RdfTypeError(Exception):
2017-10-01 14:08:14 +00:00
"""rdftype encountered that is not known by builder"""
2017-09-30 14:48:27 +00:00
pass
class DwcNamespaceError(Exception):
"""Namespace link is not available in the currently provided links"""
pass
class DwcBuildReader():
2017-09-30 14:48:27 +00:00
def __init__(self, dwc_build_file):
"""Custom Reader switching between raw Github or local file"""
self.dwc_build_file = dwc_build_file
2017-09-30 14:48:27 +00:00
def __enter__(self):
if "https://raw.github" in self.dwc_build_file:
self.open_dwc_term = request.urlopen(self.dwc_build_file)
else:
self.open_dwc_term = open(self.dwc_build_file, 'rb')
return self.open_dwc_term
def __exit__(self, *args):
self.open_dwc_term.close()
class DwcDigester(object):
2017-09-30 14:48:27 +00:00
2018-10-14 10:09:40 +00:00
def __init__(self, term_versions):
2018-10-14 10:14:05 +00:00
"""Digest the term document of Darwin Core to support automatic
2018-10-14 10:09:40 +00:00
generation of derivatives
2017-10-01 14:08:14 +00:00
Parameters
-----------
term_versions : str
2018-10-14 10:14:05 +00:00
Either a relative path and filename of the normative Dwc document
or a URL link to the raw Github version of the file
2017-10-01 14:08:14 +00:00
Notes
-----
2018-10-14 10:09:40 +00:00
Remark that the sequence of the term versions entries is
essential for the automatic generation of the individual documents
(mainly the index.html)
2017-10-01 14:08:14 +00:00
"""
2017-09-30 14:48:27 +00:00
self.term_versions = term_versions
self.term_versions_data = {}
self._store_versions()
# create the defined data-object for the different outputs
self.template_data = self.process_terms()
def versions(self):
2018-10-14 10:14:05 +00:00
"""Iterator providing the terms as represented in the normative term
versions file
"""
with DwcBuildReader(self.term_versions) as versions:
for vterm in csv.DictReader(io.TextIOWrapper(versions), delimiter=','):
if vterm["status"] == "recommended":
yield vterm
2017-09-30 14:48:27 +00:00
def _store_versions(self):
2018-10-14 10:14:05 +00:00
"""Collect all the versions data in a dictionary as the
term_versions_data attribute
"""
2017-09-30 14:48:27 +00:00
for term in self.versions():
self.term_versions_data[term["term_iri"]] = term
2017-10-01 14:58:00 +00:00
@property
def _version_terms(self):
2018-10-14 10:14:05 +00:00
"""Get an overview of the terms in the term_versions file
"""
2017-09-30 14:48:27 +00:00
return set(self.term_versions_data.keys())
def _select_versions_term(self, term_iri):
2018-10-14 10:14:05 +00:00
"""Select a specific term of the versions data, using term_iri match
"""
2017-09-30 14:48:27 +00:00
return self.term_versions_data[term_iri]
@staticmethod
def split_iri(term_iri):
2018-10-14 10:14:05 +00:00
"""Split an iri field into the namespace url and the local name
of the term
"""
prog = re.compile("(.*/)([^/]*$)")
namespace, local_name = prog.findall(term_iri)[0]
return namespace, local_name
2017-09-30 14:49:25 +00:00
@staticmethod
def resolve_namespace_abbrev(namespace):
2018-10-14 10:14:05 +00:00
"""Using the NAMESPACE constant, get the namespace abbreviation by
providing the namespace link
Parameters
-----------
namespace : str
valid key of the NAMESPACES variable
"""
if namespace not in NAMESPACES.keys():
raise DwcNamespaceError("The namespace url is currently not supported in NAMESPACES")
return NAMESPACES[namespace]
2017-10-01 15:32:49 +00:00
def get_term_definition(self, term_iri):
2018-10-14 10:09:40 +00:00
"""Extract the required information from the terms table to show on
the webpage of a single term by using the term_iri as the identifier
2017-10-01 15:32:49 +00:00
Notes
------
2018-10-14 10:09:40 +00:00
Due to the current implementation, make sure to provide the same keys
represented in the record-level specific version `process_terms`
method (room for improvement)
"""
vs_term = self._select_versions_term(term_iri)
2017-09-30 14:49:25 +00:00
term_data = {}
2020-08-07 19:46:51 +00:00
term_data["label"] = vs_term['term_localName'] # See https://github.com/tdwg/dwc/issues/253#issuecomment-670098202
term_data["iri"] = term_iri
2018-10-14 10:09:40 +00:00
term_data["class"] = vs_term['organized_in']
term_data["definition"] = self.convert_link(vs_term['definition'])
2018-10-14 10:09:40 +00:00
term_data["comments"] = self.convert_link(self.convert_code(vs_term['comments']))
2018-10-15 11:24:32 +00:00
term_data["examples"] = self.convert_link(self.convert_code(vs_term['examples']))
2017-09-30 14:49:25 +00:00
term_data["rdf_type"] = vs_term['rdf_type']
namespace_url, _ = self.split_iri(term_iri)
term_data["namespace"] = self.resolve_namespace_abbrev(namespace_url)
2017-09-30 14:49:25 +00:00
return term_data
@staticmethod
def convert_code(text_with_backticks):
2018-10-14 10:14:05 +00:00
"""Takes all back-quoted sections in a text field and converts it to
the html tagged version of code blocks <code>...</code>
"""
return re.sub(r'`([^`]*)`', r'<code>\1</code>', text_with_backticks)
@staticmethod
def convert_link(text_with_urls):
2018-10-14 10:14:05 +00:00
"""Takes all links in a text field and converts it to the html tagged
version of the link
"""
def _handle_matched(inputstring):
"""quick hack version of url handling on the current prime versions data"""
url = inputstring.group()
return "<a href=\"{}\">{}</a>".format(url, url)
regx = "(http[s]?://[\w\d:#@%/;$()~_?\+-;=\\\.&]*)(?<![\)\.,])"
return re.sub(regx, _handle_matched, text_with_urls)
def process_terms(self):
2018-10-14 10:09:40 +00:00
"""Parse the config terms (sequence matters!)
Collect all required data from both the normative versions file and
the config file and return the template ready data.
2017-10-01 15:32:49 +00:00
Returns
-------
2018-10-14 10:09:40 +00:00
Data object that can be digested by the html-template file. Contains
the term data formatted to create the indidivual outputs, each list
element is a dictionary representing a class group. Hence, the data
object is structured as follows:
2017-09-30 14:49:25 +00:00
2017-10-01 15:32:49 +00:00
[
{'name' : class_group_name_1, 'label': xxxx,...,
'terms':
[
{'name' : term_1, 'label': xxxx,...},
{'name' : term_2, 'label': xxxx,...},
...
]}
{'name' : class_group_name_2,...
...},
...
]
"""
2017-09-30 14:49:25 +00:00
template_data = []
in_class = "Record-level"
2017-10-01 15:32:49 +00:00
# sequence matters in config and it starts with Record-level which we populate here ad-hoc
2017-09-30 14:49:25 +00:00
class_group = {}
class_group["label"] = "Record-level"
class_group["iri"] = None
class_group["class"] = None
class_group["definition"] = None
class_group["comments"] = None
class_group["rdf_type"] = None
2017-09-30 14:49:25 +00:00
class_group["terms"] = []
class_group["namespace"] = None
addedUseWithIRI = False
2018-10-14 10:09:40 +00:00
for term in self.versions(): # sequence of the terms file used as order
term_data = self.get_term_definition(term['term_iri'])
test = term['term_iri']
2017-09-30 14:49:25 +00:00
if term_data["rdf_type"] == "http://www.w3.org/2000/01/rdf-schema#Class":
# new class encountered
2017-09-30 14:49:25 +00:00
# store previous section in template_data
template_data.append(class_group)
#start new class group
class_group = term_data
class_group["terms"] = []
in_class = term_data["label"] # check on the class working in
elif term['term_iri']=='http://purl.org/dc/terms/language':
# Vulnerable to ordering terms in term_versions.csv, but...
# This is the first row of dwciri terms
# store previous section in template_data
template_data.append(class_group)
#start a class group for UseWithIRI
class_group = {"label":"UseWithIRI"}
class_group["terms"] = []
in_class = "UseWithIRI" # check on the class working in
addedUseWithIRI = True
class_group['terms'].append(term_data)
2017-09-30 14:49:25 +00:00
else:
class_group['terms'].append(term_data)
# save the last class to template_data
template_data.append(class_group)
return template_data
2018-10-29 18:50:13 +00:00
def create_html(self, html_template="terms.tmpl",
html_output="../docs/terms/index.md"):
2018-10-14 10:14:05 +00:00
"""build html with the processed term info, by filling in the
tmpl-template
2017-10-01 14:08:14 +00:00
Parameters
-----------
html_template : str
2017-12-06 19:20:31 +00:00
relative path and filename to the Jinja2 compatible
2017-10-01 14:08:14 +00:00
template
html_output : str
relative path and filename to write the resulting index.html
"""
2017-09-30 15:13:14 +00:00
data = {}
data["class_groups"] = self.template_data
2017-12-06 19:20:31 +00:00
2018-10-15 11:24:09 +00:00
env = Environment(
loader = FileSystemLoader(os.path.dirname(html_template)),
trim_blocks = True
)
2017-12-06 19:20:31 +00:00
template = env.get_template(os.path.basename(html_template))
html = template.render(data)
2017-09-30 14:49:25 +00:00
index_page = open(html_output, "w")
index_page.write(str(html))
index_page.close()
2017-09-30 15:13:14 +00:00
def simple_dwc_terms(self):
2018-10-14 10:14:05 +00:00
"""Only extract those terms that are simple dwc, defined as `simple`
in the flags column of the config file of terms
"""
2017-10-01 01:55:07 +00:00
properties = []
2018-10-14 10:09:40 +00:00
for term in self.versions():
term_data = self.get_term_definition(term['term_iri'])
2017-10-01 14:08:14 +00:00
if (term_data["rdf_type"] == "http://www.w3.org/1999/02/22-rdf-syntax-ns#Property" and
term["flags"] == "simple"):
properties.append(term_data["label"])
2017-10-01 01:55:07 +00:00
return properties
def create_dwc_list(self, file_output="../dist/simple_dwc_vertical.csv"):
2018-10-14 10:14:05 +00:00
"""Build a list of simple dwc terms and write it to file
2017-10-01 15:32:49 +00:00
Parameters
-----------
file_output : str
relative path and filename to write the resulting list
"""
2017-10-01 01:55:07 +00:00
with codecs.open(file_output, 'w', 'utf-8') as dwc_list_file:
for term in self.simple_dwc_terms():
2017-10-01 13:23:58 +00:00
dwc_list_file.write(term + "\n")
2017-10-01 01:55:07 +00:00
def create_dwc_header(self, file_output="../dist/simple_dwc_horizontal.csv"):
2018-10-14 10:14:05 +00:00
"""Build a header of simple dwc terms and write it to file
2017-10-01 15:32:49 +00:00
Parameters
-----------
file_output : str
relative path and filename to write the resulting list
"""
2017-10-01 01:55:07 +00:00
with codecs.open(file_output, 'w', 'utf-8') as dwc_header_file:
properties = self.simple_dwc_terms()
2017-10-01 01:55:07 +00:00
dwc_header_file.write(",".join(properties))
dwc_header_file.write("\n")
2017-09-30 15:13:14 +00:00
def main():
"""Building up the Quick Reference Guide html and derivatives"""
2017-09-30 15:13:14 +00:00
term_versions_file = "../vocabulary/term_versions.csv"
2017-09-30 15:13:14 +00:00
print("Running build process:")
2018-10-14 10:09:40 +00:00
my_dwc = DwcDigester(term_versions_file)
print("Building Quick Reference Guide")
my_dwc.create_html()
print("Building simple DwC CSV files")
my_dwc.create_dwc_list()
my_dwc.create_dwc_header()
print("Done!")
2017-09-30 15:13:14 +00:00
if __name__ == "__main__":
sys.exit(main())