sparqlquery.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. #!/usr/bin/env python
  2. """
  3. A commandline tool for querying with SPARQL on local files and remote sparql endpoints with custom serialization.
  4. example usage:
  5. ```bash
  6. sq path/to/data.ttl -q "SELECT ?x WHERE {?x a foaf:Person. }"
  7. rdfpipe test.ttl | sparqlquery - -q "SELECT ?x WHERE {?x a foaf:Person. }" --format json
  8. sq data.ttl -q "ASK {:john a foaf:Person}" --format xml | grep true
  9. sq path/to/data.ttl --query-file query.rq
  10. sq data1.ttl data2.ttl -q "DESCRIBE <http://example.com/john>" --format turtle:+spacious
  11. sq http://example.com/sparqlendpoint --query-file query.rq
  12. sq http://example.com/sparqlendpoint --query-file query.rq --username user --password secret
  13. sq /pyth/to/berkeley.db -q "SELECT ?x WHERE {?x a foaf:Person. }" --remote-storetype BerkeleyDB
  14. ```
  15. Tip: You can check the truth value for an ASK query, by regex in stdout for 'true'
  16. or 'false'.
  17. """
  18. from __future__ import annotations
  19. import argparse
  20. import inspect
  21. import logging
  22. import os
  23. import sys
  24. from inspect import Parameter
  25. from typing import Any, Dict, List, Optional, Tuple, Type
  26. from urllib.parse import urlparse
  27. try:
  28. # Pyparsing >=3.0.0
  29. from pyparsing.exceptions import ParseException
  30. except ImportError:
  31. # Pyparsing 2
  32. from pyparsing import ParseException
  33. from rdflib.graph import Dataset, Graph
  34. from rdflib.plugin import PluginException
  35. from rdflib.plugin import get as get_plugin
  36. from rdflib.plugin import plugins as get_plugins
  37. from rdflib.query import Result, ResultSerializer
  38. from rdflib.serializer import Serializer
  39. from rdflib.store import Store
  40. from .rdfpipe import _format_and_kws
  41. __all__ = ["sparqlquery"]
  42. class _ArgumentError(Exception):
  43. pass
  44. class _PrintHelpError(Exception):
  45. pass
  46. class InvalidQueryError(Exception):
  47. pass
  48. def sparqlquery(
  49. endpoints: List[str],
  50. query: str,
  51. result_format: Optional[str] = None,
  52. result_keywords: Dict[str, str] = {},
  53. auth: Optional[Tuple[str, str]] = None,
  54. use_stdin: bool = False,
  55. remote_storetype: Optional[str] = None,
  56. ):
  57. if use_stdin:
  58. g = Graph().parse(sys.stdin)
  59. else:
  60. g = _get_graph(endpoints, auth, remote_storetype)
  61. try:
  62. results: Result = g.query(query)
  63. except ParseException as err:
  64. raise InvalidQueryError(query) from err
  65. if result_format is not None:
  66. ret_bytes = results.serialize(format=result_format, **result_keywords)
  67. else:
  68. ret_bytes = results.serialize(**result_keywords)
  69. if ret_bytes is not None:
  70. print(ret_bytes.decode())
  71. def _dest_is_local(dest: str):
  72. if os.path.isabs(dest):
  73. return True
  74. q = urlparse(dest)
  75. # Handle Windows drive letters (single letter followed by colon)
  76. if len(q.scheme) == 1 and q.scheme.isalpha():
  77. return True
  78. return q.scheme in ["", "file"]
  79. def _dest_is_internet_addr(dest: str):
  80. q = urlparse(dest)
  81. return q.scheme in ["http", "https"]
  82. def _get_graph(
  83. endpoints, auth: Optional[Tuple[str, str]], remote_storetype: Optional[str]
  84. ) -> Graph:
  85. graph: Graph
  86. if remote_storetype is not None:
  87. storeplugin = get_plugin(remote_storetype, Store)
  88. if auth:
  89. store = storeplugin(endpoints[0], auth=auth) # type: ignore[call-arg]
  90. else:
  91. store = storeplugin(endpoints[0])
  92. graph = Dataset(store)
  93. else:
  94. graph = Graph()
  95. for x in endpoints:
  96. graph.parse(location=x)
  97. return graph
  98. def _extract_query_and_format(parser) -> Tuple[Dict[str, Any], Optional[str]]:
  99. opts: Dict[str, Any] = {}
  100. tmp_args, rest_args = parser.parse_known_args()
  101. if tmp_args.query and tmp_args.queryfile is None:
  102. query = tmp_args.query
  103. elif tmp_args.queryfile and tmp_args.query is None:
  104. with open(tmp_args.queryfile, "r") as f:
  105. query = f.read()
  106. else:
  107. query = None
  108. if query is None:
  109. construct = False
  110. elif "DESCRIBE" in query or "CONSTRUCT" in query:
  111. construct = True
  112. else:
  113. construct = False
  114. if tmp_args.format is not None:
  115. format_, format_keywords = _format_and_kws(tmp_args.format)
  116. elif construct:
  117. format_keywords = {}
  118. format_ = "turtle"
  119. construct = True
  120. else:
  121. format_keywords = {}
  122. format_ = "json"
  123. epilog = _create_epilog_from_format(format_, construct)
  124. opts = {
  125. "query": query,
  126. "result_format": format_,
  127. "result_keywords": format_keywords,
  128. }
  129. return opts, epilog
  130. def parse_args():
  131. extra_kwargs: Dict[str, Any] = {}
  132. if sys.version_info > (3, 9):
  133. extra_kwargs["exit_on_error"] = False
  134. parser = argparse.ArgumentParser(
  135. prog="sparqlquery",
  136. description=__doc__,
  137. add_help=False, # add dynamic epilog before help is added
  138. formatter_class=argparse.RawDescriptionHelpFormatter,
  139. # else __doc__ wont be printed on error:
  140. **extra_kwargs,
  141. )
  142. parser.add_argument(
  143. "-q",
  144. "--query",
  145. type=str,
  146. help="Sparql query. Cannot be set together with -qf/--queryfile.",
  147. )
  148. parser.add_argument(
  149. "-qf",
  150. "--queryfile",
  151. type=str,
  152. help="File from where the sparql query is read. "
  153. "Cannot be set together with -q/--query",
  154. )
  155. parser.add_argument(
  156. "-f",
  157. "--format",
  158. type=str,
  159. help="Print sparql result in given format. "
  160. "Defaults to 'json' on SELECT, to 'xml' on ASK "
  161. "and to 'turtle' on DESCRIBE and CONSTRUCT. "
  162. "Keywords as described in epilog can be given "
  163. "after format like: "
  164. "FORMAT:(+)KW1,-KW2,KW3=VALUE.",
  165. )
  166. opts: Dict[str, Any]
  167. opts, parser.epilog = _extract_query_and_format(parser)
  168. parser.add_argument(
  169. "endpoint",
  170. nargs="+",
  171. type=str,
  172. help="Endpoints for sparql query. "
  173. "Can be set to multiple files. "
  174. "Reads from stdin if '-' is given. ",
  175. )
  176. parser.add_argument(
  177. "-w",
  178. "--warn",
  179. action="store_true",
  180. default=False,
  181. help="Output warnings to stderr " "(by default only critical errors).",
  182. )
  183. parser.add_argument(
  184. "-h",
  185. "--help",
  186. # action="store_true",
  187. # default=False,
  188. action="help",
  189. help="show help message and exit. "
  190. "Also prints information about given format.",
  191. )
  192. parser.add_argument(
  193. "-u", "--username", type=str, help="Username used during authentication."
  194. )
  195. parser.add_argument(
  196. "-p", "--password", type=str, help="Password used during authentication."
  197. )
  198. parser.add_argument(
  199. "-rs",
  200. "--remote-storetype",
  201. type=str,
  202. help="You can specify which storetype should be used. "
  203. "Can only be set, when using a single endpoint and not stdin. "
  204. "Will default to 'SparqlStore' when endpoint is an internetaddress.",
  205. )
  206. try: # catch error because exit_on_error=False
  207. args = parser.parse_args()
  208. except argparse.ArgumentError as err:
  209. parser.print_help()
  210. raise err
  211. forbidden_format_keywords = [
  212. x
  213. for x in opts.get("result_keywords", dict())
  214. if x in {"self", "stream", "encoding", "format"}
  215. ]
  216. if forbidden_format_keywords:
  217. raise _ArgumentError(
  218. "'self', 'stream', 'encoding' and 'format' "
  219. "mustnt be used as keywords for format."
  220. )
  221. if opts.get("query") is None:
  222. parser.print_help()
  223. raise _ArgumentError("Either -q/--query or -qf/--queryfile must be provided")
  224. remote_storetype = args.remote_storetype
  225. if len(args.endpoint) == 1:
  226. if args.endpoint[0] == "-":
  227. if remote_storetype is not None:
  228. raise _ArgumentError(
  229. "Cant us remote graphtype, when endpoint is stdin(-)"
  230. )
  231. endpoints = []
  232. opts["use_stdin"] = True
  233. elif _dest_is_internet_addr(args.endpoint[0]):
  234. endpoints = args.endpoint
  235. if remote_storetype is None:
  236. remote_storetype = "SPARQLStore"
  237. else:
  238. endpoints = args.endpoint
  239. else:
  240. if remote_storetype is not None:
  241. raise _ArgumentError(
  242. "If remote graphtype is set, only a single endpoint is valid."
  243. )
  244. endpoints = list(args.endpoint)
  245. if any(not (_dest_is_local(x)) for x in args.endpoint):
  246. raise NotImplementedError(
  247. "If multiple endpoints are given, all must be local files."
  248. )
  249. if args.username is not None and args.password is not None:
  250. if remote_storetype not in ["SPARQLStore"]:
  251. raise _ArgumentError(
  252. "Can use password and username only, "
  253. "when remote-storetype is 'SPARQLStore'."
  254. )
  255. opts["auth"] = (args.username, args.password)
  256. elif args.username is None and args.password is None:
  257. pass
  258. else:
  259. parser.print_help()
  260. raise _ArgumentError("User only provided one of password and username")
  261. return endpoints, remote_storetype, args.warn, opts
  262. def _create_epilog_from_format(format_, construct) -> Optional[str]:
  263. serializer_plugin_type: Type[ResultSerializer | Serializer]
  264. if construct:
  265. serializer_plugin_type = Serializer
  266. else:
  267. serializer_plugin_type = ResultSerializer
  268. try:
  269. plugin = get_plugin(format_, serializer_plugin_type)
  270. except PluginException:
  271. available_plugins = [x.name for x in get_plugins(None, ResultSerializer)]
  272. return (
  273. f"No plugin registered for sparql result in format '{format_}'. "
  274. f"available plugins: {available_plugins}"
  275. )
  276. serialize_method = plugin.serialize # type: ignore[attr-defined]
  277. module = inspect.getmodule(serialize_method)
  278. if module is None:
  279. return None
  280. pydoc_target = ".".join([module.__name__, serialize_method.__qualname__])
  281. sig = inspect.signature(serialize_method)
  282. available_keywords = [
  283. x
  284. for x, y in sig.parameters.items()
  285. if y.kind in [Parameter.KEYWORD_ONLY, Parameter.POSITIONAL_OR_KEYWORD]
  286. ]
  287. available_keywords.pop(0) # pop self
  288. available_keywords.pop(0) # pop stream
  289. if serializer_plugin_type == Serializer:
  290. available_keywords.pop(1) # pop encoding
  291. else:
  292. available_keywords.pop(0) # pop encoding
  293. epilog = (
  294. f"For more customization for format '{format_}' "
  295. f"use `pydoc {pydoc_target}`. "
  296. )
  297. if len(available_keywords) > 0:
  298. epilog += f"Known keywords are {available_keywords}."
  299. # there is always **kwargs in .serialize
  300. epilog += " Further keywords might be valid."
  301. return epilog
  302. def main():
  303. try:
  304. (
  305. endpoints,
  306. remote_storetype,
  307. warn,
  308. opts,
  309. ) = parse_args()
  310. except _PrintHelpError:
  311. exit()
  312. except (_ArgumentError, argparse.ArgumentError) as err:
  313. print(err, file=sys.stderr)
  314. exit(2)
  315. if warn:
  316. loglevel = logging.WARNING
  317. else:
  318. loglevel = logging.CRITICAL
  319. logging.basicConfig(level=loglevel)
  320. sparqlquery(
  321. endpoints,
  322. remote_storetype=remote_storetype,
  323. **opts,
  324. )
  325. if __name__ == "__main__":
  326. main()