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