Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions crates/fmp-py/python/fmp/_native/directory/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,21 @@ class AvailableExchange:
@property
def country_name(self) -> builtins.str: ...
@property
def country_code(self) -> builtins.str: ...
def country_code(self) -> typing.Optional[builtins.str]: ...
@property
def symbol_suffix(self) -> builtins.str: ...
@property
def delay(self) -> builtins.str: ...
def delay(self) -> typing.Optional[builtins.str]: ...
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __new__(
cls,
*,
exchange: builtins.str,
name: builtins.str,
country_name: builtins.str,
country_code: builtins.str,
country_code: typing.Optional[builtins.str],
symbol_suffix: builtins.str,
delay: builtins.str,
delay: typing.Optional[builtins.str],
) -> AvailableExchange: ...
def __getnewargs_ex__(self) -> tuple[tuple, dict]: ...
def __repr__(self) -> builtins.str: ...
Expand Down Expand Up @@ -327,15 +327,15 @@ class FinancialStatementSymbol:
@property
def trading_currency(self) -> builtins.str: ...
@property
def reporting_currency(self) -> builtins.str: ...
def reporting_currency(self) -> typing.Optional[builtins.str]: ...
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __new__(
cls,
*,
symbol: builtins.str,
company_name: builtins.str,
trading_currency: builtins.str,
reporting_currency: builtins.str,
reporting_currency: typing.Optional[builtins.str],
) -> FinancialStatementSymbol: ...
def __getnewargs_ex__(self) -> tuple[tuple, dict]: ...
def __repr__(self) -> builtins.str: ...
Expand Down
8 changes: 4 additions & 4 deletions crates/fmp-py/python/fmp/_native/indexes/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class HistoricalIndexConstituent:
@property
def date_added(self) -> builtins.str: ...
@property
def added_security(self) -> builtins.str: ...
def added_security(self) -> typing.Optional[builtins.str]: ...
@property
def removed_ticker(self) -> typing.Optional[builtins.str]: ...
@property
Expand All @@ -42,18 +42,18 @@ class HistoricalIndexConstituent:
@property
def symbol(self) -> builtins.str: ...
@property
def reason(self) -> builtins.str: ...
def reason(self) -> typing.Optional[builtins.str]: ...
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __new__(
cls,
*,
date_added: builtins.str,
added_security: builtins.str,
added_security: typing.Optional[builtins.str],
removed_ticker: typing.Optional[builtins.str],
removed_security: typing.Optional[builtins.str],
date: datetime.date,
symbol: builtins.str,
reason: builtins.str,
reason: typing.Optional[builtins.str],
) -> HistoricalIndexConstituent: ...
def __getnewargs_ex__(self) -> tuple[tuple, dict]: ...
def __repr__(self) -> builtins.str: ...
Expand Down
16 changes: 8 additions & 8 deletions crates/fmp-py/src/models/directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ pub(crate) struct FinancialStatementSymbol {
#[pyo3(get)]
pub trading_currency: String,
#[pyo3(get)]
pub reporting_currency: String,
pub reporting_currency: Option<String>,
}

#[gen_stub_pymethods]
Expand All @@ -122,7 +122,7 @@ impl FinancialStatementSymbol {
symbol: String,
company_name: String,
trading_currency: String,
reporting_currency: String,
reporting_currency: Option<String>,
) -> Self {
Self {
symbol,
Expand Down Expand Up @@ -223,7 +223,7 @@ impl From<libfmp::responses::directory::FinancialStatementSymbol> for FinancialS
symbol: value.symbol.into_inner(),
company_name: value.company_name,
trading_currency: value.trading_currency.into_inner(),
reporting_currency: value.reporting_currency.into_inner(),
reporting_currency: value.reporting_currency.map(|value| value.into_inner()),
}
}
}
Expand Down Expand Up @@ -727,11 +727,11 @@ pub(crate) struct AvailableExchange {
#[pyo3(get)]
pub country_name: String,
#[pyo3(get)]
pub country_code: String,
pub country_code: Option<String>,
#[pyo3(get)]
pub symbol_suffix: String,
#[pyo3(get)]
pub delay: String,
pub delay: Option<String>,
}

