rdf4j-python 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -1,9 +1,41 @@
1
- from rdf4j_python import AsyncApiClient
1
+ from typing import Iterable, Optional
2
+
3
+ import httpx
4
+ import rdflib
5
+ import rdflib.resource
6
+ import rdflib.serializer
7
+ import rdflib.store
8
+
9
+ from rdf4j_python._client import AsyncApiClient
10
+ from rdf4j_python._driver._async_named_graph import AsyncNamedGraph
11
+ from rdf4j_python.exception.repo_exception import (
12
+ NamespaceException,
13
+ RepositoryInternalException,
14
+ RepositoryNotFoundException,
15
+ RepositoryUpdateException,
16
+ )
17
+ from rdf4j_python.model import Namespace, RDF4JDataSet
18
+ from rdf4j_python.model.term import (
19
+ Context,
20
+ Object,
21
+ Predicate,
22
+ RDFStatement,
23
+ Subject,
24
+ )
2
25
  from rdf4j_python.utils.const import Rdf4jContentType
26
+ from rdf4j_python.utils.helpers import serialize_statements
27
+
3
28
 
29
+ class AsyncRdf4JRepository:
30
+ """Asynchronous interface for interacting with an RDF4J repository."""
4
31
 
5
- class AsyncRepository:
6
32
  def __init__(self, client: AsyncApiClient, repository_id: str):
33
+ """Initializes the repository interface.
34
+
35
+ Args:
36
+ client (AsyncApiClient): The RDF4J API client.
37
+ repository_id (str): The ID of the RDF4J repository.
38
+ """
7
39
  self._client = client
8
40
  self._repository_id = repository_id
9
41
 
@@ -13,39 +45,338 @@ class AsyncRepository:
13
45
  infer: bool = True,
14
46
  accept: Rdf4jContentType = Rdf4jContentType.SPARQL_RESULTS_JSON,
15
47
  ):
48
+ """Executes a SPARQL SELECT query.
49
+
50
+ Args:
51
+ sparql_query (str): The SPARQL query string.
52
+ infer (bool): Whether to include inferred statements. Defaults to True.
53
+ accept (Rdf4jContentType): The expected response format.
54
+
55
+ Returns:
56
+ dict or str: Parsed JSON results or raw response text.
57
+ """
16
58
  path = f"/repositories/{self._repository_id}"
17
59
  params = {"query": sparql_query, "infer": str(infer).lower()}
18
60
  headers = {"Accept": accept.value}
19
61
  response = await self._client.get(path, params=params, headers=headers)
62
+ self._handle_repo_not_found_exception(response)
20
63
  if "json" in response.headers.get("Content-Type", ""):
21
64
  return response.json()
22
65
  return response.text
23
66
 
24
67
  async def update(self, sparql_update: str):
68
+ """Executes a SPARQL UPDATE command.
69
+
70
+ Args:
71
+ sparql_update (str): The SPARQL update string.
72
+
73
+ Raises:
74
+ RepositoryNotFoundException: If the repository doesn't exist.
75
+ httpx.HTTPStatusError: If the update fails.
76
+ """
25
77
  path = f"/repositories/{self._repository_id}/statements"
26
78
  headers = {"Content-Type": Rdf4jContentType.SPARQL_UPDATE.value}
27
79
  response = await self._client.post(path, data=sparql_update, headers=headers)
28
- response.raise_for_status()
29
-
30
- async def replace_statements(
31
- self, rdf_data: str, content_type: Rdf4jContentType = Rdf4jContentType.TURTLE
32
- ):
33
- path = f"/repositories/{self._repository_id}/statements"
34
- headers = {"Content-Type": content_type.value}
35
- response = await self._client.put(path, data=rdf_data, headers=headers)
80
+ self._handle_repo_not_found_exception(response)
36
81
  response.raise_for_status()
37
82
 
38
83
  async def get_namespaces(self):
84
+ """Retrieves all namespaces in the repository.
85
+
86
+ Returns:
87
+ list[Namespace]: A list of namespace objects.
88
+
89
+ Raises:
90
+ RepositoryNotFoundException: If the repository doesn't exist.
91
+ """
39
92
  path = f"/repositories/{self._repository_id}/namespaces"
