fmp-stable-api 2.0.0__tar.gz → 2.0.2__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.
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/PKG-INFO +4 -4
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/__init__.py +1 -1
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/client.py +32 -16
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/endpoints.py +22 -1
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/fmp_endpoints.json +889 -120
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/gui/manager.py +225 -2
- fmp_stable_api-2.0.2/fmp_stable_api/rate_limiter.py +58 -0
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api.egg-info/PKG-INFO +4 -4
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api.egg-info/SOURCES.txt +1 -2
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api.egg-info/top_level.txt +0 -1
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/pyproject.toml +4 -4
- fmp_stable_api-2.0.0/fmp_stable_api/rate_limiter.py +0 -37
- fmp_stable_api-2.0.0/tests/test_fmp.py +0 -108
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/LICENSE +0 -0
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/README.md +0 -0
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/gui/FMP_icon.png +0 -0
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/gui/__init__.py +0 -0
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/key_manager.py +0 -0
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/mcp_server.py +0 -0
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/skill.md +0 -0
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/updater.py +0 -0
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api.egg-info/dependency_links.txt +0 -0
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api.egg-info/entry_points.txt +0 -0
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api.egg-info/requires.txt +0 -0
- {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/setup.cfg +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fmp-stable-api
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Financial Modeling Prep API client — auto-updating endpoints, GUI, and MCP server
|
|
5
5
|
Author-email: FMP Dev <vr2969@g.rit.edu>
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
8
|
-
Project-URL: Repository, https://github.com/
|
|
9
|
-
Project-URL: Bug Tracker, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://github.com/Vimal-Seshadri-Raguraman/fmp-stable-api
|
|
8
|
+
Project-URL: Repository, https://github.com/Vimal-Seshadri-Raguraman/fmp-stable-api
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/Vimal-Seshadri-Raguraman/fmp-stable-api/issues
|
|
10
10
|
Keywords: finance,financial,modeling,prep,api,stock,market
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
@@ -26,6 +26,15 @@ API_KEY_TYPES = {
|
|
|
26
26
|
"Custom": None,
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
CLIENT_TYPE_TO_TIER = {
|
|
30
|
+
"Basic": "FREE",
|
|
31
|
+
"Starter": "STARTER",
|
|
32
|
+
"Premium": "PREMIUM",
|
|
33
|
+
"Ultimate": "ULTIMATE",
|
|
34
|
+
"Enterprise": "ULTIMATE",
|
|
35
|
+
"Custom": "ULTIMATE",
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
|
|
30
39
|
class CategoryProxy:
|
|
31
40
|
"""
|
|
@@ -35,10 +44,11 @@ class CategoryProxy:
|
|
|
35
44
|
client.Search.search_symbol(symbol="AAPL")
|
|
36
45
|
"""
|
|
37
46
|
|
|
38
|
-
def __init__(self, category_name: str, endpoints: Dict, make_request: Callable):
|
|
47
|
+
def __init__(self, category_name: str, endpoints: Dict, make_request: Callable, tier: str = "FREE"):
|
|
39
48
|
object.__setattr__(self, "_category_name", category_name)
|
|
40
49
|
object.__setattr__(self, "_endpoints", endpoints)
|
|
41
50
|
object.__setattr__(self, "_make_request", make_request)
|
|
51
|
+
object.__setattr__(self, "_tier", tier)
|
|
42
52
|
object.__setattr__(self, "_cache", {})
|
|
43
53
|
|
|
44
54
|
def __getattr__(self, name: str) -> Callable:
|
|
@@ -48,6 +58,7 @@ class CategoryProxy:
|
|
|
48
58
|
|
|
49
59
|
endpoints = object.__getattribute__(self, "_endpoints")
|
|
50
60
|
make_request = object.__getattribute__(self, "_make_request")
|
|
61
|
+
tier = object.__getattribute__(self, "_tier")
|
|
51
62
|
|
|
52
63
|
# Match by snake_case function name
|
|
53
64
|
for endpoint_key, config in endpoints.items():
|
|
@@ -60,6 +71,8 @@ class CategoryProxy:
|
|
|
60
71
|
required_params=config.get("required_params", []),
|
|
61
72
|
optional_params=config.get("optional_params", []),
|
|
62
73
|
make_request=make_request,
|
|
74
|
+
access=config.get("access", {}),
|
|
75
|
+
tier=tier,
|
|
63
76
|
)
|
|
64
77
|
fn.__name__ = name
|
|
65
78
|
cache[name] = fn
|
|
@@ -71,15 +84,19 @@ class CategoryProxy:
|
|
|
71
84
|
def help(self) -> None:
|
|
72
85
|
category = object.__getattribute__(self, "_category_name")
|
|
73
86
|
endpoints = object.__getattribute__(self, "_endpoints")
|
|
87
|
+
tier = object.__getattribute__(self, "_tier")
|
|
74
88
|
print(f"\n{'='*60}")
|
|
75
|
-
print(f"Category: {category}")
|
|
89
|
+
print(f"Category: {category} (your tier: {tier})")
|
|
76
90
|
print(f"{'='*60}")
|
|
77
91
|
for endpoint_key, config in endpoints.items():
|
|
78
92
|
func_name = endpoint_key.replace("-", "_").replace(" ", "_").lower()
|
|
79
93
|
desc = config.get("description", "")
|
|
80
94
|
req = [safe_param_name(p) for p in config.get("required_params", [])]
|
|
81
95
|
opt = [safe_param_name(p) for p in config.get("optional_params", [])]
|
|
82
|
-
|
|
96
|
+
access = config.get("access", {})
|
|
97
|
+
tier_access = access.get(tier, "FULL") if access else "FULL"
|
|
98
|
+
badge = "" if tier_access == "FULL" else f" [{tier_access}]"
|
|
99
|
+
print(f"\n {func_name}{badge}")
|
|
83
100
|
if desc:
|
|
84
101
|
print(f" {desc}")
|
|
85
102
|
if req:
|
|
@@ -155,12 +172,20 @@ class FMP:
|
|
|
155
172
|
|
|
156
173
|
self._api_key = client_key
|
|
157
174
|
self._client_type = client_type
|
|
175
|
+
self._tier = CLIENT_TYPE_TO_TIER.get(client_type, "FREE")
|
|
158
176
|
self._minute_limit = minute_limit
|
|
159
177
|
self._rate_limiter = RateLimiter(minute_limit)
|
|
160
178
|
|
|
161
|
-
# Load config and derive base URL
|
|
179
|
+
# Load config and derive base URL; auto-refresh if cache is stale (>24h)
|
|
162
180
|
self._config = load_config()
|
|
163
181
|
self._base_url = get_base_url(self._config)
|
|
182
|
+
try:
|
|
183
|
+
from .updater import update_endpoints
|
|
184
|
+
if update_endpoints():
|
|
185
|
+
self._config = load_config()
|
|
186
|
+
self._base_url = get_base_url(self._config)
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
164
189
|
|
|
165
190
|
# HTTP session with retry
|
|
166
191
|
self._session = self._build_session()
|
|
@@ -206,7 +231,7 @@ class FMP:
|
|
|
206
231
|
config = load_config()
|
|
207
232
|
category_key = category_map[name]
|
|
208
233
|
endpoints_dict = list_endpoints(config, category_key)
|
|
209
|
-
proxy = CategoryProxy(name, endpoints_dict, self._make_request)
|
|
234
|
+
proxy = CategoryProxy(name, endpoints_dict, self._make_request, self._tier)
|
|
210
235
|
self.__dict__[name] = proxy
|
|
211
236
|
return proxy
|
|
212
237
|
|
|
@@ -216,19 +241,13 @@ class FMP:
|
|
|
216
241
|
|
|
217
242
|
def request(self, url: str, params: Optional[Dict] = None) -> Dict:
|
|
218
243
|
"""Make a GET request to any FMP URL."""
|
|
219
|
-
|
|
220
|
-
wait = self._rate_limiter.seconds_until_reset()
|
|
221
|
-
raise Exception(
|
|
222
|
-
f"Rate limit reached ({self._minute_limit} req/min). "
|
|
223
|
-
f"Resets in {wait:.0f}s."
|
|
224
|
-
)
|
|
244
|
+
self._rate_limiter.acquire()
|
|
225
245
|
|
|
226
246
|
if params is None:
|
|
227
247
|
params = {}
|
|
228
248
|
params["apikey"] = self._api_key
|
|
229
249
|
|
|
230
250
|
response = self._session.get(url, params=params, timeout=30)
|
|
231
|
-
self._rate_limiter.record_request()
|
|
232
251
|
|
|
233
252
|
if response.status_code == 429:
|
|
234
253
|
time.sleep(60)
|
|
@@ -250,16 +269,13 @@ class FMP:
|
|
|
250
269
|
filename: Optional[str] = None,
|
|
251
270
|
) -> str:
|
|
252
271
|
"""Download a URL to a file; returns the saved filename."""
|
|
253
|
-
|
|
254
|
-
wait = self._rate_limiter.seconds_until_reset()
|
|
255
|
-
raise Exception(f"Rate limit reached. Resets in {wait:.0f}s.")
|
|
272
|
+
self._rate_limiter.acquire()
|
|
256
273
|
|
|
257
274
|
if params is None:
|
|
258
275
|
params = {}
|
|
259
276
|
params["apikey"] = self._api_key
|
|
260
277
|
|
|
261
278
|
response = self._session.get(url, params=params, timeout=30)
|
|
262
|
-
self._rate_limiter.record_request()
|
|
263
279
|
|
|
264
280
|
if response.status_code == 401:
|
|
265
281
|
raise Exception("Invalid API key.")
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
import json
|
|
3
3
|
import keyword
|
|
4
4
|
import os
|
|
5
|
-
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Callable, Dict, List, Optional
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
_BUNDLED = os.path.join(os.path.dirname(__file__), "fmp_endpoints.json")
|
|
9
10
|
|
|
11
|
+
TIERS = ["FREE", "STARTER", "PREMIUM", "ULTIMATE"]
|
|
12
|
+
ACCESS_LEVELS = ["FULL", "LIMITED", "NO_ACCESS"]
|
|
13
|
+
DEFAULT_ACCESS = {tier: "FULL" for tier in TIERS}
|
|
14
|
+
|
|
10
15
|
|
|
11
16
|
def load_config() -> Dict:
|
|
12
17
|
"""Load endpoints config via updater if available, else read bundled file."""
|
|
@@ -44,6 +49,8 @@ def build_endpoint_func(
|
|
|
44
49
|
required_params: List[str],
|
|
45
50
|
optional_params: List[str],
|
|
46
51
|
make_request: Callable,
|
|
52
|
+
access: Optional[Dict] = None,
|
|
53
|
+
tier: Optional[str] = None,
|
|
47
54
|
) -> Callable:
|
|
48
55
|
"""
|
|
49
56
|
Return a callable that validates required params and calls make_request.
|
|
@@ -56,6 +63,20 @@ def build_endpoint_func(
|
|
|
56
63
|
all_safe = set(safe_required + safe_optional)
|
|
57
64
|
|
|
58
65
|
def endpoint_func(**kwargs):
|
|
66
|
+
if tier and access:
|
|
67
|
+
tier_access = access.get(tier, "FULL")
|
|
68
|
+
if tier_access == "NO_ACCESS":
|
|
69
|
+
raise PermissionError(
|
|
70
|
+
f"This endpoint is not available on the {tier} tier. "
|
|
71
|
+
"Upgrade your plan to access it."
|
|
72
|
+
)
|
|
73
|
+
if tier_access == "LIMITED":
|
|
74
|
+
warnings.warn(
|
|
75
|
+
f"This endpoint has LIMITED access on the {tier} tier. "
|
|
76
|
+
"The response may be restricted.",
|
|
77
|
+
stacklevel=2,
|
|
78
|
+
)
|
|
79
|
+
|
|
59
80
|
# Validate required params
|
|
60
81
|
for param in safe_required:
|
|
61
82
|
if param not in kwargs:
|