Skip to content

Commit ae53ec0

Browse files
committed
docs: enhance Client class documentation
1 parent 548f240 commit ae53ec0

File tree

1 file changed

+85
-30
lines changed

1 file changed

+85
-30
lines changed

ORStools/common/client.py

Lines changed: 85 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,27 @@
4545

4646

4747
class 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}\nParameters: {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

Comments
 (0)