#[gen_stub_pymethods]
Expand All @@ -745,9 +745,9 @@ impl AvailableExchange {
exchange: String,
name: String,
country_name: String,
country_code: String,
country_code: Option<String>,
symbol_suffix: String,
delay: String,
delay: Option<String>,
) -> Self {
Self {
exchange,
Expand Down Expand Up @@ -864,7 +864,7 @@ impl From<libfmp::responses::directory::AvailableExchange> for AvailableExchange
exchange: value.exchange.into_inner(),
name: value.name,
country_name: value.country_name,
country_code: value.country_code.into_inner(),
country_code: value.country_code.map(|value| value.into_inner()),
symbol_suffix: value.symbol_suffix,
delay: value.delay,
}
Expand Down
8 changes: 4 additions & 4 deletions crates/fmp-py/src/models/indexes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ pub(crate) struct HistoricalIndexConstituent {
#[pyo3(get)]
pub date_added: String,
#[pyo3(get)]
pub added_security: String,
pub added_security: Option<String>,
#[pyo3(get)]
pub removed_ticker: Option<String>,
#[pyo3(get)]
Expand All @@ -307,7 +307,7 @@ pub(crate) struct HistoricalIndexConstituent {
#[pyo3(get)]
pub symbol: String,
#[pyo3(get)]
pub reason: String,
pub reason: Option<String>,
}

#[gen_stub_pymethods]
Expand All @@ -319,12 +319,12 @@ impl HistoricalIndexConstituent {
#[pyo3(signature = (*, date_added, added_security, removed_ticker, removed_security, date, symbol, reason))]
fn new(
date_added: String,
added_security: String,
added_security: Option<String>,
removed_ticker: Option<String>,
removed_security: Option<String>,
date: ::chrono::NaiveDate,
symbol: String,
reason: String,
reason: Option<String>,
) -> Self {
Self {
date_added,
Expand Down
38 changes: 38 additions & 0 deletions crates/fmp-py/tests/test_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,44 @@ def test_available_exchanges_preserves_omitted_false_and_true(
assert row.delay == "Real-time"


def test_available_exchanges_decodes_blank_country_code_and_null_delay_as_none(
client: Any, fixture_server: FixtureServer
) -> None:
"""A blank ``countryCode`` and a null ``delay`` decode as ``None``; ``countryName`` stays ``""``."""
row = {
"exchange": "CRYPTO",
"name": "Cryptocurrency",
"countryName": "",
"countryCode": "",
"symbolSuffix": "N/A",
"delay": None,
}
fixture_server.route("/available-exchanges", [row])
decoded = client.directory.available_exchanges()[0]

assert decoded.country_name == ""
assert decoded.country_code is None
assert decoded.delay is None


@pytest.mark.parametrize("reporting_currency", [None, ""], ids=["null", "empty"])
def test_financial_statement_symbols_decode_missing_reporting_currency_as_none(
client: Any, fixture_server: FixtureServer, reporting_currency: str | None
) -> None:
"""A null or blank ``reportingCurrency`` decodes as ``None``."""
row = {
"symbol": "1609.HK",
"companyName": "Chong Kin Group Holdings Limited",
"tradingCurrency": "HKD",
"reportingCurrency": reporting_currency,
}
fixture_server.route("/financial-statement-symbol-list", [row])
decoded = client.directory.financial_statement_symbols()[0]

assert decoded.trading_currency == "HKD"
assert decoded.reporting_currency is None


def test_available_sectors_decodes_the_open_sector_value(client: Any, fixture_server: FixtureServer) -> None:
"""``available_sectors`` maps to ``/available-sectors``."""
fixture_server.route("/available-sectors", load_fixture("directory_available_sectors.json"))
Expand Down
24 changes: 23 additions & 1 deletion crates/fmp-py/tests/test_indexes_b.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,29 @@ def test_historical_constituents_take_no_arguments(
assert row.removed_ticker == removed_ticker
assert row.date == date
assert isinstance(row.date_added, str)
assert row.reason
assert (row.reason is None) == (symbol == "GOOGL")


def test_historical_constituents_decode_null_members_and_blank_removed_ticker_as_none(
client: Any, fixture_server: FixtureServer
) -> None:
"""Null ``addedSecurity`` and ``reason`` and a blank ``removedTicker`` decode as ``None``."""
row = {
"addedSecurity": None,
"date": "2026-07-06",
"dateAdded": "July 7, 2026",
"reason": None,
"removedSecurity": "",
"removedTicker": "",
"symbol": "SPCX",
}
fixture_server.route("/historical-nasdaq-constituent", [row])
decoded = client.indexes.historical_nasdaq_constituents()[0]

assert decoded.added_security is None
assert decoded.reason is None
assert decoded.removed_ticker is None
assert decoded.removed_security == ""


@pytest.mark.parametrize(
Expand Down
3 changes: 2 additions & 1 deletion crates/fmp-py/tests/typecheck_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,12 @@ def check_directory_contract(client: FmpClient) -> None:
changed_on: datetime.date = changes[0].date
exchanges: list[AvailableExchange] = client.directory.available_exchanges(extended=True)
suffix: str = exchanges[0].symbol_suffix
delay: str | None = exchanges[0].delay
entries: list[CikListing] = client.directory.cik_list(page=0, limit=1000)
cik: str = entries[0].cik
transcripts: list[EarningsTranscriptAvailability] = client.directory.earnings_transcript_list()
count: str = transcripts[0].no_of_transcripts
_ = (changed_on, suffix, cik, count)
_ = (changed_on, suffix, delay, cik, count)


def check_congressional_contract(client: FmpClient) -> None:
Expand Down
19 changes: 15 additions & 4 deletions crates/libfmp/src/responses/directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@
//!
//! The Python binding exposes these models under `fmp.directory`.

use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};

use crate::{
codecs::NumericString,
types::{Cik, CountryCode, CurrencyCode, Date, ExchangeCode, Industry, Sector, Ticker},
};

fn required_option<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
Option::<T>::deserialize(deserializer)
}

/// One worldwide company or instrument in the company-symbol directory.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
Expand All @@ -26,7 +34,8 @@ pub struct FinancialStatementSymbol {
pub symbol: Ticker,
pub company_name: String,
pub trading_currency: CurrencyCode,
pub reporting_currency: CurrencyCode,
#[serde(deserialize_with = "crate::codecs::empty_or_null::deserialize")]
pub reporting_currency: Option<CurrencyCode>,
}

/// One US SEC entity in the CIK directory.
Expand Down Expand Up @@ -85,9 +94,11 @@ pub struct AvailableExchange {
pub exchange: ExchangeCode,
pub name: String,
pub country_name: String,
pub country_code: CountryCode,
#[serde(deserialize_with = "crate::codecs::empty_or_null::deserialize")]
pub country_code: Option<CountryCode>,
pub symbol_suffix: String,
pub delay: String,
#[serde(deserialize_with = "required_option")]
pub delay: Option<String>,
}

/// One sector accepted by provider sector filters.
Expand Down
6 changes: 4 additions & 2 deletions crates/libfmp/src/responses/indexes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@ pub struct IndexConstituent {
#[serde(rename_all = "camelCase")]
pub struct HistoricalIndexConstituent {
pub date_added: OpaqueDateText,
pub added_security: String,
#[serde(deserialize_with = "required_option")]
pub added_security: Option<String>,
#[serde(deserialize_with = "crate::codecs::empty_or_null::deserialize")]
pub removed_ticker: Option<Ticker>,
#[serde(deserialize_with = "required_option")]
pub removed_security: Option<String>,
pub date: Date,
pub symbol: Ticker,
pub reason: String,
#[serde(deserialize_with = "required_option")]
pub reason: Option<String>,
}
28 changes: 27 additions & 1 deletion crates/libfmp/tests/directory_responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use libfmp::responses::directory::{
ActivelyTradingSymbol, CikListing, CompanySymbol, EarningsTranscriptAvailability, EtfSymbol,
FinancialStatementSymbol, SymbolChange,
};
use serde_json::{Value, json};

#[test]
fn documented_directory_rows_decode_exact_field_names_and_wire_types() {
Expand Down Expand Up @@ -30,7 +31,10 @@ fn documented_directory_rows_decode_exact_field_names_and_wire_types() {
assert_eq!(financials[0].symbol.as_str(), "RMES.CN");
assert_eq!(financials[0].company_name, "Red Metal Resources Ltd.");
assert_eq!(financials[0].trading_currency.as_str(), "CAD");
assert_eq!(financials[0].reporting_currency.as_str(), "USD");
assert_eq!(
financials[0].reporting_currency.as_ref().unwrap().as_str(),
"USD"
);

assert_eq!(ciks[0].cik.as_str(), "0002137358");
assert_eq!(ciks[0].company_name, "Osotspa Public Co Limited/ADR");
Expand Down Expand Up @@ -69,6 +73,28 @@ fn documented_directory_rows_decode_exact_field_names_and_wire_types() {
assert!(transcript_wire["noOfTranscripts"].is_string());
}

#[test]
fn financial_statement_symbol_reporting_currency_decodes_null_and_empty_as_none() {
for reporting_currency in [Value::Null, json!("")] {
let row = json!({
"symbol": "1609.HK",
"companyName": "Chong Kin Group Holdings Limited",
"tradingCurrency": "HKD",
"reportingCurrency": reporting_currency,
});
let decoded: FinancialStatementSymbol = serde_json::from_value(row).unwrap();
assert_eq!(decoded.reporting_currency, None);
assert!(serde_json::to_value(&decoded).unwrap()["reportingCurrency"].is_null());
}

let missing = json!({
"symbol": "1609.HK",
"companyName": "Chong Kin Group Holdings Limited",
"tradingCurrency": "HKD",
});
assert!(serde_json::from_value::<FinancialStatementSymbol>(missing).is_err());
}

#[test]
fn every_directory_response_contract_preserves_empty_arrays() {
let empty = include_str!("fixtures/directory_empty.json");
Expand Down
Loading
Loading