fmp-data 0.0.0__py3-none-any.whl
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_data/__init__.py +138 -0
- fmp_data/alternative/__init__.py +34 -0
- fmp_data/alternative/client.py +150 -0
- fmp_data/alternative/endpoints.py +533 -0
- fmp_data/alternative/mapping.py +737 -0
- fmp_data/alternative/models.py +296 -0
- fmp_data/alternative/schema.py +165 -0
- fmp_data/base.py +316 -0
- fmp_data/client.py +269 -0
- fmp_data/company/__init__.py +40 -0
- fmp_data/company/client.py +238 -0
- fmp_data/company/endpoints.py +711 -0
- fmp_data/company/hints.py +77 -0
- fmp_data/company/mapping.py +1361 -0
- fmp_data/company/models.py +543 -0
- fmp_data/company/schema.py +132 -0
- fmp_data/config.py +207 -0
- fmp_data/economics/__init__.py +16 -0
- fmp_data/economics/client.py +52 -0
- fmp_data/economics/endpoints.py +164 -0
- fmp_data/economics/mapping.py +641 -0
- fmp_data/economics/models.py +75 -0
- fmp_data/economics/schema.py +91 -0
- fmp_data/exceptions.py +54 -0
- fmp_data/fundamental/__init__.py +40 -0
- fmp_data/fundamental/client.py +87 -0
- fmp_data/fundamental/endpoints.py +403 -0
- fmp_data/fundamental/mapping.py +867 -0
- fmp_data/fundamental/models.py +913 -0
- fmp_data/fundamental/schema.py +40 -0
- fmp_data/institutional/__init__.py +26 -0
- fmp_data/institutional/client.py +141 -0
- fmp_data/institutional/endpoints.py +321 -0
- fmp_data/institutional/mapping.py +749 -0
- fmp_data/institutional/models.py +301 -0
- fmp_data/institutional/schema.py +99 -0
- fmp_data/intelligence/__init__.py +58 -0
- fmp_data/intelligence/client.py +331 -0
- fmp_data/intelligence/endpoints.py +788 -0
- fmp_data/intelligence/mapping.py +1677 -0
- fmp_data/intelligence/models.py +707 -0
- fmp_data/intelligence/schema.py +57 -0
- fmp_data/investment/__init__.py +24 -0
- fmp_data/investment/client.py +104 -0
- fmp_data/investment/endpoints.py +241 -0
- fmp_data/investment/mapping.py +658 -0
- fmp_data/investment/models.py +220 -0
- fmp_data/investment/schema.py +106 -0
- fmp_data/lc/__init__.py +256 -0
- fmp_data/lc/config.py +88 -0
- fmp_data/lc/embedding.py +140 -0
- fmp_data/lc/hints.py +66 -0
- fmp_data/lc/mapping.py +107 -0
- fmp_data/lc/models.py +98 -0
- fmp_data/lc/registry.py +693 -0
- fmp_data/lc/utils.py +35 -0
- fmp_data/lc/validation.py +267 -0
- fmp_data/lc/vector_store.py +592 -0
- fmp_data/logger.py +358 -0
- fmp_data/market/__init__.py +18 -0
- fmp_data/market/client.py +106 -0
- fmp_data/market/endpoints.py +358 -0
- fmp_data/market/hints.py +22 -0
- fmp_data/market/mapping.py +854 -0
- fmp_data/market/models.py +310 -0
- fmp_data/market/schema.py +186 -0
- fmp_data/mcp/__init__.py +0 -0
- fmp_data/mcp/server.py +101 -0
- fmp_data/mcp/tool_loader.py +74 -0
- fmp_data/mcp/tools_manifest.py +17 -0
- fmp_data/models.py +265 -0
- fmp_data/rate_limit.py +136 -0
- fmp_data/schema.py +158 -0
- fmp_data/technical/__init__.py +28 -0
- fmp_data/technical/client.py +214 -0
- fmp_data/technical/endpoints.py +102 -0
- fmp_data/technical/mapping.py +452 -0
- fmp_data/technical/models.py +87 -0
- fmp_data/technical/schema.py +261 -0
- fmp_data-0.0.0.dist-info/METADATA +732 -0
- fmp_data-0.0.0.dist-info/RECORD +84 -0
- fmp_data-0.0.0.dist-info/WHEEL +4 -0
- fmp_data-0.0.0.dist-info/entry_points.txt +10 -0
- fmp_data-0.0.0.dist-info/licenses/LICENSE +21 -0
fmp_data/base.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# fmp_data/base.py
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from tenacity import (
|
|
11
|
+
after_log,
|
|
12
|
+
before_sleep_log,
|
|
13
|
+
retry,
|
|
14
|
+
retry_if_exception_type,
|
|
15
|
+
stop_after_attempt,
|
|
16
|
+
wait_exponential,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from fmp_data.config import ClientConfig
|
|
20
|
+
from fmp_data.exceptions import (
|
|
21
|
+
AuthenticationError,
|
|
22
|
+
FMPError,
|
|
23
|
+
RateLimitError,
|
|
24
|
+
ValidationError,
|
|
25
|
+
)
|
|
26
|
+
from fmp_data.logger import FMPLogger, log_api_call
|
|
27
|
+
from fmp_data.models import Endpoint
|
|
28
|
+
from fmp_data.rate_limit import FMPRateLimiter, QuotaConfig
|
|
29
|
+
|
|
30
|
+
T = TypeVar("T", bound=BaseModel)
|
|
31
|
+
|
|
32
|
+
logger = FMPLogger().get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BaseClient:
|
|
36
|
+
def __init__(self, config: ClientConfig) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Initialize the BaseClient with the provided configuration.
|
|
39
|
+
"""
|
|
40
|
+
self.config = config
|
|
41
|
+
self.logger = FMPLogger().get_logger(__name__)
|
|
42
|
+
self.max_rate_limit_retries = getattr(config, "max_rate_limit_retries", 3)
|
|
43
|
+
self._rate_limit_retry_count = 0
|
|
44
|
+
|
|
45
|
+
# Configure logging based on config
|
|
46
|
+
FMPLogger().configure(self.config.logging)
|
|
47
|
+
|
|
48
|
+
self._setup_http_client()
|
|
49
|
+
self.logger.info(
|
|
50
|
+
"Initializing API client",
|
|
51
|
+
extra={"base_url": self.config.base_url, "timeout": self.config.timeout},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Initialize rate limiter
|
|
55
|
+
self._rate_limiter = FMPRateLimiter(
|
|
56
|
+
QuotaConfig(
|
|
57
|
+
daily_limit=self.config.rate_limit.daily_limit,
|
|
58
|
+
requests_per_second=self.config.rate_limit.requests_per_second,
|
|
59
|
+
requests_per_minute=self.config.rate_limit.requests_per_minute,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def _setup_http_client(self) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Setup HTTP client with default configuration.
|
|
66
|
+
"""
|
|
67
|
+
self.client = httpx.Client(
|
|
68
|
+
timeout=self.config.timeout,
|
|
69
|
+
follow_redirects=True,
|
|
70
|
+
headers={
|
|
71
|
+
"User-Agent": "FMP-Python-Client/1.0",
|
|
72
|
+
"Accept": "application/json",
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def close(self) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Clean up resources (close the httpx client).
|
|
79
|
+
"""
|
|
80
|
+
if hasattr(self, "client") and self.client is not None:
|
|
81
|
+
self.client.close()
|
|
82
|
+
|
|
83
|
+
def _handle_rate_limit(self, wait_time: float) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Handle rate limiting by waiting or raising an exception based on retry count.
|
|
86
|
+
"""
|
|
87
|
+
self._rate_limit_retry_count += 1
|
|
88
|
+
|
|
89
|
+
if self._rate_limit_retry_count > self.max_rate_limit_retries:
|
|
90
|
+
self._rate_limit_retry_count = 0 # Reset for next request
|
|
91
|
+
raise RateLimitError(
|
|
92
|
+
f"Rate limit exceeded after "
|
|
93
|
+
f"{self.max_rate_limit_retries} retries. "
|
|
94
|
+
f"Please wait {wait_time:.1f} seconds",
|
|
95
|
+
retry_after=wait_time,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.logger.warning(
|
|
99
|
+
f"Rate limit reached "
|
|
100
|
+
f"(attempt {self._rate_limit_retry_count}/{self.max_rate_limit_retries}), "
|
|
101
|
+
f"waiting {wait_time:.1f} seconds before retrying"
|
|
102
|
+
)
|
|
103
|
+
time.sleep(wait_time)
|
|
104
|
+
|
|
105
|
+
@retry(
|
|
106
|
+
stop=stop_after_attempt(3),
|
|
107
|
+
wait=wait_exponential(multiplier=1, min=4, max=10),
|
|
108
|
+
retry=retry_if_exception_type(
|
|
109
|
+
(httpx.TimeoutException, httpx.NetworkError, httpx.HTTPStatusError)
|
|
110
|
+
),
|
|
111
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
|
112
|
+
after=after_log(logger, logging.INFO),
|
|
113
|
+
)
|
|
114
|
+
@log_api_call()
|
|
115
|
+
def request(self, endpoint: Endpoint[T], **kwargs: Any) -> T | list[T]:
|
|
116
|
+
"""
|
|
117
|
+
Make request with rate limiting and retry logic.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
endpoint: The Endpoint object describing the request (method, path, etc.).
|
|
121
|
+
**kwargs: Arbitrary keyword arguments passed as request parameters.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Either a single Pydantic model of type T or a list of T.
|
|
125
|
+
"""
|
|
126
|
+
# First, check if we're already over the rate limit
|
|
127
|
+
if not self._rate_limiter.should_allow_request():
|
|
128
|
+
wait_time = self._rate_limiter.get_wait_time()
|
|
129
|
+
raise RateLimitError(
|
|
130
|
+
f"Rate limit exceeded. Please wait {wait_time:.1f} seconds",
|
|
131
|
+
retry_after=wait_time,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self._rate_limit_retry_count = 0 # Reset counter at start of new request
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
self._rate_limiter.record_request()
|
|
138
|
+
|
|
139
|
+
# Validate and process parameters
|
|
140
|
+
validated_params = endpoint.validate_params(kwargs)
|
|
141
|
+
|
|
142
|
+
# Build URL
|
|
143
|
+
url = endpoint.build_url(self.config.base_url, validated_params)
|
|
144
|
+
|
|
145
|
+
# Extract query parameters and add API key
|
|
146
|
+
query_params = endpoint.get_query_params(validated_params)
|
|
147
|
+
query_params["apikey"] = self.config.api_key
|
|
148
|
+
|
|
149
|
+
self.logger.debug(
|
|
150
|
+
f"Making request to {endpoint.name}",
|
|
151
|
+
extra={
|
|
152
|
+
"url": url,
|
|
153
|
+
"endpoint": endpoint.name,
|
|
154
|
+
"method": endpoint.method.value,
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
response = self.client.request(
|
|
159
|
+
endpoint.method.value, url, params=query_params
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Handle 429 responses from the API
|
|
163
|
+
if response.status_code == 429:
|
|
164
|
+
self._rate_limiter.handle_response(response.status_code, response.text)
|
|
165
|
+
wait_time = self._rate_limiter.get_wait_time()
|
|
166
|
+
raise RateLimitError(
|
|
167
|
+
f"Rate limit exceeded. Please wait {wait_time:.1f} seconds",
|
|
168
|
+
retry_after=wait_time,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
data = self.handle_response(response)
|
|
172
|
+
return self._process_response(endpoint, data)
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
self.logger.error(
|
|
176
|
+
f"Request failed: {str(e)}",
|
|
177
|
+
extra={"endpoint": endpoint.name, "error": str(e)},
|
|
178
|
+
exc_info=True,
|
|
179
|
+
)
|
|
180
|
+
raise
|
|
181
|
+
|
|
182
|
+
def handle_response(self, response: httpx.Response) -> dict[str, Any] | list[Any]:
|
|
183
|
+
"""
|
|
184
|
+
Handle API response and errors, returning dict or list from JSON.
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
RateLimitError: If status is 429
|
|
188
|
+
AuthenticationError: If status is 401
|
|
189
|
+
ValidationError: If status is 400
|
|
190
|
+
FMPError: For other 4xx/5xx errors or invalid JSON
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
response.raise_for_status()
|
|
194
|
+
data = response.json()
|
|
195
|
+
if not isinstance(data, dict | list):
|
|
196
|
+
raise FMPError(
|
|
197
|
+
f"Unexpected response type: {type(data)}. Expected dict or list.",
|
|
198
|
+
response={"data": data},
|
|
199
|
+
)
|
|
200
|
+
return data # Now mypy knows this is dict[str, Any] | list[Any]
|
|
201
|
+
except httpx.HTTPStatusError as e:
|
|
202
|
+
error_details: dict[str, Any] = {}
|
|
203
|
+
try:
|
|
204
|
+
error_details = e.response.json()
|
|
205
|
+
except json.JSONDecodeError:
|
|
206
|
+
error_details["raw_content"] = e.response.content.decode()
|
|
207
|
+
|
|
208
|
+
if e.response.status_code == 429:
|
|
209
|
+
wait_time = self._rate_limiter.get_wait_time()
|
|
210
|
+
raise RateLimitError(
|
|
211
|
+
f"Rate limit exceeded. Please wait {wait_time:.1f} seconds",
|
|
212
|
+
status_code=429,
|
|
213
|
+
response=error_details,
|
|
214
|
+
retry_after=wait_time,
|
|
215
|
+
) from e
|
|
216
|
+
elif e.response.status_code == 401:
|
|
217
|
+
raise AuthenticationError(
|
|
218
|
+
"Invalid API key or authentication failed",
|
|
219
|
+
status_code=401,
|
|
220
|
+
response=error_details,
|
|
221
|
+
) from e
|
|
222
|
+
elif e.response.status_code == 400:
|
|
223
|
+
raise ValidationError(
|
|
224
|
+
f"Invalid request parameters: {error_details}",
|
|
225
|
+
status_code=400,
|
|
226
|
+
response=error_details,
|
|
227
|
+
) from e
|
|
228
|
+
else:
|
|
229
|
+
raise FMPError(
|
|
230
|
+
f"HTTP {e.response.status_code} error occurred: {error_details}",
|
|
231
|
+
status_code=e.response.status_code,
|
|
232
|
+
response=error_details,
|
|
233
|
+
) from e
|
|
234
|
+
except json.JSONDecodeError as e:
|
|
235
|
+
raise FMPError(
|
|
236
|
+
f"Invalid JSON response from API: {str(e)}",
|
|
237
|
+
response={"raw_content": response.content.decode()},
|
|
238
|
+
) from e
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def _process_response(endpoint: Endpoint[T], data: Any) -> T | list[T]:
|
|
242
|
+
"""
|
|
243
|
+
Process the response data with warnings, returning T or list[T].
|
|
244
|
+
"""
|
|
245
|
+
if isinstance(data, dict):
|
|
246
|
+
# Check for error messages
|
|
247
|
+
if "Error Message" in data:
|
|
248
|
+
raise FMPError(data["Error Message"])
|
|
249
|
+
if "message" in data:
|
|
250
|
+
raise FMPError(data["message"])
|
|
251
|
+
if "error" in data:
|
|
252
|
+
raise FMPError(data["error"])
|
|
253
|
+
|
|
254
|
+
if isinstance(data, list):
|
|
255
|
+
processed_items: list[T] = []
|
|
256
|
+
for item in data:
|
|
257
|
+
with warnings.catch_warnings(record=True) as w:
|
|
258
|
+
warnings.simplefilter("always")
|
|
259
|
+
if isinstance(item, dict):
|
|
260
|
+
processed_item = endpoint.response_model.model_validate(item)
|
|
261
|
+
else:
|
|
262
|
+
# If it's not a dict, try to feed it into the first field
|
|
263
|
+
model = endpoint.response_model
|
|
264
|
+
try:
|
|
265
|
+
first_field = next(iter(model.__annotations__))
|
|
266
|
+
field_info = model.model_fields[first_field]
|
|
267
|
+
field_name = field_info.alias or first_field
|
|
268
|
+
processed_item = model.model_validate({field_name: item})
|
|
269
|
+
except (StopIteration, KeyError, AttributeError) as exc:
|
|
270
|
+
raise ValueError(
|
|
271
|
+
f"Invalid model structure for {model.__name__}"
|
|
272
|
+
) from exc
|
|
273
|
+
for warning in w:
|
|
274
|
+
logger.warning(f"Validation warning: {warning.message}")
|
|
275
|
+
processed_items.append(processed_item)
|
|
276
|
+
return processed_items
|
|
277
|
+
return endpoint.response_model.model_validate(data)
|
|
278
|
+
|
|
279
|
+
async def request_async(self, endpoint: Endpoint[T], **kwargs: Any) -> T | list[T]:
|
|
280
|
+
"""
|
|
281
|
+
Make async request with rate limiting, returning T or list[T].
|
|
282
|
+
"""
|
|
283
|
+
validated_params = endpoint.validate_params(kwargs)
|
|
284
|
+
url = endpoint.build_url(self.config.base_url, validated_params)
|
|
285
|
+
query_params = endpoint.get_query_params(validated_params)
|
|
286
|
+
query_params["apikey"] = self.config.api_key
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
async with httpx.AsyncClient(
|
|
290
|
+
timeout=self.config.timeout,
|
|
291
|
+
follow_redirects=True,
|
|
292
|
+
headers={
|
|
293
|
+
"User-Agent": "FMP-Python-Client/1.0",
|
|
294
|
+
"Accept": "application/json",
|
|
295
|
+
},
|
|
296
|
+
) as client:
|
|
297
|
+
response = await client.request(
|
|
298
|
+
endpoint.method.value, url, params=query_params
|
|
299
|
+
)
|
|
300
|
+
data = self.handle_response(response)
|
|
301
|
+
return self._process_response(endpoint, data)
|
|
302
|
+
except Exception as e:
|
|
303
|
+
self.logger.error(f"Async request failed: {str(e)}")
|
|
304
|
+
raise
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class EndpointGroup:
|
|
308
|
+
"""Abstract base class for endpoint groups"""
|
|
309
|
+
|
|
310
|
+
def __init__(self, client: BaseClient) -> None:
|
|
311
|
+
self._client = client
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def client(self) -> BaseClient:
|
|
315
|
+
"""Get the client instance."""
|
|
316
|
+
return self._client
|
fmp_data/client.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# client.py
|
|
2
|
+
import logging
|
|
3
|
+
import types
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
7
|
+
|
|
8
|
+
from fmp_data.alternative import AlternativeMarketsClient
|
|
9
|
+
from fmp_data.base import BaseClient
|
|
10
|
+
from fmp_data.company.client import CompanyClient
|
|
11
|
+
from fmp_data.config import ClientConfig, LoggingConfig, LogHandlerConfig
|
|
12
|
+
from fmp_data.economics import EconomicsClient
|
|
13
|
+
from fmp_data.exceptions import ConfigError
|
|
14
|
+
from fmp_data.fundamental import FundamentalClient
|
|
15
|
+
from fmp_data.institutional import InstitutionalClient
|
|
16
|
+
from fmp_data.intelligence import MarketIntelligenceClient
|
|
17
|
+
from fmp_data.investment import InvestmentClient
|
|
18
|
+
from fmp_data.logger import FMPLogger
|
|
19
|
+
from fmp_data.market import MarketClient
|
|
20
|
+
from fmp_data.technical import TechnicalClient
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FMPDataClient(BaseClient):
|
|
24
|
+
"""Main client for FMP Data API"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
api_key: str | None = None,
|
|
29
|
+
timeout: int = 30,
|
|
30
|
+
max_retries: int = 3,
|
|
31
|
+
base_url: str = "https://financialmodelingprep.com/api",
|
|
32
|
+
config: ClientConfig | None = None,
|
|
33
|
+
debug: bool = False,
|
|
34
|
+
):
|
|
35
|
+
self._initialized: bool = False
|
|
36
|
+
self._logger: logging.Logger | None = None
|
|
37
|
+
self._company: CompanyClient | None = None
|
|
38
|
+
self._market: MarketClient | None = None
|
|
39
|
+
self._fundamental: FundamentalClient | None = None
|
|
40
|
+
self._technical: TechnicalClient | None = None
|
|
41
|
+
self._intelligence: MarketIntelligenceClient | None = None
|
|
42
|
+
self._institutional: InstitutionalClient | None = None
|
|
43
|
+
self._investment: InvestmentClient | None = None
|
|
44
|
+
self._alternative: AlternativeMarketsClient | None = None
|
|
45
|
+
self._economics: EconomicsClient | None = None
|
|
46
|
+
|
|
47
|
+
if not api_key and (config is None or not config.api_key):
|
|
48
|
+
raise ConfigError("Invalid client configuration: API key is required")
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
if config is not None:
|
|
52
|
+
self._config = config
|
|
53
|
+
else:
|
|
54
|
+
logging_config = LoggingConfig(
|
|
55
|
+
level="DEBUG" if debug else "INFO",
|
|
56
|
+
handlers={
|
|
57
|
+
"console": LogHandlerConfig(
|
|
58
|
+
class_name="StreamHandler",
|
|
59
|
+
level="DEBUG" if debug else "INFO",
|
|
60
|
+
format=(
|
|
61
|
+
"%(asctime)s - %(levelname)s -"
|
|
62
|
+
" %(name)s - %(message)s"
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
self._config = ClientConfig(
|
|
70
|
+
api_key=api_key or "", # Handle None case
|
|
71
|
+
timeout=timeout,
|
|
72
|
+
max_retries=max_retries,
|
|
73
|
+
base_url=base_url,
|
|
74
|
+
logging=logging_config,
|
|
75
|
+
)
|
|
76
|
+
except PydanticValidationError as e:
|
|
77
|
+
raise ConfigError("Invalid client configuration") from e
|
|
78
|
+
|
|
79
|
+
FMPLogger().configure(self._config.logging)
|
|
80
|
+
self._logger = FMPLogger().get_logger(__name__)
|
|
81
|
+
|
|
82
|
+
super().__init__(self._config)
|
|
83
|
+
self._initialized = True
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
if not hasattr(self, "_logger") or self._logger is None:
|
|
87
|
+
self._logger = FMPLogger().get_logger(__name__)
|
|
88
|
+
self._logger.error(f"Failed to initialize client: {str(e)}")
|
|
89
|
+
raise
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_env(cls, debug: bool = False) -> "FMPDataClient":
|
|
93
|
+
"""
|
|
94
|
+
Create client instance from environment variables
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
debug: Enable debug logging if True
|
|
98
|
+
"""
|
|
99
|
+
config = ClientConfig.from_env()
|
|
100
|
+
if debug:
|
|
101
|
+
config.logging.level = "DEBUG"
|
|
102
|
+
if "console" in config.logging.handlers:
|
|
103
|
+
config.logging.handlers["console"].level = "DEBUG"
|
|
104
|
+
|
|
105
|
+
return cls(config=config)
|
|
106
|
+
|
|
107
|
+
def __enter__(self) -> "FMPDataClient":
|
|
108
|
+
"""Context manager enter"""
|
|
109
|
+
if not self._initialized:
|
|
110
|
+
self._setup_http_client()
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
def __exit__(
|
|
114
|
+
self,
|
|
115
|
+
exc_type: type[BaseException] | None,
|
|
116
|
+
exc_val: BaseException | None,
|
|
117
|
+
exc_tb: types.TracebackType | None,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Context manager exit
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
exc_type: Exception type if an error occurred
|
|
124
|
+
exc_val: Exception value if an error occurred
|
|
125
|
+
exc_tb: Exception traceback if an error occurred
|
|
126
|
+
"""
|
|
127
|
+
self.close()
|
|
128
|
+
if exc_type is not None and exc_val is not None and self.logger:
|
|
129
|
+
self.logger.error(
|
|
130
|
+
"Error in context manager",
|
|
131
|
+
extra={"error_type": exc_type.__name__, "error": str(exc_val)},
|
|
132
|
+
exc_info=True, # Use True instead of tuple to let logger handle it
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def close(self) -> None:
|
|
136
|
+
"""Clean up resources"""
|
|
137
|
+
try:
|
|
138
|
+
if hasattr(self, "client") and self.client is not None:
|
|
139
|
+
self.client.close()
|
|
140
|
+
if hasattr(self, "_initialized") and self._initialized:
|
|
141
|
+
logger = getattr(self, "_logger", None)
|
|
142
|
+
if logger is not None:
|
|
143
|
+
logger.info("FMP Data client closed")
|
|
144
|
+
except Exception as e:
|
|
145
|
+
# Log if possible, but don't raise
|
|
146
|
+
logger = getattr(self, "_logger", None)
|
|
147
|
+
if logger is not None:
|
|
148
|
+
logger.error(f"Error during cleanup: {str(e)}")
|
|
149
|
+
|
|
150
|
+
def __del__(self) -> None:
|
|
151
|
+
"""Destructor that ensures resources are cleaned up"""
|
|
152
|
+
try:
|
|
153
|
+
if hasattr(self, "_initialized") and self._initialized:
|
|
154
|
+
self.close()
|
|
155
|
+
except (Exception, BaseException) as e:
|
|
156
|
+
# Suppress any errors during cleanup
|
|
157
|
+
warnings.warn(
|
|
158
|
+
f"Error during FMPDataClient cleanup: {str(e)}",
|
|
159
|
+
ResourceWarning,
|
|
160
|
+
stacklevel=2,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def company(self) -> CompanyClient:
|
|
165
|
+
"""Get or create the company client instance"""
|
|
166
|
+
if not self._initialized:
|
|
167
|
+
raise RuntimeError("Client not properly initialized")
|
|
168
|
+
|
|
169
|
+
if self._company is None:
|
|
170
|
+
if self.logger:
|
|
171
|
+
self.logger.debug("Initializing company client")
|
|
172
|
+
self._company = CompanyClient(self)
|
|
173
|
+
return self._company
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def market(self) -> MarketClient:
|
|
177
|
+
"""Get or create the market data client instance"""
|
|
178
|
+
if not self._initialized:
|
|
179
|
+
raise RuntimeError("Client not properly initialized")
|
|
180
|
+
|
|
181
|
+
if self._market is None:
|
|
182
|
+
if self.logger:
|
|
183
|
+
self.logger.debug("Initializing market client")
|
|
184
|
+
self._market = MarketClient(self)
|
|
185
|
+
return self._market
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def fundamental(self) -> FundamentalClient:
|
|
189
|
+
"""Get or create the fundamental client instance"""
|
|
190
|
+
if not self._initialized:
|
|
191
|
+
raise RuntimeError("Client not properly initialized")
|
|
192
|
+
|
|
193
|
+
if self._fundamental is None:
|
|
194
|
+
if self.logger:
|
|
195
|
+
self.logger.debug("Initializing fundamental client")
|
|
196
|
+
self._fundamental = FundamentalClient(self)
|
|
197
|
+
return self._fundamental
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def technical(self) -> TechnicalClient:
|
|
201
|
+
"""Get or create the technical analysis client instance"""
|
|
202
|
+
if not self._initialized:
|
|
203
|
+
raise RuntimeError("Client not properly initialized")
|
|
204
|
+
|
|
205
|
+
if self._technical is None:
|
|
206
|
+
if self.logger:
|
|
207
|
+
self.logger.debug("Initializing technical analysis client")
|
|
208
|
+
self._technical = TechnicalClient(self)
|
|
209
|
+
return self._technical
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def intelligence(self) -> MarketIntelligenceClient:
|
|
213
|
+
"""Get or create the market intelligence client instance"""
|
|
214
|
+
if not self._initialized:
|
|
215
|
+
raise RuntimeError("Client not properly initialized")
|
|
216
|
+
|
|
217
|
+
if self._intelligence is None:
|
|
218
|
+
if self.logger:
|
|
219
|
+
self.logger.debug("Initializing market intelligence client")
|
|
220
|
+
self._intelligence = MarketIntelligenceClient(self)
|
|
221
|
+
return self._intelligence
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def institutional(self) -> InstitutionalClient:
|
|
225
|
+
"""Get or create the institutional activity client instance"""
|
|
226
|
+
if not self._initialized:
|
|
227
|
+
raise RuntimeError("Client not properly initialized")
|
|
228
|
+
|
|
229
|
+
if self._institutional is None:
|
|
230
|
+
if self.logger:
|
|
231
|
+
self.logger.debug("Initializing institutional activity client")
|
|
232
|
+
self._institutional = InstitutionalClient(self)
|
|
233
|
+
return self._institutional
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def investment(self) -> InvestmentClient:
|
|
237
|
+
"""Get or create the investment products client instance"""
|
|
238
|
+
if not self._initialized:
|
|
239
|
+
raise RuntimeError("Client not properly initialized")
|
|
240
|
+
|
|
241
|
+
if self._investment is None:
|
|
242
|
+
if self.logger:
|
|
243
|
+
self.logger.debug("Initializing investment products client")
|
|
244
|
+
self._investment = InvestmentClient(self)
|
|
245
|
+
return self._investment
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def alternative(self) -> AlternativeMarketsClient:
|
|
249
|
+
"""Get or create the alternative markets client instance"""
|
|
250
|
+
if not self._initialized:
|
|
251
|
+
raise RuntimeError("Client not properly initialized")
|
|
252
|
+
|
|
253
|
+
if self._alternative is None:
|
|
254
|
+
if self.logger:
|
|
255
|
+
self.logger.debug("Initializing alternative markets client")
|
|
256
|
+
self._alternative = AlternativeMarketsClient(self)
|
|
257
|
+
return self._alternative
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def economics(self) -> EconomicsClient:
|
|
261
|
+
"""Get or create the economics data client instance"""
|
|
262
|
+
if not self._initialized:
|
|
263
|
+
raise RuntimeError("Client not properly initialized")
|
|
264
|
+
|
|
265
|
+
if self._economics is None:
|
|
266
|
+
if self.logger:
|
|
267
|
+
self.logger.debug("Initializing economics data client")
|
|
268
|
+
self._economics = EconomicsClient(self)
|
|
269
|
+
return self._economics
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# company/__init__.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fmp_data.company.client import CompanyClient
|
|
5
|
+
from fmp_data.company.models import (
|
|
6
|
+
CompanyCoreInformation,
|
|
7
|
+
CompanyExecutive,
|
|
8
|
+
CompanyNote,
|
|
9
|
+
CompanyProfile,
|
|
10
|
+
EmployeeCount,
|
|
11
|
+
ExecutiveCompensation,
|
|
12
|
+
GeographicRevenueSegment,
|
|
13
|
+
HistoricalPrice,
|
|
14
|
+
HistoricalShareFloat,
|
|
15
|
+
IntradayPrice,
|
|
16
|
+
ProductRevenueSegment,
|
|
17
|
+
Quote,
|
|
18
|
+
ShareFloat,
|
|
19
|
+
SimpleQuote,
|
|
20
|
+
SymbolChange,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Quote",
|
|
25
|
+
"SimpleQuote",
|
|
26
|
+
"HistoricalPrice",
|
|
27
|
+
"IntradayPrice",
|
|
28
|
+
"CompanyClient",
|
|
29
|
+
"CompanyProfile",
|
|
30
|
+
"CompanyCoreInformation",
|
|
31
|
+
"CompanyExecutive",
|
|
32
|
+
"CompanyNote",
|
|
33
|
+
"EmployeeCount",
|
|
34
|
+
"ExecutiveCompensation",
|
|
35
|
+
"ShareFloat",
|
|
36
|
+
"HistoricalShareFloat",
|
|
37
|
+
"GeographicRevenueSegment",
|
|
38
|
+
"ProductRevenueSegment",
|
|
39
|
+
"SymbolChange",
|
|
40
|
+
]
|