rdf4j-python 0.1.5__py3-none-any.whl → 0.2.0__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.
rdf4j_python/__init__.py CHANGED
@@ -2,13 +2,80 @@
2
2
  RDF4J Python is a Python library for interacting with RDF4J repositories.
3
3
  """
4
4
 
5
- from ._driver import AsyncNamedGraph, AsyncRdf4j, AsyncRdf4JRepository
6
- from .exception import * # noqa: F403
7
- from .model import * # noqa: F403
8
- from .utils import * # noqa: F403
5
+ from ._driver import (
6
+ AsyncNamedGraph,
7
+ AsyncRdf4j,
8
+ AsyncRdf4JRepository,
9
+ AsyncTransaction,
10
+ IsolationLevel,
11
+ TransactionState,
12
+ )
13
+ from .exception import (
14
+ NamespaceException,
15
+ NetworkError,
16
+ QueryError,
17
+ Rdf4jError,
18
+ RepositoryCreationException,
19
+ RepositoryDeletionException,
20
+ RepositoryError,
21
+ RepositoryInternalException,
22
+ RepositoryNotFoundException,
23
+ RepositoryUpdateException,
24
+ TransactionError,
25
+ TransactionStateError,
26
+ )
27
+ from .model import (
28
+ IRI,
29
+ BlankNode,
30
+ Context,
31
+ DefaultGraph,
32
+ Literal,
33
+ Namespace,
34
+ Object,
35
+ Predicate,
36
+ Quad,
37
+ QuadResultSet,
38
+ RepositoryMetadata,
39
+ Subject,
40
+ Triple,
41
+ Variable,
42
+ )
9
43
 
10
44
  __all__ = [
45
+ # Main classes
11
46
  "AsyncRdf4j",
12
47
  "AsyncRdf4JRepository",
13
48
  "AsyncNamedGraph",
49
+ # Transaction
50
+ "AsyncTransaction",
51
+ "IsolationLevel",
52
+ "TransactionState",
53
+ # Exceptions
54
+ "Rdf4jError",
55
+ "RepositoryError",
56
+ "RepositoryCreationException",
57
+ "RepositoryDeletionException",
58
+ "RepositoryNotFoundException",
59
+ "RepositoryInternalException",
60
+ "RepositoryUpdateException",
61
+ "NamespaceException",
62
+ "NetworkError",
63
+ "QueryError",
64
+ "TransactionError",
65
+ "TransactionStateError",
66
+ # Model types
67
+ "Namespace",
68
+ "RepositoryMetadata",
69
+ "IRI",
70
+ "BlankNode",
71
+ "Literal",
72
+ "DefaultGraph",
73
+ "Variable",
74
+ "Quad",
75
+ "Triple",
76
+ "Subject",
77
+ "Predicate",
78
+ "Object",
79
+ "Context",
80
+ "QuadResultSet",
14
81
  ]
@@ -1,7 +1,11 @@
1
- from typing import Any, Dict, Optional
1
+ import logging
2
+ from types import TracebackType
3
+ from typing import Any, Self
2
4
 
3
5
  import httpx
4
6
 
7
+ logger = logging.getLogger("rdf4j_python")
8
+
5
9
 
6
10
  class BaseClient:
7
11
  """Base HTTP client that provides shared URL building functionality."""
@@ -42,18 +46,20 @@ class BaseClient:
42
46
  class SyncApiClient(BaseClient):
43
47
  """Synchronous API client using httpx.Client."""
44
48
 
45
- def __init__(self, base_url: str, timeout: int = 10):
49
+ def __init__(self, base_url: str, timeout: int = 10, retries: int = 3) -> None:
46
50
  """
47
51
  Initializes the SyncApiClient.
48
52
 
49
53
  Args:
50
54
  base_url (str): The base URL for the API endpoints.
51
55
  timeout (int, optional): Request timeout in seconds. Defaults to 10.
56
+ retries (int, optional): Number of retries for failed requests. Defaults to 3.
52
57
  """
53
58
  super().__init__(base_url, timeout)
54
- self.client = httpx.Client(timeout=self.timeout)
59
+ transport = httpx.HTTPTransport(retries=retries)
60
+ self.client = httpx.Client(timeout=self.timeout, transport=transport)
55
61
 
56
- def __enter__(self):
62
+ def __enter__(self) -> Self:
57
63
  """
58
64
  Enters the context and initializes the HTTP client.
59
65
 
@@ -63,7 +69,12 @@ class SyncApiClient(BaseClient):
63
69
  self.client.__enter__()
64
70
  return self
65
71
 
66
- def __exit__(self, exc_type, exc_value, traceback):
72
+ def __exit__(
73
+ self,
74
+ exc_type: type[BaseException] | None,
75
+ exc_value: BaseException | None,
76
+ traceback: TracebackType | None,
77
+ ) -> None:
67
78
  """
68
79
  Exits the context and closes the HTTP client.
69
80
  """
@@ -72,99 +83,129 @@ class SyncApiClient(BaseClient):
72
83
  def get(
73
84
  self,
74
85
  path: str,
75
- params: Optional[Dict[str, Any]] = None,
76
- headers: Optional[Dict[str, str]] = None,
86
+ params: dict[str, Any] | None = None,
87
+ headers: dict[str, str] | None = None,
77
88
  ) -> httpx.Response:
78
89
  """
79
90
  Sends a GET request.
80
91
 
81
92
  Args:
82
93
  path (str): API endpoint path.
83
- params (Optional[Dict[str, Any]]): Query parameters.
84
- headers (Optional[Dict[str, str]]): Request headers.
94
+ params (dict[str, Any] | None): Query parameters.
95
+ headers (dict[str, str] | None): Request headers.
85
96
 
86
97
  Returns:
87
98
  httpx.Response: The HTTP response.
88
99
  """
89
- return self.client.get(self._build_url(path), params=params, headers=headers)
100
+ url = self._build_url(path)
101
+ logger.debug("GET %s params=%s", url, params)
102
+ response = self.client.get(url, params=params, headers=headers)
103
+ logger.debug("Response %s: %s bytes", response.status_code, len(response.content))
104
+ return response
90
105
 
91
106
  def post(
92
107
  self,
93
108
  path: str,
94
- data: Optional[Dict[str, Any]] = None,
95
- json: Optional[Any] = None,
96
- headers: Optional[Dict[str, str]] = None,
109
+ content: str | bytes | None = None,
110
+ json: Any | None = None,
111
+ headers: dict[str, str] | None = None,
112
+ params: dict[str, Any] | None = None,
97
113
  ) -> httpx.Response:
98
114
  """
99
115
  Sends a POST request.
100
116
 
101
117
  Args:
102
118
  path (str): API endpoint path.
103
- data (Optional[Dict[str, Any]]): Form-encoded body data.
104
- json (Optional[Any]): JSON-encoded body data.
105
- headers (Optional[Dict[str, str]]): Request headers.
119
+ content (str | bytes | None): Raw content to include in the request body.
120
+ json (Any | None): JSON-encoded body data.
121
+ headers (dict[str, str] | None): Request headers.
122
+ params (dict[str, Any] | None): Query parameters.
106
123
 
107
124
  Returns:
108
125
  httpx.Response: The HTTP response.
109
126
  """
