pylode 2.8.3__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. pyLODE-2.8.3.dist-info/LICENSE +643 -0
  2. pyLODE-2.8.3.dist-info/METADATA +772 -0
  3. pyLODE-2.8.3.dist-info/RECORD +63 -0
  4. pyLODE-2.8.3.dist-info/WHEEL +6 -0
  5. pyLODE-2.8.3.dist-info/entry_points.txt +3 -0
  6. pyLODE-2.8.3.dist-info/top_level.txt +1 -0
  7. pylode/__init__.py +5 -0
  8. pylode/cli.py +191 -0
  9. pylode/common.py +191 -0
  10. pylode/curies.py +2208 -0
  11. pylode/profiles/__init__.py +4 -0
  12. pylode/profiles/base.py +463 -0
  13. pylode/profiles/ontdoc.py +1283 -0
  14. pylode/profiles/prof.py +276 -0
  15. pylode/profiles/profile.py +135 -0
  16. pylode/profiles/vocpub.py +605 -0
  17. pylode/server.py +24 -0
  18. pylode/style/pylode.css +179 -0
  19. pylode/templates/agent.html +11 -0
  20. pylode/templates/agent.md +11 -0
  21. pylode/templates/namespaces.html +9 -0
  22. pylode/templates/namespaces.md +5 -0
  23. pylode/templates/ontdoc/class.html +121 -0
  24. pylode/templates/ontdoc/class.md +40 -0
  25. pylode/templates/ontdoc/classes.html +11 -0
  26. pylode/templates/ontdoc/classes.md +7 -0
  27. pylode/templates/ontdoc/document.html +42 -0
  28. pylode/templates/ontdoc/document.md +16 -0
  29. pylode/templates/ontdoc/metadata.html +127 -0
  30. pylode/templates/ontdoc/metadata.md +111 -0
  31. pylode/templates/ontdoc/named_individual.html +65 -0
  32. pylode/templates/ontdoc/named_individual.md +25 -0
  33. pylode/templates/ontdoc/named_individuals.html +11 -0
  34. pylode/templates/ontdoc/named_individuals.md +7 -0
  35. pylode/templates/ontdoc/properties.html +65 -0
  36. pylode/templates/ontdoc/properties.md +45 -0
  37. pylode/templates/ontdoc/property.html +129 -0
  38. pylode/templates/ontdoc/property.md +37 -0
  39. pylode/templates/orcid.html +19 -0
  40. pylode/templates/orcid.md +1 -0
  41. pylode/templates/prof/prof.html +38 -0
  42. pylode/templates/prof/prof.md +16 -0
  43. pylode/templates/prof/profile.html +90 -0
  44. pylode/templates/prof/profile.md +85 -0
  45. pylode/templates/prof/resource_descriptor.html +49 -0
  46. pylode/templates/prof/resource_descriptor.md +21 -0
  47. pylode/templates/vocpub/collection.html +51 -0
  48. pylode/templates/vocpub/collection.md +23 -0
  49. pylode/templates/vocpub/collections.html +11 -0
  50. pylode/templates/vocpub/collections.md +7 -0
  51. pylode/templates/vocpub/concept.html +115 -0
  52. pylode/templates/vocpub/concept.md +40 -0
  53. pylode/templates/vocpub/concept_hierarchy.html +80 -0
  54. pylode/templates/vocpub/concept_scheme.html +105 -0
  55. pylode/templates/vocpub/concept_scheme.md +102 -0
  56. pylode/templates/vocpub/concepts.html +9 -0
  57. pylode/templates/vocpub/concepts.md +7 -0
  58. pylode/templates/vocpub/taxonomy.html +42 -0
  59. pylode/templates/vocpub/taxonomy.md +17 -0
  60. pylode/tests/__init__.py +0 -0
  61. pylode/tests/test_ont_html_description.py +7 -0
  62. pylode/tests/test_skos.py +8 -0
  63. pylode/tests/tests_all.py +50 -0