40
- response = await self._client.get(path)
41
- if Rdf4jContentType.SPARQL_RESULTS_JSON in response.headers.get(
42
- "Content-Type", ""
43
- ):
44
- return response.json()
45
- return response.text
93
+ headers = {"Accept": Rdf4jContentType.SPARQL_RESULTS_JSON}
94
+ response = await self._client.get(path, headers=headers)
95
+ result = rdflib.query.Result.parse(
96
+ response, format=Rdf4jContentType.SPARQL_RESULTS_JSON
97
+ )
98
+ self._handle_repo_not_found_exception(response)
99
+ return [Namespace.from_rdflib_binding(binding) for binding in result.bindings]
46
100
 
47
101
  async def set_namespace(self, prefix: str, namespace: str):
102
+ """Sets a namespace prefix.
103
+
104
+ Args:
105
+ prefix (str): The namespace prefix.
106
+ namespace (str): The namespace URI.
107
+
108
+ Raises:
109
+ RepositoryNotFoundException: If the repository doesn't exist.
110
+ NamespaceException: If the request fails.
111
+ """
48
112
  path = f"/repositories/{self._repository_id}/namespaces/{prefix}"
49
113
  headers = {"Content-Type": Rdf4jContentType.NTRIPLES.value}
50
- response = await self._client.put(path, data=namespace, headers=headers)
114
+ response = await self._client.put(path, content=namespace, headers=headers)
115
+ self._handle_repo_not_found_exception(response)
116
+ if response.status_code != httpx.codes.NO_CONTENT:
117
+ raise NamespaceException(f"Failed to set namespace: {response.text}")
118
+
119
+ async def get_namespace(self, prefix: str) -> Namespace:
120
+ """Gets a namespace by its prefix.
121
+
122
+ Args:
123
+ prefix (str): The namespace prefix.
124
+
125
+ Returns:
126
+ Namespace: The namespace object.
127
+
128
+ Raises:
129
+ RepositoryNotFoundException: If the repository doesn't exist.
130
+ NamespaceException: If retrieval fails.
131
+ """
132
+ path = f"/repositories/{self._repository_id}/namespaces/{prefix}"
133
+ headers = {"Accept": Rdf4jContentType.NTRIPLES.value}
134
+ response = await self._client.get(path, headers=headers)
135
+ self._handle_repo_not_found_exception(response)
136
+
137
+ if response.status_code != httpx.codes.OK:
138
+ raise NamespaceException(f"Failed to get namespace: {response.text}")
139
+
140
+ return Namespace(prefix, response.text)
141
+
142
+ async def delete_namespace(self, prefix: str):
143
+ """Deletes a namespace by prefix.
144
+
145
+ Args:
146
+ prefix (str): The namespace prefix.
147
+
148
+ Raises:
149
+ RepositoryNotFoundException: If the repository doesn't exist.
150
+ httpx.HTTPStatusError: If deletion fails.
151
+ """
152
+ path = f"/repositories/{self._repository_id}/namespaces/{prefix}"
153
+ response = await self._client.delete(path)
154
+ self._handle_repo_not_found_exception(response)
51
155
  response.raise_for_status()
