Skip to content

Commit beec970

Browse files
authored
feat(DEVC-1752): added requests.Session reuse; replaced third-party tenacity with builtin urllib3 Retry (#100)
* feat(DEVC-1752): added requests.Session reuse; replaced third-party tenacity with builtin urllib3 Retry * feat(DEVC-1752): bump version for fakeredis since py3.8 no longer supported for versions since 2.30.0 and upper * feat(DEVC-1752): remove redundant tests according to new schema for retry logic * feat(DEVC-1752): skip some tests on py3.13 regarding logging/capsyst * feat(DEVC-1752): bumped minor version for python 3.13 to 3.13.3 * feat(DEVC-1752): remove unused dependency tenacity for python-sdk
1 parent 3d3ded9 commit beec970

14 files changed

Lines changed: 118 additions & 184 deletions

File tree

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: CI
33
on: push
44

55
env:
6-
PYTHON_VERSIONS: '[ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13" ]'
6+
PYTHON_VERSIONS: '[ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.3" ]'
77

88
jobs:
99

CHANGELOG.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [1.14.1] - 2025-07-25
10+
### Added
11+
- Session mechanism for significantly decrease number of an http load on data-api for apps with intensive calling
12+
- Added possibility to adjust some params related to connection pool
13+
- `POOL_CONNECTIONS_COUNT`: Total pools count
14+
- `POOL_MAX_SIZE`: Max connections count per pool/host
15+
- `POOL_BLOCK`: Wait until connection released or not (instantly raise an exception)
16+
- `MAX_RETRY_COUNT`: If 0 then retires will be disabled, otherwise retrying logic will be used
17+
- Move retrying logic from `tenacity` to internal `urllib3.util.Retry(...)`
18+
- Removed redundant dependency `tenacity` from `python-sdk`
19+
- Bump version for `py3.13` to `py3.13.3` at CI version matrix in order to fix broken tests for logging
20+
- Bump version for `fakeredis` to fix some tests
21+
22+
923
## [1.14.0] - 2025-04-17
1024
### Fixed
1125
- merge_events parameter for scheduled data time apps should result in correct start/end times in a final app event.
@@ -405,7 +419,8 @@ env variables, that should be used to configure logging.
405419
- Event classes: `StreamEvent`, `ScheduledEvent` and `TaskEvent`.
406420

407421

408-
[Unreleased] https://github.com/corva-ai/python-sdk/compare/v1.14.0...master
422+
[Unreleased] https://github.com/corva-ai/python-sdk/compare/v1.14.1...master
423+
[1.14.1] https://github.com/corva-ai/python-sdk/compare/v1.14.0...v1.14.1
409424
[1.14.0] https://github.com/corva-ai/python-sdk/compare/v1.13.1...v1.14.0
410425
[1.13.1] https://github.com/corva-ai/python-sdk/compare/v1.13.0...v1.13.1
411426
[1.13.0] https://github.com/corva-ai/python-sdk/compare/v1.12.1...v1.13.0

docs/antora-playbook.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ content:
77
start_path: docs
88
branches: []
99
# branches: HEAD # Use this for local development
10-
tags: [v1.14.0]
10+
tags: [v1.14.1]
1111
asciidoc:
1212
attributes:
1313
page-toclevels: 5

docs/antora.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
name: corva-sdk
2-
version: ~
2+
version: 1.14.1
33
nav: [modules/ROOT/nav.adoc]

docs/modules/ROOT/examples/api/tutorial008.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

setup.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,11 @@
3939
packages=setuptools.find_packages("src"),
4040
package_dir={"": "src"},
4141
install_requires=[
42-
"fakeredis[lua] >=2.26.2, <3.0.0",
42+
"fakeredis[lua] >=2.26.2, <2.30.0",
4343
"pydantic >=1.8.2, <2.0.0",
4444
"redis >=5.2.1, <6.0.0",
4545
"requests >=2.32.3, <3.0.0",
4646
"urllib3 <2", # lambda doesnt support version 2 yet
47-
"tenacity >=8.2.3, <9.0.0",
4847
],
4948
python_requires='>=3.8, <4.0',
5049
license='The Unlicense',

src/corva/api.py

Lines changed: 29 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
import json
22
import posixpath
33
import re
4-
from http import HTTPStatus
54
from typing import List, Optional, Sequence, Union
65

76
import requests
8-
from tenacity import (
9-
RetryError,
10-
retry,
11-
retry_if_result,
12-
stop_after_attempt,
13-
wait_random_exponential,
14-
)
7+
8+
from corva.api_utils import get_requests_session, get_retry_strategy
9+
from corva.configuration import SETTINGS
1510

1611

1712
class Api:
@@ -21,26 +16,34 @@ class Api:
2116
convenient URL usage and reasonable timeouts to API requests.
2217
"""
2318

24-
TIMEOUT_LIMITS = (3, 30) # seconds
25-
DEFAULT_MAX_RETRIES = int(0)
26-
2719
def __init__(
2820
self,
2921
*,
3022
api_url: str,
3123
data_api_url: str,
3224
api_key: str,
3325
app_key: str,
34-
timeout: Optional[int] = None,
3526
app_connection_id: Optional[int] = None,
27+
max_retries: Optional[int] = 3,
28+
backoff_factor_retries: Optional[float] = 1,
29+
pool_conn_count: Optional[int] = None,
30+
pool_max_size: Optional[int] = None,
31+
pool_block: Optional[bool] = None,
3632
):
3733
self.api_url = api_url
3834
self.data_api_url = data_api_url
3935
self.api_key = api_key
4036
self.app_key = app_key
4137
self.app_connection_id = app_connection_id
42-
self.timeout = timeout or self.TIMEOUT_LIMITS[1]
43-
self._max_retries = self.DEFAULT_MAX_RETRIES
38+
self._session = get_requests_session(
39+
retry_strategy=get_retry_strategy(
40+
max_retries=max_retries or SETTINGS.MAX_RETRY_COUNT,
41+
backoff_factor=backoff_factor_retries or SETTINGS.BACKOFF_FACTOR,
42+
),
43+
pool_connections_count=(pool_conn_count or SETTINGS.POOL_CONNECTIONS_COUNT),
44+
pool_max_size=pool_max_size or SETTINGS.POOL_MAX_SIZE,
45+
pool_block=pool_block or SETTINGS.POOL_BLOCK,
46+
)
4447

4548
@property
4649
def default_headers(self):
@@ -49,16 +52,6 @@ def default_headers(self):
4952
"X-Corva-App": self.app_key,
5053
}
5154

52-
@property
53-
def max_retries(self) -> int:
54-
return self._max_retries
55-
56-
@max_retries.setter
57-
def max_retries(self, value: int):
58-
if not (0 <= value <= 10):
59-
raise ValueError("Values between 0 and 10 are allowed")
60-
self._max_retries = value
61-
6255
def get(self, path: str, **kwargs):
6356
return self._request("GET", path, **kwargs)
6457

@@ -99,15 +92,15 @@ def _get_url(self, path: str):
9992

10093
return posixpath.join(self.api_url, path)
10194

102-
@staticmethod
10395
def _execute_request(
96+
self,
10497
method: str,
10598
url: str,
10699
params: Optional[dict],
107100
data: Optional[dict],
108101
headers: Optional[dict] = None,
109102
timeout: Optional[int] = None,
110-
):
103+
) -> requests.Response:
111104
"""Executes the request.
112105
113106
Args:
@@ -116,12 +109,12 @@ def _execute_request(
116109
data: request body, that will be casted to json.
117110
params: url query string params.
118111
headers: additional headers to include in request.
119-
timeout: custom request timeout in seconds.
120112
121113
Returns:
122114
requests.Response instance.
123115
"""
124-
return requests.request(
116+
117+
return self._session.request(
125118
method=method,
126119
url=url,
127120
params=params,
@@ -148,70 +141,25 @@ def _request(
148141
data: request body, that will be casted to json.
149142
params: url query string params.
150143
headers: additional headers to include in request.
151-
timeout: custom request timeout in seconds.
152144
153145
Returns:
154146
requests.Response instance.
155147
"""
156-
retryable_status_codes = [
157-
HTTPStatus.TOO_MANY_REQUESTS, # 428
158-
HTTPStatus.INTERNAL_SERVER_ERROR, # 500
159-
HTTPStatus.BAD_GATEWAY, # 502
160-
HTTPStatus.SERVICE_UNAVAILABLE, # 503
161-
HTTPStatus.GATEWAY_TIMEOUT, # 504
162-
]
163-
164-
timeout = timeout or self.timeout
165-
self._validate_timeout(timeout)
166-
167148
url = self._get_url(path)
168149

169150
headers = {
170151
**self.default_headers,
171152
**(headers or {}),
172153
}
173154

174-
if self.max_retries > 0:
175-
retry_decorator = retry(
176-
stop=stop_after_attempt(self.max_retries),
177-
wait=wait_random_exponential(multiplier=0.25, max=10),
178-
retry=retry_if_result(
179-
lambda r: r.status_code in retryable_status_codes
180-
),
181-
)
182-
retrying_request = retry_decorator(self._execute_request)
183-
try:
184-
response = retrying_request(
185-
method=method,
186-
url=url,
187-
params=params,
188-
data=data,
189-
headers=headers,
190-
timeout=timeout,
191-
)
192-
except RetryError as e:
193-
if not e.last_attempt.failed:
194-
response = e.last_attempt.result()
195-
else:
196-
raise
197-
else:
198-
response = self._execute_request(
199-
method=method,
200-
url=url,
201-
params=params,
202-
data=data,
203-
headers=headers,
204-
timeout=timeout,
205-
)
206-
207-
return response
208-
209-
def _validate_timeout(self, timeout: int) -> None:
210-
if self.TIMEOUT_LIMITS[0] > timeout or self.TIMEOUT_LIMITS[1] < timeout:
211-
raise ValueError(
212-
f"Timeout must be between {self.TIMEOUT_LIMITS[0]} and "
213-
f"{self.TIMEOUT_LIMITS[1]} seconds."
214-
)
155+
return self._execute_request(
156+
method=method,
157+
url=url,
158+
params=params,
159+
data=data,
160+
headers=headers,
161+
timeout=timeout,
162+
)
215163

216164
def get_dataset(
217165
self,

src/corva/api_utils.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from typing import Optional
2+
3+
import requests
4+
from requests.adapters import HTTPAdapter
5+
from urllib3 import Retry
6+
7+
RETRYABLE_STATUS_CODES = (
8+
429, # HTTPStatus.TOO_MANY_REQUESTS
9+
500, # HTTPStatus.INTERNAL_SERVER_ERROR
10+
502, # HTTPStatus.BAD_GATEWAY
11+
503, # HTTPStatus.SERVICE_UNAVAILABLE
12+
504, # HTTPStatus.GATEWAY_TIMEOUT
13+
)
14+
15+
# All HTTP methods allowed, see this discussion:
16+
# https://corva.slack.com/archives/C0411LUPVL6/p1753451234091869
17+
ALLOWED_RETRY_METHODS = (
18+
"GET",
19+
"POST",
20+
"PUT",
21+
"PATCH",
22+
"DELETE",
23+
"OPTIONS",
24+
"HEAD",
25+
"TRACE",
26+
)
27+
28+
29+
def get_retry_strategy(max_retries: int, backoff_factor: float = 1) -> Retry:
30+
return Retry(
31+
total=max_retries,
32+
backoff_factor=backoff_factor,
33+
status_forcelist=RETRYABLE_STATUS_CODES,
34+
raise_on_status=False,
35+
allowed_methods=ALLOWED_RETRY_METHODS,
36+
)
37+
38+
39+
def get_requests_session(
40+
pool_connections_count: int,
41+
pool_max_size: int,
42+
pool_block: bool,
43+
retry_strategy: Optional[Retry] = None,
44+
) -> requests.Session:
45+
adapter = HTTPAdapter(
46+
max_retries=retry_strategy,
47+
pool_connections=pool_connections_count,
48+
pool_maxsize=pool_max_size,
49+
pool_block=pool_block,
50+
)
51+
52+
session = requests.Session()
53+
54+
session.mount('https://', adapter)
55+
session.mount('http://', adapter)
56+
57+
return session

src/corva/configuration.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,14 @@ class Settings(pydantic.BaseSettings):
2323
# secrets
2424
SECRETS_CACHE_TTL: int = int(datetime.timedelta(minutes=5).total_seconds())
2525

26+
# keep-alive
27+
POOL_CONNECTIONS_COUNT: int = 20 # Total pools count
28+
POOL_MAX_SIZE: int = 20 # Max connections count per pool/host
29+
POOL_BLOCK: bool = True # Wait until connection released
30+
31+
# retry
32+
MAX_RETRY_COUNT: int = 3 # If `0` then retries will be disabled
33+
BACKOFF_FACTOR: float = 1.0
34+
2635

2736
SETTINGS = Settings()

src/corva/handlers.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,6 @@ def wrapper(
173173
data_api_url=SETTINGS.DATA_API_ROOT_URL,
174174
api_key=api_key,
175175
app_key=SETTINGS.APP_KEY,
176-
timeout=None,
177176
app_connection_id=event.app_connection_id,
178177
)
179178

@@ -282,7 +281,6 @@ def wrapper(
282281
data_api_url=SETTINGS.DATA_API_ROOT_URL,
283282
api_key=api_key,
284283
app_key=SETTINGS.APP_KEY,
285-
timeout=None,
286284
app_connection_id=event.app_connection_id,
287285
)
288286

@@ -392,7 +390,6 @@ def wrapper(
392390
data_api_url=SETTINGS.DATA_API_ROOT_URL,
393391
api_key=api_key,
394392
app_key=SETTINGS.APP_KEY,
395-
timeout=None,
396393
app_connection_id=None,
397394
)
398395

0 commit comments

Comments
 (0)