financialmodelingprep-sdk 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AakshRanjan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: financialmodelingprep-sdk
3
+ Version: 0.1.0
4
+ Summary: Async Python SDK for the Financial Modeling Prep stable API.
5
+ Keywords: fmp,financial-modeling-prep,finance,stocks,sdk
6
+ Author: Aaksh Ranjan
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Typing :: Typed
16
+ Requires-Dist: aiohttp>=3.14.3
17
+ Requires-Dist: pydantic>=2.13.5
18
+ Requires-Dist: tenacity>=9.1.4
19
+ Requires-Python: >=3.12
20
+ Project-URL: Homepage, https://github.com/AakshRanjan/Finance-Intelligence-Graph
21
+ Project-URL: Repository, https://github.com/AakshRanjan/Finance-Intelligence-Graph
22
+ Project-URL: Issues, https://github.com/AakshRanjan/Finance-Intelligence-Graph/issues
23
+ Project-URL: Changelog, https://github.com/AakshRanjan/Finance-Intelligence-Graph/blob/master/FMP-SDK/CHANGELOG.md
24
+ Description-Content-Type: text/markdown
25
+
26
+ # financialmodelingprep-sdk
27
+
28
+ Async Python SDK for the [Financial Modeling Prep](https://site.financialmodelingprep.com/) stable API. Responses are validated with Pydantic; HTTP calls retry through a shared session.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install financialmodelingprep-sdk
34
+ ```
35
+
36
+ Requires Python 3.12+. You need an FMP API key.
37
+
38
+ ## Usage
39
+
40
+ ```python
41
+ import asyncio
42
+
43
+ from fmp_sdk import FMPSession
44
+
45
+
46
+ async def main() -> None:
47
+ async with FMPSession("YOUR_API_KEY") as session:
48
+ bars = await session.chart("AAPL").historical_price_eod_light()
49
+ print(bars[0].date, bars[0].price)
50
+
51
+
52
+ asyncio.run(main())
53
+ ```
54
+
55
+ `FMPSession.chart(symbol)` binds a symbol so you do not pass it on every call. You can also pass `symbol=` per request.
56
+
57
+ ## Versioning
58
+
59
+ Releases are cut automatically from Conventional Commits that touch this package:
60
+
61
+ - `feat:` — minor
62
+ - `fix:` / `perf:` — patch
63
+ - `feat!:` or a `BREAKING CHANGE:` footer — major (stays 0.x until 1.0)
64
+
65
+ See [CHANGELOG.md](CHANGELOG.md) for published versions.
@@ -0,0 +1,40 @@
1
+ # financialmodelingprep-sdk
2
+
3
+ Async Python SDK for the [Financial Modeling Prep](https://site.financialmodelingprep.com/) stable API. Responses are validated with Pydantic; HTTP calls retry through a shared session.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install financialmodelingprep-sdk
9
+ ```
10
+
11
+ Requires Python 3.12+. You need an FMP API key.
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ import asyncio
17
+
18
+ from fmp_sdk import FMPSession
19
+
20
+
21
+ async def main() -> None:
22
+ async with FMPSession("YOUR_API_KEY") as session:
23
+ bars = await session.chart("AAPL").historical_price_eod_light()
24
+ print(bars[0].date, bars[0].price)
25
+
26
+
27
+ asyncio.run(main())
28
+ ```
29
+
30
+ `FMPSession.chart(symbol)` binds a symbol so you do not pass it on every call. You can also pass `symbol=` per request.
31
+
32
+ ## Versioning
33
+
34
+ Releases are cut automatically from Conventional Commits that touch this package:
35
+
36
+ - `feat:` — minor
37
+ - `fix:` / `perf:` — patch
38
+ - `feat!:` or a `BREAKING CHANGE:` footer — major (stays 0.x until 1.0)
39
+
40
+ See [CHANGELOG.md](CHANGELOG.md) for published versions.
@@ -0,0 +1,70 @@
1
+ [project]
2
+ name = "financialmodelingprep-sdk"
3
+ version = "0.1.0"
4
+ description = "Async Python SDK for the Financial Modeling Prep stable API."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ keywords = [
10
+ "fmp",
11
+ "financial-modeling-prep",
12
+ "finance",
13
+ "stocks",
14
+ "sdk",
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = [
26
+ "aiohttp>=3.14.3",
27
+ "pydantic>=2.13.5",
28
+ "tenacity>=9.1.4",
29
+ ]
30
+
31
+ [[project.authors]]
32
+ name = "Aaksh Ranjan"
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/AakshRanjan/Finance-Intelligence-Graph"
36
+ Repository = "https://github.com/AakshRanjan/Finance-Intelligence-Graph"
37
+ Issues = "https://github.com/AakshRanjan/Finance-Intelligence-Graph/issues"
38
+ Changelog = "https://github.com/AakshRanjan/Finance-Intelligence-Graph/blob/master/FMP-SDK/CHANGELOG.md"
39
+
40
+ [build-system]
41
+ requires = ["uv_build>=0.9.10,<0.10.0"]
42
+ build-backend = "uv_build"
43
+
44
+ [tool.uv.build-backend]
45
+ module-name = "fmp_sdk"
46
+
47
+ [tool.semantic_release]
48
+ commit_parser = "conventional-monorepo"
49
+ version_toml = ["pyproject.toml:project.version"]
50
+ tag_format = "fmp-sdk-v{version}"
51
+ allow_zero_version = true
52
+ major_on_zero = false
53
+ assets = ["uv.lock"]
54
+ commit_message = "chore(release): fmp-sdk@{version}"
55
+ build_command = """
56
+ pip install uv
57
+ uv lock --upgrade-package "$PACKAGE_NAME"
58
+ git add uv.lock
59
+ uv build
60
+ """
61
+
62
+ [tool.semantic_release.commit_parser_options]
63
+ path_filters = ["."]
64
+
65
+ [tool.semantic_release.changelog]
66
+ mode = "update"
67
+ insertion_flag = "<!-- version list -->"
68
+
69
+ [tool.semantic_release.changelog.default_templates]
70
+ changelog_file = "CHANGELOG.md"
@@ -0,0 +1,68 @@
1
+ [project]
2
+ name = "financialmodelingprep-sdk"
3
+ version = "0.1.0"
4
+ description = "Async Python SDK for the Financial Modeling Prep stable API."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Aaksh Ranjan" }]
9
+ requires-python = ">=3.12"
10
+ keywords = [
11
+ "fmp",
12
+ "financial-modeling-prep",
13
+ "finance",
14
+ "stocks",
15
+ "sdk",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = [
27
+ "aiohttp>=3.14.3",
28
+ "pydantic>=2.13.5",
29
+ "tenacity>=9.1.4",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/AakshRanjan/Finance-Intelligence-Graph"
34
+ Repository = "https://github.com/AakshRanjan/Finance-Intelligence-Graph"
35
+ Issues = "https://github.com/AakshRanjan/Finance-Intelligence-Graph/issues"
36
+ Changelog = "https://github.com/AakshRanjan/Finance-Intelligence-Graph/blob/master/FMP-SDK/CHANGELOG.md"
37
+
38
+ [build-system]
39
+ requires = ["uv_build>=0.9.10,<0.10.0"]
40
+ build-backend = "uv_build"
41
+
42
+ [tool.uv.build-backend]
43
+ module-name = "fmp_sdk"
44
+
45
+ [tool.semantic_release]
46
+ commit_parser = "conventional-monorepo"
47
+ version_toml = ["pyproject.toml:project.version"]
48
+ tag_format = "fmp-sdk-v{version}"
49
+ allow_zero_version = true
50
+ major_on_zero = false
51
+ assets = ["uv.lock"]
52
+ commit_message = "chore(release): fmp-sdk@{version}"
53
+ build_command = """
54
+ pip install uv
55
+ uv lock --upgrade-package "$PACKAGE_NAME"
56
+ git add uv.lock
57
+ uv build
58
+ """
59
+
60
+ [tool.semantic_release.commit_parser_options]
61
+ path_filters = ["."]
62
+
63
+ [tool.semantic_release.changelog]
64
+ mode = "update"
65
+ insertion_flag = "<!-- version list -->"
66
+
67
+ [tool.semantic_release.changelog.default_templates]
68
+ changelog_file = "CHANGELOG.md"
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import TYPE_CHECKING, Any, TypeVar
5
+
6
+ import aiohttp
7
+ from fmp_sdk.exception import FMPResponseError, _RetryableHTTPError
8
+ from fmp_sdk.utils.retrySession import RetrySession
9
+ from pydantic import TypeAdapter, ValidationError
10
+
11
+ if TYPE_CHECKING:
12
+ from fmp_sdk.chart.chart import Chart
13
+
14
+ T = TypeVar("T")
15
+
16
+
17
+ class FMPSession:
18
+ """Async Financial Modeling Prep client backed by RetrySession."""
19
+
20
+ BASE_URL = "https://financialmodelingprep.com/stable"
21
+
22
+ def __init__(
23
+ self,
24
+ api_key: str,
25
+ *,
26
+ base_url: str = BASE_URL,
27
+ **retry_kwargs: Any,
28
+ ) -> None:
29
+ self._api_key = api_key
30
+ self._base_url = base_url.rstrip("/")
31
+ self._session = RetrySession(
32
+ headers={"apikey": api_key},
33
+ **retry_kwargs,
34
+ )
35
+
36
+ async def __aenter__(self) -> FMPSession:
37
+ await self._session.__aenter__()
38
+ return self
39
+
40
+ async def __aexit__(self, *exc: object) -> None:
41
+ await self._session.__aexit__(*exc)
42
+
43
+ async def close(self) -> None:
44
+ await self._session.close()
45
+
46
+ def chart(self, symbol: str | None = None) -> Chart:
47
+ from fmp_sdk.chart.chart import Chart
48
+
49
+ return Chart(self, symbol)
50
+
51
+ async def get(
52
+ self,
53
+ path: str,
54
+ *,
55
+ response_model: type[T],
56
+ **params: Any,
57
+ ) -> T:
58
+ """GET a stable API path and return a validated pydantic payload."""
59
+ url = f"{self._base_url}/{path.lstrip('/')}"
60
+ query = {key: value for key, value in params.items() if value is not None}
61
+ response = await self._session.get(url, params=query)
62
+ try:
63
+ response.raise_for_status()
64
+ data = await response.json()
65
+ finally:
66
+ response.release()
67
+ try:
68
+ return TypeAdapter(response_model).validate_python(data)
69
+ except ValidationError as exc:
70
+ raise FMPResponseError(path, exc) from exc
71
+
72
+ async def check_connectivity(self) -> bool:
73
+ """Return True if the FMP API is reachable and accepts the API key."""
74
+ url = f"{self._base_url}/search-symbol"
75
+ try:
76
+ response = await self._session.get(url, params={"query": "AAPL"})
77
+ try:
78
+ return response.status == 200
79
+ finally:
80
+ response.release()
81
+ except (
82
+ aiohttp.ClientError,
83
+ asyncio.TimeoutError,
84
+ _RetryableHTTPError,
85
+ ):
86
+ return False
@@ -0,0 +1,27 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from fmp_sdk.FMPSession import FMPSession
4
+ from fmp_sdk.chart import (
5
+ Chart,
6
+ HistoricalChartBar,
7
+ HistoricalPriceEodAdjusted,
8
+ HistoricalPriceEodFull,
9
+ HistoricalPriceEodLight,
10
+ )
11
+ from fmp_sdk.exception import FMPResponseError
12
+
13
+ try:
14
+ __version__ = version("financialmodelingprep-sdk")
15
+ except PackageNotFoundError:
16
+ __version__ = "0.0.0"
17
+
18
+ __all__ = [
19
+ "Chart",
20
+ "FMPResponseError",
21
+ "FMPSession",
22
+ "HistoricalChartBar",
23
+ "HistoricalPriceEodAdjusted",
24
+ "HistoricalPriceEodFull",
25
+ "HistoricalPriceEodLight",
26
+ "__version__",
27
+ ]
@@ -0,0 +1,17 @@
1
+ from fmp_sdk.chart.chart import Chart
2
+ from fmp_sdk.chart.models import (
3
+ HistoricalChartBar,
4
+ HistoricalPriceEodAdjusted,
5
+ HistoricalPriceEodFull,
6
+ HistoricalPriceEodLight,
7
+ )
8
+ from fmp_sdk.exception import FMPResponseError
9
+
10
+ __all__ = [
11
+ "Chart",
12
+ "FMPResponseError",
13
+ "HistoricalChartBar",
14
+ "HistoricalPriceEodAdjusted",
15
+ "HistoricalPriceEodFull",
16
+ "HistoricalPriceEodLight",
17
+ ]
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Literal
4
+
5
+ from fmp_sdk.chart.models import (
6
+ HistoricalChartBar,
7
+ HistoricalPriceEodAdjusted,
8
+ HistoricalPriceEodFull,
9
+ HistoricalPriceEodLight,
10
+ )
11
+
12
+ if TYPE_CHECKING:
13
+ from fmp_sdk.FMPSession import FMPSession
14
+
15
+ ChartInterval = Literal["1min", "5min", "15min", "30min", "1hour", "4hour"]
16
+
17
+
18
+ def _normalize_symbol(symbol: str | None) -> str | None:
19
+ if symbol is None:
20
+ return None
21
+ normalized = symbol.strip().upper()
22
+ return normalized or None
23
+
24
+
25
+ class Chart:
26
+ """FMP Charts endpoints for one optional bound symbol."""
27
+
28
+ def __init__(
29
+ self,
30
+ session: FMPSession,
31
+ symbol: str | None = None,
32
+ ) -> None:
33
+ self._session = session
34
+ self._symbol = _normalize_symbol(symbol)
35
+
36
+ def _resolve_symbol(self, symbol: str | None) -> str:
37
+ resolved = _normalize_symbol(symbol) or self._symbol
38
+ if resolved is None:
39
+ raise ValueError("symbol is required")
40
+ return resolved
41
+
42
+ def _query(
43
+ self,
44
+ symbol: str | None,
45
+ from_: str | None,
46
+ to: str | None,
47
+ ) -> dict[str, str | None]:
48
+ return {
49
+ "symbol": self._resolve_symbol(symbol),
50
+ "from": from_,
51
+ "to": to,
52
+ }
53
+
54
+ async def historical_price_eod_light(
55
+ self,
56
+ symbol: str | None = None,
57
+ *,
58
+ from_: str | None = None,
59
+ to: str | None = None,
60
+ ) -> list[HistoricalPriceEodLight]:
61
+ return await self._session.get(
62
+ "historical-price-eod/light",
63
+ response_model=list[HistoricalPriceEodLight],
64
+ **self._query(symbol, from_, to),
65
+ )
66
+
67
+ async def historical_price_eod_full(
68
+ self,
69
+ symbol: str | None = None,
70
+ *,
71
+ from_: str | None = None,
72
+ to: str | None = None,
73
+ ) -> list[HistoricalPriceEodFull]:
74
+ return await self._session.get(
75
+ "historical-price-eod/full",
76
+ response_model=list[HistoricalPriceEodFull],
77
+ **self._query(symbol, from_, to),
78
+ )
79
+
80
+ async def historical_price_eod_non_split_adjusted(
81
+ self,
82
+ symbol: str | None = None,
83
+ *,
84
+ from_: str | None = None,
85
+ to: str | None = None,
86
+ ) -> list[HistoricalPriceEodAdjusted]:
87
+ return await self._session.get(
88
+ "historical-price-eod/non-split-adjusted",
89
+ response_model=list[HistoricalPriceEodAdjusted],
90
+ **self._query(symbol, from_, to),
91
+ )
92
+
93
+ async def historical_price_eod_dividend_adjusted(
94
+ self,
95
+ symbol: str | None = None,
96
+ *,
97
+ from_: str | None = None,
98
+ to: str | None = None,
99
+ ) -> list[HistoricalPriceEodAdjusted]:
100
+ return await self._session.get(
101
+ "historical-price-eod/dividend-adjusted",
102
+ response_model=list[HistoricalPriceEodAdjusted],
103
+ **self._query(symbol, from_, to),
104
+ )
105
+
106
+ async def historical_chart(
107
+ self,
108
+ interval: ChartInterval,
109
+ symbol: str | None = None,
110
+ *,
111
+ from_: str | None = None,
112
+ to: str | None = None,
113
+ ) -> list[HistoricalChartBar]:
114
+ return await self._session.get(
115
+ f"historical-chart/{interval}",
116
+ response_model=list[HistoricalChartBar],
117
+ **self._query(symbol, from_, to),
118
+ )
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date, datetime
4
+
5
+ from pydantic import Field
6
+
7
+ from fmp_sdk.models import FMPBaseModel
8
+
9
+
10
+ class HistoricalPriceEodLight(FMPBaseModel):
11
+ """Row from GET /stable/historical-price-eod/light."""
12
+
13
+ symbol: str
14
+ date: date
15
+ price: float
16
+ volume: float
17
+
18
+
19
+ class HistoricalPriceEodFull(FMPBaseModel):
20
+ """Row from GET /stable/historical-price-eod/full."""
21
+
22
+ symbol: str
23
+ date: date
24
+ open: float
25
+ high: float
26
+ low: float
27
+ close: float
28
+ volume: float
29
+ change: float
30
+ change_percent: float = Field(alias="changePercent")
31
+ vwap: float
32
+
33
+
34
+ class HistoricalPriceEodAdjusted(FMPBaseModel):
35
+ """Row from non-split-adjusted and dividend-adjusted EOD endpoints."""
36
+
37
+ symbol: str
38
+ date: date
39
+ adj_open: float = Field(alias="adjOpen")
40
+ adj_high: float = Field(alias="adjHigh")
41
+ adj_low: float = Field(alias="adjLow")
42
+ adj_close: float = Field(alias="adjClose")
43
+ volume: float
44
+
45
+
46
+ class HistoricalChartBar(FMPBaseModel):
47
+ """Row from GET /stable/historical-chart/{interval}."""
48
+
49
+ date: datetime
50
+ open: float
51
+ high: float
52
+ low: float
53
+ close: float
54
+ volume: float
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ import aiohttp
4
+ from pydantic import ValidationError
5
+
6
+
7
+ class _RetryableHTTPError(Exception):
8
+ """Raised internally when an HTTP status should trigger a retry."""
9
+
10
+ def __init__(self, response: aiohttp.ClientResponse) -> None:
11
+ self.response = response
12
+ super().__init__(
13
+ f"Retryable HTTP {response.status} for {response.method} {response.url}"
14
+ )
15
+
16
+
17
+ class FMPResponseError(Exception):
18
+ """Raised when an FMP JSON payload does not match the expected model."""
19
+
20
+ def __init__(self, path: str, error: ValidationError) -> None:
21
+ self.path = path
22
+ self.error = error
23
+ super().__init__(f"Invalid FMP response for {path}: {error}")
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+
6
+ class FMPBaseModel(BaseModel):
7
+ """Shared pydantic config for FMP JSON payloads."""
8
+
9
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
File without changes
@@ -0,0 +1,3 @@
1
+ from fmp_sdk.utils.retrySession import RetrySession
2
+
3
+ __all__ = ["RetrySession"]
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ import aiohttp
7
+ from fmp_sdk.exception import _RetryableHTTPError
8
+ from tenacity import (
9
+ AsyncRetrying,
10
+ retry_if_exception_type,
11
+ stop_after_attempt,
12
+ wait_exponential,
13
+ )
14
+
15
+
16
+ class RetrySession:
17
+ """Thin aiohttp.ClientSession wrapper with tenacity retries."""
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ session: aiohttp.ClientSession | None = None,
23
+ max_attempts: int = 5,
24
+ retry_statuses: frozenset[int] = frozenset({429, 500, 502, 503, 504}),
25
+ wait_multiplier: float = 1.0,
26
+ wait_max: float = 60.0,
27
+ **session_kwargs: Any,
28
+ ) -> None:
29
+ self._external_session = session
30
+ self._session = session
31
+ self._session_kwargs = session_kwargs
32
+ self._owns_session = session is None
33
+ self._max_attempts = max_attempts
34
+ self._retry_statuses = retry_statuses
35
+ self._wait_multiplier = wait_multiplier
36
+ self._wait_max = wait_max
37
+
38
+ def _ensure_session(self) -> aiohttp.ClientSession:
39
+ if self._session is None or self._session.closed:
40
+ self._session = aiohttp.ClientSession(**self._session_kwargs)
41
+ self._owns_session = True
42
+ return self._session
43
+
44
+ async def __aenter__(self) -> RetrySession:
45
+ self._ensure_session()
46
+ return self
47
+
48
+ async def __aexit__(self, *exc: object) -> None:
49
+ await self.close()
50
+
51
+ async def close(self) -> None:
52
+ if (
53
+ self._owns_session
54
+ and self._session is not None
55
+ and not self._session.closed
56
+ ):
57
+ await self._session.close()
58
+ self._session = None
59
+
60
+ async def request(
61
+ self, method: str, url: str, **kwargs: Any
62
+ ) -> aiohttp.ClientResponse:
63
+ session = self._ensure_session()
64
+
65
+ try:
66
+ async for attempt in AsyncRetrying(
67
+ stop=stop_after_attempt(self._max_attempts),
68
+ wait=wait_exponential(
69
+ multiplier=self._wait_multiplier,
70
+ max=self._wait_max,
71
+ ),
72
+ retry=retry_if_exception_type(
73
+ (
74
+ aiohttp.ClientConnectionError,
75
+ aiohttp.ServerTimeoutError,
76
+ asyncio.TimeoutError,
77
+ _RetryableHTTPError,
78
+ )
79
+ ),
80
+ reraise=True,
81
+ ):
82
+ with attempt:
83
+ response = await session.request(method, url, **kwargs)
84
+ if response.status in self._retry_statuses:
85
+ await response.read()
86
+ response.release()
87
+ raise _RetryableHTTPError(response)
88
+ return response
89
+ except _RetryableHTTPError as exc:
90
+ response = exc.response
91
+ raise aiohttp.ClientResponseError(
92
+ request_info=response.request_info,
93
+ history=response.history,
94
+ status=response.status,
95
+ message=response.reason or "",
96
+ headers=response.headers,
97
+ ) from exc
98
+
99
+ raise RuntimeError(
100
+ "RetrySession.request completed without a response"
101
+ ) # pragma: no cover
102
+
103
+ async def get(self, url: str, **kwargs: Any) -> aiohttp.ClientResponse:
104
+ return await self.request("GET", url, **kwargs)
105
+
106
+ async def post(self, url: str, **kwargs: Any) -> aiohttp.ClientResponse:
107
+ return await self.request("POST", url, **kwargs)
108
+
109
+ async def patch(self, url: str, **kwargs: Any) -> aiohttp.ClientResponse:
110
+ return await self.request("PATCH", url, **kwargs)
111
+
112
+ async def delete(self, url: str, **kwargs: Any) -> aiohttp.ClientResponse:
113
+ return await self.request("DELETE", url, **kwargs)