4545
4646
4747class Client (QObject ):
48- """Performs requests to the ORS API services."""
48+ """Performs requests to the ORS API services.
49+
50+ This class handles HTTP communication with the openrouteservice API,
51+ including authentication, retry logic for rate limiting, and progress
52+ tracking.
53+
54+ Signals:
55+ overQueryLimit: Emitted when rate limit is hit, passes delay in seconds
56+ downloadProgress: Emitted during download, passes progress ratio (0-1)
57+ """
4958
5059 overQueryLimit = pyqtSignal (int )
5160 downloadProgress = pyqtSignal (int )
5261
53-
5462 def __init__ (self , provider : Optional [dict ] = None , agent : Optional [str ] = None ) -> None :
55- """
56- :param provider: A openrouteservice provider from config.yml
57- :type provider: dict
63+ """Initialize the ORS API client.
5864
59- :param retry_timeout: Timeout across multiple retryable requests, in
60- seconds.
61- :type retry_timeout: int
65+ :param provider: Provider configuration containing base_url, key, timeout, and ENV_VARS
66+ :type provider: dict
67+ :param agent: User agent string for the HTTP requests
68+ :type agent: str
6269 """
6370 QObject .__init__ (self )
6471
@@ -87,8 +94,20 @@ def _request(
8794 self ,
8895 post_json : Optional [dict ],
8996 blocking_request : QgsBlockingNetworkRequest ,
90- request : QNetworkRequest
97+ request : QNetworkRequest ,
9198 ) -> str :
99+ """Execute a blocking network request.
100+
101+ :param post_json: JSON payload for POST requests, None for GET requests
102+ :type post_json: dict or None
103+ :param blocking_request: QGIS blocking network request handler
104+ :type blocking_request: QgsBlockingNetworkRequest
105+ :param request: Network request with URL and headers
106+ :type request: QNetworkRequest
107+ :return: Network reply content
108+ :rtype: QgsNetworkReplyContent
109+ :raises: Various ApiError exceptions based on HTTP status codes
110+ """
92111 if post_json is not None :
93112 result = blocking_request .post (request , json .dumps (post_json ).encode ())
94113 else :
@@ -108,19 +127,40 @@ def fetch_with_retry(
108127 post_json : Optional [dict ] = None ,
109128 max_retries : int = 100 ,
110129 ):
130+ """Fetch data from the API with automatic retry on rate limit errors.
131+
132+ Implements exponential backoff with jitter for retries. The first retry
133+ occurs after exactly 61 seconds, subsequent retries use exponential
134+ backoff capped at 5 minutes.
135+
136+ :param url: API endpoint path (e.g., "/v2/directions/driving-car/geojson")
137+ :type url: str
138+ :param params: URL query parameters
139+ :type params: dict
140+ :param first_request_time: Timestamp of the first request attempt
141+ :type first_request_time: datetime or None
142+ :param post_json: JSON payload for POST requests
143+ :type post_json: dict or None
144+ :param max_retries: Maximum number of retry attempts
145+ :type max_retries: int
146+ :return: Parsed JSON response from the API
147+ :rtype: dict
148+ :raises exceptions.Timeout: When total retry time exceeds configured timeout
149+ :raises exceptions.ApiError: On non-retryable API errors (4xx except 429)
150+ """
111151 first_request_time = datetime .now ()
112152
113153 authed_url = self ._generate_auth_url (url , params )
114154 self .url = self .base_url + authed_url
115155
116156 request = QNetworkRequest (QUrl (self .url ))
117-
157+
118158 for header , value in self .headers .items ():
119159 request .setRawHeader (header .encode (), value .encode ())
120160
121161 blocking_request = QgsBlockingNetworkRequest ()
122162
123- blocking_request .downloadProgress .connect (lambda r , t : self .downloadProgress .emit (r / t ))
163+ blocking_request .downloadProgress .connect (lambda r , t : self .downloadProgress .emit (r / t ))
124164
125165 logger .log (f"url: { self .url } \n Parameters: { json .dumps (post_json , indent = 2 )} " , 0 )
126166
@@ -131,7 +171,7 @@ def fetch_with_retry(
131171 reply = self ._request (post_json , blocking_request , request )
132172 content = reply .content ().data ().decode ()
133173 break
134-
174+
135175 except exceptions .OverQueryLimit as e :
136176 if datetime .now () - first_request_time > timedelta (seconds = self .timeout ):
137177 raise exceptions .Timeout ()
@@ -140,9 +180,9 @@ def fetch_with_retry(
140180
141181 delay_seconds = self .get_delay_seconds (i )
142182 self .overQueryLimit .emit (delay_seconds )
143-
183+
144184 loop = QEventLoop ()
145- QTimer .singleShot (delay_seconds * 1000 , loop .quit ) # milliseconds
185+ QTimer .singleShot (delay_seconds * 1000 , loop .quit )
146186 loop .exec_ ()
147187
148188 except exceptions .ApiError as e :
@@ -164,19 +204,38 @@ def fetch_with_retry(
164204 return json .loads (content )
165205
166206 def get_delay_seconds (self , retry_counter : int ) -> int :
207+ """Calculate delay before next retry attempt.
208+
209+ First retry waits exactly 61 seconds. Subsequent retries use exponential
210+ backoff with base delay of 60s * 1.5^(retry_counter-1), capped at 300s,
211+ with random jitter between 30-60% of the calculated delay.
212+
213+ :param retry_counter: Zero-based retry attempt number
214+ :type retry_counter: int
215+ :return: Delay in seconds before next retry
216+ :rtype: int
217+ """
167218 if retry_counter == 0 :
168- delay_seconds = 61 # First retry after exactly 61 seconds
219+ delay_seconds = 61
169220 else :
170- # Exponential backoff starting from 60 seconds
171- base_delay = min (60 * (1.5 ** (retry_counter - 1 )), 300 ) # Cap at 5 minutes
221+ base_delay = min (60 * (1.5 ** (retry_counter - 1 )), 300 )
172222 jitter = random .uniform (0.3 , 0.6 )
173223 delay_seconds = base_delay * jitter
174224
175225 logger .log (f"Retry Counter: { retry_counter } , Delay: { delay_seconds :.2f} s" , 1 )
176226 return int (delay_seconds )
177227
178228 def _check_status (self , reply : QNetworkReply ) -> None :
179- """Check response status and raise appropriate exceptions."""
229+ """Check HTTP response status and raise appropriate exceptions.
230+
231+ :param reply: Network reply to check
232+ :type reply: QNetworkReply
233+ :raises exceptions.InvalidKey: On 403 Forbidden (invalid API key)
234+ :raises exceptions.OverQueryLimit: On 429 Too Many Requests
235+ :raises exceptions.ApiError: On 4xx client errors
236+ :raises exceptions.GenericServerError: On 5xx server errors
237+ :raises Exception: On network errors or invalid responses
238+ """
180239 status_code = reply .attribute (QNetworkRequest .HttpStatusCodeAttribute )
181240 message = reply .content ().data ().decode ()
182241
@@ -197,19 +256,15 @@ def _check_status(self, reply: QNetworkReply) -> None:
197256 raise exceptions .GenericServerError (str (status_code ), message )
198257
199258 def _generate_auth_url (self , path : str , params : Union [Dict , List ]) -> str :
200- """Returns the path and query string portion of the request URL, first
201- adding any necessary parameters.
202-
203- :param path: The path portion of the URL.
204- :type path: string
205-
206- :param params: URL parameters.
207- :type params: dict or list of key/value tuples
208-
209- :returns: encoded URL
210- :rtype: string
259+ """Generate authenticated URL with query parameters.
260+
261+ :param path: API endpoint path
262+ :type path: str
263+ :param params: Query parameters as dict or list of tuples
264+ :type params: dict or list
265+ :return: URL path with encoded query string
266+ :rtype: str
211267 """
212-
213268 if isinstance (params , dict ):
214269 params = sorted (dict (** params ).items ())
215270
0 commit comments