defined_namespace_creator.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. """
  2. This rdflib Python script creates a DefinedNamespace Python file from a given RDF file
  3. It is a very simple script: it finds all things defined in the RDF file within a given
  4. namespace:
  5. <thing> a ?x
  6. where ?x is anything and <thing> starts with the given namespace
  7. Nicholas J. Car, Dec, 2021
  8. """
  9. from __future__ import annotations
  10. import argparse
  11. import datetime
  12. import keyword
  13. from pathlib import Path
  14. from typing import TYPE_CHECKING, Iterable, List, Tuple
  15. from rdflib.graph import Graph
  16. from rdflib.namespace import DCTERMS, OWL, RDFS, SKOS
  17. from rdflib.util import guess_format
  18. if TYPE_CHECKING:
  19. from rdflib.query import ResultRow
  20. def validate_namespace(namespace: str) -> None:
  21. if not namespace.endswith(("/", "#")):
  22. raise ValueError("The supplied namespace must end with '/' or '#'")
  23. def validate_object_id(object_id: str) -> None:
  24. for c in object_id:
  25. if not c.isupper():
  26. raise ValueError("The supplied object_id must be an all-capitals string")
  27. # This function is not used: it was originally written to get classes and to be used
  28. # alongside a method to get properties, but then it was decided that a single function
  29. # to get everything in the namespace, get_target_namespace_elements(), was both simper
  30. # and better covered all namespace elements, so that function is used instead.
  31. #
  32. # def get_classes(g, target_namespace):
  33. # namespaces = {"dcterms": DCTERMS, "owl": OWL, "rdfs": RDFS, "skos": SKOS}
  34. # q = """
  35. # SELECT DISTINCT ?x ?def
  36. # WHERE {
  37. # # anything that is an instance of owl:Class or rdfs:Class
  38. # # or any subclass of them
  39. # VALUES ?c { owl:Class rdfs:Class }
  40. # ?x rdfs:subClassOf*/a ?c .
  41. #
  42. # # get any definitions, if they have one
  43. # OPTIONAL {
  44. # ?x rdfs:comment|dcterms:description|skos:definition ?def
  45. # }
  46. #
  47. # # only get results for the targetted namespace (supplied by user)
  48. # FILTER STRSTARTS(STR(?x), "xxx")
  49. # }
  50. # """.replace("xxx", target_namespace)
  51. # classes = []
  52. # for r in g.query(q, initNs=namespaces):
  53. # classes.append((str(r[0]), str(r[1])))
  54. #
  55. # classes.sort(key=lambda tup: tup[1])
  56. #
  57. # return classes
  58. def get_target_namespace_elements(
  59. g: Graph, target_namespace: str
  60. ) -> Tuple[List[Tuple[str, str]], List[str], List[str]]:
  61. namespaces = {"dcterms": DCTERMS, "owl": OWL, "rdfs": RDFS, "skos": SKOS}
  62. q = """
  63. SELECT ?s (GROUP_CONCAT(DISTINCT STR(?def)) AS ?defs)
  64. WHERE {
  65. # all things in the RDF data (anything RDF.type...)
  66. ?s a ?o .
  67. # get any definitions, if they have one
  68. OPTIONAL {
  69. ?s dcterms:description|rdfs:comment|skos:definition ?def
  70. }
  71. # only get results for the target namespace (supplied by user)
  72. FILTER STRSTARTS(STR(?s), "xxx")
  73. FILTER (STR(?s) != "xxx")
  74. }
  75. GROUP BY ?s
  76. """.replace(
  77. "xxx", target_namespace
  78. )
  79. elements: List[Tuple[str, str]] = []
  80. for r in g.query(q, initNs=namespaces):
  81. if TYPE_CHECKING:
  82. assert isinstance(r, ResultRow)
  83. elements.append((str(r[0]), str(r[1])))
  84. elements.sort(key=lambda tup: tup[0])
  85. elements_strs: List[str] = []
  86. non_python_elements_strs: List[str] = []
  87. for e in elements:
  88. name = e[0].replace(target_namespace, "")
  89. desc = e[1].replace("\n", " ")
  90. if name.isidentifier() and not keyword.iskeyword(name):
  91. elements_strs.append(f" {name}: URIRef # {desc}\n")
  92. else:
  93. non_python_elements_strs.append(f""" "{name}", # {desc}\n""")
  94. return elements, elements_strs, non_python_elements_strs
  95. def make_dn_file(
  96. output_file_name: Path,
  97. target_namespace: str,
  98. elements_strs: Iterable[str],
  99. non_python_elements_strs: List[str],
  100. object_id: str,
  101. fail: bool,
  102. ) -> None:
  103. header = f'''from rdflib.namespace import DefinedNamespace, Namespace
  104. from rdflib.term import URIRef
  105. class {object_id}(DefinedNamespace):
  106. """
  107. DESCRIPTION_EDIT_ME_!
  108. Generated from: SOURCE_RDF_FILE_EDIT_ME_!
  109. Date: {datetime.datetime.utcnow()}
  110. """
  111. '''
  112. with open(output_file_name, "w") as f:
  113. f.write(header)
  114. f.write("\n")
  115. f.write(f' _NS = Namespace("{target_namespace}")')
  116. f.write("\n\n")
  117. if fail:
  118. f.write(" _fail = True")
  119. f.write("\n\n")
  120. f.writelines(elements_strs)
  121. if len(non_python_elements_strs) > 0:
  122. f.write("\n")
  123. f.write(" # Valid non-python identifiers")
  124. f.write("\n")
  125. f.write(" _extras = [")
  126. f.write("\n")
  127. f.writelines(non_python_elements_strs)
  128. f.write(" ]")
  129. f.write("\n")
  130. if __name__ == "__main__":
  131. parser = argparse.ArgumentParser()
  132. parser.add_argument(
  133. "ontology_file",
  134. type=str,
  135. help="Path to the RDF ontology to extract a DefinedNamespace from.",
  136. )
  137. parser.add_argument(
  138. "target_namespace",
  139. type=str,
  140. help="The namespace within the ontology that you want to create a "
  141. "DefinedNamespace for.",
  142. )
  143. parser.add_argument(
  144. "object_id",
  145. type=str,
  146. help="The RDFlib object ID of the DefinedNamespace, e.g. GEO for GeoSPARQL.",
  147. )
  148. parser.add_argument(
  149. "-f",
  150. "--fail",
  151. dest="fail",
  152. action="store_true",
  153. help="Whether (true) or not (false) to mimic ClosedNamespace and fail on "
  154. "non-element use",
  155. )
  156. parser.add_argument("--no-fail", dest="fail", action="store_false")
  157. parser.set_defaults(feature=False)
  158. args = parser.parse_args()
  159. fmt = guess_format(args.ontology_file)
  160. if fmt is None:
  161. print("The format of the file you've supplied is unknown.")
  162. exit(1)
  163. g = Graph().parse(args.ontology_file, format=fmt)
  164. validate_namespace(args.target_namespace)
  165. validate_object_id(args.object_id)
  166. print(
  167. f"Creating DefinedNamespace file {args.object_id} "
  168. f"for {args.target_namespace}..."
  169. )
  170. print(f"Ontology with {len(g)} triples loaded...")
  171. print("Getting all namespace elements...")
  172. elements = get_target_namespace_elements(g, args.target_namespace)
  173. output_file_name = Path().cwd() / f"_{args.object_id}.py"
  174. print(f"Creating DefinedNamespace Python file {output_file_name}")
  175. make_dn_file(
  176. output_file_name,
  177. args.target_namespace,
  178. elements[1],
  179. elements[2],
  180. args.object_id,
  181. args.fail,
  182. )