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.
Files changed (25) hide show
  1. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/PKG-INFO +4 -4
  2. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/__init__.py +1 -1
  3. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/client.py +32 -16
  4. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/endpoints.py +22 -1
  5. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/fmp_endpoints.json +889 -120
  6. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/gui/manager.py +225 -2
  7. fmp_stable_api-2.0.2/fmp_stable_api/rate_limiter.py +58 -0
  8. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api.egg-info/PKG-INFO +4 -4
  9. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api.egg-info/SOURCES.txt +1 -2
  10. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api.egg-info/top_level.txt +0 -1
  11. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/pyproject.toml +4 -4
  12. fmp_stable_api-2.0.0/fmp_stable_api/rate_limiter.py +0 -37
  13. fmp_stable_api-2.0.0/tests/test_fmp.py +0 -108
  14. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/LICENSE +0 -0
  15. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/README.md +0 -0
  16. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/gui/FMP_icon.png +0 -0
  17. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/gui/__init__.py +0 -0
  18. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/key_manager.py +0 -0
  19. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/mcp_server.py +0 -0
  20. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/skill.md +0 -0
  21. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api/updater.py +0 -0
  22. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api.egg-info/dependency_links.txt +0 -0
  23. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api.egg-info/entry_points.txt +0 -0
  24. {fmp_stable_api-2.0.0 → fmp_stable_api-2.0.2}/fmp_stable_api.egg-info/requires.txt +0 -0
  25. {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.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/YOUR_USERNAME/YOUR_REPO
8
- Project-URL: Repository, https://github.com/YOUR_USERNAME/YOUR_REPO
9
- Project-URL: Bug Tracker, https://github.com/YOUR_USERNAME/YOUR_REPO/issues
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
@@ -2,5 +2,5 @@ from .client import FMP
2
2
  from .updater import update_endpoints
3
3
  from .key_manager import KeyManager
4
4
 
5
- __version__ = "1.0.0"
5
+ __version__ = "2.0.2"
6
6
  __all__ = ["FMP", "update_endpoints", "KeyManager"]
@@ -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
- print(f"\n {func_name}")
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
- if not self._rate_limiter.can_make_request():
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
- if not self._rate_limiter.can_make_request():
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
- from typing import Callable, Dict, List
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: