rdf4j-python 0.1.2__tar.gz → 0.1.4__tar.gz

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.
Files changed (40) hide show
  1. {rdf4j_python-0.1.2/rdf4j_python.egg-info → rdf4j_python-0.1.4}/PKG-INFO +5 -2
  2. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/README.md +1 -0
  3. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/pyproject.toml +7 -6
  4. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/_driver/_async_named_graph.py +8 -7
  5. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/_driver/_async_rdf4j_db.py +6 -5
  6. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/_driver/_async_repository.py +90 -50
  7. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/model/__init__.py +0 -2
  8. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/model/_namespace.py +38 -15
  9. rdf4j_python-0.1.4/rdf4j_python/model/_repository_info.py +62 -0
  10. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/model/repository_config.py +111 -58
  11. rdf4j_python-0.1.4/rdf4j_python/model/term.py +20 -0
  12. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/model/vocabulary.py +1 -0
  13. rdf4j_python-0.1.4/rdf4j_python/utils/helpers.py +20 -0
  14. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4/rdf4j_python.egg-info}/PKG-INFO +5 -2
  15. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python.egg-info/SOURCES.txt +1 -2
  16. rdf4j_python-0.1.4/rdf4j_python.egg-info/requires.txt +5 -0
  17. rdf4j_python-0.1.4/tests/test_async_named_graph.py +89 -0
  18. rdf4j_python-0.1.4/tests/test_helpers.py +21 -0
  19. rdf4j_python-0.1.4/tests/test_rdf4j_repository.py +388 -0
  20. rdf4j_python-0.1.2/rdf4j_python/model/_base_model.py +0 -48
  21. rdf4j_python-0.1.2/rdf4j_python/model/_dataset.py +0 -38
  22. rdf4j_python-0.1.2/rdf4j_python/model/_repository_info.py +0 -52
  23. rdf4j_python-0.1.2/rdf4j_python/model/term.py +0 -14
  24. rdf4j_python-0.1.2/rdf4j_python/utils/helpers.py +0 -22
  25. rdf4j_python-0.1.2/rdf4j_python.egg-info/requires.txt +0 -2
  26. rdf4j_python-0.1.2/tests/test_async_named_graph.py +0 -83
  27. rdf4j_python-0.1.2/tests/test_rdf4j_repository.py +0 -241
  28. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/LICENSE +0 -0
  29. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/__init__.py +0 -0
  30. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/_client/__init__.py +0 -0
  31. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/_client/_client.py +0 -0
  32. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/_driver/__init__.py +0 -0
  33. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/exception/__init__.py +0 -0
  34. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/exception/repo_exception.py +0 -0
  35. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/utils/__init__.py +0 -0
  36. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python/utils/const.py +0 -0
  37. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python.egg-info/dependency_links.txt +0 -0
  38. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/rdf4j_python.egg-info/top_level.txt +0 -0
  39. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/setup.cfg +0 -0
  40. {rdf4j_python-0.1.2 → rdf4j_python-0.1.4}/tests/test_client.py +0 -0
@@ -1,13 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rdf4j-python
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: The Python client for RDF4J
5
5
  Author-email: Chengxu Bian <cbian564@gmail.com>
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: httpx>=0.28.1
10
- Requires-Dist: rdflib>=7.1.4
10
+ Requires-Dist: pyoxigraph>=0.4.10
11
+ Provides-Extra: sparqlwrapper
12
+ Requires-Dist: sparqlwrapper>=2.0.0; extra == "sparqlwrapper"
11
13
  Dynamic: license-file
12
14
 
13
15
  # 🐍 rdf4j-python
@@ -60,6 +62,7 @@ async with AsyncRdf4j("http://localhost:19780/rdf4j-server") as db:
60
62
  Literal("test_object"),
61
63
  )
62
64
  await repo.get_statements(subject=IRI("http://example.com/subject"))