156
+
157
+ async def clear_all_namespaces(self):
158
+ """Removes all namespaces from the repository.
159
+
160
+ Raises:
161
+ RepositoryNotFoundException: If the repository doesn't exist.
162
+ httpx.HTTPStatusError: If clearing fails.
163
+ """
164
+ path = f"/repositories/{self._repository_id}/namespaces"
165
+ response = await self._client.delete(path)
166
+ self._handle_repo_not_found_exception(response)
167
+ response.raise_for_status()
168
+
169
+ async def size(self) -> int:
170
+ """Gets the number of statements in the repository.
171
+
172
+ Returns:
173
+ int: The total number of RDF statements.
174
+
175
+ Raises:
176
+ RepositoryNotFoundException: If the repository doesn't exist.
177
+ RepositoryInternalException: If retrieval fails.
178
+ """
179
+ path = f"/repositories/{self._repository_id}/size"
180
+ response = await self._client.get(path)
181
+ self._handle_repo_not_found_exception(response)
182
+
183
+ if response.status_code != httpx.codes.OK:
184
+ raise RepositoryInternalException(f"Failed to get size: {response.text}")
185
+
186
+ return int(response.text.strip())
187
+
188
+ async def get_statements(
189
+ self,
190
+ subject: Optional[Subject] = None,
191
+ predicate: Optional[Predicate] = None,
192
+ object_: Optional[Object] = None,
193
+ contexts: Optional[list[Context]] = None,
194
+ infer: bool = True,
195
+ ) -> RDF4JDataSet:
196
+ """Retrieves statements matching the given pattern.
197
+
198
+ Args:
199
+ subject (Optional[Subject]): Filter by subject.
200
+ predicate (Optional[Predicate]): Filter by predicate.
201
+ object_ (Optional[Object]): Filter by object.
202
+ contexts (Optional[list[Context]]): Filter by context (named graph).
203
+
204
+ Returns:
205
+ DataSet: Dataset of matching RDF statements.
206
+
207
+ Raises:
208
+ RepositoryNotFoundException: If the repository doesn't exist.
209
+ """
210
+ path = f"/repositories/{self._repository_id}/statements"
211
+ params = {}
212
+
213
+ if subject:
214
+ params["subj"] = subject.n3()
215
+ if predicate:
216
+ params["pred"] = predicate.n3()
217
+ if object_:
218
+ params["obj"] = object_.n3()
219
+ if contexts:
220
+ params["context"] = [ctx.n3() for ctx in contexts]
221
+ params["infer"] = str(infer).lower()
222
+
223
+ headers = {"Accept": Rdf4jContentType.NQUADS}
224
+ response = await self._client.get(path, params=params, headers=headers)
225
+ dataset = RDF4JDataSet()
226
+ dataset.parse(data=response.text, format="nquads")
227
+ return dataset
228
+
229
+ async def delete_statements(
230
+ self,
231
+ subject: Optional[Subject] = None,
232
+ predicate: Optional[Predicate] = None,
233
+ object_: Optional[Object] = None,
234
+ contexts: Optional[list[Context]] = None,
235
+ ):
236
+ """Deletes statements from the repository matching the given pattern.
237
+
238
+ Args:
239
+ subject (Optional[Subject]): Filter by subject (N-Triples encoded).
240
+ predicate (Optional[Predicate]): Filter by predicate (N-Triples encoded).
241
+ object_ (Optional[Object]): Filter by object (N-Triples encoded).
242
+ contexts (Optional[list[Context]]): One or more specific contexts to restrict deletion to.
243
+ Use 'null' as a string to delete context-less statements.
244
+
245
+ Raises:
246
+ RepositoryNotFoundException: If the repository does not exist.
247
+ RepositoryUpdateException: If the deletion fails.
248
+ """
249
+ path = f"/repositories/{self._repository_id}/statements"
250
+ params = {}
251
+
252
+ if subject:
253
+ params["subj"] = subject.n3()
254
+ if predicate:
255
+ params["pred"] = predicate.n3()
256
+ if object_:
257
+ params["obj"] = object_.n3()
258
+ if contexts:
259
+ params["context"] = [ctx.n3() for ctx in contexts]
260
+
261
+ response = await self._client.delete(path, params=params)
262
+ self._handle_repo_not_found_exception(response)
263
+
264
+ if response.status_code != httpx.codes.NO_CONTENT:
265
+ raise RepositoryUpdateException(
266
+ f"Failed to delete statements: {response.text}"
267
+ )
268
+
269
+ async def add_statement(
270
+ self,
271
+ subject: Subject,
272
+ predicate: Predicate,
273
+ object: Object,
274
+ context: Optional[Context] = None,
275
+ ):
276
+ """Adds a single RDF statement to the repository.
277
+
278
+ Args:
279
+ subject (Node): The subject of the triple.
280
+ predicate (Node): The predicate of the triple.
281
+ object (Node): The object of the triple.
282
+ context (IdentifiedNode): The context (named graph).
283
+
284
+ Raises:
285
+ RepositoryNotFoundException: If the repository doesn't exist.
286
+ httpx.HTTPStatusError: If addition fails.
287
+ """
288
+ path = f"/repositories/{self._repository_id}/statements"
289
+ response = await self._client.post(
290
+ path,
291
+ content=serialize_statements([(subject, predicate, object, context)]),
292
+ headers={"Content-Type": Rdf4jContentType.NQUADS},
293
+ )
294
+ self._handle_repo_not_found_exception(response)
295
+ if response.status_code != httpx.codes.NO_CONTENT:
296
+ raise RepositoryUpdateException(f"Failed to add statement: {response.text}")
297
+
298
+ async def add_statements(self, statements: Iterable[RDFStatement]):
299
+ """Adds a list of RDF statements to the repository.
300
+
301
+ Args:
302
+ statements (Iterable[RDFStatement]): A list of RDF statements.
303
+ RDFStatement: A tuple of subject, predicate, object, and context.
304
+
305
+ Raises:
306
+ RepositoryNotFoundException: If the repository doesn't exist.
307
+ httpx.HTTPStatusError: If addition fails.
308
+ """
309
+ path = f"/repositories/{self._repository_id}/statements"
310
+ response = await self._client.post(
311
+ path,
312
+ content=serialize_statements(statements),
313
+ headers={"Content-Type": Rdf4jContentType.NQUADS},
314
+ )
315
+ self._handle_repo_not_found_exception(response)
316
+ if response.status_code != httpx.codes.NO_CONTENT:
317
+ raise RepositoryUpdateException(
318
+ f"Failed to add statements: {response.text}"
319
+ )
320
+
321
+ async def replace_statements(
322
+ self,
323
+ statements: Iterable[RDFStatement],
324
+ contexts: Optional[list[Context]] = None,
325
+ base_uri: Optional[str] = None,
326
+ ):
327
+ """Replaces all repository statements with the given RDF data.
328
+
329
+ Args:
330
+ statements (Iterable[RDFStatement]): RDF statements to load.
331
+ contexts (Optional[list[Context]]): One or more specific contexts to restrict deletion to.
332
+
333
+ Raises:
334
+ RepositoryNotFoundException: If the repository doesn't exist.
335
+ httpx.HTTPStatusError: If the operation fails.
336
+ """
337
+ path = f"/repositories/{self._repository_id}/statements"
338
+ headers = {"Content-Type": Rdf4jContentType.NQUADS.value}
339
+
340
+ params = {}
341
+ if contexts:
342
+ params["context"] = [ctx.n3() for ctx in contexts]
343
+ if base_uri:
344
+ params["baseUri"] = base_uri
345
+
346
+ response = await self._client.put(
347
+ path,
348
+ content=serialize_statements(statements),
349
+ headers=headers,
350
+ params=params,
351
+ )
352
+ self._handle_repo_not_found_exception(response)
353
+ if response.status_code != httpx.codes.NO_CONTENT:
354
+ raise RepositoryUpdateException(
355
+ f"Failed to replace statements: {response.text}"
356
+ )
357
+
358
+ async def get_named_graph(self, graph: str) -> AsyncNamedGraph:
359
+ """Retrieves a named graph in the repository.
360
+
361
+ Returns:
362
+ AsyncNamedGraph: A named graph object.
363
+ """
364
+ return AsyncNamedGraph(self._client, self._repository_id, graph)
365
+
366
+ def _handle_repo_not_found_exception(self, response: httpx.Response):
367
+ """Raises a RepositoryNotFoundException if response is 404.
368
+
369
+ Args:
370
+ response (httpx.Response): HTTP response object.
371
+
372
+ Raises:
373
+ RepositoryNotFoundException: If repository is not found.
374
+ """
375
+ if response.status_code == httpx.codes.NOT_FOUND:
376
+ raise RepositoryNotFoundException(
377
+ f"Repository {self._repository_id} not found"
378
+ )
379
+
380
+ @property
381
+ def repository_id(self) -> str:
382
+ return self._repository_id
@@ -0,0 +1,5 @@
1
+ """
2
+ RDF4J Python Exception Module
3
+ """
4
+
5
+ from .repo_exception import * # noqa: F403
@@ -1,4 +1,34 @@
1
- class RepositoryCreationException(Exception): ...
1
+ class RepositoryCreationException(Exception):
2
+ """
3
+ Exception raised when a repository creation fails.
4
+ """
2
5
 
