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.
- financialmodelingprep_sdk-0.1.0/LICENSE +21 -0
- financialmodelingprep_sdk-0.1.0/PKG-INFO +65 -0
- financialmodelingprep_sdk-0.1.0/README.md +40 -0
- financialmodelingprep_sdk-0.1.0/pyproject.toml +70 -0
- financialmodelingprep_sdk-0.1.0/pyproject.toml.orig +68 -0
- financialmodelingprep_sdk-0.1.0/src/fmp_sdk/FMPSession.py +86 -0
- financialmodelingprep_sdk-0.1.0/src/fmp_sdk/__init__.py +27 -0
- financialmodelingprep_sdk-0.1.0/src/fmp_sdk/chart/__init__.py +17 -0
- financialmodelingprep_sdk-0.1.0/src/fmp_sdk/chart/chart.py +118 -0
- financialmodelingprep_sdk-0.1.0/src/fmp_sdk/chart/models.py +54 -0
- financialmodelingprep_sdk-0.1.0/src/fmp_sdk/exception.py +23 -0
- financialmodelingprep_sdk-0.1.0/src/fmp_sdk/models.py +9 -0
- financialmodelingprep_sdk-0.1.0/src/fmp_sdk/py.typed +0 -0
- financialmodelingprep_sdk-0.1.0/src/fmp_sdk/utils/__init__.py +3 -0
- financialmodelingprep_sdk-0.1.0/src/fmp_sdk/utils/retrySession.py +113 -0
|
@@ -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}")
|
|
File without changes
|
|
@@ -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)
|