110
- return self.client.post(
111
- self._build_url(path), data=data, json=json, headers=headers
127
+ url = self._build_url(path)
128
+ logger.debug("POST %s params=%s", url, params)
129
+ response = self.client.post(
130
+ url, content=content, json=json, headers=headers, params=params
112
131
  )
132
+ logger.debug("Response %s: %s bytes", response.status_code, len(response.content))
133
+ return response
113
134
 
114
135
  def put(
115
136
  self,
116
137
  path: str,
117
- data: Optional[Dict[str, Any]] = None,
118
- json: Optional[Any] = None,
119
- headers: Optional[Dict[str, str]] = None,
138
+ content: str | bytes | None = None,
139
+ params: dict[str, Any] | None = None,
140
+ json: Any | None = None,
141
+ headers: dict[str, str] | None = None,
120
142
  ) -> httpx.Response:
121
143
  """
122
144
  Sends a PUT request.
123
145
 
124
146
  Args:
125
147
  path (str): API endpoint path.
126
- data (Optional[Dict[str, Any]]): Form-encoded body data.
127
- json (Optional[Any]): JSON-encoded body data.
128
- headers (Optional[Dict[str, str]]): Request headers.
148
+ content (str | bytes | None): Raw content to include in the request body.
149
+ params (dict[str, Any] | None): Query parameters.
150
+ json (Any | None): JSON-encoded body data.
151
+ headers (dict[str, str] | None): Request headers.
129
152
 
130
153
  Returns:
131
154
  httpx.Response: The HTTP response.
132
155
  """
133
- return self.client.put(
134
- self._build_url(path), data=data, json=json, headers=headers
156
+ url = self._build_url(path)
157
+ logger.debug("PUT %s params=%s", url, params)
158
+ response = self.client.put(
159
+ url,
160
+ content=content,
161
+ json=json,
162
+ headers=headers,
163
+ params=params,
135
164
  )
165
+ logger.debug("Response %s: %s bytes", response.status_code, len(response.content))
166
+ return response
136
167
 
137
168
  def delete(
138
- self, path: str, headers: Optional[Dict[str, str]] = None
169
+ self,
170
+ path: str,
171
+ params: dict[str, Any] | None = None,
172
+ headers: dict[str, str] | None = None,
139
173
  ) -> httpx.Response:
140
174
  """
141
175
  Sends a DELETE request.
142
176
 
143
177
  Args:
144
178
  path (str): API endpoint path.
145
- headers (Optional[Dict[str, str]]): Request headers.
179
+ params (dict[str, Any] | None): Query parameters.
180
+ headers (dict[str, str] | None): Request headers.
146
181
 
147
182
  Returns:
148
183
  httpx.Response: The HTTP response.
149
184
  """
150
- return self.client.delete(self._build_url(path), headers=headers)
185
+ url = self._build_url(path)
186
+ logger.debug("DELETE %s params=%s", url, params)
187
+ response = self.client.delete(url, params=params, headers=headers)
188
+ logger.debug("Response %s: %s bytes", response.status_code, len(response.content))
189
+ return response
151
190
 
152
191
 
153
192
  class AsyncApiClient(BaseClient):
154
193
  """Asynchronous API client using httpx.AsyncClient."""
155
194
 
156
- def __init__(self, base_url: str, timeout: int = 10):
195
+ def __init__(self, base_url: str, timeout: int = 10, retries: int = 3) -> None:
157
196
  """
158
197
  Initializes the AsyncApiClient.
159
198
 
160
199
  Args:
161
200
  base_url (str): The base URL for the API endpoints.
162
201
  timeout (int, optional): Request timeout in seconds. Defaults to 10.
202
+ retries (int, optional): Number of retries for failed requests. Defaults to 3.
163
203
  """
164
204
  super().__init__(base_url, timeout)
165
- self.client = httpx.AsyncClient(timeout=self.timeout)
205
+ transport = httpx.AsyncHTTPTransport(retries=retries)
206
+ self.client = httpx.AsyncClient(timeout=self.timeout, transport=transport)
166
207
 
167
- async def __aenter__(self):
208
+ async def __aenter__(self) -> Self:
168
209
  """
169
210
  Enters the async context and initializes the HTTP client.
170
211
 
@@ -174,7 +215,12 @@ class AsyncApiClient(BaseClient):
174
215
  await self.client.__aenter__()
175
216
  return self
176
217
 
177
- async def __aexit__(self, exc_type, exc_value, traceback):
218
+ async def __aexit__(
219
+ self,
220
+ exc_type: type[BaseException] | None,
221
+ exc_value: BaseException | None,
222
+ traceback: TracebackType | None,
223
+ ) -> None:
178
224
  """
179
225
  Exits the async context and closes the HTTP client.
180
226
  """
@@ -183,93 +229,114 @@ class AsyncApiClient(BaseClient):
183
229
  async def get(
184
230
  self,
185
231
  path: str,
186
- params: Optional[Dict[str, Any]] = None,
187
- headers: Optional[Dict[str, str]] = None,
232
+ params: dict[str, Any] | None = None,
233
+ headers: dict[str, str] | None = None,
188
234
  ) -> httpx.Response:
189
235
  """
190
236
  Sends an asynchronous GET request.
191
237
 
192
238
  Args:
193
239
  path (str): API endpoint path.
194
- params (Optional[Dict[str, Any]]): Query parameters.
195
- headers (Optional[Dict[str, str]]): Request headers.
240
+ params (dict[str, Any] | None): Query parameters.
241
+ headers (dict[str, str] | None): Request headers.
196
242
 
197
243
  Returns:
198
244
  httpx.Response: The HTTP response.
199
245
  """
200
- return await self.client.get(
201
- self._build_url(path), params=params, headers=headers
202
- )
246
+ url = self._build_url(path)
247
+ logger.debug("GET %s params=%s", url, params)
248
+ response = await self.client.get(url, params=params, headers=headers)
249
+ logger.debug("Response %s: %s bytes", response.status_code, len(response.content))
250
+ return response
203
251
 
204
252
  async def post(
205
253
  self,
206
254
  path: str,
207
- content: Optional[str] = None,
208
- json: Optional[Any] = None,
209
- headers: Optional[Dict[str, str]] = None,
255
+ content: str | bytes | None = None,
256
+ json: Any | None = None,
257
+ headers: dict[str, str] | None = None,
258
+ params: dict[str, Any] | None = None,
210
259
  ) -> httpx.Response:
211
260
  """
212
261
  Sends an asynchronous POST request.
213
262
 
214
263
  Args:
215
264
  path (str): API endpoint path.
216
- content (Optional[str]): Raw string content to include in the request body.
217
- json (Optional[Any]): JSON-encoded body data.
218
- headers (Optional[Dict[str, str]]): Request headers.
265
+ content (str | bytes | None): Raw content to include in the request body.
266
+ json (Any | None): JSON-encoded body data.
267
+ headers (dict[str, str] | None): Request headers.
268
+ params (dict[str, Any] | None): Query parameters.
219
269
 
220
270
  Returns:
221
271
  httpx.Response: The HTTP response.
222
272
  """
223
- return await self.client.post(
224
- self._build_url(path), content=content, json=json, headers=headers
273
+ url = self._build_url(path)
274
+ logger.debug("POST %s params=%s", url, params)
275
+ response = await self.client.post(
276
+ url, content=content, json=json, headers=headers, params=params
225
277
  )
278
+ logger.debug("Response %s: %s bytes", response.status_code, len(response.content))
279
+ return response
226
280
 
227
281
  async def put(
228
282
  self,
229
283
  path: str,
230
- content: Optional[bytes] = None,
231
- params: Optional[Dict[str, Any]] = None,
232
- json: Optional[Any] = None,
233
- headers: Optional[Dict[str, str]] = None,
284
+ content: str | bytes | None = None,
285
+ params: dict[str, Any] | None = None,
286
+ json: Any | None = None,
287
+ headers: dict[str, str] | None = None,
234
288
  ) -> httpx.Response:
235
289
  """
236
290
  Sends an asynchronous PUT request.
237
291
 
238
292
  Args:
239
293
  path (str): API endpoint path.
240
- content (Optional[bytes]): Raw bytes to include in the request body.
241
- params (Optional[Dict[str, Any]]): Query parameters.
242
- json (Optional[Any]): JSON-encoded body data.
243
- headers (Optional[Dict[str, str]]): Request headers.
294
+ content (str | bytes | None): Raw content to include in the request body.
295
+ params (dict[str, Any] | None): Query parameters.
296
+ json (Any | None): JSON-encoded body data.
297
+ headers (dict[str, str] | None): Request headers.
244
298
 
245
299
  Returns:
246
300
  httpx.Response: The HTTP response.
247
301
  """
248
- return await self.client.put(
249
- self._build_url(path),
302
+ url = self._build_url(path)
303
+ logger.debug("PUT %s params=%s", url, params)
304
+ response = await self.client.put(
305
+ url,
250
306
  content=content,
251
307
  json=json,
252
308
  headers=headers,
253
309
  params=params,
254
310
  )
311
+ logger.debug("Response %s: %s bytes", response.status_code, len(response.content))
312
+ return response
255
313
 
256
314
  async def delete(
257
315
  self,
258
316
  path: str,
259
- params: Optional[Dict[str, Any]] = None,
260
- headers: Optional[Dict[str, str]] = None,
317
+ params: dict[str, Any] | None = None,
318
+ headers: dict[str, str] | None = None,
261
319
  ) -> httpx.Response:
262
320
  """
263
321
  Sends an asynchronous DELETE request.
264
322
 
265
323
  Args:
266
324
  path (str): API endpoint path.
267
- params (Optional[Dict[str, Any]]): Query parameters.
268
- headers (Optional[Dict[str, str]]): Request headers.
325
+ params (dict[str, Any] | None): Query parameters.
326
+ headers (dict[str, str] | None): Request headers.
269
327
 
270
328
  Returns:
271
329
  httpx.Response: The HTTP response.
272
330
  """
273
- return await self.client.delete(
274
- self._build_url(path), params=params, headers=headers
275
- )
331
+ url = self._build_url(path)
332
+ logger.debug("DELETE %s params=%s", url, params)
333
+ response = await self.client.delete(url, params=params, headers=headers)
334
+ logger.debug("Response %s: %s bytes", response.status_code, len(response.content))
335
+ return response
336
+
337
+ async def aclose(self) -> None:
338
+ """
339
+ Asynchronously closes the client connection.
340
+ """
341
+ logger.debug("Closing async client connection")
342
+ await self.client.aclose()
@@ -1,9 +1,13 @@
1
1
  from ._async_named_graph import AsyncNamedGraph
2
2
  from ._async_rdf4j_db import AsyncRdf4j
3
3
  from ._async_repository import AsyncRdf4JRepository
4
+ from ._async_transaction import AsyncTransaction, IsolationLevel, TransactionState
4
5
 
5
6
  __all__ = [
6
7
  "AsyncRdf4j",
7
8
  "AsyncRdf4JRepository",
8
9
  "AsyncNamedGraph",
10
+ "AsyncTransaction",
11
+ "IsolationLevel",
12
+ "TransactionState",
9
13
  ]
@@ -1,3 +1,6 @@
1
+ from types import TracebackType
2
+ from typing import Self
3
+
1
4
  import httpx
2
5
  import pyoxigraph as og
3
6
 
@@ -26,17 +29,23 @@ class AsyncRdf4j:
26
29
  base_url (str): Base URL of the RDF4J server.
27
30
  """
28
31
  self._base_url = base_url.rstrip("/")
32
+ self._client = AsyncApiClient(base_url=self._base_url)
29
33
 
30
- async def __aenter__(self):
34
+ async def __aenter__(self) -> Self:
31
35
  """Enters the async context and initializes the HTTP client.
32
36
 
33
37
  Returns:
34
38
  AsyncRdf4j: The initialized RDF4J interface.
35
39
  """
36
- self._client = await AsyncApiClient(base_url=self._base_url).__aenter__()
40
+ await self._client.__aenter__()
37
41
  return self
38
42
 
39
- async def __aexit__(self, exc_type, exc_value, traceback):
43
+ async def __aexit__(
44
+ self,
45
+ exc_type: type[BaseException] | None,
46
+ exc_value: BaseException | None,
47
+ traceback: TracebackType | None,
48
+ ) -> None:
40
49
  """Closes the HTTP client when exiting the async context."""
41
50
  await self._client.__aexit__(exc_type, exc_value, traceback)
42
51
 
@@ -69,7 +78,10 @@ class AsyncRdf4j:
69
78
  query_solutions = og.parse_query_results(
70
79
  response.text, format=og.QueryResultsFormat.JSON
71
80
  )
72
- assert isinstance(query_solutions, og.QuerySolutions)
81
+ if not isinstance(query_solutions, og.QuerySolutions):
82
+ raise TypeError(
83
+ f"Expected QuerySolutions, got {type(query_solutions).__name__}"
84
+ )
73
85
  return [
74
86
  RepositoryMetadata.from_sparql_query_solution(query_solution)
75
87
  for query_solution in query_solutions
@@ -103,7 +115,7 @@ class AsyncRdf4j:
103
115
  RepositoryCreationException: If repository creation fails.
104
116
  """
105
117
  path = f"/repositories/{config.repo_id}"
106
- headers = {"Content-Type": Rdf4jContentType.TURTLE}
118
+ headers = {"Content-Type": Rdf4jContentType.TURTLE.value}
107
119
  response: httpx.Response = await self._client.put(
108
120
  path, content=config.to_turtle(), headers=headers
109
121
  )
@@ -113,7 +125,7 @@ class AsyncRdf4j:
113
125
  )
114
126
  return AsyncRdf4JRepository(self._client, config.repo_id)
115
127
 
116
- async def delete_repository(self, repository_id: str):
128
+ async def delete_repository(self, repository_id: str) -> None:
117
129
  """Deletes a repository and all its data and configuration.
118
130
 
119
131
  Args:
@@ -128,3 +140,30 @@ class AsyncRdf4j:
128
140
  raise RepositoryDeletionException(
129
141
  f"Failed to delete repository '{repository_id}': {response.status_code} - {response.text}"
130
142
  )
143
+
144
+ async def health_check(self) -> bool:
145
+ """Checks if the RDF4J server is reachable and healthy.
146
+
147
+ This method attempts to fetch the protocol version from the server
148
+ to verify connectivity.
149
+
150
+ Returns:
151
+ bool: True if the server is reachable and responds correctly,
152
+ False otherwise.
153
+
154
+ Example:
155
+ >>> async with AsyncRdf4j("http://localhost:8080/rdf4j-server") as db:
156
+ ... if await db.health_check():
157
+ ... print("Server is healthy")
158
+ ... else:
159
+ ... print("Server is not reachable")
160
+ """
161
+ try:
162
+ response = await self._client.get("/protocol")
163
+ return response.status_code == httpx.codes.OK
164
+ except Exception:
165
+ return False
166
+
167
+ async def aclose(self) -> None:
168
+ """Asynchronously closes the client connection."""
169
+ await self._client.aclose()