3
6
 
4
- class RepositoryDeletionException(Exception): ...
7
+ class RepositoryDeletionException(Exception):
8
+ """
9
+ Exception raised when a repository deletion fails.
10
+ """
11
+
12
+
13
+ class NamespaceException(Exception):
14
+ """
15
+ Exception raised when a namespace operation fails.
16
+ """
17
+
18
+
19
+ class RepositoryNotFoundException(Exception):
20
+ """
21
+ Exception raised when a repository is not found.
22
+ """
23
+
24
+
25
+ class RepositoryInternalException(Exception):
26
+ """
27
+ Exception raised when a repository internal error occurs.
28
+ """
29
+
30
+
31
+ class RepositoryUpdateException(Exception):
32
+ """
33
+ Exception raised when a repository update fails.
34
+ """
@@ -0,0 +1,13 @@
1
+ """
2
+ RDF4J Python Model Module
3
+ """
4
+
5
+ from ._dataset import RDF4JDataSet
6
+ from ._namespace import Namespace
7
+ from ._repository_info import RepositoryMetadata
8
+
9
+ __all__ = [
10
+ "Namespace",
11
+ "RepositoryMetadata",
12
+ "RDF4JDataSet",
13
+ ]
@@ -5,13 +5,25 @@ from rdflib.term import Identifier, Literal, URIRef, Variable
5
5
 
