# `transport` is exposed purely for testing (httpx.MockTransport) — production callers # never pass it, so real requests always go over the network via the default transport. self._client = httpx.Client(base_url=self._base_url, timeout=timeout, transport=transport) def _get(self, path: str, **params) -> list | dict: params["apikey"] = self._api_key resp = self._client.get(path, params=params) if resp.status_code == 401: raise ProviderAuthError(f"FMP auth failed for {path}") if resp.status_code == 429: raise ProviderRateLimitError(f"FMP rate limit hit for {path}") if resp.status_code == 404: raise ProviderNotFoundError(f"FMP 404 for {path}") resp.raise_for_status() return resp.json() def get_company_profile(self, ticker: str, exchange_mic: Optional[str] = None) -> ProviderCompanyProfile: data = self._get(f"/profile/{ticker}") if not data: raise ProviderNotFoundError(f"No FMP profile for {ticker}") row = data[0] return ProviderCompanyProfile( ticker=ticker, exchange_mic=row.get("exchangeShortName"), legal_name=row.get("companyName", ticker), display_name=row.get("companyName", ticker), country_iso2=row.get("country"), sector=row.get("sector"), industry=row.get("industry"), currency=row.get("currency", "USD"), isin=row.get("isin"), website=row.get("website"), description=row.get("description"), beta=row.get("beta"), ) def _get_statements(self, endpoint: str, mapping: dict, ticker: str, period: str, limit: int) -> list[ProviderFinancialPeriod]: """AUDIT FIX (Part D): line items are now type-coerced and sanity-checked before leaving the adapter. A period is never rejected wholesale -- one impossible field does not invalidate the other thirty -- but a non-numeric or arithmetically-impossible value (a negative revenue, a negative share count) is dropped with a recorded reason instead of travelling into the metrics engine as a string or a nonsense number. `self.last_statement_validation` holds the report for the most recent call.""" rows = self._get(f"/{endpoint}/{ticker}", period=period, limit=limit) report = ValidationReport() out = [] for row in rows: period_end = _parse_date(row.get("date")) out.append(ProviderFinancialPeriod( period_end=period_end, period_type="FY" if period == "annual" else row.get("period", "Q"), filing_date=_parse_date(row.get("fillingDate") or row.get("acceptedDate")), currency=row.get("reportedCurrency", "USD"), line_items=validate_line_items( _map_fields(row, mapping), report, row_key=str(period_end), ), )) self.last_statement_validation = report return out def get_income_statements(self, ticker: str, period: str = "annual", limit: int = 11) -> list[ProviderFinancialPeriod]: return self._get_statements("income-statement", _INCOME_MAP, ticker, period, limit) def get_balance_sheets(self, ticker: str, period: str = "annual", limit: int = 11) -> list[ProviderFinancialPeriod]: return self._get_statements("balance-sheet-statement", _BALANCE_MAP, ticker, period, limit) def get_cash_flows(self, ticker: str, period: str = "annual", limit: int = 11) -> list[ProviderFinancialPeriod]: return self._get_statements("cash-flow-statement", _CASHFLOW_MAP, ticker, period, limit) def get_prices(self, ticker: str, start: date, end: date) -> list[ProviderPriceBar]: """AUDIT FIX (Part D — docs/AUDIT_VALIDATION_D.md): see EODHDAdapter.get_prices() for the full reasoning. Same policy applied here so the two adapters cannot diverge: reject the row, not the batch, and record why. `self.last_price_validation` holds the report.""" data = self._get(f"/historical-price-full/{ticker}", **{"from": start.isoformat(), "to": end.isoformat()}) rows = data.get("historical", []) if isinstance(data, dict) else [] report = ValidationReport() bars = [] for r in rows: clean = validate_price_bar( { "date": r.get("date"), "open": r.get("open"), "high": r.get("high"), "low": r.get("low"), "close": r.get("close"), "adjusted_close": r.get("adjClose"), "volume": r.get("volume"), }, report, ) if clean is None: continue bars.append(ProviderPriceBar( date=_parse_date(clean["date"]), open=clean["open"], high=clean["high"], low=clean["low"], close=clean["close"], adjusted_close=clean["adjusted_close"], volume=clean["volume"], currency="USD", )) self.last_price_validation = report return bars def get_estimates(self, ticker: str) -> list[ProviderEstimateRow]: rows = self._get(f"/analyst-estimates/{ticker}") out = [] for r in rows: period_end = _parse_date(r.get("date")) for metric_key, fmp_key in (("eps", "estimatedEpsAvg"), ("revenue", "estimatedRevenueAvg")): if r.get(fmp_key) is not None: out.append(ProviderEstimateRow( period_end=period_end, metric=metric_key, consensus_value=r[fmp_key], num_analysts=r.get("numberAnalystEstimatedRevenue"), as_of_date=date.today(), )) return out