jsonld.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. """
  2. This serialiser will output an RDF Graph as a JSON-LD formatted document. See http://json-ld.org/
  3. Example:
  4. ```python
  5. >>> from rdflib import Graph
  6. >>> testrdf = '''
  7. ... @prefix dc: <http://purl.org/dc/terms/> .
  8. ... <http://example.org/about>
  9. ... dc:title "Someone's Homepage"@en .
  10. ... '''
  11. >>> g = Graph().parse(data=testrdf, format='n3')
  12. >>> print(g.serialize(format='json-ld', indent=2))
  13. [
  14. {
  15. "@id": "http://example.org/about",
  16. "http://purl.org/dc/terms/title": [
  17. {
  18. "@language": "en",
  19. "@value": "Someone's Homepage"
  20. }
  21. ]
  22. }
  23. ]
  24. ```
  25. """
  26. # From: https://github.com/RDFLib/rdflib-jsonld/blob/feature/json-ld-1.1/rdflib_jsonld/serializer.py
  27. # NOTE: This code writes the entire JSON object into memory before serialising,
  28. # but we should consider streaming the output to deal with arbitrarily large
  29. # graphs.
  30. from __future__ import annotations
  31. import warnings
  32. from typing import IO, Any, Dict, List, Optional
  33. from rdflib.graph import DATASET_DEFAULT_GRAPH_ID, Graph, _ObjectType
  34. from rdflib.namespace import RDF, XSD
  35. from rdflib.serializer import Serializer
  36. from rdflib.term import BNode, IdentifiedNode, Identifier, Literal, URIRef
  37. from ..shared.jsonld.context import UNDEF, Context
  38. from ..shared.jsonld.keys import CONTEXT, GRAPH, ID, LANG, LIST, SET, VOCAB
  39. from ..shared.jsonld.util import _HAS_ORJSON, json, orjson
  40. __all__ = ["JsonLDSerializer", "from_rdf"]
  41. PLAIN_LITERAL_TYPES = {XSD.boolean, XSD.integer, XSD.double, XSD.string}
  42. class JsonLDSerializer(Serializer):
  43. """JSON-LD RDF graph serializer."""
  44. def __init__(self, store: Graph):
  45. super(JsonLDSerializer, self).__init__(store)
  46. def serialize(
  47. self,
  48. stream: IO[bytes],
  49. base: Optional[str] = None,
  50. encoding: Optional[str] = None,
  51. **kwargs: Any,
  52. ) -> None:
  53. # TODO: docstring w. args and return value
  54. encoding = encoding or "utf-8"
  55. if encoding not in ("utf-8", "utf-16"):
  56. warnings.warn(
  57. "JSON should be encoded as unicode. " f"Given encoding was: {encoding}"
  58. )
  59. context_data = kwargs.get("context")
  60. use_native_types = (kwargs.get("use_native_types", False),)
  61. use_rdf_type = kwargs.get("use_rdf_type", False)
  62. auto_compact = kwargs.get("auto_compact", False)
  63. indent = kwargs.get("indent", 2)
  64. separators = kwargs.get("separators", (",", ": "))
  65. sort_keys = kwargs.get("sort_keys", True)
  66. ensure_ascii = kwargs.get("ensure_ascii", False)
  67. obj = from_rdf(
  68. self.store,
  69. context_data,
  70. base,
  71. use_native_types,
  72. use_rdf_type,
  73. auto_compact=auto_compact,
  74. )
  75. if _HAS_ORJSON:
  76. option: int = orjson.OPT_NON_STR_KEYS
  77. if indent is not None:
  78. option |= orjson.OPT_INDENT_2
  79. if sort_keys:
  80. option |= orjson.OPT_SORT_KEYS
  81. if ensure_ascii:
  82. warnings.warn("Cannot use ensure_ascii with orjson")
  83. data_bytes = orjson.dumps(obj, option=option)
  84. stream.write(data_bytes)
  85. else:
  86. data = json.dumps(
  87. obj,
  88. indent=indent,
  89. separators=separators,
  90. sort_keys=sort_keys,
  91. ensure_ascii=ensure_ascii,
  92. )
  93. stream.write(data.encode(encoding, "replace"))
  94. def from_rdf(
  95. graph,
  96. context_data=None,
  97. base=None,
  98. use_native_types=False,
  99. use_rdf_type=False,
  100. auto_compact=False,
  101. startnode=None,
  102. index=False,
  103. ):
  104. # TODO: docstring w. args and return value
  105. # TODO: support for index and startnode
  106. if not context_data and auto_compact:
  107. context_data = dict(
  108. (pfx, str(ns))
  109. for (pfx, ns) in graph.namespaces()
  110. if pfx and str(ns) != "http://www.w3.org/XML/1998/namespace"
  111. )
  112. if isinstance(context_data, Context):
  113. context = context_data
  114. context_data = context.to_dict()
  115. else:
  116. context = Context(context_data, base=base)
  117. converter = Converter(context, use_native_types, use_rdf_type)
  118. result = converter.convert(graph)
  119. if converter.context.active:
  120. if isinstance(result, list):
  121. result = {context.get_key(GRAPH): result}
  122. result[CONTEXT] = context_data
  123. return result
  124. class Converter:
  125. def __init__(self, context: Context, use_native_types: bool, use_rdf_type: bool):
  126. self.context = context
  127. self.use_native_types = context.active or use_native_types
  128. self.use_rdf_type = use_rdf_type
  129. def convert(self, graph: Graph):
  130. # TODO: bug in rdflib dataset parsing (nquads et al):
  131. # plain triples end up in separate unnamed graphs (rdflib issue #436)
  132. if graph.context_aware:
  133. # type error: "Graph" has no attribute "contexts"
  134. all_contexts = list(graph.contexts()) # type: ignore[attr-defined]
  135. has_dataset_default_id = any(
  136. c.identifier == DATASET_DEFAULT_GRAPH_ID for c in all_contexts
  137. )
  138. if (
  139. has_dataset_default_id
  140. # # type error: "Graph" has no attribute "contexts"
  141. and graph.default_context.identifier == DATASET_DEFAULT_GRAPH_ID # type: ignore[attr-defined]
  142. ):
  143. default_graph = graph.default_context # type: ignore[attr-defined]
  144. else:
  145. default_graph = Graph()
  146. graphs = [default_graph]
  147. default_graph_id = default_graph.identifier
  148. for g in all_contexts:
  149. if g in graphs:
  150. continue
  151. if isinstance(g.identifier, URIRef):
  152. graphs.append(g)
  153. else:
  154. default_graph += g
  155. else:
  156. graphs = [graph]
  157. default_graph_id = graph.identifier
  158. context = self.context
  159. objs: List[Any] = []
  160. for g in graphs:
  161. obj = {}
  162. graphname = None
  163. if isinstance(g.identifier, URIRef):
  164. if g.identifier != default_graph_id:
  165. graphname = context.shrink_iri(g.identifier)
  166. obj[context.id_key] = graphname
  167. nodes = self.from_graph(g)
  168. if not graphname and len(nodes) == 1:
  169. obj.update(nodes[0])
  170. else:
  171. if not nodes:
  172. continue
  173. obj[context.graph_key] = nodes
  174. if objs and objs[0].get(context.get_key(ID)) == graphname:
  175. objs[0].update(obj)
  176. else:
  177. objs.append(obj)
  178. if len(graphs) == 1 and len(objs) == 1 and not self.context.active:
  179. default = objs[0]
  180. items = default.get(context.graph_key)
  181. if len(default) == 1 and items:
  182. objs = items
  183. elif len(objs) == 1 and self.context.active:
  184. objs = objs[0]
  185. return objs
  186. def from_graph(self, graph: Graph):
  187. nodemap: Dict[Any, Any] = {}
  188. for s in set(graph.subjects()):
  189. ## only iri:s and unreferenced (rest will be promoted to top if needed)
  190. if isinstance(s, URIRef) or (
  191. isinstance(s, BNode) and not any(graph.subjects(None, s))
  192. ):
  193. self.process_subject(graph, s, nodemap)
  194. return list(nodemap.values())
  195. def process_subject(self, graph: Graph, s: IdentifiedNode, nodemap):
  196. if isinstance(s, URIRef):
  197. node_id = self.context.shrink_iri(s)
  198. elif isinstance(s, BNode):
  199. node_id = s.n3()
  200. else:
  201. # This does not seem right, this probably should be an error.
  202. node_id = None
  203. # used_as_object = any(graph.subjects(None, s))
  204. if node_id in nodemap:
  205. return None
  206. node = {}
  207. node[self.context.id_key] = node_id
  208. nodemap[node_id] = node
  209. for p, o in graph.predicate_objects(s):
  210. # type error: Argument 3 to "add_to_node" of "Converter" has incompatible type "Node"; expected "IdentifiedNode"
  211. # type error: Argument 4 to "add_to_node" of "Converter" has incompatible type "Node"; expected "Identifier"
  212. self.add_to_node(graph, s, p, o, node, nodemap) # type: ignore[arg-type]
  213. return node
  214. def add_to_node(
  215. self,
  216. graph: Graph,
  217. s: IdentifiedNode,
  218. p: IdentifiedNode,
  219. o: Identifier,
  220. s_node: Dict[str, Any],
  221. nodemap,
  222. ):
  223. context = self.context
  224. if isinstance(o, Literal):
  225. datatype = str(o.datatype) if o.datatype else None
  226. language = o.language
  227. term = context.find_term(str(p), datatype, language=language)
  228. else:
  229. containers = [LIST, None] if graph.value(o, RDF.first) else [None]
  230. for container in containers:
  231. for coercion in (ID, VOCAB, UNDEF):
  232. # type error: Argument 2 to "find_term" of "Context" has incompatible type "object"; expected "Union[str, Defined, None]"
  233. # type error: Argument 3 to "find_term" of "Context" has incompatible type "Optional[str]"; expected "Union[Defined, str]"
  234. term = context.find_term(str(p), coercion, container) # type: ignore[arg-type]
  235. if term:
  236. break
  237. if term:
  238. break
  239. node = None
  240. use_set = not context.active
  241. if term:
  242. p_key = term.name
  243. if term.type:
  244. node = self.type_coerce(o, term.type)
  245. # type error: "Identifier" has no attribute "language"
  246. elif term.language and o.language == term.language: # type: ignore[attr-defined]
  247. node = str(o)
  248. # type error: Right operand of "and" is never evaluated
  249. elif context.language and (term.language is None and o.language is None): # type: ignore[unreachable]
  250. node = str(o) # type: ignore[unreachable]
  251. if LIST in term.container:
  252. node = [
  253. self.type_coerce(v, term.type)
  254. or self.to_raw_value(graph, s, v, nodemap)
  255. for v in self.to_collection(graph, o)
  256. ]
  257. elif LANG in term.container and language:
  258. value = s_node.setdefault(p_key, {})
  259. values = value.get(language)
  260. node = str(o)
  261. if values or SET in term.container:
  262. if not isinstance(values, list):
  263. value[language] = values = [values]
  264. values.append(node)
  265. else:
  266. value[language] = node
  267. return
  268. elif SET in term.container:
  269. use_set = True
  270. else:
  271. p_key = context.to_symbol(p)
  272. # TODO: for coercing curies - quite clumsy; unify to_symbol and find_term?
  273. key_term = context.terms.get(p_key)
  274. if key_term and (key_term.type or key_term.container):
  275. p_key = p
  276. if not term and p == RDF.type and not self.use_rdf_type:
  277. if isinstance(o, URIRef):
  278. node = context.to_symbol(o)
  279. p_key = context.type_key
  280. if node is None:
  281. node = self.to_raw_value(graph, s, o, nodemap)
  282. value = s_node.get(p_key)
  283. if value:
  284. if not isinstance(value, list):
  285. value = [value]
  286. value.append(node)
  287. elif use_set:
  288. value = [node]
  289. else:
  290. value = node
  291. s_node[p_key] = value
  292. def type_coerce(self, o: Identifier, coerce_type: str):
  293. if coerce_type == ID:
  294. if isinstance(o, URIRef):
  295. return self.context.shrink_iri(o)
  296. elif isinstance(o, BNode):
  297. return o.n3()
  298. else:
  299. return o
  300. elif coerce_type == VOCAB and isinstance(o, URIRef):
  301. return self.context.to_symbol(o)
  302. elif isinstance(o, Literal) and str(o.datatype) == coerce_type:
  303. return o
  304. else:
  305. return None
  306. def to_raw_value(
  307. self, graph: Graph, s: IdentifiedNode, o: Identifier, nodemap: Dict[str, Any]
  308. ):
  309. context = self.context
  310. coll = self.to_collection(graph, o)
  311. if coll is not None:
  312. coll = [
  313. self.to_raw_value(graph, s, lo, nodemap)
  314. for lo in self.to_collection(graph, o)
  315. ]
  316. return {context.list_key: coll}
  317. elif isinstance(o, BNode):
  318. embed = (
  319. False # TODO: self.context.active or using startnode and only one ref
  320. )
  321. onode = self.process_subject(graph, o, nodemap)
  322. if onode:
  323. if embed and not any(s2 for s2 in graph.subjects(None, o) if s2 != s):
  324. return onode
  325. else:
  326. nodemap[onode[context.id_key]] = onode
  327. return {context.id_key: o.n3()}
  328. elif isinstance(o, URIRef):
  329. # TODO: embed if o != startnode (else reverse)
  330. return {context.id_key: context.shrink_iri(o)}
  331. elif isinstance(o, Literal):
  332. # TODO: if compact
  333. native = self.use_native_types and o.datatype in PLAIN_LITERAL_TYPES
  334. if native:
  335. v = o.toPython()
  336. else:
  337. v = str(o)
  338. if o.datatype:
  339. if native and self.context.active:
  340. return v
  341. return {
  342. context.type_key: context.to_symbol(o.datatype),
  343. context.value_key: v,
  344. }
  345. elif o.language and o.language != context.language:
  346. return {context.lang_key: o.language, context.value_key: v}
  347. # type error: Right operand of "and" is never evaluated
  348. elif not context.active or context.language and not o.language: # type: ignore[unreachable]
  349. return {context.value_key: v}
  350. else:
  351. return v
  352. def to_collection(self, graph: Graph, l_: Identifier):
  353. if l_ != RDF.nil and not graph.value(l_, RDF.first):
  354. return None
  355. list_nodes: List[Optional[_ObjectType]] = []
  356. chain = set([l_])
  357. while l_:
  358. if l_ == RDF.nil:
  359. return list_nodes
  360. if isinstance(l_, URIRef):
  361. return None
  362. first, rest = None, None
  363. for p, o in graph.predicate_objects(l_):
  364. if not first and p == RDF.first:
  365. first = o
  366. elif not rest and p == RDF.rest:
  367. rest = o
  368. elif p != RDF.type or o != RDF.List:
  369. return None
  370. list_nodes.append(first)
  371. # type error: Incompatible types in assignment (expression has type "Optional[Node]", variable has type "Identifier")
  372. l_ = rest # type: ignore[assignment]
  373. if l_ in chain:
  374. return None
  375. chain.add(l_)