65
+ results = await repo.query("SELECT * WHERE { ?s ?p ?o }")
63
66
  ```
64
67
 
65
68
  For more detailed examples, refer to the [examples](https://github.com/odysa/rdf4j-python/tree/main/examples) directory.
@@ -48,6 +48,7 @@ async with AsyncRdf4j("http://localhost:19780/rdf4j-server") as db:
48
48
  Literal("test_object"),
49
49
  )
50
50
  await repo.get_statements(subject=IRI("http://example.com/subject"))
51
+ results = await repo.query("SELECT * WHERE { ?s ?p ?o }")
51
52
  ```
52
53
 
53
54
  For more detailed examples, refer to the [examples](https://github.com/odysa/rdf4j-python/tree/main/examples) directory.
@@ -1,11 +1,14 @@
1
1
  [project]
2
2
  name = "rdf4j-python"
3
3
  authors = [{ name = "Chengxu Bian", email = "cbian564@gmail.com" }]
4
- version = "0.1.2"
4
+ version = "0.1.4"
5
5
  description = "The Python client for RDF4J"
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.10"
8
- dependencies = ["httpx>=0.28.1", "rdflib>=7.1.4"]
8
+ dependencies = ["httpx>=0.28.1", "pyoxigraph>=0.4.10"]
9
+
10
+ [project.optional-dependencies]
11
+ sparqlwrapper = ["sparqlwrapper>=2.0.0"]
9
12
 
10
13
  [dependency-groups]
11
14
  dev = [
@@ -13,11 +16,9 @@ dev = [
13
16
  "pytest-asyncio>=0.26.0",
14
17
  "pytest-docker>=3.2.1",
15
18
  "ruff>=0.11.8",
19
+ "ty>=0.0.1a7",
16
20
  ]
17
- docs = [
18
- "furo>=2024.8.6",
19
- "sphinx>=8",
20
- ]
21
+ docs = ["furo>=2024.8.6", "sphinx>=8"]
21
22
 
22
23
 
23
24
  [tool.pytest.ini_options]
@@ -1,8 +1,9 @@
1
1
  from typing import Iterable
2
2
 
3
+ import pyoxigraph as og
4
+
3
5
  from rdf4j_python._client import AsyncApiClient
4
- from rdf4j_python.model import RDF4JDataSet
5
- from rdf4j_python.model.term import IRI, RDFStatement
6
+ from rdf4j_python.model.term import IRI, Quad, QuadResultSet, Triple
6
7
  from rdf4j_python.utils.const import Rdf4jContentType
7
8
  from rdf4j_python.utils.helpers import serialize_statements
8
9
 
@@ -22,11 +23,11 @@ class AsyncNamedGraph:
22
23
  self._repository_id = repository_id
23
24
  self._graph_uri = graph_uri
24
25
 
25
- async def get(self) -> RDF4JDataSet:
26
+ async def get(self) -> QuadResultSet:
26
27
  """Fetches all RDF statements from this named graph.
27
28
 
28
29
  Returns:
29
- str: RDF data serialized in the requested format.
30
+ QuadResultSet: RDF data serialized in the requested format.
30
31
 
31
32
  Raises:
32
33
  httpx.HTTPStatusError: If the request fails.
@@ -35,13 +36,13 @@ class AsyncNamedGraph:
35
36
  headers = {"Accept": Rdf4jContentType.NQUADS}
36
37
  response = await self._client.get(path, headers=headers)
37
38
  response.raise_for_status()
38
- return RDF4JDataSet.from_raw_text(response.text)
39
+ return og.parse(response.content, format=og.RdfFormat.N_QUADS)
39
40
 
40
- async def add(self, statements: Iterable[RDFStatement]):
41
+ async def add(self, statements: Iterable[Quad] | Iterable[Triple]):
41
42
  """Adds RDF statements to this named graph.
42
43
 
43
44
  Args:
44
- statements (Iterable[RDFStatement]): RDF statements to add.
45
+ statements (Iterable[Quad] | Iterable[Triple]): RDF statements to add.
45
46
 
46
47
  Raises:
47
48
  httpx.HTTPStatusError: If the request fails.
@@ -1,5 +1,5 @@
1
1
  import httpx
2
- import rdflib
2
+ import pyoxigraph as og
3
3
 
4
4
  from rdf4j_python._client import AsyncApiClient
5
5
  from rdf4j_python.exception.repo_exception import (
@@ -66,12 +66,13 @@ class AsyncRdf4j:
66
66
  "/repositories",
67
67
  headers={"Accept": Rdf4jContentType.SPARQL_RESULTS_JSON},
68
68
  )
69
- result = rdflib.query.Result.parse(
70
- response, format=Rdf4jContentType.SPARQL_RESULTS_JSON
69
+ query_solutions = og.parse_query_results(
70
+ response.text, format=og.QueryResultsFormat.JSON
71
71
  )
72
+ assert isinstance(query_solutions, og.QuerySolutions)
72
73
  return [
73
- RepositoryMetadata.from_rdflib_binding(binding)
74
- for binding in result.bindings
74
+ RepositoryMetadata.from_sparql_query_solution(query_solution)
75
+ for query_solution in query_solutions
75
76
  ]
76
77
 
77
78
  async def get_repository(self, repository_id: str) -> AsyncRdf4JRepository:
@@ -1,10 +1,7 @@
1
1
  from typing import Iterable, Optional
2
2
 
3
3
  import httpx
4
- import rdflib
5
- import rdflib.resource
6
- import rdflib.serializer
7
- import rdflib.store
4
+ import pyoxigraph as og
8
5
 
9
6
  from rdf4j_python._client import AsyncApiClient
10
7
  from rdf4j_python._driver._async_named_graph import AsyncNamedGraph
@@ -14,21 +11,35 @@ from rdf4j_python.exception.repo_exception import (
14
11
  RepositoryNotFoundException,
15
12
  RepositoryUpdateException,
16
13
  )
17
- from rdf4j_python.model import Namespace, RDF4JDataSet
14
+ from rdf4j_python.model import Namespace
18
15
  from rdf4j_python.model.term import (
16
+ IRI,
19
17
  Context,
20
18
  Object,
21
19
  Predicate,
22
- RDFStatement,
20
+ Quad,
21
+ QuadResultSet,
23
22
  Subject,
23
+ Triple,
24
24
  )
25
25
  from rdf4j_python.utils.const import Rdf4jContentType
26
26
  from rdf4j_python.utils.helpers import serialize_statements
27
27
 
28
+ try:
29
+ from SPARQLWrapper import SPARQLWrapper
30
+
31
+ _has_sparql_wrapper = True
32
+ except ImportError:
33
+ _has_sparql_wrapper = False
34
+
28
35
 
29
36
  class AsyncRdf4JRepository:
30
37
  """Asynchronous interface for interacting with an RDF4J repository."""
31
38
 
39
+ _client: AsyncApiClient
40
+ _repository_id: str
41
+ _sparql_wrapper: Optional["SPARQLWrapper"] = None
42
+
32
43
  def __init__(self, client: AsyncApiClient, repository_id: str):
33
44
  """Initializes the repository interface.
34
45
 
@@ -39,32 +50,47 @@ class AsyncRdf4JRepository:
39
50
  self._client = client
40
51
  self._repository_id = repository_id
41
52
 
53
+ async def get_sparql_wrapper(self) -> "SPARQLWrapper":
54
+ """Returns the SPARQLWrapper for the repository.
55
+
56
+ Returns:
57
+ SPARQLWrapper: The SPARQLWrapper for the repository.
58
+ """
59
+ if not _has_sparql_wrapper:
60
+ raise ImportError(
61
+ "SPARQLWrapper is not installed. Please install it with `pip install rdf4j-python[sparqlwrapper]`"
62
+ )
63
+
64
+ if self._sparql_wrapper is None:
65
+ self._sparql_wrapper = SPARQLWrapper(
66
+ f"{self._client.get_base_url()}/repositories/{self._repository_id}"
67
+ )
68
+ return self._sparql_wrapper
69
+
42
70
  async def query(
43
71
  self,
44
72
  sparql_query: str,
45
73
  infer: bool = True,
46
- accept: Rdf4jContentType = Rdf4jContentType.SPARQL_RESULTS_JSON,
47
- ):
74
+ ) -> og.QuerySolutions | og.QueryBoolean:
48
75
  """Executes a SPARQL SELECT query.
49
76
 
50
77
  Args:
51
78
  sparql_query (str): The SPARQL query string.
52
79
  infer (bool): Whether to include inferred statements. Defaults to True.
53
- accept (Rdf4jContentType): The expected response format.
54
80
 
55
81
  Returns:
56
- dict or str: Parsed JSON results or raw response text.
82
+ og.QuerySolutions | og.QueryBoolean: Parsed query results.
57
83
  """
58
84
  path = f"/repositories/{self._repository_id}"
59
85
  params = {"query": sparql_query, "infer": str(infer).lower()}
60
- headers = {"Accept": accept.value}
86
+ headers = {"Accept": Rdf4jContentType.SPARQL_RESULTS_JSON}
61
87
  response = await self._client.get(path, params=params, headers=headers)
62
88
  self._handle_repo_not_found_exception(response)
63
- if "json" in response.headers.get("Content-Type", ""):
64
- return response.json()
65
- return response.text
89
+ return og.parse_query_results(response.text, format=og.QueryResultsFormat.JSON)
66
90
 
67
- async def update(self, sparql_update: str):
91
+ async def update(
92
+ self, sparql_update_query: str, content_type: Rdf4jContentType
93
+ ) -> None:
68
94
  """Executes a SPARQL UPDATE command.
69
95
 
70
96
  Args:
@@ -74,11 +100,15 @@ class AsyncRdf4JRepository:
74
100
  RepositoryNotFoundException: If the repository doesn't exist.
75
101
  httpx.HTTPStatusError: If the update fails.
76
102
  """
103
+ # TODO: handle update results
77
104
  path = f"/repositories/{self._repository_id}/statements"
78
- headers = {"Content-Type": Rdf4jContentType.SPARQL_UPDATE.value}
79
- response = await self._client.post(path, data=sparql_update, headers=headers)
105
+ headers = {"Content-Type": content_type}
106
+ response = await self._client.post(
107
+ path, content=sparql_update_query, headers=headers
108
+ )
80
109
  self._handle_repo_not_found_exception(response)
81
- response.raise_for_status()
110
+ if response.status_code != httpx.codes.NO_CONTENT:
111
+ raise RepositoryUpdateException(f"Failed to update: {response.text}")
82
112
 
83
113
  async def get_namespaces(self):
84
114
  """Retrieves all namespaces in the repository.
@@ -92,26 +122,33 @@ class AsyncRdf4JRepository:
92
122
  path = f"/repositories/{self._repository_id}/namespaces"
93
123
  headers = {"Accept": Rdf4jContentType.SPARQL_RESULTS_JSON}
94
124
  response = await self._client.get(path, headers=headers)
95
- result = rdflib.query.Result.parse(
96
- response, format=Rdf4jContentType.SPARQL_RESULTS_JSON
97
- )
98
125
  self._handle_repo_not_found_exception(response)
99
- return [Namespace.from_rdflib_binding(binding) for binding in result.bindings]
100
126
 
101
- async def set_namespace(self, prefix: str, namespace: str):
127
+ query_solutions = og.parse_query_results(
128
+ response.text, format=og.QueryResultsFormat.JSON
129
+ )
130
+ assert isinstance(query_solutions, og.QuerySolutions)
131
+ return [
132
+ Namespace.from_sparql_query_solution(query_solution)
133
+ for query_solution in query_solutions
134
+ ]
135
+
136
+ async def set_namespace(self, prefix: str, namespace: IRI):
102
137
  """Sets a namespace prefix.
103
138
 
104
139
  Args:
105
140
  prefix (str): The namespace prefix.
106
- namespace (str): The namespace URI.
141
+ namespace (IRI): The namespace URI.
107
142
 
108
143
  Raises:
109
144
  RepositoryNotFoundException: If the repository doesn't exist.
110
145
  NamespaceException: If the request fails.
111
146
  """
112
147
  path = f"/repositories/{self._repository_id}/namespaces/{prefix}"
113
- headers = {"Content-Type": Rdf4jContentType.NTRIPLES.value}
114
- response = await self._client.put(path, content=namespace, headers=headers)
148
+ headers = {"Content-Type": Rdf4jContentType.NTRIPLES}
149
+ response = await self._client.put(
150
+ path, content=namespace.value, headers=headers
151
+ )
115
152
  self._handle_repo_not_found_exception(response)
116
153
  if response.status_code != httpx.codes.NO_CONTENT:
117
154
  raise NamespaceException(f"Failed to set namespace: {response.text}")
@@ -130,7 +167,7 @@ class AsyncRdf4JRepository:
130
167
  NamespaceException: If retrieval fails.
131
168
  """
132
169
  path = f"/repositories/{self._repository_id}/namespaces/{prefix}"
133
- headers = {"Accept": Rdf4jContentType.NTRIPLES.value}
170
+ headers = {"Accept": Rdf4jContentType.NTRIPLES}
134
171
  response = await self._client.get(path, headers=headers)
135
172
  self._handle_repo_not_found_exception(response)
136
173
 
@@ -192,7 +229,7 @@ class AsyncRdf4JRepository:
192
229
  object_: Optional[Object] = None,
193
230
  contexts: Optional[list[Context]] = None,
194
231
  infer: bool = True,
195
- ) -> RDF4JDataSet:
232
+ ) -> QuadResultSet:
196
233
  """Retrieves statements matching the given pattern.
197
234
 
198
235
  Args:
@@ -202,7 +239,7 @@ class AsyncRdf4JRepository:
202
239
  contexts (Optional[list[Context]]): Filter by context (named graph).
203
240
 
204
241
  Returns:
205
- DataSet: Dataset of matching RDF statements.
242
+ QuadResultSet: QuadResultSet of matching RDF statements.
206
243
 
207
244
  Raises:
208
245
  RepositoryNotFoundException: If the repository doesn't exist.
@@ -211,20 +248,18 @@ class AsyncRdf4JRepository:
211
248
  params = {}
212
249
 
213
250
  if subject:
214
- params["subj"] = subject.n3()
251
+ params["subj"] = str(subject)
215
252
  if predicate:
216
- params["pred"] = predicate.n3()
253
+ params["pred"] = str(predicate)
217
254
  if object_:
218
- params["obj"] = object_.n3()
255
+ params["obj"] = str(object_)
219
256
  if contexts:
220
- params["context"] = [ctx.n3() for ctx in contexts]
257
+ params["context"] = [str(ctx) for ctx in contexts]
221
258
  params["infer"] = str(infer).lower()
222
259
 
223
260
  headers = {"Accept": Rdf4jContentType.NQUADS}
224
261
  response = await self._client.get(path, params=params, headers=headers)
225
- dataset = RDF4JDataSet()
226
- dataset.parse(data=response.text, format="nquads")
227
- return dataset
262
+ return og.parse(response.content, format=og.RdfFormat.N_QUADS)
228
263
 
229
264
  async def delete_statements(
230
265
  self,
@@ -250,13 +285,13 @@ class AsyncRdf4JRepository:
250
285
  params = {}
251
286
 
252
287
  if subject:
253
- params["subj"] = subject.n3()
288
+ params["subj"] = str(subject)
254
289
  if predicate:
255
- params["pred"] = predicate.n3()
290
+ params["pred"] = str(predicate)
256
291
  if object_:
257
- params["obj"] = object_.n3()
292
+ params["obj"] = str(object_)
258
293
  if contexts:
259
- params["context"] = [ctx.n3() for ctx in contexts]
294
+ params["context"] = [str(ctx) for ctx in contexts]
260
295
 
261
296
  response = await self._client.delete(path, params=params)
262
297
  self._handle_repo_not_found_exception(response)
@@ -286,21 +321,26 @@ class AsyncRdf4JRepository:
286
321
  httpx.HTTPStatusError: If addition fails.
287
322
  """
288
323
  path = f"/repositories/{self._repository_id}/statements"
324
+ statement: Triple | Quad
325
+ if context is None:
326
+ statement = Triple(subject, predicate, object)
327
+ else:
328
+ statement = Quad(subject, predicate, object, context)
329
+
289
330
  response = await self._client.post(
290
331
  path,
291
- content=serialize_statements([(subject, predicate, object, context)]),
332
+ content=serialize_statements([statement]),
292
333
  headers={"Content-Type": Rdf4jContentType.NQUADS},
293
334
  )
294
335
  self._handle_repo_not_found_exception(response)
295
336
  if response.status_code != httpx.codes.NO_CONTENT:
296
337
  raise RepositoryUpdateException(f"Failed to add statement: {response.text}")
297
338
 
298
- async def add_statements(self, statements: Iterable[RDFStatement]):
339
+ async def add_statements(self, statements: Iterable[Quad] | Iterable[Triple]):
299
340
  """Adds a list of RDF statements to the repository.
300
341
 
301
342
  Args:
302
- statements (Iterable[RDFStatement]): A list of RDF statements.
303
- RDFStatement: A tuple of subject, predicate, object, and context.
343
+ statements (Iterable[Quad] | Iterable[Triple]): A list of RDF statements.
304
344
 
305
345
  Raises:
306
346
  RepositoryNotFoundException: If the repository doesn't exist.
@@ -320,26 +360,26 @@ class AsyncRdf4JRepository:
320
360
 
321
361
  async def replace_statements(
322
362
  self,
323
- statements: Iterable[RDFStatement],
324
- contexts: Optional[list[Context]] = None,
363
+ statements: Iterable[Quad] | Iterable[Triple],
364
+ contexts: Optional[Iterable[Context]] = None,
325
365
  base_uri: Optional[str] = None,
326
366
  ):
327
367
  """Replaces all repository statements with the given RDF data.
328
368
 
329
369
  Args:
330
- statements (Iterable[RDFStatement]): RDF statements to load.
331
- contexts (Optional[list[Context]]): One or more specific contexts to restrict deletion to.
370
+ statements (Iterable[Quad] | Iterable[Triple]): RDF statements to load.
371
+ contexts (Optional[Iterable[Context]]): One or more specific contexts to restrict deletion to.
332
372
 
333
373
  Raises:
334
374
  RepositoryNotFoundException: If the repository doesn't exist.
335
375
  httpx.HTTPStatusError: If the operation fails.
336
376
  """
337
377
  path = f"/repositories/{self._repository_id}/statements"
338
- headers = {"Content-Type": Rdf4jContentType.NQUADS.value}
378
+ headers = {"Content-Type": Rdf4jContentType.NQUADS}
339
379
 
340
380
  params = {}
341
381
  if contexts:
342
- params["context"] = [ctx.n3() for ctx in contexts]
382
+ params["context"] = [str(ctx) for ctx in contexts]
343
383
  if base_uri:
344
384
  params["baseUri"] = base_uri
345
385
 
@@ -2,12 +2,10 @@
2
2
  RDF4J Python Model Module
3
3
  """
4
4
 
5
- from ._dataset import RDF4JDataSet
6
5
  from ._namespace import Namespace
7
6
  from ._repository_info import RepositoryMetadata
8
7
 
9
8
  __all__ = [
10
9
  "Namespace",
11
10
  "RepositoryMetadata",
12
- "RDF4JDataSet",
13
11
  ]
@@ -1,11 +1,32 @@
1
- from typing import Mapping
2
-
3
- from rdflib.namespace import Namespace as RdflibNamespace
4
- from rdflib.term import Identifier, Variable
1
+ import pyoxigraph as og
5
2
 
6
3
  from rdf4j_python.model.term import IRI
7
4
 
8
- from ._base_model import _BaseModel
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
13
+
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)
9
30
 
10
31
 
11
32
  class Namespace:
@@ -14,7 +35,7 @@ class Namespace:
14
35
  """
15
36
 
16
37
  _prefix: str
17
- _namespace: RdflibNamespace
38
+ _namespace: _Namespace
18
39
 
19
40
  def __init__(self, prefix: str, namespace: str):
20
41
  """
@@ -25,24 +46,26 @@ class Namespace:
25
46
  namespace (str): The namespace URI.
26
47
  """
27
48
  self._prefix = prefix
28
- self._namespace = RdflibNamespace(namespace)
49
+ self._namespace = _Namespace(namespace)
29
50
 
30
51
  @classmethod
31
- def from_rdflib_binding(cls, binding: Mapping[Variable, Identifier]) -> "Namespace":
52
+ def from_sparql_query_solution(
53
+ cls, query_solution: og.QuerySolution
54
+ ) -> "Namespace":
32
55
  """
33
- Creates a Namespace from a RDFlib binding.
56
+ Creates a Namespace from a binding.
34
57
 
35
58
  Args:
36
- binding (Mapping[Variable, Identifier]): The RDFlib binding.
59
+ binding (Mapping[Variable, Identifier]): The binding.
37
60
 
38
61
  Returns:
39
62
  Namespace: The created Namespace.
40
63
  """
41
- prefix = _BaseModel.get_literal(binding, "prefix", "")
42
- namespace = _BaseModel.get_literal(binding, "namespace", "")
64
+ prefix: og.Literal = query_solution[og.Variable("prefix")]
65
+ namespace: og.NamedNode = query_solution[og.Variable("namespace")]
43
66
  return cls(
44
- prefix=prefix,
45
- namespace=namespace,
67
+ prefix=prefix.value,
68
+ namespace=namespace.value,
46
69
  )
47
70
 
48
71
  def __str__(self):
@@ -85,7 +108,7 @@ class Namespace:
85
108
  Returns:
86
109
  IRI: The IRI for the term.
87
110
  """
88
- return IRI(self._namespace.term(name))
111
+ return self._namespace.term(name)
89
112
 
90
113
  def __getitem__(self, item: str) -> IRI:
91
114
  """
@@ -0,0 +1,62 @@
1
+ from dataclasses import dataclass
2
+
3
+ import pyoxigraph as og
4
+
5
+
6
+ @dataclass
7
+ class RepositoryMetadata:
8
+ """
9
+ Represents a repository metadata RDF4J.
10
+ """
11
+
12
+ id: str # The repository identifier
13
+ uri: str # The full URI to the repository
14
+ title: str # A human-readable title (currently reusing id)
15
+ readable: bool # Whether the repository is readable
16
+ writable: bool # Whether the repository is writable
17
+
18
+ def __str__(self):
19
+ """
20
+ Returns a string representation of the RepositoryMetadata.
21
+
22
+ Returns:
23
+ str: A string representation of the RepositoryMetadata.
24
+ """
25
+ return f"Repository(id={self.id}, title={self.title}, uri={self.uri})"
26
+
27
+ @classmethod
28
+ def from_sparql_query_solution(
29
+ cls, query_solution: og.QuerySolution
30
+ ) -> "RepositoryMetadata":
31
+ """
32
+ Create a RepositoryMetadata instance from a SPARQL query result.
33
+
34
+ Args:
35
+ query_solution (og.QuerySolution): The SPARQL query result.
36
+
37
+ Returns:
38
+ RepositoryMetadata: The RepositoryMetadata instance.
39
+
40
+ Raises:
41
+ ValueError: If the query solution is missing required fields.
42
+ """
43
+
44
+ # Construct and return the Repository object
45
+ if query_solution["id"] is None:
46
+ raise ValueError("id is required")
47
+ if query_solution["uri"] is None:
48
+ raise ValueError("uri is required")
49
+ if query_solution["title"] is None:
50
+ raise ValueError("title is required")
51
+ if query_solution["readable"] is None:
52
+ raise ValueError("readable is required")
53
+ if query_solution["writable"] is None:
54
+ raise ValueError("writable is required")
55
+
56
+ return cls(
57
+ id=query_solution["id"].value,
58
+ uri=query_solution["uri"].value,
59
+ title=query_solution["title"].value,
60
+ readable=bool(query_solution["readable"].value),
61
+ writable=bool(query_solution["writable"].value),
62
+ )