@@ -0,0 +1,4 @@
1
+ from .ontdoc import OntDoc
2
+ from .prof import Prof
3
+ from .vocpub import VocPub
4
+ from .profile import *
@@ -0,0 +1,463 @@
1
+ import collections
2
+
3
+ from jinja2 import Environment, FileSystemLoader
4
+ from rdflib import SDO, SKOS, OWL, URIRef, RDF, PROF, Literal, XSD, Graph, Namespace, FOAF
5
+
6
+ import pylode.profiles.profile
7
+ from pylode.common import TEMPLATES_DIR
8
+
9
+
10
+ class BaseProfile:
11
+ def __init__(self, g, source_info, outputformat="html", exclude_css=False, default_language="en", get_curies_online=False):
12
+ self.outputformat = outputformat
13
+ self.exclude_css = exclude_css
14
+ self.default_language = default_language
15
+ self.get_curies_online = get_curies_online
16
+ self.default_namespace = None
17
+ self.G = g
18
+ self.source_info = source_info
19
+ self.G.bind("sdo", SDO)
20
+ self.G.bind("skos", SKOS)
21
+ self.NAMESPACES = collections.OrderedDict()
22
+ self.FIDS = {}
23
+ self.METADATA = {}
24
+
25
+ def _load_template(self, template_file):
26
+ return Environment(loader=FileSystemLoader(TEMPLATES_DIR)).get_template(template_file)
27
+
28
+ def _expand_graph(self):
29
+ """Abstract method"""
30
+
31
+ # TODO: replace this with rdflib native method
32
+ def _get_namespace_from_uri(self, uri):
33
+ # split on hash
34
+ segments = uri.split("#")
35
+ if len(segments) == 2:
36
+ return segments[0] + "#"
37
+ else:
38
+ segments = uri.split("/")
39
+ if len(segments) > 1:
40
+ return "/".join(segments[0:-1]) + "/"
41
+ else:
42
+ return None
43
+
44
+ def _get_uri_id(self, uri):
45
+ # split on hash
46
+ segments = uri.split("#")
47
+ if len(segments) == 2:
48
+ return segments[1]
49
+ else:
50
+ return uri.split("/")[-1] # could return None if URI ends in /
51
+
52
+ def _make_formatted_uri(self, uri):
53
+ """Abstract method"""
54
+
55
+ def _get_curie(self, uri):
56
+ n = self._get_namespace_from_uri(str(uri))
57
+ for k, v in self.NAMESPACES.items():
58
+ if v == n or v.strip("/#") == n:
59
+ if k == ":":
60
+ return "{}".format(self._get_uri_id(uri))
61
+ else:
62
+ return "{}:{}".format(k, self._get_uri_id(uri))
63
+
64
+ # if no match, return the original URI
65
+ return uri
66
+
67
+ def _get_curie_prefix(uself, uri, existing_curies):
68
+ ns_count = 0
69
+
70
+ from curies import CURIES
71
+
72
+ # TODO: replace this with a once-per run update CURIES function
73
+ def get_curie_online(uri):
74
+ import requests
75
+ try:
76
+ r = requests.get(
77
+ "http://prefix.cc/reverse", params={"uri": uri, "format": "txt"}
78
+ )
79
+ if r.status_code == 200:
80
+ # primitive check to see if it really is prefix.cc replying with a text/plain response
81
+ if r.headers["Content-Type"] == "text/plain":
82
+ return r.text.split("\t")[0]
83
+ else:
84
+ return None
85
+ else:
86
+ return None
87
+ except requests.exceptions.ConnectionError:
88
+ # presumably this module can't access the internet or prefix.cc is down
89
+ return None
90
+
91
+ def get_curie_from_namespace(uri, existing_curies, ns_count):
92
+ # strip off trailing hash or slash and return last path segment
93
+ c = uri.rstrip("#/").split("/")[-1]
94
+
95
+ # prevent CURIE collision = return nsX (X int) if we already have this one
96
+ if c in existing_curies:
97
+ ns_count += 1
98
+ return "ns" + str(ns_count)
99
+
100
+ return c
101
+
102
+ # attempt to look up the well-known curie for this Namespace in http://prefix.cc dump
103
+ for k, v in CURIES.items():
104
+ if v == uri:
105
+ return k
106
+
107
+ # attempt to look up the well-known CURIE for this Namespace using http://prefix.cc online (more up-to-date)
108
+ c = get_curie_online(uri)
109
+ if c is not None:
110
+ return c
111
+
112
+ # can't find CURIE online so make up one
113
+ c = get_curie_from_namespace(uri, existing_curies, ns_count)
114
+ return c if c is not None else ""
115
+
116
+ def _make_title_from_uri(self, uri):
117
+ # can't tolerate any URI faults so return None if anything is wrong
118
+
119
+ # URIs with no path segments or ending in slash
120
+ segments = uri.split("/")
121
+ if len(segments[-1]) < 1:
122
+ return None
123
+
124
+ # URIs with only a domain - no path segments
125
+ if len(segments) < 4:
126
+ return None
127
+
128
+ # URIs ending in hash
129
+ if segments[-1].endswith("#"):
130
+ return None
131
+
132
+ return (
133
+ segments[-1].split("#")[-1]
134
+ if segments[-1].split("#")[-1] != ""
135
+ else segments[-1].split("#")[-2]
136
+ )
137
+
138
+ def _extract_namespaces(self):
139
+ """
140
+ First we get the namespaces from rdflib
141
+
142
+ Then we cycle through all the URIs in the graph (all s, p & o),
143
+ create a set of them,
144
+ extract their base URIS (i.e. a non-duplicative list of them)
145
+ see if they are in the namespaces,
146
+ if not, generate their CURIE and add them to namespaces
147
+ """
148
+ # get declared namespaces, keyed by URI
149
+ ns = {}
150
+ uri_bases = set()
151
+ for k, v in self.G.namespaces():
152
+ ns[str(v)] = k
153
+
154
+ # get other namespaces by extracting base URIs from all URIs
155
+ for s, p, o in self.G:
156
+ # exclude certain annotation URIs
157
+ # and individuals (SDO.identifier)
158
+ # exclude known annoying URIs (ORCID)
159
+ if (
160
+ p == OWL.versionIRI
161
+ or p == OWL.imports
162
+ or p == SDO.identifier
163
+ or str(o).startswith("https://orcid")
164
+ ):
165
+ pass
166
+ else:
167
+ # add only URI subjects (not Blank Nodes)
168
+ if type(s) == URIRef:
169
+ uri_bases.add(self._get_namespace_from_uri(str(s)))
170
+
171
+ # predicates are always URIs so add them all
172
+ uri_bases.add(self._get_namespace_from_uri(str(p)))
173
+
174
+ # add only URI objects (not Blank Nodes or Literals), exclude emails
175
+ if type(o) == URIRef and "@" not in str(o):
176
+ uri_bases.add(self._get_namespace_from_uri(str(o)))
177
+
178
+ # for the de-duplicated URIs, if the uri_base is not in namespaces, get CURIE and add it
179
+ for uri_base in uri_bases:
180
+ if ns.get(uri_base) is None:
181
+ if self.get_curies_online:
182
+ uri_prefix = self._get_curie_prefix(uri_base, [x for x in ns.values()])
183
+ ns[uri_base] = uri_prefix
184
+
185
+ # invert the key/values in instances
186
+ for k, v in sorted(ns.items(), key=lambda x: x[1]):
187
+ if v == "": # can't use empty dict keys in Python
188
+ self.NAMESPACES[":"] = k
189
+ else:
190
+ self.NAMESPACES[v] = k
191
+
192
+ del(self.NAMESPACES["xml"]) # that bloody XML namespace has to go!
193
+
194
+ def _get_default_namespace(self):
195
+ self.METADATA["default_namespace"] = None
196
+
197
+ # if this ontology declares a preferred URI, use that
198
+ if self.METADATA.get("preferredNamespaceUri"):
199
+ self.METADATA["default_namespace"] = self.METADATA.get(
200
+ "preferredNamespaceUri"
201
+ )
202
+
203
+ ont_uri = ""
204
+
205
+ # if not, try the URI of the ontology compared to all prefixes
206
+ for s in self.G.subjects(predicate=RDF.type, object=OWL.Ontology):
207
+ ont_uri = str(s)
208
+
209
+ for s in self.G.subjects(predicate=RDF.type, object=SKOS.ConceptScheme):
210
+ ont_uri = str(s)
211
+
212
+ for s in self.G.subjects(predicate=RDF.type, object=PROF.Profile):
213
+ ont_uri = str(s)
214
+
215
+ for k, v in self.NAMESPACES.items():
216
+ # i.e. the ontology URI is the same as the default namespace + / or #
217
+ if v == ont_uri + "/" or v == ont_uri + "#":
218
+ self.METADATA["default_namespace"] = v
219
+
220
+ if self.NAMESPACES.get("") is not None:
221
+ del self.NAMESPACES[""]
222
+
223
+ def _make_namespaces(self):
224
+ return BaseProfile._load_template(self, "namespaces." + self.outputformat).render(
225
+ namespaces=self.NAMESPACES,
226
+ default_namespace=self.METADATA["default_namespace"],
227
+ )
228
+
229
+ # makes the fragment ID for a class, property, Named Individual (any entity) based on URI or name
230
+ def _make_fid(self, title, uri):
231
+ # does this URI already have a fid?
232
+ existing_fid = self.FIDS.get(uri)
233
+ if existing_fid is not None:
234
+ return existing_fid
235
+
236
+ # if we get here, there is no fid, so make one
237
+ def _remove_non_ascii_chars(s):
238
+ return "".join(j for j in s if ord(j) < 128).replace("&", "")
239
+
240
+ # try creating an ID from label
241
+ # lowercase, remove spaces, escape all non-ASCII chars
242
+ if title is not None:
243
+ fid = _remove_non_ascii_chars(title.replace(" ", ""))
244
+
245
+ # do not return fid if it's already in use
246
+ if fid not in self.FIDS.values():
247
+ self.FIDS[uri] = fid
248
+ return fid
249
+
250
+ # this fid is already present so generate a new one from the URI instead
251
+
252
+ # split URI for last slash segment
253
+ segments = uri.split("/")
254
+ # return None for empty string - URI ends in slash
255
+ if len(segments[-1]) < 1:
256
+ return None
257
+
258
+ # return None for domains, i.e. ['http:', '', '{domain}'] - no path segments
259
+ if len(segments) < 4:
260
+ return None
261
+
262
+ # split out hash URIs
263
+ # remove any training hashes
264
+ if segments[-1].endswith("#"):
265
+ return None
266
+
267
+ fid = (
268
+ segments[-1].split("#")[-1]
269
+ if segments[-1].split("#")[-1] != ""
270
+ else segments[-1].split("#")[-2]
271
+ )
272
+ # fid = fid.lower()
273
+
274
+ # do not return fid if it's already in use
275
+ if fid not in self.FIDS.values():
276
+ self.FIDS[uri] = fid
277
+ return fid
278
+ else:
279
+ # since it's in use but we've exhausted generation options, just add 1 to existing fid name
280
+ self.FIDS[uri] = fid + "1"
281
+ return fid + "1" # yeah yeah, there could be more than one but unlikely
282
+
283
+ def _make_schemaorg_metadata(self):
284
+ uri = URIRef(self.METADATA.get("uri"))
285
+ name = Literal(self.METADATA.get("title"))
286
+ publishers = ""
287
+ creators = ""
288
+ if self.METADATA.get("created") is not None:
289
+ date_created = Literal(self.METADATA.get("created"), datatype=XSD.date)
290
+ if self.METADATA.get("modified") is not None:
291
+ date_modified = Literal(self.METADATA.get("modified"), datatype=XSD.date)
292
+ if self.METADATA.get("description") is not None:
293
+ description = Literal(self.METADATA.get("description"))
294
+ if self.METADATA.get("license") is not None:
295
+ if self.outputformat == "md":
296
+ license = URIRef(self.METADATA.get("license").split('(')[0].strip('[]'))
297
+ else:
298
+ license = URIRef(self.METADATA.get("license").split('"')[1])
299
+ else:
300
+ license = None
301
+ if self.METADATA.get("rights") is not None:
302
+ rights = Literal(self.METADATA.get("rights"))
303
+ copyright_holder = ""
304
+ if self.METADATA.get("created") is not None:
305
+ copyright_year = Literal(
306
+ self.METADATA.get("created").split("-")[0], datatype=XSD.int
307
+ )
308
+ if self.METADATA.get("repository") is not None:
309
+ if self.outputformat == "md":
310
+ repository = URIRef(self.METADATA.get("repository").split('(')[0].strip('[]'))
311
+ else:
312
+ repository = URIRef(self.METADATA.get("repository").split('"')[1])
313
+
314
+ """
315
+ @prefix sdo: <https://schema.org/> .
316
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
317
+
318
+ <http://linked.data.gov.au/def/crs> a sdo:DigitalDocument ;
319
+ sdo:name "CRS Ontology" ;
320
+ sdo:publisher <http://catalogue.linked.data.gov.au/org/naa> ;
321
+ sdo:creator [
322
+ sdo:name "Nicholas J. Car" ;
323
+ sdo:identifier <http://orcid.org/0000-0002-8742-7730> ;
324
+ sdo:email <nicholas.car@csiro.au> ;
325
+ sdo:memberOf [
326
+ sdo:name "CSIRO" ;
327
+ sdo:identifier <http://catalogue.linked.data.gov.au/org/csiro> ;
328
+ ] ;
329
+ ] ;
330
+ sdo:date_created "2018-09-10"^^xsd:date ;
331
+ sdo:date_modified "2019-05-31"^^xsd:date ;
332
+ sdo:license <https://creativecommons.org/licenses/by/4.0/> ;
333
+
334
+ sdo:copyrightHolder <http://catalogue.linked.data.gov.au/org/naa> ;
335
+ sdo:copyright_year "2019"^^xsd:gYear ;
336
+ sdo:encodingFormat <https://w3id.org/mediatype/text/html> ;
337
+ .
338
+
339
+ <http://catalogue.linked.data.gov.au/org/naa>
340
+ a sdo:Organization ;
341
+ sdo:name "National Archives of Australia" ;
342
+ sdo:identifier <http://catalogue.linked.data.gov.au/org/naa> ;
343
+ .
344
+ """
345
+ g = Graph()
346
+ SDO = Namespace("https://schema.org/")
347
+ g.bind("sdo", SDO)
348
+ g.bind("xsd", XSD)
349
+
350
+ g.add((uri, RDF.type, SDO.DefinedTermSet))
351
+ g.add((uri, SDO.name, name))
352
+ # g.add((uri, SDO.publishers, SDO.DigitalDocument))
353
+ # g.add((uri, SDO.creators, SDO.DigitalDocument))
354
+ if self.METADATA.get("date_created") is not None:
355
+ g.add((uri, SDO.dateCreated, date_created))
356
+ if self.METADATA.get("date_modified") is not None:
357
+ g.add((uri, SDO.dateModified, date_modified))
358
+ if self.METADATA.get("description") is not None:
359
+ g.add((uri, SDO.description, description))
360
+ if license is not None:
361
+ g.add((uri, SDO.license, license))
362
+ if self.METADATA.get("rights") is not None:
363
+ g.add((uri, SDO.rights, rights))
364
+ # g.add((uri, SDO.copyrightHolder, copyrightHolder))
365
+ if self.METADATA.get("copyright_year") is not None:
366
+ g.add((uri, SDO.copyrightYear, copyright_year))
367
+ if self.METADATA.get("repository") is not None:
368
+ g.add((uri, SDO.codeRepository, repository))
369
+
370
+ return g.serialize(format="json-ld").decode("utf-8")
371
+
372
+ def _make_agent_link(self, name, url=None, email=None, affiliation=None):
373
+ if self.outputformat == "md":
374
+ orcid = None
375
+ if url is not None:
376
+ if "orcid.org" in url:
377
+ orcid = BaseProfile._load_template(self, "orcid.md").render()
378
+
379
+ return BaseProfile._load_template(self, "agent.md").render(
380
+ url=url,
381
+ name=name,
382
+ orcid=orcid,
383
+ email=email.replace("mailto:", "") if email is not None else None,
384
+ affiliation=affiliation
385
+ )
386
+ else: # self.outputformat == "html":
387
+ orcid = None
388
+ if url is not None:
389
+ if "orcid.org" in url:
390
+ orcid = BaseProfile._load_template(self, "orcid.html").render()
391
+
392
+ return BaseProfile._load_template(self, "agent.html").render(
393
+ url=url,
394
+ name=name,
395
+ orcid=orcid,
396
+ email=email.replace("mailto:", "") if email is not None else None,
397
+ affiliation=affiliation
398
+ )
399
+
400
+ def _make_agent_html(self, agent_node):
401
+ # we understand foaf:name, foaf:homepage & sdo:name & sdo:identifier & sdo:email (as a URI)
402
+ # TODO: cater for other Agent representations
403
+
404
+ name = None
405
+ url = None
406
+ email = None
407
+ org_name = None
408
+ org_url = None
409
+ org_email = None
410
+ for p, o in self.G.predicate_objects(subject=agent_node):
411
+ if p in [FOAF.homepage, SDO.identifier]:
412
+ url = str(o)
413
+ elif p in [FOAF.name, SDO.name]:
414
+ name = str(o)
415
+ elif p in [FOAF.mbox, SDO.email]:
416
+ email = str(o).split("/")[-1].split("#")[-1] # remove base URI leaving only email address
417
+ elif p in [SDO.memberOf, SDO.affiliation]:
418
+ for p2, o2 in self.G.predicate_objects(subject=o):
419
+ if p2 in [FOAF.homepage, SDO.identifier, SDO.url]: # TODO: split homepage form IDs, cater for rdfs:seeAlso
420
+ org_url = str(o2)
421
+ elif p2 in [FOAF.name, SDO.name]:
422
+ org_name = str(o2)
423
+ elif p in [FOAF.mbox, SDO.email]:
424
+ org_email = str(o2).split("/")[-1].split("#")[-1] # remove base URI leaving only email address
425
+
426
+ # use the URI of the Agent for its URL if no FOAF.homepage or SDO.identifier has been set
427
+ if url is None and type(agent_node) == URIRef:
428
+ url = str(agent_node)
429
+
430
+ agent = self._make_agent_link(name, url=url, email=email)
431
+
432
+ if org_name is not None:
433
+ org = self._make_agent_link(org_name, url=org_url, email=org_email)
434
+ agent += " of " + org
435
+
436
+ return agent
437
+
438
+ def _make_source_file_link(self):
439
+ # if the source is a URI, use that
440
+ # if it's a file path, only use the file name
441
+ if self.source_info[0].startswith("http"):
442
+ uri_of_rdf = self.source_info[0]
443
+ else:
444
+ uri_of_rdf = self.source_info[0].split("/")[-1]
445
+ if self.outputformat == "md":
446
+ return 'RDF ([{}]({}))'.format(
447
+ uri_of_rdf, self.source_info[1]
448
+ )
449
+ else:
450
+ return '<a href="{}">RDF ({})</a>'.format(
451
+ uri_of_rdf, self.source_info[1]
452
+ )
453
+
454
+ def generate_document(self):
455
+ if self.ouputformat == "md":
456
+ return """# Empty pyLODE output (Markdown)"""
457
+ else: # HTML
458
+ return """<!DOCTYPE html>""" \
459
+ """<html lang="en">""" \
460
+ """ <body>""" \
461
+ """ <h1>Empty pyLODE output (HTML)</h1>""" \
462
+ """</body>""" \
463
+ """</html>"""