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
34 changes: 18 additions & 16 deletions crates/fmp-py/python/fmp/_native/calendar/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ class DividendEvent:
@property
def date(self) -> datetime.date: ...
@property
def record_date(self) -> datetime.date: ...
def record_date(self) -> typing.Optional[datetime.date]: ...
@property
def payment_date(self) -> datetime.date: ...
def payment_date(self) -> typing.Optional[datetime.date]: ...
@property
def declaration_date(self) -> typing.Optional[datetime.date]: ...
@property
Expand All @@ -150,8 +150,8 @@ class DividendEvent:
*,
symbol: builtins.str,
date: datetime.date,
record_date: datetime.date,
payment_date: datetime.date,
record_date: typing.Optional[datetime.date],
payment_date: typing.Optional[datetime.date],
declaration_date: typing.Optional[datetime.date],
adj_dividend: builtins.float,
dividend: builtins.float,
Expand Down Expand Up @@ -187,11 +187,11 @@ class EarningsEvent:
@property
def eps_actual(self) -> typing.Optional[builtins.float]: ...
@property
def eps_estimated(self) -> builtins.float: ...
def eps_estimated(self) -> typing.Optional[builtins.float]: ...
@property
def revenue_actual(self) -> typing.Optional[builtins.float]: ...
@property
def revenue_estimated(self) -> builtins.float: ...
def revenue_estimated(self) -> typing.Optional[builtins.float]: ...
@property
def last_updated(self) -> datetime.date: ...
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
Expand All @@ -201,9 +201,9 @@ class EarningsEvent:
symbol: builtins.str,
date: datetime.date,
eps_actual: typing.Optional[builtins.float],
eps_estimated: builtins.float,
eps_estimated: typing.Optional[builtins.float],
revenue_actual: typing.Optional[builtins.float],
revenue_estimated: builtins.float,
revenue_estimated: typing.Optional[builtins.float],
last_updated: datetime.date,
) -> EarningsEvent: ...
def __getnewargs_ex__(self) -> tuple[tuple, dict]: ...
Expand Down Expand Up @@ -239,7 +239,7 @@ class IpoCalendarEvent:
@property
def company(self) -> builtins.str: ...
@property
def exchange(self) -> builtins.str: ...
def exchange(self) -> typing.Optional[builtins.str]: ...
@property
def actions(self) -> builtins.str: ...
@property
Expand All @@ -256,7 +256,7 @@ class IpoCalendarEvent:
date: datetime.date,
daa: builtins.str,
company: builtins.str,
exchange: builtins.str,
exchange: typing.Optional[builtins.str],
actions: builtins.str,
shares: typing.Optional[builtins.str],
price_range: typing.Optional[builtins.str],
Expand Down Expand Up @@ -353,9 +353,11 @@ class IpoProspectus:
@property
def price_public_total(self) -> builtins.float: ...
@property
def discounts_and_commissions_per_share(self) -> builtins.float: ...
def discounts_and_commissions_per_share(
self,
) -> typing.Optional[builtins.float]: ...
@property
def discounts_and_commissions_total(self) -> builtins.float: ...
def discounts_and_commissions_total(self) -> typing.Optional[builtins.float]: ...
@property
def proceeds_before_expenses_per_share(self) -> builtins.float: ...
@property
Expand All @@ -375,8 +377,8 @@ class IpoProspectus:
cik: builtins.str,
price_public_per_share: builtins.float,
price_public_total: builtins.float,
discounts_and_commissions_per_share: builtins.float,
discounts_and_commissions_total: builtins.float,
discounts_and_commissions_per_share: typing.Optional[builtins.float],
discounts_and_commissions_total: typing.Optional[builtins.float],
proceeds_before_expenses_per_share: builtins.float,
proceeds_before_expenses_total: builtins.float,
form: builtins.str,
Expand Down Expand Up @@ -411,7 +413,7 @@ class StockSplitEvent:
@property
def denominator(self) -> builtins.float: ...
@property
def split_type(self) -> builtins.str: ...
def split_type(self) -> typing.Optional[builtins.str]: ...
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __new__(
cls,
Expand All @@ -420,7 +422,7 @@ class StockSplitEvent:
date: datetime.date,
numerator: builtins.float,
denominator: builtins.float,
split_type: builtins.str,
split_type: typing.Optional[builtins.str],
) -> StockSplitEvent: ...
def __getnewargs_ex__(self) -> tuple[tuple, dict]: ...
def __repr__(self) -> builtins.str: ...
Expand Down
38 changes: 19 additions & 19 deletions crates/fmp-py/src/models/calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ pub(crate) struct DividendEvent {
#[pyo3(get)]
pub date: ::chrono::NaiveDate,
#[pyo3(get)]
pub record_date: ::chrono::NaiveDate,
pub record_date: Option<::chrono::NaiveDate>,
#[pyo3(get)]
pub payment_date: ::chrono::NaiveDate,
pub payment_date: Option<::chrono::NaiveDate>,
#[pyo3(get)]
pub declaration_date: Option<::chrono::NaiveDate>,
#[pyo3(get)]
Expand All @@ -39,8 +39,8 @@ impl DividendEvent {
fn new(
symbol: String,
date: ::chrono::NaiveDate,
record_date: ::chrono::NaiveDate,
payment_date: ::chrono::NaiveDate,
record_date: Option<::chrono::NaiveDate>,
payment_date: Option<::chrono::NaiveDate>,
declaration_date: Option<::chrono::NaiveDate>,
adj_dividend: f64,
dividend: f64,
Expand Down Expand Up @@ -191,8 +191,8 @@ impl From<libfmp::responses::calendar::DividendEvent> for DividendEvent {
Self {
symbol: value.symbol.into_inner(),
date: value.date.into_inner(),
record_date: value.record_date.into_inner(),
payment_date: value.payment_date.into_inner(),
record_date: value.record_date.map(|value| value.into_inner()),
payment_date: value.payment_date.map(|value| value.into_inner()),
declaration_date: value.declaration_date.map(|value| value.into_inner()),
adj_dividend: value.adj_dividend,
dividend: value.dividend,
Expand All @@ -219,11 +219,11 @@ pub(crate) struct EarningsEvent {
#[pyo3(get)]
pub eps_actual: Option<f64>,
#[pyo3(get)]
pub eps_estimated: f64,
pub eps_estimated: Option<f64>,
#[pyo3(get)]
pub revenue_actual: Option<f64>,
#[pyo3(get)]
pub revenue_estimated: f64,
pub revenue_estimated: Option<f64>,
#[pyo3(get)]
pub last_updated: ::chrono::NaiveDate,
}
Expand All @@ -239,9 +239,9 @@ impl EarningsEvent {
symbol: String,
date: ::chrono::NaiveDate,
eps_actual: Option<f64>,
eps_estimated: f64,
eps_estimated: Option<f64>,
revenue_actual: Option<f64>,
revenue_estimated: f64,
revenue_estimated: Option<f64>,
last_updated: ::chrono::NaiveDate,
) -> Self {
Self {
Expand Down Expand Up @@ -400,7 +400,7 @@ pub(crate) struct IpoCalendarEvent {
#[pyo3(get)]
pub company: String,
#[pyo3(get)]
pub exchange: String,
pub exchange: Option<String>,
#[pyo3(get)]
pub actions: String,
shares: Option<::serde_json::Value>,
Expand All @@ -420,7 +420,7 @@ impl IpoCalendarEvent {
date: ::chrono::NaiveDate,
daa: String,
company: String,
exchange: String,
exchange: Option<String>,
actions: String,
shares: Option<String>,
price_range: Option<String>,
Expand Down Expand Up @@ -629,7 +629,7 @@ impl From<libfmp::responses::calendar::IpoCalendarEvent> for IpoCalendarEvent {
date: value.date.into_inner(),
daa: value.daa.as_str().to_owned(),
company: value.company,
exchange: value.exchange.into_inner(),
exchange: value.exchange.map(|value| value.into_inner()),
actions: value.actions,
shares: value.shares,
price_range: value.price_range,
Expand Down Expand Up @@ -836,9 +836,9 @@ pub(crate) struct IpoProspectus {
#[pyo3(get)]
pub price_public_total: f64,
#[pyo3(get)]
pub discounts_and_commissions_per_share: f64,
pub discounts_and_commissions_per_share: Option<f64>,
#[pyo3(get)]
pub discounts_and_commissions_total: f64,
pub discounts_and_commissions_total: Option<f64>,
#[pyo3(get)]
pub proceeds_before_expenses_per_share: f64,
#[pyo3(get)]
Expand All @@ -864,8 +864,8 @@ impl IpoProspectus {
cik: String,
price_public_per_share: f64,
price_public_total: f64,
discounts_and_commissions_per_share: f64,
discounts_and_commissions_total: f64,
discounts_and_commissions_per_share: Option<f64>,
discounts_and_commissions_total: Option<f64>,
proceeds_before_expenses_per_share: f64,
proceeds_before_expenses_total: f64,
form: String,
Expand Down Expand Up @@ -1133,7 +1133,7 @@ pub(crate) struct StockSplitEvent {
#[pyo3(get)]
pub denominator: f64,
#[pyo3(get)]
pub split_type: String,
pub split_type: Option<String>,
}

#[gen_stub_pymethods]
Expand All @@ -1148,7 +1148,7 @@ impl StockSplitEvent {
date: ::chrono::NaiveDate,
numerator: f64,
denominator: f64,
split_type: String,
split_type: Option<String>,
) -> Self {
Self {
symbol,
Expand Down
36 changes: 36 additions & 0 deletions crates/fmp-py/tests/test_calendar_a.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,39 @@ def test_earnings_calendar_decodes_fractional_revenue(client: Any, fixture_serve
assert isinstance(rows[0].revenue_estimated, float)
assert rows[0].revenue_estimated == 1_086_300_000.5
assert rows[0].revenue_actual == 1_101_500_000.0


@pytest.mark.parametrize("absent", ["", None])
def test_dividends_calendar_absent_record_and_payment_dates_are_none(
client: Any, fixture_server: FixtureServer, absent: str | None
) -> None:
"""Issue #368: ``""`` and ``null`` record / payment dates decode as ``None``."""
body = load_fixture("dividends_calendar.json")
body[0]["recordDate"] = absent
body[0]["paymentDate"] = absent
fixture_server.route("/dividends-calendar", body)
row = client.calendar.dividends_calendar()[0]

assert row.record_date is None
assert row.payment_date is None


def test_earnings_calendar_null_estimates_are_none(client: Any, fixture_server: FixtureServer) -> None:
"""Issue #368: ``null`` EPS and revenue estimates decode as ``None``."""
body = load_fixture("earnings_calendar.json")
body[0]["epsEstimated"] = None
body[0]["revenueEstimated"] = None
fixture_server.route("/earnings-calendar", body)
row = client.calendar.earnings_calendar()[0]

assert row.eps_estimated is None
assert row.revenue_estimated is None


def test_stock_splits_calendar_null_split_type_is_none(client: Any, fixture_server: FixtureServer) -> None:
"""Issue #368: a ``null`` split type decodes as ``None``."""
body = load_fixture("stock_splits_calendar.json")
body[0]["splitType"] = None
fixture_server.route("/splits-calendar", body)

assert client.calendar.stock_splits_calendar()[0].split_type is None
16 changes: 16 additions & 0 deletions crates/fmp-py/tests/test_calendar_b.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@ def test_ipos_prospectus_with_from_only(client: Any, fixture_server: FixtureServ
assert row.proceeds_before_expenses_total == pytest.approx(82_251.0)


def test_ipos_null_exchange_and_discounts_are_none(client: Any, fixture_server: FixtureServer) -> None:
"""Issue #368: a ``null`` exchange and ``null`` discount values decode as ``None``."""
calendar = load_fixture("ipos_calendar.json")
calendar[0]["exchange"] = None
fixture_server.route("/ipos-calendar", calendar)
prospectus = load_fixture("ipos_prospectus.json")
prospectus[0]["discountsAndCommissionsPerShare"] = None
prospectus[0]["discountsAndCommissionsTotal"] = None
fixture_server.route("/ipos-prospectus", prospectus)

assert client.calendar.ipos_calendar()[0].exchange is None
row = client.calendar.ipos_prospectus()[0]
assert row.discounts_and_commissions_per_share is None
assert row.discounts_and_commissions_total is None


def test_ipos_prospectus_with_both_dates(client: Any, fixture_server: FixtureServer) -> None:
"""``ipos_prospectus`` encodes both ISO strings in ``from``, ``to`` order."""
fixture_server.route("/ipos-prospectus", load_fixture("ipos_prospectus.json"))
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 @@ -136,9 +136,10 @@ def check_calendar_contract(client: FmpClient) -> None:
from_=datetime.date(2026, 4, 27), to="2026-07-26", page=0, include_report_times=True
)
eps_actual: float | None = earnings[0].eps_actual
eps_estimated: float | None = earnings[0].eps_estimated
ipos: list[IpoCalendarEvent] = calendar.ipos_calendar(from_="2026-03-06", to=datetime.date(2026, 6, 6))
ipo_date: datetime.date = ipos[0].date
_ = (yield_, declared, eps_actual, ipo_date)
_ = (yield_, declared, eps_actual, eps_estimated, ipo_date)


def check_bulk_contract(client: FmpClient) -> None:
Expand Down
28 changes: 18 additions & 10 deletions crates/libfmp/src/responses/calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ where
pub struct DividendEvent {
pub symbol: Ticker,
pub date: Date,
pub record_date: Date,
pub payment_date: Date,
#[serde(with = "empty_or_null_date")]
pub record_date: Option<Date>,
#[serde(with = "empty_or_null_date")]
pub payment_date: Option<Date>,
#[serde(with = "empty_or_null_date")]
pub declaration_date: Option<Date>,
pub adj_dividend: Price,
Expand All @@ -41,12 +43,14 @@ pub struct EarningsEvent {
pub date: Date,
#[serde(deserialize_with = "required_option")]
pub eps_actual: Option<f64>,
pub eps_estimated: f64,
#[serde(deserialize_with = "required_option")]
pub eps_estimated: Option<f64>,
#[serde(deserialize_with = "required_option")]
#[serde(serialize_with = "crate::codecs::integral_f64::serialize_option")]
pub revenue_actual: Option<MarketValue>,
#[serde(serialize_with = "crate::codecs::integral_f64::serialize")]
pub revenue_estimated: MarketValue,
#[serde(deserialize_with = "required_option")]
#[serde(serialize_with = "crate::codecs::integral_f64::serialize_option")]
pub revenue_estimated: Option<MarketValue>,
pub last_updated: Date,
}

Expand All @@ -59,7 +63,8 @@ pub struct IpoCalendarEvent {
pub date: Date,
pub daa: IsoTimestamp,
pub company: String,
pub exchange: ExchangeCode,
#[serde(deserialize_with = "required_option")]
pub exchange: Option<ExchangeCode>,
pub actions: String,
#[serde(deserialize_with = "required_option")]
pub shares: Option<DynamicJson>,
Expand Down Expand Up @@ -96,9 +101,11 @@ pub struct IpoProspectus {
pub price_public_per_share: Price,
#[serde(serialize_with = "crate::codecs::integral_f64::serialize")]
pub price_public_total: MarketValue,
pub discounts_and_commissions_per_share: Price,
#[serde(serialize_with = "crate::codecs::integral_f64::serialize")]
pub discounts_and_commissions_total: MarketValue,
#[serde(deserialize_with = "required_option")]
pub discounts_and_commissions_per_share: Option<Price>,
#[serde(deserialize_with = "required_option")]
#[serde(serialize_with = "crate::codecs::integral_f64::serialize_option")]
pub discounts_and_commissions_total: Option<MarketValue>,
pub proceeds_before_expenses_per_share: Price,
#[serde(serialize_with = "crate::codecs::integral_f64::serialize")]
pub proceeds_before_expenses_total: MarketValue,
Expand All @@ -117,5 +124,6 @@ pub struct StockSplitEvent {
pub numerator: SplitTerm,
#[serde(serialize_with = "crate::codecs::integral_f64::serialize")]
pub denominator: SplitTerm,
pub split_type: String,
#[serde(deserialize_with = "required_option")]
pub split_type: Option<String>,
}
4 changes: 2 additions & 2 deletions crates/libfmp/tests/calendar_earnings_endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,12 @@ async fn custom_proxy_preserves_query_states_order_auth_headers_and_bare_arrays(
assert_eq!(omitted.len(), 1);
assert_eq!(omitted[0].eps_actual, None);
assert_eq!(omitted[0].revenue_actual, None);
assert_eq!(omitted[0].revenue_estimated, 109_038_900_000.0);
assert_eq!(omitted[0].revenue_estimated, Some(109_038_900_000.0));
assert_eq!(explicit_false, omitted);
assert_eq!(calendar_omitted.len(), 1);
assert_eq!(calendar_omitted[0].eps_actual, Some(0.549));
assert_eq!(calendar_omitted[0].revenue_actual, Some(1_101_500_000.0));
assert_eq!(calendar_omitted[0].revenue_estimated, 1_086_300_000.0);
assert_eq!(calendar_omitted[0].revenue_estimated, Some(1_086_300_000.0));
assert_eq!(calendar_true, calendar_omitted);

let requests = executor.requests();
Expand Down
Loading
Loading