44import time
55import warnings
66from io import BytesIO
7+ from typing import Literal
78from urllib .parse import urlencode
89
910import pycurl
1011
1112from sparqlite .exceptions import EndpointError , QueryError
1213
14+ HTTPMethod = Literal ["GET" , "POST" ]
15+
1316
1417class SPARQLClient :
15- """Synchronous SPARQL 1.1 client with connection pooling and automatic retry."""
1618
1719 def __init__ (
1820 self ,
@@ -22,24 +24,16 @@ def __init__(
2224 backoff_factor : float = 0.5 ,
2325 timeout : float | None = None ,
2426 ):
25- """Initialize the SPARQL client.
26-
27- Args:
28- endpoint: The SPARQL endpoint URL.
29- max_retries: Maximum number of retry attempts for transient errors.
30- backoff_factor: Factor for exponential backoff (wait = factor * 2^retry).
31- timeout: Request timeout in seconds. None means no timeout.
32- """
3327 self .endpoint = endpoint
3428 self .max_retries = max_retries
3529 self .backoff_factor = backoff_factor
3630 self .timeout = timeout
37- self ._curl = pycurl .Curl ()
31+ self ._curl : pycurl . Curl | None = pycurl .Curl ()
3832
3933 def __enter__ (self ) -> "SPARQLClient" :
4034 return self
4135
42- def __exit__ (self , exc_type , exc_val , exc_tb ) -> None :
36+ def __exit__ (self , exc_type : object , exc_val : object , exc_tb : object ) -> None :
4337 self .close ()
4438
4539 def __del__ (self ) -> None :
@@ -53,10 +47,6 @@ def __del__(self) -> None:
5347 self .close ()
5448
5549 def close (self ) -> None :
56- """Close the client and release resources.
57-
58- This method is idempotent - calling it multiple times is safe.
59- """
6050 if self ._curl is not None :
6151 self ._curl .close ()
6252 self ._curl = None
@@ -66,42 +56,49 @@ def _request(
6656 query : str ,
6757 accept : str ,
6858 * ,
59+ method : HTTPMethod = "GET" ,
6960 is_update : bool = False ,
7061 ) -> bytes :
71- """Execute an HTTP request with retry logic."""
72- last_error = None
62+ if self ._curl is None :
63+ raise EndpointError ("Client is closed" )
64+
65+ curl = self ._curl
66+ last_error : EndpointError | None = None
7367
7468 for attempt in range (self .max_retries + 1 ):
7569 if attempt > 0 :
7670 wait_time = self .backoff_factor * (2 ** attempt )
7771 time .sleep (wait_time )
7872
7973 buffer = BytesIO ()
80- self . _curl .reset ()
74+ curl .reset ()
8175
8276 if self .timeout is not None :
83- self . _curl .setopt (pycurl .TIMEOUT_MS , int (self .timeout * 1000 ))
77+ curl .setopt (pycurl .TIMEOUT_MS , int (self .timeout * 1000 ))
8478
8579 try :
86- self ._curl .setopt (pycurl .URL , self .endpoint )
87- self ._curl .setopt (pycurl .WRITEDATA , buffer )
88- self ._curl .setopt (
80+ param_key = "update" if is_update else "query"
81+
82+ if method == "GET" :
83+ url = f"{ self .endpoint } ?{ urlencode ({param_key : query })} "
84+ curl .setopt (pycurl .URL , url )
85+ curl .setopt (pycurl .HTTPGET , 1 )
86+ else :
87+ curl .setopt (pycurl .URL , self .endpoint )
88+ curl .setopt (pycurl .POSTFIELDS , urlencode ({param_key : query }))
89+
90+ curl .setopt (pycurl .WRITEDATA , buffer )
91+ curl .setopt (
8992 pycurl .HTTPHEADER ,
9093 [
9194 f"Accept: { accept } " ,
9295 "User-Agent: sparqlite/0.1.0" ,
9396 ],
9497 )
9598
96- if is_update :
97- post_data = urlencode ({"update" : query })
98- else :
99- post_data = urlencode ({"query" : query })
100-
101- self ._curl .setopt (pycurl .POSTFIELDS , post_data )
102- self ._curl .perform ()
99+ curl .perform ()
103100
104- status_code = self . _curl .getinfo (pycurl .RESPONSE_CODE )
101+ status_code = curl .getinfo (pycurl .RESPONSE_CODE )
105102
106103 if status_code == 400 :
107104 raise QueryError (f"Query syntax error: { buffer .getvalue ().decode ()} " )
@@ -131,62 +128,30 @@ def _request(
131128 last_error = EndpointError (f"Request error: { error_msg } " )
132129 continue
133130
134- raise last_error
135-
136- def query (self , query : str ) -> dict :
137- """Execute a SELECT query.
131+ raise last_error # type: ignore[misc]
138132
139- Args:
140- query: The SPARQL SELECT query string.
141-
142- Returns:
143- Dictionary with SPARQL JSON results format.
144- """
145- content = self ._request (query , "application/sparql-results+json" )
133+ def query (self , query : str , * , method : HTTPMethod = "GET" ) -> dict :
134+ content = self ._request (
135+ query , "application/sparql-results+json" , method = method
136+ )
146137 return json .loads (content )
147138
148- def select (self , query : str ) -> dict :
149- """Execute a SELECT query. Alias for query()."""
150- return self .query (query )
151-
152- def ask (self , query : str ) -> bool :
153- """Execute an ASK query.
139+ def select (self , query : str , * , method : HTTPMethod = "GET" ) -> dict :
140+ return self .query (query , method = method )
154141
155- Args:
156- query: The SPARQL ASK query string.
157-
158- Returns:
159- Boolean result of the ASK query.
160- """
161- content = self ._request (query , "application/sparql-results+json" )
142+ def ask (self , query : str , * , method : HTTPMethod = "GET" ) -> bool :
143+ content = self ._request (
144+ query , "application/sparql-results+json" , method = method
145+ )
162146 return json .loads (content )["boolean" ]
163147
164- def construct (self , query : str ) -> bytes :
165- """Execute a CONSTRUCT query.
166-
167- Args:
168- query: The SPARQL CONSTRUCT query string.
148+ def construct (self , query : str , * , method : HTTPMethod = "GET" ) -> bytes :
149+ return self ._request (query , "application/n-triples" , method = method )
169150
170- Returns:
171- Raw N-Triples bytes.
172- """
173- return self ._request (query , "application/n-triples" )
174-
175- def describe (self , query : str ) -> bytes :
176- """Execute a DESCRIBE query.
177-
178- Args:
179- query: The SPARQL DESCRIBE query string.
180-
181- Returns:
182- Raw N-Triples bytes.
183- """
184- return self ._request (query , "application/n-triples" )
151+ def describe (self , query : str , * , method : HTTPMethod = "GET" ) -> bytes :
152+ return self ._request (query , "application/n-triples" , method = method )
185153
186154 def update (self , query : str ) -> None :
187- """Execute a SPARQL UPDATE query (INSERT, DELETE, etc.).
188-
189- Args:
190- query: The SPARQL UPDATE query string.
191- """
192- self ._request (query , "application/sparql-results+json" , is_update = True )
155+ self ._request (
156+ query , "application/sparql-results+json" , method = "POST" , is_update = True
157+ )
0 commit comments