6
6
 
7
7
  class _BaseModel(ABC):
8
+ """Abstract base class providing utility methods for parsing RDF query results."""
9
+
8
10
  @staticmethod
9
11
  def get_literal(
10
12
  result: Mapping[Variable, Identifier],
11
13
  var_name: str,
12
14
  default: Optional[str] = None,
13
- ):
14
- """Extract and convert an RDFLib Literal to a Python value."""
15
+ ) -> Optional[str]:
16
+ """
17
+ Extracts a literal value from a SPARQL query result.
18
+
19
+ Args:
20
+ result (Mapping[Variable, Identifier]): A mapping of variable bindings from a query result.
21
+ var_name (str): The variable name to extract.
22
+ default (Optional[str], optional): The value to return if the variable is not found or is not a Literal. Defaults to None.
23
+
24
+ Returns:
25
+ Optional[str]: The Python representation of the literal, or the default value.
26
+ """
15
27
  val = result.get(Variable(var_name))
16
28
  return val.toPython() if isinstance(val, Literal) else default
17
29
 
@@ -20,7 +32,17 @@ class _BaseModel(ABC):
20
32
  result: Mapping[Variable, Identifier],
21
33
  var_name: str,
22
34
  default: Optional[str] = None,
23
- ):
24
- """Extract and convert an RDFLib URIRef to a string."""
35
+ ) -> Optional[str]:
36
+ """
37
+ Extracts a URI value from a SPARQL query result.
38
+
39
+ Args:
40
+ result (Mapping[Variable, Identifier]): A mapping of variable bindings from a query result.
41
+ var_name (str): The variable name to extract.
42
+ default (Optional[str], optional): The value to return if the variable is not found or is not a URIRef. Defaults to None.
43
+
44
+ Returns:
45
+ Optional[str]: The URI string, or the default value.
46
+ """
25
47
  val = result.get(Variable(var_name))
26
48
  return str(val) if isinstance(val, URIRef) else default
@@ -0,0 +1,38 @@
1
+ from rdflib import Dataset as _Dataset
2
+
3
+ from rdf4j_python.model.term import IRI
4
+
5
+
6
+ class RDF4JDataSet(_Dataset):
7
+ """
8
+ An RDFLib Dataset subclass with RDF4J-specific utility methods.
9
+ """
10
+
11
+ def as_list(self) -> list[tuple]:
12
+ """
13
+ Converts all quads in the dataset to a list of 4-tuples.
14
+
15
+ Replaces the RDF4J default context IRI ("urn:x-rdflib:default") with None.
16
+
17
+ Returns:
18
+ list[tuple]: A list of (subject, predicate, object, context) quads.
19
+ """
20
+ return [
21
+ (s, p, o, ctx if ctx != IRI("urn:x-rdflib:default") else None)
22
+ for s, p, o, ctx in self.quads((None, None, None, None))
23
+ ]
24
+
25
+ @staticmethod
26
+ def from_raw_text(text: str) -> "RDF4JDataSet":
27
+ """
28
+ Parses a string of N-Quads RDF data into an RDF4JDataSet.
29
+
30
+ Args:
31
+ text (str): The RDF data in N-Quads format.
32
+
33
+ Returns:
34
+ RDF4JDataSet: A populated dataset.
35
+ """
36
+ ds = RDF4JDataSet()
37
+ ds.parse(data=text, format="nquads")
38
+ return ds
@@ -0,0 +1,134 @@
1
+ from typing import Mapping
2
+
3
+ from rdflib.namespace import Namespace as RdflibNamespace
4
+ from rdflib.term import Identifier, Variable
5
+
6
+ from rdf4j_python.model.term import IRI
7
+
8
+ from ._base_model import _BaseModel
9
+
10
+
11
+ class Namespace:
12
+ """
13
+ Represents a namespace in RDF4J.
14
+ """
15
+
16
+ _prefix: str
17
+ _namespace: RdflibNamespace
18
+
19
+ def __init__(self, prefix: str, namespace: str):
20
+ """
21
+ Initializes a new Namespace.
22
+
23
+ Args:
24
+ prefix (str): The prefix of the namespace.
25
+ namespace (str): The namespace URI.
26
+ """
27
+ self._prefix = prefix
28
+ self._namespace = RdflibNamespace(namespace)
29
+
30
+ @classmethod
31
+ def from_rdflib_binding(cls, binding: Mapping[Variable, Identifier]) -> "Namespace":
32
+ """
33
+ Creates a Namespace from a RDFlib binding.
34
+
35
+ Args:
36
+ binding (Mapping[Variable, Identifier]): The RDFlib binding.
37
+
38
+ Returns:
39
+ Namespace: The created Namespace.
40
+ """
41
+ prefix = _BaseModel.get_literal(binding, "prefix", "")
42
+ namespace = _BaseModel.get_literal(binding, "namespace", "")
43
+ return cls(
44
+ prefix=prefix,
45
+ namespace=namespace,
46
+ )
47
+
48
+ def __str__(self):
49
+ """
50
+ Returns a string representation of the Namespace.
51
+
52
+ Returns:
53
+ str: A string representation of the Namespace.
54
+ """
55
+ return f"{self._prefix}: {self._namespace}"
56
+
57
+ def __repr__(self):
58
+ """
59
+ Returns a string representation of the Namespace.
60
+
61
+ Returns:
62
+ str: A string representation of the Namespace.
63
+ """
64
+ return f"Namespace(prefix={self._prefix}, namespace={self._namespace})"
65
+
66
+ def __contains__(self, item: str) -> bool:
67
+ """
68
+ Checks if the Namespace contains a given item.
69
+
70
+ Args:
71
+ item (str): The item to check.
72
+
73
+ Returns:
74
+ bool: True if the Namespace contains the item, False otherwise.
75
+ """
76
+ return item in self._namespace
77
+
78
+ def term(self, name: str) -> IRI:
79
+ """
80
+ Returns the IRI for a given term.
81
+
82
+ Args:
83
+ name (str): The term name.
84
+
85
+ Returns:
86
+ IRI: The IRI for the term.
87
+ """
88
+ return IRI(self._namespace.term(name))
89
+
90
+ def __getitem__(self, item: str) -> IRI:
91
+ """
92
+ Returns the IRI for a given term.
93
+
94
+ Args:
95
+ item (str): The term name.
96
+
97
+ Returns:
98
+ IRI: The IRI for the term.
99
+ """
100
+ return self.term(item)
101
+
102
+ def __getattr__(self, item: str) -> IRI:
103
+ """
104
+ Returns the IRI for a given term.
105
+
106
+ Args:
107
+ item (str): The term name.
108
+
109
+ Returns:
110
+ IRI: The IRI for the term.
111
+ """
112
+ if item.startswith("__"):
113
+ raise AttributeError
114
+ return self.term(item)
115
+
116
+ @property
117
+ def namespace(self) -> IRI:
118
+ """
119
+ Returns the namespace URI.
120
+
121
+ Returns:
122
+ IRI: The namespace URI.
123
+ """
124
+ return IRI(self._namespace)
125
+
126
+ @property
127
+ def prefix(self) -> str:
128
+ """
129
+ Returns the prefix of the namespace.
130
+
131
+ Returns:
132
+ str: The prefix of the namespace.
133
+ """
134
+ return self._prefix