From 376f1e65457c668fa02ffd9f90a65afac2447fdd Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:12:09 +0800 Subject: [PATCH 01/31] fix(libfmp): add CSV response contract and row decoder Bulk FMP routes answer with `text/csv` rather than JSON (issue #376), so the response layer needs a way to accept and decode CSV bodies. Without it, those routes cannot be modelled correctly. **CSV response contract:** * Added a `TEXT_CSV` media type constant and a `Csv` variant to `ExpectedContentType`, validated case-insensitively against the body's media type. * Added a `ResponseContract::csv` constructor that decodes a headed CSV body into rows. An empty or header-only body yields no rows, and the JSON provider-message check is deliberately skipped since a short header-only body is data, not an error. * Added the `csv_rows` module with the row decoder and wired the `csv` dependency into the workspace and `libfmp` crate. **Error reporting:** * Extended `DecodeErrorKind::Syntax` to also cover malformed CSV records. No route uses the CSV contract yet. --- Cargo.lock | 28 ++++ Cargo.toml | 1 + crates/libfmp/Cargo.toml | 1 + crates/libfmp/src/endpoints/csv_rows.rs | 183 ++++++++++++++++++++++++ crates/libfmp/src/endpoints/mod.rs | 25 ++++ crates/libfmp/src/error.rs | 2 +- 6 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 crates/libfmp/src/endpoints/csv_rows.rs diff --git a/Cargo.lock b/Cargo.lock index 60ccac25..1903f3dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -174,6 +174,27 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "deranged" version = "0.5.8" @@ -711,6 +732,7 @@ version = "1.1.0" dependencies = [ "bytes", "chrono", + "csv", "http", "libc", "reqwest", @@ -1344,6 +1366,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" diff --git a/Cargo.toml b/Cargo.toml index f3dd1927..6b265f6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ license = "MIT" [workspace.dependencies] bytes = "1.12.1" chrono = { version = "0.4.45", default-features = false, features = ["std"] } +csv = "1.4" http = "1.5.0" libc = "0.2.189" libfmp = { path = "crates/libfmp" } diff --git a/crates/libfmp/Cargo.toml b/crates/libfmp/Cargo.toml index 7fd53c52..d6a79051 100644 --- a/crates/libfmp/Cargo.toml +++ b/crates/libfmp/Cargo.toml @@ -31,6 +31,7 @@ harness = false [dependencies] bytes.workspace = true chrono.workspace = true +csv.workspace = true http.workspace = true reqwest.workspace = true serde.workspace = true diff --git a/crates/libfmp/src/endpoints/csv_rows.rs b/crates/libfmp/src/endpoints/csv_rows.rs new file mode 100644 index 00000000..cde27501 --- /dev/null +++ b/crates/libfmp/src/endpoints/csv_rows.rs @@ -0,0 +1,183 @@ +//! CSV row decoding for the bulk routes (ADR 0035). +//! +//! The `csv` crate only splits the body into records. Each record is then +//! handed to the row's `Deserialize` impl as a map from header name to cell, +//! so serde names, renames, and field codecs apply unchanged and a failure +//! reports `[row].member` like the JSON decoder. A cell is always text: an +//! empty cell is the only absence marker, so it becomes `None` for an +//! `Option` member, stays `""` for a string member, and is reported as +//! [`DecodeErrorKind::Null`] anywhere a value is required. + +use bytes::Bytes; +use serde::de::{ + self, DeserializeOwned, IntoDeserializer, Unexpected, Visitor, + value::{Error as CellError, MapDeserializer}, +}; + +use super::{DecodeFailure, ResponseMetadata, missing_member_name}; +use crate::error::DecodeErrorKind; + +pub(super) fn decode_csv( + body: Bytes, + _metadata: ResponseMetadata<'_>, +) -> std::result::Result, DecodeFailure> +where + Row: DeserializeOwned, +{ + let mut reader = csv::ReaderBuilder::new() + .has_headers(true) + .flexible(false) + .from_reader(body.as_ref()); + let headers = reader.headers().map_err(|_| syntax(None))?.clone(); + let mut rows = Vec::new(); + let mut record = csv::StringRecord::new(); + loop { + match reader.read_record(&mut record) { + Ok(true) => {} + Ok(false) => return Ok(rows), + Err(_) => return Err(syntax(Some(rows.len()))), + } + let cells = headers.iter().zip(record.iter().map(Cell)); + let row = serde_path_to_error::deserialize(MapDeserializer::new(cells)) + .map_err(|error| row_failure(rows.len(), &error, &headers, &record))?; + rows.push(row); + } +} + +fn syntax(row: Option) -> DecodeFailure { + DecodeFailure::Shape { + path: row.map(|row| format!("[{row}]")), + kind: DecodeErrorKind::Syntax, + } +} + +fn row_failure( + row: usize, + error: &serde_path_to_error::Error, + headers: &csv::StringRecord, + record: &csv::StringRecord, +) -> DecodeFailure { + let message = error.inner().to_string(); + let member = if error.path().iter().next().is_some() { + Some(error.path().to_string()) + } else { + missing_member_name(&message).map(str::to_owned) + }; + let kind = if message.starts_with("missing field ") { + DecodeErrorKind::MissingMember + } else if member + .as_deref() + .and_then(|member| headers.iter().position(|header| header == member)) + .and_then(|column| record.get(column)) + .is_some_and(str::is_empty) + { + DecodeErrorKind::Null + } else if message.starts_with("invalid type: ") { + DecodeErrorKind::WrongType + } else { + DecodeErrorKind::InvalidValue + }; + let path = match member { + Some(member) => format!("[{row}].{member}"), + None => format!("[{row}]"), + }; + DecodeFailure::Shape { + path: Some(path), + kind, + } +} + +struct Cell<'a>(&'a str); + +impl<'de> IntoDeserializer<'de, CellError> for Cell<'_> { + type Deserializer = Self; + + fn into_deserializer(self) -> Self { + self + } +} + +macro_rules! parse_cell { + ($($method:ident => $visit:ident),* $(,)?) => { + $( + fn $method>(self, visitor: V) -> Result { + match self.0.parse() { + Ok(value) => visitor.$visit(value), + Err(_) => Err(de::Error::invalid_type(Unexpected::Str(self.0), &visitor)), + } + } + )* + }; +} + +impl<'de> de::Deserializer<'de> for Cell<'_> { + type Error = CellError; + + fn deserialize_any>(self, visitor: V) -> Result { + visitor.visit_str(self.0) + } + + fn deserialize_option>(self, visitor: V) -> Result { + if self.0.is_empty() { + visitor.visit_none() + } else { + visitor.visit_some(self) + } + } + + fn deserialize_bool>(self, visitor: V) -> Result { + match self.0 { + "true" => visitor.visit_bool(true), + "false" => visitor.visit_bool(false), + other => Err(de::Error::invalid_type(Unexpected::Str(other), &visitor)), + } + } + + fn deserialize_unit>(self, visitor: V) -> Result { + if self.0.is_empty() { + visitor.visit_unit() + } else { + Err(de::Error::invalid_type(Unexpected::Str(self.0), &visitor)) + } + } + + fn deserialize_newtype_struct>( + self, + _name: &'static str, + visitor: V, + ) -> Result { + visitor.visit_newtype_struct(self) + } + + fn deserialize_enum>( + self, + _name: &'static str, + _variants: &'static [&'static str], + visitor: V, + ) -> Result { + visitor.visit_enum(self.0.into_deserializer()) + } + + fn deserialize_ignored_any>(self, visitor: V) -> Result { + visitor.visit_unit() + } + + parse_cell! { + deserialize_i8 => visit_i8, + deserialize_i16 => visit_i16, + deserialize_i32 => visit_i32, + deserialize_i64 => visit_i64, + deserialize_i128 => visit_i128, + deserialize_u8 => visit_u8, + deserialize_u16 => visit_u16, + deserialize_u32 => visit_u32, + deserialize_u64 => visit_u64, + deserialize_u128 => visit_u128, + deserialize_f32 => visit_f32, + deserialize_f64 => visit_f64, + } + + serde::forward_to_deserialize_any! { + char str string bytes byte_buf unit_struct seq tuple tuple_struct map struct identifier + } +} diff --git a/crates/libfmp/src/endpoints/mod.rs b/crates/libfmp/src/endpoints/mod.rs index a0364718..6c3ebf28 100644 --- a/crates/libfmp/src/endpoints/mod.rs +++ b/crates/libfmp/src/endpoints/mod.rs @@ -10,6 +10,7 @@ pub mod commodities; pub mod company; pub mod congressional; pub mod crypto; +mod csv_rows; pub mod dcf; pub mod directory; pub mod economics; @@ -44,6 +45,9 @@ use metadata::EndpointMetadata; /// The JSON media type requested by FMP's structured-data endpoints. pub const APPLICATION_JSON: &str = "application/json"; +/// The CSV media type FMP answers on every bulk route (ADR 0035). +pub const TEXT_CSV: &str = "text/csv"; + /// A typed endpoint description that contains no transport configuration. /// /// Domain endpoint modules own values of this type. Base URLs, @@ -260,6 +264,8 @@ impl QueryParameters for [(&str, &str); N] { pub enum ExpectedContentType { /// Standard JSON and registered vendor JSON media types (`+json`). Json, + /// `text/csv`, compared case-insensitively. + Csv, /// One of the listed binary media types, compared case-insensitively. Binary(&'static [&'static str]), } @@ -272,6 +278,7 @@ impl ExpectedContentType { .trim(); match self { Self::Json => valid_json_media_type(media_type), + Self::Csv => media_type.eq_ignore_ascii_case(TEXT_CSV), Self::Binary(expected) => expected .iter() .any(|expected| media_type.eq_ignore_ascii_case(expected)), @@ -389,6 +396,24 @@ where } } +impl ResponseContract> +where + Row: DeserializeOwned, +{ + /// Creates a contract that decodes a headed CSV body into rows. + /// + /// An empty or header-only body yields no rows. The provider-message + /// check of the JSON contract does not apply: a short header-only CSV + /// body is data, not an error message. + pub const fn csv() -> Self { + Self { + expected_content_type: ExpectedContentType::Csv, + decoder: csv_rows::decode_csv::, + response: PhantomData, + } + } +} + impl ResponseContract { /// Creates a raw binary contract restricted to documented media types. pub const fn binary(expected_content_types: &'static [&'static str]) -> Self { diff --git a/crates/libfmp/src/error.rs b/crates/libfmp/src/error.rs index b15eca99..dfd0f088 100644 --- a/crates/libfmp/src/error.rs +++ b/crates/libfmp/src/error.rs @@ -89,7 +89,7 @@ pub enum ConfigurationErrorKind { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum DecodeErrorKind { - /// The body is not well-formed JSON. + /// The body is not well-formed JSON, or a CSV record is malformed. Syntax, /// A member that requires a value was JSON `null`. Null, From 8d31f2b6abb394774b1dfa4b2c66e50a57b54ada Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:13:45 +0800 Subject: [PATCH 02/31] fix(libfmp): raise CSV bulk body limit to 256 MiB default Adds `EndpointSpec::get_csv`, which marks CSV bulk descriptors with a 256 MiB default response body limit. Whole-market bulk bodies exceed the 64 MiB general default (`ratios-ttm-bulk` measured 65.6 MiB on 2026-10-01), so the former shared default truncated them. The bulk default applies only while the client limit is unset. Making `ClientBuilder::max_response_body_bytes` an `Option` lets an explicitly configured limit, once set, apply to every endpoint, bulk and XLSX included, while each endpoint falls back to its own default otherwise. Adds decoder unit tests covering header-name mapping, quoted commas, verbatim numbers, null and malformed cells, missing columns, wrong cell counts, one-line headers, and content-type acceptance, plus a transport test asserting the per-endpoint and configured limits. Related to #376 --- crates/libfmp/src/client.rs | 35 ++++--- crates/libfmp/src/endpoints/csv_rows.rs | 132 ++++++++++++++++++++++++ crates/libfmp/src/endpoints/mod.rs | 33 ++++++ crates/libfmp/tests/transport.rs | 44 +++++++- 4 files changed, 230 insertions(+), 14 deletions(-) diff --git a/crates/libfmp/src/client.rs b/crates/libfmp/src/client.rs index 314c047e..dc209619 100644 --- a/crates/libfmp/src/client.rs +++ b/crates/libfmp/src/client.rs @@ -33,9 +33,17 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); /// Default maximum bytes buffered for each HTTP response (64 MiB). /// -/// This finite default also applies to bulk and XLSX convenience methods. -/// Callers expecting a larger response can raise the client limit explicitly. +/// This finite default also applies to XLSX convenience methods. Callers +/// expecting a larger response can raise the client limit explicitly. pub const DEFAULT_MAX_RESPONSE_BODY_BYTES: usize = 64 * 1024 * 1024; + +/// Default maximum bytes buffered for each CSV bulk response (256 MiB). +/// +/// Whole-market bulk bodies exceed the general default: `ratios-ttm-bulk` +/// measured 65.6 MiB on 2026-10-01. It applies only while the client limit +/// is unset; [`ClientBuilder::max_response_body_bytes`] replaces it for +/// every endpoint, bulk included (ADR 0035). +pub const DEFAULT_BULK_MAX_RESPONSE_BODY_BYTES: usize = 256 * 1024 * 1024; const MAX_REDIRECTS: usize = 10; #[derive(Clone)] @@ -66,7 +74,7 @@ pub struct ClientBuilder { user_agent: String, timeout: Duration, connect_timeout: Duration, - max_response_body_bytes: usize, + max_response_body_bytes: Option, danger_allow_insecure_authentication: bool, redirect_policy: RedirectPolicy, executor: Option>, @@ -84,7 +92,7 @@ impl Default for ClientBuilder { user_agent: format!("libfmp/{VERSION}"), timeout: DEFAULT_TIMEOUT, connect_timeout: DEFAULT_CONNECT_TIMEOUT, - max_response_body_bytes: DEFAULT_MAX_RESPONSE_BODY_BYTES, + max_response_body_bytes: None, danger_allow_insecure_authentication: false, redirect_policy: RedirectPolicy::SameOrigin, executor: None, @@ -180,10 +188,12 @@ impl ClientBuilder { /// error body while streaming. Buffers returned by custom executors are /// checked defensively for every status, including redirects. An endpoint /// can replace the limit with - /// [`EndpointSpec::with_max_response_body_bytes`]. Bulk and XLSX - /// convenience methods inherit this client-wide value. + /// [`EndpointSpec::with_max_response_body_bytes`]. Until this is set, + /// each endpoint uses its own default: [`DEFAULT_MAX_RESPONSE_BODY_BYTES`], + /// or [`DEFAULT_BULK_MAX_RESPONSE_BODY_BYTES`] for CSV bulk routes. Once + /// set, it applies to every endpoint, bulk and XLSX included. pub fn max_response_body_bytes(mut self, max_bytes: usize) -> Self { - self.max_response_body_bytes = max_bytes; + self.max_response_body_bytes = Some(max_bytes); self } @@ -300,7 +310,7 @@ struct ClientInner { executor: Arc, deadline_backend: DeadlineBackend, timeout: Duration, - max_response_body_bytes: usize, + max_response_body_bytes: Option, redirect_policy: RedirectPolicy, } @@ -396,7 +406,8 @@ impl Client { apply_query_auth(&mut url, self.inner.auth.query.as_ref()); let max_body_bytes = endpoint .max_response_body_bytes() - .unwrap_or(self.inner.max_response_body_bytes); + .or(self.inner.max_response_body_bytes) + .unwrap_or(endpoint.default_max_response_body_bytes()); let response = self .inner .executor @@ -1115,9 +1126,7 @@ mod tests { #[test] fn default_response_limit_is_the_documented_finite_value() { assert_eq!(DEFAULT_MAX_RESPONSE_BODY_BYTES, 64 * 1024 * 1024); - assert_eq!( - ClientBuilder::default().max_response_body_bytes, - DEFAULT_MAX_RESPONSE_BODY_BYTES - ); + assert_eq!(DEFAULT_BULK_MAX_RESPONSE_BODY_BYTES, 256 * 1024 * 1024); + assert_eq!(ClientBuilder::default().max_response_body_bytes, None); } } diff --git a/crates/libfmp/src/endpoints/csv_rows.rs b/crates/libfmp/src/endpoints/csv_rows.rs index cde27501..f5af3497 100644 --- a/crates/libfmp/src/endpoints/csv_rows.rs +++ b/crates/libfmp/src/endpoints/csv_rows.rs @@ -181,3 +181,135 @@ impl<'de> de::Deserializer<'de> for Cell<'_> { char str string bytes byte_buf unit_struct seq tuple tuple_struct map struct identifier } } + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::*; + use crate::{codecs::NumericString, endpoints::ResponseContract, types::Date}; + + #[derive(Debug, PartialEq, Deserialize)] + #[serde(rename_all = "camelCase")] + struct Row { + symbol: String, + date: Date, + price: f64, + listed: bool, + volume: Option, + #[serde(rename = "Stock Price")] + stock_price: Option, + peers: String, + } + + const HEADER: &str = + "\"symbol\",\"date\",\"price\",\"listed\",\"volume\",\"Stock Price\",\"peers\"\n"; + + fn decode(body: &str) -> std::result::Result, DecodeFailure> { + ResponseContract::>::csv().decode( + Bytes::from(body.to_owned()), + ResponseMetadata::new("text/csv", None), + ) + } + + fn failure(body: &str) -> (Option, DecodeErrorKind) { + match decode(body).unwrap_err() { + DecodeFailure::Shape { path, kind } => (path, kind), + DecodeFailure::ProviderMessage => panic!("CSV never reports a provider message"), + } + } + + #[test] + fn empty_and_header_only_bodies_decode_to_no_rows() { + assert_eq!(decode("").unwrap(), []); + assert_eq!(decode(HEADER).unwrap(), []); + assert_eq!(decode("symbol,price\n").unwrap(), []); + } + + #[test] + fn cells_decode_by_header_name_with_quoted_commas_and_verbatim_numbers() { + let body = format!( + "{HEADER}\"AAPL\",\"2025-06-02\",201.7,true,6.9148336e-9,12.5,\"MSFT,GOOG\"\n\ + \"\",\"2025-06-02\",-0.5,false,,,\"say \"\"hi\"\"\"\n" + ); + let rows = decode(&body).unwrap(); + + assert_eq!(rows[0].symbol, "AAPL"); + assert_eq!(rows[0].date, Date::parse("2025-06-02").unwrap()); + assert_eq!(rows[0].price, 201.7); + assert!(rows[0].listed); + assert_eq!(rows[0].volume.as_ref().unwrap().as_str(), "6.9148336e-9"); + assert_eq!(rows[0].stock_price, Some(12.5)); + assert_eq!(rows[0].peers, "MSFT,GOOG"); + assert_eq!(rows[1].symbol, ""); + assert_eq!(rows[1].volume, None); + assert_eq!(rows[1].stock_price, None); + assert_eq!(rows[1].peers, "say \"hi\""); + } + + #[test] + fn an_empty_cell_on_a_required_member_is_a_null_at_its_row_and_member() { + let body = format!("{HEADER}A,2025-06-02,1,true,1,1,x\nB,2025-06-02,,true,1,1,x\n"); + assert_eq!( + failure(&body), + (Some("[1].price".into()), DecodeErrorKind::Null) + ); + let body = format!("{HEADER}A,,1,true,1,1,x\n"); + assert_eq!( + failure(&body), + (Some("[0].date".into()), DecodeErrorKind::Null) + ); + } + + #[test] + fn malformed_cells_are_classified_without_their_values() { + let body = format!("{HEADER}A,2025-06-02,abc,true,1,1,x\n"); + assert_eq!( + failure(&body), + (Some("[0].price".into()), DecodeErrorKind::WrongType) + ); + let body = format!("{HEADER}A,2025-06-02,1,yes,1,1,x\n"); + assert_eq!( + failure(&body), + (Some("[0].listed".into()), DecodeErrorKind::WrongType) + ); + let body = format!("{HEADER}A,2025-13-40,1,true,1,1,x\n"); + assert_eq!( + failure(&body), + (Some("[0].date".into()), DecodeErrorKind::InvalidValue) + ); + } + + #[test] + fn a_missing_column_is_a_missing_member() { + let body = "symbol,date,price,listed,volume,Stock Price\nA,2025-06-02,1,true,1,1\n"; + assert_eq!( + failure(body), + (Some("[0].peers".into()), DecodeErrorKind::MissingMember) + ); + } + + #[test] + fn a_record_with_the_wrong_cell_count_is_a_syntax_error_at_its_row() { + let body = format!("{HEADER}A,2025-06-02,1,true,1,1,x\nB,2025-06-02\n"); + assert_eq!( + failure(&body), + (Some("[1]".into()), DecodeErrorKind::Syntax) + ); + } + + #[test] + fn a_one_line_message_is_a_header_not_a_provider_message() { + assert_eq!(decode("Invalid name\n").unwrap(), []); + } + + #[test] + fn the_csv_contract_accepts_only_text_csv() { + let expected = ResponseContract::>::csv().expected_content_type(); + + assert!(expected.matches("text/csv")); + assert!(expected.matches("Text/CSV; charset=utf-8")); + assert!(!expected.matches("application/json")); + assert!(!expected.matches("text/plain")); + } +} diff --git a/crates/libfmp/src/endpoints/mod.rs b/crates/libfmp/src/endpoints/mod.rs index 6c3ebf28..a3a973d3 100644 --- a/crates/libfmp/src/endpoints/mod.rs +++ b/crates/libfmp/src/endpoints/mod.rs @@ -60,6 +60,7 @@ pub struct EndpointSpec { response: ResponseContract, metadata: EndpointMetadata, max_response_body_bytes: Option, + default_max_response_body_bytes: usize, } impl EndpointSpec { @@ -79,6 +80,7 @@ impl EndpointSpec { response, metadata: EndpointMetadata::new(), max_response_body_bytes: None, + default_max_response_body_bytes: crate::client::DEFAULT_MAX_RESPONSE_BODY_BYTES, } } @@ -131,6 +133,15 @@ impl EndpointSpec { pub const fn max_response_body_bytes(&self) -> Option { self.max_response_body_bytes } + + /// Returns the limit used when neither this endpoint nor the client sets one. + /// + /// This is [`crate::client::DEFAULT_MAX_RESPONSE_BODY_BYTES`] except for + /// CSV bulk descriptors, which use + /// [`crate::client::DEFAULT_BULK_MAX_RESPONSE_BODY_BYTES`]. + pub const fn default_max_response_body_bytes(&self) -> usize { + self.default_max_response_body_bytes + } } impl EndpointSpec @@ -162,6 +173,28 @@ where } } +impl EndpointSpec> +where + Row: DeserializeOwned, +{ + /// Creates a reusable GET descriptor for a CSV bulk response. + /// + /// Each record decodes into one `Row` by header name. Unless the endpoint + /// or the client sets a limit, the body may grow to + /// [`crate::client::DEFAULT_BULK_MAX_RESPONSE_BODY_BYTES`]. + pub const fn get_csv(id: &'static str, relative_path: &'static str, query: Q) -> Self { + let mut spec = Self::with_response( + HttpMethod::Get, + id, + relative_path, + query, + ResponseContract::csv(), + ); + spec.default_max_response_body_bytes = crate::client::DEFAULT_BULK_MAX_RESPONSE_BODY_BYTES; + spec + } +} + impl EndpointSpec { /// Creates a reusable GET descriptor for a binary response. /// diff --git a/crates/libfmp/tests/transport.rs b/crates/libfmp/tests/transport.rs index 9ba54d66..dd740c4d 100644 --- a/crates/libfmp/tests/transport.rs +++ b/crates/libfmp/tests/transport.rs @@ -12,7 +12,7 @@ use bytes::Bytes; use http::header::{CONTENT_TYPE, HeaderMap, HeaderValue, LOCATION}; use libfmp::{ Client, - client::EndpointSpec, + client::{DEFAULT_BULK_MAX_RESPONSE_BODY_BYTES, DEFAULT_MAX_RESPONSE_BODY_BYTES, EndpointSpec}, config::{Authentication, RedirectPolicy}, endpoints::BinaryBody, error::{ConfigurationErrorKind, ErrorCategory}, @@ -711,6 +711,48 @@ async fn endpoint_override_replaces_the_client_body_limit() { assert_eq!(executor.requests()[0].max_response_body_bytes(), 11); } +#[tokio::test] +async fn csv_bulk_endpoints_default_to_the_bulk_body_limit_until_the_client_sets_one() { + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("text/csv")); + let csv = || TransportResponse::new(200, headers.clone(), b"ok\ntrue\n".as_slice()); + let bulk = EndpointSpec::<(), Vec>::get_csv("bulk-test", "bulk-test", ()); + let executor = Arc::new(ScriptedExecutor::new(vec![ + Ok(csv()), + Ok(json_response()), + Ok(csv()), + ])); + let unset = Client::builder() + .base_url("https://example.test") + .executor(executor.clone()) + .build() + .unwrap(); + let configured = Client::builder() + .base_url("https://example.test") + .max_response_body_bytes(64) + .executor(executor.clone()) + .build() + .unwrap(); + + assert_eq!(unset.execute(&bulk).await.unwrap(), [Answer { ok: true }]); + unset.execute(&endpoint()).await.unwrap(); + configured.execute(&bulk).await.unwrap(); + + let limits: Vec<_> = executor + .requests() + .iter() + .map(PreparedRequest::max_response_body_bytes) + .collect(); + assert_eq!( + limits, + [ + DEFAULT_BULK_MAX_RESPONSE_BODY_BYTES, + DEFAULT_MAX_RESPONSE_BODY_BYTES, + 64 + ] + ); +} + #[tokio::test] async fn binary_response_retains_the_transport_buffer_without_copying() { let bytes = Bytes::from_static(b"binary-response"); From 28dbb7124fdf907e2183cb302c2656a1af60e669 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:17:59 +0800 Subject: [PATCH 03/31] fix(sdk/go): handle text/csv bulk responses with larger body limit This commit adds support for the bulk routes that answer with text/csv (issue #376). It introduces a hand-written getCSV decoder in sdk/go, though no generated method calls it yet. **CSV decoding:** * Added csv.go with a getCSV decoder and csv_test.go covering its behaviour, giving the SDK a path to parse the text/csv bodies that bulk endpoints return. **Bulk body limit:** * Added DefaultBulkMaxResponseBodyBytes (256 MiB) so CSV bulk bodies are buffered under a larger cap than the 64 MiB default while WithMaxResponseBodyBytes is unset; once it is set, bulk endpoints honour the configured value. * Tracked whether WithMaxResponseBodyBytes was supplied via maxResponseBodyBytesSet, and resolved bulkMaxResponseBodyBytes on the client accordingly. * Threaded an explicit maxBodyBytes argument through executeRedirects and roundTrip so each request enforces the limit that fits its route instead of the single client-wide cap. --- sdk/go/binary.go | 2 +- sdk/go/client.go | 24 +++++- sdk/go/csv.go | 180 ++++++++++++++++++++++++++++++++++++++++++++ sdk/go/csv_test.go | 141 ++++++++++++++++++++++++++++++++++ sdk/go/transport.go | 14 ++-- 5 files changed, 351 insertions(+), 10 deletions(-) create mode 100644 sdk/go/csv.go create mode 100644 sdk/go/csv_test.go diff --git a/sdk/go/binary.go b/sdk/go/binary.go index 1913f257..296348b3 100644 --- a/sdk/go/binary.go +++ b/sdk/go/binary.go @@ -49,7 +49,7 @@ func (c *Client) getBinary(ctx context.Context, endpointID, relativePath string, ctx, cancel := context.WithTimeout(ctx, c.timeout) defer cancel() - resp, err := c.executeRedirects(ctx, endpointID, target) + resp, err := c.executeRedirects(ctx, endpointID, target, c.maxResponseBodyBytes) if err != nil { return BinaryPayload{}, err } diff --git a/sdk/go/client.go b/sdk/go/client.go index 16f1fe6d..18e0a862 100644 --- a/sdk/go/client.go +++ b/sdk/go/client.go @@ -24,6 +24,10 @@ const ( DefaultConnectTimeout = 10 * time.Second // DefaultMaxResponseBodyBytes is the largest body buffered per response (64 MiB). DefaultMaxResponseBodyBytes int64 = 64 * 1024 * 1024 + // DefaultBulkMaxResponseBodyBytes is the largest CSV bulk body buffered + // while WithMaxResponseBodyBytes is unset (256 MiB): ratios-ttm-bulk + // measured 65.6 MiB on 2026-10-01 (ADR 0035). + DefaultBulkMaxResponseBodyBytes int64 = 256 * 1024 * 1024 maxRedirects = 10 ) @@ -64,6 +68,7 @@ type clientConfig struct { timeout time.Duration connectTimeout time.Duration maxResponseBodyBytes int64 + maxResponseBodyBytesSet bool dangerAllowInsecureAuth bool redirectPolicy RedirectPolicy httpClient *http.Client @@ -123,8 +128,14 @@ func WithConnectTimeout(timeout time.Duration) Option { } // WithMaxResponseBodyBytes sets the largest body buffered for one response. +// It applies to every endpoint, bulk included; until it is set, bulk +// endpoints use DefaultBulkMaxResponseBodyBytes and the others use +// DefaultMaxResponseBodyBytes. func WithMaxResponseBodyBytes(maxBytes int64) Option { - return func(c *clientConfig) { c.maxResponseBodyBytes = maxBytes } + return func(c *clientConfig) { + c.maxResponseBodyBytes = maxBytes + c.maxResponseBodyBytesSet = true + } } // WithDangerAllowInsecureAuthentication allows credentials over plaintext @@ -165,7 +176,10 @@ type Client struct { ownsTransport bool timeout time.Duration maxResponseBodyBytes int64 - redirectPolicy RedirectPolicy + // bulkMaxResponseBodyBytes caps CSV bulk bodies: the configured limit, + // or DefaultBulkMaxResponseBodyBytes when none was set. + bulkMaxResponseBodyBytes int64 + redirectPolicy RedirectPolicy } // NewClient validates the options and builds a client with the direct FMP @@ -256,6 +270,10 @@ func NewClient(opts ...Option) (*Client, error) { maxResponseBodyBytes: cfg.maxResponseBodyBytes, redirectPolicy: cfg.redirectPolicy, } + client.bulkMaxResponseBodyBytes = DefaultBulkMaxResponseBodyBytes + if cfg.maxResponseBodyBytesSet { + client.bulkMaxResponseBodyBytes = cfg.maxResponseBodyBytes + } client.bindNamespaces() return client, nil } @@ -306,7 +324,7 @@ func (c *Client) getJSON(ctx context.Context, endpointID, relativePath string, q ctx, cancel := context.WithTimeout(ctx, c.timeout) defer cancel() - resp, err := c.executeRedirects(ctx, endpointID, target) + resp, err := c.executeRedirects(ctx, endpointID, target, c.maxResponseBodyBytes) if err != nil { return err } diff --git a/sdk/go/csv.go b/sdk/go/csv.go new file mode 100644 index 00000000..64d0fce9 --- /dev/null +++ b/sdk/go/csv.go @@ -0,0 +1,180 @@ +package fmp + +import ( + "bytes" + "context" + "encoding/csv" + "encoding/json/jsontext" + "encoding/json/v2" + "errors" + "io" + "reflect" + "slices" + "strconv" + "strings" +) + +// textCSV is the media type FMP answers on every bulk route (ADR 0035). +const textCSV = "text/csv" + +// cellKind says how one CSV cell is written into the JSON member its model +// field decodes from. +type cellKind uint8 + +const ( + cellString cellKind = iota + cellPlainString + cellNumber + cellBool +) + +// csvColumn is the decode plan of one model member: its cell kind and +// whether the field is a pointer, which makes an empty cell absent. +type csvColumn struct { + kind cellKind + optional bool +} + +// getCSV issues one GET for a bulk endpoint and decodes the headed CSV body +// into rows of T, mirroring the Rust crate's CSV contract. Each record is +// written as a JSON object keyed by the header names and decoded with the +// model's own JSON rules, so a failure reports "//". An empty +// or header-only body yields no rows, and the provider-message check of +// getJSON does not apply. The redirect, timeout, and no-retry rules of +// getJSON are unchanged; the body cap is the bulk limit. +func getCSV[T any](ctx context.Context, c *Client, endpointID, relativePath string, query []queryParam) ([]T, error) { + if err := validateRelativePath(relativePath); err != nil { + return nil, err + } + target := c.buildEndpointURL(relativePath) + rawQuery, err := encodeQuery(query, c.auth.queryName) + if err != nil { + return nil, err + } + target.RawQuery = rawQuery + + ctx, cancel := context.WithTimeout(ctx, c.timeout) + defer cancel() + resp, err := c.executeRedirects(ctx, endpointID, target, c.bulkMaxResponseBodyBytes) + if err != nil { + return nil, err + } + + contentType := resp.header.Get("Content-Type") + switch { + case contentType == "": + return nil, decodeError(endpointID, resp.status, c.safeBody(resp.body), + "successful response omitted its content type", nil) + case !strings.EqualFold(mediaTypeOf(contentType), textCSV): + return nil, decodeError(endpointID, resp.status, c.safeBody(resp.body), + "successful response used an unexpected content type", nil) + } + rows, path, kind, cause := decodeCSVRows[T](resp.body) + if kind != DecodeKindNone { + decoded := decodeError(endpointID, resp.status, c.safeBody(resp.body), + "successful response could not be decoded", c.redactCause(cause)) + decoded.Path, decoded.DecodeKind = c.redactor.Redact(path), kind + return nil, decoded + } + return rows, nil +} + +// decodeCSVRows decodes body into rows of T. On failure it returns the JSON +// pointer of the row (and member), a DecodeKind, and the cause. +func decodeCSVRows[T any](body []byte) ([]T, string, DecodeKind, error) { + reader := csv.NewReader(bytes.NewReader(body)) + reader.ReuseRecord = true + header, err := reader.Read() + if errors.Is(err, io.EOF) { + return []T{}, "", DecodeKindNone, nil + } + if err != nil { + return nil, "", DecodeKindSyntax, err + } + header = slices.Clone(header) + plan := csvPlan(reflect.TypeFor[T]()) + rows := []T{} + var object []byte + for { + record, err := reader.Read() + if errors.Is(err, io.EOF) { + return rows, "", DecodeKindNone, nil + } + row := "/" + strconv.Itoa(len(rows)) + if err != nil { + return nil, row, DecodeKindSyntax, err + } + object = appendCSVObject(object[:0], header, record, plan) + var value T + if err := json.Unmarshal(object, &value); err != nil { + path, kind := decodeLocation(object, err) + return nil, row + path, kind, err + } + rows = append(rows, value) + } +} + +// appendCSVObject writes one record as a JSON object. An empty cell is null +// unless the member is a required plain string, where it stays "". +func appendCSVObject(dst []byte, header, record []string, plan map[string]csvColumn) []byte { + dst = append(dst, '{') + for index, name := range header { + if index > 0 { + dst = append(dst, ',') + } + dst, _ = jsontext.AppendQuote(dst, name) + dst = append(dst, ':') + cell, column := record[index], plan[name] + switch { + case cell == "" && (column.optional || column.kind != cellPlainString): + dst = append(dst, "null"...) + case column.kind == cellNumber && isJSONNumber(cell), + column.kind == cellBool && (cell == "true" || cell == "false"): + dst = append(dst, cell...) + default: + dst, _ = jsontext.AppendQuote(dst, cell) + } + } + return append(dst, '}') +} + +func isJSONNumber(cell string) bool { + value := jsontext.Value(cell) + return value.IsValid() && value.Kind() == '0' +} + +// csvPlan maps every json-tagged field of the struct type to its column plan. +func csvPlan(model reflect.Type) map[string]csvColumn { + plan := map[string]csvColumn{} + if model.Kind() != reflect.Struct { + return plan + } + for field := range model.Fields() { + name, _, _ := strings.Cut(field.Tag.Get("json"), ",") + if name == "" || name == "-" { + continue + } + fieldType, optional := field.Type, false + if fieldType.Kind() == reflect.Pointer { + fieldType, optional = fieldType.Elem(), true + } + plan[name] = csvColumn{kind: cellKindOf(fieldType), optional: optional} + } + return plan +} + +func cellKindOf(fieldType reflect.Type) cellKind { + switch fieldType.Kind() { + case reflect.Bool: + return cellBool + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return cellNumber + case reflect.String: + if fieldType == reflect.TypeFor[string]() { + return cellPlainString + } + } + return cellString +} diff --git a/sdk/go/csv_test.go b/sdk/go/csv_test.go new file mode 100644 index 00000000..e4aeff32 --- /dev/null +++ b/sdk/go/csv_test.go @@ -0,0 +1,141 @@ +package fmp + +import ( + "context" + "errors" + "net/http" + "testing" +) + +const csvEndpoint = "csv-contract-test" + +type csvProbeRow struct { + Symbol string `json:"symbol"` + Date Date `json:"date"` + Price float64 `json:"price"` + Listed bool `json:"listed"` + Volume *string `json:"volume"` + StockPrice *float64 `json:"Stock Price"` + Peers string `json:"peers"` +} + +const csvProbeHeader = `"symbol","date","price","listed","volume","Stock Price","peers"` + "\n" + +func csvServer(t *testing.T, contentType, body string) *Client { + t.Helper() + server, _ := newServer(t, func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", contentType) + _, _ = w.Write([]byte(body)) + }) + return newClient(t, server) +} + +func getCSVProbe[T any](t *testing.T, body string) ([]T, error) { + t.Helper() + return getCSV[T](context.Background(), csvServer(t, "text/csv", body), csvEndpoint, "bulk-probe", nil) +} + +func assertCSVDecodeError(t *testing.T, err error, path string, kind DecodeKind) { + t.Helper() + var typed *Error + if !errors.As(err, &typed) { + t.Fatalf("error %v (%T) is not *Error", err, err) + } + if typed.Category != CategoryDecode || typed.Endpoint != csvEndpoint || typed.Path != path || typed.DecodeKind != kind { + t.Fatalf("error = %+v, want decode at %q kind %v", typed, path, kind) + } +} + +// TestGetCSVDecodesEmptyAndHeaderOnlyBodiesToNoRows mirrors +// empty_and_header_only_bodies_decode_to_no_rows. +func TestGetCSVDecodesEmptyAndHeaderOnlyBodiesToNoRows(t *testing.T) { + t.Parallel() + for _, body := range []string{"", csvProbeHeader, "symbol,price\n", "Invalid name\n"} { + rows, err := getCSVProbe[csvProbeRow](t, body) + if err != nil || rows == nil || len(rows) != 0 { + t.Fatalf("body %q: rows = %v, err = %v", body, rows, err) + } + } +} + +// TestGetCSVDecodesCellsByHeaderName mirrors +// cells_decode_by_header_name_with_quoted_commas_and_verbatim_numbers. +func TestGetCSVDecodesCellsByHeaderName(t *testing.T) { + t.Parallel() + rows, err := getCSVProbe[csvProbeRow](t, csvProbeHeader+ + `"AAPL","2025-06-02",201.7,true,6.9148336e-9,12.5,"MSFT,GOOG"`+"\n"+ + `"","2025-06-02",-0.5,false,,,"say ""hi"""`+"\n") + if err != nil { + t.Fatal(err) + } + first, second := rows[0], rows[1] + if first.Symbol != "AAPL" || first.Date != mustParseDate(t, "2025-06-02") || first.Price != 201.7 || !first.Listed || + first.Volume == nil || *first.Volume != "6.9148336e-9" || first.StockPrice == nil || *first.StockPrice != 12.5 || + first.Peers != "MSFT,GOOG" { + t.Fatalf("first row = %+v", first) + } + if second.Symbol != "" || second.Volume != nil || second.StockPrice != nil || second.Peers != `say "hi"` { + t.Fatalf("second row = %+v", second) + } +} + +// TestGetCSVReportsRowAndMemberOfAFailure mirrors the Rust decoder's +// null, missing-member, and syntax tests on a generated model. +func TestGetCSVReportsRowAndMemberOfAFailure(t *testing.T) { + t.Parallel() + const header = `"symbol","date","dcf","Stock Price"` + "\n" + cases := []struct { + body string + path string + kind DecodeKind + }{ + {header + "A,2025-06-02,1,2\nB,,1,2\n", "/1/date", DecodeKindNull}, + {"symbol,date,Stock Price\nA,2025-06-02,2\n", "/0/dcf", DecodeKindMissingMember}, + {header + "A,2025-06-02,1,2\nB,2025-06-02\n", "/1", DecodeKindSyntax}, + } + for _, tc := range cases { + _, err := getCSVProbe[BulkDCFValuation](t, tc.body) + assertCSVDecodeError(t, err, tc.path, tc.kind) + } +} + +func TestGetCSVAcceptsOnlyTextCSV(t *testing.T) { + t.Parallel() + rows, err := getCSV[csvProbeRow](context.Background(), csvServer(t, "Text/CSV; charset=utf-8", csvProbeHeader), + csvEndpoint, "bulk-probe", nil) + if err != nil || len(rows) != 0 { + t.Fatalf("rows = %v, err = %v", rows, err) + } + _, err = getCSV[csvProbeRow](context.Background(), csvServer(t, "application/json", `{"Error Message":"x"}`), + csvEndpoint, "bulk-probe", nil) + var typed *Error + if !errors.As(err, &typed) || typed.Category != CategoryDecode || + typed.Message != "successful response used an unexpected content type" { + t.Fatalf("err = %v", err) + } +} + +// TestBulkBodyLimitDefaultsUntilTheClientSetsOne mirrors +// csv_bulk_endpoints_default_to_the_bulk_body_limit_until_the_client_sets_one. +func TestBulkBodyLimitDefaultsUntilTheClientSetsOne(t *testing.T) { + t.Parallel() + server, _ := newServer(t, func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/csv") + _, _ = w.Write([]byte(csvProbeHeader)) + }) + unset := newClient(t, server) + if unset.bulkMaxResponseBodyBytes != DefaultBulkMaxResponseBodyBytes || + unset.maxResponseBodyBytes != DefaultMaxResponseBodyBytes { + t.Fatalf("limits = %d, %d", unset.bulkMaxResponseBodyBytes, unset.maxResponseBodyBytes) + } + configured := newClient(t, server, WithMaxResponseBodyBytes(8)) + if configured.bulkMaxResponseBodyBytes != 8 { + t.Fatalf("bulk limit = %d, want the configured 8", configured.bulkMaxResponseBodyBytes) + } + _, err := getCSV[csvProbeRow](context.Background(), configured, csvEndpoint, "bulk-probe", nil) + var typed *Error + if !errors.As(err, &typed) || typed.Category != CategoryTransport || + typed.Message != "response body exceeded configured limit" { + t.Fatalf("err = %v", err) + } +} diff --git a/sdk/go/transport.go b/sdk/go/transport.go index 7950a377..0fe598cd 100644 --- a/sdk/go/transport.go +++ b/sdk/go/transport.go @@ -50,10 +50,11 @@ type bufferedResponse struct { body []byte } -func (c *Client) executeRedirects(ctx context.Context, endpointID string, target *url.URL) (*bufferedResponse, error) { +func (c *Client) executeRedirects(ctx context.Context, endpointID string, target *url.URL, + maxBodyBytes int64) (*bufferedResponse, error) { for hop := 0; hop <= maxRedirects; hop++ { c.applyQueryAuth(target) - resp, err := c.roundTrip(ctx, endpointID, target) + resp, err := c.roundTrip(ctx, endpointID, target, maxBodyBytes) if err != nil { return nil, err } @@ -97,7 +98,8 @@ func (c *Client) redirectDestination(current *url.URL, location string) (*url.UR return destination, true } -func (c *Client) roundTrip(ctx context.Context, endpointID string, target *url.URL) (*bufferedResponse, error) { +func (c *Client) roundTrip(ctx context.Context, endpointID string, target *url.URL, + maxBodyBytes int64) (*bufferedResponse, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil) if err != nil { return nil, transportError(endpointID, "request execution failed", c.redactCause(err)) @@ -121,10 +123,10 @@ func (c *Client) roundTrip(ctx context.Context, endpointID string, target *url.U if isRedirect(resp.StatusCode) { return buffered, nil } - if resp.ContentLength > c.maxResponseBodyBytes { + if resp.ContentLength > maxBodyBytes { return nil, transportError(endpointID, "response body exceeded configured limit", nil) } - body, err := io.ReadAll(io.LimitReader(resp.Body, c.maxResponseBodyBytes+1)) + body, err := io.ReadAll(io.LimitReader(resp.Body, maxBodyBytes+1)) if err != nil { message := "request execution failed" if errors.Is(err, context.DeadlineExceeded) { @@ -132,7 +134,7 @@ func (c *Client) roundTrip(ctx context.Context, endpointID string, target *url.U } return nil, transportError(endpointID, message, c.redactCause(err)) } - if int64(len(body)) > c.maxResponseBodyBytes { + if int64(len(body)) > maxBodyBytes { return nil, transportError(endpointID, "response body exceeded configured limit", nil) } buffered.body = body From 3bb640cb5c1784c32b2cc872172c54fa31eb5dcd Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:19:15 +0800 Subject: [PATCH 04/31] fix(fmp-py-gen): resolve CSV contract in wire resolver and gen_go Teach the registry wire resolver and gen_go about the CSV response contract so CSV endpoints are recognized and rendered correctly. * `EndpointSpec::get_csv` and `ResponseContract::csv()` now resolve to the new `Contract::CsvRows` variant in the wire resolver. * gen_go introduces a `ResponseKind::CsvRows` variant and renders such methods as `getCSV[Row]`, while reusing the row result type and zero value for the Go signature. * The wire surface validation and tests accept `Contract::CsvRows` alongside `Rows`, covering both the `get_csv` constructor and the explicit `ResponseContract::csv()` form. No descriptor uses the CSV contract yet, so generated output is unchanged. Refs #376 --- crates/fmp-py-gen/src/bin/gen_go/methods.rs | 9 +++++++-- crates/fmp-py-gen/src/bin/gen_go/render.rs | 10 +++++++++- crates/fmp-py-gen/src/registry/wire/mod.rs | 19 ++++++++++++++++++- .../fmp-py-gen/src/registry/wire/resolve.rs | 14 ++++++++++++-- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/crates/fmp-py-gen/src/bin/gen_go/methods.rs b/crates/fmp-py-gen/src/bin/gen_go/methods.rs index e640a178..bd94907d 100644 --- a/crates/fmp-py-gen/src/bin/gen_go/methods.rs +++ b/crates/fmp-py-gen/src/bin/gen_go/methods.rs @@ -69,6 +69,7 @@ impl QueryPlan { #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum ResponseKind { Rows(String), + CsvRows(String), Single(String), Dynamic, Binary(Vec), @@ -270,7 +271,9 @@ impl Context<'_> { let response = match (&endpoint.response_model, &wire.contract) { (_, Contract::Binary(types)) if endpoint.binary => ResponseKind::Binary(types.clone()), (None, Contract::Rows) if endpoint.dynamic => ResponseKind::Dynamic, - (Some(model), Contract::Rows) => { + (Some(model), contract @ (Contract::Rows | Contract::CsvRows)) + if !(endpoint.single && *contract == Contract::CsvRows) => + { let owner = self.model_owner.get(&model.name).ok_or_else(|| { format!("response model `{}` is not a discovered struct", model.name) })?; @@ -281,7 +284,9 @@ impl Context<'_> { model.name )); } - if endpoint.single { + if *contract == Contract::CsvRows { + ResponseKind::CsvRows(go_name(&model.name)) + } else if endpoint.single { ResponseKind::Single(go_name(&model.name)) } else { ResponseKind::Rows(go_name(&model.name)) diff --git a/crates/fmp-py-gen/src/bin/gen_go/render.rs b/crates/fmp-py-gen/src/bin/gen_go/render.rs index 09ef28f6..4f65b1c4 100644 --- a/crates/fmp-py-gen/src/bin/gen_go/render.rs +++ b/crates/fmp-py-gen/src/bin/gen_go/render.rs @@ -336,7 +336,9 @@ fn render_method(namespace: &NamespacePlan, method: &MethodPlan, out: &mut Strin .as_ref() .map_or(String::new(), |query| format!(", q {query}")); let (result, zero) = match &method.response { - ResponseKind::Rows(model) => (format!("[]{model}"), "nil".to_string()), + ResponseKind::Rows(model) | ResponseKind::CsvRows(model) => { + (format!("[]{model}"), "nil".to_string()) + } ResponseKind::Single(model) => (model.clone(), format!("{model}{{}}")), ResponseKind::Dynamic => ("[]jsontext.Value".to_string(), "nil".to_string()), ResponseKind::Binary(_) => ("BinaryPayload".to_string(), "BinaryPayload{}".to_string()), @@ -368,6 +370,12 @@ fn render_method(namespace: &NamespacePlan, method: &MethodPlan, out: &mut Strin types.join(", ") ); } + ResponseKind::CsvRows(model) => { + let _ = writeln!( + out, + "\treturn getCSV[{model}](ctx, n.client, {id:?}, {path:?}, {params})\n}}\n" + ); + } ResponseKind::Rows(_) | ResponseKind::Single(_) | ResponseKind::Dynamic => { let _ = writeln!( out, diff --git a/crates/fmp-py-gen/src/registry/wire/mod.rs b/crates/fmp-py-gen/src/registry/wire/mod.rs index 06d834e5..c94b2ed0 100644 --- a/crates/fmp-py-gen/src/registry/wire/mod.rs +++ b/crates/fmp-py-gen/src/registry/wire/mod.rs @@ -66,6 +66,8 @@ impl fmt::Display for HttpMethod { pub enum Contract { /// A JSON array of rows. Rows, + /// A headed CSV body, one row per record (ADR 0035). + CsvRows, /// A binary body with the listed acceptable content types. Binary(Vec), } @@ -273,7 +275,8 @@ mod tests { assert!(!wire.id.is_empty(), "{}", wire.method); assert_eq!(wire.http_method, HttpMethod::Get, "{}", wire.method); match (&wire.contract, endpoint.binary) { - (Contract::Rows, false) | (Contract::Binary(_), true) => {} + (Contract::Rows | Contract::CsvRows, false) + | (Contract::Binary(_), true) => {} (contract, binary) => { panic!( "{}: contract {contract:?} vs binary = {binary}", @@ -510,11 +513,19 @@ mod tests { pub fn explicit(query: CustomQuery) -> EndpointSpec { EndpointSpec::with_response(HttpMethod::Get, "x", "x-path", query, ResponseContract::binary(&["a/b"])) } + pub fn bulk() -> EndpointSpec<(), Vec> { + EndpointSpec::get_csv("dcf-bulk", "dcf-bulk", ()) + } + pub fn explicit_csv() -> EndpointSpec<(), Vec> { + EndpointSpec::with_response(HttpMethod::Get, "y", "y-path", (), ResponseContract::csv()) + } impl Client { pub async fn quotes(&self) -> Result> { self.execute("es()).await } pub async fn custom(&self, query: CustomQuery) -> Result> { self.execute(&custom(query)).await } pub async fn xlsx(&self, query: CustomQuery) -> Result { self.execute(&xlsx(query)).await } pub async fn explicit(&self, query: CustomQuery) -> Result { self.execute(&explicit(query)).await } + pub async fn bulk(&self) -> Result> { self.execute(&bulk()).await } + pub async fn explicit_csv(&self) -> Result> { self.execute(&explicit_csv()).await } } "#; let surface = surface(&[("dcf.rs", source)]); @@ -548,6 +559,12 @@ mod tests { Contract::Binary(vec!["a/b".to_owned()]) ); assert_eq!(surface.endpoints["explicit"].relative_path, "x-path"); + assert_eq!(surface.endpoints["bulk"].contract, Contract::CsvRows); + assert_eq!(surface.endpoints["bulk"].relative_path, "dcf-bulk"); + assert_eq!( + surface.endpoints["explicit_csv"].contract, + Contract::CsvRows + ); } #[test] diff --git a/crates/fmp-py-gen/src/registry/wire/resolve.rs b/crates/fmp-py-gen/src/registry/wire/resolve.rs index 39047e28..433d41ef 100644 --- a/crates/fmp-py-gen/src/registry/wire/resolve.rs +++ b/crates/fmp-py-gen/src/registry/wire/resolve.rs @@ -297,6 +297,13 @@ impl Evaluator<'_> { contract: Contract::Binary(strings(3, "content types")?), metadata: None, }), + "get_csv" => Ok(Spec { + id: string(0, "id")?, + relative_path: string(1, "path")?, + http_method: HttpMethod::Get, + contract: Contract::CsvRows, + metadata: None, + }), "new" => Ok(Spec { http_method: http_method(0)?, id: string(1, "id")?, @@ -315,7 +322,7 @@ impl Evaluator<'_> { } } - /// Reads `ResponseContract::json()` or `ResponseContract::binary(list)`. + /// Reads `ResponseContract::json()`, `::csv()`, or `::binary(list)`. fn response_contract( &self, expr: Option<&Expr>, @@ -323,7 +330,7 @@ impl Evaluator<'_> { ) -> Result { let unsupported = |expr: Option<&Expr>| { format!( - "response contract is not `ResponseContract::json()` or `::binary(..)`: {}", + "response contract is not `ResponseContract::json()`, `::csv()` or `::binary(..)`: {}", expr.map(text).unwrap_or_default() ) }; @@ -343,6 +350,9 @@ impl Evaluator<'_> { ([head, kind], None) if head == "ResponseContract" && kind == "json" => { Ok(Contract::Rows) } + ([head, kind], None) if head == "ResponseContract" && kind == "csv" => { + Ok(Contract::CsvRows) + } ([head, kind], Some(list)) if head == "ResponseContract" && kind == "binary" => { match self.eval(list, &BTreeMap::new(), module) { Value::Strs(values) => Ok(Contract::Binary(values)), From 9224ff03304f12025be74b6ae531824e0d3954fc Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:22:08 +0800 Subject: [PATCH 05/31] fix(sdk/go): require every model member to be a CSV header column getCSV now verifies on the first record that every json-tagged model member, pointer or not, has a matching header column. A missing member is reported as a missing member at /0/ in declaration order, mirroring the Rust decoder's contract. A new missingColumn helper walks the model's fields in serde-reported declaration order and returns the first json-tagged name the header does not carry. Header-only bodies with missing columns still yield no rows without error, since there is no first record to check. Refs #376 --- sdk/go/csv.go | 25 ++++++++++++++++++++++++- sdk/go/csv_test.go | 6 ++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/sdk/go/csv.go b/sdk/go/csv.go index 64d0fce9..9a3c9469 100644 --- a/sdk/go/csv.go +++ b/sdk/go/csv.go @@ -38,7 +38,9 @@ type csvColumn struct { // getCSV issues one GET for a bulk endpoint and decodes the headed CSV body // into rows of T, mirroring the Rust crate's CSV contract. Each record is // written as a JSON object keyed by the header names and decoded with the -// model's own JSON rules, so a failure reports "//". An empty +// model's own JSON rules, so a failure reports "//". Every +// json-tagged member, pointer or not, must be a header column; the first +// record checks it. An empty // or header-only body yields no rows, and the provider-message check of // getJSON does not apply. The redirect, timeout, and no-retry rules of // getJSON are unchanged; the body cap is the bulk limit. @@ -104,6 +106,12 @@ func decodeCSVRows[T any](body []byte) ([]T, string, DecodeKind, error) { if err != nil { return nil, row, DecodeKindSyntax, err } + if len(rows) == 0 { + if missing, ok := missingColumn(header, reflect.TypeFor[T]()); ok { + return nil, string(jsontext.Pointer(row).AppendToken(missing)), DecodeKindMissingMember, + errors.New("a model member is not a header column") + } + } object = appendCSVObject(object[:0], header, record, plan) var value T if err := json.Unmarshal(object, &value); err != nil { @@ -138,6 +146,21 @@ func appendCSVObject(dst []byte, header, record []string, plan map[string]csvCol return append(dst, '}') } +// missingColumn reports the first member of model, in declaration order as +// serde reports it, that the header does not carry. +func missingColumn(header []string, model reflect.Type) (string, bool) { + if model.Kind() != reflect.Struct { + return "", false + } + for field := range model.Fields() { + name, _, _ := strings.Cut(field.Tag.Get("json"), ",") + if name != "" && name != "-" && !slices.Contains(header, name) { + return name, true + } + } + return "", false +} + func isJSONNumber(cell string) bool { value := jsontext.Value(cell) return value.IsValid() && value.Kind() == '0' diff --git a/sdk/go/csv_test.go b/sdk/go/csv_test.go index e4aeff32..6d0a14af 100644 --- a/sdk/go/csv_test.go +++ b/sdk/go/csv_test.go @@ -97,6 +97,12 @@ func TestGetCSVReportsRowAndMemberOfAFailure(t *testing.T) { _, err := getCSVProbe[BulkDCFValuation](t, tc.body) assertCSVDecodeError(t, err, tc.path, tc.kind) } + _, err := getCSVProbe[csvProbeRow](t, `"symbol","date","price","listed","Stock Price","peers"`+"\nA,2025-06-02,1,true,1,x\n") + assertCSVDecodeError(t, err, "/0/volume", DecodeKindMissingMember) + rows, err := getCSVProbe[csvProbeRow](t, "symbol,date\n") + if err != nil || len(rows) != 0 { + t.Fatalf("header-only body with missing columns: rows = %v, err = %v", rows, err) + } } func TestGetCSVAcceptsOnlyTextCSV(t *testing.T) { From dec4f27000d961653ff1ddad3326baf0b618d511 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:22:34 +0800 Subject: [PATCH 06/31] fix(libfmp): require every CSV row member to be a header column Every member of the CSV row, `Option` or not, must now be present as a header column. The first record checks the serde struct field list, so a dropped or renamed provider column surfaces as a missing-member error instead of silently decoding to `None`. This catches upstream schema changes at the point of decode rather than leaving absent values to propagate unnoticed. Related to #376 --- crates/libfmp/src/endpoints/csv_rows.rs | 64 ++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/crates/libfmp/src/endpoints/csv_rows.rs b/crates/libfmp/src/endpoints/csv_rows.rs index f5af3497..9e414af8 100644 --- a/crates/libfmp/src/endpoints/csv_rows.rs +++ b/crates/libfmp/src/endpoints/csv_rows.rs @@ -3,10 +3,12 @@ //! The `csv` crate only splits the body into records. Each record is then //! handed to the row's `Deserialize` impl as a map from header name to cell, //! so serde names, renames, and field codecs apply unchanged and a failure -//! reports `[row].member` like the JSON decoder. A cell is always text: an -//! empty cell is the only absence marker, so it becomes `None` for an -//! `Option` member, stays `""` for a string member, and is reported as -//! [`DecodeErrorKind::Null`] anywhere a value is required. +//! reports `[row].member` like the JSON decoder. Every member of the row, +//! `Option` or not, must be a header column; the first record checks it. A +//! cell is always text: an empty cell is the only absence marker, so it +//! becomes `None` for an `Option` member, stays `""` for a string member, +//! and is reported as [`DecodeErrorKind::Null`] anywhere a value is +//! required. use bytes::Bytes; use serde::de::{ @@ -37,8 +39,12 @@ where Ok(false) => return Ok(rows), Err(_) => return Err(syntax(Some(rows.len()))), } - let cells = headers.iter().zip(record.iter().map(Cell)); - let row = serde_path_to_error::deserialize(MapDeserializer::new(cells)) + let cells = RowCells { + headers: &headers, + record: &record, + require_columns: rows.is_empty(), + }; + let row = serde_path_to_error::deserialize(cells) .map_err(|error| row_failure(rows.len(), &error, &headers, &record))?; rows.push(row); } @@ -87,6 +93,44 @@ fn row_failure( } } +struct RowCells<'a> { + headers: &'a csv::StringRecord, + record: &'a csv::StringRecord, + require_columns: bool, +} + +impl<'de> de::Deserializer<'de> for RowCells<'_> { + type Error = CellError; + + fn deserialize_any>(self, visitor: V) -> Result { + visitor.visit_map(MapDeserializer::new( + self.headers.iter().zip(self.record.iter().map(Cell)), + )) + } + + fn deserialize_struct>( + self, + _name: &'static str, + fields: &'static [&'static str], + visitor: V, + ) -> Result { + if self.require_columns + && let Some(field) = fields + .iter() + .find(|field| !self.headers.iter().any(|header| header == **field)) + { + return Err(de::Error::missing_field(field)); + } + self.deserialize_any(visitor) + } + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string bytes byte_buf + option unit unit_struct newtype_struct seq tuple tuple_struct map enum identifier + ignored_any + } +} + struct Cell<'a>(&'a str); impl<'de> IntoDeserializer<'de, CellError> for Cell<'_> { @@ -281,12 +325,18 @@ mod tests { } #[test] - fn a_missing_column_is_a_missing_member() { + fn a_missing_column_is_a_missing_member_even_for_an_option() { let body = "symbol,date,price,listed,volume,Stock Price\nA,2025-06-02,1,true,1,1\n"; assert_eq!( failure(body), (Some("[0].peers".into()), DecodeErrorKind::MissingMember) ); + let body = "symbol,date,price,listed,Stock Price,peers\nA,2025-06-02,1,true,1,x\n"; + assert_eq!( + failure(body), + (Some("[0].volume".into()), DecodeErrorKind::MissingMember) + ); + assert_eq!(decode("symbol,date\n").unwrap(), []); } #[test] From caba107eeb8dbd4a8de9f8b8e4bbc8b9aa4967e4 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:35:32 +0800 Subject: [PATCH 07/31] test(libfmp): add shared bulk CSV test helpers Add a shared helper module that decodes a CSV bulk fixture through the real client and CSV contract. It verifies the round trip per cell, the member blank/drop contract, and handles empty and header-only bodies. The bulk suites adopt this helper in the following commits. Issue #376 --- crates/libfmp/tests/support/bulk_csv.rs | 163 ++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 crates/libfmp/tests/support/bulk_csv.rs diff --git a/crates/libfmp/tests/support/bulk_csv.rs b/crates/libfmp/tests/support/bulk_csv.rs new file mode 100644 index 00000000..0503b978 --- /dev/null +++ b/crates/libfmp/tests/support/bulk_csv.rs @@ -0,0 +1,163 @@ +//! Shared CSV contract checks for the bulk test suites (ADR 0035). +//! +//! Bulk routes answer `text/csv`, so these helpers decode a fixture through +//! the real client and CSV contract instead of `serde_json`. + +use std::sync::Arc; + +use http::header::{CONTENT_TYPE, HeaderMap, HeaderValue}; +use libfmp::{ + Client, + client::{EndpointSpec, QueryParameters}, + error::DecodeErrorKind, + transport::TransportResponse, +}; +use serde::Serialize; +use serde_json::Value; + +use crate::support::FixtureExecutor; + +/// A `200 text/csv` fixture response. +fn csv_fixture(body: impl Into>) -> TransportResponse { + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("text/csv")); + TransportResponse::new(200, headers, body) +} + +/// Decodes `body` through `endpoint` on a fixture client, as the bulk method does. +pub fn decode( + endpoint: &EndpointSpec>, + body: impl Into>, +) -> libfmp::Result> +where + Q: QueryParameters, +{ + let client = Client::builder() + .base_url("https://fixture.test") + .executor(Arc::new(FixtureExecutor::new([csv_fixture(body)]))) + .build() + .unwrap(); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(client.execute(endpoint)) +} + +/// Asserts every fixture row decodes and re-encodes its first row with +/// `fields` members, each equal to its cell: verbatim text for strings and +/// numeric strings, the parsed value for numbers and booleans, and null or +/// `""` for an empty cell. +pub fn assert_round_trip(endpoint: &EndpointSpec>, fixture: &[u8], fields: usize) +where + Q: QueryParameters, + R: Serialize, +{ + let (header, records) = read(fixture); + let rows = decode(endpoint, fixture).unwrap(); + assert_eq!(rows.len(), records.len()); + let Value::Object(encoded) = serde_json::to_value(&rows[0]).unwrap() else { + panic!("a row re-encodes as a JSON object"); + }; + assert_eq!(encoded.len(), fields); + for (member, value) in &encoded { + let column = header.iter().position(|name| name == member); + let cell = &records[0][column.unwrap_or_else(|| panic!("{member} is a header column"))]; + let matches = match value { + Value::Null => cell.is_empty(), + Value::String(text) => text == cell, + Value::Bool(flag) => flag.to_string() == *cell, + Value::Number(number) => cell.parse::().ok() == number.as_f64(), + _ => false, + }; + assert!(matches, "{member} does not re-encode its cell"); + } +} + +/// Asserts the CSV member contract on the fixture's first record: a blanked +/// cell decodes only for a member in `blankable`, and is otherwise a null at +/// `[0].member`; dropping a model member's column is a missing member even +/// for an `Option`, while any other column, and an unknown one, is ignored. +pub fn assert_members(endpoint: &EndpointSpec>, fixture: &[u8], blankable: &[&str]) +where + Q: QueryParameters, + R: Serialize, +{ + let (header, records) = read(fixture); + let rows = decode(endpoint, fixture).unwrap(); + let Value::Object(members) = serde_json::to_value(&rows[0]).unwrap() else { + panic!("a row re-encodes as a JSON object"); + }; + for (column, member) in header.iter().enumerate() { + let mut blank = records[0].clone(); + blank[column].clear(); + let result = decode(endpoint, write(&header, &blank)); + if blankable.contains(&member.as_str()) { + assert!(result.is_ok(), "blank {member} did not decode"); + } else { + assert_failure(result, member, DecodeErrorKind::Null); + } + + let keep = |cells: &[String]| -> Vec { + cells + .iter() + .enumerate() + .filter(|(index, _)| *index != column) + .map(|(_, cell)| cell.clone()) + .collect() + }; + let dropped = decode(endpoint, write(&keep(&header), &keep(&records[0]))); + if members.contains_key(member) { + assert_failure(dropped, member, DecodeErrorKind::MissingMember); + } else { + assert!(dropped.is_ok(), "dropping {member} failed"); + } + } + let mut future_header = header.clone(); + future_header.push("futureColumn".to_owned()); + let mut future = records[0].clone(); + future.push("future value".to_owned()); + assert!(decode(endpoint, write(&future_header, &future)).is_ok()); +} + +/// Asserts the empty and header-only bodies of an endpoint decode to no rows. +pub fn assert_empty_bodies(endpoint: &EndpointSpec>, fixture: &[u8]) +where + Q: QueryParameters, +{ + let (header, _) = read(fixture); + assert!(decode(endpoint, Vec::new()).unwrap().is_empty()); + assert!(decode(endpoint, write(&header, &[])).unwrap().is_empty()); +} + +fn assert_failure(result: libfmp::Result, member: &str, kind: DecodeErrorKind) { + let Err(error) = result else { + panic!("{member}: expected a {kind:?} failure"); + }; + assert_eq!(error.decode_path(), Some(format!("[0].{member}").as_str())); + assert_eq!(error.decode_kind(), Some(kind), "{member}"); +} + +fn read(fixture: &[u8]) -> (Vec, Vec>) { + let mut reader = csv::Reader::from_reader(fixture); + let header = reader + .headers() + .unwrap() + .iter() + .map(str::to_owned) + .collect(); + let records = reader + .records() + .map(|record| record.unwrap().iter().map(str::to_owned).collect()) + .collect(); + (header, records) +} + +fn write(header: &[String], record: &[String]) -> Vec { + let mut writer = csv::Writer::from_writer(Vec::new()); + writer.write_record(header).unwrap(); + if !record.is_empty() { + writer.write_record(record).unwrap(); + } + writer.into_inner().unwrap() +} From a34c6f20043e81b342e3980f5ce1dec892912191 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:37:13 +0800 Subject: [PATCH 08/31] fix(bulk)!: decode snapshot bulk routes as text/csv The seven snapshot bulk routes (profile-bulk, rating-bulk, dcf-bulk, scores-bulk, price-target-summary-bulk, etf-holder-bulk, and upgrades-downgrades-consensus-bulk) now describe their endpoints with `EndpointSpec::get_csv`, because FMP answers these requests with text/csv rather than JSON. The generated Go methods regenerate to `getCSV`, real CSV fixtures replace the JSON bodies in the endpoint tests, and `.gitignore` un-ignores the CSV fixtures so they can be committed. The live etf-holder-bulk header now carries a clean `lastUpdated` column, so `BulkEtfHolding.last_updated_raw: String` (the wire key `lastUpdated` with a trailing quote, routed through the embedded fallback) becomes `last_updated: Date`. The Go `LastUpdatedRaw string` becomes `LastUpdated Date`, dropping the bespoke marshal/unmarshal shadow handling, and the Python `last_updated_raw: str` becomes `last_updated: date`. BREAKING CHANGE: the seven snapshot bulk methods now require a text/csv response; BulkEtfHolding.last_updated_raw is renamed to last_updated and typed as Date in Rust, Python and Go. --- .gitignore | 1 + crates/fmp-py-gen/src/bin/gen_go/tests.rs | 24 +++++++ .../fmp/_native/bulk/snapshots/__init__.pyi | 6 +- crates/fmp-py/src/models/bulk/snapshots.rs | 26 ++++---- crates/libfmp/src/endpoints/bulk.rs | 14 ++--- crates/libfmp/src/responses/bulk/snapshots.rs | 3 +- .../libfmp/tests/bulk_snapshot_endpoints.rs | 52 +++++++++------- .../libfmp/tests/bulk_snapshot_responses.rs | 7 +-- .../tests/fixtures/bulk_company_profiles.csv | 3 + .../tests/fixtures/bulk_dcf_valuations.csv | 3 + .../tests/fixtures/bulk_etf_holdings.csv | 3 + .../tests/fixtures/bulk_etf_holdings.json | 2 +- .../tests/fixtures/bulk_financial_scores.csv | 3 + .../fixtures/bulk_price_target_summaries.csv | 3 + .../tests/fixtures/bulk_stock_ratings.csv | 3 + .../bulk_upgrades_downgrades_consensus.csv | 3 + sdk/go/bulk.go | 42 +++---------- sdk/go/bulk_models.go | 62 +++++-------------- sdk/go/bulk_parity_test.go | 51 +-------------- sdk/go/bulk_test.go | 56 ++++++++++------- 20 files changed, 158 insertions(+), 209 deletions(-) create mode 100644 crates/libfmp/tests/fixtures/bulk_company_profiles.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_dcf_valuations.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_etf_holdings.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_financial_scores.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_price_target_summaries.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_stock_ratings.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_upgrades_downgrades_consensus.csv diff --git a/.gitignore b/.gitignore index a65fefc7..d5792166 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ Carthage/Build/ # Data *.csv +!crates/libfmp/tests/fixtures/*.csv *.h5 *.hdf5 *.pickle diff --git a/crates/fmp-py-gen/src/bin/gen_go/tests.rs b/crates/fmp-py-gen/src/bin/gen_go/tests.rs index 4ba383ae..f2e78e0a 100644 --- a/crates/fmp-py-gen/src/bin/gen_go/tests.rs +++ b/crates/fmp-py-gen/src/bin/gen_go/tests.rs @@ -196,6 +196,30 @@ fn nested_namespaces_embed_children_by_registry_path() { ); } +/// Bulk descriptors use `EndpointSpec::get_csv`, so their methods decode +/// through `getCSV` instead of `getJSON` (ADR 0035). +#[test] +fn csv_bulk_endpoints_render_through_get_csv() { + let files = + generate_domains(&domain_set(&["bulk", "company"])).expect("bulk generates with company"); + let (_, bulk) = files + .iter() + .find(|(name, _)| name == "bulk.go") + .expect("bulk.go is rendered"); + assert!( + bulk.contains( + "return getCSV[BulkStockRating](ctx, n.client, \"rating-bulk\", \"rating-bulk\", nil)" + ), + "{bulk}" + ); + assert!( + bulk.contains( + "return getCSV[CompanyProfile](ctx, n.client, \"profile-bulk\", \"profile-bulk\", params)" + ), + "{bulk}" + ); +} + #[test] fn cross_domain_models_require_their_owner_to_be_generated() { let error = diff --git a/crates/fmp-py/python/fmp/_native/bulk/snapshots/__init__.pyi b/crates/fmp-py/python/fmp/_native/bulk/snapshots/__init__.pyi index a1a45ebe..8f0e1343 100644 --- a/crates/fmp-py/python/fmp/_native/bulk/snapshots/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/bulk/snapshots/__init__.pyi @@ -62,7 +62,7 @@ class BulkEtfHolding: typing.Literal["cusip"], typing.Literal["isin"], typing.Literal["market_value"], - typing.Literal["last_updated_raw"], + typing.Literal["last_updated"], ] @property @@ -82,7 +82,7 @@ class BulkEtfHolding: @property def market_value(self) -> builtins.str: ... @property - def last_updated_raw(self) -> builtins.str: ... + def last_updated(self) -> datetime.date: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... def __new__( cls, @@ -95,7 +95,7 @@ class BulkEtfHolding: cusip: builtins.str, isin: builtins.str, market_value: builtins.str, - last_updated_raw: builtins.str, + last_updated: datetime.date, ) -> BulkEtfHolding: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... diff --git a/crates/fmp-py/src/models/bulk/snapshots.rs b/crates/fmp-py/src/models/bulk/snapshots.rs index 16381577..4f222bdc 100644 --- a/crates/fmp-py/src/models/bulk/snapshots.rs +++ b/crates/fmp-py/src/models/bulk/snapshots.rs @@ -857,7 +857,7 @@ pub(crate) struct BulkEtfHolding { #[pyo3(get)] pub market_value: String, #[pyo3(get)] - pub last_updated_raw: String, + pub last_updated: ::chrono::NaiveDate, } #[gen_stub_pymethods] @@ -866,7 +866,7 @@ impl BulkEtfHolding { #[new] #[allow(clippy::too_many_arguments)] #[allow(clippy::fn_params_excessive_bools)] - #[pyo3(signature = (*, symbol, name, shares_number, asset, weight_percentage, cusip, isin, market_value, last_updated_raw))] + #[pyo3(signature = (*, symbol, name, shares_number, asset, weight_percentage, cusip, isin, market_value, last_updated))] fn new( symbol: String, name: String, @@ -876,7 +876,7 @@ impl BulkEtfHolding { cusip: String, isin: String, market_value: String, - last_updated_raw: String, + last_updated: ::chrono::NaiveDate, ) -> Self { Self { symbol, @@ -887,7 +887,7 @@ impl BulkEtfHolding { cusip, isin, market_value, - last_updated_raw, + last_updated, } } @@ -895,7 +895,7 @@ impl BulkEtfHolding { const __hash__: Option> = None; #[classattr] - #[gen_stub(override_return_type(type_repr = "tuple[typing.Literal['symbol'], typing.Literal['name'], typing.Literal['shares_number'], typing.Literal['asset'], typing.Literal['weight_percentage'], typing.Literal['cusip'], typing.Literal['isin'], typing.Literal['market_value'], typing.Literal['last_updated_raw']]", imports = ("typing",)))] + #[gen_stub(override_return_type(type_repr = "tuple[typing.Literal['symbol'], typing.Literal['name'], typing.Literal['shares_number'], typing.Literal['asset'], typing.Literal['weight_percentage'], typing.Literal['cusip'], typing.Literal['isin'], typing.Literal['market_value'], typing.Literal['last_updated']]", imports = ("typing",)))] fn __match_args__(py: Python<'_>) -> PyResult> { PyTuple::new( py, @@ -908,7 +908,7 @@ impl BulkEtfHolding { "cusip", "isin", "market_value", - "last_updated_raw", + "last_updated", ], ) } @@ -937,8 +937,8 @@ impl BulkEtfHolding { self.market_value.clone().into_bound_py_any(py)?, )?; kwargs.set_item( - "last_updated_raw", - self.last_updated_raw.clone().into_bound_py_any(py)?, + "last_updated", + self.last_updated.clone().into_bound_py_any(py)?, )?; Ok((PyTuple::empty(py), kwargs)) } @@ -965,8 +965,8 @@ impl BulkEtfHolding { self.market_value.clone().into_bound_py_any(py)?, ), ( - "last_updated_raw", - self.last_updated_raw.clone().into_bound_py_any(py)?, + "last_updated", + self.last_updated.clone().into_bound_py_any(py)?, ), ], ) @@ -1010,8 +1010,8 @@ impl BulkEtfHolding { crate::models::convert::DictValue::dict_value(&self.market_value, py)?, )?; dict.set_item( - "last_updated_raw", - crate::models::convert::DictValue::dict_value(&self.last_updated_raw, py)?, + "last_updated", + crate::models::convert::DictValue::dict_value(&self.last_updated, py)?, )?; Ok(dict) } @@ -1028,7 +1028,7 @@ impl From for BulkEtfHolding { cusip: value.cusip, isin: value.isin.into_inner(), market_value: value.market_value.into_inner(), - last_updated_raw: value.last_updated_raw, + last_updated: value.last_updated.into_inner(), } } } diff --git a/crates/libfmp/src/endpoints/bulk.rs b/crates/libfmp/src/endpoints/bulk.rs index f633a5fb..15b9ffbc 100644 --- a/crates/libfmp/src/endpoints/bulk.rs +++ b/crates/libfmp/src/endpoints/bulk.rs @@ -169,39 +169,39 @@ const US_ONLY: EndpointMetadata = pub fn bulk_company_profiles( query: BulkPartQuery, ) -> EndpointSpec> { - EndpointSpec::get("profile-bulk", "profile-bulk", query).with_metadata(WORLDWIDE) + EndpointSpec::get_csv("profile-bulk", "profile-bulk", query).with_metadata(WORLDWIDE) } /// Describes `GET rating-bulk` without binding a transport. pub fn bulk_stock_ratings() -> EndpointSpec<(), Vec> { - EndpointSpec::get("rating-bulk", "rating-bulk", ()).with_metadata(WORLDWIDE) + EndpointSpec::get_csv("rating-bulk", "rating-bulk", ()).with_metadata(WORLDWIDE) } /// Describes `GET dcf-bulk` without binding a transport. pub fn bulk_dcf_valuations() -> EndpointSpec<(), Vec> { - EndpointSpec::get("dcf-bulk", "dcf-bulk", ()).with_metadata(WORLDWIDE) + EndpointSpec::get_csv("dcf-bulk", "dcf-bulk", ()).with_metadata(WORLDWIDE) } /// Describes `GET scores-bulk` without binding a transport. pub fn bulk_financial_scores() -> EndpointSpec<(), Vec> { - EndpointSpec::get("scores-bulk", "scores-bulk", ()).with_metadata(WORLDWIDE) + EndpointSpec::get_csv("scores-bulk", "scores-bulk", ()).with_metadata(WORLDWIDE) } /// Describes `GET price-target-summary-bulk` without binding a transport. pub fn bulk_price_target_summaries() -> EndpointSpec<(), Vec> { - EndpointSpec::get("price-target-summary-bulk", "price-target-summary-bulk", ()) + EndpointSpec::get_csv("price-target-summary-bulk", "price-target-summary-bulk", ()) .with_metadata(US_ONLY) } /// Describes `GET etf-holder-bulk` without binding a transport. pub fn bulk_etf_holdings(query: BulkPartQuery) -> EndpointSpec> { - EndpointSpec::get("etf-holder-bulk", "etf-holder-bulk", query).with_metadata(WORLDWIDE) + EndpointSpec::get_csv("etf-holder-bulk", "etf-holder-bulk", query).with_metadata(WORLDWIDE) } /// Describes `GET upgrades-downgrades-consensus-bulk` without binding a transport. pub fn bulk_upgrades_downgrades_consensus() -> EndpointSpec<(), Vec> { - EndpointSpec::get( + EndpointSpec::get_csv( "upgrades-downgrades-consensus-bulk", "upgrades-downgrades-consensus-bulk", (), diff --git a/crates/libfmp/src/responses/bulk/snapshots.rs b/crates/libfmp/src/responses/bulk/snapshots.rs index b25d1aa7..09e22c13 100644 --- a/crates/libfmp/src/responses/bulk/snapshots.rs +++ b/crates/libfmp/src/responses/bulk/snapshots.rs @@ -83,8 +83,7 @@ pub struct BulkEtfHolding { pub cusip: String, pub isin: Isin, pub market_value: NumericString, - #[serde(rename = "lastUpdated\"")] - pub last_updated_raw: String, + pub last_updated: Date, } /// One worldwide upgrades/downgrades-consensus bulk row. diff --git a/crates/libfmp/tests/bulk_snapshot_endpoints.rs b/crates/libfmp/tests/bulk_snapshot_endpoints.rs index 0cc61c02..f64ebcca 100644 --- a/crates/libfmp/tests/bulk_snapshot_endpoints.rs +++ b/crates/libfmp/tests/bulk_snapshot_endpoints.rs @@ -26,25 +26,29 @@ use libfmp::{ types::{BulkPart, StringValueError}, }; -use support::{FixtureExecutor, json_fixture}; +use support::{FixtureExecutor, fixture_response, json_fixture}; -const PROFILE: &[u8] = include_bytes!("fixtures/bulk_company_profiles.json"); -const RATING: &[u8] = include_bytes!("fixtures/bulk_stock_ratings.json"); -const DCF: &[u8] = include_bytes!("fixtures/bulk_dcf_valuations.json"); -const SCORES: &[u8] = include_bytes!("fixtures/bulk_financial_scores.json"); -const TARGET: &[u8] = include_bytes!("fixtures/bulk_price_target_summaries.json"); -const ETF: &[u8] = include_bytes!("fixtures/bulk_etf_holdings.json"); -const CONSENSUS: &[u8] = include_bytes!("fixtures/bulk_upgrades_downgrades_consensus.json"); +const PROFILE: &[u8] = include_bytes!("fixtures/bulk_company_profiles.csv"); +const RATING: &[u8] = include_bytes!("fixtures/bulk_stock_ratings.csv"); +const DCF: &[u8] = include_bytes!("fixtures/bulk_dcf_valuations.csv"); +const SCORES: &[u8] = include_bytes!("fixtures/bulk_financial_scores.csv"); +const TARGET: &[u8] = include_bytes!("fixtures/bulk_price_target_summaries.csv"); +const ETF: &[u8] = include_bytes!("fixtures/bulk_etf_holdings.csv"); +const CONSENSUS: &[u8] = include_bytes!("fixtures/bulk_upgrades_downgrades_consensus.csv"); + +fn csv_fixture(body: &'static [u8]) -> TransportResponse { + fixture_response(Some("text/csv"), body) +} fn fixtures() -> [TransportResponse; 7] { [ - json_fixture(PROFILE), - json_fixture(RATING), - json_fixture(DCF), - json_fixture(SCORES), - json_fixture(TARGET), - json_fixture(ETF), - json_fixture(CONSENSUS), + csv_fixture(PROFILE), + csv_fixture(RATING), + csv_fixture(DCF), + csv_fixture(SCORES), + csv_fixture(TARGET), + csv_fixture(ETF), + csv_fixture(CONSENSUS), ] } @@ -176,13 +180,13 @@ async fn custom_proxy_sends_one_exact_request_per_method_with_auth_headers_and_f .unwrap(); let consensus = client.bulk_upgrades_downgrades_consensus().await.unwrap(); - assert_eq!(profile[0].symbol.as_str(), "AAPL"); - assert_eq!(rating[0].rating, "B+"); - assert_eq!(dcf[0].stock_price.as_str(), "6.54"); + assert_eq!(profile[1].symbol.as_str(), "AMAT"); + assert_eq!(rating[1].rating, "C+"); + assert_eq!(dcf[1].stock_price.as_str(), "2.39"); assert_eq!(scores[0].reported_currency.as_str(), "CNY"); - assert_eq!(target[0].publishers, "[\"\"TheFly\""); - assert_eq!(etf[0].last_updated_raw, "2024-09-06\""); - assert_eq!(consensus[0].symbol, ""); + assert_eq!(target[1].symbol.as_str(), "AA"); + assert_eq!(etf[1].asset.as_str(), "3665.TW"); + assert_eq!(consensus[1].consensus, "Hold"); let requests = executor.requests(); assert_eq!(requests.len(), 7); @@ -281,11 +285,11 @@ async fn direct_fmp_header_and_query_auth_preserve_paths_and_exact_query_shape() } #[tokio::test] -async fn empty_arrays_decode_and_malformed_roots_keep_all_endpoint_identities() { +async fn empty_bodies_decode_and_json_bodies_keep_all_endpoint_identities() { let executor = Arc::new(FixtureExecutor::new( - std::iter::repeat_with(|| json_fixture(b"[]")) + std::iter::repeat_with(|| csv_fixture(b"")) .take(7) - .chain(std::iter::repeat_with(|| json_fixture(b"{}")).take(7)), + .chain(std::iter::repeat_with(|| json_fixture(b"[]")).take(7)), )); let client = Client::builder() .authentication(Authentication::fmp_header("secret")) diff --git a/crates/libfmp/tests/bulk_snapshot_responses.rs b/crates/libfmp/tests/bulk_snapshot_responses.rs index 6ed0be56..cd8f3f35 100644 --- a/crates/libfmp/tests/bulk_snapshot_responses.rs +++ b/crates/libfmp/tests/bulk_snapshot_responses.rs @@ -9,7 +9,7 @@ use libfmp::{ }, company::CompanyProfile, }, - types::Ticker, + types::{Date, Ticker}, }; const PROFILE: &[u8] = include_bytes!("fixtures/bulk_company_profiles.json"); @@ -63,10 +63,7 @@ fn malformed_documented_strings_and_wire_keys_are_preserved_verbatim() { let etf = rows::(ETF).remove(0); assert_eq!(etf.cusip, ""); - assert_eq!(etf.last_updated_raw, "2024-09-06\""); - let etf_json = serde_json::to_value(etf).unwrap(); - assert_eq!(etf_json["lastUpdated\""], "2024-09-06\""); - assert!(etf_json.get("lastUpdated").is_none()); + assert_eq!(etf.last_updated, Date::parse("2024-09-06").unwrap()); let consensus = rows::(CONSENSUS).remove(0); assert_eq!(consensus.symbol, ""); diff --git a/crates/libfmp/tests/fixtures/bulk_company_profiles.csv b/crates/libfmp/tests/fixtures/bulk_company_profiles.csv new file mode 100644 index 00000000..f3e605ad --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_company_profiles.csv @@ -0,0 +1,3 @@ +"symbol","price","marketCap","beta","lastDividend","range","change","changePercentage","volume","averageVolume","companyName","currency","cik","isin","cusip","exchangeFullName","exchange","industry","website","description","ceo","sector","country","fullTimeEmployees","phone","address","city","state","zip","image","ipoDate","defaultImage","isEtf","isActivelyTrading","isAdr","isFund" +"WMB",67.78,82906291252,0.618,2.075,"55.56-80.08",-0.85,-1.23853,2020441,7138175,"The Williams Companies, Inc.","USD","0000107263","US9694571004","969457100","New York Stock Exchange","NYSE","Oil & Gas Midstream","https://www.williams.com","The Williams Companies, Inc., alongside its subsidiaries, operates as a prominent energy infrastructure entity, primarily conducting business throughout the United States. The company’s operations are organized into four key segments: Transmission & Gulf of Mexico, Northeast G&P, West, and Gas & NGL Marketing Services. The Transmission & Gulf of Mexico division manages crucial natural gas pipelines such as Transco and Northwest, in addition to natural gas gathering and processing, and crude oil production handling and transportation assets situated in the Gulf Coast. This segment also oversees various petrochemical and feedstock pipelines. Focusing on midstream activities, the Northeast G&P segment handles gathering, processing, and fractionation within the Marcellus Shale region, predominantly in Pennsylvania and New York, and the Utica Shale region of eastern Ohio. The West segment delivers gas gathering, processing, and treating services across the Rocky Mountain areas of Colorado and Wyoming, the Barnett Shale in north-central Texas, the Eagle Ford Shale in South Texas, the Haynesville Shale in northwest Louisiana, and the expansive Mid-Continent region (including the Anadarko, Arkoma, and Permian basins). This segment also operates natural gas liquid (NGL) fractionation and storage facilities located near Conway in central Kansas. The Gas & NGL Marketing Services segment provides comprehensive wholesale marketing, trading, storage, and transportation of natural gas to utilities, municipalities, power generators, and producers, while also offering risk and asset management and NGL marketing services. The company possesses and operates an extensive network, including 30,000 miles of pipelines, 29 processing facilities, 7 fractionation facilities, and an approximate NGL storage capacity of 23 million barrels. The Williams Companies, Inc. was established in 1908 and maintains its headquarters in Tulsa, Oklahoma.","Chad J. Zamarin","Energy","US","5987","800-945-5426","One Williams Center","Tulsa","OK","74172","https://images.financialmodelingprep.com/symbol/WMB.png","1981-12-31",false,false,true,false,false +"AMAT",508.787,403956017733,1.597,1.98,"203.4-739.67",-3.223,-0.62948,1689500,8607662,"Applied Materials, Inc.","USD","0000006951","US0382221051","038222105","NASDAQ","NASDAQ","Semiconductors","https://www.appliedmaterials.com","Applied Materials, Inc. engages in provision of materials engineering solutions used to produce semiconductors. The firm also focuses on design, development, production, and servicing of the critical wafer fabrication tools used for customers to manufacture semiconductors. It operates through the following segments: Semiconductor Systems and Applied Global Services (AGS). The Semiconductor Systems segment includes designing, development, manufacturing and sale of equipment used to fabricate semiconductor chips referred to as integrated circuits. The AGS segment engages in provision of services, spares, and factory automation software to customer fabrication plants globally. The company was founded on November 10, 1967 and is headquartered in Santa Clara, CA.","Gary E. Dickerson","Technology","US","36500","408 727 5555","3050 Bowers Ave","Santa Clara","CA","95052-8039","https://images.financialmodelingprep.com/symbol/AMAT.png","1980-03-17",false,false,true,false,false diff --git a/crates/libfmp/tests/fixtures/bulk_dcf_valuations.csv b/crates/libfmp/tests/fixtures/bulk_dcf_valuations.csv new file mode 100644 index 00000000..fdef9f6e --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_dcf_valuations.csv @@ -0,0 +1,3 @@ +"symbol","date","dcf","Stock Price" +"000006.SZ","2026-09-29",2.525226853334803,7.62 +"000008.SZ","2026-09-29",-5.17533839393595,2.39 diff --git a/crates/libfmp/tests/fixtures/bulk_etf_holdings.csv b/crates/libfmp/tests/fixtures/bulk_etf_holdings.csv new file mode 100644 index 00000000..361ae769 --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_etf_holdings.csv @@ -0,0 +1,3 @@ +"symbol","name","sharesNumber","asset","weightPercentage","cusip","isin","marketValue","lastUpdated" +" -- ","Tidewater Inc",91962,"TDW",2.63,"88642R109","US88642R1095",6457163.796,"2026-09-27" +"0050.TW","Bizlink Holding Inc.",4103274,"3665.TW",0.39,"G11474106","KYG114741062",9672220672.39,"2026-09-24" diff --git a/crates/libfmp/tests/fixtures/bulk_etf_holdings.json b/crates/libfmp/tests/fixtures/bulk_etf_holdings.json index 6e5e50ca..9ad608c7 100644 --- a/crates/libfmp/tests/fixtures/bulk_etf_holdings.json +++ b/crates/libfmp/tests/fixtures/bulk_etf_holdings.json @@ -3,7 +3,7 @@ "asset": "009150.KS", "cusip": "", "isin": "KR7009150004", - "lastUpdated\"": "2024-09-06\"", + "lastUpdated": "2024-09-06", "marketValue": "1553142.49", "name": "SAMSUNG ELECTRO MECHANICS LTD", "sharesNumber": "15514", diff --git a/crates/libfmp/tests/fixtures/bulk_financial_scores.csv b/crates/libfmp/tests/fixtures/bulk_financial_scores.csv new file mode 100644 index 00000000..47b55404 --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_financial_scores.csv @@ -0,0 +1,3 @@ +"symbol","reportedCurrency","altmanZScore","piotroskiScore","workingCapital","totalAssets","retainedEarnings","ebit","marketCap","totalLiabilities","revenue" +"000001.SZ","CNY",-0.06634014283050256,4,-1105320000000,6031727000000,287857000000,52022000000,219286875637,5563513000000,208921000000 +"000002.SZ","CNY",0.04042819128279265,4,99594827000,959761483000,-59369866000,-69715634000,44168410698,743955251000,198278574000 diff --git a/crates/libfmp/tests/fixtures/bulk_price_target_summaries.csv b/crates/libfmp/tests/fixtures/bulk_price_target_summaries.csv new file mode 100644 index 00000000..6cbdda52 --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_price_target_summaries.csv @@ -0,0 +1,3 @@ +"symbol","lastMonthCount","lastMonthAvgPriceTarget","lastQuarterCount","lastQuarterAvgPriceTarget","lastYearCount","lastYearAvgPriceTarget","allTimeCount","allTimeAvgPriceTarget","publishers" +"A",2,160,11,172.27,33,167.64,53,159.49,"[""StreetInsider"",""Benzinga"",""Pulse 2.0""]" +"AA",1,60,6,63,24,61.33,52,57.76,"[""StreetInsider"",""Benzinga"",""Pulse 2.0""]" diff --git a/crates/libfmp/tests/fixtures/bulk_stock_ratings.csv b/crates/libfmp/tests/fixtures/bulk_stock_ratings.csv new file mode 100644 index 00000000..b1a1ad9c --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_stock_ratings.csv @@ -0,0 +1,3 @@ +"symbol","date","rating","discountedCashFlowScore","returnOnEquityScore","returnOnAssetsScore","debtToEquityScore","priceToEarningsScore","priceToBookScore" +"000001.SZ","2026-09-30","B-",1,3,2,1,4,4 +"000002.SZ","2026-09-30","C+",4,1,1,1,1,4 diff --git a/crates/libfmp/tests/fixtures/bulk_upgrades_downgrades_consensus.csv b/crates/libfmp/tests/fixtures/bulk_upgrades_downgrades_consensus.csv new file mode 100644 index 00000000..f9c4d6ae --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_upgrades_downgrades_consensus.csv @@ -0,0 +1,3 @@ +"symbol","strongBuy","buy","hold","sell","strongSell","consensus" +"000550.SZ",1,14,4,0,0,"Buy" +"0005.HK",0,5,10,2,0,"Hold" diff --git a/sdk/go/bulk.go b/sdk/go/bulk.go index 22e3b227..211bd93b 100644 --- a/sdk/go/bulk.go +++ b/sdk/go/bulk.go @@ -139,11 +139,7 @@ func (n *BulkNamespace) CompanyProfiles(ctx context.Context, q BulkPartQuery) ([ if err != nil { return nil, err } - var out []CompanyProfile - if err := n.client.getJSON(ctx, "profile-bulk", "profile-bulk", params, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[CompanyProfile](ctx, n.client, "profile-bulk", "profile-bulk", params) } // StockRatings retrieves worldwide stock ratings in one provider bulk @@ -151,11 +147,7 @@ func (n *BulkNamespace) CompanyProfiles(ctx context.Context, q BulkPartQuery) ([ // // GET rating-bulk func (n *BulkNamespace) StockRatings(ctx context.Context) ([]BulkStockRating, error) { - var out []BulkStockRating - if err := n.client.getJSON(ctx, "rating-bulk", "rating-bulk", nil, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkStockRating](ctx, n.client, "rating-bulk", "rating-bulk", nil) } // DCFValuations retrieves worldwide discounted-cash-flow valuations in one @@ -163,11 +155,7 @@ func (n *BulkNamespace) StockRatings(ctx context.Context) ([]BulkStockRating, er // // GET dcf-bulk func (n *BulkNamespace) DCFValuations(ctx context.Context) ([]BulkDCFValuation, error) { - var out []BulkDCFValuation - if err := n.client.getJSON(ctx, "dcf-bulk", "dcf-bulk", nil, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkDCFValuation](ctx, n.client, "dcf-bulk", "dcf-bulk", nil) } // FinancialScores retrieves worldwide financial scores in one provider bulk @@ -175,11 +163,7 @@ func (n *BulkNamespace) DCFValuations(ctx context.Context) ([]BulkDCFValuation, // // GET scores-bulk func (n *BulkNamespace) FinancialScores(ctx context.Context) ([]BulkFinancialScore, error) { - var out []BulkFinancialScore - if err := n.client.getJSON(ctx, "scores-bulk", "scores-bulk", nil, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkFinancialScore](ctx, n.client, "scores-bulk", "scores-bulk", nil) } // PriceTargetSummaries retrieves US price-target summaries in one provider @@ -187,11 +171,7 @@ func (n *BulkNamespace) FinancialScores(ctx context.Context) ([]BulkFinancialSco // // GET price-target-summary-bulk func (n *BulkNamespace) PriceTargetSummaries(ctx context.Context) ([]BulkPriceTargetSummary, error) { - var out []BulkPriceTargetSummary - if err := n.client.getJSON(ctx, "price-target-summary-bulk", "price-target-summary-bulk", nil, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkPriceTargetSummary](ctx, n.client, "price-target-summary-bulk", "price-target-summary-bulk", nil) } // ETFHoldings retrieves one provider partition of worldwide ETF holdings. @@ -202,11 +182,7 @@ func (n *BulkNamespace) ETFHoldings(ctx context.Context, q BulkPartQuery) ([]Bul if err != nil { return nil, err } - var out []BulkETFHolding - if err := n.client.getJSON(ctx, "etf-holder-bulk", "etf-holder-bulk", params, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkETFHolding](ctx, n.client, "etf-holder-bulk", "etf-holder-bulk", params) } // UpgradesDowngradesConsensus retrieves worldwide upgrades/downgrades @@ -214,11 +190,7 @@ func (n *BulkNamespace) ETFHoldings(ctx context.Context, q BulkPartQuery) ([]Bul // // GET upgrades-downgrades-consensus-bulk func (n *BulkNamespace) UpgradesDowngradesConsensus(ctx context.Context) ([]BulkUpgradesDowngradesConsensus, error) { - var out []BulkUpgradesDowngradesConsensus - if err := n.client.getJSON(ctx, "upgrades-downgrades-consensus-bulk", "upgrades-downgrades-consensus-bulk", nil, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkUpgradesDowngradesConsensus](ctx, n.client, "upgrades-downgrades-consensus-bulk", "upgrades-downgrades-consensus-bulk", nil) } // KeyMetricsTTM retrieves worldwide trailing-twelve-month key metrics in one diff --git a/sdk/go/bulk_models.go b/sdk/go/bulk_models.go index 9fdf02d3..6e6dfecb 100644 --- a/sdk/go/bulk_models.go +++ b/sdk/go/bulk_models.go @@ -2553,23 +2553,22 @@ type BulkETFHolding struct { CUSIP string `json:"cusip"` ISIN string `json:"isin"` MarketValue string `json:"marketValue"` - LastUpdatedRaw string `json:"-"` + LastUpdated Date `json:"lastUpdated"` } // bulkETFHoldingShadow mirrors BulkETFHolding with a pointer or raw value for // every required member so a missing or null member is observable after -// decoding. The embedded fallback collects the members json/v2 cannot spell in -// a struct tag. +// decoding. type bulkETFHoldingShadow struct { - Symbol *string `json:"symbol"` - Name *string `json:"name"` - SharesNumber *string `json:"sharesNumber"` - Asset *string `json:"asset"` - WeightPercentage *string `json:"weightPercentage"` - CUSIP *string `json:"cusip"` - ISIN *string `json:"isin"` - MarketValue *string `json:"marketValue"` - RawMembers map[string]jsontext.Value `json:",embed"` + Symbol *string `json:"symbol"` + Name *string `json:"name"` + SharesNumber *string `json:"sharesNumber"` + Asset *string `json:"asset"` + WeightPercentage *string `json:"weightPercentage"` + CUSIP *string `json:"cusip"` + ISIN *string `json:"isin"` + MarketValue *string `json:"marketValue"` + LastUpdated *Date `json:"lastUpdated"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -2580,7 +2579,6 @@ func (m *BulkETFHolding) UnmarshalJSONFrom(dec *jsontext.Decoder) error { if err := json.UnmarshalDecode(dec, &shadow); err != nil { return err } - lastUpdatedRawWire := rawMember(shadow.RawMembers, "lastUpdated\"") switch { case shadow.Symbol == nil: return missingMemberError("BulkETFHolding", "symbol") @@ -2598,12 +2596,8 @@ func (m *BulkETFHolding) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("BulkETFHolding", "isin") case shadow.MarketValue == nil: return missingMemberError("BulkETFHolding", "marketValue") - case lastUpdatedRawWire == nil: - return missingMemberError("BulkETFHolding", "lastUpdated\"") - } - var lastUpdatedRaw string - if err := json.Unmarshal(lastUpdatedRawWire, &lastUpdatedRaw); err != nil { - return memberDecodeError("BulkETFHolding", "lastUpdated\"", lastUpdatedRawWire, err) + case shadow.LastUpdated == nil: + return missingMemberError("BulkETFHolding", "lastUpdated") } *m = BulkETFHolding{ Symbol: *shadow.Symbol, @@ -2614,39 +2608,11 @@ func (m *BulkETFHolding) UnmarshalJSONFrom(dec *jsontext.Decoder) error { CUSIP: *shadow.CUSIP, ISIN: *shadow.ISIN, MarketValue: *shadow.MarketValue, - LastUpdatedRaw: lastUpdatedRaw, + LastUpdated: *shadow.LastUpdated, } return nil } -// bulkETFHoldingPlain is BulkETFHolding without its JSON methods, so its -// tagged members marshal through the ordinary struct rules. -type bulkETFHoldingPlain BulkETFHolding - -// bulkETFHoldingEmit carries the members json/v2 cannot spell in a struct tag -// through an embedded fallback, next to the promoted tagged members of -// bulkETFHoldingPlain. -type bulkETFHoldingEmit struct { - bulkETFHoldingPlain - RawMembers map[string]jsontext.Value `json:",embed"` -} - -// MarshalJSONTo encodes the object with every member under its exact wire -// name, including the names json/v2 cannot spell in a struct tag. The value -// receiver keeps non-addressable values, such as slice elements, on this path. -func (m BulkETFHolding) MarshalJSONTo(enc *jsontext.Encoder) error { - rawMembers := make(map[string]jsontext.Value, 1) - lastUpdatedRawWire, err := json.Marshal(m.LastUpdatedRaw) - if err != nil { - return err - } - rawMembers["lastUpdated\""] = lastUpdatedRawWire - return json.MarshalEncode(enc, bulkETFHoldingEmit{ - bulkETFHoldingPlain: bulkETFHoldingPlain(m), - RawMembers: rawMembers, - }) -} - // BulkUpgradesDowngradesConsensus is one worldwide // upgrades/downgrades-consensus bulk row. type BulkUpgradesDowngradesConsensus struct { diff --git a/sdk/go/bulk_parity_test.go b/sdk/go/bulk_parity_test.go index 60bb6b16..2dbb5983 100644 --- a/sdk/go/bulk_parity_test.go +++ b/sdk/go/bulk_parity_test.go @@ -3,8 +3,6 @@ package fmp import ( "encoding/json/jsontext" "encoding/json/v2" - "errors" - "strconv" "strings" "testing" ) @@ -77,58 +75,15 @@ func TestDocumentedBulkSnapshotsDecodeExactValues(t *testing.T) { } } -// The ETF holding row carries the documented wire key `lastUpdated"`, which -// encoding/json/v2 cannot spell in a struct tag: the member travels through -// the embedded fallback and MarshalJSONTo (see rawMember). Its value keeps -// the trailing quote, re-encodes under the exact key, and is required. -func TestBulkEtfHoldingKeepsTheQuotedLastUpdatedKeyVerbatim(t *testing.T) { +// The ETF holding row's lastUpdated member is a strict date. +func TestBulkEtfHoldingDecodesLastUpdatedAsADate(t *testing.T) { t.Parallel() holdings := assertFixtureParity[BulkETFHolding](t, "bulk_etf_holdings.json") if want := (BulkETFHolding{Symbol: "EXCH.AS", Name: "SAMSUNG ELECTRO MECHANICS LTD", SharesNumber: "15514", Asset: "009150.KS", WeightPercentage: "0.09611", CUSIP: "", ISIN: "KR7009150004", MarketValue: "1553142.49", - LastUpdatedRaw: `2024-09-06"`}); len(holdings) != 1 || holdings[0] != want { + LastUpdated: mustParseDate(t, "2024-09-06")}); len(holdings) != 1 || holdings[0] != want { t.Fatalf("bulk_etf_holdings = %+v", holdings) } - encoded, err := json.Marshal(holdings) - if err != nil || !strings.Contains(string(encoded), `"lastUpdated\"":"2024-09-06\""`) || - strings.Contains(string(encoded), `"lastUpdated":`) || strings.Contains(string(encoded), "RawMembers") { - t.Fatalf("re-encoded holdings = %s, %v", encoded, err) - } - var decoded []BulkETFHolding - if err := json.Unmarshal(encoded, &decoded); err != nil || len(decoded) != 1 || decoded[0] != holdings[0] { - t.Fatalf("round trip = %+v, %v", decoded, err) - } - - var wire []map[string]jsontext.Value - if err := json.Unmarshal(readFixture(t, "bulk_etf_holdings.json"), &wire); err != nil { - t.Fatal(err) - } - for _, tc := range []struct{ name, member, value string }{ - {"missing raw key", `lastUpdated"`, ""}, - {"null raw key", `lastUpdated"`, "null"}, - {"missing tagged member", "symbol", ""}, - {"null tagged member", "symbol", "null"}, - } { - row := map[string]jsontext.Value{} - for member, value := range wire[0] { - row[member] = value - } - delete(row, tc.member) - if tc.value != "" { - row[tc.member] = jsontext.Value(tc.value) - } - body, err := json.Marshal([]map[string]jsontext.Value{row}) - if err != nil { - t.Fatal(err) - } - var rows []BulkETFHolding - err = json.Unmarshal(body, &rows) - var typed *Error - if !errors.As(err, &typed) || typed.Category != CategoryDecode || - !strings.Contains(typed.Message, "required member "+strconv.Quote(tc.member)+" of BulkETFHolding") { - t.Fatalf("%s: error = %v, want a Decode *Error naming %q", tc.name, err, tc.member) - } - } } // Exact values copied from crates/libfmp/tests/bulk_metrics_responses.rs, diff --git a/sdk/go/bulk_test.go b/sdk/go/bulk_test.go index e906b240..fdf6fd07 100644 --- a/sdk/go/bulk_test.go +++ b/sdk/go/bulk_test.go @@ -14,13 +14,13 @@ import ( // crates/libfmp/tests/bulk_*_endpoints.rs (year before period, one GET, no // fan-out over parts). var bulkRoutes = map[string]string{ - "/router/stable/profile-bulk?part=0": "bulk_company_profiles.json", - "/router/stable/rating-bulk": "bulk_stock_ratings.json", - "/router/stable/dcf-bulk": "bulk_dcf_valuations.json", - "/router/stable/scores-bulk": "bulk_financial_scores.json", - "/router/stable/price-target-summary-bulk": "bulk_price_target_summaries.json", - "/router/stable/etf-holder-bulk?part=segment+A%2F7": "bulk_etf_holdings.json", - "/router/stable/upgrades-downgrades-consensus-bulk": "bulk_upgrades_downgrades_consensus.json", + "/router/stable/profile-bulk?part=0": "bulk_company_profiles.csv", + "/router/stable/rating-bulk": "bulk_stock_ratings.csv", + "/router/stable/dcf-bulk": "bulk_dcf_valuations.csv", + "/router/stable/scores-bulk": "bulk_financial_scores.csv", + "/router/stable/price-target-summary-bulk": "bulk_price_target_summaries.csv", + "/router/stable/etf-holder-bulk?part=segment+A%2F7": "bulk_etf_holdings.csv", + "/router/stable/upgrades-downgrades-consensus-bulk": "bulk_upgrades_downgrades_consensus.csv", "/router/stable/key-metrics-ttm-bulk": "bulk_key_metrics_ttm.json", "/router/stable/ratios-ttm-bulk": "bulk_financial_ratios_ttm.json", "/router/stable/peers-bulk": "bulk_stock_peers.json", @@ -44,6 +44,9 @@ func bulkRouter(t *testing.T) http.HandlerFunc { return } w.Header().Set("Content-Type", "application/json") + if strings.HasSuffix(fixture, ".csv") { + w.Header().Set("Content-Type", "text/csv") + } _, _ = w.Write(readFixture(t, fixture)) } } @@ -55,31 +58,31 @@ func TestBulkMethodsUseExactPathsAndWireParameterOrder(t *testing.T) { ctx := context.Background() profiles, err := client.Bulk.CompanyProfiles(ctx, NewBulkPartQuery("0")) - if err != nil || len(profiles) != 1 || profiles[0].Symbol != "AAPL" || profiles[0].MarketCap != 4_009_711_150_080 { + if err != nil || len(profiles) != 2 || profiles[0].Symbol != "WMB" || profiles[0].MarketCap != 82_906_291_252 { t.Fatalf("CompanyProfiles = %+v, %v", profiles, err) } ratings, err := client.Bulk.StockRatings(ctx) - if err != nil || len(ratings) != 1 || ratings[0].Rating != "B+" { + if err != nil || len(ratings) != 2 || ratings[1].Rating != "C+" { t.Fatalf("StockRatings = %+v, %v", ratings, err) } dcf, err := client.Bulk.DCFValuations(ctx) - if err != nil || len(dcf) != 1 || dcf[0].StockPrice != "6.54" { + if err != nil || len(dcf) != 2 || dcf[1].StockPrice != "2.39" { t.Fatalf("DCFValuations = %+v, %v", dcf, err) } scores, err := client.Bulk.FinancialScores(ctx) - if err != nil || len(scores) != 1 || scores[0].PiotroskiScore != "5" { + if err != nil || len(scores) != 2 || scores[0].PiotroskiScore != "4" { t.Fatalf("FinancialScores = %+v, %v", scores, err) } targets, err := client.Bulk.PriceTargetSummaries(ctx) - if err != nil || len(targets) != 1 || targets[0].Symbol != "A" { + if err != nil || len(targets) != 2 || targets[1].Symbol != "AA" { t.Fatalf("PriceTargetSummaries = %+v, %v", targets, err) } holdings, err := client.Bulk.ETFHoldings(ctx, NewBulkPartQuery("segment A/7")) - if err != nil || len(holdings) != 1 || holdings[0].LastUpdatedRaw != `2024-09-06"` { + if err != nil || len(holdings) != 2 || holdings[1].Asset != "3665.TW" { t.Fatalf("ETFHoldings = %+v, %v", holdings, err) } consensus, err := client.Bulk.UpgradesDowngradesConsensus(ctx) - if err != nil || len(consensus) != 1 || consensus[0].Consensus != "Buy" { + if err != nil || len(consensus) != 2 || consensus[1].Consensus != "Hold" { t.Fatalf("UpgradesDowngradesConsensus = %+v, %v", consensus, err) } metrics, err := client.Bulk.KeyMetricsTTM(ctx) @@ -203,20 +206,27 @@ func TestBulkQueriesAreValidatedBeforeAnyRequest(t *testing.T) { } } -// A bulk row missing its raw-keyed member surfaces as a Decode error on the -// endpoint, exactly like a missing tagged member does. +// A bulk body missing a model member's column surfaces as a Decode error on +// the endpoint at the first row and that member. func TestBulkMethodsReportMissingMembersAsDecodeErrors(t *testing.T) { t.Parallel() - body := strings.Replace(string(readFixture(t, "bulk_etf_holdings.json")), `"lastUpdated\"": "2024-09-06\"",`, "", 1) - if body == string(readFixture(t, "bulk_etf_holdings.json")) { - t.Fatal("fixture no longer carries the quoted lastUpdated member on one line") - } - server, _ := newServer(t, jsonHandler(body)) + lines := strings.Split(strings.TrimSuffix(string(readFixture(t, "bulk_etf_holdings.csv")), "\n"), "\n") + if !strings.HasSuffix(lines[0], `,"lastUpdated"`) { + t.Fatal("fixture no longer ends its header with the lastUpdated column") + } + for index, line := range lines { + lines[index] = line[:strings.LastIndex(line, ",")] + } + body := strings.Join(lines, "\n") + "\n" + server, _ := newServer(t, func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/csv") + _, _ = w.Write([]byte(body)) + }) client := newClient(t, server, WithAuthentication(FMPHeader("route-secret"))) _, err := client.Bulk.ETFHoldings(context.Background(), NewBulkPartQuery("0")) typed := assertQuoteError(t, err, CategoryDecode, http.StatusOK, "etf-holder-bulk") - if cause := typed.Unwrap(); cause == nil || !strings.Contains(cause.Error(), `"lastUpdated\""`) { - t.Fatalf("cause = %v, want it to name the missing member lastUpdated\"", cause) + if typed.Path != "/0/lastUpdated" || typed.DecodeKind != DecodeKindMissingMember { + t.Fatalf("error = %+v, want a missing member at /0/lastUpdated", typed) } } From ca7e5ed6305bc0cb06a5af2f78ec72951d089639 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:39:48 +0800 Subject: [PATCH 09/31] test(libfmp): rewrite bulk snapshot suite over live CSV fixtures Reworks the bulk snapshot response suite to exercise the live CSV fixtures through the shared CSV helpers instead of the old JSON-model assertions, which no longer apply now that these routes do not answer JSON. **Testing changes:** * Replaces the JSON `include_bytes!` fixtures with their CSV counterparts and drives every assertion through the endpoint builders and the shared `bulk_csv` helpers (`assert_round_trip`, `assert_members`, `assert_empty_bodies`, `decode`). * Verifies a per-cell round trip with exact field counts for all seven bulk contracts. * Asserts the member contract: which members accept an empty cell, that every model column is required, and that unknown columns are tolerated. * Checks that empty and header-only bodies decode to no rows. * Confirms provider spellings and quoted text are preserved verbatim. * Drops the JSON-model assertions for required/nullable fields, numeric rejection, and bare-array roots. Relates to #376 --- .../libfmp/tests/bulk_snapshot_responses.rs | 227 +++++++----------- 1 file changed, 88 insertions(+), 139 deletions(-) diff --git a/crates/libfmp/tests/bulk_snapshot_responses.rs b/crates/libfmp/tests/bulk_snapshot_responses.rs index cd8f3f35..2d9b1aa8 100644 --- a/crates/libfmp/tests/bulk_snapshot_responses.rs +++ b/crates/libfmp/tests/bulk_snapshot_responses.rs @@ -1,163 +1,112 @@ -use serde::{Serialize, de::DeserializeOwned}; -use serde_json::{Map, Value, json}; +#[path = "support/bulk_csv.rs"] +mod bulk_csv; +#[allow(dead_code)] // The CSV helpers use only the fixture executor. +mod support; use libfmp::{ - responses::{ - bulk::{ - BulkDcfValuation, BulkEtfHolding, BulkFinancialScore, BulkPriceTargetSummary, - BulkStockRating, BulkUpgradesDowngradesConsensus, - }, - company::CompanyProfile, + endpoints::bulk::{ + BulkPartQuery, bulk_company_profiles, bulk_dcf_valuations, bulk_etf_holdings, + bulk_financial_scores, bulk_price_target_summaries, bulk_stock_ratings, + bulk_upgrades_downgrades_consensus, }, - types::{Date, Ticker}, + types::{BulkPart, Date, Ticker}, }; -const PROFILE: &[u8] = include_bytes!("fixtures/bulk_company_profiles.json"); -const RATING: &[u8] = include_bytes!("fixtures/bulk_stock_ratings.json"); -const DCF: &[u8] = include_bytes!("fixtures/bulk_dcf_valuations.json"); -const SCORES: &[u8] = include_bytes!("fixtures/bulk_financial_scores.json"); -const TARGET: &[u8] = include_bytes!("fixtures/bulk_price_target_summaries.json"); -const ETF: &[u8] = include_bytes!("fixtures/bulk_etf_holdings.json"); -const CONSENSUS: &[u8] = include_bytes!("fixtures/bulk_upgrades_downgrades_consensus.json"); +use bulk_csv::{assert_empty_bodies, assert_members, assert_round_trip, decode}; -#[test] -fn exact_outer_md_fixtures_round_trip_with_exact_field_counts() { - assert_exact::(PROFILE, 36); - assert_exact::(RATING, 9); - assert_exact::(DCF, 4); - assert_exact::(SCORES, 11); - assert_exact::(TARGET, 10); - assert_exact::(ETF, 9); - assert_exact::(CONSENSUS, 7); -} +const PROFILE: &[u8] = include_bytes!("fixtures/bulk_company_profiles.csv"); +const RATING: &[u8] = include_bytes!("fixtures/bulk_stock_ratings.csv"); +const DCF: &[u8] = include_bytes!("fixtures/bulk_dcf_valuations.csv"); +const SCORES: &[u8] = include_bytes!("fixtures/bulk_financial_scores.csv"); +const TARGET: &[u8] = include_bytes!("fixtures/bulk_price_target_summaries.csv"); +const ETF: &[u8] = include_bytes!("fixtures/bulk_etf_holdings.csv"); +const CONSENSUS: &[u8] = include_bytes!("fixtures/bulk_upgrades_downgrades_consensus.csv"); -#[test] -fn every_documented_field_is_required_non_null_and_unknown_fields_are_tolerated() { - assert_required_nullable::( - PROFILE, - &["cik", "cusip", "fullTimeEmployees", "phone", "ipoDate"], - ); - assert_required::(RATING); - assert_required::(DCF); - assert_required::(SCORES); - assert_required::(TARGET); - assert_required::(ETF); - assert_required::(CONSENSUS); +fn part() -> BulkPartQuery { + BulkPart::new("0").unwrap().into() } #[test] -fn numeric_string_fields_reject_json_numbers_in_every_new_contract() { - assert_number_rejected::(RATING, "discountedCashFlowScore"); - assert_number_rejected::(DCF, "dcf"); - assert_number_rejected::(DCF, "Stock Price"); - assert_number_rejected::(SCORES, "altmanZScore"); - assert_number_rejected::(TARGET, "allTimeAvgPriceTarget"); - assert_number_rejected::(ETF, "marketValue"); - assert_number_rejected::(CONSENSUS, "strongBuy"); +fn live_csv_fixtures_round_trip_every_cell_with_exact_field_counts() { + assert_round_trip(&bulk_company_profiles(part()), PROFILE, 36); + assert_round_trip(&bulk_stock_ratings(), RATING, 9); + assert_round_trip(&bulk_dcf_valuations(), DCF, 4); + assert_round_trip(&bulk_financial_scores(), SCORES, 11); + assert_round_trip(&bulk_price_target_summaries(), TARGET, 10); + assert_round_trip(&bulk_etf_holdings(part()), ETF, 9); + assert_round_trip(&bulk_upgrades_downgrades_consensus(), CONSENSUS, 7); } #[test] -fn malformed_documented_strings_and_wire_keys_are_preserved_verbatim() { - let target = rows::(TARGET).remove(0); - assert_eq!(target.publishers, "[\"\"TheFly\""); - - let etf = rows::(ETF).remove(0); - assert_eq!(etf.cusip, ""); - assert_eq!(etf.last_updated, Date::parse("2024-09-06").unwrap()); - - let consensus = rows::(CONSENSUS).remove(0); - assert_eq!(consensus.symbol, ""); +fn every_column_is_required_and_only_optional_or_text_members_accept_an_empty_cell() { + assert_members( + &bulk_company_profiles(part()), + PROFILE, + &[ + "range", + "companyName", + "cik", + "cusip", + "exchangeFullName", + "website", + "description", + "ceo", + "fullTimeEmployees", + "phone", + "address", + "city", + "state", + "zip", + "image", + "ipoDate", + ], + ); + assert_members(&bulk_stock_ratings(), RATING, &["rating"]); + assert_members(&bulk_dcf_valuations(), DCF, &[]); + assert_members(&bulk_financial_scores(), SCORES, &[]); + assert_members(&bulk_price_target_summaries(), TARGET, &["publishers"]); + assert_members(&bulk_etf_holdings(part()), ETF, &["name", "cusip"]); + assert_members( + &bulk_upgrades_downgrades_consensus(), + CONSENSUS, + &["symbol", "consensus"], + ); } #[test] -fn company_profile_is_the_existing_typed_contract_and_new_symbols_are_typed_where_proven() { - let profile = rows::(PROFILE).remove(0); - assert_eq!(profile.symbol, Ticker::new("AAPL").unwrap()); - - let rating = rows::(RATING).remove(0); - assert_eq!(rating.symbol.as_str(), "000001.SZ"); - let etf = rows::(ETF).remove(0); - assert_eq!(etf.asset.as_str(), "009150.KS"); +fn empty_and_header_only_bodies_decode_to_no_rows() { + assert_empty_bodies(&bulk_company_profiles(part()), PROFILE); + assert_empty_bodies(&bulk_stock_ratings(), RATING); + assert_empty_bodies(&bulk_dcf_valuations(), DCF); + assert_empty_bodies(&bulk_financial_scores(), SCORES); + assert_empty_bodies(&bulk_price_target_summaries(), TARGET); + assert_empty_bodies(&bulk_etf_holdings(part()), ETF); + assert_empty_bodies(&bulk_upgrades_downgrades_consensus(), CONSENSUS); } #[test] -fn all_seven_contracts_require_bare_array_roots_and_accept_empty_arrays() { - assert_array_contract::(); - assert_array_contract::(); - assert_array_contract::(); - assert_array_contract::(); - assert_array_contract::(); - assert_array_contract::(); - assert_array_contract::(); -} - -fn rows(fixture: &[u8]) -> Vec { - serde_json::from_slice(fixture).unwrap() -} +fn provider_spellings_and_quoted_text_are_preserved_verbatim() { + let dcf = decode(&bulk_dcf_valuations(), DCF).unwrap().remove(0); + assert_eq!(dcf.stock_price.as_str(), "7.62"); + assert_eq!(dcf.dcf.as_str(), "2.525226853334803"); -fn source_row(fixture: &[u8]) -> Map { - serde_json::from_slice::(fixture).unwrap()[0] - .as_object() + let target = decode(&bulk_price_target_summaries(), TARGET) .unwrap() - .clone() -} - -fn assert_exact(fixture: &[u8], fields: usize) -where - T: DeserializeOwned + Serialize, -{ - let source: Value = serde_json::from_slice(fixture).unwrap(); - assert_eq!(source[0].as_object().unwrap().len(), fields); - assert_eq!(serde_json::to_value(rows::(fixture)).unwrap(), source); -} - -fn assert_required(fixture: &[u8]) -where - T: DeserializeOwned, -{ - assert_required_nullable::(fixture, &[]); -} - -fn assert_required_nullable(fixture: &[u8], nullable: &[&str]) -where - T: DeserializeOwned, -{ - let row = source_row(fixture); - for field in row.keys() { - let mut missing = row.clone(); - missing.remove(field); - assert!( - serde_json::from_value::(Value::Object(missing)).is_err(), - "missing {field} unexpectedly decoded" - ); - - let mut null = row.clone(); - null.insert(field.clone(), Value::Null); - assert_eq!( - serde_json::from_value::(Value::Object(null)).is_ok(), - nullable.contains(&field.as_str()), - "null {field} decoded contrary to its nullability" - ); - } - - let mut future = row; - future.insert("futureField".into(), json!({"nested": true})); - assert!(serde_json::from_value::(Value::Object(future)).is_ok()); -} + .remove(0); + assert_eq!( + target.publishers, + r#"["StreetInsider","Benzinga","Pulse 2.0"]"# + ); -fn assert_number_rejected(fixture: &[u8], field: &str) -where - T: DeserializeOwned, -{ - let mut row = source_row(fixture); - row.insert(field.into(), json!(123.5)); - assert!(serde_json::from_value::(Value::Object(row)).is_err()); -} + let etf = decode(&bulk_etf_holdings(part()), ETF).unwrap().remove(0); + assert_eq!(etf.symbol.as_str(), " -- "); + assert_eq!(etf.asset, Ticker::new("TDW").unwrap()); + assert_eq!(etf.last_updated, Date::parse("2026-09-27").unwrap()); -fn assert_array_contract() -where - T: DeserializeOwned, -{ - assert!(serde_json::from_slice::>(b"[]").unwrap().is_empty()); - assert!(serde_json::from_slice::>(b"{}").is_err()); + let profile = decode(&bulk_company_profiles(part()), PROFILE) + .unwrap() + .remove(0); + assert_eq!(profile.symbol, Ticker::new("WMB").unwrap()); + assert_eq!(profile.company_name, "The Williams Companies, Inc."); + assert!(!profile.is_etf); } From c5a3efbfaccc728c969813a24466c12ac91a4c60 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:40:41 +0800 Subject: [PATCH 10/31] test(sdk/go): decode bulk parity fixtures from shared live CSV The seven bulk snapshot JSON fixtures no longer described the wire, so the Go bulk parity tests now exercise the shared live CSV fixtures directly. **CSV parity helper:** * Added `assertCSVFixtureParity` in `sdk/go/csv_test.go`, mirroring `assert_round_trip` in `crates/libfmp/tests/support/bulk_csv.rs`: every row decodes through `decodeCSVRows`, the record count matches, and the first row re-encodes an exact number of members, each a header column whose re-encoded text (verbatim string, parsed number or boolean, or null/empty cell) equals the source cell. **Parity tests:** * Switched `TestBulkFixturesDecodeAndReencodeToTheSameMemberSet` and the renamed `TestBulkSnapshotsDecodeExactValues` to the CSV fixtures with per-type member counts, updating the expected values to the live rows and preserving the provider spellings (the "Stock Price" column, the quoted publishers list, the " -- " ETF symbol). * Folded the ETF holding date check into the exact-values test. **Removed fixtures:** * Deleted the seven `crates/libfmp/tests/fixtures/bulk_*.json` snapshot fixtures that no longer describe the wire format. Refs #376 --- .../tests/fixtures/bulk_company_profiles.json | 40 --------- .../tests/fixtures/bulk_dcf_valuations.json | 8 -- .../tests/fixtures/bulk_etf_holdings.json | 13 --- .../tests/fixtures/bulk_financial_scores.json | 15 ---- .../fixtures/bulk_price_target_summaries.json | 14 --- .../tests/fixtures/bulk_stock_ratings.json | 13 --- .../bulk_upgrades_downgrades_consensus.json | 11 --- sdk/go/bulk_parity_test.go | 89 ++++++++----------- sdk/go/csv_test.go | 56 ++++++++++++ 9 files changed, 93 insertions(+), 166 deletions(-) delete mode 100644 crates/libfmp/tests/fixtures/bulk_company_profiles.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_dcf_valuations.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_etf_holdings.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_financial_scores.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_price_target_summaries.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_stock_ratings.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_upgrades_downgrades_consensus.json diff --git a/crates/libfmp/tests/fixtures/bulk_company_profiles.json b/crates/libfmp/tests/fixtures/bulk_company_profiles.json deleted file mode 100644 index 03fd292b..00000000 --- a/crates/libfmp/tests/fixtures/bulk_company_profiles.json +++ /dev/null @@ -1,40 +0,0 @@ -[ - { - "address": "One Apple Park Way", - "averageVolume": 48811139, - "beta": 1.107, - "ceo": "Timothy D. Cook", - "change": -0.83, - "changePercentage": -0.30493, - "cik": "0000320193", - "city": "Cupertino", - "companyName": "Apple Inc.", - "country": "US", - "currency": "USD", - "cusip": "037833100", - "defaultImage": false, - "description": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. The company offers iPhone, a line of smartphones; Mac, a line of personal computers; iPad, a line of multi-purpose tablets; and wearables, home, and accessories comprising AirPods, Apple TV, Apple Watch, Beats products, and HomePod. It also provides AppleCare support and cloud services; and operates various platforms, including the App Store that allow customers to discover and download applications and digital content, such as books, music, video, games, and podcasts, as well as advertising services include third-party licensing arrangements and its own advertising platforms. In addition, the company offers various subscription-based services, such as Apple Arcade, a game subscription service; Apple Fitness+, a personalized fitness service; Apple Music, which offers users a curated listening experience with on-demand radio stations; Apple News+, a subscription news and magazine service; Apple TV+, which offers exclusive original content; Apple Card, a co-branded credit card; and Apple Pay, a cashless payment service, as well as licenses its intellectual property. The company serves consumers, and small and mid-sized businesses; and the education, enterprise, and government markets. It distributes third-party applications for its products through the App Store. The company also sells its products through its retail and online stores, and direct sales force; and third-party cellular network carriers, wholesalers, retailers, and resellers. Apple Inc. was founded in 1976 and is headquartered in Cupertino, California.", - "exchange": "NASDAQ", - "exchangeFullName": "NASDAQ Global Select", - "fullTimeEmployees": "164000", - "image": "https://images.financialmodelingprep.com/symbol/AAPL.png", - "industry": "Consumer Electronics", - "ipoDate": "1980-12-12", - "isActivelyTrading": true, - "isAdr": false, - "isEtf": false, - "isFund": false, - "isin": "US0378331005", - "lastDividend": 1.03, - "marketCap": 4009711150080, - "phone": "(408) 996-1010", - "price": 271.36, - "range": "169.21-288.62", - "sector": "Technology", - "state": "CA", - "symbol": "AAPL", - "volume": 44494594, - "website": "https://www.apple.com", - "zip": "95014" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_dcf_valuations.json b/crates/libfmp/tests/fixtures/bulk_dcf_valuations.json deleted file mode 100644 index ca623eb3..00000000 --- a/crates/libfmp/tests/fixtures/bulk_dcf_valuations.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "Stock Price": "6.54", - "date": "2025-07-09", - "dcf": "179.6654688379575", - "symbol": "000002.SZ" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_etf_holdings.json b/crates/libfmp/tests/fixtures/bulk_etf_holdings.json deleted file mode 100644 index 9ad608c7..00000000 --- a/crates/libfmp/tests/fixtures/bulk_etf_holdings.json +++ /dev/null @@ -1,13 +0,0 @@ -[ - { - "asset": "009150.KS", - "cusip": "", - "isin": "KR7009150004", - "lastUpdated": "2024-09-06", - "marketValue": "1553142.49", - "name": "SAMSUNG ELECTRO MECHANICS LTD", - "sharesNumber": "15514", - "symbol": "EXCH.AS", - "weightPercentage": "0.09611" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_financial_scores.json b/crates/libfmp/tests/fixtures/bulk_financial_scores.json deleted file mode 100644 index 21c4184b..00000000 --- a/crates/libfmp/tests/fixtures/bulk_financial_scores.json +++ /dev/null @@ -1,15 +0,0 @@ -[ - { - "altmanZScore": "0.29153682196643543", - "ebit": "32590000000", - "marketCap": "236751980000", - "piotroskiScore": "5", - "reportedCurrency": "CNY", - "retainedEarnings": "255621000000", - "revenue": "167996000000", - "symbol": "000001.SZ", - "totalAssets": "5777858000000", - "totalLiabilities": "5271746000000", - "workingCapital": "746131000000" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_price_target_summaries.json b/crates/libfmp/tests/fixtures/bulk_price_target_summaries.json deleted file mode 100644 index dcd896d0..00000000 --- a/crates/libfmp/tests/fixtures/bulk_price_target_summaries.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "allTimeAvgPriceTarget": "146.61", - "allTimeCount": "18", - "lastMonthAvgPriceTarget": "0", - "lastMonthCount": "0", - "lastQuarterAvgPriceTarget": "116", - "lastQuarterCount": "1", - "lastYearAvgPriceTarget": "142.17", - "lastYearCount": "6", - "publishers": "[\"\"TheFly\"", - "symbol": "A" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_stock_ratings.json b/crates/libfmp/tests/fixtures/bulk_stock_ratings.json deleted file mode 100644 index 2ddc594e..00000000 --- a/crates/libfmp/tests/fixtures/bulk_stock_ratings.json +++ /dev/null @@ -1,13 +0,0 @@ -[ - { - "date": "2025-07-09", - "debtToEquityScore": "1", - "discountedCashFlowScore": "5", - "priceToBookScore": "4", - "priceToEarningsScore": "4", - "rating": "B+", - "returnOnAssetsScore": "2", - "returnOnEquityScore": "3", - "symbol": "000001.SZ" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_upgrades_downgrades_consensus.json b/crates/libfmp/tests/fixtures/bulk_upgrades_downgrades_consensus.json deleted file mode 100644 index 641ffb6c..00000000 --- a/crates/libfmp/tests/fixtures/bulk_upgrades_downgrades_consensus.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "buy": "1", - "consensus": "Buy", - "hold": "1", - "sell": "0", - "strongBuy": "0", - "strongSell": "0", - "symbol": "" - } -] diff --git a/sdk/go/bulk_parity_test.go b/sdk/go/bulk_parity_test.go index 2dbb5983..bb5e73c3 100644 --- a/sdk/go/bulk_parity_test.go +++ b/sdk/go/bulk_parity_test.go @@ -13,13 +13,13 @@ import ( // relatively from the shared fixture directory, never copied. func TestBulkFixturesDecodeAndReencodeToTheSameMemberSet(t *testing.T) { t.Parallel() - assertFixtureParity[CompanyProfile](t, "bulk_company_profiles.json") - assertFixtureParity[BulkStockRating](t, "bulk_stock_ratings.json") - assertFixtureParity[BulkDCFValuation](t, "bulk_dcf_valuations.json") - assertFixtureParity[BulkFinancialScore](t, "bulk_financial_scores.json") - assertFixtureParity[BulkPriceTargetSummary](t, "bulk_price_target_summaries.json") - assertFixtureParity[BulkETFHolding](t, "bulk_etf_holdings.json") - assertFixtureParity[BulkUpgradesDowngradesConsensus](t, "bulk_upgrades_downgrades_consensus.json") + assertCSVFixtureParity[CompanyProfile](t, "bulk_company_profiles.csv", 36) + assertCSVFixtureParity[BulkStockRating](t, "bulk_stock_ratings.csv", 9) + assertCSVFixtureParity[BulkDCFValuation](t, "bulk_dcf_valuations.csv", 4) + assertCSVFixtureParity[BulkFinancialScore](t, "bulk_financial_scores.csv", 11) + assertCSVFixtureParity[BulkPriceTargetSummary](t, "bulk_price_target_summaries.csv", 10) + assertCSVFixtureParity[BulkETFHolding](t, "bulk_etf_holdings.csv", 9) + assertCSVFixtureParity[BulkUpgradesDowngradesConsensus](t, "bulk_upgrades_downgrades_consensus.csv", 7) assertFixtureParity[BulkKeyMetricsTTM](t, "bulk_key_metrics_ttm.json") assertFixtureParity[BulkFinancialRatiosTTM](t, "bulk_financial_ratios_ttm.json") assertFixtureParity[BulkStockPeer](t, "bulk_stock_peers.json") @@ -34,55 +34,40 @@ func TestBulkFixturesDecodeAndReencodeToTheSameMemberSet(t *testing.T) { } // Exact values copied from crates/libfmp/tests/bulk_snapshot_responses.rs: -// every numeric cell stays the provider's string, and the documented source -// defects (the "Stock Price" key, the malformed publishers text, the empty -// CUSIP and consensus symbol) are preserved verbatim. -func TestDocumentedBulkSnapshotsDecodeExactValues(t *testing.T) { +// every numeric cell stays the provider's text, and the provider spellings +// (the "Stock Price" column, the quoted publishers list, the " -- " ETF +// symbol) are preserved verbatim. +func TestBulkSnapshotsDecodeExactValues(t *testing.T) { t.Parallel() - profiles := assertFixtureParity[CompanyProfile](t, "bulk_company_profiles.json") - if len(profiles) != 1 || profiles[0].Symbol != "AAPL" || profiles[0].CIK == nil || *profiles[0].CIK != "0000320193" { - t.Fatalf("bulk_company_profiles = %+v", profiles) - } - ratings := assertFixtureParity[BulkStockRating](t, "bulk_stock_ratings.json") - if want := (BulkStockRating{Symbol: "000001.SZ", Date: mustParseDate(t, "2025-07-09"), Rating: "B+", - DiscountedCashFlowScore: "5", ReturnOnEquityScore: "3", ReturnOnAssetsScore: "2", DebtToEquityScore: "1", - PriceToEarningsScore: "4", PriceToBookScore: "4"}); len(ratings) != 1 || ratings[0] != want { - t.Fatalf("bulk_stock_ratings = %+v", ratings) - } - dcf := assertFixtureParity[BulkDCFValuation](t, "bulk_dcf_valuations.json") - if want := (BulkDCFValuation{Symbol: "000002.SZ", Date: mustParseDate(t, "2025-07-09"), - DCF: "179.6654688379575", StockPrice: "6.54"}); len(dcf) != 1 || dcf[0] != want { - t.Fatalf("bulk_dcf_valuations = %+v", dcf) - } - if encoded, err := json.Marshal(dcf[0]); err != nil || !strings.Contains(string(encoded), `"Stock Price":"6.54"`) { + profiles := assertCSVFixtureParity[CompanyProfile](t, "bulk_company_profiles.csv", 36) + if profiles[0].Symbol != "WMB" || profiles[0].CompanyName != "The Williams Companies, Inc." || + profiles[0].CIK == nil || *profiles[0].CIK != "0000107263" || profiles[0].IsETF { + t.Fatalf("bulk_company_profiles = %+v", profiles[0]) + } + ratings := assertCSVFixtureParity[BulkStockRating](t, "bulk_stock_ratings.csv", 9) + if want := (BulkStockRating{Symbol: "000001.SZ", Date: mustParseDate(t, "2026-09-30"), Rating: "B-", + DiscountedCashFlowScore: "1", ReturnOnEquityScore: "3", ReturnOnAssetsScore: "2", DebtToEquityScore: "1", + PriceToEarningsScore: "4", PriceToBookScore: "4"}); ratings[0] != want { + t.Fatalf("bulk_stock_ratings = %+v", ratings[0]) + } + dcf := assertCSVFixtureParity[BulkDCFValuation](t, "bulk_dcf_valuations.csv", 4) + if want := (BulkDCFValuation{Symbol: "000006.SZ", Date: mustParseDate(t, "2026-09-29"), + DCF: "2.525226853334803", StockPrice: "7.62"}); dcf[0] != want { + t.Fatalf("bulk_dcf_valuations = %+v", dcf[0]) + } + if encoded, err := json.Marshal(dcf[0]); err != nil || !strings.Contains(string(encoded), `"Stock Price":"7.62"`) { t.Fatalf("re-encoded dcf = %s, %v", encoded, err) } - scores := assertFixtureParity[BulkFinancialScore](t, "bulk_financial_scores.json") - if len(scores) != 1 || scores[0].Symbol != "000001.SZ" || scores[0].ReportedCurrency != "CNY" || - scores[0].AltmanZScore != "0.29153682196643543" || scores[0].PiotroskiScore != "5" || - scores[0].TotalAssets != "5777858000000" { - t.Fatalf("bulk_financial_scores = %+v", scores) + targets := assertCSVFixtureParity[BulkPriceTargetSummary](t, "bulk_price_target_summaries.csv", 10) + if targets[0].Symbol != "A" || targets[0].Publishers != `["StreetInsider","Benzinga","Pulse 2.0"]` || + targets[0].AllTimeAvgPriceTarget != "159.49" { + t.Fatalf("bulk_price_target_summaries = %+v", targets[0]) } - targets := assertFixtureParity[BulkPriceTargetSummary](t, "bulk_price_target_summaries.json") - if len(targets) != 1 || targets[0].Symbol != "A" || targets[0].Publishers != `[""TheFly"` || - targets[0].LastMonthCount != "0" || targets[0].AllTimeAvgPriceTarget != "146.61" { - t.Fatalf("bulk_price_target_summaries = %+v", targets) - } - consensus := assertFixtureParity[BulkUpgradesDowngradesConsensus](t, "bulk_upgrades_downgrades_consensus.json") - if want := (BulkUpgradesDowngradesConsensus{Symbol: "", StrongBuy: "0", Buy: "1", Hold: "1", Sell: "0", - StrongSell: "0", Consensus: "Buy"}); len(consensus) != 1 || consensus[0] != want { - t.Fatalf("bulk_upgrades_downgrades_consensus = %+v", consensus) - } -} - -// The ETF holding row's lastUpdated member is a strict date. -func TestBulkEtfHoldingDecodesLastUpdatedAsADate(t *testing.T) { - t.Parallel() - holdings := assertFixtureParity[BulkETFHolding](t, "bulk_etf_holdings.json") - if want := (BulkETFHolding{Symbol: "EXCH.AS", Name: "SAMSUNG ELECTRO MECHANICS LTD", SharesNumber: "15514", - Asset: "009150.KS", WeightPercentage: "0.09611", CUSIP: "", ISIN: "KR7009150004", MarketValue: "1553142.49", - LastUpdated: mustParseDate(t, "2024-09-06")}); len(holdings) != 1 || holdings[0] != want { - t.Fatalf("bulk_etf_holdings = %+v", holdings) + holdings := assertCSVFixtureParity[BulkETFHolding](t, "bulk_etf_holdings.csv", 9) + if want := (BulkETFHolding{Symbol: " -- ", Name: "Tidewater Inc", SharesNumber: "91962", Asset: "TDW", + WeightPercentage: "2.63", CUSIP: "88642R109", ISIN: "US88642R1095", MarketValue: "6457163.796", + LastUpdated: mustParseDate(t, "2026-09-27")}); holdings[0] != want { + t.Fatalf("bulk_etf_holdings = %+v", holdings[0]) } } diff --git a/sdk/go/csv_test.go b/sdk/go/csv_test.go index 6d0a14af..f70c8662 100644 --- a/sdk/go/csv_test.go +++ b/sdk/go/csv_test.go @@ -1,9 +1,14 @@ package fmp import ( + "bytes" "context" + "encoding/csv" + "encoding/json/v2" "errors" "net/http" + "slices" + "strconv" "testing" ) @@ -145,3 +150,54 @@ func TestBulkBodyLimitDefaultsUntilTheClientSetsOne(t *testing.T) { t.Fatalf("err = %v", err) } } + +// assertCSVFixtureParity mirrors assert_round_trip in +// crates/libfmp/tests/support/bulk_csv.rs: every row of the shared CSV +// fixture decodes, and the first row re-encodes fields members, each a header +// column equal to its cell (verbatim text, the parsed number or boolean, and +// null or "" for an empty cell). +func assertCSVFixtureParity[T any](t *testing.T, name string, fields int) []T { + t.Helper() + raw := readFixture(t, name) + rows, path, kind, err := decodeCSVRows[T](raw) + if kind != DecodeKindNone { + t.Fatalf("%s: decode at %q (%v): %v", name, path, kind, err) + } + records, err := csv.NewReader(bytes.NewReader(raw)).ReadAll() + if err != nil || len(records) != len(rows)+1 { + t.Fatalf("%s: %d records for %d rows: %v", name, len(records), len(rows), err) + } + members := memberValues(t, rows[0]) + if len(members) != fields { + t.Fatalf("%s: %d members re-encoded, want %d", name, len(members), fields) + } + for member, value := range members { + column := slices.Index(records[0], member) + if column < 0 { + t.Fatalf("%s: member %q is not a header column", name, member) + } + cell := records[1][column] + var text string + switch value.Kind() { + case 'n': + text = "" + case '"': + if err := json.Unmarshal(value, &text); err != nil { + t.Fatal(err) + } + case '0': + want, _ := strconv.ParseFloat(cell, 64) + got, _ := strconv.ParseFloat(string(value), 64) + if got == want { + continue + } + text = string(value) + default: + text = string(value) + } + if text != cell { + t.Fatalf("%s: member %q re-encoded %s for cell %q", name, member, value, cell) + } + } + return rows +} From bc5da487a9b92c95ed0303887af55f5c7368bc7b Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:43:23 +0800 Subject: [PATCH 11/31] test(fmp-py): serve live CSV fixtures for bulk snapshot tests Bulk snapshot routes answer with CSV bodies, so the seven snapshot tests now serve the shared live CSV fixtures as ``text/csv`` instead of parsed JSON, and assert the live values from those fixtures. * Add ``load_csv_fixture`` and ``CSV_CONTENT_TYPE`` to conftest so tests can read the raw CSV bodies from the shared libfmp fixtures. * Add a ``route_csv`` helper in the bulk test module that serves a shared CSV fixture on a path as ``text/csv``, matching how the bulk routes answer. * Repoint ``company_profiles``, ``stock_ratings``, ``dcf_valuations``, ``financial_scores``, ``price_target_summaries``, ``etf_holdings``, and ``upgrades_downgrades_consensus`` at the CSV fixtures and update the expected row counts and typed fields to the live values. * Assert ``etf_holdings`` decodes ``lastUpdated`` as a ``datetime.date`` rather than keeping the raw column string. Refs #376 --- crates/fmp-py/tests/conftest.py | 7 ++ crates/fmp-py/tests/test_bulk_a.py | 109 +++++++++++++++-------------- 2 files changed, 62 insertions(+), 54 deletions(-) diff --git a/crates/fmp-py/tests/conftest.py b/crates/fmp-py/tests/conftest.py index 8e5145d2..24bb1616 100644 --- a/crates/fmp-py/tests/conftest.py +++ b/crates/fmp-py/tests/conftest.py @@ -55,12 +55,19 @@ def test_profile_decodes_documented_fixture(client, fixture_server): JSON_CONTENT_TYPE = "application/json" +CSV_CONTENT_TYPE = "text/csv" + def load_fixture(name: str) -> Any: """Return the parsed JSON body of ``crates/libfmp/tests/fixtures/``.""" return json.loads((FIXTURES_DIR / name).read_text(encoding="utf-8")) +def load_csv_fixture(name: str) -> bytes: + """Return the raw CSV body of ``crates/libfmp/tests/fixtures/``, as bulk routes answer.""" + return (FIXTURES_DIR / name).read_bytes() + + def _errors_namespace() -> SimpleNamespace: """Expose the exception hierarchy under its public names. diff --git a/crates/fmp-py/tests/test_bulk_a.py b/crates/fmp-py/tests/test_bulk_a.py index 3568cc0e..b5862a47 100644 --- a/crates/fmp-py/tests/test_bulk_a.py +++ b/crates/fmp-py/tests/test_bulk_a.py @@ -2,10 +2,9 @@ Covers the eleven bulk methods whose bodies live in ``fmp.bulk.snapshots``, ``fmp.bulk.metrics``, and (for ``company_profiles``) ``fmp.company``: one test -per method routes the documented fixture body, calls the method, and asserts -the exact request target plus a few typed fields. Bulk bodies are CSV-backed, -so most numeric columns arrive as ``str``; only the date columns decode to -``datetime.date``. The expected targets are the ones the Rust +per method routes the live CSV fixture body as ``text/csv``, calls the method, and asserts +the exact request target plus a few typed fields. Numeric columns arrive as +``str`` verbatim; only the date columns decode to ``datetime.date``. The expected targets are the ones the Rust ``bulk_snapshot_endpoints.rs`` and ``bulk_metrics_endpoints.rs`` tests pin. """ @@ -14,7 +13,7 @@ from typing import Any import pytest -from conftest import FixtureServer, load_fixture +from conftest import CSV_CONTENT_TYPE, FixtureServer, load_csv_fixture, load_fixture from fmp.bulk import BulkNamespace from fmp.bulk.metrics import BulkEarningsSurprise, BulkFinancialRatiosTtm, BulkKeyMetricsTtm, BulkStockPeer from fmp.bulk.snapshots import ( @@ -30,6 +29,11 @@ DATE_2025_07_09 = datetime.date(2025, 7, 9) +def route_csv(fixture_server: FixtureServer, path: str, fixture: str) -> None: + """Serve the shared CSV fixture on ``path`` as ``text/csv``, as the bulk routes answer.""" + fixture_server.route(path, load_csv_fixture(fixture), content_type=CSV_CONTENT_TYPE) + + def test_bulk_namespace_is_the_generated_type(client: Any) -> None: """``client.bulk`` is the generated flat namespace class.""" assert isinstance(client.bulk, BulkNamespace) @@ -37,24 +41,23 @@ def test_bulk_namespace_is_the_generated_type(client: Any) -> None: def test_company_profiles_form_encodes_the_part(client: Any, fixture_server: FixtureServer) -> None: """``company_profiles`` sends ``part`` verbatim, form-encoding spaces and slashes.""" - fixture_server.route("/profile-bulk", load_fixture("bulk_company_profiles.json")) + route_csv(fixture_server, "/profile-bulk", "bulk_company_profiles.csv") rows = client.bulk.company_profiles("segment 0/alpha") assert fixture_server.requests[0].target == "/profile-bulk?part=segment+0%2Falpha" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, CompanyProfile) - assert row.symbol == "AAPL" - assert row.company_name == "Apple Inc." - assert row.price == pytest.approx(271.36) - assert row.market_cap == 4_009_711_150_080 - assert row.ipo_date == datetime.date(1980, 12, 12) + assert row.symbol == "WMB" + assert row.company_name == "The Williams Companies, Inc." + assert row.price == pytest.approx(67.78) + assert row.market_cap == 82_906_291_252 assert row.is_etf is False def test_company_profiles_with_a_numeric_part(client: Any, fixture_server: FixtureServer) -> None: """A plain numeric partition is sent as-is without inferring a range.""" - fixture_server.route("/profile-bulk", load_fixture("bulk_company_profiles.json")) + route_csv(fixture_server, "/profile-bulk", "bulk_company_profiles.csv") client.bulk.company_profiles("0") assert fixture_server.requests[0].target == "/profile-bulk?part=0" @@ -62,98 +65,96 @@ def test_company_profiles_with_a_numeric_part(client: Any, fixture_server: Fixtu def test_stock_ratings_takes_no_arguments(client: Any, fixture_server: FixtureServer) -> None: """``stock_ratings`` maps to ``/rating-bulk`` with no query string.""" - fixture_server.route("/rating-bulk", load_fixture("bulk_stock_ratings.json")) + route_csv(fixture_server, "/rating-bulk", "bulk_stock_ratings.csv") rows = client.bulk.stock_ratings() assert fixture_server.requests[0].target == "/rating-bulk" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkStockRating) assert row.symbol == "000001.SZ" - assert row.date == DATE_2025_07_09 - assert row.rating == "B+" - assert row.discounted_cash_flow_score == "5" + assert row.date == datetime.date(2026, 9, 30) + assert row.rating == "B-" + assert row.discounted_cash_flow_score == "1" with pytest.raises(TypeError): client.bulk.stock_ratings("0") def test_dcf_valuations_decodes_the_spaced_stock_price_column(client: Any, fixture_server: FixtureServer) -> None: """``dcf_valuations`` maps the provider's ``Stock Price`` column to ``stock_price``.""" - fixture_server.route("/dcf-bulk", load_fixture("bulk_dcf_valuations.json")) + route_csv(fixture_server, "/dcf-bulk", "bulk_dcf_valuations.csv") rows = client.bulk.dcf_valuations() assert fixture_server.requests[0].target == "/dcf-bulk" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkDcfValuation) - assert row.symbol == "000002.SZ" - assert row.date == DATE_2025_07_09 - assert row.dcf == "179.6654688379575" - assert row.stock_price == "6.54" + assert row.symbol == "000006.SZ" + assert row.date == datetime.date(2026, 9, 29) + assert row.dcf == "2.525226853334803" + assert row.stock_price == "7.62" def test_financial_scores_keeps_string_backed_numbers(client: Any, fixture_server: FixtureServer) -> None: """``financial_scores`` maps to ``/scores-bulk`` and keeps the CSV strings.""" - fixture_server.route("/scores-bulk", load_fixture("bulk_financial_scores.json")) + route_csv(fixture_server, "/scores-bulk", "bulk_financial_scores.csv") rows = client.bulk.financial_scores() assert fixture_server.requests[0].target == "/scores-bulk" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkFinancialScore) assert row.symbol == "000001.SZ" assert row.reported_currency == "CNY" - assert row.altman_z_score == "0.29153682196643543" - assert row.piotroski_score == "5" - assert row.market_cap == "236751980000" + assert row.altman_z_score == "-0.06634014283050256" + assert row.piotroski_score == "4" + assert row.market_cap == "219286875637" def test_price_target_summaries_preserves_the_raw_publishers_cell(client: Any, fixture_server: FixtureServer) -> None: - """``price_target_summaries`` keeps the provider's malformed ``publishers`` cell verbatim.""" - fixture_server.route("/price-target-summary-bulk", load_fixture("bulk_price_target_summaries.json")) + """``price_target_summaries`` keeps the quoted ``publishers`` cell verbatim as one string.""" + route_csv(fixture_server, "/price-target-summary-bulk", "bulk_price_target_summaries.csv") rows = client.bulk.price_target_summaries() assert fixture_server.requests[0].target == "/price-target-summary-bulk" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkPriceTargetSummary) assert row.symbol == "A" - assert row.all_time_count == "18" - assert row.all_time_avg_price_target == "146.61" - assert row.publishers == '[""TheFly"' + assert row.all_time_count == "53" + assert row.all_time_avg_price_target == "159.49" + assert row.publishers == '["StreetInsider","Benzinga","Pulse 2.0"]' def test_etf_holdings_form_encodes_the_part(client: Any, fixture_server: FixtureServer) -> None: - """``etf_holdings`` shares the ``part`` shape and keeps the raw ``lastUpdated"`` column.""" - fixture_server.route("/etf-holder-bulk", load_fixture("bulk_etf_holdings.json")) + """``etf_holdings`` shares the ``part`` shape and decodes ``lastUpdated`` as a date.""" + route_csv(fixture_server, "/etf-holder-bulk", "bulk_etf_holdings.csv") rows = client.bulk.etf_holdings("part 1/beta") assert fixture_server.requests[0].target == "/etf-holder-bulk?part=part+1%2Fbeta" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkEtfHolding) - assert row.symbol == "EXCH.AS" - assert row.asset == "009150.KS" - assert row.isin == "KR7009150004" - assert row.weight_percentage == "0.09611" - assert row.last_updated_raw == '2024-09-06"' - - -def test_upgrades_downgrades_consensus_accepts_an_empty_symbol(client: Any, fixture_server: FixtureServer) -> None: - """``upgrades_downgrades_consensus`` decodes the documented empty-symbol row.""" - fixture_server.route( - "/upgrades-downgrades-consensus-bulk", load_fixture("bulk_upgrades_downgrades_consensus.json") - ) + assert row.symbol == " -- " + assert row.asset == "TDW" + assert row.isin == "US88642R1095" + assert row.weight_percentage == "2.63" + assert row.last_updated == datetime.date(2026, 9, 27) + + +def test_upgrades_downgrades_consensus_takes_no_arguments(client: Any, fixture_server: FixtureServer) -> None: + """``upgrades_downgrades_consensus`` maps to its bulk route and keeps the counts as text.""" + route_csv(fixture_server, "/upgrades-downgrades-consensus-bulk", "bulk_upgrades_downgrades_consensus.csv") rows = client.bulk.upgrades_downgrades_consensus() assert fixture_server.requests[0].target == "/upgrades-downgrades-consensus-bulk" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkUpgradesDowngradesConsensus) - assert row.symbol == "" + assert row.symbol == "000550.SZ" assert row.consensus == "Buy" - assert row.buy == "1" - assert row.strong_buy == "0" + assert row.buy == "14" + assert row.strong_buy == "1" def test_key_metrics_ttm_takes_no_arguments(client: Any, fixture_server: FixtureServer) -> None: From 88af3537dd7996666a411285ce7b595ee70bb3d9 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:44:20 +0800 Subject: [PATCH 12/31] fix(bulk)!: decode metric bulk routes as CSV The four metric bulk routes (key-metrics-ttm-bulk, ratios-ttm-bulk, peers-bulk, earnings-surprises-bulk) actually receive text/csv from FMP, so decoding them as JSON was wrong and failed against real responses. Switch these specs to EndpointSpec::get_csv and regenerate the Go methods to use getCSV instead of getJSON. Add live CSV fixtures for all four routes and update the Rust and Go endpoint suites to serve them as text/csv, adjusting assertions to the new fixture data. Refs: #376 BREAKING CHANGE: the four bulk methods KeyMetricsTTM, FinancialRatiosTTM, StockPeers, and EarningsSurprises now require a text/csv response and no longer decode JSON. --- crates/libfmp/src/endpoints/bulk.rs | 9 ++--- crates/libfmp/tests/bulk_metrics_endpoints.rs | 36 ++++++++++--------- .../fixtures/bulk_earnings_surprises.csv | 3 ++ .../fixtures/bulk_financial_ratios_ttm.csv | 3 ++ .../tests/fixtures/bulk_key_metrics_ttm.csv | 3 ++ .../tests/fixtures/bulk_stock_peers.csv | 3 ++ sdk/go/bulk.go | 24 +++---------- sdk/go/bulk_test.go | 16 ++++----- 8 files changed, 49 insertions(+), 48 deletions(-) create mode 100644 crates/libfmp/tests/fixtures/bulk_earnings_surprises.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_financial_ratios_ttm.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_key_metrics_ttm.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_stock_peers.csv diff --git a/crates/libfmp/src/endpoints/bulk.rs b/crates/libfmp/src/endpoints/bulk.rs index 15b9ffbc..e4d37d8f 100644 --- a/crates/libfmp/src/endpoints/bulk.rs +++ b/crates/libfmp/src/endpoints/bulk.rs @@ -211,24 +211,25 @@ pub fn bulk_upgrades_downgrades_consensus() -> EndpointSpec<(), Vec EndpointSpec<(), Vec> { - EndpointSpec::get("key-metrics-ttm-bulk", "key-metrics-ttm-bulk", ()).with_metadata(WORLDWIDE) + EndpointSpec::get_csv("key-metrics-ttm-bulk", "key-metrics-ttm-bulk", ()) + .with_metadata(WORLDWIDE) } /// Describes `GET ratios-ttm-bulk` without binding a transport. pub fn bulk_financial_ratios_ttm() -> EndpointSpec<(), Vec> { - EndpointSpec::get("ratios-ttm-bulk", "ratios-ttm-bulk", ()).with_metadata(WORLDWIDE) + EndpointSpec::get_csv("ratios-ttm-bulk", "ratios-ttm-bulk", ()).with_metadata(WORLDWIDE) } /// Describes `GET peers-bulk` without binding a transport. pub fn bulk_stock_peers() -> EndpointSpec<(), Vec> { - EndpointSpec::get("peers-bulk", "peers-bulk", ()).with_metadata(WORLDWIDE) + EndpointSpec::get_csv("peers-bulk", "peers-bulk", ()).with_metadata(WORLDWIDE) } /// Describes `GET earnings-surprises-bulk` without binding a transport. pub fn bulk_earnings_surprises( query: BulkYearQuery, ) -> EndpointSpec> { - EndpointSpec::get("earnings-surprises-bulk", "earnings-surprises-bulk", query) + EndpointSpec::get_csv("earnings-surprises-bulk", "earnings-surprises-bulk", query) .with_metadata(WORLDWIDE) } diff --git a/crates/libfmp/tests/bulk_metrics_endpoints.rs b/crates/libfmp/tests/bulk_metrics_endpoints.rs index ec462dd6..dfe75cfc 100644 --- a/crates/libfmp/tests/bulk_metrics_endpoints.rs +++ b/crates/libfmp/tests/bulk_metrics_endpoints.rs @@ -21,19 +21,23 @@ use libfmp::{ transport::{HttpMethod, TransportResponse}, }; -use support::{FixtureExecutor, json_fixture}; +use support::{FixtureExecutor, fixture_response, json_fixture}; -const KEY_METRICS: &[u8] = include_bytes!("fixtures/bulk_key_metrics_ttm.json"); -const RATIOS: &[u8] = include_bytes!("fixtures/bulk_financial_ratios_ttm.json"); -const PEERS: &[u8] = include_bytes!("fixtures/bulk_stock_peers.json"); -const SURPRISES: &[u8] = include_bytes!("fixtures/bulk_earnings_surprises.json"); +const KEY_METRICS: &[u8] = include_bytes!("fixtures/bulk_key_metrics_ttm.csv"); +const RATIOS: &[u8] = include_bytes!("fixtures/bulk_financial_ratios_ttm.csv"); +const PEERS: &[u8] = include_bytes!("fixtures/bulk_stock_peers.csv"); +const SURPRISES: &[u8] = include_bytes!("fixtures/bulk_earnings_surprises.csv"); + +fn csv_fixture(body: &'static [u8]) -> TransportResponse { + fixture_response(Some("text/csv"), body) +} fn fixtures() -> [TransportResponse; 4] { [ - json_fixture(KEY_METRICS), - json_fixture(RATIOS), - json_fixture(PEERS), - json_fixture(SURPRISES), + csv_fixture(KEY_METRICS), + csv_fixture(RATIOS), + csv_fixture(PEERS), + csv_fixture(SURPRISES), ] } @@ -106,13 +110,13 @@ async fn custom_proxy_sends_one_exact_request_per_method_with_auth_headers_and_d let peers = client.bulk_stock_peers().await.unwrap(); let surprises = client.bulk_earnings_surprises(Year(2026)).await.unwrap(); - assert_eq!(metrics[0].ev_to_ebitda_ttm.as_str(), "-14.656106051669223"); + assert_eq!(metrics[0].ev_to_ebitda_ttm.as_str(), "29.23198788110799"); assert_eq!( ratios[0].net_income_per_ebt_ttm.as_str(), - "0.8225101702576465" + "0.83539656299258" ); - assert_eq!(peers[0].peers, "600036.SS"); - assert_eq!(surprises[0].eps_actual.as_str(), "0.3631"); + assert_eq!(peers[1].symbol.as_str(), "000002.SZ"); + assert_eq!(surprises[1].eps_actual.as_str(), "-0.11675"); let requests = executor.requests(); assert_eq!(requests.len(), 4); @@ -185,11 +189,11 @@ async fn direct_fmp_header_and_query_auth_preserve_paths_and_exact_query_shape() } #[tokio::test] -async fn empty_arrays_decode_and_malformed_roots_keep_all_four_endpoint_identities() { +async fn empty_bodies_decode_and_json_bodies_keep_all_four_endpoint_identities() { let executor = Arc::new(FixtureExecutor::new( - std::iter::repeat_with(|| json_fixture(b"[]")) + std::iter::repeat_with(|| csv_fixture(b"")) .take(4) - .chain(std::iter::repeat_with(|| json_fixture(b"{}")).take(4)), + .chain(std::iter::repeat_with(|| json_fixture(b"[]")).take(4)), )); let client = Client::builder() .authentication(Authentication::fmp_header("secret")) diff --git a/crates/libfmp/tests/fixtures/bulk_earnings_surprises.csv b/crates/libfmp/tests/fixtures/bulk_earnings_surprises.csv new file mode 100644 index 00000000..7271e075 --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_earnings_surprises.csv @@ -0,0 +1,3 @@ +"symbol","date","epsActual","epsEstimated","lastUpdated" +"AUTO.OL","2024-12-31",0.1332,0.1581,"2025-10-07" +"601699.SS","2024-12-31",-0.11675,0.67,"2025-06-17" diff --git a/crates/libfmp/tests/fixtures/bulk_financial_ratios_ttm.csv b/crates/libfmp/tests/fixtures/bulk_financial_ratios_ttm.csv new file mode 100644 index 00000000..103a047c --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_financial_ratios_ttm.csv @@ -0,0 +1,3 @@ +"symbol","grossProfitMarginTTM","ebitMarginTTM","ebitdaMarginTTM","operatingProfitMarginTTM","pretaxProfitMarginTTM","continuousOperationsProfitMarginTTM","netProfitMarginTTM","bottomLineProfitMarginTTM","receivablesTurnoverTTM","payablesTurnoverTTM","inventoryTurnoverTTM","fixedAssetTurnoverTTM","assetTurnoverTTM","currentRatioTTM","quickRatioTTM","solvencyRatioTTM","cashRatioTTM","priceToEarningsRatioTTM","priceToEarningsGrowthRatioTTM","forwardPriceToEarningsGrowthRatioTTM","priceToEarningsDilutedRatioTTM","priceToEarningsDilutedGrowthRatioTTM","priceToBookRatioTTM","priceToSalesRatioTTM","priceToFreeCashFlowRatioTTM","priceToOperatingCashFlowRatioTTM","debtToAssetsRatioTTM","debtToEquityRatioTTM","debtToCapitalRatioTTM","longTermDebtToCapitalRatioTTM","financialLeverageRatioTTM","workingCapitalTurnoverRatioTTM","operatingCashFlowRatioTTM","operatingCashFlowSalesRatioTTM","freeCashFlowOperatingCashFlowRatioTTM","debtServiceCoverageRatioTTM","interestCoverageRatioTTM","shortTermOperatingCashFlowCoverageRatioTTM","operatingCashFlowCoverageRatioTTM","capitalExpenditureCoverageRatioTTM","dividendPaidAndCapexCoverageRatioTTM","dividendPayoutRatioTTM","dividendYieldTTM","enterpriseValueTTM","revenuePerShareTTM","netIncomePerShareTTM","interestDebtPerShareTTM","cashPerShareTTM","bookValuePerShareTTM","tangibleBookValuePerShareTTM","shareholdersEquityPerShareTTM","operatingCashFlowPerShareTTM","capexPerShareTTM","freeCashFlowPerShareTTM","netIncomePerEBTTTM","ebtPerEbitTTM","priceToFairValueTTM","debtToMarketCapTTM","effectiveTaxRateTTM","enterpriseValueMultipleTTM","dividendPerShareTTM" +"000001.SZ",0.5535250166330814,0.2490032117403229,0.2490032117403229,0.2490032117403229,0.2490032117403229,0.20801642726197941,0.20801642726197941,0.19822803834942396,11.876584617133762,0,0,14.59761039686976,0.03463701192046656,0.2105714236740011,0.2105714236740011,0.007811431374385213,0.19800778772590405,5.406542056074767,5.730934579439322,5.49720208631886,5.406542056074767,,0.4795381412345637,1.0746955717759343,16.049068874267334,13.435848917479506,0.26085746254762526,3.360474056734741,0.7706671368780413,0.26820180271423055,12.882414878666593,-0.4366174223979569,0.011935132757014952,0.07998717218470139,0.8371731195021244,0.02949246146924156,0.7086403944912888,0.011935132757014952,0.010620806510145728,6.141492098493201,1.0232059759980408,0.31319174394256655,0.04191876,1520706473551,10.765839575329545,2.239471484935678,84.86236002852326,14.286415102856768,24.12738217279904,23.458359455089937,24.12738217279904,0.861129063824757,0.14021495916864124,0.7209141046561157,0.83539656299258,1,0.4795381412345637,7.000292549657781,0.16460343700741994,29.23198788110799,0.485 +"000002.SZ",0.06666835318272966,-0.35160447542859574,-0.3436062587377696,-0.37712505941262214,-0.401972317997405,-0.48904104484834554,-0.4617804342288643,-0.4617804342288643,1.0423766535264125,1.5911361263677661,0.5349196265632267,6.6192161687368785,0.20659150998665363,1.1799592688017517,0.5548431689425369,-0.12094180514091163,0.10669697244200031,-0.5539661898569571,0.009588860380900295,0.04232837042623997,-0.5539661898569571,,0.49738934892132797,0.25578299899413237,35.65731601593227,12.980122127426482,0.18749632297965432,1.7611042833711095,0.6378260661784668,0.5772176041642411,9.392740376899077,1.787639576217643,0.007074995034555664,0.019747535999527614,0.36325375372663976,-2.3997780236250783,-11.174002151242703,0.13552230486057032,0.021758684201507818,1.5704836982740558,0.5419358420167008,-0.051679771183778936,0,171618750294,16.619498177734524,-7.674559085180067,15.644275053834804,4.950954525799006,18.08864773996082,17.017218420148932,8.564718985717171,0.32819413855889606,0.20897646942758963,0.11921775278259993,1.148786654089562,1.065885991833804,0.49738934892132797,3.3203982125782336,-0.21660378825265927,-2.518998346875115,0 diff --git a/crates/libfmp/tests/fixtures/bulk_key_metrics_ttm.csv b/crates/libfmp/tests/fixtures/bulk_key_metrics_ttm.csv new file mode 100644 index 00000000..c9cfe9de --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_key_metrics_ttm.csv @@ -0,0 +1,3 @@ +"symbol","marketCap","enterpriseValueTTM","evToSalesTTM","evToOperatingCashFlowTTM","evToFreeCashFlowTTM","evToEBITDATTM","netDebtToEBITDATTM","currentRatioTTM","incomeQualityTTM","grahamNumberTTM","grahamNetNetTTM","taxBurdenTTM","interestBurdenTTM","workingCapitalTTM","investedCapitalTTM","returnOnAssetsTTM","operatingReturnOnAssetsTTM","returnOnTangibleAssetsTTM","returnOnEquityTTM","returnOnInvestedCapitalTTM","returnOnCapitalEmployedTTM","earningsYieldTTM","freeCashFlowYieldTTM","capexToOperatingCashFlowTTM","capexToDepreciationTTM","capexToRevenueTTM","salesGeneralAndAdministrativeToRevenueTTM","researchAndDevelopementToRevenueTTM","stockBasedCompensationToRevenueTTM","intangiblesToTotalAssetsTTM","averageReceivablesTTM","averagePayablesTTM","averageInventoryTTM","daysOfSalesOutstandingTTM","daysOfPayablesOutstandingTTM","daysOfInventoryOutstandingTTM","operatingCycleTTM","cashConversionCycleTTM","freeCashFlowToEquityTTM","freeCashFlowToFirmTTM","tangibleAssetValueTTM","netCurrentAssetValueTTM" +"000001.SZ",224526473551,1520706473551,7.278858867950086,91.00032754179881,108.69953349185133,29.23198788110799,24.91599707815924,0.2105714236740011,0.39517120696178587,34.86736509399441,-271.7252927689378,0.83539656299258,1,-1105320000000,-1078025000000,0.007205067470726046,0.008623129603290786,0.00722060948264289,0.09289033782530935,0.007205067470726046,0.011232032300027528,0.19355846887948813,0.062308910743312615,0.16282688049787566,0,0.013024061726681377,0.02260663121466966,0,0,0.002152451528393112,8795500000,0,0,30.73274108395039,0,0,30.73274108395039,30.73274108395039,-106379000000,75317297085.8483,455231000000,-5268681000000 +"000004.SZ",67513944,-23704249,-0.1354217284200267,-0.841897327678881,-0.8962470352112417,-0.44397584773534005,-1.7084985297809208,1.11454892714022,2.388499808322787,1.1473442645398348,-0.20518332970569364,0.3283065153844871,0.7281077817851126,25605421,62495969,0.03424160485786354,-0.04084017529926973,0.03659920094471008,0.196679553604802,-0.07989108744570404,-0.12787045866623842,0.17475741923272,0.39174637760756503,0.06064143634187946,0.10716974410018917,0.009754358225523015,-0.027096298936874624,0.3538632227145465,0,0.06441659998009591,161005774.5,131472875,5696738.5,281.4264857895316,1445.3568449233546,92.13469677474816,373.56118256427976,-1071.7956623590749,29748351,26643066.309467595,92915054,19968242 diff --git a/crates/libfmp/tests/fixtures/bulk_stock_peers.csv b/crates/libfmp/tests/fixtures/bulk_stock_peers.csv new file mode 100644 index 00000000..52d15a19 --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_stock_peers.csv @@ -0,0 +1,3 @@ +"symbol","peers" +"000001.SZ","3698.HK,600000.SS,600015.SS,600016.SS,600036.SS,601166.SS,601658.SS" +"000002.SZ","600895.SS,600048.SS,001979.SZ,600376.SS,601588.SS,002244.SZ,600383.SS,600663.SS,600606.SS,600340.SS" diff --git a/sdk/go/bulk.go b/sdk/go/bulk.go index 211bd93b..b848314f 100644 --- a/sdk/go/bulk.go +++ b/sdk/go/bulk.go @@ -198,11 +198,7 @@ func (n *BulkNamespace) UpgradesDowngradesConsensus(ctx context.Context) ([]Bulk // // GET key-metrics-ttm-bulk func (n *BulkNamespace) KeyMetricsTTM(ctx context.Context) ([]BulkKeyMetricsTTM, error) { - var out []BulkKeyMetricsTTM - if err := n.client.getJSON(ctx, "key-metrics-ttm-bulk", "key-metrics-ttm-bulk", nil, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkKeyMetricsTTM](ctx, n.client, "key-metrics-ttm-bulk", "key-metrics-ttm-bulk", nil) } // FinancialRatiosTTM retrieves worldwide trailing-twelve-month financial @@ -210,22 +206,14 @@ func (n *BulkNamespace) KeyMetricsTTM(ctx context.Context) ([]BulkKeyMetricsTTM, // // GET ratios-ttm-bulk func (n *BulkNamespace) FinancialRatiosTTM(ctx context.Context) ([]BulkFinancialRatiosTTM, error) { - var out []BulkFinancialRatiosTTM - if err := n.client.getJSON(ctx, "ratios-ttm-bulk", "ratios-ttm-bulk", nil, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkFinancialRatiosTTM](ctx, n.client, "ratios-ttm-bulk", "ratios-ttm-bulk", nil) } // StockPeers retrieves worldwide stock peers in one provider bulk response. // // GET peers-bulk func (n *BulkNamespace) StockPeers(ctx context.Context) ([]BulkStockPeer, error) { - var out []BulkStockPeer - if err := n.client.getJSON(ctx, "peers-bulk", "peers-bulk", nil, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkStockPeer](ctx, n.client, "peers-bulk", "peers-bulk", nil) } // EarningsSurprises retrieves worldwide annual earnings surprises for one @@ -237,11 +225,7 @@ func (n *BulkNamespace) EarningsSurprises(ctx context.Context, q BulkYearQuery) if err != nil { return nil, err } - var out []BulkEarningsSurprise - if err := n.client.getJSON(ctx, "earnings-surprises-bulk", "earnings-surprises-bulk", params, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkEarningsSurprise](ctx, n.client, "earnings-surprises-bulk", "earnings-surprises-bulk", params) } // IncomeStatements retrieves worldwide bulk income statements for one year and diff --git a/sdk/go/bulk_test.go b/sdk/go/bulk_test.go index fdf6fd07..5f31de4f 100644 --- a/sdk/go/bulk_test.go +++ b/sdk/go/bulk_test.go @@ -21,10 +21,10 @@ var bulkRoutes = map[string]string{ "/router/stable/price-target-summary-bulk": "bulk_price_target_summaries.csv", "/router/stable/etf-holder-bulk?part=segment+A%2F7": "bulk_etf_holdings.csv", "/router/stable/upgrades-downgrades-consensus-bulk": "bulk_upgrades_downgrades_consensus.csv", - "/router/stable/key-metrics-ttm-bulk": "bulk_key_metrics_ttm.json", - "/router/stable/ratios-ttm-bulk": "bulk_financial_ratios_ttm.json", - "/router/stable/peers-bulk": "bulk_stock_peers.json", - "/router/stable/earnings-surprises-bulk?year=4294967295": "bulk_earnings_surprises.json", + "/router/stable/key-metrics-ttm-bulk": "bulk_key_metrics_ttm.csv", + "/router/stable/ratios-ttm-bulk": "bulk_financial_ratios_ttm.csv", + "/router/stable/peers-bulk": "bulk_stock_peers.csv", + "/router/stable/earnings-surprises-bulk?year=4294967295": "bulk_earnings_surprises.csv", "/router/stable/income-statement-bulk?year=2026&period=Q1": "bulk_income_statements.json", "/router/stable/income-statement-growth-bulk?year=2026&period=Q2": "bulk_income_statement_growth.json", "/router/stable/balance-sheet-statement-bulk?year=2026&period=Q3": "bulk_balance_sheet_statements.json", @@ -86,19 +86,19 @@ func TestBulkMethodsUseExactPathsAndWireParameterOrder(t *testing.T) { t.Fatalf("UpgradesDowngradesConsensus = %+v, %v", consensus, err) } metrics, err := client.Bulk.KeyMetricsTTM(ctx) - if err != nil || len(metrics) != 1 || metrics[0].MarketCap != "249171756000" { + if err != nil || len(metrics) != 2 || metrics[0].MarketCap != "224526473551" { t.Fatalf("KeyMetricsTTM = %+v, %v", metrics, err) } ratios, err := client.Bulk.FinancialRatiosTTM(ctx) - if err != nil || len(ratios) != 1 || ratios[0].GrossProfitMarginTTM != "1.1622776732779352" { + if err != nil || len(ratios) != 2 || ratios[0].GrossProfitMarginTTM != "0.5535250166330814" { t.Fatalf("FinancialRatiosTTM = %+v, %v", ratios, err) } peers, err := client.Bulk.StockPeers(ctx) - if err != nil || len(peers) != 1 || peers[0].Peers != "600036.SS" { + if err != nil || len(peers) != 2 || peers[0].Peers != "3698.HK,600000.SS,600015.SS,600016.SS,600036.SS,601166.SS,601658.SS" { t.Fatalf("StockPeers = %+v, %v", peers, err) } surprises, err := client.Bulk.EarningsSurprises(ctx, NewBulkYearQuery(4_294_967_295)) - if err != nil || len(surprises) != 1 || surprises[0].Symbol != "AMKYF" { + if err != nil || len(surprises) != 2 || surprises[0].Symbol != "AUTO.OL" { t.Fatalf("EarningsSurprises = %+v, %v", surprises, err) } income, err := client.Bulk.IncomeStatements(ctx, NewBulkStatementQuery(2026, FiscalPeriodQ1)) From 2493f59c8b3ad035012c581f756e41b79082ccea Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:47:07 +0800 Subject: [PATCH 13/31] test(libfmp): rewrite bulk metric suite over live CSV fixtures Rewrites the metric bulk response suite (key metrics TTM, ratios TTM, peers, earnings surprises) to run over the live CSV fixtures through the shared helpers instead of hand-rolled JSON assertions. Covers per-cell round trip with exact field counts, the member contract, empty and header-only bodies, numeric text kept verbatim beyond u64 and in exponent form, exact TTM wire spellings, the quoted peers list, and typed earnings dates. Adds a `with_cells` helper to the shared support for replacing named cells in a fixture's first record, and makes the member contract ignore blanks in columns the model does not read. Refs #376 --- crates/libfmp/tests/bulk_metrics_responses.rs | 225 ++++++------------ crates/libfmp/tests/support/bulk_csv.rs | 18 +- 2 files changed, 83 insertions(+), 160 deletions(-) diff --git a/crates/libfmp/tests/bulk_metrics_responses.rs b/crates/libfmp/tests/bulk_metrics_responses.rs index 58f4927c..2dfb5e2d 100644 --- a/crates/libfmp/tests/bulk_metrics_responses.rs +++ b/crates/libfmp/tests/bulk_metrics_responses.rs @@ -1,73 +1,72 @@ -use serde::{Serialize, de::DeserializeOwned}; -use serde_json::{Map, Value, json}; - -use libfmp::responses::bulk::{ - BulkEarningsSurprise, BulkFinancialRatiosTtm, BulkKeyMetricsTtm, BulkStockPeer, +#[path = "support/bulk_csv.rs"] +mod bulk_csv; +#[allow(dead_code)] // The CSV helpers use only the fixture executor. +mod support; + +use serde_json::{Map, Value}; + +use libfmp::{ + endpoints::bulk::{ + bulk_earnings_surprises, bulk_financial_ratios_ttm, bulk_key_metrics_ttm, bulk_stock_peers, + }, + error::DecodeErrorKind, + query::Year, }; -const KEY_METRICS: &[u8] = include_bytes!("fixtures/bulk_key_metrics_ttm.json"); -const RATIOS: &[u8] = include_bytes!("fixtures/bulk_financial_ratios_ttm.json"); -const PEERS: &[u8] = include_bytes!("fixtures/bulk_stock_peers.json"); -const SURPRISES: &[u8] = include_bytes!("fixtures/bulk_earnings_surprises.json"); +use bulk_csv::{assert_empty_bodies, assert_members, assert_round_trip, decode, with_cells}; + +const KEY_METRICS: &[u8] = include_bytes!("fixtures/bulk_key_metrics_ttm.csv"); +const RATIOS: &[u8] = include_bytes!("fixtures/bulk_financial_ratios_ttm.csv"); +const PEERS: &[u8] = include_bytes!("fixtures/bulk_stock_peers.csv"); +const SURPRISES: &[u8] = include_bytes!("fixtures/bulk_earnings_surprises.csv"); #[test] -fn exact_outer_md_fixtures_round_trip_with_exact_unique_key_sets() { - assert_exact::(KEY_METRICS, 43); - assert_exact::(RATIOS, 60); - assert_exact::(PEERS, 2); - assert_exact::(SURPRISES, 5); +fn live_csv_fixtures_round_trip_every_cell_with_exact_field_counts() { + assert_round_trip(&bulk_key_metrics_ttm(), KEY_METRICS, 43); + assert_round_trip(&bulk_financial_ratios_ttm(), RATIOS, 60); + assert_round_trip(&bulk_stock_peers(), PEERS, 2); + assert_round_trip(&bulk_earnings_surprises(Year(2024).into()), SURPRISES, 5); } #[test] -fn every_documented_field_is_required_non_null_and_unknown_fields_are_tolerated() { - assert_required::(KEY_METRICS); - assert_required::(RATIOS); - assert_required::(PEERS); - assert_required::(SURPRISES); +fn every_column_is_required_and_only_text_members_accept_an_empty_cell() { + assert_members(&bulk_key_metrics_ttm(), KEY_METRICS, &[]); + assert_members(&bulk_financial_ratios_ttm(), RATIOS, &[]); + assert_members(&bulk_stock_peers(), PEERS, &["peers"]); + assert_members(&bulk_earnings_surprises(Year(2024).into()), SURPRISES, &[]); } #[test] -fn every_documented_numeric_string_is_lexical_and_rejects_json_numbers() { - assert_all_numbers_rejected::(KEY_METRICS, &["symbol"]); - assert_all_numbers_rejected::(RATIOS, &["symbol"]); - assert_all_numbers_rejected::( - SURPRISES, - &["symbol", "date", "lastUpdated"], - ); - - let metrics = rows::(KEY_METRICS).remove(0); - assert_eq!(metrics.market_cap.as_str(), "249171756000"); - assert_eq!(metrics.enterprise_value_ttm.as_str(), "-496959244000"); - assert_eq!(metrics.current_ratio_ttm.as_str(), "0"); - assert_eq!( - metrics.free_cash_flow_to_firm_ttm.as_str(), - "-35237570137.11014" - ); - let ratios = rows::(RATIOS).remove(0); - assert_eq!(ratios.enterprise_value_ttm.as_str(), "-496959244000"); - assert_eq!(ratios.receivables_turnover_ttm.as_str(), "0"); - assert_eq!( - ratios.gross_profit_margin_ttm.as_str(), - "1.1622776732779352" - ); +fn empty_and_header_only_bodies_decode_to_no_rows() { + assert_empty_bodies(&bulk_key_metrics_ttm(), KEY_METRICS); + assert_empty_bodies(&bulk_financial_ratios_ttm(), RATIOS); + assert_empty_bodies(&bulk_stock_peers(), PEERS); + assert_empty_bodies(&bulk_earnings_surprises(Year(2024).into()), SURPRISES); +} +#[test] +fn numeric_cells_keep_their_exact_text_beyond_u64_and_in_exponent_form() { let beyond_u64 = "18446744073709551616"; let high_precision = "-12345678901234567890123456789.123456789012345678901234567890"; - let mut huge = source_row(KEY_METRICS); - huge.insert("marketCap".into(), json!(beyond_u64)); - huge.insert("freeCashFlowToFirmTTM".into(), json!(high_precision)); - let decoded: BulkKeyMetricsTtm = serde_json::from_value(Value::Object(huge)).unwrap(); + let body = with_cells( + KEY_METRICS, + &[ + ("marketCap", beyond_u64), + ("freeCashFlowToFirmTTM", high_precision), + ("evToSalesTTM", "6.9148336e-9"), + ], + ); + let decoded = decode(&bulk_key_metrics_ttm(), body).unwrap().remove(0); + assert_eq!(decoded.market_cap.as_str(), beyond_u64); assert_eq!(decoded.free_cash_flow_to_firm_ttm.as_str(), high_precision); - let encoded = serde_json::to_value(&decoded).unwrap(); - assert_eq!(encoded["marketCap"], beyond_u64); - assert_eq!(encoded["freeCashFlowToFirmTTM"], high_precision); + assert_eq!(decoded.ev_to_sales_ttm.as_str(), "6.9148336e-9"); } #[test] fn all_ttm_keys_and_provider_hazards_keep_exact_wire_spelling() { - let metrics = serialized_row::(KEY_METRICS); - let ratios = serialized_row::(RATIOS); + let metrics = first_row(&decode(&bulk_key_metrics_ttm(), KEY_METRICS).unwrap()); + let ratios = first_row(&decode(&bulk_financial_ratios_ttm(), RATIOS).unwrap()); assert_eq!( metrics.keys().filter(|key| key.ends_with("TTM")).count(), @@ -76,7 +75,6 @@ fn all_ttm_keys_and_provider_hazards_keep_exact_wire_spelling() { assert_eq!(ratios.keys().filter(|key| key.ends_with("TTM")).count(), 59); assert!(metrics.keys().all(|key| !key.contains("Ttm"))); assert!(ratios.keys().all(|key| !key.contains("Ttm"))); - for key in [ "evToEBITDATTM", "netDebtToEBITDATTM", @@ -86,119 +84,32 @@ fn all_ttm_keys_and_provider_hazards_keep_exact_wire_spelling() { } assert!(ratios.contains_key("netIncomePerEBTTTM")); assert!(!metrics.contains_key("researchAndDevelopmentToRevenueTTM")); - assert!(!ratios.contains_key("priceToEarningsRatioDilutedTTM")); - assert!(!ratios.contains_key("priceToEarningsGrowthRatioDilutedTTM")); + assert!(!ratios.contains_key("priceToEarningsDilutedRatioTTM")); + assert!(!ratios.contains_key("priceToEarningsDilutedGrowthRatioTTM")); } #[test] -fn peers_remain_one_scalar_string_and_earnings_dates_are_typed() { - let peers = rows::(PEERS).remove(0); +fn peers_remain_one_quoted_string_and_earnings_dates_are_typed() { + let peers = decode(&bulk_stock_peers(), PEERS).unwrap().remove(0); assert_eq!(peers.symbol.as_str(), "000001.SZ"); - assert_eq!(peers.peers, "600036.SS"); - let mut peer_row = source_row(PEERS); - peer_row.insert("peers".into(), json!(["600036.SS"])); - assert!(serde_json::from_value::(Value::Object(peer_row)).is_err()); - - let surprise = rows::(SURPRISES).remove(0); - assert_eq!(surprise.symbol.as_str(), "AMKYF"); - assert_eq!(surprise.date.to_string(), "2025-07-09"); - assert_eq!(surprise.last_updated.to_string(), "2025-07-09"); - let mut surprise_row = source_row(SURPRISES); - surprise_row.insert("date".into(), json!("2025-07-09T00:00:00")); - assert!(serde_json::from_value::(Value::Object(surprise_row)).is_err()); -} - -#[test] -fn all_four_contracts_require_bare_array_roots_and_accept_empty_arrays() { - assert_array_contract::(); - assert_array_contract::(); - assert_array_contract::(); - assert_array_contract::(); -} - -fn rows(fixture: &[u8]) -> Vec { - serde_json::from_slice(fixture).unwrap() -} - -fn source_row(fixture: &[u8]) -> Map { - serde_json::from_slice::(fixture).unwrap()[0] - .as_object() - .unwrap() - .clone() -} - -fn serialized_row(fixture: &[u8]) -> Map -where - T: DeserializeOwned + Serialize, -{ - serde_json::to_value(rows::(fixture)).unwrap()[0] - .as_object() - .unwrap() - .clone() -} - -fn assert_exact(fixture: &[u8], fields: usize) -where - T: DeserializeOwned + Serialize, -{ - let source: Value = serde_json::from_slice(fixture).unwrap(); - let row = source[0].as_object().unwrap(); - assert_eq!(row.len(), fields); assert_eq!( - row.keys().collect::>().len(), - fields + peers.peers, + "3698.HK,600000.SS,600015.SS,600016.SS,600036.SS,601166.SS,601658.SS" ); - assert_eq!(serde_json::to_value(rows::(fixture)).unwrap(), source); -} -fn assert_required(fixture: &[u8]) -where - T: DeserializeOwned, -{ - let row = source_row(fixture); - for field in row.keys() { - let mut missing = row.clone(); - missing.remove(field); - assert!( - serde_json::from_value::(Value::Object(missing)).is_err(), - "missing {field} unexpectedly decoded" - ); - - let mut null = row.clone(); - null.insert(field.clone(), Value::Null); - assert!( - serde_json::from_value::(Value::Object(null)).is_err(), - "null {field} unexpectedly decoded" - ); - } - - let mut future = row; - future.insert("futureField".into(), json!({"nested": true})); - assert!(serde_json::from_value::(Value::Object(future)).is_ok()); + let surprises = decode(&bulk_earnings_surprises(Year(2024).into()), SURPRISES).unwrap(); + assert_eq!(surprises[0].symbol.as_str(), "AUTO.OL"); + assert_eq!(surprises[0].date.to_string(), "2024-12-31"); + assert_eq!(surprises[0].last_updated.to_string(), "2025-10-07"); + let timestamped = with_cells(SURPRISES, &[("date", "2024-12-31T00:00:00")]); + let error = decode(&bulk_earnings_surprises(Year(2024).into()), timestamped).unwrap_err(); + assert_eq!(error.decode_path(), Some("[0].date")); + assert_eq!(error.decode_kind(), Some(DecodeErrorKind::InvalidValue)); } -fn assert_all_numbers_rejected(fixture: &[u8], non_numeric: &[&str]) -where - T: DeserializeOwned, -{ - let row = source_row(fixture); - for field in row - .keys() - .filter(|field| !non_numeric.contains(&field.as_str())) - { - let mut numeric = row.clone(); - numeric.insert(field.clone(), json!(123.5)); - assert!( - serde_json::from_value::(Value::Object(numeric)).is_err(), - "JSON number unexpectedly decoded for {field}" - ); +fn first_row(rows: &[R]) -> Map { + match serde_json::to_value(&rows[0]).unwrap() { + Value::Object(members) => members, + other => panic!("a row re-encodes as an object, not {other}"), } } - -fn assert_array_contract() -where - T: DeserializeOwned, -{ - assert!(serde_json::from_slice::>(b"[]").unwrap().is_empty()); - assert!(serde_json::from_slice::>(b"{}").is_err()); -} diff --git a/crates/libfmp/tests/support/bulk_csv.rs b/crates/libfmp/tests/support/bulk_csv.rs index 0503b978..9d32a347 100644 --- a/crates/libfmp/tests/support/bulk_csv.rs +++ b/crates/libfmp/tests/support/bulk_csv.rs @@ -75,8 +75,8 @@ where } /// Asserts the CSV member contract on the fixture's first record: a blanked -/// cell decodes only for a member in `blankable`, and is otherwise a null at -/// `[0].member`; dropping a model member's column is a missing member even +/// model member's cell decodes only for a member in `blankable`, and is +/// otherwise a null at `[0].member`; dropping a model member's column is a missing member even /// for an `Option`, while any other column, and an unknown one, is ignored. pub fn assert_members(endpoint: &EndpointSpec>, fixture: &[u8], blankable: &[&str]) where @@ -92,7 +92,7 @@ where let mut blank = records[0].clone(); blank[column].clear(); let result = decode(endpoint, write(&header, &blank)); - if blankable.contains(&member.as_str()) { + if blankable.contains(&member.as_str()) || !members.contains_key(member) { assert!(result.is_ok(), "blank {member} did not decode"); } else { assert_failure(result, member, DecodeErrorKind::Null); @@ -130,6 +130,18 @@ where assert!(decode(endpoint, write(&header, &[])).unwrap().is_empty()); } +/// Returns the fixture's header and first record with the named cells replaced. +#[allow(dead_code)] // Only suites that probe individual cells use it. +pub fn with_cells(fixture: &[u8], cells: &[(&str, &str)]) -> Vec { + let (header, records) = read(fixture); + let mut record = records[0].clone(); + for (name, value) in cells { + let column = header.iter().position(|column| column == name); + record[column.unwrap_or_else(|| panic!("{name} is a header column"))] = (*value).to_owned(); + } + write(&header, &record) +} + fn assert_failure(result: libfmp::Result, member: &str, kind: DecodeErrorKind) { let Err(error) = result else { panic!("{member}: expected a {kind:?} failure"); From 73346340a1e82de5e719c277e32bd78c1eb97ae0 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:48:02 +0800 Subject: [PATCH 14/31] test(sdk/go): decode metric bulk parity from CSV fixtures The key metrics TTM, financial ratios TTM, stock peers, and earnings surprise parity tests now consume the shared live CSV fixtures instead of standalone JSON ones, exercising per-cell round trips with exact member counts. **Testing Improvements:** * Switched the four metric assertions in `sdk/go/bulk_parity_test.go` to `assertCSVFixtureParity`, pinning the expected member counts (43 key metrics, 60 ratios, 2 peers, 5 surprises). * Rebuilt the numeric-text preservation check to read and rewrite CSV cells directly, confirming beyond-u64, high-precision, and exponent values such as `6.9148336e-9` survive verbatim through `decodeCSVRows`. * Updated the expected symbols and values to match the CSV data for key metrics, ratios, peers, and earnings surprises. **Fixture Cleanup:** * Deleted the now-unused `bulk_key_metrics_ttm.json`, `bulk_financial_ratios_ttm.json`, `bulk_stock_peers.json`, and `bulk_earnings_surprises.json` fixtures. Refs #376 --- .../fixtures/bulk_earnings_surprises.json | 9 -- .../fixtures/bulk_financial_ratios_ttm.json | 64 -------------- .../tests/fixtures/bulk_key_metrics_ttm.json | 47 ---------- .../tests/fixtures/bulk_stock_peers.json | 6 -- sdk/go/bulk_parity_test.go | 86 ++++++++----------- 5 files changed, 36 insertions(+), 176 deletions(-) delete mode 100644 crates/libfmp/tests/fixtures/bulk_earnings_surprises.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_financial_ratios_ttm.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_key_metrics_ttm.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_stock_peers.json diff --git a/crates/libfmp/tests/fixtures/bulk_earnings_surprises.json b/crates/libfmp/tests/fixtures/bulk_earnings_surprises.json deleted file mode 100644 index bbf606d8..00000000 --- a/crates/libfmp/tests/fixtures/bulk_earnings_surprises.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "date": "2025-07-09", - "epsActual": "0.3631", - "epsEstimated": "0.3615", - "lastUpdated": "2025-07-09", - "symbol": "AMKYF" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_financial_ratios_ttm.json b/crates/libfmp/tests/fixtures/bulk_financial_ratios_ttm.json deleted file mode 100644 index f904ade2..00000000 --- a/crates/libfmp/tests/fixtures/bulk_financial_ratios_ttm.json +++ /dev/null @@ -1,64 +0,0 @@ -[ - { - "assetTurnoverTTM": "0.029075827062555015", - "bookValuePerShareTTM": "22.260885357516656", - "bottomLineProfitMarginTTM": "0.25995857044215337", - "capexPerShareTTM": "0.10930051078304583", - "capitalExpenditureCoverageRatioTTM": "67.03702213279678", - "cashPerShareTTM": "32.81790720767194", - "cashRatioTTM": "0", - "continuousOperationsProfitMarginTTM": "0.25995857044215337", - "currentRatioTTM": "0", - "debtServiceCoverageRatioTTM": "0.24758322210087771", - "debtToAssetsRatioTTM": "0", - "debtToCapitalRatioTTM": "0", - "debtToEquityRatioTTM": "0", - "debtToMarketCapTTM": "0", - "dividendPaidAndCapexCoverageRatioTTM": "6.192364879934577", - "dividendPayoutRatioTTM": "0.5590996519509067", - "dividendPerShareTTM": "1.327", - "dividendYieldTTM": "0.10335", - "ebitMarginTTM": "0.22525536322293388", - "ebitdaMarginTTM": "0.2018381390033096", - "ebtPerEbitTTM": "0.6784217520188082", - "effectiveTaxRateTTM": "0.17748982974235347", - "enterpriseValueMultipleTTM": "-14.656106051669223", - "enterpriseValueTTM": "-496959244000", - "financialLeverageRatioTTM": "11.416164801466868", - "fixedAssetTurnoverTTM": "13.114441842310695", - "forwardPriceToEarningsGrowthRatioTTM": "2.4481492401413427", - "freeCashFlowOperatingCashFlowRatioTTM": "0.9850828696116743", - "freeCashFlowPerShareTTM": "7.21788024970599", - "grossProfitMarginTTM": "1.1622776732779352", - "interestCoverageRatioTTM": "0.7914088096104842", - "interestDebtPerShareTTM": "4.349676503966586", - "inventoryTurnoverTTM": "0", - "longTermDebtToCapitalRatioTTM": "0", - "netIncomePerEBTTTM": "0.8225101702576465", - "netIncomePerShareTTM": "1.9208740068077172", - "netProfitMarginTTM": "0.25995857044215337", - "operatingCashFlowCoverageRatioTTM": "0", - "operatingCashFlowPerShareTTM": "7.327180760489036", - "operatingCashFlowRatioTTM": "0", - "operatingCashFlowSalesRatioTTM": "0.991612895545132", - "operatingProfitMarginTTM": "0.4658682349579752", - "payablesTurnoverTTM": "0", - "pretaxProfitMarginTTM": "0.3160551441700993", - "priceToBookRatioTTM": "0.576796465809228", - "priceToEarningsGrowthRatioTTM": "-3.6096068640768793", - "priceToEarningsRatioTTM": "6.68445715569793", - "priceToFairValueTTM": "0.576796465809228", - "priceToFreeCashFlowRatioTTM": "1.518395607609901", - "priceToOperatingCashFlowRatioTTM": "1.7523793147342828", - "priceToSalesRatioTTM": "1.483200528584014", - "quickRatioTTM": "0", - "receivablesTurnoverTTM": "0", - "revenuePerShareTTM": "7.389154370023568", - "shareholdersEquityPerShareTTM": "22.260885357516656", - "shortTermOperatingCashFlowCoverageRatioTTM": "0", - "solvencyRatioTTM": "0.008534174446189174", - "symbol": "000001.SZ", - "tangibleBookValuePerShareTTM": "21.662613507347245", - "workingCapitalTurnoverRatioTTM": "0.23544250931631752" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_key_metrics_ttm.json b/crates/libfmp/tests/fixtures/bulk_key_metrics_ttm.json deleted file mode 100644 index 8524f486..00000000 --- a/crates/libfmp/tests/fixtures/bulk_key_metrics_ttm.json +++ /dev/null @@ -1,47 +0,0 @@ -[ - { - "averageInventoryTTM": "0", - "averagePayablesTTM": "0", - "averageReceivablesTTM": "0", - "capexToDepreciationTTM": "1.855862584017924", - "capexToOperatingCashFlowTTM": "0.014917130388325619", - "capexToRevenueTTM": "0.014792018857591847", - "cashConversionCycleTTM": "0", - "currentRatioTTM": "0", - "daysOfInventoryOutstandingTTM": "0", - "daysOfPayablesOutstandingTTM": "0", - "daysOfSalesOutstandingTTM": "0", - "earningsYieldTTM": "0.14960077934639543", - "enterpriseValueTTM": "-496959244000", - "evToEBITDATTM": "-14.656106051669223", - "evToFreeCashFlowTTM": "-3.028355803098073", - "evToOperatingCashFlowTTM": "-2.9831814247210167", - "evToSalesTTM": "-2.95816117050406", - "freeCashFlowToEquityTTM": "910233000000", - "freeCashFlowToFirmTTM": "-35237570137.11014", - "freeCashFlowYieldTTM": "0.6585898925077207", - "grahamNetNetTTM": "-199.05514330278228", - "grahamNumberTTM": "31.017865999534138", - "incomeQualityTTM": "15.217593861331872", - "intangiblesToTotalAssetsTTM": "0.002354159621091415", - "interestBurdenTTM": "1.4030970878917606", - "investedCapitalTTM": "772543000000", - "marketCap": "249171756000", - "netCurrentAssetValueTTM": "-4525615000000", - "netDebtToEBITDATTM": "-22.004571192638906", - "operatingCycleTTM": "0", - "operatingReturnOnAssetsTTM": "0.013555578495362656", - "researchAndDevelopementToRevenueTTM": "0", - "returnOnAssetsTTM": "0.007558510437605078", - "returnOnCapitalEmployedTTM": "0.013545504233575834", - "returnOnEquityTTM": "0.09082717681735725", - "returnOnInvestedCapitalTTM": "0.011141314993384131", - "returnOnTangibleAssetsTTM": "0.007576346366296015", - "salesGeneralAndAdministrativeToRevenueTTM": "0.10163337222314817", - "stockBasedCompensationToRevenueTTM": "0", - "symbol": "000001.SZ", - "tangibleAssetValueTTM": "492510000000", - "taxBurdenTTM": "0.8225101702576465", - "workingCapitalTTM": "746131000000" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_stock_peers.json b/crates/libfmp/tests/fixtures/bulk_stock_peers.json deleted file mode 100644 index 3c426b9d..00000000 --- a/crates/libfmp/tests/fixtures/bulk_stock_peers.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "peers": "600036.SS", - "symbol": "000001.SZ" - } -] diff --git a/sdk/go/bulk_parity_test.go b/sdk/go/bulk_parity_test.go index bb5e73c3..b16a8860 100644 --- a/sdk/go/bulk_parity_test.go +++ b/sdk/go/bulk_parity_test.go @@ -1,8 +1,9 @@ package fmp import ( - "encoding/json/jsontext" + "encoding/csv" "encoding/json/v2" + "slices" "strings" "testing" ) @@ -20,10 +21,10 @@ func TestBulkFixturesDecodeAndReencodeToTheSameMemberSet(t *testing.T) { assertCSVFixtureParity[BulkPriceTargetSummary](t, "bulk_price_target_summaries.csv", 10) assertCSVFixtureParity[BulkETFHolding](t, "bulk_etf_holdings.csv", 9) assertCSVFixtureParity[BulkUpgradesDowngradesConsensus](t, "bulk_upgrades_downgrades_consensus.csv", 7) - assertFixtureParity[BulkKeyMetricsTTM](t, "bulk_key_metrics_ttm.json") - assertFixtureParity[BulkFinancialRatiosTTM](t, "bulk_financial_ratios_ttm.json") - assertFixtureParity[BulkStockPeer](t, "bulk_stock_peers.json") - assertFixtureParity[BulkEarningsSurprise](t, "bulk_earnings_surprises.json") + assertCSVFixtureParity[BulkKeyMetricsTTM](t, "bulk_key_metrics_ttm.csv", 43) + assertCSVFixtureParity[BulkFinancialRatiosTTM](t, "bulk_financial_ratios_ttm.csv", 60) + assertCSVFixtureParity[BulkStockPeer](t, "bulk_stock_peers.csv", 2) + assertCSVFixtureParity[BulkEarningsSurprise](t, "bulk_earnings_surprises.csv", 5) assertFixtureParity[BulkIncomeStatement](t, "bulk_income_statements.json") assertFixtureParity[BulkIncomeStatementGrowth](t, "bulk_income_statement_growth.json") assertFixtureParity[BulkBalanceSheetStatement](t, "bulk_balance_sheet_statements.json") @@ -72,15 +73,14 @@ func TestBulkSnapshotsDecodeExactValues(t *testing.T) { } // Exact values copied from crates/libfmp/tests/bulk_metrics_responses.rs, -// including the beyond-u64 and high-precision strings that must survive -// untouched (ADR 0028: numeric strings are never parsed). +// including the beyond-u64, high-precision, and exponent cells that must +// survive untouched (numeric strings are never parsed). func TestBulkMetricsPreserveNumericTextIncludingBeyondU64(t *testing.T) { t.Parallel() - metrics := assertFixtureParity[BulkKeyMetricsTTM](t, "bulk_key_metrics_ttm.json") - if len(metrics) != 1 || metrics[0].Symbol != "000001.SZ" || metrics[0].MarketCap != "249171756000" || - metrics[0].EnterpriseValueTTM != "-496959244000" || metrics[0].CurrentRatioTTM != "0" || - metrics[0].FreeCashFlowToFirmTTM != "-35237570137.11014" { - t.Fatalf("bulk_key_metrics_ttm = %+v", metrics) + metrics := assertCSVFixtureParity[BulkKeyMetricsTTM](t, "bulk_key_metrics_ttm.csv", 43) + if metrics[0].Symbol != "000001.SZ" || metrics[0].MarketCap != "224526473551" || + metrics[0].EvToEbitdaTTM != "29.23198788110799" { + t.Fatalf("bulk_key_metrics_ttm = %+v", metrics[0]) } encoded, err := json.Marshal(metrics[0]) if err != nil || !strings.Contains(string(encoded), `"evToEBITDATTM":`) || @@ -89,54 +89,40 @@ func TestBulkMetricsPreserveNumericTextIncludingBeyondU64(t *testing.T) { strings.Contains(string(encoded), "Ttm") { t.Fatalf("re-encoded key metrics = %s, %v", encoded, err) } - ratios := assertFixtureParity[BulkFinancialRatiosTTM](t, "bulk_financial_ratios_ttm.json") - if len(ratios) != 1 || ratios[0].EnterpriseValueTTM != "-496959244000" || ratios[0].ReceivablesTurnoverTTM != "0" || - ratios[0].GrossProfitMarginTTM != "1.1622776732779352" { - t.Fatalf("bulk_financial_ratios_ttm = %+v", ratios) - } - if encoded, err := json.Marshal(ratios[0]); err != nil || !strings.Contains(string(encoded), `"netIncomePerEBTTTM":`) { - t.Fatalf("re-encoded ratios = %s, %v", encoded, err) + ratios := assertCSVFixtureParity[BulkFinancialRatiosTTM](t, "bulk_financial_ratios_ttm.csv", 60) + if ratios[0].GrossProfitMarginTTM != "0.5535250166330814" || ratios[0].NetIncomePerEbtTTM != "0.83539656299258" { + t.Fatalf("bulk_financial_ratios_ttm = %+v", ratios[0]) } const beyondU64 = "18446744073709551616" const highPrecision = "-12345678901234567890123456789.123456789012345678901234567890" - var wire []map[string]jsontext.Value - if err := json.Unmarshal(readFixture(t, "bulk_key_metrics_ttm.json"), &wire); err != nil { - t.Fatal(err) - } - wire[0]["marketCap"] = jsontext.Value(`"` + beyondU64 + `"`) - wire[0]["freeCashFlowToFirmTTM"] = jsontext.Value(`"` + highPrecision + `"`) - huge, err := json.Marshal(wire) + records, err := csv.NewReader(strings.NewReader(string(readFixture(t, "bulk_key_metrics_ttm.csv")))).ReadAll() if err != nil { t.Fatal(err) } - var decoded []BulkKeyMetricsTTM - if err := json.Unmarshal(huge, &decoded); err != nil || len(decoded) != 1 || - decoded[0].MarketCap != beyondU64 || decoded[0].FreeCashFlowToFirmTTM != highPrecision { + row := slices.Clone(records[1]) + for name, value := range map[string]string{"marketCap": beyondU64, "freeCashFlowToFirmTTM": highPrecision, + "evToSalesTTM": "6.9148336e-9"} { + row[slices.Index(records[0], name)] = value + } + var body strings.Builder + writer := csv.NewWriter(&body) + _ = writer.WriteAll([][]string{records[0], row}) + decoded, _, kind, err := decodeCSVRows[BulkKeyMetricsTTM]([]byte(body.String())) + if kind != DecodeKindNone || decoded[0].MarketCap != beyondU64 || + decoded[0].FreeCashFlowToFirmTTM != highPrecision || decoded[0].EvToSalesTTM != "6.9148336e-9" { t.Fatalf("beyond-u64 decode = %+v, %v", decoded, err) } - if encoded, err := json.Marshal(decoded[0]); err != nil || - !strings.Contains(string(encoded), `"marketCap":"`+beyondU64+`"`) || - !strings.Contains(string(encoded), `"freeCashFlowToFirmTTM":"`+highPrecision+`"`) { - t.Fatalf("beyond-u64 re-encode = %s, %v", encoded, err) - } - wire[0]["marketCap"] = jsontext.Value(`249171756000`) - numeric, err := json.Marshal(wire) - if err != nil { - t.Fatal(err) - } - if err := json.Unmarshal(numeric, &decoded); err == nil { - t.Fatalf("a JSON number decoded into the numeric-string member marketCap: %+v", decoded) - } - peers := assertFixtureParity[BulkStockPeer](t, "bulk_stock_peers.json") - if want := (BulkStockPeer{Symbol: "000001.SZ", Peers: "600036.SS"}); len(peers) != 1 || peers[0] != want { - t.Fatalf("bulk_stock_peers = %+v", peers) - } - surprises := assertFixtureParity[BulkEarningsSurprise](t, "bulk_earnings_surprises.json") - if want := (BulkEarningsSurprise{Symbol: "AMKYF", Date: mustParseDate(t, "2025-07-09"), EPSActual: "0.3631", - EPSEstimated: "0.3615", LastUpdated: mustParseDate(t, "2025-07-09")}); len(surprises) != 1 || surprises[0] != want { - t.Fatalf("bulk_earnings_surprises = %+v", surprises) + peers := assertCSVFixtureParity[BulkStockPeer](t, "bulk_stock_peers.csv", 2) + if want := (BulkStockPeer{Symbol: "000001.SZ", + Peers: "3698.HK,600000.SS,600015.SS,600016.SS,600036.SS,601166.SS,601658.SS"}); peers[0] != want { + t.Fatalf("bulk_stock_peers = %+v", peers[0]) + } + surprises := assertCSVFixtureParity[BulkEarningsSurprise](t, "bulk_earnings_surprises.csv", 5) + if want := (BulkEarningsSurprise{Symbol: "AUTO.OL", Date: mustParseDate(t, "2024-12-31"), EPSActual: "0.1332", + EPSEstimated: "0.1581", LastUpdated: mustParseDate(t, "2025-10-07")}); surprises[0] != want { + t.Fatalf("bulk_earnings_surprises = %+v", surprises[0]) } } From 063f04a2956ea9edd3497fc5c1cabccd9f6722fa Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:49:26 +0800 Subject: [PATCH 15/31] test(fmp-py): serve live CSV fixtures for bulk metric route tests The four bulk metric route tests (key metrics TTM, ratios TTM, peers, earnings surprises) now serve the shared live CSV fixtures as text/csv and assert the live values instead of the former JSON fixtures. The decode-error test now sends a text/plain body to a CSV route to exercise the FmpDecodeError path for non-CSV responses. Relates to #376 --- crates/fmp-py/tests/test_bulk_a.py | 51 ++++++++++++++---------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/crates/fmp-py/tests/test_bulk_a.py b/crates/fmp-py/tests/test_bulk_a.py index b5862a47..239c22d5 100644 --- a/crates/fmp-py/tests/test_bulk_a.py +++ b/crates/fmp-py/tests/test_bulk_a.py @@ -13,7 +13,7 @@ from typing import Any import pytest -from conftest import CSV_CONTENT_TYPE, FixtureServer, load_csv_fixture, load_fixture +from conftest import CSV_CONTENT_TYPE, FixtureServer, load_csv_fixture from fmp.bulk import BulkNamespace from fmp.bulk.metrics import BulkEarningsSurprise, BulkFinancialRatiosTtm, BulkKeyMetricsTtm, BulkStockPeer from fmp.bulk.snapshots import ( @@ -26,9 +26,6 @@ ) from fmp.company import CompanyProfile -DATE_2025_07_09 = datetime.date(2025, 7, 9) - - def route_csv(fixture_server: FixtureServer, path: str, fixture: str) -> None: """Serve the shared CSV fixture on ``path`` as ``text/csv``, as the bulk routes answer.""" fixture_server.route(path, load_csv_fixture(fixture), content_type=CSV_CONTENT_TYPE) @@ -159,66 +156,66 @@ def test_upgrades_downgrades_consensus_takes_no_arguments(client: Any, fixture_s def test_key_metrics_ttm_takes_no_arguments(client: Any, fixture_server: FixtureServer) -> None: """``key_metrics_ttm`` maps to ``/key-metrics-ttm-bulk`` with no query string.""" - fixture_server.route("/key-metrics-ttm-bulk", load_fixture("bulk_key_metrics_ttm.json")) + route_csv(fixture_server, "/key-metrics-ttm-bulk", "bulk_key_metrics_ttm.csv") rows = client.bulk.key_metrics_ttm() assert fixture_server.requests[0].target == "/key-metrics-ttm-bulk" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkKeyMetricsTtm) assert row.symbol == "000001.SZ" - assert row.market_cap == "249171756000" - assert row.ev_to_ebitda_ttm == "-14.656106051669223" - assert row.earnings_yield_ttm == "0.14960077934639543" + assert row.market_cap == "224526473551" + assert row.ev_to_ebitda_ttm == "29.23198788110799" + assert row.earnings_yield_ttm == "0.19355846887948813" def test_financial_ratios_ttm_takes_no_arguments(client: Any, fixture_server: FixtureServer) -> None: """``financial_ratios_ttm`` maps to ``/ratios-ttm-bulk`` with no query string.""" - fixture_server.route("/ratios-ttm-bulk", load_fixture("bulk_financial_ratios_ttm.json")) + route_csv(fixture_server, "/ratios-ttm-bulk", "bulk_financial_ratios_ttm.csv") rows = client.bulk.financial_ratios_ttm() assert fixture_server.requests[0].target == "/ratios-ttm-bulk" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkFinancialRatiosTtm) assert row.symbol == "000001.SZ" - assert row.asset_turnover_ttm == "0.029075827062555015" - assert row.book_value_per_share_ttm == "22.260885357516656" - assert row.net_income_per_ebt_ttm == "0.8225101702576465" + assert row.asset_turnover_ttm == "0.03463701192046656" + assert row.book_value_per_share_ttm == "24.12738217279904" + assert row.net_income_per_ebt_ttm == "0.83539656299258" def test_stock_peers_takes_no_arguments(client: Any, fixture_server: FixtureServer) -> None: """``stock_peers`` maps to ``/peers-bulk`` and keeps the peers cell as one string.""" - fixture_server.route("/peers-bulk", load_fixture("bulk_stock_peers.json")) + route_csv(fixture_server, "/peers-bulk", "bulk_stock_peers.csv") rows = client.bulk.stock_peers() assert fixture_server.requests[0].target == "/peers-bulk" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkStockPeer) assert row.symbol == "000001.SZ" - assert row.peers == "600036.SS" + assert row.peers == "3698.HK,600000.SS,600015.SS,600016.SS,600036.SS,601166.SS,601658.SS" def test_earnings_surprises_encodes_the_required_year(client: Any, fixture_server: FixtureServer) -> None: """``earnings_surprises`` sends ``year`` as the only query parameter.""" - fixture_server.route("/earnings-surprises-bulk", load_fixture("bulk_earnings_surprises.json")) + route_csv(fixture_server, "/earnings-surprises-bulk", "bulk_earnings_surprises.csv") rows = client.bulk.earnings_surprises(2026) assert fixture_server.requests[0].target == "/earnings-surprises-bulk?year=2026" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkEarningsSurprise) - assert row.symbol == "AMKYF" - assert row.date == DATE_2025_07_09 - assert row.eps_actual == "0.3631" - assert row.eps_estimated == "0.3615" - assert row.last_updated == DATE_2025_07_09 + assert row.symbol == "AUTO.OL" + assert row.date == datetime.date(2024, 12, 31) + assert row.eps_actual == "0.1332" + assert row.eps_estimated == "0.1581" + assert row.last_updated == datetime.date(2025, 10, 7) def test_earnings_surprises_accepts_the_year_keyword(client: Any, fixture_server: FixtureServer) -> None: """The constructor argument is also reachable by keyword.""" - fixture_server.route("/earnings-surprises-bulk", load_fixture("bulk_earnings_surprises.json")) + route_csv(fixture_server, "/earnings-surprises-bulk", "bulk_earnings_surprises.csv") client.bulk.earnings_surprises(year=2024) assert fixture_server.requests[0].target == "/earnings-surprises-bulk?year=2024" @@ -240,8 +237,8 @@ def test_status_error_carries_the_bulk_endpoint_id( def test_decode_error_names_the_bulk_endpoint( client: Any, fixture_server: FixtureServer, errors: SimpleNamespace ) -> None: - """A non-JSON body on a bulk method maps to ``FmpDecodeError``.""" - fixture_server.route("/peers-bulk", b"not-json", content_type="text/plain") + """A body that is not ``text/csv`` on a bulk method maps to ``FmpDecodeError``.""" + fixture_server.route("/peers-bulk", b"not-csv", content_type="text/plain") with pytest.raises(errors.FmpDecodeError): client.bulk.stock_peers() assert fixture_server.requests[0].target == "/peers-bulk" From 2c440e903883ada9ffb07bd692ebb397abf58275 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:50:45 +0800 Subject: [PATCH 16/31] fix(bulk)!: decode final seven bulk routes as text/csv FMP answers the income-statement-bulk, income-statement-growth-bulk, balance-sheet-statement-bulk, balance-sheet-statement-growth-bulk, cash-flow-statement-bulk, cash-flow-statement-growth-bulk, and eod-bulk routes with text/csv, so these descriptors now use EndpointSpec::get_csv. With this change all eighteen bulk routes are CSV. The generated Go methods switch from getJSON to getCSV, live CSV fixtures replace the former JSON ones, and the endpoint suites now serve those fixtures as text/csv while still checking that JSON bodies keep endpoint identities. The bulk descriptor unit test asserts the 256 MiB bulk default limit without overriding the client limit. Refs #376 BREAKING CHANGE: the income-statement-bulk, income-statement-growth-bulk, balance-sheet-statement-bulk, balance-sheet-statement-growth-bulk, cash-flow-statement-bulk, cash-flow-statement-growth-bulk, and eod-bulk methods now require a text/csv response. --- crates/libfmp/src/endpoints/bulk.rs | 32 +++++++++----- crates/libfmp/tests/bulk_balance_endpoints.rs | 26 +++++++----- .../libfmp/tests/bulk_cash_eod_endpoints.rs | 34 ++++++++------- crates/libfmp/tests/bulk_income_endpoints.rs | 24 ++++++----- .../bulk_balance_sheet_statement_growth.csv | 3 ++ .../bulk_balance_sheet_statements.csv | 3 ++ .../bulk_cash_flow_statement_growth.csv | 3 ++ .../fixtures/bulk_cash_flow_statements.csv | 3 ++ crates/libfmp/tests/fixtures/bulk_eod.csv | 3 ++ .../fixtures/bulk_income_statement_growth.csv | 3 ++ .../tests/fixtures/bulk_income_statements.csv | 3 ++ sdk/go/bulk.go | 42 ++++--------------- sdk/go/bulk_test.go | 28 ++++++------- 13 files changed, 112 insertions(+), 95 deletions(-) create mode 100644 crates/libfmp/tests/fixtures/bulk_balance_sheet_statement_growth.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_balance_sheet_statements.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_cash_flow_statement_growth.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_cash_flow_statements.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_eod.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_income_statement_growth.csv create mode 100644 crates/libfmp/tests/fixtures/bulk_income_statements.csv diff --git a/crates/libfmp/src/endpoints/bulk.rs b/crates/libfmp/src/endpoints/bulk.rs index e4d37d8f..03f8f1fe 100644 --- a/crates/libfmp/src/endpoints/bulk.rs +++ b/crates/libfmp/src/endpoints/bulk.rs @@ -237,7 +237,7 @@ pub fn bulk_earnings_surprises( pub fn bulk_income_statements( query: BulkStatementQuery, ) -> EndpointSpec> { - EndpointSpec::get("income-statement-bulk", "income-statement-bulk", query) + EndpointSpec::get_csv("income-statement-bulk", "income-statement-bulk", query) .with_metadata(WORLDWIDE) } @@ -245,7 +245,7 @@ pub fn bulk_income_statements( pub fn bulk_income_statement_growth( query: BulkStatementQuery, ) -> EndpointSpec> { - EndpointSpec::get( + EndpointSpec::get_csv( "income-statement-growth-bulk", "income-statement-growth-bulk", query, @@ -257,7 +257,7 @@ pub fn bulk_income_statement_growth( pub fn bulk_balance_sheet_statements( query: BulkStatementQuery, ) -> EndpointSpec> { - EndpointSpec::get( + EndpointSpec::get_csv( "balance-sheet-statement-bulk", "balance-sheet-statement-bulk", query, @@ -269,7 +269,7 @@ pub fn bulk_balance_sheet_statements( pub fn bulk_balance_sheet_statement_growth( query: BulkStatementQuery, ) -> EndpointSpec> { - EndpointSpec::get( + EndpointSpec::get_csv( "balance-sheet-statement-growth-bulk", "balance-sheet-statement-growth-bulk", query, @@ -281,7 +281,7 @@ pub fn bulk_balance_sheet_statement_growth( pub fn bulk_cash_flow_statements( query: BulkStatementQuery, ) -> EndpointSpec> { - EndpointSpec::get( + EndpointSpec::get_csv( "cash-flow-statement-bulk", "cash-flow-statement-bulk", query, @@ -293,7 +293,7 @@ pub fn bulk_cash_flow_statements( pub fn bulk_cash_flow_statement_growth( query: BulkStatementQuery, ) -> EndpointSpec> { - EndpointSpec::get( + EndpointSpec::get_csv( "cash-flow-statement-growth-bulk", "cash-flow-statement-growth-bulk", query, @@ -303,7 +303,7 @@ pub fn bulk_cash_flow_statement_growth( /// Describes `GET eod-bulk` without binding a transport. pub fn bulk_eod(query: BulkEodQuery) -> EndpointSpec> { - EndpointSpec::get("eod-bulk", "eod-bulk", query).with_metadata(WORLDWIDE) + EndpointSpec::get_csv("eod-bulk", "eod-bulk", query).with_metadata(WORLDWIDE) } impl Client { @@ -490,9 +490,21 @@ mod tests { } #[test] - fn bulk_descriptors_inherit_the_finite_client_response_limit() { + fn bulk_descriptors_default_to_the_bulk_limit_without_overriding_the_client() { + let bulk = crate::client::DEFAULT_BULK_MAX_RESPONSE_BODY_BYTES; assert_eq!(bulk_stock_ratings().max_response_body_bytes(), None); - assert_eq!(bulk_key_metrics_ttm().max_response_body_bytes(), None); - assert_eq!(bulk_financial_ratios_ttm().max_response_body_bytes(), None); + assert_eq!(bulk_stock_ratings().default_max_response_body_bytes(), bulk); + assert_eq!( + bulk_key_metrics_ttm().default_max_response_body_bytes(), + bulk + ); + assert_eq!( + bulk_financial_ratios_ttm().default_max_response_body_bytes(), + bulk + ); + assert_eq!( + bulk_eod(Date::parse("2025-06-02").unwrap().into()).default_max_response_body_bytes(), + bulk + ); } } diff --git a/crates/libfmp/tests/bulk_balance_endpoints.rs b/crates/libfmp/tests/bulk_balance_endpoints.rs index 9f997fba..c98614e8 100644 --- a/crates/libfmp/tests/bulk_balance_endpoints.rs +++ b/crates/libfmp/tests/bulk_balance_endpoints.rs @@ -18,13 +18,17 @@ use libfmp::{ transport::{HttpMethod, TransportResponse}, }; -use support::{FixtureExecutor, json_fixture}; +use support::{FixtureExecutor, fixture_response, json_fixture}; -const BALANCE: &[u8] = include_bytes!("fixtures/bulk_balance_sheet_statements.json"); -const GROWTH: &[u8] = include_bytes!("fixtures/bulk_balance_sheet_statement_growth.json"); +const BALANCE: &[u8] = include_bytes!("fixtures/bulk_balance_sheet_statements.csv"); +const GROWTH: &[u8] = include_bytes!("fixtures/bulk_balance_sheet_statement_growth.csv"); + +fn csv_fixture(body: &'static [u8]) -> TransportResponse { + fixture_response(Some("text/csv"), body) +} fn fixtures() -> [TransportResponse; 2] { - [json_fixture(BALANCE), json_fixture(GROWTH)] + [csv_fixture(BALANCE), csv_fixture(GROWTH)] } #[test] @@ -67,7 +71,7 @@ fn assert_response_types( #[tokio::test] async fn all_five_periods_encode_required_year_then_period_without_defaults_or_ranges() { let executor = Arc::new(FixtureExecutor::new( - std::iter::repeat_with(|| json_fixture(b"[]")).take(5), + std::iter::repeat_with(|| csv_fixture(b"")).take(5), )); let client = Client::builder() .authentication(Authentication::fmp_header("secret")) @@ -124,11 +128,11 @@ async fn custom_proxy_sends_one_exact_request_per_method_with_auth_headers_and_d .await .unwrap(); - assert_eq!(balance[0].symbol.as_str(), "MTLRP.ME"); + assert_eq!(balance[0].symbol.as_str(), "000001.SZ"); assert_eq!(balance[0].cik.as_str(), "0000000000"); - assert_eq!(balance[0].accepted_date.to_string(), "2025-03-31 07:00:00"); + assert_eq!(balance[0].accepted_date.to_string(), "2024-12-31 00:00:00"); assert_eq!(growth[0].reported_currency.as_str(), "CNY"); - assert_eq!(growth[0].period, FiscalPeriod::Q1); + assert_eq!(growth[0].period, FiscalPeriod::FullYear); let requests = executor.requests(); assert_eq!(requests.len(), 2); @@ -197,12 +201,12 @@ async fn direct_fmp_header_and_query_auth_preserve_paths_and_exact_query_order() } #[tokio::test] -async fn bare_empty_arrays_decode_and_malformed_roots_keep_both_endpoint_identities() { +async fn empty_bodies_decode_and_json_bodies_keep_both_endpoint_identities() { let executor = Arc::new(FixtureExecutor::new([ + csv_fixture(b""), + csv_fixture(b""), json_fixture(b"[]"), json_fixture(b"[]"), - json_fixture(b"{}"), - json_fixture(b"{}"), ])); let client = Client::builder() .authentication(Authentication::fmp_header("secret")) diff --git a/crates/libfmp/tests/bulk_cash_eod_endpoints.rs b/crates/libfmp/tests/bulk_cash_eod_endpoints.rs index f9288701..b8f0de01 100644 --- a/crates/libfmp/tests/bulk_cash_eod_endpoints.rs +++ b/crates/libfmp/tests/bulk_cash_eod_endpoints.rs @@ -20,17 +20,21 @@ use libfmp::{ types::Date, }; -use support::{FixtureExecutor, json_fixture}; +use support::{FixtureExecutor, fixture_response, json_fixture}; -const CASH_FLOW: &[u8] = include_bytes!("fixtures/bulk_cash_flow_statements.json"); -const GROWTH: &[u8] = include_bytes!("fixtures/bulk_cash_flow_statement_growth.json"); -const EOD: &[u8] = include_bytes!("fixtures/bulk_eod.json"); +const CASH_FLOW: &[u8] = include_bytes!("fixtures/bulk_cash_flow_statements.csv"); +const GROWTH: &[u8] = include_bytes!("fixtures/bulk_cash_flow_statement_growth.csv"); +const EOD: &[u8] = include_bytes!("fixtures/bulk_eod.csv"); + +fn csv_fixture(body: &'static [u8]) -> TransportResponse { + fixture_response(Some("text/csv"), body) +} fn fixtures() -> [TransportResponse; 3] { [ - json_fixture(CASH_FLOW), - json_fixture(GROWTH), - json_fixture(EOD), + csv_fixture(CASH_FLOW), + csv_fixture(GROWTH), + csv_fixture(EOD), ] } @@ -78,7 +82,7 @@ fn assert_response_types( #[tokio::test] async fn all_five_periods_encode_required_year_then_period_without_defaults_or_ranges() { let executor = Arc::new(FixtureExecutor::new( - std::iter::repeat_with(|| json_fixture(b"[]")).take(5), + std::iter::repeat_with(|| csv_fixture(b"")).take(5), )); let client = Client::builder() .authentication(Authentication::fmp_header("secret")) @@ -139,9 +143,9 @@ async fn custom_proxy_sends_one_exact_request_per_method_with_auth_headers_and_d assert_eq!(cash_flow[0].symbol.as_str(), "000001.SZ"); assert_eq!(cash_flow[0].cik.as_str(), "0000000000"); assert_eq!(growth[0].reported_currency.as_str(), "CNY"); - assert_eq!(growth[0].period, FiscalPeriod::Q1); - assert_eq!(eod[0].symbol.as_str(), "EGS745W1C011.CA"); - assert_eq!(eod[0].date, date); + assert_eq!(growth[0].period, FiscalPeriod::FullYear); + assert_eq!(eod[0].symbol.as_str(), "PCFD.SG"); + assert_eq!(eod[0].date, Date::parse("2025-06-02").unwrap()); let requests = executor.requests(); assert_eq!(requests.len(), 3); @@ -218,14 +222,14 @@ async fn direct_fmp_header_and_query_auth_preserve_paths_and_exact_query_order() } #[tokio::test] -async fn bare_empty_arrays_decode_and_malformed_roots_keep_all_endpoint_identities() { +async fn empty_bodies_decode_and_json_bodies_keep_all_endpoint_identities() { let executor = Arc::new(FixtureExecutor::new([ + csv_fixture(b""), + csv_fixture(b""), + csv_fixture(b""), json_fixture(b"[]"), json_fixture(b"[]"), json_fixture(b"[]"), - json_fixture(b"{}"), - json_fixture(b"{}"), - json_fixture(b"{}"), ])); let client = Client::builder() .authentication(Authentication::fmp_header("secret")) diff --git a/crates/libfmp/tests/bulk_income_endpoints.rs b/crates/libfmp/tests/bulk_income_endpoints.rs index adfe5867..ef957266 100644 --- a/crates/libfmp/tests/bulk_income_endpoints.rs +++ b/crates/libfmp/tests/bulk_income_endpoints.rs @@ -16,13 +16,17 @@ use libfmp::{ transport::{HttpMethod, TransportResponse}, }; -use support::{FixtureExecutor, json_fixture}; +use support::{FixtureExecutor, fixture_response, json_fixture}; -const INCOME: &[u8] = include_bytes!("fixtures/bulk_income_statements.json"); -const GROWTH: &[u8] = include_bytes!("fixtures/bulk_income_statement_growth.json"); +const INCOME: &[u8] = include_bytes!("fixtures/bulk_income_statements.csv"); +const GROWTH: &[u8] = include_bytes!("fixtures/bulk_income_statement_growth.csv"); + +fn csv_fixture(body: &'static [u8]) -> TransportResponse { + fixture_response(Some("text/csv"), body) +} fn fixtures() -> [TransportResponse; 2] { - [json_fixture(INCOME), json_fixture(GROWTH)] + [csv_fixture(INCOME), csv_fixture(GROWTH)] } #[test] @@ -62,7 +66,7 @@ fn assert_response_types( #[tokio::test] async fn all_five_periods_encode_required_year_then_period_without_defaults_or_ranges() { let executor = Arc::new(FixtureExecutor::new( - std::iter::repeat_with(|| json_fixture(b"[]")).take(5), + std::iter::repeat_with(|| csv_fixture(b"")).take(5), )); let client = Client::builder() .authentication(Authentication::fmp_header("secret")) @@ -118,9 +122,9 @@ async fn custom_proxy_sends_one_exact_request_per_method_with_auth_headers_and_d assert_eq!(income[0].symbol.as_str(), "000001.SZ"); assert_eq!(income[0].cik.as_str(), "0000000000"); - assert_eq!(income[0].accepted_date.to_string(), "2025-03-31 00:00:00"); + assert_eq!(income[0].accepted_date.to_string(), "2024-12-31 00:00:00"); assert_eq!(growth[0].reported_currency.as_str(), "CNY"); - assert_eq!(growth[0].period, FiscalPeriod::Q1); + assert_eq!(growth[0].period, FiscalPeriod::FullYear); let requests = executor.requests(); assert_eq!(requests.len(), 2); @@ -184,12 +188,12 @@ async fn direct_fmp_header_and_query_auth_preserve_paths_and_exact_query_order() } #[tokio::test] -async fn bare_empty_arrays_decode_and_malformed_roots_keep_both_endpoint_identities() { +async fn empty_bodies_decode_and_json_bodies_keep_both_endpoint_identities() { let executor = Arc::new(FixtureExecutor::new([ + csv_fixture(b""), + csv_fixture(b""), json_fixture(b"[]"), json_fixture(b"[]"), - json_fixture(b"{}"), - json_fixture(b"{}"), ])); let client = Client::builder() .authentication(Authentication::fmp_header("secret")) diff --git a/crates/libfmp/tests/fixtures/bulk_balance_sheet_statement_growth.csv b/crates/libfmp/tests/fixtures/bulk_balance_sheet_statement_growth.csv new file mode 100644 index 00000000..1bebb1ea --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_balance_sheet_statement_growth.csv @@ -0,0 +1,3 @@ +"symbol","date","fiscalYear","period","reportedCurrency","growthCashAndCashEquivalents","growthShortTermInvestments","growthCashAndShortTermInvestments","growthNetReceivables","growthInventory","growthOtherCurrentAssets","growthTotalCurrentAssets","growthPropertyPlantEquipmentNet","growthGoodwill","growthIntangibleAssets","growthGoodwillAndIntangibleAssets","growthLongTermInvestments","growthTaxAssets","growthOtherNonCurrentAssets","growthTotalNonCurrentAssets","growthOtherAssets","growthTotalAssets","growthAccountPayables","growthShortTermDebt","growthTaxPayables","growthDeferredRevenue","growthOtherCurrentLiabilities","growthTotalCurrentLiabilities","growthLongTermDebt","growthDeferredRevenueNonCurrent","growthDeferredTaxLiabilitiesNonCurrent","growthOtherNonCurrentLiabilities","growthTotalNonCurrentLiabilities","growthOtherLiabilities","growthTotalLiabilities","growthPreferredStock","growthCommonStock","growthRetainedEarnings","growthAccumulatedOtherComprehensiveIncomeLoss","growthOthertotalStockholdersEquity","growthTotalStockholdersEquity","growthMinorityInterest","growthTotalEquity","growthTotalLiabilitiesAndStockholdersEquity","growthTotalInvestments","growthTotalDebt","growthNetDebt","growthAccountsReceivables","growthOtherReceivables","growthPrepaids","growthTotalPayables","growthOtherPayables","growthAccruedExpenses","growthCapitalLeaseObligationsCurrent","growthAdditionalPaidInCapital","growthTreasuryStock" +"000001.SZ","2024-12-31","2024","FY","CNY",0.13745663137040692,0.3981363245708905,0.2493622132819799,0,0,0,0.2493622132819799,-0.12238614496472097,0,-0.057988523104802174,-0.027061310782241013,-0.00034695865151514703,0.14362829730970125,-0.07488693367017299,-0.017498660254692634,0,0.032602509058340653,0,0,0.5108742004264393,0,0.055551063599693966,0.05612584115347865,-0.034898040702044546,0,0,-0.10559478857406393,-0.05659307561871587,0,0.031211459790708822,0,0,0.10101466633522406,0,0.0012528839248880808,0.04766602869192595,0,0.04766602869192595,0.032602509058340653,0.04673722173415161,-0.034898040702044546,-0.5927764260610976,0,0,0,0.14174413790507734,0.14174413790507734,0,0,0,0 +"000002.SZ","2024-12-31","2024","FY","CNY",-0.11672629689164585,11.982811811370647,-0.11507622608909761,-0.12132529270967031,-0.2603497824915679,-0.17460445350918893,-0.202343793972393,-0.07410898156342684,0.02434416697326749,-0.24268756803052952,-0.1544327862044751,-0.11435863579178189,0.1190329250633101,0.24337089671762815,0.03992686936474342,0,-0.1452571930861965,-0.27814218557737913,14.018458726430618,-0.02819354338907874,0,-0.11188223593418148,-0.12500034406798766,-0.21206661859377662,-1,3.702333174793378,1.0101077832674181,-0.18487040775435004,0,-0.14022062854026723,0,0,-0.5285236432432463,0,0.011267077683393927,-0.1918703241972824,-0.10490211307319319,-0.1590308675502114,-0.1452571930861965,-0.11311806388110532,-0.15260982701384376,-0.1726718823071758,0.11203340779101978,-0.1276637028305984,-0.040289532846611395,-0.17369740510948883,-0.055450442026140453,0,0,0,0 diff --git a/crates/libfmp/tests/fixtures/bulk_balance_sheet_statements.csv b/crates/libfmp/tests/fixtures/bulk_balance_sheet_statements.csv new file mode 100644 index 00000000..3326cbce --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_balance_sheet_statements.csv @@ -0,0 +1,3 @@ +"date","symbol","reportedCurrency","cik","filingDate","acceptedDate","fiscalYear","period","cashAndCashEquivalents","shortTermInvestments","cashAndShortTermInvestments","netReceivables","accountsReceivables","otherReceivables","inventory","prepaids","otherCurrentAssets","totalCurrentAssets","propertyPlantEquipmentNet","goodwill","intangibleAssets","goodwillAndIntangibleAssets","longTermInvestments","taxAssets","otherNonCurrentAssets","totalNonCurrentAssets","otherAssets","totalAssets","totalPayables","accountPayables","otherPayables","accruedExpenses","shortTermDebt","capitalLeaseObligationsCurrent","taxPayables","deferredRevenue","otherCurrentLiabilities","totalCurrentLiabilities","longTermDebt","capitalLeaseObligationsNonCurrent","deferredRevenueNonCurrent","deferredTaxLiabilitiesNonCurrent","otherNonCurrentLiabilities","totalNonCurrentLiabilities","otherLiabilities","capitalLeaseObligations","totalLiabilities","treasuryStock","preferredStock","commonStock","retainedEarnings","additionalPaidInCapital","accumulatedOtherComprehensiveIncomeLoss","otherTotalStockholdersEquity","totalStockholdersEquity","totalEquity","minorityInterest","totalLiabilitiesAndTotalEquity","totalInvestments","totalDebt","netDebt" +"2024-12-31","000001.SZ","CNY","0000000000","2024-12-31","2024-12-31 00:00:00","2024","FY",680935000000,629571000000,1310506000000,0,0,0,0,0,0,1310506000000,13682000000,7568000000,6238000000,13806000000,3359465000000,52329000000,1019482000000,4458764000000,0,5769270000000,30335000000,0,30335000000,0,0,0,14172000000,0,4177546000000,4207881000000,756251000000,0,0,0,310296000000,1066547000000,0,0,5274428000000,0,19953000000,19406000000,243605000000,80816000000,0,131062000000,494842000000,494842000000,0,5769270000000,3989036000000,756251000000,75316000000 +"2025-06-30","0002.KL","MYR","0000000000","2025-06-30","2025-06-30 00:00:00","2024","FY",32176000,18511000,50687000,35231000,32651000,2580000,51764000,272000,-230000,137724000,209468000,0,0,0,1261000,0,1261000,210729000,0,348453000,59313000,13277000,46036000,0,0,960000,1911000,0,35854000,60273000,1531000,82000,294000,6179000,0,8086000,0,1042000,68359000,0,0,86276000,193818000,0,0,0,280094000,280094000,0,348453000,18511000,2573000,-29603000 diff --git a/crates/libfmp/tests/fixtures/bulk_cash_flow_statement_growth.csv b/crates/libfmp/tests/fixtures/bulk_cash_flow_statement_growth.csv new file mode 100644 index 00000000..cfa10899 --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_cash_flow_statement_growth.csv @@ -0,0 +1,3 @@ +"symbol","date","fiscalYear","period","reportedCurrency","growthNetIncome","growthDepreciationAndAmortization","growthDeferredIncomeTax","growthStockBasedCompensation","growthChangeInWorkingCapital","growthAccountsReceivables","growthInventory","growthAccountsPayables","growthOtherWorkingCapital","growthOtherNonCashItems","growthNetCashProvidedByOperatingActivites","growthInvestmentsInPropertyPlantAndEquipment","growthAcquisitionsNet","growthPurchasesOfInvestments","growthSalesMaturitiesOfInvestments","growthOtherInvestingActivites","growthNetCashUsedForInvestingActivites","growthDebtRepayment","growthCommonStockIssued","growthCommonStockRepurchased","growthDividendsPaid","growthOtherFinancingActivites","growthNetCashUsedProvidedByFinancingActivities","growthEffectOfForexChangesOnCash","growthNetChangeInCash","growthCashAtEndOfPeriod","growthCashAtBeginningOfPeriod","growthOperatingCashFlow","growthCapitalExpenditure","growthFreeCashFlow","growthNetDebtIssuance","growthLongTermNetDebtIssuance","growthShortTermNetDebtIssuance","growthNetStockIssuance","growthPreferredDividendsPaid","growthIncomeTaxesPaid","growthInterestPaid" +"000001.SZ","2024-12-31","2024","FY","CNY",-0.04191152728446884,-0.10923518487253081,0,0,-0.5531031325616376,0,0,0,-0.5531031325616376,-1.0121039890537837,-0.31499767469527695,0.22868217054263565,0,-0.4583663057845359,0.4677030505827389,0.5753424657534246,-0.21869023028077422,0.1434209903752942,0,0,-1.5747016706443915,0.021323938438716855,-10.139943132649277,-0.20336700336700336,-1.5438314469055117,-0.13839829118869018,0.3413590853071616,-0.31499767469527695,0.22868217054263565,-0.3179880266323505,-3.1534626292196504,-3.1534626292196504,0,0,-1.5747016706443915,0,0 +"000002.SZ","2024-12-31","2024","FY","CNY",-3.418825680531423,-0.03954598688771192,0,0,0.9581421763368241,-0.7995560363059401,-0.15061396395902196,1,0,34.307963887705945,-0.028749152677538976,0.4648098236195674,1.7915065697370418,0.6916693011479459,0.3825348800713944,0.16537124469098757,3.3418475696338668,0.9862757836011588,0,0.5291703272930137,0.39958402090830386,-1.0898368285307443,0.24828523296720706,2.804478045943588,0.6549502718538156,-0.13341078193124575,-0.2788334388323215,-0.028749152677538976,0.4648098236195674,0.8412007641619601,-1.3619064495275701,-1.3619064495275701,0,0.5291703272930137,0.39958402090830386,0,0 diff --git a/crates/libfmp/tests/fixtures/bulk_cash_flow_statements.csv b/crates/libfmp/tests/fixtures/bulk_cash_flow_statements.csv new file mode 100644 index 00000000..a06c5413 --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_cash_flow_statements.csv @@ -0,0 +1,3 @@ +"date","symbol","reportedCurrency","cik","filingDate","acceptedDate","fiscalYear","period","netIncome","depreciationAndAmortization","deferredIncomeTax","stockBasedCompensation","changeInWorkingCapital","accountsReceivables","inventory","accountsPayables","otherWorkingCapital","otherNonCashItems","netCashProvidedByOperatingActivities","investmentsInPropertyPlantAndEquipment","acquisitionsNet","purchasesOfInvestments","salesMaturitiesOfInvestments","otherInvestingActivities","netCashProvidedByInvestingActivities","netDebtIssuance","longTermNetDebtIssuance","shortTermNetDebtIssuance","netStockIssuance","netCommonStockIssuance","commonStockIssuance","commonStockRepurchased","netPreferredStockIssuance","netDividendsPaid","commonDividendsPaid","preferredDividendsPaid","otherFinancingActivities","netCashProvidedByFinancingActivities","effectOfForexChangesOnCash","netChangeInCash","cashAtEndOfPeriod","cashAtBeginningOfPeriod","operatingCashFlow","capitalExpenditure","freeCashFlow","incomeTaxesPaid","interestPaid" +"2024-03-31","000001.SZ","CNY","0000000000","2024-03-31","2024-03-30 20:00:00","2024","Q1",14932000000,0,0,0,0,0,0,0,0,-36314000000,-21382000000,-241000000,17000000,-148027000000,116781000000,39781000000,8311000000,25543000000,25543000000,0,0,0,0,0,0,-2029000000,-2029000000,0,-713000000,22801000000,923000000,10653000000,308872000000,298219000000,-21382000000,-241000000,-21623000000,0,0 +"2024-09-30","0002.KL","MYR","0000000000","2024-09-30","2024-09-30 00:00:00","2024","Q1",11698000,3512000,0,0,1648000,373000,6004000,-4729000,-4729000,3466000,20324000,-7292000,0,0,0,869000,-6423000,0,0,0,0,0,0,0,0,0,0,0,-246000,-246000,-6028000,7627000,73640000,66013000,20324000,-7292000,13032000,0,0 diff --git a/crates/libfmp/tests/fixtures/bulk_eod.csv b/crates/libfmp/tests/fixtures/bulk_eod.csv new file mode 100644 index 00000000..56f0c949 --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_eod.csv @@ -0,0 +1,3 @@ +"symbol","date","open","low","high","close","adjClose","volume" +"PCFD.SG","2025-06-02",7.35,7.35,19.71,19.71,19.71,0 +"HKDCNH","2025-06-02",0.91858,0.91692,0.9186,0.91741,0.91741,0 diff --git a/crates/libfmp/tests/fixtures/bulk_income_statement_growth.csv b/crates/libfmp/tests/fixtures/bulk_income_statement_growth.csv new file mode 100644 index 00000000..e61e8b1f --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_income_statement_growth.csv @@ -0,0 +1,3 @@ +"symbol","date","fiscalYear","period","reportedCurrency","growthRevenue","growthCostOfRevenue","growthGrossProfit","growthGrossProfitRatio","growthResearchAndDevelopmentExpenses","growthGeneralAndAdministrativeExpenses","growthSellingAndMarketingExpenses","growthOtherExpenses","growthOperatingExpenses","growthCostAndExpenses","growthInterestIncome","growthInterestExpense","growthDepreciationAndAmortization","growthEBITDA","growthOperatingIncome","growthIncomeBeforeTax","growthIncomeTaxExpense","growthNetIncome","growthEPS","growthEPSDiluted","growthWeightedAverageShsOut","growthWeightedAverageShsOutDil","growthEBIT","growthNonOperatingIncomeExcludingInterest","growthNetInterestIncome","growthTotalOtherIncomeExpensesNet","growthNetIncomeFromContinuingOperations","growthOtherAdjustmentsToNetIncome","growthNetIncomeDeductions" +"000001.SZ","2024-12-31","2024","FY","CNY",-0.08220846812871789,-0.04261762720522504,-0.1085837561909392,-0.028737776781126968,0,-0.1517311140742103,0,-0.13618841669800677,-0.13935247152203825,-0.09036186322835774,-0.12844383328134543,-0.04261762720522504,-0.09851265211512458,-0.055489307576118925,-0.051630340621643164,-0.051630340621643164,-0.09171623901269645,-0.04191152728446884,-0.04444444444444448,-0.04444444444444448,0.0667522931052252,0.0667522931052252,-0.051630340621643164,0,-0.20818537007059862,0,-0.04191152728446884,0,0 +"000002.SZ","2024-12-31","2024","FY","CNY",-0.2631572956889765,-0.2191553347697649,-0.5079774190529218,-0.3322556116134429,-0.05250863825309522,0.10876775722617925,-0.1538891907475451,1.7355792412327944,0.9316205360627275,-0.1092062238913554,-0.29996369591965133,0,-0.03954598688771192,-2.0231006134221006,-2.5603808626246773,-2.583153209542906,-0.8377116473277169,-5.068051837900253,-5.048543689320389,-5.048543689320389,0.006365235696615214,0.006365235696615214,-2.5429478654093614,2.4484685657454373,-0.29996369591965133,-3.7861487450471896,-3.3809633548006857,0,0 diff --git a/crates/libfmp/tests/fixtures/bulk_income_statements.csv b/crates/libfmp/tests/fixtures/bulk_income_statements.csv new file mode 100644 index 00000000..c4ea2d11 --- /dev/null +++ b/crates/libfmp/tests/fixtures/bulk_income_statements.csv @@ -0,0 +1,3 @@ +"date","symbol","reportedCurrency","cik","filingDate","acceptedDate","fiscalYear","period","revenue","costOfRevenue","grossProfit","researchAndDevelopmentExpenses","generalAndAdministrativeExpenses","sellingAndMarketingExpenses","sellingGeneralAndAdministrativeExpenses","otherExpenses","operatingExpenses","costAndExpenses","netInterestIncome","interestIncome","interestExpense","depreciationAndAmortization","ebitda","ebit","nonOperatingIncomeExcludingInterest","operatingIncome","totalOtherIncomeExpensesNet","incomeBeforeTax","incomeTaxExpense","netIncomeFromContinuingOperations","netIncomeFromDiscontinuedOperations","otherAdjustmentsToNetIncome","netIncome","netIncomeDeductions","bottomLineNetIncome","eps","epsDiluted","weightedAverageShsOut","weightedAverageShsOutDil" +"2024-12-31","000001.SZ","CNY","0000000000","2024-12-31","2024-12-31 00:00:00","2024","FY",251641000000,104954000000,146687000000,0,18449000000,0,18449000000,73500000000,91949000000,196903000000,93427000000,198381000000,104954000000,4667000000,59405000000,54738000000,0,54738000000,0,54738000000,10230000000,44508000000,0,0,44508000000,0,44508000000,2.15,2.15,20701395000,20701395000 +"2025-06-30","0002.KL","MYR","0000000000","2025-06-30","2025-06-30 00:00:00","2024","FY",230784000,177777000,53007000,0,0,0,0,1624000,1624000,179401000,2258000,2285000,27000,13605000,64988000,51383000,1649000,53032000,-27000,51356000,8881000,42475000,0,0,42475000,0,42475000,0.29,0.29,148314000,148314000 diff --git a/sdk/go/bulk.go b/sdk/go/bulk.go index b848314f..8f1032ee 100644 --- a/sdk/go/bulk.go +++ b/sdk/go/bulk.go @@ -237,11 +237,7 @@ func (n *BulkNamespace) IncomeStatements(ctx context.Context, q BulkStatementQue if err != nil { return nil, err } - var out []BulkIncomeStatement - if err := n.client.getJSON(ctx, "income-statement-bulk", "income-statement-bulk", params, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkIncomeStatement](ctx, n.client, "income-statement-bulk", "income-statement-bulk", params) } // IncomeStatementGrowth retrieves worldwide bulk income-statement growth for @@ -253,11 +249,7 @@ func (n *BulkNamespace) IncomeStatementGrowth(ctx context.Context, q BulkStateme if err != nil { return nil, err } - var out []BulkIncomeStatementGrowth - if err := n.client.getJSON(ctx, "income-statement-growth-bulk", "income-statement-growth-bulk", params, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkIncomeStatementGrowth](ctx, n.client, "income-statement-growth-bulk", "income-statement-growth-bulk", params) } // BalanceSheetStatements retrieves worldwide bulk balance sheets for one year @@ -269,11 +261,7 @@ func (n *BulkNamespace) BalanceSheetStatements(ctx context.Context, q BulkStatem if err != nil { return nil, err } - var out []BulkBalanceSheetStatement - if err := n.client.getJSON(ctx, "balance-sheet-statement-bulk", "balance-sheet-statement-bulk", params, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkBalanceSheetStatement](ctx, n.client, "balance-sheet-statement-bulk", "balance-sheet-statement-bulk", params) } // BalanceSheetStatementGrowth retrieves worldwide bulk balance-sheet growth @@ -285,11 +273,7 @@ func (n *BulkNamespace) BalanceSheetStatementGrowth(ctx context.Context, q BulkS if err != nil { return nil, err } - var out []BulkBalanceSheetStatementGrowth - if err := n.client.getJSON(ctx, "balance-sheet-statement-growth-bulk", "balance-sheet-statement-growth-bulk", params, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkBalanceSheetStatementGrowth](ctx, n.client, "balance-sheet-statement-growth-bulk", "balance-sheet-statement-growth-bulk", params) } // CashFlowStatements retrieves worldwide bulk cash-flow statements for one @@ -301,11 +285,7 @@ func (n *BulkNamespace) CashFlowStatements(ctx context.Context, q BulkStatementQ if err != nil { return nil, err } - var out []BulkCashFlowStatement - if err := n.client.getJSON(ctx, "cash-flow-statement-bulk", "cash-flow-statement-bulk", params, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkCashFlowStatement](ctx, n.client, "cash-flow-statement-bulk", "cash-flow-statement-bulk", params) } // CashFlowStatementGrowth retrieves worldwide bulk cash-flow growth for one @@ -317,11 +297,7 @@ func (n *BulkNamespace) CashFlowStatementGrowth(ctx context.Context, q BulkState if err != nil { return nil, err } - var out []BulkCashFlowStatementGrowth - if err := n.client.getJSON(ctx, "cash-flow-statement-growth-bulk", "cash-flow-statement-growth-bulk", params, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkCashFlowStatementGrowth](ctx, n.client, "cash-flow-statement-growth-bulk", "cash-flow-statement-growth-bulk", params) } // Eod retrieves worldwide bulk end-of-day prices for one required date. @@ -332,9 +308,5 @@ func (n *BulkNamespace) Eod(ctx context.Context, q BulkEodQuery) ([]BulkEodBar, if err != nil { return nil, err } - var out []BulkEodBar - if err := n.client.getJSON(ctx, "eod-bulk", "eod-bulk", params, &out); err != nil { - return nil, err - } - return out, nil + return getCSV[BulkEodBar](ctx, n.client, "eod-bulk", "eod-bulk", params) } diff --git a/sdk/go/bulk_test.go b/sdk/go/bulk_test.go index 5f31de4f..10334288 100644 --- a/sdk/go/bulk_test.go +++ b/sdk/go/bulk_test.go @@ -25,13 +25,13 @@ var bulkRoutes = map[string]string{ "/router/stable/ratios-ttm-bulk": "bulk_financial_ratios_ttm.csv", "/router/stable/peers-bulk": "bulk_stock_peers.csv", "/router/stable/earnings-surprises-bulk?year=4294967295": "bulk_earnings_surprises.csv", - "/router/stable/income-statement-bulk?year=2026&period=Q1": "bulk_income_statements.json", - "/router/stable/income-statement-growth-bulk?year=2026&period=Q2": "bulk_income_statement_growth.json", - "/router/stable/balance-sheet-statement-bulk?year=2026&period=Q3": "bulk_balance_sheet_statements.json", - "/router/stable/balance-sheet-statement-growth-bulk?year=2026&period=Q4": "bulk_balance_sheet_statement_growth.json", - "/router/stable/cash-flow-statement-bulk?year=4294967295&period=FY": "bulk_cash_flow_statements.json", - "/router/stable/cash-flow-statement-growth-bulk?year=2025&period=FY": "bulk_cash_flow_statement_growth.json", - "/router/stable/eod-bulk?date=2024-10-22": "bulk_eod.json", + "/router/stable/income-statement-bulk?year=2026&period=Q1": "bulk_income_statements.csv", + "/router/stable/income-statement-growth-bulk?year=2026&period=Q2": "bulk_income_statement_growth.csv", + "/router/stable/balance-sheet-statement-bulk?year=2026&period=Q3": "bulk_balance_sheet_statements.csv", + "/router/stable/balance-sheet-statement-growth-bulk?year=2026&period=Q4": "bulk_balance_sheet_statement_growth.csv", + "/router/stable/cash-flow-statement-bulk?year=4294967295&period=FY": "bulk_cash_flow_statements.csv", + "/router/stable/cash-flow-statement-growth-bulk?year=2025&period=FY": "bulk_cash_flow_statement_growth.csv", + "/router/stable/eod-bulk?date=2024-10-22": "bulk_eod.csv", } func bulkRouter(t *testing.T) http.HandlerFunc { @@ -102,31 +102,31 @@ func TestBulkMethodsUseExactPathsAndWireParameterOrder(t *testing.T) { t.Fatalf("EarningsSurprises = %+v, %v", surprises, err) } income, err := client.Bulk.IncomeStatements(ctx, NewBulkStatementQuery(2026, FiscalPeriodQ1)) - if err != nil || len(income) != 1 || income[0].Revenue != "33644000000" { + if err != nil || len(income) != 2 || income[0].Revenue != "251641000000" { t.Fatalf("IncomeStatements = %+v, %v", income, err) } incomeGrowth, err := client.Bulk.IncomeStatementGrowth(ctx, NewBulkStatementQuery(2026, FiscalPeriodQ2)) - if err != nil || len(incomeGrowth) != 1 || incomeGrowth[0].GrowthEbit != "1" { + if err != nil || len(incomeGrowth) != 2 || incomeGrowth[0].GrowthRevenue != "-0.08220846812871789" { t.Fatalf("IncomeStatementGrowth = %+v, %v", incomeGrowth, err) } balance, err := client.Bulk.BalanceSheetStatements(ctx, NewBulkStatementQuery(2026, FiscalPeriodQ3)) - if err != nil || len(balance) != 1 || balance[0].Symbol != "MTLRP.ME" { + if err != nil || len(balance) != 2 || balance[1].Symbol != "0002.KL" { t.Fatalf("BalanceSheetStatements = %+v, %v", balance, err) } balanceGrowth, err := client.Bulk.BalanceSheetStatementGrowth(ctx, NewBulkStatementQuery(2026, FiscalPeriodQ4)) - if err != nil || len(balanceGrowth) != 1 || balanceGrowth[0].GrowthShortTermInvestments != "0" { + if err != nil || len(balanceGrowth) != 2 || balanceGrowth[0].GrowthShortTermInvestments != "0.3981363245708905" { t.Fatalf("BalanceSheetStatementGrowth = %+v, %v", balanceGrowth, err) } cashFlow, err := client.Bulk.CashFlowStatements(ctx, NewBulkStatementQuery(4_294_967_295, FiscalPeriodFullYear)) - if err != nil || len(cashFlow) != 1 || cashFlow[0].NetIncome != "0" { + if err != nil || len(cashFlow) != 2 || cashFlow[0].NetIncome != "14932000000" { t.Fatalf("CashFlowStatements = %+v, %v", cashFlow, err) } cashGrowth, err := client.Bulk.CashFlowStatementGrowth(ctx, NewBulkStatementQuery(2025, FiscalPeriodFullYear)) - if err != nil || len(cashGrowth) != 1 || cashGrowth[0].GrowthNetDebtIssuance != "1" { + if err != nil || len(cashGrowth) != 2 || cashGrowth[0].GrowthNetIncome != "-0.04191152728446884" { t.Fatalf("CashFlowStatementGrowth = %+v, %v", cashGrowth, err) } eod, err := client.Bulk.Eod(ctx, NewBulkEodQuery(mustParseDate(t, "2024-10-22"))) - if err != nil || len(eod) != 1 || eod[0].Volume != "920904" { + if err != nil || len(eod) != 2 || eod[1].Close != "0.91741" { t.Fatalf("Eod = %+v, %v", eod, err) } From 9da203e980baa810a55c3d229d7ecb6f17815c94 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:55:12 +0800 Subject: [PATCH 17/31] test(libfmp): rewrite bulk income suite over live CSV fixtures Rewrites the bulk income statement and income growth response suite over the live CSV fixtures through the shared CSV helpers: per-cell round trip with exact field counts, every column required with no member accepting an empty cell, empty and header-only bodies, identity representations with invalid date and datetime cells, and the exact acronym wire names. Adds first_row and assert_cell_failure helpers to the shared support. The JSON-model assertions are dropped because these routes no longer answer JSON. Relates to #376 --- crates/libfmp/tests/bulk_income_responses.rs | 215 +++++-------------- crates/libfmp/tests/support/bulk_csv.rs | 22 ++ 2 files changed, 74 insertions(+), 163 deletions(-) diff --git a/crates/libfmp/tests/bulk_income_responses.rs b/crates/libfmp/tests/bulk_income_responses.rs index 0b2af68e..6fb1d9b8 100644 --- a/crates/libfmp/tests/bulk_income_responses.rs +++ b/crates/libfmp/tests/bulk_income_responses.rs @@ -1,94 +1,76 @@ -use serde::{Serialize, de::DeserializeOwned}; -use serde_json::{Map, Value, json}; +#[path = "support/bulk_csv.rs"] +mod bulk_csv; +#[allow(dead_code)] // The CSV helpers use only the fixture executor. +mod support; use libfmp::{ - query::FiscalPeriod, - responses::bulk::{BulkIncomeStatement, BulkIncomeStatementGrowth}, + endpoints::bulk::{BulkStatementQuery, bulk_income_statement_growth, bulk_income_statements}, + error::DecodeErrorKind, + query::{FiscalPeriod, Year}, }; -const INCOME: &[u8] = include_bytes!("fixtures/bulk_income_statements.json"); -const GROWTH: &[u8] = include_bytes!("fixtures/bulk_income_statement_growth.json"); +use bulk_csv::{ + assert_cell_failure, assert_empty_bodies, assert_members, assert_round_trip, decode, first_row, + with_cells, +}; -#[test] -fn exact_outer_md_fixtures_round_trip_with_exact_unique_key_sets() { - assert_exact::(INCOME, 39); - assert_exact::(GROWTH, 34); +const INCOME: &[u8] = include_bytes!("fixtures/bulk_income_statements.csv"); +const GROWTH: &[u8] = include_bytes!("fixtures/bulk_income_statement_growth.csv"); + +fn query() -> BulkStatementQuery { + BulkStatementQuery::new(Year(2024), FiscalPeriod::FullYear) } #[test] -fn every_documented_field_is_required_non_null_and_unknown_fields_are_tolerated() { - assert_required::(INCOME); - assert_required::(GROWTH); +fn live_csv_fixtures_round_trip_every_cell_with_exact_field_counts() { + assert_round_trip(&bulk_income_statements(query()), INCOME, 39); + assert_round_trip(&bulk_income_statement_growth(query()), GROWTH, 34); } #[test] -fn every_documented_metric_preserves_numeric_text_and_rejects_json_numbers() { - assert_all_numbers_rejected::( - INCOME, - &[ - "date", - "symbol", - "reportedCurrency", - "cik", - "filingDate", - "acceptedDate", - "fiscalYear", - "period", - ], - ); - assert_all_numbers_rejected::( - GROWTH, - &["symbol", "date", "fiscalYear", "period", "reportedCurrency"], - ); - - let income = rows::(INCOME).remove(0); - assert_eq!(income.revenue.as_str(), "33644000000"); - assert_eq!(income.cost_of_revenue.as_str(), "0"); - assert_eq!( - income.total_other_income_expenses_net.as_str(), - "-7392000000" - ); - assert_eq!(income.eps.as_str(), "0.62"); - assert_eq!(income.weighted_average_shs_out.as_str(), "22735483871"); +fn every_column_is_required_and_no_member_accepts_an_empty_cell() { + assert_members(&bulk_income_statements(query()), INCOME, &[]); + assert_members(&bulk_income_statement_growth(query()), GROWTH, &[]); +} - let growth = rows::(GROWTH).remove(0); - assert_eq!(growth.growth_ebit.as_str(), "1"); - assert_eq!(growth.growth_cost_of_revenue.as_str(), "0"); - assert_eq!(growth.growth_other_expenses.as_str(), "-0.9860376183912135"); - assert_eq!( - growth.growth_operating_income.as_str(), - "-0.018874787810201278" - ); +#[test] +fn empty_and_header_only_bodies_decode_to_no_rows() { + assert_empty_bodies(&bulk_income_statements(query()), INCOME); + assert_empty_bodies(&bulk_income_statement_growth(query()), GROWTH); } #[test] -fn identity_fields_are_narrow_and_preserve_documented_representations() { - let income = rows::(INCOME).remove(0); +fn identity_fields_are_narrow_and_preserve_provider_representations() { + let income = decode(&bulk_income_statements(query()), INCOME) + .unwrap() + .remove(0); assert_eq!(income.symbol.as_str(), "000001.SZ"); assert_eq!(income.reported_currency.as_str(), "CNY"); assert_eq!(income.cik.as_str(), "0000000000"); - assert_eq!(income.date.to_string(), "2025-03-31"); - assert_eq!(income.filing_date.to_string(), "2025-03-31"); - assert_eq!(income.accepted_date.to_string(), "2025-03-31 00:00:00"); - assert_eq!(income.fiscal_year.as_str(), "2025"); - assert_eq!(income.period, FiscalPeriod::Q1); - - let mut invalid_date = source_row(INCOME); - invalid_date.insert("date".into(), json!("2025-03-31 00:00:00")); - assert!(serde_json::from_value::(Value::Object(invalid_date)).is_err()); - let mut invalid_datetime = source_row(INCOME); - invalid_datetime.insert("acceptedDate".into(), json!("2025-03-31")); - assert!( - serde_json::from_value::(Value::Object(invalid_datetime)).is_err() - ); - let mut numeric_cik = source_row(INCOME); - numeric_cik.insert("cik".into(), json!(0)); - assert!(serde_json::from_value::(Value::Object(numeric_cik)).is_err()); + assert_eq!(income.date.to_string(), "2024-12-31"); + assert_eq!(income.accepted_date.to_string(), "2024-12-31 00:00:00"); + assert_eq!(income.fiscal_year.as_str(), "2024"); + assert_eq!(income.period, FiscalPeriod::FullYear); + assert_eq!(income.revenue.as_str(), "251641000000"); + + for (member, cell) in [ + ("date", "2024-12-31 00:00:00"), + ("acceptedDate", "2024-12-31"), + ] { + assert_cell_failure( + decode( + &bulk_income_statements(query()), + with_cells(INCOME, &[(member, cell)]), + ), + member, + DecodeErrorKind::InvalidValue, + ); + } } #[test] -fn acronym_hazards_keep_exact_documented_wire_names() { - let growth = serialized_row::(GROWTH); +fn acronym_hazards_keep_exact_provider_wire_names() { + let growth = first_row(&bulk_income_statement_growth(query()), GROWTH); for key in [ "growthEBITDA", "growthEPS", @@ -106,96 +88,3 @@ fn acronym_hazards_keep_exact_documented_wire_names() { assert!(!growth.contains_key(incorrect)); } } - -#[test] -fn both_contracts_require_bare_array_roots_and_accept_empty_arrays() { - assert_array_contract::(); - assert_array_contract::(); -} - -fn rows(fixture: &[u8]) -> Vec { - serde_json::from_slice(fixture).unwrap() -} - -fn source_row(fixture: &[u8]) -> Map { - serde_json::from_slice::(fixture).unwrap()[0] - .as_object() - .unwrap() - .clone() -} - -fn serialized_row(fixture: &[u8]) -> Map -where - T: DeserializeOwned + Serialize, -{ - serde_json::to_value(rows::(fixture)).unwrap()[0] - .as_object() - .unwrap() - .clone() -} - -fn assert_exact(fixture: &[u8], fields: usize) -where - T: DeserializeOwned + Serialize, -{ - let source: Value = serde_json::from_slice(fixture).unwrap(); - let row = source[0].as_object().unwrap(); - assert_eq!(row.len(), fields); - assert_eq!( - row.keys().collect::>().len(), - fields - ); - assert_eq!(serde_json::to_value(rows::(fixture)).unwrap(), source); -} - -fn assert_required(fixture: &[u8]) -where - T: DeserializeOwned, -{ - let row = source_row(fixture); - for field in row.keys() { - let mut missing = row.clone(); - missing.remove(field); - assert!( - serde_json::from_value::(Value::Object(missing)).is_err(), - "missing {field} unexpectedly decoded" - ); - - let mut null = row.clone(); - null.insert(field.clone(), Value::Null); - assert!( - serde_json::from_value::(Value::Object(null)).is_err(), - "null {field} unexpectedly decoded" - ); - } - - let mut future = row; - future.insert("futureField".into(), json!({"nested": true})); - assert!(serde_json::from_value::(Value::Object(future)).is_ok()); -} - -fn assert_all_numbers_rejected(fixture: &[u8], non_numeric: &[&str]) -where - T: DeserializeOwned, -{ - let row = source_row(fixture); - for field in row - .keys() - .filter(|field| !non_numeric.contains(&field.as_str())) - { - let mut numeric = row.clone(); - numeric.insert(field.clone(), json!(123.5)); - assert!( - serde_json::from_value::(Value::Object(numeric)).is_err(), - "JSON number unexpectedly decoded for {field}" - ); - } -} - -fn assert_array_contract() -where - T: DeserializeOwned, -{ - assert!(serde_json::from_slice::>(b"[]").unwrap().is_empty()); - assert!(serde_json::from_slice::>(b"{}").is_err()); -} diff --git a/crates/libfmp/tests/support/bulk_csv.rs b/crates/libfmp/tests/support/bulk_csv.rs index 9d32a347..fdfca99d 100644 --- a/crates/libfmp/tests/support/bulk_csv.rs +++ b/crates/libfmp/tests/support/bulk_csv.rs @@ -142,6 +142,28 @@ pub fn with_cells(fixture: &[u8], cells: &[(&str, &str)]) -> Vec { write(&header, &record) } +/// Decodes the fixture and returns its first row re-encoded as JSON members. +#[allow(dead_code)] // Only suites that check wire spellings use it. +pub fn first_row( + endpoint: &EndpointSpec>, + fixture: &[u8], +) -> serde_json::Map +where + Q: QueryParameters, + R: Serialize, +{ + match serde_json::to_value(&decode(endpoint, fixture).unwrap()[0]).unwrap() { + Value::Object(members) => members, + other => panic!("a row re-encodes as a JSON object, not {other}"), + } +} + +/// Asserts that a decode failed at `[0].member` with `kind`. +#[allow(dead_code)] // Only suites that probe individual cells use it. +pub fn assert_cell_failure(result: libfmp::Result, member: &str, kind: DecodeErrorKind) { + assert_failure(result, member, kind); +} + fn assert_failure(result: libfmp::Result, member: &str, kind: DecodeErrorKind) { let Err(error) = result else { panic!("{member}: expected a {kind:?} failure"); From fe3e29edb5cf3c351862389e25636cbccd0f7ac2 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:55:39 +0800 Subject: [PATCH 18/31] test(libfmp): rewrite bulk balance response suite over live CSV fixtures Rewrite the bulk balance sheet and balance growth response tests to run over the live CSV fixtures through the shared CSV helpers. Coverage now asserts per-cell round trip with exact field counts, requires every column with no member accepting an empty cell, checks empty and header-only bodies, verifies identity representations, and pins the exact provider typo and liabilities-equity wire names. Drop the JSON-model assertions because these routes no longer answer JSON. Relates to #376 --- crates/libfmp/tests/bulk_balance_responses.rs | 202 +++--------------- 1 file changed, 34 insertions(+), 168 deletions(-) diff --git a/crates/libfmp/tests/bulk_balance_responses.rs b/crates/libfmp/tests/bulk_balance_responses.rs index 2dc788c9..f7ac199a 100644 --- a/crates/libfmp/tests/bulk_balance_responses.rs +++ b/crates/libfmp/tests/bulk_balance_responses.rs @@ -1,97 +1,56 @@ -use serde::{Serialize, de::DeserializeOwned}; -use serde_json::{Map, Value, json}; +#[path = "support/bulk_csv.rs"] +mod bulk_csv; +#[allow(dead_code)] // The CSV helpers use only the fixture executor. +mod support; use libfmp::{ - query::FiscalPeriod, - responses::bulk::{BulkBalanceSheetStatement, BulkBalanceSheetStatementGrowth}, + endpoints::bulk::{ + BulkStatementQuery, bulk_balance_sheet_statement_growth, bulk_balance_sheet_statements, + }, + query::{FiscalPeriod, Year}, }; -const BALANCE: &[u8] = include_bytes!("fixtures/bulk_balance_sheet_statements.json"); -const GROWTH: &[u8] = include_bytes!("fixtures/bulk_balance_sheet_statement_growth.json"); +use bulk_csv::{assert_empty_bodies, assert_members, assert_round_trip, decode, first_row}; -#[test] -fn exact_outer_md_fixtures_round_trip_with_exact_unique_key_sets() { - assert_exact::(BALANCE, 61); - assert_exact::(GROWTH, 56); +const BALANCE: &[u8] = include_bytes!("fixtures/bulk_balance_sheet_statements.csv"); +const GROWTH: &[u8] = include_bytes!("fixtures/bulk_balance_sheet_statement_growth.csv"); + +fn query() -> BulkStatementQuery { + BulkStatementQuery::new(Year(2024), FiscalPeriod::FullYear) } #[test] -fn every_documented_field_is_required_non_null_and_unknown_fields_are_tolerated() { - assert_required::(BALANCE); - assert_required::(GROWTH); +fn live_csv_fixtures_round_trip_every_cell_with_exact_field_counts() { + assert_round_trip(&bulk_balance_sheet_statements(query()), BALANCE, 61); + assert_round_trip(&bulk_balance_sheet_statement_growth(query()), GROWTH, 56); } #[test] -fn every_documented_metric_preserves_numeric_text_and_rejects_json_numbers() { - assert_all_numbers_rejected::( - BALANCE, - &[ - "date", - "symbol", - "reportedCurrency", - "cik", - "filingDate", - "acceptedDate", - "fiscalYear", - "period", - ], - ); - assert_all_numbers_rejected::( - GROWTH, - &["symbol", "date", "fiscalYear", "period", "reportedCurrency"], - ); - - let balance = rows::(BALANCE).remove(0); - assert_eq!(balance.total_assets.as_str(), "247871857000"); - assert_eq!(balance.retained_earnings.as_str(), "-5066509000"); - assert_eq!(balance.other_assets.as_str(), "0"); - assert_eq!(balance.net_debt.as_str(), "183764862000"); - - let growth = rows::(GROWTH).remove(0); - assert_eq!( - growth.growth_cash_and_cash_equivalents.as_str(), - "0.09574482145872953" - ); - assert_eq!(growth.growth_short_term_investments.as_str(), "0"); - assert_eq!( - growth.growth_total_payables.as_str(), - "-0.12022416350749959" - ); +fn every_column_is_required_and_no_member_accepts_an_empty_cell() { + assert_members(&bulk_balance_sheet_statements(query()), BALANCE, &[]); + assert_members(&bulk_balance_sheet_statement_growth(query()), GROWTH, &[]); } #[test] -fn identity_fields_are_narrow_and_preserve_documented_representations() { - let balance = rows::(BALANCE).remove(0); - assert_eq!(balance.symbol.as_str(), "MTLRP.ME"); - assert_eq!(balance.reported_currency.as_str(), "RUB"); - assert_eq!(balance.cik.as_str(), "0000000000"); - assert_eq!(balance.date.to_string(), "2025-03-31"); - assert_eq!(balance.filing_date.to_string(), "2025-05-31"); - assert_eq!(balance.accepted_date.to_string(), "2025-03-31 07:00:00"); - assert_eq!(balance.fiscal_year.as_str(), "2025"); - assert_eq!(balance.period, FiscalPeriod::Q1); +fn empty_and_header_only_bodies_decode_to_no_rows() { + assert_empty_bodies(&bulk_balance_sheet_statements(query()), BALANCE); + assert_empty_bodies(&bulk_balance_sheet_statement_growth(query()), GROWTH); +} - let mut invalid_date = source_row(BALANCE); - invalid_date.insert("date".into(), json!("2025-03-31 00:00:00")); - assert!( - serde_json::from_value::(Value::Object(invalid_date)).is_err() - ); - let mut invalid_datetime = source_row(BALANCE); - invalid_datetime.insert("acceptedDate".into(), json!("2025-03-31")); - assert!( - serde_json::from_value::(Value::Object(invalid_datetime)) - .is_err() - ); - let mut numeric_cik = source_row(BALANCE); - numeric_cik.insert("cik".into(), json!(0)); - assert!( - serde_json::from_value::(Value::Object(numeric_cik)).is_err() - ); +#[test] +fn identity_fields_are_narrow_and_preserve_provider_representations() { + let balance = decode(&bulk_balance_sheet_statements(query()), BALANCE).unwrap(); + assert_eq!(balance[1].symbol.as_str(), "0002.KL"); + assert_eq!(balance[1].reported_currency.as_str(), "MYR"); + assert_eq!(balance[1].cik.as_str(), "0000000000"); + assert_eq!(balance[1].accepted_date.to_string(), "2025-06-30 00:00:00"); + assert_eq!(balance[1].fiscal_year.as_str(), "2024"); + assert_eq!(balance[0].total_assets.as_str(), "5769270000000"); } #[test] fn provider_typo_and_liabilities_equity_wording_keep_exact_wire_names() { - let growth = serialized_row::(GROWTH); + let growth = first_row(&bulk_balance_sheet_statement_growth(query()), GROWTH); for key in [ "growthOthertotalStockholdersEquity", "growthTotalLiabilitiesAndStockholdersEquity", @@ -105,96 +64,3 @@ fn provider_typo_and_liabilities_equity_wording_keep_exact_wire_names() { assert!(!growth.contains_key(incorrect)); } } - -#[test] -fn both_contracts_require_bare_array_roots_and_accept_empty_arrays() { - assert_array_contract::(); - assert_array_contract::(); -} - -fn rows(fixture: &[u8]) -> Vec { - serde_json::from_slice(fixture).unwrap() -} - -fn source_row(fixture: &[u8]) -> Map { - serde_json::from_slice::(fixture).unwrap()[0] - .as_object() - .unwrap() - .clone() -} - -fn serialized_row(fixture: &[u8]) -> Map -where - T: DeserializeOwned + Serialize, -{ - serde_json::to_value(rows::(fixture)).unwrap()[0] - .as_object() - .unwrap() - .clone() -} - -fn assert_exact(fixture: &[u8], fields: usize) -where - T: DeserializeOwned + Serialize, -{ - let source: Value = serde_json::from_slice(fixture).unwrap(); - let row = source[0].as_object().unwrap(); - assert_eq!(row.len(), fields); - assert_eq!( - row.keys().collect::>().len(), - fields - ); - assert_eq!(serde_json::to_value(rows::(fixture)).unwrap(), source); -} - -fn assert_required(fixture: &[u8]) -where - T: DeserializeOwned, -{ - let row = source_row(fixture); - for field in row.keys() { - let mut missing = row.clone(); - missing.remove(field); - assert!( - serde_json::from_value::(Value::Object(missing)).is_err(), - "missing {field} unexpectedly decoded" - ); - - let mut null = row.clone(); - null.insert(field.clone(), Value::Null); - assert!( - serde_json::from_value::(Value::Object(null)).is_err(), - "null {field} unexpectedly decoded" - ); - } - - let mut future = row; - future.insert("futureField".into(), json!({"nested": true})); - assert!(serde_json::from_value::(Value::Object(future)).is_ok()); -} - -fn assert_all_numbers_rejected(fixture: &[u8], non_numeric: &[&str]) -where - T: DeserializeOwned, -{ - let row = source_row(fixture); - for field in row - .keys() - .filter(|field| !non_numeric.contains(&field.as_str())) - { - let mut numeric = row.clone(); - numeric.insert(field.clone(), json!(123.5)); - assert!( - serde_json::from_value::(Value::Object(numeric)).is_err(), - "JSON number unexpectedly decoded for {field}" - ); - } -} - -fn assert_array_contract() -where - T: DeserializeOwned, -{ - assert!(serde_json::from_slice::>(b"[]").unwrap().is_empty()); - assert!(serde_json::from_slice::>(b"{}").is_err()); -} From 1d9031a0f75046387d9100fe46ad7dfbe6fa22b6 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:56:03 +0800 Subject: [PATCH 19/31] test(libfmp): rewrite bulk cash and eod suite over live CSV fixtures Rewrites the bulk cash flow, cash flow growth and eod response suite over the live CSV fixtures through the shared CSV helpers: per-cell round trip with exact field counts, every column required with no member accepting an empty cell, empty and header-only bodies, identity and price representations, and the exact Activites typo wire names. The JSON-model assertions are dropped because these routes no longer answer JSON. Issue #376 --- .../libfmp/tests/bulk_cash_eod_responses.rs | 229 ++++-------------- 1 file changed, 48 insertions(+), 181 deletions(-) diff --git a/crates/libfmp/tests/bulk_cash_eod_responses.rs b/crates/libfmp/tests/bulk_cash_eod_responses.rs index 64aa3b7e..d9a36551 100644 --- a/crates/libfmp/tests/bulk_cash_eod_responses.rs +++ b/crates/libfmp/tests/bulk_cash_eod_responses.rs @@ -1,102 +1,71 @@ -use serde::{Serialize, de::DeserializeOwned}; -use serde_json::{Map, Value, json}; +#[path = "support/bulk_csv.rs"] +mod bulk_csv; +#[allow(dead_code)] // The CSV helpers use only the fixture executor. +mod support; use libfmp::{ - query::FiscalPeriod, - responses::bulk::{BulkCashFlowStatement, BulkCashFlowStatementGrowth, BulkEodBar}, + endpoints::bulk::{ + BulkStatementQuery, bulk_cash_flow_statement_growth, bulk_cash_flow_statements, bulk_eod, + }, + query::{FiscalPeriod, Year}, + types::Date, }; -const CASH_FLOW: &[u8] = include_bytes!("fixtures/bulk_cash_flow_statements.json"); -const GROWTH: &[u8] = include_bytes!("fixtures/bulk_cash_flow_statement_growth.json"); -const EOD: &[u8] = include_bytes!("fixtures/bulk_eod.json"); +use bulk_csv::{assert_empty_bodies, assert_members, assert_round_trip, decode, first_row}; -#[test] -fn exact_outer_md_fixtures_round_trip_with_exact_unique_key_sets() { - assert_exact::(CASH_FLOW, 47); - assert_exact::(GROWTH, 42); - assert_exact::(EOD, 8); +const CASH_FLOW: &[u8] = include_bytes!("fixtures/bulk_cash_flow_statements.csv"); +const GROWTH: &[u8] = include_bytes!("fixtures/bulk_cash_flow_statement_growth.csv"); +const EOD: &[u8] = include_bytes!("fixtures/bulk_eod.csv"); + +fn query() -> BulkStatementQuery { + BulkStatementQuery::new(Year(2024), FiscalPeriod::Q1) } -#[test] -fn every_documented_field_is_required_non_null_and_unknown_fields_are_tolerated() { - assert_required::(CASH_FLOW); - assert_required::(GROWTH); - assert_required::(EOD); +fn day() -> Date { + Date::parse("2025-06-02").unwrap() } #[test] -fn every_documented_metric_preserves_numeric_text_and_rejects_json_numbers() { - assert_all_numbers_rejected::( - CASH_FLOW, - &[ - "date", - "symbol", - "reportedCurrency", - "cik", - "filingDate", - "acceptedDate", - "fiscalYear", - "period", - ], - ); - assert_all_numbers_rejected::( - GROWTH, - &["symbol", "date", "fiscalYear", "period", "reportedCurrency"], - ); - assert_all_numbers_rejected::(EOD, &["symbol", "date"]); - - let cash_flow = rows::(CASH_FLOW).remove(0); - assert_eq!(cash_flow.other_non_cash_items.as_str(), "162946000000"); - assert_eq!(cash_flow.purchases_of_investments.as_str(), "-227916000000"); - assert_eq!(cash_flow.net_income.as_str(), "0"); +fn live_csv_fixtures_round_trip_every_cell_with_exact_field_counts() { + assert_round_trip(&bulk_cash_flow_statements(query()), CASH_FLOW, 47); + assert_round_trip(&bulk_cash_flow_statement_growth(query()), GROWTH, 42); + assert_round_trip(&bulk_eod(day().into()), EOD, 8); +} - let growth = rows::(GROWTH).remove(0); - assert_eq!( - growth - .growth_net_cash_used_provided_by_financing_activities - .as_str(), - "-3.2122934677858628" - ); - assert_eq!(growth.growth_net_debt_issuance.as_str(), "1"); +#[test] +fn every_column_is_required_and_no_member_accepts_an_empty_cell() { + assert_members(&bulk_cash_flow_statements(query()), CASH_FLOW, &[]); + assert_members(&bulk_cash_flow_statement_growth(query()), GROWTH, &[]); + assert_members(&bulk_eod(day().into()), EOD, &[]); +} - let eod = rows::(EOD).remove(0); - assert_eq!(eod.open.as_str(), "2.67"); - assert_eq!(eod.low.as_str(), "2.7"); - assert_eq!(eod.volume.as_str(), "920904"); +#[test] +fn empty_and_header_only_bodies_decode_to_no_rows() { + assert_empty_bodies(&bulk_cash_flow_statements(query()), CASH_FLOW); + assert_empty_bodies(&bulk_cash_flow_statement_growth(query()), GROWTH); + assert_empty_bodies(&bulk_eod(day().into()), EOD); } #[test] -fn identity_fields_are_narrow_and_preserve_documented_representations() { - let cash_flow = rows::(CASH_FLOW).remove(0); - assert_eq!(cash_flow.symbol.as_str(), "000001.SZ"); - assert_eq!(cash_flow.reported_currency.as_str(), "CNY"); +fn identity_fields_and_prices_preserve_provider_representations() { + let cash_flow = decode(&bulk_cash_flow_statements(query()), CASH_FLOW) + .unwrap() + .remove(0); assert_eq!(cash_flow.cik.as_str(), "0000000000"); - assert_eq!(cash_flow.date.to_string(), "2025-03-31"); - assert_eq!(cash_flow.filing_date.to_string(), "2025-03-31"); - assert_eq!(cash_flow.accepted_date.to_string(), "2025-03-31 00:00:00"); - assert_eq!(cash_flow.fiscal_year.as_str(), "2025"); + assert_eq!(cash_flow.accepted_date.to_string(), "2024-03-30 20:00:00"); assert_eq!(cash_flow.period, FiscalPeriod::Q1); + assert_eq!(cash_flow.net_income.as_str(), "14932000000"); - let eod = rows::(EOD).remove(0); - assert_eq!(eod.symbol.as_str(), "EGS745W1C011.CA"); - assert_eq!(eod.date.to_string(), "2024-10-22"); - - let mut numeric_cik = source_row(CASH_FLOW); - numeric_cik.insert("cik".into(), json!(0)); - assert!(serde_json::from_value::(Value::Object(numeric_cik)).is_err()); - let mut invalid_datetime = source_row(CASH_FLOW); - invalid_datetime.insert("acceptedDate".into(), json!("2025-03-31")); - assert!( - serde_json::from_value::(Value::Object(invalid_datetime)).is_err() - ); - let mut invalid_eod_date = source_row(EOD); - invalid_eod_date.insert("date".into(), json!("2024-10-22 00:00:00")); - assert!(serde_json::from_value::(Value::Object(invalid_eod_date)).is_err()); + let eod = decode(&bulk_eod(day().into()), EOD).unwrap(); + assert_eq!(eod[1].symbol.as_str(), "HKDCNH"); + assert_eq!(eod[1].date, day()); + assert_eq!(eod[1].open.as_str(), "0.91858"); + assert_eq!(eod[1].volume.as_str(), "0"); } #[test] -fn provider_activity_typos_keep_exact_wire_names_with_corrected_rust_fields() { - let growth = serialized_row::(GROWTH); +fn activities_typos_keep_exact_provider_wire_names() { + let growth = first_row(&bulk_cash_flow_statement_growth(query()), GROWTH); for key in [ "growthNetCashProvidedByOperatingActivites", "growthOtherInvestingActivites", @@ -106,107 +75,5 @@ fn provider_activity_typos_keep_exact_wire_names_with_corrected_rust_fields() { ] { assert!(growth.contains_key(key), "missing exact wire key {key}"); } - for incorrect in [ - "growthNetCashProvidedByOperatingActivities", - "growthOtherInvestingActivities", - "growthNetCashUsedForInvestingActivities", - "growthOtherFinancingActivities", - "growthNetCashUsedProvidedByFinancingActivites", - ] { - assert!(!growth.contains_key(incorrect)); - } -} - -#[test] -fn all_three_contracts_require_bare_array_roots_and_accept_empty_arrays() { - assert_array_contract::(); - assert_array_contract::(); - assert_array_contract::(); -} - -fn rows(fixture: &[u8]) -> Vec { - serde_json::from_slice(fixture).unwrap() -} - -fn source_row(fixture: &[u8]) -> Map { - serde_json::from_slice::(fixture).unwrap()[0] - .as_object() - .unwrap() - .clone() -} - -fn serialized_row(fixture: &[u8]) -> Map -where - T: DeserializeOwned + Serialize, -{ - serde_json::to_value(rows::(fixture)).unwrap()[0] - .as_object() - .unwrap() - .clone() -} - -fn assert_exact(fixture: &[u8], fields: usize) -where - T: DeserializeOwned + Serialize, -{ - let source: Value = serde_json::from_slice(fixture).unwrap(); - let row = source[0].as_object().unwrap(); - assert_eq!(row.len(), fields); - assert_eq!( - row.keys().collect::>().len(), - fields - ); - assert_eq!(serde_json::to_value(rows::(fixture)).unwrap(), source); -} - -fn assert_required(fixture: &[u8]) -where - T: DeserializeOwned, -{ - let row = source_row(fixture); - for field in row.keys() { - let mut missing = row.clone(); - missing.remove(field); - assert!( - serde_json::from_value::(Value::Object(missing)).is_err(), - "missing {field} unexpectedly decoded" - ); - - let mut null = row.clone(); - null.insert(field.clone(), Value::Null); - assert!( - serde_json::from_value::(Value::Object(null)).is_err(), - "null {field} unexpectedly decoded" - ); - } - - let mut future = row; - future.insert("futureField".into(), json!({"nested": true})); - assert!(serde_json::from_value::(Value::Object(future)).is_ok()); -} - -fn assert_all_numbers_rejected(fixture: &[u8], non_numeric: &[&str]) -where - T: DeserializeOwned, -{ - let row = source_row(fixture); - for field in row - .keys() - .filter(|field| !non_numeric.contains(&field.as_str())) - { - let mut numeric = row.clone(); - numeric.insert(field.clone(), json!(123.5)); - assert!( - serde_json::from_value::(Value::Object(numeric)).is_err(), - "JSON number unexpectedly decoded for {field}" - ); - } -} - -fn assert_array_contract() -where - T: DeserializeOwned, -{ - assert!(serde_json::from_slice::>(b"[]").unwrap().is_empty()); - assert!(serde_json::from_slice::>(b"{}").is_err()); + assert!(!growth.contains_key("growthOtherInvestingActivities")); } From 2ca4f9412551d08dee42372085815d17089b010a Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:57:19 +0800 Subject: [PATCH 20/31] test(sdk/go): decode statement and eod bulk parity from CSV fixtures Switch the Go statement and EOD bulk parity tests to decode the shared live CSV fixtures instead of JSON. Each assertion now exercises a per-cell round trip, exact member counts, identity representations (ten-zero CIK, naive accepted date, period), and provider typo wire names. Delete the seven obsolete statement and eod JSON fixtures that are no longer referenced. Extend TestEveryFixtureIsReferencedByAGoTest so it also requires every .csv fixture to be named by at least one Go test, alongside the existing .json check. Refs #376 --- .../bulk_balance_sheet_statement_growth.json | 60 -------------- .../bulk_balance_sheet_statements.json | 65 --------------- .../bulk_cash_flow_statement_growth.json | 46 ----------- .../fixtures/bulk_cash_flow_statements.json | 51 ------------ crates/libfmp/tests/fixtures/bulk_eod.json | 12 --- .../bulk_income_statement_growth.json | 38 --------- .../fixtures/bulk_income_statements.json | 43 ---------- sdk/go/bulk_parity_test.go | 81 +++++++------------ sdk/go/fixtures_test.go | 4 +- 9 files changed, 32 insertions(+), 368 deletions(-) delete mode 100644 crates/libfmp/tests/fixtures/bulk_balance_sheet_statement_growth.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_balance_sheet_statements.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_cash_flow_statement_growth.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_cash_flow_statements.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_eod.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_income_statement_growth.json delete mode 100644 crates/libfmp/tests/fixtures/bulk_income_statements.json diff --git a/crates/libfmp/tests/fixtures/bulk_balance_sheet_statement_growth.json b/crates/libfmp/tests/fixtures/bulk_balance_sheet_statement_growth.json deleted file mode 100644 index b9e9cf79..00000000 --- a/crates/libfmp/tests/fixtures/bulk_balance_sheet_statement_growth.json +++ /dev/null @@ -1,60 +0,0 @@ -[ - { - "date": "2025-03-31", - "fiscalYear": "2025", - "growthAccountPayables": "0", - "growthAccountsReceivables": "0", - "growthAccruedExpenses": "0", - "growthAccumulatedOtherComprehensiveIncomeLoss": "0", - "growthAdditionalPaidInCapital": "0", - "growthCapitalLeaseObligationsCurrent": "0", - "growthCashAndCashEquivalents": "0.09574482145872953", - "growthCashAndShortTermInvestments": "0.09574482145872953", - "growthCommonStock": "0", - "growthDeferredRevenue": "0", - "growthDeferredRevenueNonCurrent": "0", - "growthDeferredTaxLiabilitiesNonCurrent": "0", - "growthGoodwill": "0", - "growthGoodwillAndIntangibleAssets": "-0.01477618426770969", - "growthIntangibleAssets": "-0.03270278935556268", - "growthInventory": "0", - "growthLongTermDebt": "0", - "growthLongTermInvestments": "-0.0774117797082201", - "growthMinorityInterest": "0", - "growthNetDebt": "-0.09574482145872953", - "growthNetReceivables": "0", - "growthOtherAssets": "0.001488576544346165", - "growthOtherCurrentAssets": "0", - "growthOtherCurrentLiabilities": "0.12022416350749959", - "growthOtherLiabilities": "-0.0005084911577141635", - "growthOtherNonCurrentAssets": "0.07678934705504345", - "growthOtherNonCurrentLiabilities": "0", - "growthOtherPayables": "-0.12022416350749959", - "growthOtherReceivables": "0", - "growthOthertotalStockholdersEquity": "-0.0035208940994345805", - "growthPreferredStock": "0", - "growthPrepaids": "0", - "growthPropertyPlantEquipmentNet": "-0.06373337231398918", - "growthRetainedEarnings": "0.049325752755485314", - "growthShortTermDebt": "0", - "growthShortTermInvestments": "0", - "growthTaxAssets": "0", - "growthTaxPayables": "-0.0279424216765453", - "growthTotalAssets": "0.001488576544346165", - "growthTotalCurrentAssets": "0.09574482145872953", - "growthTotalCurrentLiabilities": "0", - "growthTotalDebt": "0", - "growthTotalEquity": "0.022774946346510602", - "growthTotalInvestments": "-0.0774117797082201", - "growthTotalLiabilities": "-0.0005084911577141635", - "growthTotalLiabilitiesAndStockholdersEquity": "0.001488576544346165", - "growthTotalNonCurrentAssets": "-0.01112505367669385", - "growthTotalNonCurrentLiabilities": "0", - "growthTotalPayables": "-0.12022416350749959", - "growthTotalStockholdersEquity": "0.022774946346510602", - "growthTreasuryStock": "0", - "period": "Q1", - "reportedCurrency": "CNY", - "symbol": "000001.SZ" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_balance_sheet_statements.json b/crates/libfmp/tests/fixtures/bulk_balance_sheet_statements.json deleted file mode 100644 index 92df6b8f..00000000 --- a/crates/libfmp/tests/fixtures/bulk_balance_sheet_statements.json +++ /dev/null @@ -1,65 +0,0 @@ -[ - { - "acceptedDate": "2025-03-31 07:00:00", - "accountPayables": "3861497000", - "accountsReceivables": "9666577000", - "accruedExpenses": "0", - "accumulatedOtherComprehensiveIncomeLoss": "0", - "additionalPaidInCapital": "6023340000", - "capitalLeaseObligations": "0", - "capitalLeaseObligationsCurrent": "0", - "capitalLeaseObligationsNonCurrent": "0", - "cashAndCashEquivalents": "1985000", - "cashAndShortTermInvestments": "1985000", - "cik": "0000000000", - "commonStock": "5550277000", - "date": "2025-03-31", - "deferredRevenue": "0", - "deferredRevenueNonCurrent": "0", - "deferredTaxLiabilitiesNonCurrent": "737391000", - "filingDate": "2025-05-31", - "fiscalYear": "2025", - "goodwill": "0", - "goodwillAndIntangibleAssets": "5665000", - "intangibleAssets": "5665000", - "inventory": "4520000", - "longTermDebt": "178923999000", - "longTermInvestments": "237373355000", - "minorityInterest": "0", - "netDebt": "183764862000", - "netReceivables": "9666577000", - "otherAssets": "0", - "otherCurrentAssets": "27293000", - "otherCurrentLiabilities": "146647000", - "otherLiabilities": "0", - "otherNonCurrentAssets": "0", - "otherNonCurrentLiabilities": "52574304000", - "otherPayables": "0", - "otherReceivables": "0", - "otherTotalStockholdersEquity": "0", - "period": "Q1", - "preferredStock": "0", - "prepaids": "0", - "propertyPlantEquipmentNet": "194000", - "reportedCurrency": "RUB", - "retainedEarnings": "-5066509000", - "shortTermDebt": "4842848000", - "shortTermInvestments": "0", - "symbol": "MTLRP.ME", - "taxAssets": "791813000", - "taxPayables": "2484576000", - "totalAssets": "247871857000", - "totalCurrentAssets": "9700830000", - "totalCurrentLiabilities": "8851455000", - "totalDebt": "183766847000", - "totalEquity": "6784622000", - "totalInvestments": "237373355000", - "totalLiabilities": "244087635000", - "totalLiabilitiesAndTotalEquity": "247871857000", - "totalNonCurrentAssets": "238171027000", - "totalNonCurrentLiabilities": "232235780000", - "totalPayables": "3861497000", - "totalStockholdersEquity": "6784622000", - "treasuryStock": "0" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_cash_flow_statement_growth.json b/crates/libfmp/tests/fixtures/bulk_cash_flow_statement_growth.json deleted file mode 100644 index c8ffa81a..00000000 --- a/crates/libfmp/tests/fixtures/bulk_cash_flow_statement_growth.json +++ /dev/null @@ -1,46 +0,0 @@ -[ - { - "date": "2025-03-31", - "fiscalYear": "2025", - "growthAccountsPayables": "0", - "growthAccountsReceivables": "0", - "growthAcquisitionsNet": "0", - "growthCapitalExpenditure": "0.7332280978689818", - "growthCashAtBeginningOfPeriod": "-0.07809495106059301", - "growthCashAtEndOfPeriod": "0.11426914604625096", - "growthChangeInWorkingCapital": "0", - "growthCommonStockIssued": "0", - "growthCommonStockRepurchased": "0", - "growthDebtRepayment": "1", - "growthDeferredIncomeTax": "0", - "growthDepreciationAndAmortization": "0", - "growthDividendsPaid": "0.6798284344644885", - "growthEffectOfForexChangesOnCash": "-1.0731570061902083", - "growthFreeCashFlow": "3.16553689621649", - "growthIncomeTaxesPaid": "0", - "growthInterestPaid": "0", - "growthInventory": "0", - "growthInvestmentsInPropertyPlantAndEquipment": "0.7332280978689818", - "growthLongTermNetDebtIssuance": "1", - "growthNetCashProvidedByOperatingActivites": "3.2072823819457614", - "growthNetCashUsedForInvestingActivites": "2.1699343339587243", - "growthNetCashUsedProvidedByFinancingActivities": "-3.2122934677858628", - "growthNetChangeInCash": "2.348938711752274", - "growthNetDebtIssuance": "1", - "growthNetIncome": "0", - "growthNetStockIssuance": "0", - "growthOperatingCashFlow": "3.2072823819457614", - "growthOtherFinancingActivites": "-1.7077146619443309", - "growthOtherInvestingActivites": "-0.8417721518987342", - "growthOtherNonCashItems": "3.2072823819457614", - "growthOtherWorkingCapital": "0", - "growthPreferredDividendsPaid": "0.6798284344644885", - "growthPurchasesOfInvestments": "-0.12254537395030414", - "growthSalesMaturitiesOfInvestments": "0.3847853673478318", - "growthShortTermNetDebtIssuance": "0", - "growthStockBasedCompensation": "0", - "period": "Q1", - "reportedCurrency": "CNY", - "symbol": "000001.SZ" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_cash_flow_statements.json b/crates/libfmp/tests/fixtures/bulk_cash_flow_statements.json deleted file mode 100644 index d0a97404..00000000 --- a/crates/libfmp/tests/fixtures/bulk_cash_flow_statements.json +++ /dev/null @@ -1,51 +0,0 @@ -[ - { - "acceptedDate": "2025-03-31 00:00:00", - "accountsPayables": "0", - "accountsReceivables": "0", - "acquisitionsNet": "0", - "capitalExpenditure": "-338000000", - "cashAtBeginningOfPeriod": "256946000000", - "cashAtEndOfPeriod": "286307000000", - "changeInWorkingCapital": "0", - "cik": "0000000000", - "commonDividendsPaid": "-2538000000", - "commonStockIssuance": "0", - "commonStockRepurchased": "0", - "date": "2025-03-31", - "deferredIncomeTax": "0", - "depreciationAndAmortization": "0", - "effectOfForexChangesOnCash": "-130000000", - "filingDate": "2025-03-31", - "fiscalYear": "2025", - "freeCashFlow": "162608000000", - "incomeTaxesPaid": "0", - "interestPaid": "0", - "inventory": "0", - "investmentsInPropertyPlantAndEquipment": "-338000000", - "longTermNetDebtIssuance": "0", - "netCashProvidedByFinancingActivities": "-158398000000", - "netCashProvidedByInvestingActivities": "24943000000", - "netCashProvidedByOperatingActivities": "162946000000", - "netChangeInCash": "29361000000", - "netCommonStockIssuance": "0", - "netDebtIssuance": "0", - "netDividendsPaid": "-2538000000", - "netIncome": "0", - "netPreferredStockIssuance": "0", - "netStockIssuance": "0", - "operatingCashFlow": "162946000000", - "otherFinancingActivities": "-155860000000", - "otherInvestingActivities": "25000000", - "otherNonCashItems": "162946000000", - "otherWorkingCapital": "0", - "period": "Q1", - "preferredDividendsPaid": "0", - "purchasesOfInvestments": "-227916000000", - "reportedCurrency": "CNY", - "salesMaturitiesOfInvestments": "253172000000", - "shortTermNetDebtIssuance": "0", - "stockBasedCompensation": "0", - "symbol": "000001.SZ" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_eod.json b/crates/libfmp/tests/fixtures/bulk_eod.json deleted file mode 100644 index cde7c75e..00000000 --- a/crates/libfmp/tests/fixtures/bulk_eod.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "adjClose": "2.93", - "close": "2.93", - "date": "2024-10-22", - "high": "2.9", - "low": "2.7", - "open": "2.67", - "symbol": "EGS745W1C011.CA", - "volume": "920904" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_income_statement_growth.json b/crates/libfmp/tests/fixtures/bulk_income_statement_growth.json deleted file mode 100644 index 1b96ebb0..00000000 --- a/crates/libfmp/tests/fixtures/bulk_income_statement_growth.json +++ /dev/null @@ -1,38 +0,0 @@ -[ - { - "date": "2025-03-31", - "fiscalYear": "2025", - "growthCostAndExpenses": "-0.095830920671685", - "growthCostOfRevenue": "0", - "growthDepreciationAndAmortization": "0", - "growthEBIT": "1", - "growthEBITDA": "0", - "growthEPS": "1.6956521739130435", - "growthEPSDiluted": "1.6956521739130435", - "growthGeneralAndAdministrativeExpenses": "1.7466809598416757", - "growthGrossProfit": "-0.04159070191431176", - "growthGrossProfitRatio": "0", - "growthIncomeBeforeTax": "1.4139262224764084", - "growthIncomeTaxExpense": "0.2582392776523702", - "growthInterestExpense": "-0.08421879522057303", - "growthInterestIncome": "-0.003105727849505302", - "growthNetIncome": "1.9495710399665203", - "growthNetIncomeDeductions": "0", - "growthNetIncomeFromContinuingOperations": "1.9495710399665203", - "growthNetInterestIncome": "0.09080465272126753", - "growthNonOperatingIncomeExcludingInterest": "-0.5659209985158163", - "growthOperatingExpenses": "-0.095830920671685", - "growthOperatingIncome": "-0.018874787810201278", - "growthOtherAdjustmentsToNetIncome": "0", - "growthOtherExpenses": "-0.9860376183912135", - "growthResearchAndDevelopmentExpenses": "0", - "growthRevenue": "-0.04159070191431176", - "growthSellingAndMarketingExpenses": "0", - "growthTotalOtherIncomeExpensesNet": "0.5835023664638269", - "growthWeightedAverageShsOut": "0.09825852256371011", - "growthWeightedAverageShsOutDil": "0.09825852256371011", - "period": "Q1", - "reportedCurrency": "CNY", - "symbol": "000001.SZ" - } -] diff --git a/crates/libfmp/tests/fixtures/bulk_income_statements.json b/crates/libfmp/tests/fixtures/bulk_income_statements.json deleted file mode 100644 index 12dbf28f..00000000 --- a/crates/libfmp/tests/fixtures/bulk_income_statements.json +++ /dev/null @@ -1,43 +0,0 @@ -[ - { - "acceptedDate": "2025-03-31 00:00:00", - "bottomLineNetIncome": "14096000000", - "cik": "0000000000", - "costAndExpenses": "9369000000", - "costOfRevenue": "0", - "date": "2025-03-31", - "depreciationAndAmortization": "0", - "ebit": "0", - "ebitda": "16802000000", - "eps": "0.62", - "epsDiluted": "0.62", - "filingDate": "2025-03-31", - "fiscalYear": "2025", - "generalAndAdministrativeExpenses": "9055000000", - "grossProfit": "33644000000", - "incomeBeforeTax": "16883000000", - "incomeTaxExpense": "2787000000", - "interestExpense": "22150000000", - "interestIncome": "44938000000", - "netIncome": "14096000000", - "netIncomeDeductions": "0", - "netIncomeFromContinuingOperations": "14096000000", - "netIncomeFromDiscontinuedOperations": "0", - "netInterestIncome": "22788000000", - "nonOperatingIncomeExcludingInterest": "24275000000", - "operatingExpenses": "9369000000", - "operatingIncome": "24275000000", - "otherAdjustmentsToNetIncome": "0", - "otherExpenses": "314000000", - "period": "Q1", - "reportedCurrency": "CNY", - "researchAndDevelopmentExpenses": "0", - "revenue": "33644000000", - "sellingAndMarketingExpenses": "0", - "sellingGeneralAndAdministrativeExpenses": "9055000000", - "symbol": "000001.SZ", - "totalOtherIncomeExpensesNet": "-7392000000", - "weightedAverageShsOut": "22735483871", - "weightedAverageShsOutDil": "22735483871" - } -] diff --git a/sdk/go/bulk_parity_test.go b/sdk/go/bulk_parity_test.go index b16a8860..3d1cfee1 100644 --- a/sdk/go/bulk_parity_test.go +++ b/sdk/go/bulk_parity_test.go @@ -25,13 +25,13 @@ func TestBulkFixturesDecodeAndReencodeToTheSameMemberSet(t *testing.T) { assertCSVFixtureParity[BulkFinancialRatiosTTM](t, "bulk_financial_ratios_ttm.csv", 60) assertCSVFixtureParity[BulkStockPeer](t, "bulk_stock_peers.csv", 2) assertCSVFixtureParity[BulkEarningsSurprise](t, "bulk_earnings_surprises.csv", 5) - assertFixtureParity[BulkIncomeStatement](t, "bulk_income_statements.json") - assertFixtureParity[BulkIncomeStatementGrowth](t, "bulk_income_statement_growth.json") - assertFixtureParity[BulkBalanceSheetStatement](t, "bulk_balance_sheet_statements.json") - assertFixtureParity[BulkBalanceSheetStatementGrowth](t, "bulk_balance_sheet_statement_growth.json") - assertFixtureParity[BulkCashFlowStatement](t, "bulk_cash_flow_statements.json") - assertFixtureParity[BulkCashFlowStatementGrowth](t, "bulk_cash_flow_statement_growth.json") - assertFixtureParity[BulkEodBar](t, "bulk_eod.json") + assertCSVFixtureParity[BulkIncomeStatement](t, "bulk_income_statements.csv", 39) + assertCSVFixtureParity[BulkIncomeStatementGrowth](t, "bulk_income_statement_growth.csv", 34) + assertCSVFixtureParity[BulkBalanceSheetStatement](t, "bulk_balance_sheet_statements.csv", 61) + assertCSVFixtureParity[BulkBalanceSheetStatementGrowth](t, "bulk_balance_sheet_statement_growth.csv", 56) + assertCSVFixtureParity[BulkCashFlowStatement](t, "bulk_cash_flow_statements.csv", 47) + assertCSVFixtureParity[BulkCashFlowStatementGrowth](t, "bulk_cash_flow_statement_growth.csv", 42) + assertCSVFixtureParity[BulkEodBar](t, "bulk_eod.csv", 8) } // Exact values copied from crates/libfmp/tests/bulk_snapshot_responses.rs: @@ -127,62 +127,41 @@ func TestBulkMetricsPreserveNumericTextIncludingBeyondU64(t *testing.T) { } // Exact values copied from crates/libfmp/tests/bulk_{income,balance,cash_eod}_responses.rs: -// identity fields keep their documented representations (ten-zero CIK, naive -// accepted date, Q1 period) and the provider typos keep their wire spelling. +// identity fields keep their provider representations (ten-zero CIK, naive +// accepted date, period) and the provider typos keep their wire spelling. func TestBulkStatementsDecodeExactValuesAndKeepProviderTypos(t *testing.T) { t.Parallel() - income := assertFixtureParity[BulkIncomeStatement](t, "bulk_income_statements.json") - if len(income) != 1 || income[0].Symbol != "000001.SZ" || income[0].ReportedCurrency != "CNY" || - income[0].CIK != "0000000000" || income[0].Date != mustParseDate(t, "2025-03-31") || - income[0].FilingDate != mustParseDate(t, "2025-03-31") || - income[0].AcceptedDate != mustParseDateTime(t, "2025-03-31 00:00:00") || income[0].FiscalYear != "2025" || - income[0].Period != "Q1" || income[0].Revenue != "33644000000" || income[0].CostOfRevenue != "0" || - income[0].TotalOtherIncomeExpensesNet != "-7392000000" || income[0].EPS != "0.62" || - income[0].WeightedAverageShsOut != "22735483871" { - t.Fatalf("bulk_income_statements = %+v", income) - } - incomeGrowth := assertFixtureParity[BulkIncomeStatementGrowth](t, "bulk_income_statement_growth.json") - if len(incomeGrowth) != 1 || incomeGrowth[0].GrowthEbit != "1" || incomeGrowth[0].GrowthCostOfRevenue != "0" || - incomeGrowth[0].GrowthOtherExpenses != "-0.9860376183912135" || - incomeGrowth[0].GrowthOperatingIncome != "-0.018874787810201278" { - t.Fatalf("bulk_income_statement_growth = %+v", incomeGrowth) - } + income := assertCSVFixtureParity[BulkIncomeStatement](t, "bulk_income_statements.csv", 39) + if income[0].Symbol != "000001.SZ" || income[0].ReportedCurrency != "CNY" || income[0].CIK != "0000000000" || + income[0].Date != mustParseDate(t, "2024-12-31") || + income[0].AcceptedDate != mustParseDateTime(t, "2024-12-31 00:00:00") || income[0].FiscalYear != "2024" || + income[0].Period != "FY" || income[0].Revenue != "251641000000" { + t.Fatalf("bulk_income_statements = %+v", income[0]) + } + incomeGrowth := assertCSVFixtureParity[BulkIncomeStatementGrowth](t, "bulk_income_statement_growth.csv", 34) if encoded, err := json.Marshal(incomeGrowth[0]); err != nil || !strings.Contains(string(encoded), `"growthEBITDA":`) || !strings.Contains(string(encoded), `"growthEPSDiluted":`) || !strings.Contains(string(encoded), `"growthEBIT":`) { t.Fatalf("re-encoded income growth = %s, %v", encoded, err) } - balance := assertFixtureParity[BulkBalanceSheetStatement](t, "bulk_balance_sheet_statements.json") - if len(balance) != 1 || balance[0].Symbol != "MTLRP.ME" || balance[0].ReportedCurrency != "RUB" || - balance[0].CIK != "0000000000" || balance[0].FilingDate != mustParseDate(t, "2025-05-31") || - balance[0].AcceptedDate != mustParseDateTime(t, "2025-03-31 07:00:00") || balance[0].Period != "Q1" || - balance[0].TotalAssets != "247871857000" || balance[0].RetainedEarnings != "-5066509000" || - balance[0].OtherAssets != "0" || balance[0].NetDebt != "183764862000" { + balance := assertCSVFixtureParity[BulkBalanceSheetStatement](t, "bulk_balance_sheet_statements.csv", 61) + if balance[1].Symbol != "0002.KL" || balance[1].ReportedCurrency != "MYR" || + balance[1].AcceptedDate != mustParseDateTime(t, "2025-06-30 00:00:00") || balance[0].TotalAssets != "5769270000000" { t.Fatalf("bulk_balance_sheet_statements = %+v", balance) } - balanceGrowth := assertFixtureParity[BulkBalanceSheetStatementGrowth](t, "bulk_balance_sheet_statement_growth.json") - if len(balanceGrowth) != 1 || balanceGrowth[0].GrowthCashAndCashEquivalents != "0.09574482145872953" || - balanceGrowth[0].GrowthShortTermInvestments != "0" || balanceGrowth[0].GrowthTotalPayables != "-0.12022416350749959" { - t.Fatalf("bulk_balance_sheet_statement_growth = %+v", balanceGrowth) - } + balanceGrowth := assertCSVFixtureParity[BulkBalanceSheetStatementGrowth](t, "bulk_balance_sheet_statement_growth.csv", 56) if encoded, err := json.Marshal(balanceGrowth[0]); err != nil || !strings.Contains(string(encoded), `"growthOthertotalStockholdersEquity":`) || !strings.Contains(string(encoded), `"growthTotalLiabilitiesAndStockholdersEquity":`) { t.Fatalf("re-encoded balance growth = %s, %v", encoded, err) } - cashFlow := assertFixtureParity[BulkCashFlowStatement](t, "bulk_cash_flow_statements.json") - if len(cashFlow) != 1 || cashFlow[0].Symbol != "000001.SZ" || cashFlow[0].CIK != "0000000000" || - cashFlow[0].AcceptedDate != mustParseDateTime(t, "2025-03-31 00:00:00") || cashFlow[0].Period != "Q1" || - cashFlow[0].OtherNonCashItems != "162946000000" || cashFlow[0].PurchasesOfInvestments != "-227916000000" || - cashFlow[0].NetIncome != "0" { - t.Fatalf("bulk_cash_flow_statements = %+v", cashFlow) - } - cashGrowth := assertFixtureParity[BulkCashFlowStatementGrowth](t, "bulk_cash_flow_statement_growth.json") - if len(cashGrowth) != 1 || cashGrowth[0].GrowthNetCashUsedProvidedByFinancingActivities != "-3.2122934677858628" || - cashGrowth[0].GrowthNetDebtIssuance != "1" { - t.Fatalf("bulk_cash_flow_statement_growth = %+v", cashGrowth) + cashFlow := assertCSVFixtureParity[BulkCashFlowStatement](t, "bulk_cash_flow_statements.csv", 47) + if cashFlow[0].CIK != "0000000000" || cashFlow[0].AcceptedDate != mustParseDateTime(t, "2024-03-30 20:00:00") || + cashFlow[0].Period != "Q1" || cashFlow[0].NetIncome != "14932000000" { + t.Fatalf("bulk_cash_flow_statements = %+v", cashFlow[0]) } + cashGrowth := assertCSVFixtureParity[BulkCashFlowStatementGrowth](t, "bulk_cash_flow_statement_growth.csv", 42) if encoded, err := json.Marshal(cashGrowth[0]); err != nil || !strings.Contains(string(encoded), `"growthNetCashProvidedByOperatingActivites":`) || !strings.Contains(string(encoded), `"growthOtherInvestingActivites":`) || @@ -191,9 +170,9 @@ func TestBulkStatementsDecodeExactValuesAndKeepProviderTypos(t *testing.T) { t.Fatalf("re-encoded cash-flow growth = %s, %v", encoded, err) } - eod := assertFixtureParity[BulkEodBar](t, "bulk_eod.json") - if want := (BulkEodBar{Symbol: "EGS745W1C011.CA", Date: mustParseDate(t, "2024-10-22"), Open: "2.67", Low: "2.7", - High: "2.9", Close: "2.93", AdjClose: "2.93", Volume: "920904"}); len(eod) != 1 || eod[0] != want { - t.Fatalf("bulk_eod = %+v", eod) + eod := assertCSVFixtureParity[BulkEodBar](t, "bulk_eod.csv", 8) + if want := (BulkEodBar{Symbol: "HKDCNH", Date: mustParseDate(t, "2025-06-02"), Open: "0.91858", Low: "0.91692", + High: "0.9186", Close: "0.91741", AdjClose: "0.91741", Volume: "0"}); eod[1] != want { + t.Fatalf("bulk_eod = %+v", eod[1]) } } diff --git a/sdk/go/fixtures_test.go b/sdk/go/fixtures_test.go index 7c313bc9..5700efed 100644 --- a/sdk/go/fixtures_test.go +++ b/sdk/go/fixtures_test.go @@ -222,7 +222,7 @@ func mutateFixtureMember(t *testing.T, fixture, member string, value jsontext.Va // TestEveryFixtureIsReferencedByAGoTest enforces the ADR 0030 acceptance // check that every shared fixture decodes through a Go model: each .json -// file under the fixture directory must be named, as a quoted literal, in at +// or .csv file under the fixture directory must be named, as a quoted literal, in at // least one *_test.go file of this package. The fixture README is not a // fixture and is the only file the scan skips. Every unreferenced fixture is // listed, so a newly recorded fixture fails here until a domain test uses it. @@ -246,7 +246,7 @@ func TestEveryFixtureIsReferencedByAGoTest(t *testing.T) { } var fixtures, unreferenced []string for _, entry := range entries { - if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" { + if entry.IsDir() || !slices.Contains([]string{".json", ".csv"}, filepath.Ext(entry.Name())) { continue } fixtures = append(fixtures, entry.Name()) From b8b90f2766bc7d01f60a959c4dec95841e186bef Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 10:59:28 +0800 Subject: [PATCH 21/31] test(fmp-py): serve CSV fixtures for bulk statement and eod tests Switch the six bulk statement routes and the eod route to serve the shared live CSV fixtures as text/csv via a new `route_csv` helper, and update the assertions to match the live values in those fixtures. Relates to #376 --- crates/fmp-py/tests/test_bulk_b.py | 120 +++++++++++++++-------------- 1 file changed, 61 insertions(+), 59 deletions(-) diff --git a/crates/fmp-py/tests/test_bulk_b.py b/crates/fmp-py/tests/test_bulk_b.py index f863ca74..2cbc5730 100644 --- a/crates/fmp-py/tests/test_bulk_b.py +++ b/crates/fmp-py/tests/test_bulk_b.py @@ -12,148 +12,150 @@ from typing import Any import pytest -from conftest import FixtureServer, load_fixture +from conftest import CSV_CONTENT_TYPE, FixtureServer, load_csv_fixture from fmp.bulk.balance import BulkBalanceSheetStatement, BulkBalanceSheetStatementGrowth from fmp.bulk.cash_flow import BulkCashFlowStatement, BulkCashFlowStatementGrowth from fmp.bulk.eod import BulkEodBar from fmp.bulk.income import BulkIncomeStatement, BulkIncomeStatementGrowth -DATE_2025_03_31 = datetime.date(2025, 3, 31) DATE_2024_10_22 = datetime.date(2024, 10, 22) +def route_csv(fixture_server: FixtureServer, path: str, fixture: str) -> None: + """Serve the shared CSV fixture on ``path`` as ``text/csv``, as the bulk routes answer.""" + fixture_server.route(path, load_csv_fixture(fixture), content_type=CSV_CONTENT_TYPE) + + def test_income_statements_encodes_year_then_period(client: Any, fixture_server: FixtureServer) -> None: """``income_statements`` sends ``year`` before ``period`` and decodes the datetime column.""" - fixture_server.route("/income-statement-bulk", load_fixture("bulk_income_statements.json")) + route_csv(fixture_server, "/income-statement-bulk", "bulk_income_statements.csv") rows = client.bulk.income_statements(2026, "Q1") assert fixture_server.requests[0].target == "/income-statement-bulk?year=2026&period=Q1" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkIncomeStatement) assert row.symbol == "000001.SZ" - assert row.date == DATE_2025_03_31 - assert row.filing_date == DATE_2025_03_31 - assert row.accepted_date == datetime.datetime(2025, 3, 31, 0, 0, 0) - assert (row.fiscal_year, row.period) == ("2025", "Q1") - assert row.revenue == "33644000000" - assert row.net_income == "14096000000" - assert row.eps == "0.62" + assert row.date == datetime.date(2024, 12, 31) + assert row.filing_date == datetime.date(2024, 12, 31) + assert row.accepted_date == datetime.datetime(2024, 12, 31, 0, 0, 0) + assert (row.fiscal_year, row.period) == ("2024", "FY") + assert row.revenue == "251641000000" + assert row.net_income == "44508000000" + assert row.eps == "2.15" def test_income_statement_growth_with_keyword_arguments(client: Any, fixture_server: FixtureServer) -> None: """``income_statement_growth`` accepts both constructor arguments by keyword.""" - fixture_server.route("/income-statement-growth-bulk", load_fixture("bulk_income_statement_growth.json")) + route_csv(fixture_server, "/income-statement-growth-bulk", "bulk_income_statement_growth.csv") rows = client.bulk.income_statement_growth(period="Q1", year=2026) assert fixture_server.requests[0].target == "/income-statement-growth-bulk?year=2026&period=Q1" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkIncomeStatementGrowth) assert row.symbol == "000001.SZ" - assert row.date == DATE_2025_03_31 - assert (row.fiscal_year, row.period) == ("2025", "Q1") - assert row.growth_revenue == "-0.04159070191431176" - assert row.growth_net_income == "1.9495710399665203" - assert row.growth_eps == "1.6956521739130435" + assert row.date == datetime.date(2024, 12, 31) + assert (row.fiscal_year, row.period) == ("2024", "FY") + assert row.growth_revenue == "-0.08220846812871789" + assert row.growth_net_income == "-0.04191152728446884" + assert row.growth_eps == "-0.04444444444444448" def test_balance_sheet_statements_with_the_full_year_period(client: Any, fixture_server: FixtureServer) -> None: """``balance_sheet_statements`` accepts ``FY`` and decodes the mid-day accepted timestamp.""" - fixture_server.route("/balance-sheet-statement-bulk", load_fixture("bulk_balance_sheet_statements.json")) + route_csv(fixture_server, "/balance-sheet-statement-bulk", "bulk_balance_sheet_statements.csv") rows = client.bulk.balance_sheet_statements(2025, "FY") assert fixture_server.requests[0].target == "/balance-sheet-statement-bulk?year=2025&period=FY" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkBalanceSheetStatement) - assert row.symbol == "MTLRP.ME" - assert row.date == DATE_2025_03_31 - assert row.filing_date == datetime.date(2025, 5, 31) - assert row.accepted_date == datetime.datetime(2025, 3, 31, 7, 0, 0) - assert row.total_assets == "247871857000" - assert row.cash_and_cash_equivalents == "1985000" - assert row.total_debt == "183766847000" + assert row.symbol == "000001.SZ" + assert row.date == datetime.date(2024, 12, 31) + assert row.filing_date == datetime.date(2024, 12, 31) + assert row.accepted_date == datetime.datetime(2024, 12, 31, 0, 0, 0) + assert row.total_assets == "5769270000000" + assert row.cash_and_cash_equivalents == "680935000000" + assert row.total_debt == "756251000000" def test_balance_sheet_statement_growth_is_case_insensitive_on_period( client: Any, fixture_server: FixtureServer ) -> None: """A lower-case fiscal period is normalised to the documented upper-case spelling.""" - fixture_server.route( - "/balance-sheet-statement-growth-bulk", load_fixture("bulk_balance_sheet_statement_growth.json") - ) + route_csv(fixture_server, "/balance-sheet-statement-growth-bulk", "bulk_balance_sheet_statement_growth.csv") rows = client.bulk.balance_sheet_statement_growth(2026, "q4") assert fixture_server.requests[0].target == "/balance-sheet-statement-growth-bulk?year=2026&period=Q4" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkBalanceSheetStatementGrowth) assert row.symbol == "000001.SZ" - assert row.date == DATE_2025_03_31 - assert row.growth_total_assets == "0.001488576544346165" - assert row.growth_cash_and_cash_equivalents == "0.09574482145872953" - assert row.growth_net_debt == "-0.09574482145872953" + assert row.date == datetime.date(2024, 12, 31) + assert row.growth_total_assets == "0.032602509058340653" + assert row.growth_cash_and_cash_equivalents == "0.13745663137040692" + assert row.growth_net_debt == "-0.5927764260610976" def test_cash_flow_statements_encodes_year_then_period(client: Any, fixture_server: FixtureServer) -> None: """``cash_flow_statements`` maps to ``/cash-flow-statement-bulk`` in documented order.""" - fixture_server.route("/cash-flow-statement-bulk", load_fixture("bulk_cash_flow_statements.json")) + route_csv(fixture_server, "/cash-flow-statement-bulk", "bulk_cash_flow_statements.csv") rows = client.bulk.cash_flow_statements(2026, "Q2") assert fixture_server.requests[0].target == "/cash-flow-statement-bulk?year=2026&period=Q2" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkCashFlowStatement) assert row.symbol == "000001.SZ" - assert row.date == DATE_2025_03_31 - assert row.accepted_date == datetime.datetime(2025, 3, 31, 0, 0, 0) - assert row.net_income == "0" - assert row.free_cash_flow == "162608000000" - assert row.capital_expenditure == "-338000000" + assert row.date == datetime.date(2024, 3, 31) + assert row.accepted_date == datetime.datetime(2024, 3, 30, 20, 0, 0) + assert row.net_income == "14932000000" + assert row.free_cash_flow == "-21623000000" + assert row.capital_expenditure == "-241000000" def test_cash_flow_statement_growth_encodes_year_then_period(client: Any, fixture_server: FixtureServer) -> None: """``cash_flow_statement_growth`` maps to ``/cash-flow-statement-growth-bulk``.""" - fixture_server.route("/cash-flow-statement-growth-bulk", load_fixture("bulk_cash_flow_statement_growth.json")) + route_csv(fixture_server, "/cash-flow-statement-growth-bulk", "bulk_cash_flow_statement_growth.csv") rows = client.bulk.cash_flow_statement_growth(2026, "Q3") assert fixture_server.requests[0].target == "/cash-flow-statement-growth-bulk?year=2026&period=Q3" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkCashFlowStatementGrowth) assert row.symbol == "000001.SZ" - assert row.date == DATE_2025_03_31 - assert (row.fiscal_year, row.period) == ("2025", "Q1") - assert row.growth_net_income == "0" - assert row.growth_free_cash_flow == "3.16553689621649" - assert row.growth_capital_expenditure == "0.7332280978689818" + assert row.date == datetime.date(2024, 12, 31) + assert (row.fiscal_year, row.period) == ("2024", "FY") + assert row.growth_net_income == "-0.04191152728446884" + assert row.growth_free_cash_flow == "-0.3179880266323505" + assert row.growth_capital_expenditure == "0.22868217054263565" def test_eod_accepts_a_date_object(client: Any, fixture_server: FixtureServer) -> None: """``eod`` encodes a ``datetime.date`` as the ISO ``date`` parameter.""" - fixture_server.route("/eod-bulk", load_fixture("bulk_eod.json")) + route_csv(fixture_server, "/eod-bulk", "bulk_eod.csv") rows = client.bulk.eod(DATE_2024_10_22) assert fixture_server.requests[0].target == "/eod-bulk?date=2024-10-22" - assert len(rows) == 1 + assert len(rows) == 2 row = rows[0] assert isinstance(row, BulkEodBar) - assert row.symbol == "EGS745W1C011.CA" - assert row.date == DATE_2024_10_22 - assert row.open == "2.67" - assert row.close == "2.93" - assert row.adj_close == "2.93" - assert row.volume == "920904" + assert row.symbol == "PCFD.SG" + assert row.date == datetime.date(2025, 6, 2) + assert row.open == "7.35" + assert row.close == "19.71" + assert row.adj_close == "19.71" + assert row.volume == "0" def test_eod_accepts_an_iso_string(client: Any, fixture_server: FixtureServer) -> None: """``eod`` also takes the date as an ISO string, by keyword.""" - fixture_server.route("/eod-bulk", load_fixture("bulk_eod.json")) + route_csv(fixture_server, "/eod-bulk", "bulk_eod.csv") rows = client.bulk.eod(date="2024-10-22") assert fixture_server.requests[0].target == "/eod-bulk?date=2024-10-22" - assert rows[0].date == DATE_2024_10_22 + assert rows[1].date == datetime.date(2025, 6, 2) def test_statement_arguments_are_required(client: Any, fixture_server: FixtureServer) -> None: From bf3a171a050bd9d9c5085fc656be6c099499551b Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 11:04:33 +0800 Subject: [PATCH 22/31] fix(bulk)!: make empty numeric snapshot cells optional per ADR 0035 Under ADR 0035, an empty CSV cell is the only absence marker for bulk rows and sparse empties vary by period, so every numeric metric member becomes optional while identity members stay required. Every NumericString member of BulkStockRating, BulkDcfValuation, BulkFinancialScore, BulkPriceTargetSummary, BulkEtfHolding and BulkUpgradesDowngradesConsensus now decodes as absent when its cell is empty. Typed codes seen empty in the live census also become optional via the empty_or_null codec: BulkFinancialScore.reported_currency (313 of 62k rows), BulkEtfHolding.asset and BulkEtfHolding.isin. Fixtures gain the live rows with empty cells and tests assert None for them. Regenerated Python models/stubs and Go models are included. Refs #376 BREAKING CHANGE: discounted_cash_flow_score, return_on_equity_score, return_on_assets_score, debt_to_equity_score, price_to_earnings_score, price_to_book_score, dcf, stock_price, reported_currency, altman_z_score, piotroski_score, working_capital, total_assets, retained_earnings, ebit, market_cap, total_liabilities, revenue, last_month_count, last_month_avg_price_target, last_quarter_count, last_quarter_avg_price_target, last_year_count, last_year_avg_price_target, all_time_count, all_time_avg_price_target, shares_number, asset, weight_percentage, isin, market_value, strong_buy, buy, hold, sell and strong_sell are now Option (asset Option, isin Option, reported_currency Option) in Rust, Optional[str] in Python and *string in Go. --- .../fmp/_native/bulk/snapshots/__init__.pyi | 144 ++++----- crates/fmp-py/src/models/bulk/snapshots.rs | 231 +++++++------ crates/fmp-py/tests/test_bulk_a.py | 11 +- crates/libfmp/src/responses/bulk/snapshots.rs | 75 ++--- .../libfmp/tests/bulk_snapshot_endpoints.rs | 9 +- .../libfmp/tests/bulk_snapshot_responses.rs | 51 ++- .../tests/fixtures/bulk_dcf_valuations.csv | 1 + .../tests/fixtures/bulk_etf_holdings.csv | 2 + .../tests/fixtures/bulk_financial_scores.csv | 1 + crates/libfmp/tests/support/bulk_csv.rs | 28 +- sdk/go/bulk_models.go | 306 ++++++++---------- sdk/go/bulk_parity_test.go | 28 +- sdk/go/bulk_test.go | 6 +- sdk/go/csv_test.go | 9 + 14 files changed, 478 insertions(+), 424 deletions(-) diff --git a/crates/fmp-py/python/fmp/_native/bulk/snapshots/__init__.pyi b/crates/fmp-py/python/fmp/_native/bulk/snapshots/__init__.pyi index 8f0e1343..95df9813 100644 --- a/crates/fmp-py/python/fmp/_native/bulk/snapshots/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/bulk/snapshots/__init__.pyi @@ -29,17 +29,17 @@ class BulkDcfValuation: @property def date(self) -> datetime.date: ... @property - def dcf(self) -> builtins.str: ... + def dcf(self) -> typing.Optional[builtins.str]: ... @property - def stock_price(self) -> builtins.str: ... + def stock_price(self) -> typing.Optional[builtins.str]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... def __new__( cls, *, symbol: builtins.str, date: datetime.date, - dcf: builtins.str, - stock_price: builtins.str, + dcf: typing.Optional[builtins.str], + stock_price: typing.Optional[builtins.str], ) -> BulkDcfValuation: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... @@ -70,17 +70,17 @@ class BulkEtfHolding: @property def name(self) -> builtins.str: ... @property - def shares_number(self) -> builtins.str: ... + def shares_number(self) -> typing.Optional[builtins.str]: ... @property - def asset(self) -> builtins.str: ... + def asset(self) -> typing.Optional[builtins.str]: ... @property - def weight_percentage(self) -> builtins.str: ... + def weight_percentage(self) -> typing.Optional[builtins.str]: ... @property def cusip(self) -> builtins.str: ... @property - def isin(self) -> builtins.str: ... + def isin(self) -> typing.Optional[builtins.str]: ... @property - def market_value(self) -> builtins.str: ... + def market_value(self) -> typing.Optional[builtins.str]: ... @property def last_updated(self) -> datetime.date: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... @@ -89,12 +89,12 @@ class BulkEtfHolding: *, symbol: builtins.str, name: builtins.str, - shares_number: builtins.str, - asset: builtins.str, - weight_percentage: builtins.str, + shares_number: typing.Optional[builtins.str], + asset: typing.Optional[builtins.str], + weight_percentage: typing.Optional[builtins.str], cusip: builtins.str, - isin: builtins.str, - market_value: builtins.str, + isin: typing.Optional[builtins.str], + market_value: typing.Optional[builtins.str], last_updated: datetime.date, ) -> BulkEtfHolding: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... @@ -126,40 +126,40 @@ class BulkFinancialScore: @property def symbol(self) -> builtins.str: ... @property - def reported_currency(self) -> builtins.str: ... + def reported_currency(self) -> typing.Optional[builtins.str]: ... @property - def altman_z_score(self) -> builtins.str: ... + def altman_z_score(self) -> typing.Optional[builtins.str]: ... @property - def piotroski_score(self) -> builtins.str: ... + def piotroski_score(self) -> typing.Optional[builtins.str]: ... @property - def working_capital(self) -> builtins.str: ... + def working_capital(self) -> typing.Optional[builtins.str]: ... @property - def total_assets(self) -> builtins.str: ... + def total_assets(self) -> typing.Optional[builtins.str]: ... @property - def retained_earnings(self) -> builtins.str: ... + def retained_earnings(self) -> typing.Optional[builtins.str]: ... @property - def ebit(self) -> builtins.str: ... + def ebit(self) -> typing.Optional[builtins.str]: ... @property - def market_cap(self) -> builtins.str: ... + def market_cap(self) -> typing.Optional[builtins.str]: ... @property - def total_liabilities(self) -> builtins.str: ... + def total_liabilities(self) -> typing.Optional[builtins.str]: ... @property - def revenue(self) -> builtins.str: ... + def revenue(self) -> typing.Optional[builtins.str]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... def __new__( cls, *, symbol: builtins.str, - reported_currency: builtins.str, - altman_z_score: builtins.str, - piotroski_score: builtins.str, - working_capital: builtins.str, - total_assets: builtins.str, - retained_earnings: builtins.str, - ebit: builtins.str, - market_cap: builtins.str, - total_liabilities: builtins.str, - revenue: builtins.str, + reported_currency: typing.Optional[builtins.str], + altman_z_score: typing.Optional[builtins.str], + piotroski_score: typing.Optional[builtins.str], + working_capital: typing.Optional[builtins.str], + total_assets: typing.Optional[builtins.str], + retained_earnings: typing.Optional[builtins.str], + ebit: typing.Optional[builtins.str], + market_cap: typing.Optional[builtins.str], + total_liabilities: typing.Optional[builtins.str], + revenue: typing.Optional[builtins.str], ) -> BulkFinancialScore: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... @@ -189,21 +189,21 @@ class BulkPriceTargetSummary: @property def symbol(self) -> builtins.str: ... @property - def last_month_count(self) -> builtins.str: ... + def last_month_count(self) -> typing.Optional[builtins.str]: ... @property - def last_month_avg_price_target(self) -> builtins.str: ... + def last_month_avg_price_target(self) -> typing.Optional[builtins.str]: ... @property - def last_quarter_count(self) -> builtins.str: ... + def last_quarter_count(self) -> typing.Optional[builtins.str]: ... @property - def last_quarter_avg_price_target(self) -> builtins.str: ... + def last_quarter_avg_price_target(self) -> typing.Optional[builtins.str]: ... @property - def last_year_count(self) -> builtins.str: ... + def last_year_count(self) -> typing.Optional[builtins.str]: ... @property - def last_year_avg_price_target(self) -> builtins.str: ... + def last_year_avg_price_target(self) -> typing.Optional[builtins.str]: ... @property - def all_time_count(self) -> builtins.str: ... + def all_time_count(self) -> typing.Optional[builtins.str]: ... @property - def all_time_avg_price_target(self) -> builtins.str: ... + def all_time_avg_price_target(self) -> typing.Optional[builtins.str]: ... @property def publishers(self) -> builtins.str: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... @@ -211,14 +211,14 @@ class BulkPriceTargetSummary: cls, *, symbol: builtins.str, - last_month_count: builtins.str, - last_month_avg_price_target: builtins.str, - last_quarter_count: builtins.str, - last_quarter_avg_price_target: builtins.str, - last_year_count: builtins.str, - last_year_avg_price_target: builtins.str, - all_time_count: builtins.str, - all_time_avg_price_target: builtins.str, + last_month_count: typing.Optional[builtins.str], + last_month_avg_price_target: typing.Optional[builtins.str], + last_quarter_count: typing.Optional[builtins.str], + last_quarter_avg_price_target: typing.Optional[builtins.str], + last_year_count: typing.Optional[builtins.str], + last_year_avg_price_target: typing.Optional[builtins.str], + all_time_count: typing.Optional[builtins.str], + all_time_avg_price_target: typing.Optional[builtins.str], publishers: builtins.str, ) -> BulkPriceTargetSummary: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... @@ -252,17 +252,17 @@ class BulkStockRating: @property def rating(self) -> builtins.str: ... @property - def discounted_cash_flow_score(self) -> builtins.str: ... + def discounted_cash_flow_score(self) -> typing.Optional[builtins.str]: ... @property - def return_on_equity_score(self) -> builtins.str: ... + def return_on_equity_score(self) -> typing.Optional[builtins.str]: ... @property - def return_on_assets_score(self) -> builtins.str: ... + def return_on_assets_score(self) -> typing.Optional[builtins.str]: ... @property - def debt_to_equity_score(self) -> builtins.str: ... + def debt_to_equity_score(self) -> typing.Optional[builtins.str]: ... @property - def price_to_earnings_score(self) -> builtins.str: ... + def price_to_earnings_score(self) -> typing.Optional[builtins.str]: ... @property - def price_to_book_score(self) -> builtins.str: ... + def price_to_book_score(self) -> typing.Optional[builtins.str]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... def __new__( cls, @@ -270,12 +270,12 @@ class BulkStockRating: symbol: builtins.str, date: datetime.date, rating: builtins.str, - discounted_cash_flow_score: builtins.str, - return_on_equity_score: builtins.str, - return_on_assets_score: builtins.str, - debt_to_equity_score: builtins.str, - price_to_earnings_score: builtins.str, - price_to_book_score: builtins.str, + discounted_cash_flow_score: typing.Optional[builtins.str], + return_on_equity_score: typing.Optional[builtins.str], + return_on_assets_score: typing.Optional[builtins.str], + debt_to_equity_score: typing.Optional[builtins.str], + price_to_earnings_score: typing.Optional[builtins.str], + price_to_book_score: typing.Optional[builtins.str], ) -> BulkStockRating: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... @@ -302,15 +302,15 @@ class BulkUpgradesDowngradesConsensus: @property def symbol(self) -> builtins.str: ... @property - def strong_buy(self) -> builtins.str: ... + def strong_buy(self) -> typing.Optional[builtins.str]: ... @property - def buy(self) -> builtins.str: ... + def buy(self) -> typing.Optional[builtins.str]: ... @property - def hold(self) -> builtins.str: ... + def hold(self) -> typing.Optional[builtins.str]: ... @property - def sell(self) -> builtins.str: ... + def sell(self) -> typing.Optional[builtins.str]: ... @property - def strong_sell(self) -> builtins.str: ... + def strong_sell(self) -> typing.Optional[builtins.str]: ... @property def consensus(self) -> builtins.str: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... @@ -318,11 +318,11 @@ class BulkUpgradesDowngradesConsensus: cls, *, symbol: builtins.str, - strong_buy: builtins.str, - buy: builtins.str, - hold: builtins.str, - sell: builtins.str, - strong_sell: builtins.str, + strong_buy: typing.Optional[builtins.str], + buy: typing.Optional[builtins.str], + hold: typing.Optional[builtins.str], + sell: typing.Optional[builtins.str], + strong_sell: typing.Optional[builtins.str], consensus: builtins.str, ) -> BulkUpgradesDowngradesConsensus: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... diff --git a/crates/fmp-py/src/models/bulk/snapshots.rs b/crates/fmp-py/src/models/bulk/snapshots.rs index 4f222bdc..5f4898d0 100644 --- a/crates/fmp-py/src/models/bulk/snapshots.rs +++ b/crates/fmp-py/src/models/bulk/snapshots.rs @@ -16,17 +16,17 @@ pub(crate) struct BulkStockRating { #[pyo3(get)] pub rating: String, #[pyo3(get)] - pub discounted_cash_flow_score: String, + pub discounted_cash_flow_score: Option, #[pyo3(get)] - pub return_on_equity_score: String, + pub return_on_equity_score: Option, #[pyo3(get)] - pub return_on_assets_score: String, + pub return_on_assets_score: Option, #[pyo3(get)] - pub debt_to_equity_score: String, + pub debt_to_equity_score: Option, #[pyo3(get)] - pub price_to_earnings_score: String, + pub price_to_earnings_score: Option, #[pyo3(get)] - pub price_to_book_score: String, + pub price_to_book_score: Option, } #[gen_stub_pymethods] @@ -40,12 +40,12 @@ impl BulkStockRating { symbol: String, date: ::chrono::NaiveDate, rating: String, - discounted_cash_flow_score: String, - return_on_equity_score: String, - return_on_assets_score: String, - debt_to_equity_score: String, - price_to_earnings_score: String, - price_to_book_score: String, + discounted_cash_flow_score: Option, + return_on_equity_score: Option, + return_on_assets_score: Option, + debt_to_equity_score: Option, + price_to_earnings_score: Option, + price_to_book_score: Option, ) -> Self { Self { symbol, @@ -208,12 +208,16 @@ impl From for BulkStockRating { symbol: value.symbol.into_inner(), date: value.date.into_inner(), rating: value.rating, - discounted_cash_flow_score: value.discounted_cash_flow_score.into_inner(), - return_on_equity_score: value.return_on_equity_score.into_inner(), - return_on_assets_score: value.return_on_assets_score.into_inner(), - debt_to_equity_score: value.debt_to_equity_score.into_inner(), - price_to_earnings_score: value.price_to_earnings_score.into_inner(), - price_to_book_score: value.price_to_book_score.into_inner(), + discounted_cash_flow_score: value + .discounted_cash_flow_score + .map(|value| value.into_inner()), + return_on_equity_score: value.return_on_equity_score.map(|value| value.into_inner()), + return_on_assets_score: value.return_on_assets_score.map(|value| value.into_inner()), + debt_to_equity_score: value.debt_to_equity_score.map(|value| value.into_inner()), + price_to_earnings_score: value + .price_to_earnings_score + .map(|value| value.into_inner()), + price_to_book_score: value.price_to_book_score.map(|value| value.into_inner()), } } } @@ -233,9 +237,9 @@ pub(crate) struct BulkDcfValuation { #[pyo3(get)] pub date: ::chrono::NaiveDate, #[pyo3(get)] - pub dcf: String, + pub dcf: Option, #[pyo3(get)] - pub stock_price: String, + pub stock_price: Option, } #[gen_stub_pymethods] @@ -245,7 +249,12 @@ impl BulkDcfValuation { #[allow(clippy::too_many_arguments)] #[allow(clippy::fn_params_excessive_bools)] #[pyo3(signature = (*, symbol, date, dcf, stock_price))] - fn new(symbol: String, date: ::chrono::NaiveDate, dcf: String, stock_price: String) -> Self { + fn new( + symbol: String, + date: ::chrono::NaiveDate, + dcf: Option, + stock_price: Option, + ) -> Self { Self { symbol, date, @@ -324,8 +333,8 @@ impl From for BulkDcfValuation { Self { symbol: value.symbol.into_inner(), date: value.date.into_inner(), - dcf: value.dcf.into_inner(), - stock_price: value.stock_price.into_inner(), + dcf: value.dcf.map(|value| value.into_inner()), + stock_price: value.stock_price.map(|value| value.into_inner()), } } } @@ -343,25 +352,25 @@ pub(crate) struct BulkFinancialScore { #[pyo3(get)] pub symbol: String, #[pyo3(get)] - pub reported_currency: String, + pub reported_currency: Option, #[pyo3(get)] - pub altman_z_score: String, + pub altman_z_score: Option, #[pyo3(get)] - pub piotroski_score: String, + pub piotroski_score: Option, #[pyo3(get)] - pub working_capital: String, + pub working_capital: Option, #[pyo3(get)] - pub total_assets: String, + pub total_assets: Option, #[pyo3(get)] - pub retained_earnings: String, + pub retained_earnings: Option, #[pyo3(get)] - pub ebit: String, + pub ebit: Option, #[pyo3(get)] - pub market_cap: String, + pub market_cap: Option, #[pyo3(get)] - pub total_liabilities: String, + pub total_liabilities: Option, #[pyo3(get)] - pub revenue: String, + pub revenue: Option, } #[gen_stub_pymethods] @@ -373,16 +382,16 @@ impl BulkFinancialScore { #[pyo3(signature = (*, symbol, reported_currency, altman_z_score, piotroski_score, working_capital, total_assets, retained_earnings, ebit, market_cap, total_liabilities, revenue))] fn new( symbol: String, - reported_currency: String, - altman_z_score: String, - piotroski_score: String, - working_capital: String, - total_assets: String, - retained_earnings: String, - ebit: String, - market_cap: String, - total_liabilities: String, - revenue: String, + reported_currency: Option, + altman_z_score: Option, + piotroski_score: Option, + working_capital: Option, + total_assets: Option, + retained_earnings: Option, + ebit: Option, + market_cap: Option, + total_liabilities: Option, + revenue: Option, ) -> Self { Self { symbol, @@ -561,16 +570,16 @@ impl From for BulkFinancialScore { fn from(value: libfmp::responses::bulk::BulkFinancialScore) -> Self { Self { symbol: value.symbol.into_inner(), - reported_currency: value.reported_currency.into_inner(), - altman_z_score: value.altman_z_score.into_inner(), - piotroski_score: value.piotroski_score.into_inner(), - working_capital: value.working_capital.into_inner(), - total_assets: value.total_assets.into_inner(), - retained_earnings: value.retained_earnings.into_inner(), - ebit: value.ebit.into_inner(), - market_cap: value.market_cap.into_inner(), - total_liabilities: value.total_liabilities.into_inner(), - revenue: value.revenue.into_inner(), + reported_currency: value.reported_currency.map(|value| value.into_inner()), + altman_z_score: value.altman_z_score.map(|value| value.into_inner()), + piotroski_score: value.piotroski_score.map(|value| value.into_inner()), + working_capital: value.working_capital.map(|value| value.into_inner()), + total_assets: value.total_assets.map(|value| value.into_inner()), + retained_earnings: value.retained_earnings.map(|value| value.into_inner()), + ebit: value.ebit.map(|value| value.into_inner()), + market_cap: value.market_cap.map(|value| value.into_inner()), + total_liabilities: value.total_liabilities.map(|value| value.into_inner()), + revenue: value.revenue.map(|value| value.into_inner()), } } } @@ -588,21 +597,21 @@ pub(crate) struct BulkPriceTargetSummary { #[pyo3(get)] pub symbol: String, #[pyo3(get)] - pub last_month_count: String, + pub last_month_count: Option, #[pyo3(get)] - pub last_month_avg_price_target: String, + pub last_month_avg_price_target: Option, #[pyo3(get)] - pub last_quarter_count: String, + pub last_quarter_count: Option, #[pyo3(get)] - pub last_quarter_avg_price_target: String, + pub last_quarter_avg_price_target: Option, #[pyo3(get)] - pub last_year_count: String, + pub last_year_count: Option, #[pyo3(get)] - pub last_year_avg_price_target: String, + pub last_year_avg_price_target: Option, #[pyo3(get)] - pub all_time_count: String, + pub all_time_count: Option, #[pyo3(get)] - pub all_time_avg_price_target: String, + pub all_time_avg_price_target: Option, #[pyo3(get)] pub publishers: String, } @@ -616,14 +625,14 @@ impl BulkPriceTargetSummary { #[pyo3(signature = (*, symbol, last_month_count, last_month_avg_price_target, last_quarter_count, last_quarter_avg_price_target, last_year_count, last_year_avg_price_target, all_time_count, all_time_avg_price_target, publishers))] fn new( symbol: String, - last_month_count: String, - last_month_avg_price_target: String, - last_quarter_count: String, - last_quarter_avg_price_target: String, - last_year_count: String, - last_year_avg_price_target: String, - all_time_count: String, - all_time_avg_price_target: String, + last_month_count: Option, + last_month_avg_price_target: Option, + last_quarter_count: Option, + last_quarter_avg_price_target: Option, + last_year_count: Option, + last_year_avg_price_target: Option, + all_time_count: Option, + all_time_avg_price_target: Option, publishers: String, ) -> Self { Self { @@ -817,14 +826,22 @@ impl From for BulkPriceTargetSu fn from(value: libfmp::responses::bulk::BulkPriceTargetSummary) -> Self { Self { symbol: value.symbol.into_inner(), - last_month_count: value.last_month_count.into_inner(), - last_month_avg_price_target: value.last_month_avg_price_target.into_inner(), - last_quarter_count: value.last_quarter_count.into_inner(), - last_quarter_avg_price_target: value.last_quarter_avg_price_target.into_inner(), - last_year_count: value.last_year_count.into_inner(), - last_year_avg_price_target: value.last_year_avg_price_target.into_inner(), - all_time_count: value.all_time_count.into_inner(), - all_time_avg_price_target: value.all_time_avg_price_target.into_inner(), + last_month_count: value.last_month_count.map(|value| value.into_inner()), + last_month_avg_price_target: value + .last_month_avg_price_target + .map(|value| value.into_inner()), + last_quarter_count: value.last_quarter_count.map(|value| value.into_inner()), + last_quarter_avg_price_target: value + .last_quarter_avg_price_target + .map(|value| value.into_inner()), + last_year_count: value.last_year_count.map(|value| value.into_inner()), + last_year_avg_price_target: value + .last_year_avg_price_target + .map(|value| value.into_inner()), + all_time_count: value.all_time_count.map(|value| value.into_inner()), + all_time_avg_price_target: value + .all_time_avg_price_target + .map(|value| value.into_inner()), publishers: value.publishers, } } @@ -845,17 +862,17 @@ pub(crate) struct BulkEtfHolding { #[pyo3(get)] pub name: String, #[pyo3(get)] - pub shares_number: String, + pub shares_number: Option, #[pyo3(get)] - pub asset: String, + pub asset: Option, #[pyo3(get)] - pub weight_percentage: String, + pub weight_percentage: Option, #[pyo3(get)] pub cusip: String, #[pyo3(get)] - pub isin: String, + pub isin: Option, #[pyo3(get)] - pub market_value: String, + pub market_value: Option, #[pyo3(get)] pub last_updated: ::chrono::NaiveDate, } @@ -870,12 +887,12 @@ impl BulkEtfHolding { fn new( symbol: String, name: String, - shares_number: String, - asset: String, - weight_percentage: String, + shares_number: Option, + asset: Option, + weight_percentage: Option, cusip: String, - isin: String, - market_value: String, + isin: Option, + market_value: Option, last_updated: ::chrono::NaiveDate, ) -> Self { Self { @@ -1022,12 +1039,12 @@ impl From for BulkEtfHolding { Self { symbol: value.symbol.into_inner(), name: value.name, - shares_number: value.shares_number.into_inner(), - asset: value.asset.into_inner(), - weight_percentage: value.weight_percentage.into_inner(), + shares_number: value.shares_number.map(|value| value.into_inner()), + asset: value.asset.map(|value| value.into_inner()), + weight_percentage: value.weight_percentage.map(|value| value.into_inner()), cusip: value.cusip, - isin: value.isin.into_inner(), - market_value: value.market_value.into_inner(), + isin: value.isin.map(|value| value.into_inner()), + market_value: value.market_value.map(|value| value.into_inner()), last_updated: value.last_updated.into_inner(), } } @@ -1046,15 +1063,15 @@ pub(crate) struct BulkUpgradesDowngradesConsensus { #[pyo3(get)] pub symbol: String, #[pyo3(get)] - pub strong_buy: String, + pub strong_buy: Option, #[pyo3(get)] - pub buy: String, + pub buy: Option, #[pyo3(get)] - pub hold: String, + pub hold: Option, #[pyo3(get)] - pub sell: String, + pub sell: Option, #[pyo3(get)] - pub strong_sell: String, + pub strong_sell: Option, #[pyo3(get)] pub consensus: String, } @@ -1068,11 +1085,11 @@ impl BulkUpgradesDowngradesConsensus { #[pyo3(signature = (*, symbol, strong_buy, buy, hold, sell, strong_sell, consensus))] fn new( symbol: String, - strong_buy: String, - buy: String, - hold: String, - sell: String, - strong_sell: String, + strong_buy: Option, + buy: Option, + hold: Option, + sell: Option, + strong_sell: Option, consensus: String, ) -> Self { Self { @@ -1186,11 +1203,11 @@ impl From fn from(value: libfmp::responses::bulk::BulkUpgradesDowngradesConsensus) -> Self { Self { symbol: value.symbol, - strong_buy: value.strong_buy.into_inner(), - buy: value.buy.into_inner(), - hold: value.hold.into_inner(), - sell: value.sell.into_inner(), - strong_sell: value.strong_sell.into_inner(), + strong_buy: value.strong_buy.map(|value| value.into_inner()), + buy: value.buy.map(|value| value.into_inner()), + hold: value.hold.map(|value| value.into_inner()), + sell: value.sell.map(|value| value.into_inner()), + strong_sell: value.strong_sell.map(|value| value.into_inner()), consensus: value.consensus, } } diff --git a/crates/fmp-py/tests/test_bulk_a.py b/crates/fmp-py/tests/test_bulk_a.py index 239c22d5..a6e81475 100644 --- a/crates/fmp-py/tests/test_bulk_a.py +++ b/crates/fmp-py/tests/test_bulk_a.py @@ -83,13 +83,15 @@ def test_dcf_valuations_decodes_the_spaced_stock_price_column(client: Any, fixtu rows = client.bulk.dcf_valuations() assert fixture_server.requests[0].target == "/dcf-bulk" - assert len(rows) == 2 + assert len(rows) == 3 row = rows[0] assert isinstance(row, BulkDcfValuation) assert row.symbol == "000006.SZ" assert row.date == datetime.date(2026, 9, 29) assert row.dcf == "2.525226853334803" assert row.stock_price == "7.62" + assert rows[2].dcf is None + assert rows[2].stock_price == "1.72" def test_financial_scores_keeps_string_backed_numbers(client: Any, fixture_server: FixtureServer) -> None: @@ -98,7 +100,7 @@ def test_financial_scores_keeps_string_backed_numbers(client: Any, fixture_serve rows = client.bulk.financial_scores() assert fixture_server.requests[0].target == "/scores-bulk" - assert len(rows) == 2 + assert len(rows) == 3 row = rows[0] assert isinstance(row, BulkFinancialScore) assert row.symbol == "000001.SZ" @@ -106,6 +108,8 @@ def test_financial_scores_keeps_string_backed_numbers(client: Any, fixture_serve assert row.altman_z_score == "-0.06634014283050256" assert row.piotroski_score == "4" assert row.market_cap == "219286875637" + assert rows[2].reported_currency is None + assert rows[2].altman_z_score is None def test_price_target_summaries_preserves_the_raw_publishers_cell(client: Any, fixture_server: FixtureServer) -> None: @@ -129,7 +133,7 @@ def test_etf_holdings_form_encodes_the_part(client: Any, fixture_server: Fixture rows = client.bulk.etf_holdings("part 1/beta") assert fixture_server.requests[0].target == "/etf-holder-bulk?part=part+1%2Fbeta" - assert len(rows) == 2 + assert len(rows) == 4 row = rows[0] assert isinstance(row, BulkEtfHolding) assert row.symbol == " -- " @@ -137,6 +141,7 @@ def test_etf_holdings_form_encodes_the_part(client: Any, fixture_server: Fixture assert row.isin == "US88642R1095" assert row.weight_percentage == "2.63" assert row.last_updated == datetime.date(2026, 9, 27) + assert (rows[2].asset, rows[2].isin, rows[2].cusip) == (None, None, "") def test_upgrades_downgrades_consensus_takes_no_arguments(client: Any, fixture_server: FixtureServer) -> None: diff --git a/crates/libfmp/src/responses/bulk/snapshots.rs b/crates/libfmp/src/responses/bulk/snapshots.rs index 09e22c13..0b2933fc 100644 --- a/crates/libfmp/src/responses/bulk/snapshots.rs +++ b/crates/libfmp/src/responses/bulk/snapshots.rs @@ -15,12 +15,12 @@ pub struct BulkStockRating { pub symbol: Ticker, pub date: Date, pub rating: String, - pub discounted_cash_flow_score: NumericString, - pub return_on_equity_score: NumericString, - pub return_on_assets_score: NumericString, - pub debt_to_equity_score: NumericString, - pub price_to_earnings_score: NumericString, - pub price_to_book_score: NumericString, + pub discounted_cash_flow_score: Option, + pub return_on_equity_score: Option, + pub return_on_assets_score: Option, + pub debt_to_equity_score: Option, + pub price_to_earnings_score: Option, + pub price_to_book_score: Option, } /// One worldwide discounted-cash-flow bulk row. @@ -30,9 +30,9 @@ pub struct BulkStockRating { pub struct BulkDcfValuation { pub symbol: Ticker, pub date: Date, - pub dcf: NumericString, + pub dcf: Option, #[serde(rename = "Stock Price")] - pub stock_price: NumericString, + pub stock_price: Option, } /// One worldwide financial-score bulk row. @@ -41,16 +41,17 @@ pub struct BulkDcfValuation { #[serde(rename_all = "camelCase")] pub struct BulkFinancialScore { pub symbol: Ticker, - pub reported_currency: CurrencyCode, - pub altman_z_score: NumericString, - pub piotroski_score: NumericString, - pub working_capital: NumericString, - pub total_assets: NumericString, - pub retained_earnings: NumericString, - pub ebit: NumericString, - pub market_cap: NumericString, - pub total_liabilities: NumericString, - pub revenue: NumericString, + #[serde(deserialize_with = "crate::codecs::empty_or_null::deserialize")] + pub reported_currency: Option, + pub altman_z_score: Option, + pub piotroski_score: Option, + pub working_capital: Option, + pub total_assets: Option, + pub retained_earnings: Option, + pub ebit: Option, + pub market_cap: Option, + pub total_liabilities: Option, + pub revenue: Option, } /// One US price-target-summary bulk row. @@ -59,14 +60,14 @@ pub struct BulkFinancialScore { #[serde(rename_all = "camelCase")] pub struct BulkPriceTargetSummary { pub symbol: Ticker, - pub last_month_count: NumericString, - pub last_month_avg_price_target: NumericString, - pub last_quarter_count: NumericString, - pub last_quarter_avg_price_target: NumericString, - pub last_year_count: NumericString, - pub last_year_avg_price_target: NumericString, - pub all_time_count: NumericString, - pub all_time_avg_price_target: NumericString, + pub last_month_count: Option, + pub last_month_avg_price_target: Option, + pub last_quarter_count: Option, + pub last_quarter_avg_price_target: Option, + pub last_year_count: Option, + pub last_year_avg_price_target: Option, + pub all_time_count: Option, + pub all_time_avg_price_target: Option, pub publishers: String, } @@ -77,12 +78,14 @@ pub struct BulkPriceTargetSummary { pub struct BulkEtfHolding { pub symbol: Ticker, pub name: String, - pub shares_number: NumericString, - pub asset: Ticker, - pub weight_percentage: NumericString, + pub shares_number: Option, + #[serde(deserialize_with = "crate::codecs::empty_or_null::deserialize")] + pub asset: Option, + pub weight_percentage: Option, pub cusip: String, - pub isin: Isin, - pub market_value: NumericString, + #[serde(deserialize_with = "crate::codecs::empty_or_null::deserialize")] + pub isin: Option, + pub market_value: Option, pub last_updated: Date, } @@ -92,10 +95,10 @@ pub struct BulkEtfHolding { #[serde(rename_all = "camelCase")] pub struct BulkUpgradesDowngradesConsensus { pub symbol: String, - pub strong_buy: NumericString, - pub buy: NumericString, - pub hold: NumericString, - pub sell: NumericString, - pub strong_sell: NumericString, + pub strong_buy: Option, + pub buy: Option, + pub hold: Option, + pub sell: Option, + pub strong_sell: Option, pub consensus: String, } diff --git a/crates/libfmp/tests/bulk_snapshot_endpoints.rs b/crates/libfmp/tests/bulk_snapshot_endpoints.rs index f64ebcca..58eaed58 100644 --- a/crates/libfmp/tests/bulk_snapshot_endpoints.rs +++ b/crates/libfmp/tests/bulk_snapshot_endpoints.rs @@ -182,10 +182,13 @@ async fn custom_proxy_sends_one_exact_request_per_method_with_auth_headers_and_f assert_eq!(profile[1].symbol.as_str(), "AMAT"); assert_eq!(rating[1].rating, "C+"); - assert_eq!(dcf[1].stock_price.as_str(), "2.39"); - assert_eq!(scores[0].reported_currency.as_str(), "CNY"); + assert_eq!(dcf[1].stock_price.as_ref().unwrap().as_str(), "2.39"); + assert_eq!( + scores[0].reported_currency.as_ref().unwrap().as_str(), + "CNY" + ); assert_eq!(target[1].symbol.as_str(), "AA"); - assert_eq!(etf[1].asset.as_str(), "3665.TW"); + assert_eq!(etf[1].asset.as_ref().unwrap().as_str(), "3665.TW"); assert_eq!(consensus[1].consensus, "Hold"); let requests = executor.requests(); diff --git a/crates/libfmp/tests/bulk_snapshot_responses.rs b/crates/libfmp/tests/bulk_snapshot_responses.rs index 2d9b1aa8..d1b4a14f 100644 --- a/crates/libfmp/tests/bulk_snapshot_responses.rs +++ b/crates/libfmp/tests/bulk_snapshot_responses.rs @@ -12,7 +12,9 @@ use libfmp::{ types::{BulkPart, Date, Ticker}, }; -use bulk_csv::{assert_empty_bodies, assert_members, assert_round_trip, decode}; +use bulk_csv::{ + assert_empty_bodies, assert_members, assert_required_members, assert_round_trip, decode, +}; const PROFILE: &[u8] = include_bytes!("fixtures/bulk_company_profiles.csv"); const RATING: &[u8] = include_bytes!("fixtures/bulk_stock_ratings.csv"); @@ -38,7 +40,7 @@ fn live_csv_fixtures_round_trip_every_cell_with_exact_field_counts() { } #[test] -fn every_column_is_required_and_only_optional_or_text_members_accept_an_empty_cell() { +fn every_column_is_required_and_only_identity_members_reject_an_empty_cell() { assert_members( &bulk_company_profiles(part()), PROFILE, @@ -61,16 +63,12 @@ fn every_column_is_required_and_only_optional_or_text_members_accept_an_empty_ce "ipoDate", ], ); - assert_members(&bulk_stock_ratings(), RATING, &["rating"]); - assert_members(&bulk_dcf_valuations(), DCF, &[]); - assert_members(&bulk_financial_scores(), SCORES, &[]); - assert_members(&bulk_price_target_summaries(), TARGET, &["publishers"]); - assert_members(&bulk_etf_holdings(part()), ETF, &["name", "cusip"]); - assert_members( - &bulk_upgrades_downgrades_consensus(), - CONSENSUS, - &["symbol", "consensus"], - ); + assert_required_members(&bulk_stock_ratings(), RATING, &["symbol", "date"]); + assert_required_members(&bulk_dcf_valuations(), DCF, &["symbol", "date"]); + assert_required_members(&bulk_financial_scores(), SCORES, &["symbol"]); + assert_required_members(&bulk_price_target_summaries(), TARGET, &["symbol"]); + assert_required_members(&bulk_etf_holdings(part()), ETF, &["symbol", "lastUpdated"]); + assert_required_members(&bulk_upgrades_downgrades_consensus(), CONSENSUS, &[]); } #[test] @@ -86,9 +84,9 @@ fn empty_and_header_only_bodies_decode_to_no_rows() { #[test] fn provider_spellings_and_quoted_text_are_preserved_verbatim() { - let dcf = decode(&bulk_dcf_valuations(), DCF).unwrap().remove(0); - assert_eq!(dcf.stock_price.as_str(), "7.62"); - assert_eq!(dcf.dcf.as_str(), "2.525226853334803"); + let dcf = decode(&bulk_dcf_valuations(), DCF).unwrap(); + assert_eq!(dcf[0].stock_price.as_ref().unwrap().as_str(), "7.62"); + assert_eq!(dcf[0].dcf.as_ref().unwrap().as_str(), "2.525226853334803"); let target = decode(&bulk_price_target_summaries(), TARGET) .unwrap() @@ -100,7 +98,7 @@ fn provider_spellings_and_quoted_text_are_preserved_verbatim() { let etf = decode(&bulk_etf_holdings(part()), ETF).unwrap().remove(0); assert_eq!(etf.symbol.as_str(), " -- "); - assert_eq!(etf.asset, Ticker::new("TDW").unwrap()); + assert_eq!(etf.asset, Some(Ticker::new("TDW").unwrap())); assert_eq!(etf.last_updated, Date::parse("2026-09-27").unwrap()); let profile = decode(&bulk_company_profiles(part()), PROFILE) @@ -110,3 +108,24 @@ fn provider_spellings_and_quoted_text_are_preserved_verbatim() { assert_eq!(profile.company_name, "The Williams Companies, Inc."); assert!(!profile.is_etf); } + +#[test] +fn live_empty_cells_decode_as_absent_members_and_empty_text() { + let dcf = decode(&bulk_dcf_valuations(), DCF).unwrap().remove(2); + assert_eq!(dcf.symbol.as_str(), "000023.SZ"); + assert_eq!(dcf.dcf, None); + assert_eq!(dcf.stock_price.unwrap().as_str(), "1.72"); + + let scores = decode(&bulk_financial_scores(), SCORES).unwrap().remove(2); + assert_eq!(scores.symbol.as_str(), "AAAU"); + assert_eq!(scores.reported_currency, None); + assert_eq!(scores.altman_z_score, None); + assert_eq!(scores.piotroski_score.unwrap().as_str(), "2"); + + let etf = decode(&bulk_etf_holdings(part()), ETF).unwrap().remove(2); + assert_eq!(etf.name, "Other/Cash"); + assert_eq!(etf.asset, None); + assert_eq!(etf.cusip, ""); + assert_eq!(etf.isin, None); + assert_eq!(etf.shares_number.unwrap().as_str(), "0"); +} diff --git a/crates/libfmp/tests/fixtures/bulk_dcf_valuations.csv b/crates/libfmp/tests/fixtures/bulk_dcf_valuations.csv index fdef9f6e..a45947be 100644 --- a/crates/libfmp/tests/fixtures/bulk_dcf_valuations.csv +++ b/crates/libfmp/tests/fixtures/bulk_dcf_valuations.csv @@ -1,3 +1,4 @@ "symbol","date","dcf","Stock Price" "000006.SZ","2026-09-29",2.525226853334803,7.62 "000008.SZ","2026-09-29",-5.17533839393595,2.39 +"000023.SZ","2024-07-29",,1.72 diff --git a/crates/libfmp/tests/fixtures/bulk_etf_holdings.csv b/crates/libfmp/tests/fixtures/bulk_etf_holdings.csv index 361ae769..05d9cb6c 100644 --- a/crates/libfmp/tests/fixtures/bulk_etf_holdings.csv +++ b/crates/libfmp/tests/fixtures/bulk_etf_holdings.csv @@ -1,3 +1,5 @@ "symbol","name","sharesNumber","asset","weightPercentage","cusip","isin","marketValue","lastUpdated" " -- ","Tidewater Inc",91962,"TDW",2.63,"88642R109","US88642R1095",6457163.796,"2026-09-27" "0050.TW","Bizlink Holding Inc.",4103274,"3665.TW",0.39,"G11474106","KYG114741062",9672220672.39,"2026-09-24" +" -- ","Other/Cash",0,"",0.15,"","",360428.571,"2026-09-27" +" -- ","Expro Group Holdings Nv",134790,"",0.74,"","",1823848.8606,"2026-09-27" diff --git a/crates/libfmp/tests/fixtures/bulk_financial_scores.csv b/crates/libfmp/tests/fixtures/bulk_financial_scores.csv index 47b55404..a60f6848 100644 --- a/crates/libfmp/tests/fixtures/bulk_financial_scores.csv +++ b/crates/libfmp/tests/fixtures/bulk_financial_scores.csv @@ -1,3 +1,4 @@ "symbol","reportedCurrency","altmanZScore","piotroskiScore","workingCapital","totalAssets","retainedEarnings","ebit","marketCap","totalLiabilities","revenue" "000001.SZ","CNY",-0.06634014283050256,4,-1105320000000,6031727000000,287857000000,52022000000,219286875637,5563513000000,208921000000 "000002.SZ","CNY",0.04042819128279265,4,99594827000,959761483000,-59369866000,-69715634000,44168410698,743955251000,198278574000 +"AAAU",,,2,0,524010983,0,-96900994,624598741,0,0 diff --git a/crates/libfmp/tests/support/bulk_csv.rs b/crates/libfmp/tests/support/bulk_csv.rs index fdfca99d..7d836d5a 100644 --- a/crates/libfmp/tests/support/bulk_csv.rs +++ b/crates/libfmp/tests/support/bulk_csv.rs @@ -78,10 +78,36 @@ where /// model member's cell decodes only for a member in `blankable`, and is /// otherwise a null at `[0].member`; dropping a model member's column is a missing member even /// for an `Option`, while any other column, and an unknown one, is ignored. +#[allow(dead_code)] // Each suite uses the member contract that fits its rows. pub fn assert_members(endpoint: &EndpointSpec>, fixture: &[u8], blankable: &[&str]) where Q: QueryParameters, R: Serialize, +{ + check_members(endpoint, fixture, |member| blankable.contains(&member)); +} + +/// [`assert_members`] for a bulk CSV row under ADR 0035, where every member +/// accepts an empty cell except the identity members in `required`. +#[allow(dead_code)] // Each suite uses the member contract that fits its rows. +pub fn assert_required_members( + endpoint: &EndpointSpec>, + fixture: &[u8], + required: &[&str], +) where + Q: QueryParameters, + R: Serialize, +{ + check_members(endpoint, fixture, |member| !required.contains(&member)); +} + +fn check_members( + endpoint: &EndpointSpec>, + fixture: &[u8], + blankable: impl Fn(&str) -> bool, +) where + Q: QueryParameters, + R: Serialize, { let (header, records) = read(fixture); let rows = decode(endpoint, fixture).unwrap(); @@ -92,7 +118,7 @@ where let mut blank = records[0].clone(); blank[column].clear(); let result = decode(endpoint, write(&header, &blank)); - if blankable.contains(&member.as_str()) || !members.contains_key(member) { + if blankable(member) || !members.contains_key(member) { assert!(result.is_ok(), "blank {member} did not decode"); } else { assert_failure(result, member, DecodeErrorKind::Null); diff --git a/sdk/go/bulk_models.go b/sdk/go/bulk_models.go index 6e6dfecb..1de9bd9f 100644 --- a/sdk/go/bulk_models.go +++ b/sdk/go/bulk_models.go @@ -2275,15 +2275,15 @@ func (m *BulkEarningsSurprise) UnmarshalJSONFrom(dec *jsontext.Decoder) error { // BulkStockRating is one worldwide stock-rating bulk row. type BulkStockRating struct { - Symbol string `json:"symbol"` - Date Date `json:"date"` - Rating string `json:"rating"` - DiscountedCashFlowScore string `json:"discountedCashFlowScore"` - ReturnOnEquityScore string `json:"returnOnEquityScore"` - ReturnOnAssetsScore string `json:"returnOnAssetsScore"` - DebtToEquityScore string `json:"debtToEquityScore"` - PriceToEarningsScore string `json:"priceToEarningsScore"` - PriceToBookScore string `json:"priceToBookScore"` + Symbol string `json:"symbol"` + Date Date `json:"date"` + Rating string `json:"rating"` + DiscountedCashFlowScore *string `json:"discountedCashFlowScore"` + ReturnOnEquityScore *string `json:"returnOnEquityScore"` + ReturnOnAssetsScore *string `json:"returnOnAssetsScore"` + DebtToEquityScore *string `json:"debtToEquityScore"` + PriceToEarningsScore *string `json:"priceToEarningsScore"` + PriceToBookScore *string `json:"priceToBookScore"` } // bulkStockRatingShadow mirrors BulkStockRating with a pointer or raw value @@ -2316,39 +2316,27 @@ func (m *BulkStockRating) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("BulkStockRating", "date") case shadow.Rating == nil: return missingMemberError("BulkStockRating", "rating") - case shadow.DiscountedCashFlowScore == nil: - return missingMemberError("BulkStockRating", "discountedCashFlowScore") - case shadow.ReturnOnEquityScore == nil: - return missingMemberError("BulkStockRating", "returnOnEquityScore") - case shadow.ReturnOnAssetsScore == nil: - return missingMemberError("BulkStockRating", "returnOnAssetsScore") - case shadow.DebtToEquityScore == nil: - return missingMemberError("BulkStockRating", "debtToEquityScore") - case shadow.PriceToEarningsScore == nil: - return missingMemberError("BulkStockRating", "priceToEarningsScore") - case shadow.PriceToBookScore == nil: - return missingMemberError("BulkStockRating", "priceToBookScore") } *m = BulkStockRating{ Symbol: *shadow.Symbol, Date: *shadow.Date, Rating: *shadow.Rating, - DiscountedCashFlowScore: *shadow.DiscountedCashFlowScore, - ReturnOnEquityScore: *shadow.ReturnOnEquityScore, - ReturnOnAssetsScore: *shadow.ReturnOnAssetsScore, - DebtToEquityScore: *shadow.DebtToEquityScore, - PriceToEarningsScore: *shadow.PriceToEarningsScore, - PriceToBookScore: *shadow.PriceToBookScore, + DiscountedCashFlowScore: shadow.DiscountedCashFlowScore, + ReturnOnEquityScore: shadow.ReturnOnEquityScore, + ReturnOnAssetsScore: shadow.ReturnOnAssetsScore, + DebtToEquityScore: shadow.DebtToEquityScore, + PriceToEarningsScore: shadow.PriceToEarningsScore, + PriceToBookScore: shadow.PriceToBookScore, } return nil } // BulkDCFValuation is one worldwide discounted-cash-flow bulk row. type BulkDCFValuation struct { - Symbol string `json:"symbol"` - Date Date `json:"date"` - DCF string `json:"dcf"` - StockPrice string `json:"Stock Price"` + Symbol string `json:"symbol"` + Date Date `json:"date"` + DCF *string `json:"dcf"` + StockPrice *string `json:"Stock Price"` } // bulkDCFValuationShadow mirrors BulkDCFValuation with a pointer or raw value @@ -2374,40 +2362,19 @@ func (m *BulkDCFValuation) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("BulkDCFValuation", "symbol") case shadow.Date == nil: return missingMemberError("BulkDCFValuation", "date") - case shadow.DCF == nil: - return missingMemberError("BulkDCFValuation", "dcf") - case shadow.StockPrice == nil: - return missingMemberError("BulkDCFValuation", "Stock Price") } *m = BulkDCFValuation{ Symbol: *shadow.Symbol, Date: *shadow.Date, - DCF: *shadow.DCF, - StockPrice: *shadow.StockPrice, + DCF: shadow.DCF, + StockPrice: shadow.StockPrice, } return nil } // BulkFinancialScore is one worldwide financial-score bulk row. type BulkFinancialScore struct { - Symbol string `json:"symbol"` - ReportedCurrency string `json:"reportedCurrency"` - AltmanZScore string `json:"altmanZScore"` - PiotroskiScore string `json:"piotroskiScore"` - WorkingCapital string `json:"workingCapital"` - TotalAssets string `json:"totalAssets"` - RetainedEarnings string `json:"retainedEarnings"` - Ebit string `json:"ebit"` - MarketCap string `json:"marketCap"` - TotalLiabilities string `json:"totalLiabilities"` - Revenue string `json:"revenue"` -} - -// bulkFinancialScoreShadow mirrors BulkFinancialScore with a pointer or raw -// value for every required member so a missing or null member is observable -// after decoding. -type bulkFinancialScoreShadow struct { - Symbol *string `json:"symbol"` + Symbol string `json:"symbol"` ReportedCurrency *string `json:"reportedCurrency"` AltmanZScore *string `json:"altmanZScore"` PiotroskiScore *string `json:"piotroskiScore"` @@ -2420,6 +2387,23 @@ type bulkFinancialScoreShadow struct { Revenue *string `json:"revenue"` } +// bulkFinancialScoreShadow mirrors BulkFinancialScore with a pointer or raw +// value for every required member so a missing or null member is observable +// after decoding. +type bulkFinancialScoreShadow struct { + Symbol *string `json:"symbol"` + ReportedCurrency jsontext.Value `json:"reportedCurrency"` + AltmanZScore *string `json:"altmanZScore"` + PiotroskiScore *string `json:"piotroskiScore"` + WorkingCapital *string `json:"workingCapital"` + TotalAssets *string `json:"totalAssets"` + RetainedEarnings *string `json:"retainedEarnings"` + Ebit *string `json:"ebit"` + MarketCap *string `json:"marketCap"` + TotalLiabilities *string `json:"totalLiabilities"` + Revenue *string `json:"revenue"` +} + // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode // error naming the first required member that is missing or null, as the // Rust decoder does. Unknown members are ignored. @@ -2431,55 +2415,47 @@ func (m *BulkFinancialScore) UnmarshalJSONFrom(dec *jsontext.Decoder) error { switch { case shadow.Symbol == nil: return missingMemberError("BulkFinancialScore", "symbol") - case shadow.ReportedCurrency == nil: + case len(shadow.ReportedCurrency) == 0: return missingMemberError("BulkFinancialScore", "reportedCurrency") - case shadow.AltmanZScore == nil: - return missingMemberError("BulkFinancialScore", "altmanZScore") - case shadow.PiotroskiScore == nil: - return missingMemberError("BulkFinancialScore", "piotroskiScore") - case shadow.WorkingCapital == nil: - return missingMemberError("BulkFinancialScore", "workingCapital") - case shadow.TotalAssets == nil: - return missingMemberError("BulkFinancialScore", "totalAssets") - case shadow.RetainedEarnings == nil: - return missingMemberError("BulkFinancialScore", "retainedEarnings") - case shadow.Ebit == nil: - return missingMemberError("BulkFinancialScore", "ebit") - case shadow.MarketCap == nil: - return missingMemberError("BulkFinancialScore", "marketCap") - case shadow.TotalLiabilities == nil: - return missingMemberError("BulkFinancialScore", "totalLiabilities") - case shadow.Revenue == nil: - return missingMemberError("BulkFinancialScore", "revenue") + } + var reportedCurrency *string + if shadow.ReportedCurrency.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.ReportedCurrency, &value); err != nil { + return memberDecodeError("BulkFinancialScore", "reportedCurrency", shadow.ReportedCurrency, err) + } + if value != "" { + reportedCurrency = &value + } } *m = BulkFinancialScore{ Symbol: *shadow.Symbol, - ReportedCurrency: *shadow.ReportedCurrency, - AltmanZScore: *shadow.AltmanZScore, - PiotroskiScore: *shadow.PiotroskiScore, - WorkingCapital: *shadow.WorkingCapital, - TotalAssets: *shadow.TotalAssets, - RetainedEarnings: *shadow.RetainedEarnings, - Ebit: *shadow.Ebit, - MarketCap: *shadow.MarketCap, - TotalLiabilities: *shadow.TotalLiabilities, - Revenue: *shadow.Revenue, + ReportedCurrency: reportedCurrency, + AltmanZScore: shadow.AltmanZScore, + PiotroskiScore: shadow.PiotroskiScore, + WorkingCapital: shadow.WorkingCapital, + TotalAssets: shadow.TotalAssets, + RetainedEarnings: shadow.RetainedEarnings, + Ebit: shadow.Ebit, + MarketCap: shadow.MarketCap, + TotalLiabilities: shadow.TotalLiabilities, + Revenue: shadow.Revenue, } return nil } // BulkPriceTargetSummary is one US price-target-summary bulk row. type BulkPriceTargetSummary struct { - Symbol string `json:"symbol"` - LastMonthCount string `json:"lastMonthCount"` - LastMonthAvgPriceTarget string `json:"lastMonthAvgPriceTarget"` - LastQuarterCount string `json:"lastQuarterCount"` - LastQuarterAvgPriceTarget string `json:"lastQuarterAvgPriceTarget"` - LastYearCount string `json:"lastYearCount"` - LastYearAvgPriceTarget string `json:"lastYearAvgPriceTarget"` - AllTimeCount string `json:"allTimeCount"` - AllTimeAvgPriceTarget string `json:"allTimeAvgPriceTarget"` - Publishers string `json:"publishers"` + Symbol string `json:"symbol"` + LastMonthCount *string `json:"lastMonthCount"` + LastMonthAvgPriceTarget *string `json:"lastMonthAvgPriceTarget"` + LastQuarterCount *string `json:"lastQuarterCount"` + LastQuarterAvgPriceTarget *string `json:"lastQuarterAvgPriceTarget"` + LastYearCount *string `json:"lastYearCount"` + LastYearAvgPriceTarget *string `json:"lastYearAvgPriceTarget"` + AllTimeCount *string `json:"allTimeCount"` + AllTimeAvgPriceTarget *string `json:"allTimeAvgPriceTarget"` + Publishers string `json:"publishers"` } // bulkPriceTargetSummaryShadow mirrors BulkPriceTargetSummary with a pointer @@ -2509,35 +2485,19 @@ func (m *BulkPriceTargetSummary) UnmarshalJSONFrom(dec *jsontext.Decoder) error switch { case shadow.Symbol == nil: return missingMemberError("BulkPriceTargetSummary", "symbol") - case shadow.LastMonthCount == nil: - return missingMemberError("BulkPriceTargetSummary", "lastMonthCount") - case shadow.LastMonthAvgPriceTarget == nil: - return missingMemberError("BulkPriceTargetSummary", "lastMonthAvgPriceTarget") - case shadow.LastQuarterCount == nil: - return missingMemberError("BulkPriceTargetSummary", "lastQuarterCount") - case shadow.LastQuarterAvgPriceTarget == nil: - return missingMemberError("BulkPriceTargetSummary", "lastQuarterAvgPriceTarget") - case shadow.LastYearCount == nil: - return missingMemberError("BulkPriceTargetSummary", "lastYearCount") - case shadow.LastYearAvgPriceTarget == nil: - return missingMemberError("BulkPriceTargetSummary", "lastYearAvgPriceTarget") - case shadow.AllTimeCount == nil: - return missingMemberError("BulkPriceTargetSummary", "allTimeCount") - case shadow.AllTimeAvgPriceTarget == nil: - return missingMemberError("BulkPriceTargetSummary", "allTimeAvgPriceTarget") case shadow.Publishers == nil: return missingMemberError("BulkPriceTargetSummary", "publishers") } *m = BulkPriceTargetSummary{ Symbol: *shadow.Symbol, - LastMonthCount: *shadow.LastMonthCount, - LastMonthAvgPriceTarget: *shadow.LastMonthAvgPriceTarget, - LastQuarterCount: *shadow.LastQuarterCount, - LastQuarterAvgPriceTarget: *shadow.LastQuarterAvgPriceTarget, - LastYearCount: *shadow.LastYearCount, - LastYearAvgPriceTarget: *shadow.LastYearAvgPriceTarget, - AllTimeCount: *shadow.AllTimeCount, - AllTimeAvgPriceTarget: *shadow.AllTimeAvgPriceTarget, + LastMonthCount: shadow.LastMonthCount, + LastMonthAvgPriceTarget: shadow.LastMonthAvgPriceTarget, + LastQuarterCount: shadow.LastQuarterCount, + LastQuarterAvgPriceTarget: shadow.LastQuarterAvgPriceTarget, + LastYearCount: shadow.LastYearCount, + LastYearAvgPriceTarget: shadow.LastYearAvgPriceTarget, + AllTimeCount: shadow.AllTimeCount, + AllTimeAvgPriceTarget: shadow.AllTimeAvgPriceTarget, Publishers: *shadow.Publishers, } return nil @@ -2545,30 +2505,30 @@ func (m *BulkPriceTargetSummary) UnmarshalJSONFrom(dec *jsontext.Decoder) error // BulkETFHolding is one worldwide ETF holding bulk row. type BulkETFHolding struct { - Symbol string `json:"symbol"` - Name string `json:"name"` - SharesNumber string `json:"sharesNumber"` - Asset string `json:"asset"` - WeightPercentage string `json:"weightPercentage"` - CUSIP string `json:"cusip"` - ISIN string `json:"isin"` - MarketValue string `json:"marketValue"` - LastUpdated Date `json:"lastUpdated"` + Symbol string `json:"symbol"` + Name string `json:"name"` + SharesNumber *string `json:"sharesNumber"` + Asset *string `json:"asset"` + WeightPercentage *string `json:"weightPercentage"` + CUSIP string `json:"cusip"` + ISIN *string `json:"isin"` + MarketValue *string `json:"marketValue"` + LastUpdated Date `json:"lastUpdated"` } // bulkETFHoldingShadow mirrors BulkETFHolding with a pointer or raw value for // every required member so a missing or null member is observable after // decoding. type bulkETFHoldingShadow struct { - Symbol *string `json:"symbol"` - Name *string `json:"name"` - SharesNumber *string `json:"sharesNumber"` - Asset *string `json:"asset"` - WeightPercentage *string `json:"weightPercentage"` - CUSIP *string `json:"cusip"` - ISIN *string `json:"isin"` - MarketValue *string `json:"marketValue"` - LastUpdated *Date `json:"lastUpdated"` + Symbol *string `json:"symbol"` + Name *string `json:"name"` + SharesNumber *string `json:"sharesNumber"` + Asset jsontext.Value `json:"asset"` + WeightPercentage *string `json:"weightPercentage"` + CUSIP *string `json:"cusip"` + ISIN jsontext.Value `json:"isin"` + MarketValue *string `json:"marketValue"` + LastUpdated *Date `json:"lastUpdated"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -2584,30 +2544,44 @@ func (m *BulkETFHolding) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("BulkETFHolding", "symbol") case shadow.Name == nil: return missingMemberError("BulkETFHolding", "name") - case shadow.SharesNumber == nil: - return missingMemberError("BulkETFHolding", "sharesNumber") - case shadow.Asset == nil: + case len(shadow.Asset) == 0: return missingMemberError("BulkETFHolding", "asset") - case shadow.WeightPercentage == nil: - return missingMemberError("BulkETFHolding", "weightPercentage") case shadow.CUSIP == nil: return missingMemberError("BulkETFHolding", "cusip") - case shadow.ISIN == nil: + case len(shadow.ISIN) == 0: return missingMemberError("BulkETFHolding", "isin") - case shadow.MarketValue == nil: - return missingMemberError("BulkETFHolding", "marketValue") case shadow.LastUpdated == nil: return missingMemberError("BulkETFHolding", "lastUpdated") } + var asset *string + if shadow.Asset.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.Asset, &value); err != nil { + return memberDecodeError("BulkETFHolding", "asset", shadow.Asset, err) + } + if value != "" { + asset = &value + } + } + var isin *string + if shadow.ISIN.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.ISIN, &value); err != nil { + return memberDecodeError("BulkETFHolding", "isin", shadow.ISIN, err) + } + if value != "" { + isin = &value + } + } *m = BulkETFHolding{ Symbol: *shadow.Symbol, Name: *shadow.Name, - SharesNumber: *shadow.SharesNumber, - Asset: *shadow.Asset, - WeightPercentage: *shadow.WeightPercentage, + SharesNumber: shadow.SharesNumber, + Asset: asset, + WeightPercentage: shadow.WeightPercentage, CUSIP: *shadow.CUSIP, - ISIN: *shadow.ISIN, - MarketValue: *shadow.MarketValue, + ISIN: isin, + MarketValue: shadow.MarketValue, LastUpdated: *shadow.LastUpdated, } return nil @@ -2616,13 +2590,13 @@ func (m *BulkETFHolding) UnmarshalJSONFrom(dec *jsontext.Decoder) error { // BulkUpgradesDowngradesConsensus is one worldwide // upgrades/downgrades-consensus bulk row. type BulkUpgradesDowngradesConsensus struct { - Symbol string `json:"symbol"` - StrongBuy string `json:"strongBuy"` - Buy string `json:"buy"` - Hold string `json:"hold"` - Sell string `json:"sell"` - StrongSell string `json:"strongSell"` - Consensus string `json:"consensus"` + Symbol string `json:"symbol"` + StrongBuy *string `json:"strongBuy"` + Buy *string `json:"buy"` + Hold *string `json:"hold"` + Sell *string `json:"sell"` + StrongSell *string `json:"strongSell"` + Consensus string `json:"consensus"` } // bulkUpgradesDowngradesConsensusShadow mirrors @@ -2649,26 +2623,16 @@ func (m *BulkUpgradesDowngradesConsensus) UnmarshalJSONFrom(dec *jsontext.Decode switch { case shadow.Symbol == nil: return missingMemberError("BulkUpgradesDowngradesConsensus", "symbol") - case shadow.StrongBuy == nil: - return missingMemberError("BulkUpgradesDowngradesConsensus", "strongBuy") - case shadow.Buy == nil: - return missingMemberError("BulkUpgradesDowngradesConsensus", "buy") - case shadow.Hold == nil: - return missingMemberError("BulkUpgradesDowngradesConsensus", "hold") - case shadow.Sell == nil: - return missingMemberError("BulkUpgradesDowngradesConsensus", "sell") - case shadow.StrongSell == nil: - return missingMemberError("BulkUpgradesDowngradesConsensus", "strongSell") case shadow.Consensus == nil: return missingMemberError("BulkUpgradesDowngradesConsensus", "consensus") } *m = BulkUpgradesDowngradesConsensus{ Symbol: *shadow.Symbol, - StrongBuy: *shadow.StrongBuy, - Buy: *shadow.Buy, - Hold: *shadow.Hold, - Sell: *shadow.Sell, - StrongSell: *shadow.StrongSell, + StrongBuy: shadow.StrongBuy, + Buy: shadow.Buy, + Hold: shadow.Hold, + Sell: shadow.Sell, + StrongSell: shadow.StrongSell, Consensus: *shadow.Consensus, } return nil diff --git a/sdk/go/bulk_parity_test.go b/sdk/go/bulk_parity_test.go index 3d1cfee1..0931c52d 100644 --- a/sdk/go/bulk_parity_test.go +++ b/sdk/go/bulk_parity_test.go @@ -46,30 +46,34 @@ func TestBulkSnapshotsDecodeExactValues(t *testing.T) { t.Fatalf("bulk_company_profiles = %+v", profiles[0]) } ratings := assertCSVFixtureParity[BulkStockRating](t, "bulk_stock_ratings.csv", 9) - if want := (BulkStockRating{Symbol: "000001.SZ", Date: mustParseDate(t, "2026-09-30"), Rating: "B-", - DiscountedCashFlowScore: "1", ReturnOnEquityScore: "3", ReturnOnAssetsScore: "2", DebtToEquityScore: "1", - PriceToEarningsScore: "4", PriceToBookScore: "4"}); ratings[0] != want { + if ratings[0].Rating != "B-" || cellText(ratings[0].PriceToBookScore) != "4" { t.Fatalf("bulk_stock_ratings = %+v", ratings[0]) } dcf := assertCSVFixtureParity[BulkDCFValuation](t, "bulk_dcf_valuations.csv", 4) - if want := (BulkDCFValuation{Symbol: "000006.SZ", Date: mustParseDate(t, "2026-09-29"), - DCF: "2.525226853334803", StockPrice: "7.62"}); dcf[0] != want { - t.Fatalf("bulk_dcf_valuations = %+v", dcf[0]) - } if encoded, err := json.Marshal(dcf[0]); err != nil || !strings.Contains(string(encoded), `"Stock Price":"7.62"`) { t.Fatalf("re-encoded dcf = %s, %v", encoded, err) } + if dcf[2].Symbol != "000023.SZ" || dcf[2].DCF != nil || cellText(dcf[2].StockPrice) != "1.72" { + t.Fatalf("empty dcf cell = %+v", dcf[2]) + } + scores := assertCSVFixtureParity[BulkFinancialScore](t, "bulk_financial_scores.csv", 11) + if scores[2].Symbol != "AAAU" || scores[2].ReportedCurrency != nil || scores[2].AltmanZScore != nil || + cellText(scores[2].PiotroskiScore) != "2" { + t.Fatalf("empty score cells = %+v", scores[2]) + } targets := assertCSVFixtureParity[BulkPriceTargetSummary](t, "bulk_price_target_summaries.csv", 10) - if targets[0].Symbol != "A" || targets[0].Publishers != `["StreetInsider","Benzinga","Pulse 2.0"]` || - targets[0].AllTimeAvgPriceTarget != "159.49" { + if targets[0].Publishers != `["StreetInsider","Benzinga","Pulse 2.0"]` || + cellText(targets[0].AllTimeAvgPriceTarget) != "159.49" { t.Fatalf("bulk_price_target_summaries = %+v", targets[0]) } holdings := assertCSVFixtureParity[BulkETFHolding](t, "bulk_etf_holdings.csv", 9) - if want := (BulkETFHolding{Symbol: " -- ", Name: "Tidewater Inc", SharesNumber: "91962", Asset: "TDW", - WeightPercentage: "2.63", CUSIP: "88642R109", ISIN: "US88642R1095", MarketValue: "6457163.796", - LastUpdated: mustParseDate(t, "2026-09-27")}); holdings[0] != want { + if holdings[0].Symbol != " -- " || cellText(holdings[0].Asset) != "TDW" || + holdings[0].LastUpdated != mustParseDate(t, "2026-09-27") { t.Fatalf("bulk_etf_holdings = %+v", holdings[0]) } + if holdings[2].Name != "Other/Cash" || holdings[2].Asset != nil || holdings[2].ISIN != nil || holdings[2].CUSIP != "" { + t.Fatalf("empty holding cells = %+v", holdings[2]) + } } // Exact values copied from crates/libfmp/tests/bulk_metrics_responses.rs, diff --git a/sdk/go/bulk_test.go b/sdk/go/bulk_test.go index 10334288..2be04b87 100644 --- a/sdk/go/bulk_test.go +++ b/sdk/go/bulk_test.go @@ -66,11 +66,11 @@ func TestBulkMethodsUseExactPathsAndWireParameterOrder(t *testing.T) { t.Fatalf("StockRatings = %+v, %v", ratings, err) } dcf, err := client.Bulk.DCFValuations(ctx) - if err != nil || len(dcf) != 2 || dcf[1].StockPrice != "2.39" { + if err != nil || len(dcf) != 3 || cellText(dcf[1].StockPrice) != "2.39" { t.Fatalf("DCFValuations = %+v, %v", dcf, err) } scores, err := client.Bulk.FinancialScores(ctx) - if err != nil || len(scores) != 2 || scores[0].PiotroskiScore != "4" { + if err != nil || len(scores) != 3 || cellText(scores[0].PiotroskiScore) != "4" { t.Fatalf("FinancialScores = %+v, %v", scores, err) } targets, err := client.Bulk.PriceTargetSummaries(ctx) @@ -78,7 +78,7 @@ func TestBulkMethodsUseExactPathsAndWireParameterOrder(t *testing.T) { t.Fatalf("PriceTargetSummaries = %+v, %v", targets, err) } holdings, err := client.Bulk.ETFHoldings(ctx, NewBulkPartQuery("segment A/7")) - if err != nil || len(holdings) != 2 || holdings[1].Asset != "3665.TW" { + if err != nil || len(holdings) != 4 || cellText(holdings[1].Asset) != "3665.TW" { t.Fatalf("ETFHoldings = %+v, %v", holdings, err) } consensus, err := client.Bulk.UpgradesDowngradesConsensus(ctx) diff --git a/sdk/go/csv_test.go b/sdk/go/csv_test.go index f70c8662..dbd1534c 100644 --- a/sdk/go/csv_test.go +++ b/sdk/go/csv_test.go @@ -201,3 +201,12 @@ func assertCSVFixtureParity[T any](t *testing.T, name string, fields int) []T { } return rows } + +// cellText returns the text of an optional numeric-string member, or "" when +// its CSV cell was empty. +func cellText(value *string) string { + if value == nil { + return "" + } + return *value +} From e337b0f22880414d72f2fde6b197af2f5bb17c62 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 11:10:58 +0800 Subject: [PATCH 23/31] fix(bulk)!: make numeric metric members optional for empty cells Under ADR 0035, an empty CSV cell is the only absence marker in bulk responses, and sparse empties vary by reporting period. Treating every numeric metric as required wrongly rejected valid live rows, so each numeric metric member now becomes optional while identity members stay required. Every NumericString member of BulkKeyMetricsTtm, BulkFinancialRatiosTtm and BulkEarningsSurprise is now Option (Optional[str] in Python, *string in Go). BulkStockPeer.peers stays a String, and an empty cell stays an empty string. Fixtures gain live rows exercising empty cells (ADAMO key metrics, ARS.AX ratios, GUD.TO epsEstimated, an empty peers list), and tests assert the absent members decode to None. Regenerated Python models and stubs and Go models are included. BREAKING CHANGE: every numeric member of BulkKeyMetricsTtm, BulkFinancialRatiosTtm and BulkEarningsSurprise is now optional in Rust, Python and Go. --- .../fmp/_native/bulk/metrics/__init__.pyi | 436 ++++++----- crates/fmp-py/src/models/bulk/metrics.rs | 710 ++++++++++-------- crates/fmp-py/tests/test_bulk_a.py | 10 +- crates/libfmp/src/responses/bulk/metrics.rs | 206 ++--- crates/libfmp/tests/bulk_metrics_endpoints.rs | 12 +- crates/libfmp/tests/bulk_metrics_responses.rs | 52 +- .../fixtures/bulk_earnings_surprises.csv | 1 + .../fixtures/bulk_financial_ratios_ttm.csv | 1 + .../tests/fixtures/bulk_key_metrics_ttm.csv | 1 + .../tests/fixtures/bulk_stock_peers.csv | 1 + sdk/go/bulk_models.go | 628 ++++++---------- sdk/go/bulk_parity_test.go | 20 +- sdk/go/bulk_test.go | 8 +- 13 files changed, 1024 insertions(+), 1062 deletions(-) diff --git a/crates/fmp-py/python/fmp/_native/bulk/metrics/__init__.pyi b/crates/fmp-py/python/fmp/_native/bulk/metrics/__init__.pyi index e0b43f33..94841fc6 100644 --- a/crates/fmp-py/python/fmp/_native/bulk/metrics/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/bulk/metrics/__init__.pyi @@ -28,9 +28,9 @@ class BulkEarningsSurprise: @property def date(self) -> datetime.date: ... @property - def eps_actual(self) -> builtins.str: ... + def eps_actual(self) -> typing.Optional[builtins.str]: ... @property - def eps_estimated(self) -> builtins.str: ... + def eps_estimated(self) -> typing.Optional[builtins.str]: ... @property def last_updated(self) -> datetime.date: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... @@ -39,8 +39,8 @@ class BulkEarningsSurprise: *, symbol: builtins.str, date: datetime.date, - eps_actual: builtins.str, - eps_estimated: builtins.str, + eps_actual: typing.Optional[builtins.str], + eps_estimated: typing.Optional[builtins.str], last_updated: datetime.date, ) -> BulkEarningsSurprise: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... @@ -121,187 +121,205 @@ class BulkFinancialRatiosTtm: @property def symbol(self) -> builtins.str: ... @property - def gross_profit_margin_ttm(self) -> builtins.str: ... + def gross_profit_margin_ttm(self) -> typing.Optional[builtins.str]: ... @property - def ebit_margin_ttm(self) -> builtins.str: ... + def ebit_margin_ttm(self) -> typing.Optional[builtins.str]: ... @property - def ebitda_margin_ttm(self) -> builtins.str: ... + def ebitda_margin_ttm(self) -> typing.Optional[builtins.str]: ... @property - def operating_profit_margin_ttm(self) -> builtins.str: ... + def operating_profit_margin_ttm(self) -> typing.Optional[builtins.str]: ... @property - def pretax_profit_margin_ttm(self) -> builtins.str: ... + def pretax_profit_margin_ttm(self) -> typing.Optional[builtins.str]: ... @property - def continuous_operations_profit_margin_ttm(self) -> builtins.str: ... + def continuous_operations_profit_margin_ttm( + self, + ) -> typing.Optional[builtins.str]: ... @property - def net_profit_margin_ttm(self) -> builtins.str: ... + def net_profit_margin_ttm(self) -> typing.Optional[builtins.str]: ... @property - def bottom_line_profit_margin_ttm(self) -> builtins.str: ... + def bottom_line_profit_margin_ttm(self) -> typing.Optional[builtins.str]: ... @property - def receivables_turnover_ttm(self) -> builtins.str: ... + def receivables_turnover_ttm(self) -> typing.Optional[builtins.str]: ... @property - def payables_turnover_ttm(self) -> builtins.str: ... + def payables_turnover_ttm(self) -> typing.Optional[builtins.str]: ... @property - def inventory_turnover_ttm(self) -> builtins.str: ... + def inventory_turnover_ttm(self) -> typing.Optional[builtins.str]: ... @property - def fixed_asset_turnover_ttm(self) -> builtins.str: ... + def fixed_asset_turnover_ttm(self) -> typing.Optional[builtins.str]: ... @property - def asset_turnover_ttm(self) -> builtins.str: ... + def asset_turnover_ttm(self) -> typing.Optional[builtins.str]: ... @property - def current_ratio_ttm(self) -> builtins.str: ... + def current_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def quick_ratio_ttm(self) -> builtins.str: ... + def quick_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def solvency_ratio_ttm(self) -> builtins.str: ... + def solvency_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def cash_ratio_ttm(self) -> builtins.str: ... + def cash_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def price_to_earnings_ratio_ttm(self) -> builtins.str: ... + def price_to_earnings_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def price_to_earnings_growth_ratio_ttm(self) -> builtins.str: ... + def price_to_earnings_growth_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def forward_price_to_earnings_growth_ratio_ttm(self) -> builtins.str: ... + def forward_price_to_earnings_growth_ratio_ttm( + self, + ) -> typing.Optional[builtins.str]: ... @property - def price_to_book_ratio_ttm(self) -> builtins.str: ... + def price_to_book_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def price_to_sales_ratio_ttm(self) -> builtins.str: ... + def price_to_sales_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def price_to_free_cash_flow_ratio_ttm(self) -> builtins.str: ... + def price_to_free_cash_flow_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def price_to_operating_cash_flow_ratio_ttm(self) -> builtins.str: ... + def price_to_operating_cash_flow_ratio_ttm( + self, + ) -> typing.Optional[builtins.str]: ... @property - def debt_to_assets_ratio_ttm(self) -> builtins.str: ... + def debt_to_assets_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def debt_to_equity_ratio_ttm(self) -> builtins.str: ... + def debt_to_equity_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def debt_to_capital_ratio_ttm(self) -> builtins.str: ... + def debt_to_capital_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def long_term_debt_to_capital_ratio_ttm(self) -> builtins.str: ... + def long_term_debt_to_capital_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def financial_leverage_ratio_ttm(self) -> builtins.str: ... + def financial_leverage_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def working_capital_turnover_ratio_ttm(self) -> builtins.str: ... + def working_capital_turnover_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def operating_cash_flow_ratio_ttm(self) -> builtins.str: ... + def operating_cash_flow_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def operating_cash_flow_sales_ratio_ttm(self) -> builtins.str: ... + def operating_cash_flow_sales_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def free_cash_flow_operating_cash_flow_ratio_ttm(self) -> builtins.str: ... + def free_cash_flow_operating_cash_flow_ratio_ttm( + self, + ) -> typing.Optional[builtins.str]: ... @property - def debt_service_coverage_ratio_ttm(self) -> builtins.str: ... + def debt_service_coverage_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def interest_coverage_ratio_ttm(self) -> builtins.str: ... + def interest_coverage_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def short_term_operating_cash_flow_coverage_ratio_ttm(self) -> builtins.str: ... + def short_term_operating_cash_flow_coverage_ratio_ttm( + self, + ) -> typing.Optional[builtins.str]: ... @property - def operating_cash_flow_coverage_ratio_ttm(self) -> builtins.str: ... + def operating_cash_flow_coverage_ratio_ttm( + self, + ) -> typing.Optional[builtins.str]: ... @property - def capital_expenditure_coverage_ratio_ttm(self) -> builtins.str: ... + def capital_expenditure_coverage_ratio_ttm( + self, + ) -> typing.Optional[builtins.str]: ... @property - def dividend_paid_and_capex_coverage_ratio_ttm(self) -> builtins.str: ... + def dividend_paid_and_capex_coverage_ratio_ttm( + self, + ) -> typing.Optional[builtins.str]: ... @property - def dividend_payout_ratio_ttm(self) -> builtins.str: ... + def dividend_payout_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def dividend_yield_ttm(self) -> builtins.str: ... + def dividend_yield_ttm(self) -> typing.Optional[builtins.str]: ... @property - def enterprise_value_ttm(self) -> builtins.str: ... + def enterprise_value_ttm(self) -> typing.Optional[builtins.str]: ... @property - def revenue_per_share_ttm(self) -> builtins.str: ... + def revenue_per_share_ttm(self) -> typing.Optional[builtins.str]: ... @property - def net_income_per_share_ttm(self) -> builtins.str: ... + def net_income_per_share_ttm(self) -> typing.Optional[builtins.str]: ... @property - def interest_debt_per_share_ttm(self) -> builtins.str: ... + def interest_debt_per_share_ttm(self) -> typing.Optional[builtins.str]: ... @property - def cash_per_share_ttm(self) -> builtins.str: ... + def cash_per_share_ttm(self) -> typing.Optional[builtins.str]: ... @property - def book_value_per_share_ttm(self) -> builtins.str: ... + def book_value_per_share_ttm(self) -> typing.Optional[builtins.str]: ... @property - def tangible_book_value_per_share_ttm(self) -> builtins.str: ... + def tangible_book_value_per_share_ttm(self) -> typing.Optional[builtins.str]: ... @property - def shareholders_equity_per_share_ttm(self) -> builtins.str: ... + def shareholders_equity_per_share_ttm(self) -> typing.Optional[builtins.str]: ... @property - def operating_cash_flow_per_share_ttm(self) -> builtins.str: ... + def operating_cash_flow_per_share_ttm(self) -> typing.Optional[builtins.str]: ... @property - def capex_per_share_ttm(self) -> builtins.str: ... + def capex_per_share_ttm(self) -> typing.Optional[builtins.str]: ... @property - def free_cash_flow_per_share_ttm(self) -> builtins.str: ... + def free_cash_flow_per_share_ttm(self) -> typing.Optional[builtins.str]: ... @property - def net_income_per_ebt_ttm(self) -> builtins.str: ... + def net_income_per_ebt_ttm(self) -> typing.Optional[builtins.str]: ... @property - def ebt_per_ebit_ttm(self) -> builtins.str: ... + def ebt_per_ebit_ttm(self) -> typing.Optional[builtins.str]: ... @property - def price_to_fair_value_ttm(self) -> builtins.str: ... + def price_to_fair_value_ttm(self) -> typing.Optional[builtins.str]: ... @property - def debt_to_market_cap_ttm(self) -> builtins.str: ... + def debt_to_market_cap_ttm(self) -> typing.Optional[builtins.str]: ... @property - def effective_tax_rate_ttm(self) -> builtins.str: ... + def effective_tax_rate_ttm(self) -> typing.Optional[builtins.str]: ... @property - def enterprise_value_multiple_ttm(self) -> builtins.str: ... + def enterprise_value_multiple_ttm(self) -> typing.Optional[builtins.str]: ... @property - def dividend_per_share_ttm(self) -> builtins.str: ... + def dividend_per_share_ttm(self) -> typing.Optional[builtins.str]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... def __new__( cls, *, symbol: builtins.str, - gross_profit_margin_ttm: builtins.str, - ebit_margin_ttm: builtins.str, - ebitda_margin_ttm: builtins.str, - operating_profit_margin_ttm: builtins.str, - pretax_profit_margin_ttm: builtins.str, - continuous_operations_profit_margin_ttm: builtins.str, - net_profit_margin_ttm: builtins.str, - bottom_line_profit_margin_ttm: builtins.str, - receivables_turnover_ttm: builtins.str, - payables_turnover_ttm: builtins.str, - inventory_turnover_ttm: builtins.str, - fixed_asset_turnover_ttm: builtins.str, - asset_turnover_ttm: builtins.str, - current_ratio_ttm: builtins.str, - quick_ratio_ttm: builtins.str, - solvency_ratio_ttm: builtins.str, - cash_ratio_ttm: builtins.str, - price_to_earnings_ratio_ttm: builtins.str, - price_to_earnings_growth_ratio_ttm: builtins.str, - forward_price_to_earnings_growth_ratio_ttm: builtins.str, - price_to_book_ratio_ttm: builtins.str, - price_to_sales_ratio_ttm: builtins.str, - price_to_free_cash_flow_ratio_ttm: builtins.str, - price_to_operating_cash_flow_ratio_ttm: builtins.str, - debt_to_assets_ratio_ttm: builtins.str, - debt_to_equity_ratio_ttm: builtins.str, - debt_to_capital_ratio_ttm: builtins.str, - long_term_debt_to_capital_ratio_ttm: builtins.str, - financial_leverage_ratio_ttm: builtins.str, - working_capital_turnover_ratio_ttm: builtins.str, - operating_cash_flow_ratio_ttm: builtins.str, - operating_cash_flow_sales_ratio_ttm: builtins.str, - free_cash_flow_operating_cash_flow_ratio_ttm: builtins.str, - debt_service_coverage_ratio_ttm: builtins.str, - interest_coverage_ratio_ttm: builtins.str, - short_term_operating_cash_flow_coverage_ratio_ttm: builtins.str, - operating_cash_flow_coverage_ratio_ttm: builtins.str, - capital_expenditure_coverage_ratio_ttm: builtins.str, - dividend_paid_and_capex_coverage_ratio_ttm: builtins.str, - dividend_payout_ratio_ttm: builtins.str, - dividend_yield_ttm: builtins.str, - enterprise_value_ttm: builtins.str, - revenue_per_share_ttm: builtins.str, - net_income_per_share_ttm: builtins.str, - interest_debt_per_share_ttm: builtins.str, - cash_per_share_ttm: builtins.str, - book_value_per_share_ttm: builtins.str, - tangible_book_value_per_share_ttm: builtins.str, - shareholders_equity_per_share_ttm: builtins.str, - operating_cash_flow_per_share_ttm: builtins.str, - capex_per_share_ttm: builtins.str, - free_cash_flow_per_share_ttm: builtins.str, - net_income_per_ebt_ttm: builtins.str, - ebt_per_ebit_ttm: builtins.str, - price_to_fair_value_ttm: builtins.str, - debt_to_market_cap_ttm: builtins.str, - effective_tax_rate_ttm: builtins.str, - enterprise_value_multiple_ttm: builtins.str, - dividend_per_share_ttm: builtins.str, + gross_profit_margin_ttm: typing.Optional[builtins.str], + ebit_margin_ttm: typing.Optional[builtins.str], + ebitda_margin_ttm: typing.Optional[builtins.str], + operating_profit_margin_ttm: typing.Optional[builtins.str], + pretax_profit_margin_ttm: typing.Optional[builtins.str], + continuous_operations_profit_margin_ttm: typing.Optional[builtins.str], + net_profit_margin_ttm: typing.Optional[builtins.str], + bottom_line_profit_margin_ttm: typing.Optional[builtins.str], + receivables_turnover_ttm: typing.Optional[builtins.str], + payables_turnover_ttm: typing.Optional[builtins.str], + inventory_turnover_ttm: typing.Optional[builtins.str], + fixed_asset_turnover_ttm: typing.Optional[builtins.str], + asset_turnover_ttm: typing.Optional[builtins.str], + current_ratio_ttm: typing.Optional[builtins.str], + quick_ratio_ttm: typing.Optional[builtins.str], + solvency_ratio_ttm: typing.Optional[builtins.str], + cash_ratio_ttm: typing.Optional[builtins.str], + price_to_earnings_ratio_ttm: typing.Optional[builtins.str], + price_to_earnings_growth_ratio_ttm: typing.Optional[builtins.str], + forward_price_to_earnings_growth_ratio_ttm: typing.Optional[builtins.str], + price_to_book_ratio_ttm: typing.Optional[builtins.str], + price_to_sales_ratio_ttm: typing.Optional[builtins.str], + price_to_free_cash_flow_ratio_ttm: typing.Optional[builtins.str], + price_to_operating_cash_flow_ratio_ttm: typing.Optional[builtins.str], + debt_to_assets_ratio_ttm: typing.Optional[builtins.str], + debt_to_equity_ratio_ttm: typing.Optional[builtins.str], + debt_to_capital_ratio_ttm: typing.Optional[builtins.str], + long_term_debt_to_capital_ratio_ttm: typing.Optional[builtins.str], + financial_leverage_ratio_ttm: typing.Optional[builtins.str], + working_capital_turnover_ratio_ttm: typing.Optional[builtins.str], + operating_cash_flow_ratio_ttm: typing.Optional[builtins.str], + operating_cash_flow_sales_ratio_ttm: typing.Optional[builtins.str], + free_cash_flow_operating_cash_flow_ratio_ttm: typing.Optional[builtins.str], + debt_service_coverage_ratio_ttm: typing.Optional[builtins.str], + interest_coverage_ratio_ttm: typing.Optional[builtins.str], + short_term_operating_cash_flow_coverage_ratio_ttm: typing.Optional[ + builtins.str + ], + operating_cash_flow_coverage_ratio_ttm: typing.Optional[builtins.str], + capital_expenditure_coverage_ratio_ttm: typing.Optional[builtins.str], + dividend_paid_and_capex_coverage_ratio_ttm: typing.Optional[builtins.str], + dividend_payout_ratio_ttm: typing.Optional[builtins.str], + dividend_yield_ttm: typing.Optional[builtins.str], + enterprise_value_ttm: typing.Optional[builtins.str], + revenue_per_share_ttm: typing.Optional[builtins.str], + net_income_per_share_ttm: typing.Optional[builtins.str], + interest_debt_per_share_ttm: typing.Optional[builtins.str], + cash_per_share_ttm: typing.Optional[builtins.str], + book_value_per_share_ttm: typing.Optional[builtins.str], + tangible_book_value_per_share_ttm: typing.Optional[builtins.str], + shareholders_equity_per_share_ttm: typing.Optional[builtins.str], + operating_cash_flow_per_share_ttm: typing.Optional[builtins.str], + capex_per_share_ttm: typing.Optional[builtins.str], + free_cash_flow_per_share_ttm: typing.Optional[builtins.str], + net_income_per_ebt_ttm: typing.Optional[builtins.str], + ebt_per_ebit_ttm: typing.Optional[builtins.str], + price_to_fair_value_ttm: typing.Optional[builtins.str], + debt_to_market_cap_ttm: typing.Optional[builtins.str], + effective_tax_rate_ttm: typing.Optional[builtins.str], + enterprise_value_multiple_ttm: typing.Optional[builtins.str], + dividend_per_share_ttm: typing.Optional[builtins.str], ) -> BulkFinancialRatiosTtm: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... @@ -364,136 +382,142 @@ class BulkKeyMetricsTtm: @property def symbol(self) -> builtins.str: ... @property - def market_cap(self) -> builtins.str: ... + def market_cap(self) -> typing.Optional[builtins.str]: ... @property - def enterprise_value_ttm(self) -> builtins.str: ... + def enterprise_value_ttm(self) -> typing.Optional[builtins.str]: ... @property - def ev_to_sales_ttm(self) -> builtins.str: ... + def ev_to_sales_ttm(self) -> typing.Optional[builtins.str]: ... @property - def ev_to_operating_cash_flow_ttm(self) -> builtins.str: ... + def ev_to_operating_cash_flow_ttm(self) -> typing.Optional[builtins.str]: ... @property - def ev_to_free_cash_flow_ttm(self) -> builtins.str: ... + def ev_to_free_cash_flow_ttm(self) -> typing.Optional[builtins.str]: ... @property - def ev_to_ebitda_ttm(self) -> builtins.str: ... + def ev_to_ebitda_ttm(self) -> typing.Optional[builtins.str]: ... @property - def net_debt_to_ebitda_ttm(self) -> builtins.str: ... + def net_debt_to_ebitda_ttm(self) -> typing.Optional[builtins.str]: ... @property - def current_ratio_ttm(self) -> builtins.str: ... + def current_ratio_ttm(self) -> typing.Optional[builtins.str]: ... @property - def income_quality_ttm(self) -> builtins.str: ... + def income_quality_ttm(self) -> typing.Optional[builtins.str]: ... @property - def graham_number_ttm(self) -> builtins.str: ... + def graham_number_ttm(self) -> typing.Optional[builtins.str]: ... @property - def graham_net_net_ttm(self) -> builtins.str: ... + def graham_net_net_ttm(self) -> typing.Optional[builtins.str]: ... @property - def tax_burden_ttm(self) -> builtins.str: ... + def tax_burden_ttm(self) -> typing.Optional[builtins.str]: ... @property - def interest_burden_ttm(self) -> builtins.str: ... + def interest_burden_ttm(self) -> typing.Optional[builtins.str]: ... @property - def working_capital_ttm(self) -> builtins.str: ... + def working_capital_ttm(self) -> typing.Optional[builtins.str]: ... @property - def invested_capital_ttm(self) -> builtins.str: ... + def invested_capital_ttm(self) -> typing.Optional[builtins.str]: ... @property - def return_on_assets_ttm(self) -> builtins.str: ... + def return_on_assets_ttm(self) -> typing.Optional[builtins.str]: ... @property - def operating_return_on_assets_ttm(self) -> builtins.str: ... + def operating_return_on_assets_ttm(self) -> typing.Optional[builtins.str]: ... @property - def return_on_tangible_assets_ttm(self) -> builtins.str: ... + def return_on_tangible_assets_ttm(self) -> typing.Optional[builtins.str]: ... @property - def return_on_equity_ttm(self) -> builtins.str: ... + def return_on_equity_ttm(self) -> typing.Optional[builtins.str]: ... @property - def return_on_invested_capital_ttm(self) -> builtins.str: ... + def return_on_invested_capital_ttm(self) -> typing.Optional[builtins.str]: ... @property - def return_on_capital_employed_ttm(self) -> builtins.str: ... + def return_on_capital_employed_ttm(self) -> typing.Optional[builtins.str]: ... @property - def earnings_yield_ttm(self) -> builtins.str: ... + def earnings_yield_ttm(self) -> typing.Optional[builtins.str]: ... @property - def free_cash_flow_yield_ttm(self) -> builtins.str: ... + def free_cash_flow_yield_ttm(self) -> typing.Optional[builtins.str]: ... @property - def capex_to_operating_cash_flow_ttm(self) -> builtins.str: ... + def capex_to_operating_cash_flow_ttm(self) -> typing.Optional[builtins.str]: ... @property - def capex_to_depreciation_ttm(self) -> builtins.str: ... + def capex_to_depreciation_ttm(self) -> typing.Optional[builtins.str]: ... @property - def capex_to_revenue_ttm(self) -> builtins.str: ... + def capex_to_revenue_ttm(self) -> typing.Optional[builtins.str]: ... @property - def sales_general_and_administrative_to_revenue_ttm(self) -> builtins.str: ... + def sales_general_and_administrative_to_revenue_ttm( + self, + ) -> typing.Optional[builtins.str]: ... @property - def research_and_development_to_revenue_ttm(self) -> builtins.str: ... + def research_and_development_to_revenue_ttm( + self, + ) -> typing.Optional[builtins.str]: ... @property - def stock_based_compensation_to_revenue_ttm(self) -> builtins.str: ... + def stock_based_compensation_to_revenue_ttm( + self, + ) -> typing.Optional[builtins.str]: ... @property - def intangibles_to_total_assets_ttm(self) -> builtins.str: ... + def intangibles_to_total_assets_ttm(self) -> typing.Optional[builtins.str]: ... @property - def average_receivables_ttm(self) -> builtins.str: ... + def average_receivables_ttm(self) -> typing.Optional[builtins.str]: ... @property - def average_payables_ttm(self) -> builtins.str: ... + def average_payables_ttm(self) -> typing.Optional[builtins.str]: ... @property - def average_inventory_ttm(self) -> builtins.str: ... + def average_inventory_ttm(self) -> typing.Optional[builtins.str]: ... @property - def days_of_sales_outstanding_ttm(self) -> builtins.str: ... + def days_of_sales_outstanding_ttm(self) -> typing.Optional[builtins.str]: ... @property - def days_of_payables_outstanding_ttm(self) -> builtins.str: ... + def days_of_payables_outstanding_ttm(self) -> typing.Optional[builtins.str]: ... @property - def days_of_inventory_outstanding_ttm(self) -> builtins.str: ... + def days_of_inventory_outstanding_ttm(self) -> typing.Optional[builtins.str]: ... @property - def operating_cycle_ttm(self) -> builtins.str: ... + def operating_cycle_ttm(self) -> typing.Optional[builtins.str]: ... @property - def cash_conversion_cycle_ttm(self) -> builtins.str: ... + def cash_conversion_cycle_ttm(self) -> typing.Optional[builtins.str]: ... @property - def free_cash_flow_to_equity_ttm(self) -> builtins.str: ... + def free_cash_flow_to_equity_ttm(self) -> typing.Optional[builtins.str]: ... @property - def free_cash_flow_to_firm_ttm(self) -> builtins.str: ... + def free_cash_flow_to_firm_ttm(self) -> typing.Optional[builtins.str]: ... @property - def tangible_asset_value_ttm(self) -> builtins.str: ... + def tangible_asset_value_ttm(self) -> typing.Optional[builtins.str]: ... @property - def net_current_asset_value_ttm(self) -> builtins.str: ... + def net_current_asset_value_ttm(self) -> typing.Optional[builtins.str]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... def __new__( cls, *, symbol: builtins.str, - market_cap: builtins.str, - enterprise_value_ttm: builtins.str, - ev_to_sales_ttm: builtins.str, - ev_to_operating_cash_flow_ttm: builtins.str, - ev_to_free_cash_flow_ttm: builtins.str, - ev_to_ebitda_ttm: builtins.str, - net_debt_to_ebitda_ttm: builtins.str, - current_ratio_ttm: builtins.str, - income_quality_ttm: builtins.str, - graham_number_ttm: builtins.str, - graham_net_net_ttm: builtins.str, - tax_burden_ttm: builtins.str, - interest_burden_ttm: builtins.str, - working_capital_ttm: builtins.str, - invested_capital_ttm: builtins.str, - return_on_assets_ttm: builtins.str, - operating_return_on_assets_ttm: builtins.str, - return_on_tangible_assets_ttm: builtins.str, - return_on_equity_ttm: builtins.str, - return_on_invested_capital_ttm: builtins.str, - return_on_capital_employed_ttm: builtins.str, - earnings_yield_ttm: builtins.str, - free_cash_flow_yield_ttm: builtins.str, - capex_to_operating_cash_flow_ttm: builtins.str, - capex_to_depreciation_ttm: builtins.str, - capex_to_revenue_ttm: builtins.str, - sales_general_and_administrative_to_revenue_ttm: builtins.str, - research_and_development_to_revenue_ttm: builtins.str, - stock_based_compensation_to_revenue_ttm: builtins.str, - intangibles_to_total_assets_ttm: builtins.str, - average_receivables_ttm: builtins.str, - average_payables_ttm: builtins.str, - average_inventory_ttm: builtins.str, - days_of_sales_outstanding_ttm: builtins.str, - days_of_payables_outstanding_ttm: builtins.str, - days_of_inventory_outstanding_ttm: builtins.str, - operating_cycle_ttm: builtins.str, - cash_conversion_cycle_ttm: builtins.str, - free_cash_flow_to_equity_ttm: builtins.str, - free_cash_flow_to_firm_ttm: builtins.str, - tangible_asset_value_ttm: builtins.str, - net_current_asset_value_ttm: builtins.str, + market_cap: typing.Optional[builtins.str], + enterprise_value_ttm: typing.Optional[builtins.str], + ev_to_sales_ttm: typing.Optional[builtins.str], + ev_to_operating_cash_flow_ttm: typing.Optional[builtins.str], + ev_to_free_cash_flow_ttm: typing.Optional[builtins.str], + ev_to_ebitda_ttm: typing.Optional[builtins.str], + net_debt_to_ebitda_ttm: typing.Optional[builtins.str], + current_ratio_ttm: typing.Optional[builtins.str], + income_quality_ttm: typing.Optional[builtins.str], + graham_number_ttm: typing.Optional[builtins.str], + graham_net_net_ttm: typing.Optional[builtins.str], + tax_burden_ttm: typing.Optional[builtins.str], + interest_burden_ttm: typing.Optional[builtins.str], + working_capital_ttm: typing.Optional[builtins.str], + invested_capital_ttm: typing.Optional[builtins.str], + return_on_assets_ttm: typing.Optional[builtins.str], + operating_return_on_assets_ttm: typing.Optional[builtins.str], + return_on_tangible_assets_ttm: typing.Optional[builtins.str], + return_on_equity_ttm: typing.Optional[builtins.str], + return_on_invested_capital_ttm: typing.Optional[builtins.str], + return_on_capital_employed_ttm: typing.Optional[builtins.str], + earnings_yield_ttm: typing.Optional[builtins.str], + free_cash_flow_yield_ttm: typing.Optional[builtins.str], + capex_to_operating_cash_flow_ttm: typing.Optional[builtins.str], + capex_to_depreciation_ttm: typing.Optional[builtins.str], + capex_to_revenue_ttm: typing.Optional[builtins.str], + sales_general_and_administrative_to_revenue_ttm: typing.Optional[builtins.str], + research_and_development_to_revenue_ttm: typing.Optional[builtins.str], + stock_based_compensation_to_revenue_ttm: typing.Optional[builtins.str], + intangibles_to_total_assets_ttm: typing.Optional[builtins.str], + average_receivables_ttm: typing.Optional[builtins.str], + average_payables_ttm: typing.Optional[builtins.str], + average_inventory_ttm: typing.Optional[builtins.str], + days_of_sales_outstanding_ttm: typing.Optional[builtins.str], + days_of_payables_outstanding_ttm: typing.Optional[builtins.str], + days_of_inventory_outstanding_ttm: typing.Optional[builtins.str], + operating_cycle_ttm: typing.Optional[builtins.str], + cash_conversion_cycle_ttm: typing.Optional[builtins.str], + free_cash_flow_to_equity_ttm: typing.Optional[builtins.str], + free_cash_flow_to_firm_ttm: typing.Optional[builtins.str], + tangible_asset_value_ttm: typing.Optional[builtins.str], + net_current_asset_value_ttm: typing.Optional[builtins.str], ) -> BulkKeyMetricsTtm: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... diff --git a/crates/fmp-py/src/models/bulk/metrics.rs b/crates/fmp-py/src/models/bulk/metrics.rs index 784a2882..66396529 100644 --- a/crates/fmp-py/src/models/bulk/metrics.rs +++ b/crates/fmp-py/src/models/bulk/metrics.rs @@ -12,89 +12,89 @@ pub(crate) struct BulkKeyMetricsTtm { #[pyo3(get)] pub symbol: String, #[pyo3(get)] - pub market_cap: String, + pub market_cap: Option, #[pyo3(get)] - pub enterprise_value_ttm: String, + pub enterprise_value_ttm: Option, #[pyo3(get)] - pub ev_to_sales_ttm: String, + pub ev_to_sales_ttm: Option, #[pyo3(get)] - pub ev_to_operating_cash_flow_ttm: String, + pub ev_to_operating_cash_flow_ttm: Option, #[pyo3(get)] - pub ev_to_free_cash_flow_ttm: String, + pub ev_to_free_cash_flow_ttm: Option, #[pyo3(get)] - pub ev_to_ebitda_ttm: String, + pub ev_to_ebitda_ttm: Option, #[pyo3(get)] - pub net_debt_to_ebitda_ttm: String, + pub net_debt_to_ebitda_ttm: Option, #[pyo3(get)] - pub current_ratio_ttm: String, + pub current_ratio_ttm: Option, #[pyo3(get)] - pub income_quality_ttm: String, + pub income_quality_ttm: Option, #[pyo3(get)] - pub graham_number_ttm: String, + pub graham_number_ttm: Option, #[pyo3(get)] - pub graham_net_net_ttm: String, + pub graham_net_net_ttm: Option, #[pyo3(get)] - pub tax_burden_ttm: String, + pub tax_burden_ttm: Option, #[pyo3(get)] - pub interest_burden_ttm: String, + pub interest_burden_ttm: Option, #[pyo3(get)] - pub working_capital_ttm: String, + pub working_capital_ttm: Option, #[pyo3(get)] - pub invested_capital_ttm: String, + pub invested_capital_ttm: Option, #[pyo3(get)] - pub return_on_assets_ttm: String, + pub return_on_assets_ttm: Option, #[pyo3(get)] - pub operating_return_on_assets_ttm: String, + pub operating_return_on_assets_ttm: Option, #[pyo3(get)] - pub return_on_tangible_assets_ttm: String, + pub return_on_tangible_assets_ttm: Option, #[pyo3(get)] - pub return_on_equity_ttm: String, + pub return_on_equity_ttm: Option, #[pyo3(get)] - pub return_on_invested_capital_ttm: String, + pub return_on_invested_capital_ttm: Option, #[pyo3(get)] - pub return_on_capital_employed_ttm: String, + pub return_on_capital_employed_ttm: Option, #[pyo3(get)] - pub earnings_yield_ttm: String, + pub earnings_yield_ttm: Option, #[pyo3(get)] - pub free_cash_flow_yield_ttm: String, + pub free_cash_flow_yield_ttm: Option, #[pyo3(get)] - pub capex_to_operating_cash_flow_ttm: String, + pub capex_to_operating_cash_flow_ttm: Option, #[pyo3(get)] - pub capex_to_depreciation_ttm: String, + pub capex_to_depreciation_ttm: Option, #[pyo3(get)] - pub capex_to_revenue_ttm: String, + pub capex_to_revenue_ttm: Option, #[pyo3(get)] - pub sales_general_and_administrative_to_revenue_ttm: String, + pub sales_general_and_administrative_to_revenue_ttm: Option, #[pyo3(get)] - pub research_and_development_to_revenue_ttm: String, + pub research_and_development_to_revenue_ttm: Option, #[pyo3(get)] - pub stock_based_compensation_to_revenue_ttm: String, + pub stock_based_compensation_to_revenue_ttm: Option, #[pyo3(get)] - pub intangibles_to_total_assets_ttm: String, + pub intangibles_to_total_assets_ttm: Option, #[pyo3(get)] - pub average_receivables_ttm: String, + pub average_receivables_ttm: Option, #[pyo3(get)] - pub average_payables_ttm: String, + pub average_payables_ttm: Option, #[pyo3(get)] - pub average_inventory_ttm: String, + pub average_inventory_ttm: Option, #[pyo3(get)] - pub days_of_sales_outstanding_ttm: String, + pub days_of_sales_outstanding_ttm: Option, #[pyo3(get)] - pub days_of_payables_outstanding_ttm: String, + pub days_of_payables_outstanding_ttm: Option, #[pyo3(get)] - pub days_of_inventory_outstanding_ttm: String, + pub days_of_inventory_outstanding_ttm: Option, #[pyo3(get)] - pub operating_cycle_ttm: String, + pub operating_cycle_ttm: Option, #[pyo3(get)] - pub cash_conversion_cycle_ttm: String, + pub cash_conversion_cycle_ttm: Option, #[pyo3(get)] - pub free_cash_flow_to_equity_ttm: String, + pub free_cash_flow_to_equity_ttm: Option, #[pyo3(get)] - pub free_cash_flow_to_firm_ttm: String, + pub free_cash_flow_to_firm_ttm: Option, #[pyo3(get)] - pub tangible_asset_value_ttm: String, + pub tangible_asset_value_ttm: Option, #[pyo3(get)] - pub net_current_asset_value_ttm: String, + pub net_current_asset_value_ttm: Option, } #[gen_stub_pymethods] @@ -106,48 +106,48 @@ impl BulkKeyMetricsTtm { #[pyo3(signature = (*, symbol, market_cap, enterprise_value_ttm, ev_to_sales_ttm, ev_to_operating_cash_flow_ttm, ev_to_free_cash_flow_ttm, ev_to_ebitda_ttm, net_debt_to_ebitda_ttm, current_ratio_ttm, income_quality_ttm, graham_number_ttm, graham_net_net_ttm, tax_burden_ttm, interest_burden_ttm, working_capital_ttm, invested_capital_ttm, return_on_assets_ttm, operating_return_on_assets_ttm, return_on_tangible_assets_ttm, return_on_equity_ttm, return_on_invested_capital_ttm, return_on_capital_employed_ttm, earnings_yield_ttm, free_cash_flow_yield_ttm, capex_to_operating_cash_flow_ttm, capex_to_depreciation_ttm, capex_to_revenue_ttm, sales_general_and_administrative_to_revenue_ttm, research_and_development_to_revenue_ttm, stock_based_compensation_to_revenue_ttm, intangibles_to_total_assets_ttm, average_receivables_ttm, average_payables_ttm, average_inventory_ttm, days_of_sales_outstanding_ttm, days_of_payables_outstanding_ttm, days_of_inventory_outstanding_ttm, operating_cycle_ttm, cash_conversion_cycle_ttm, free_cash_flow_to_equity_ttm, free_cash_flow_to_firm_ttm, tangible_asset_value_ttm, net_current_asset_value_ttm))] fn new( symbol: String, - market_cap: String, - enterprise_value_ttm: String, - ev_to_sales_ttm: String, - ev_to_operating_cash_flow_ttm: String, - ev_to_free_cash_flow_ttm: String, - ev_to_ebitda_ttm: String, - net_debt_to_ebitda_ttm: String, - current_ratio_ttm: String, - income_quality_ttm: String, - graham_number_ttm: String, - graham_net_net_ttm: String, - tax_burden_ttm: String, - interest_burden_ttm: String, - working_capital_ttm: String, - invested_capital_ttm: String, - return_on_assets_ttm: String, - operating_return_on_assets_ttm: String, - return_on_tangible_assets_ttm: String, - return_on_equity_ttm: String, - return_on_invested_capital_ttm: String, - return_on_capital_employed_ttm: String, - earnings_yield_ttm: String, - free_cash_flow_yield_ttm: String, - capex_to_operating_cash_flow_ttm: String, - capex_to_depreciation_ttm: String, - capex_to_revenue_ttm: String, - sales_general_and_administrative_to_revenue_ttm: String, - research_and_development_to_revenue_ttm: String, - stock_based_compensation_to_revenue_ttm: String, - intangibles_to_total_assets_ttm: String, - average_receivables_ttm: String, - average_payables_ttm: String, - average_inventory_ttm: String, - days_of_sales_outstanding_ttm: String, - days_of_payables_outstanding_ttm: String, - days_of_inventory_outstanding_ttm: String, - operating_cycle_ttm: String, - cash_conversion_cycle_ttm: String, - free_cash_flow_to_equity_ttm: String, - free_cash_flow_to_firm_ttm: String, - tangible_asset_value_ttm: String, - net_current_asset_value_ttm: String, + market_cap: Option, + enterprise_value_ttm: Option, + ev_to_sales_ttm: Option, + ev_to_operating_cash_flow_ttm: Option, + ev_to_free_cash_flow_ttm: Option, + ev_to_ebitda_ttm: Option, + net_debt_to_ebitda_ttm: Option, + current_ratio_ttm: Option, + income_quality_ttm: Option, + graham_number_ttm: Option, + graham_net_net_ttm: Option, + tax_burden_ttm: Option, + interest_burden_ttm: Option, + working_capital_ttm: Option, + invested_capital_ttm: Option, + return_on_assets_ttm: Option, + operating_return_on_assets_ttm: Option, + return_on_tangible_assets_ttm: Option, + return_on_equity_ttm: Option, + return_on_invested_capital_ttm: Option, + return_on_capital_employed_ttm: Option, + earnings_yield_ttm: Option, + free_cash_flow_yield_ttm: Option, + capex_to_operating_cash_flow_ttm: Option, + capex_to_depreciation_ttm: Option, + capex_to_revenue_ttm: Option, + sales_general_and_administrative_to_revenue_ttm: Option, + research_and_development_to_revenue_ttm: Option, + stock_based_compensation_to_revenue_ttm: Option, + intangibles_to_total_assets_ttm: Option, + average_receivables_ttm: Option, + average_payables_ttm: Option, + average_inventory_ttm: Option, + days_of_sales_outstanding_ttm: Option, + days_of_payables_outstanding_ttm: Option, + days_of_inventory_outstanding_ttm: Option, + operating_cycle_ttm: Option, + cash_conversion_cycle_ttm: Option, + free_cash_flow_to_equity_ttm: Option, + free_cash_flow_to_firm_ttm: Option, + tangible_asset_value_ttm: Option, + net_current_asset_value_ttm: Option, ) -> Self { Self { symbol, @@ -900,54 +900,92 @@ impl From for BulkKeyMetricsTtm { fn from(value: libfmp::responses::bulk::BulkKeyMetricsTtm) -> Self { Self { symbol: value.symbol.into_inner(), - market_cap: value.market_cap.into_inner(), - enterprise_value_ttm: value.enterprise_value_ttm.into_inner(), - ev_to_sales_ttm: value.ev_to_sales_ttm.into_inner(), - ev_to_operating_cash_flow_ttm: value.ev_to_operating_cash_flow_ttm.into_inner(), - ev_to_free_cash_flow_ttm: value.ev_to_free_cash_flow_ttm.into_inner(), - ev_to_ebitda_ttm: value.ev_to_ebitda_ttm.into_inner(), - net_debt_to_ebitda_ttm: value.net_debt_to_ebitda_ttm.into_inner(), - current_ratio_ttm: value.current_ratio_ttm.into_inner(), - income_quality_ttm: value.income_quality_ttm.into_inner(), - graham_number_ttm: value.graham_number_ttm.into_inner(), - graham_net_net_ttm: value.graham_net_net_ttm.into_inner(), - tax_burden_ttm: value.tax_burden_ttm.into_inner(), - interest_burden_ttm: value.interest_burden_ttm.into_inner(), - working_capital_ttm: value.working_capital_ttm.into_inner(), - invested_capital_ttm: value.invested_capital_ttm.into_inner(), - return_on_assets_ttm: value.return_on_assets_ttm.into_inner(), - operating_return_on_assets_ttm: value.operating_return_on_assets_ttm.into_inner(), - return_on_tangible_assets_ttm: value.return_on_tangible_assets_ttm.into_inner(), - return_on_equity_ttm: value.return_on_equity_ttm.into_inner(), - return_on_invested_capital_ttm: value.return_on_invested_capital_ttm.into_inner(), - return_on_capital_employed_ttm: value.return_on_capital_employed_ttm.into_inner(), - earnings_yield_ttm: value.earnings_yield_ttm.into_inner(), - free_cash_flow_yield_ttm: value.free_cash_flow_yield_ttm.into_inner(), - capex_to_operating_cash_flow_ttm: value.capex_to_operating_cash_flow_ttm.into_inner(), - capex_to_depreciation_ttm: value.capex_to_depreciation_ttm.into_inner(), - capex_to_revenue_ttm: value.capex_to_revenue_ttm.into_inner(), + market_cap: value.market_cap.map(|value| value.into_inner()), + enterprise_value_ttm: value.enterprise_value_ttm.map(|value| value.into_inner()), + ev_to_sales_ttm: value.ev_to_sales_ttm.map(|value| value.into_inner()), + ev_to_operating_cash_flow_ttm: value + .ev_to_operating_cash_flow_ttm + .map(|value| value.into_inner()), + ev_to_free_cash_flow_ttm: value + .ev_to_free_cash_flow_ttm + .map(|value| value.into_inner()), + ev_to_ebitda_ttm: value.ev_to_ebitda_ttm.map(|value| value.into_inner()), + net_debt_to_ebitda_ttm: value.net_debt_to_ebitda_ttm.map(|value| value.into_inner()), + current_ratio_ttm: value.current_ratio_ttm.map(|value| value.into_inner()), + income_quality_ttm: value.income_quality_ttm.map(|value| value.into_inner()), + graham_number_ttm: value.graham_number_ttm.map(|value| value.into_inner()), + graham_net_net_ttm: value.graham_net_net_ttm.map(|value| value.into_inner()), + tax_burden_ttm: value.tax_burden_ttm.map(|value| value.into_inner()), + interest_burden_ttm: value.interest_burden_ttm.map(|value| value.into_inner()), + working_capital_ttm: value.working_capital_ttm.map(|value| value.into_inner()), + invested_capital_ttm: value.invested_capital_ttm.map(|value| value.into_inner()), + return_on_assets_ttm: value.return_on_assets_ttm.map(|value| value.into_inner()), + operating_return_on_assets_ttm: value + .operating_return_on_assets_ttm + .map(|value| value.into_inner()), + return_on_tangible_assets_ttm: value + .return_on_tangible_assets_ttm + .map(|value| value.into_inner()), + return_on_equity_ttm: value.return_on_equity_ttm.map(|value| value.into_inner()), + return_on_invested_capital_ttm: value + .return_on_invested_capital_ttm + .map(|value| value.into_inner()), + return_on_capital_employed_ttm: value + .return_on_capital_employed_ttm + .map(|value| value.into_inner()), + earnings_yield_ttm: value.earnings_yield_ttm.map(|value| value.into_inner()), + free_cash_flow_yield_ttm: value + .free_cash_flow_yield_ttm + .map(|value| value.into_inner()), + capex_to_operating_cash_flow_ttm: value + .capex_to_operating_cash_flow_ttm + .map(|value| value.into_inner()), + capex_to_depreciation_ttm: value + .capex_to_depreciation_ttm + .map(|value| value.into_inner()), + capex_to_revenue_ttm: value.capex_to_revenue_ttm.map(|value| value.into_inner()), sales_general_and_administrative_to_revenue_ttm: value .sales_general_and_administrative_to_revenue_ttm - .into_inner(), + .map(|value| value.into_inner()), research_and_development_to_revenue_ttm: value .research_and_development_to_revenue_ttm - .into_inner(), + .map(|value| value.into_inner()), stock_based_compensation_to_revenue_ttm: value .stock_based_compensation_to_revenue_ttm - .into_inner(), - intangibles_to_total_assets_ttm: value.intangibles_to_total_assets_ttm.into_inner(), - average_receivables_ttm: value.average_receivables_ttm.into_inner(), - average_payables_ttm: value.average_payables_ttm.into_inner(), - average_inventory_ttm: value.average_inventory_ttm.into_inner(), - days_of_sales_outstanding_ttm: value.days_of_sales_outstanding_ttm.into_inner(), - days_of_payables_outstanding_ttm: value.days_of_payables_outstanding_ttm.into_inner(), - days_of_inventory_outstanding_ttm: value.days_of_inventory_outstanding_ttm.into_inner(), - operating_cycle_ttm: value.operating_cycle_ttm.into_inner(), - cash_conversion_cycle_ttm: value.cash_conversion_cycle_ttm.into_inner(), - free_cash_flow_to_equity_ttm: value.free_cash_flow_to_equity_ttm.into_inner(), - free_cash_flow_to_firm_ttm: value.free_cash_flow_to_firm_ttm.into_inner(), - tangible_asset_value_ttm: value.tangible_asset_value_ttm.into_inner(), - net_current_asset_value_ttm: value.net_current_asset_value_ttm.into_inner(), + .map(|value| value.into_inner()), + intangibles_to_total_assets_ttm: value + .intangibles_to_total_assets_ttm + .map(|value| value.into_inner()), + average_receivables_ttm: value + .average_receivables_ttm + .map(|value| value.into_inner()), + average_payables_ttm: value.average_payables_ttm.map(|value| value.into_inner()), + average_inventory_ttm: value.average_inventory_ttm.map(|value| value.into_inner()), + days_of_sales_outstanding_ttm: value + .days_of_sales_outstanding_ttm + .map(|value| value.into_inner()), + days_of_payables_outstanding_ttm: value + .days_of_payables_outstanding_ttm + .map(|value| value.into_inner()), + days_of_inventory_outstanding_ttm: value + .days_of_inventory_outstanding_ttm + .map(|value| value.into_inner()), + operating_cycle_ttm: value.operating_cycle_ttm.map(|value| value.into_inner()), + cash_conversion_cycle_ttm: value + .cash_conversion_cycle_ttm + .map(|value| value.into_inner()), + free_cash_flow_to_equity_ttm: value + .free_cash_flow_to_equity_ttm + .map(|value| value.into_inner()), + free_cash_flow_to_firm_ttm: value + .free_cash_flow_to_firm_ttm + .map(|value| value.into_inner()), + tangible_asset_value_ttm: value + .tangible_asset_value_ttm + .map(|value| value.into_inner()), + net_current_asset_value_ttm: value + .net_current_asset_value_ttm + .map(|value| value.into_inner()), } } } @@ -965,123 +1003,123 @@ pub(crate) struct BulkFinancialRatiosTtm { #[pyo3(get)] pub symbol: String, #[pyo3(get)] - pub gross_profit_margin_ttm: String, + pub gross_profit_margin_ttm: Option, #[pyo3(get)] - pub ebit_margin_ttm: String, + pub ebit_margin_ttm: Option, #[pyo3(get)] - pub ebitda_margin_ttm: String, + pub ebitda_margin_ttm: Option, #[pyo3(get)] - pub operating_profit_margin_ttm: String, + pub operating_profit_margin_ttm: Option, #[pyo3(get)] - pub pretax_profit_margin_ttm: String, + pub pretax_profit_margin_ttm: Option, #[pyo3(get)] - pub continuous_operations_profit_margin_ttm: String, + pub continuous_operations_profit_margin_ttm: Option, #[pyo3(get)] - pub net_profit_margin_ttm: String, + pub net_profit_margin_ttm: Option, #[pyo3(get)] - pub bottom_line_profit_margin_ttm: String, + pub bottom_line_profit_margin_ttm: Option, #[pyo3(get)] - pub receivables_turnover_ttm: String, + pub receivables_turnover_ttm: Option, #[pyo3(get)] - pub payables_turnover_ttm: String, + pub payables_turnover_ttm: Option, #[pyo3(get)] - pub inventory_turnover_ttm: String, + pub inventory_turnover_ttm: Option, #[pyo3(get)] - pub fixed_asset_turnover_ttm: String, + pub fixed_asset_turnover_ttm: Option, #[pyo3(get)] - pub asset_turnover_ttm: String, + pub asset_turnover_ttm: Option, #[pyo3(get)] - pub current_ratio_ttm: String, + pub current_ratio_ttm: Option, #[pyo3(get)] - pub quick_ratio_ttm: String, + pub quick_ratio_ttm: Option, #[pyo3(get)] - pub solvency_ratio_ttm: String, + pub solvency_ratio_ttm: Option, #[pyo3(get)] - pub cash_ratio_ttm: String, + pub cash_ratio_ttm: Option, #[pyo3(get)] - pub price_to_earnings_ratio_ttm: String, + pub price_to_earnings_ratio_ttm: Option, #[pyo3(get)] - pub price_to_earnings_growth_ratio_ttm: String, + pub price_to_earnings_growth_ratio_ttm: Option, #[pyo3(get)] - pub forward_price_to_earnings_growth_ratio_ttm: String, + pub forward_price_to_earnings_growth_ratio_ttm: Option, #[pyo3(get)] - pub price_to_book_ratio_ttm: String, + pub price_to_book_ratio_ttm: Option, #[pyo3(get)] - pub price_to_sales_ratio_ttm: String, + pub price_to_sales_ratio_ttm: Option, #[pyo3(get)] - pub price_to_free_cash_flow_ratio_ttm: String, + pub price_to_free_cash_flow_ratio_ttm: Option, #[pyo3(get)] - pub price_to_operating_cash_flow_ratio_ttm: String, + pub price_to_operating_cash_flow_ratio_ttm: Option, #[pyo3(get)] - pub debt_to_assets_ratio_ttm: String, + pub debt_to_assets_ratio_ttm: Option, #[pyo3(get)] - pub debt_to_equity_ratio_ttm: String, + pub debt_to_equity_ratio_ttm: Option, #[pyo3(get)] - pub debt_to_capital_ratio_ttm: String, + pub debt_to_capital_ratio_ttm: Option, #[pyo3(get)] - pub long_term_debt_to_capital_ratio_ttm: String, + pub long_term_debt_to_capital_ratio_ttm: Option, #[pyo3(get)] - pub financial_leverage_ratio_ttm: String, + pub financial_leverage_ratio_ttm: Option, #[pyo3(get)] - pub working_capital_turnover_ratio_ttm: String, + pub working_capital_turnover_ratio_ttm: Option, #[pyo3(get)] - pub operating_cash_flow_ratio_ttm: String, + pub operating_cash_flow_ratio_ttm: Option, #[pyo3(get)] - pub operating_cash_flow_sales_ratio_ttm: String, + pub operating_cash_flow_sales_ratio_ttm: Option, #[pyo3(get)] - pub free_cash_flow_operating_cash_flow_ratio_ttm: String, + pub free_cash_flow_operating_cash_flow_ratio_ttm: Option, #[pyo3(get)] - pub debt_service_coverage_ratio_ttm: String, + pub debt_service_coverage_ratio_ttm: Option, #[pyo3(get)] - pub interest_coverage_ratio_ttm: String, + pub interest_coverage_ratio_ttm: Option, #[pyo3(get)] - pub short_term_operating_cash_flow_coverage_ratio_ttm: String, + pub short_term_operating_cash_flow_coverage_ratio_ttm: Option, #[pyo3(get)] - pub operating_cash_flow_coverage_ratio_ttm: String, + pub operating_cash_flow_coverage_ratio_ttm: Option, #[pyo3(get)] - pub capital_expenditure_coverage_ratio_ttm: String, + pub capital_expenditure_coverage_ratio_ttm: Option, #[pyo3(get)] - pub dividend_paid_and_capex_coverage_ratio_ttm: String, + pub dividend_paid_and_capex_coverage_ratio_ttm: Option, #[pyo3(get)] - pub dividend_payout_ratio_ttm: String, + pub dividend_payout_ratio_ttm: Option, #[pyo3(get)] - pub dividend_yield_ttm: String, + pub dividend_yield_ttm: Option, #[pyo3(get)] - pub enterprise_value_ttm: String, + pub enterprise_value_ttm: Option, #[pyo3(get)] - pub revenue_per_share_ttm: String, + pub revenue_per_share_ttm: Option, #[pyo3(get)] - pub net_income_per_share_ttm: String, + pub net_income_per_share_ttm: Option, #[pyo3(get)] - pub interest_debt_per_share_ttm: String, + pub interest_debt_per_share_ttm: Option, #[pyo3(get)] - pub cash_per_share_ttm: String, + pub cash_per_share_ttm: Option, #[pyo3(get)] - pub book_value_per_share_ttm: String, + pub book_value_per_share_ttm: Option, #[pyo3(get)] - pub tangible_book_value_per_share_ttm: String, + pub tangible_book_value_per_share_ttm: Option, #[pyo3(get)] - pub shareholders_equity_per_share_ttm: String, + pub shareholders_equity_per_share_ttm: Option, #[pyo3(get)] - pub operating_cash_flow_per_share_ttm: String, + pub operating_cash_flow_per_share_ttm: Option, #[pyo3(get)] - pub capex_per_share_ttm: String, + pub capex_per_share_ttm: Option, #[pyo3(get)] - pub free_cash_flow_per_share_ttm: String, + pub free_cash_flow_per_share_ttm: Option, #[pyo3(get)] - pub net_income_per_ebt_ttm: String, + pub net_income_per_ebt_ttm: Option, #[pyo3(get)] - pub ebt_per_ebit_ttm: String, + pub ebt_per_ebit_ttm: Option, #[pyo3(get)] - pub price_to_fair_value_ttm: String, + pub price_to_fair_value_ttm: Option, #[pyo3(get)] - pub debt_to_market_cap_ttm: String, + pub debt_to_market_cap_ttm: Option, #[pyo3(get)] - pub effective_tax_rate_ttm: String, + pub effective_tax_rate_ttm: Option, #[pyo3(get)] - pub enterprise_value_multiple_ttm: String, + pub enterprise_value_multiple_ttm: Option, #[pyo3(get)] - pub dividend_per_share_ttm: String, + pub dividend_per_share_ttm: Option, } #[gen_stub_pymethods] @@ -1093,65 +1131,65 @@ impl BulkFinancialRatiosTtm { #[pyo3(signature = (*, symbol, gross_profit_margin_ttm, ebit_margin_ttm, ebitda_margin_ttm, operating_profit_margin_ttm, pretax_profit_margin_ttm, continuous_operations_profit_margin_ttm, net_profit_margin_ttm, bottom_line_profit_margin_ttm, receivables_turnover_ttm, payables_turnover_ttm, inventory_turnover_ttm, fixed_asset_turnover_ttm, asset_turnover_ttm, current_ratio_ttm, quick_ratio_ttm, solvency_ratio_ttm, cash_ratio_ttm, price_to_earnings_ratio_ttm, price_to_earnings_growth_ratio_ttm, forward_price_to_earnings_growth_ratio_ttm, price_to_book_ratio_ttm, price_to_sales_ratio_ttm, price_to_free_cash_flow_ratio_ttm, price_to_operating_cash_flow_ratio_ttm, debt_to_assets_ratio_ttm, debt_to_equity_ratio_ttm, debt_to_capital_ratio_ttm, long_term_debt_to_capital_ratio_ttm, financial_leverage_ratio_ttm, working_capital_turnover_ratio_ttm, operating_cash_flow_ratio_ttm, operating_cash_flow_sales_ratio_ttm, free_cash_flow_operating_cash_flow_ratio_ttm, debt_service_coverage_ratio_ttm, interest_coverage_ratio_ttm, short_term_operating_cash_flow_coverage_ratio_ttm, operating_cash_flow_coverage_ratio_ttm, capital_expenditure_coverage_ratio_ttm, dividend_paid_and_capex_coverage_ratio_ttm, dividend_payout_ratio_ttm, dividend_yield_ttm, enterprise_value_ttm, revenue_per_share_ttm, net_income_per_share_ttm, interest_debt_per_share_ttm, cash_per_share_ttm, book_value_per_share_ttm, tangible_book_value_per_share_ttm, shareholders_equity_per_share_ttm, operating_cash_flow_per_share_ttm, capex_per_share_ttm, free_cash_flow_per_share_ttm, net_income_per_ebt_ttm, ebt_per_ebit_ttm, price_to_fair_value_ttm, debt_to_market_cap_ttm, effective_tax_rate_ttm, enterprise_value_multiple_ttm, dividend_per_share_ttm))] fn new( symbol: String, - gross_profit_margin_ttm: String, - ebit_margin_ttm: String, - ebitda_margin_ttm: String, - operating_profit_margin_ttm: String, - pretax_profit_margin_ttm: String, - continuous_operations_profit_margin_ttm: String, - net_profit_margin_ttm: String, - bottom_line_profit_margin_ttm: String, - receivables_turnover_ttm: String, - payables_turnover_ttm: String, - inventory_turnover_ttm: String, - fixed_asset_turnover_ttm: String, - asset_turnover_ttm: String, - current_ratio_ttm: String, - quick_ratio_ttm: String, - solvency_ratio_ttm: String, - cash_ratio_ttm: String, - price_to_earnings_ratio_ttm: String, - price_to_earnings_growth_ratio_ttm: String, - forward_price_to_earnings_growth_ratio_ttm: String, - price_to_book_ratio_ttm: String, - price_to_sales_ratio_ttm: String, - price_to_free_cash_flow_ratio_ttm: String, - price_to_operating_cash_flow_ratio_ttm: String, - debt_to_assets_ratio_ttm: String, - debt_to_equity_ratio_ttm: String, - debt_to_capital_ratio_ttm: String, - long_term_debt_to_capital_ratio_ttm: String, - financial_leverage_ratio_ttm: String, - working_capital_turnover_ratio_ttm: String, - operating_cash_flow_ratio_ttm: String, - operating_cash_flow_sales_ratio_ttm: String, - free_cash_flow_operating_cash_flow_ratio_ttm: String, - debt_service_coverage_ratio_ttm: String, - interest_coverage_ratio_ttm: String, - short_term_operating_cash_flow_coverage_ratio_ttm: String, - operating_cash_flow_coverage_ratio_ttm: String, - capital_expenditure_coverage_ratio_ttm: String, - dividend_paid_and_capex_coverage_ratio_ttm: String, - dividend_payout_ratio_ttm: String, - dividend_yield_ttm: String, - enterprise_value_ttm: String, - revenue_per_share_ttm: String, - net_income_per_share_ttm: String, - interest_debt_per_share_ttm: String, - cash_per_share_ttm: String, - book_value_per_share_ttm: String, - tangible_book_value_per_share_ttm: String, - shareholders_equity_per_share_ttm: String, - operating_cash_flow_per_share_ttm: String, - capex_per_share_ttm: String, - free_cash_flow_per_share_ttm: String, - net_income_per_ebt_ttm: String, - ebt_per_ebit_ttm: String, - price_to_fair_value_ttm: String, - debt_to_market_cap_ttm: String, - effective_tax_rate_ttm: String, - enterprise_value_multiple_ttm: String, - dividend_per_share_ttm: String, + gross_profit_margin_ttm: Option, + ebit_margin_ttm: Option, + ebitda_margin_ttm: Option, + operating_profit_margin_ttm: Option, + pretax_profit_margin_ttm: Option, + continuous_operations_profit_margin_ttm: Option, + net_profit_margin_ttm: Option, + bottom_line_profit_margin_ttm: Option, + receivables_turnover_ttm: Option, + payables_turnover_ttm: Option, + inventory_turnover_ttm: Option, + fixed_asset_turnover_ttm: Option, + asset_turnover_ttm: Option, + current_ratio_ttm: Option, + quick_ratio_ttm: Option, + solvency_ratio_ttm: Option, + cash_ratio_ttm: Option, + price_to_earnings_ratio_ttm: Option, + price_to_earnings_growth_ratio_ttm: Option, + forward_price_to_earnings_growth_ratio_ttm: Option, + price_to_book_ratio_ttm: Option, + price_to_sales_ratio_ttm: Option, + price_to_free_cash_flow_ratio_ttm: Option, + price_to_operating_cash_flow_ratio_ttm: Option, + debt_to_assets_ratio_ttm: Option, + debt_to_equity_ratio_ttm: Option, + debt_to_capital_ratio_ttm: Option, + long_term_debt_to_capital_ratio_ttm: Option, + financial_leverage_ratio_ttm: Option, + working_capital_turnover_ratio_ttm: Option, + operating_cash_flow_ratio_ttm: Option, + operating_cash_flow_sales_ratio_ttm: Option, + free_cash_flow_operating_cash_flow_ratio_ttm: Option, + debt_service_coverage_ratio_ttm: Option, + interest_coverage_ratio_ttm: Option, + short_term_operating_cash_flow_coverage_ratio_ttm: Option, + operating_cash_flow_coverage_ratio_ttm: Option, + capital_expenditure_coverage_ratio_ttm: Option, + dividend_paid_and_capex_coverage_ratio_ttm: Option, + dividend_payout_ratio_ttm: Option, + dividend_yield_ttm: Option, + enterprise_value_ttm: Option, + revenue_per_share_ttm: Option, + net_income_per_share_ttm: Option, + interest_debt_per_share_ttm: Option, + cash_per_share_ttm: Option, + book_value_per_share_ttm: Option, + tangible_book_value_per_share_ttm: Option, + shareholders_equity_per_share_ttm: Option, + operating_cash_flow_per_share_ttm: Option, + capex_per_share_ttm: Option, + free_cash_flow_per_share_ttm: Option, + net_income_per_ebt_ttm: Option, + ebt_per_ebit_ttm: Option, + price_to_fair_value_ttm: Option, + debt_to_market_cap_ttm: Option, + effective_tax_rate_ttm: Option, + enterprise_value_multiple_ttm: Option, + dividend_per_share_ttm: Option, ) -> Self { Self { symbol, @@ -2229,89 +2267,143 @@ impl From for BulkFinancialRati fn from(value: libfmp::responses::bulk::BulkFinancialRatiosTtm) -> Self { Self { symbol: value.symbol.into_inner(), - gross_profit_margin_ttm: value.gross_profit_margin_ttm.into_inner(), - ebit_margin_ttm: value.ebit_margin_ttm.into_inner(), - ebitda_margin_ttm: value.ebitda_margin_ttm.into_inner(), - operating_profit_margin_ttm: value.operating_profit_margin_ttm.into_inner(), - pretax_profit_margin_ttm: value.pretax_profit_margin_ttm.into_inner(), + gross_profit_margin_ttm: value + .gross_profit_margin_ttm + .map(|value| value.into_inner()), + ebit_margin_ttm: value.ebit_margin_ttm.map(|value| value.into_inner()), + ebitda_margin_ttm: value.ebitda_margin_ttm.map(|value| value.into_inner()), + operating_profit_margin_ttm: value + .operating_profit_margin_ttm + .map(|value| value.into_inner()), + pretax_profit_margin_ttm: value + .pretax_profit_margin_ttm + .map(|value| value.into_inner()), continuous_operations_profit_margin_ttm: value .continuous_operations_profit_margin_ttm - .into_inner(), - net_profit_margin_ttm: value.net_profit_margin_ttm.into_inner(), - bottom_line_profit_margin_ttm: value.bottom_line_profit_margin_ttm.into_inner(), - receivables_turnover_ttm: value.receivables_turnover_ttm.into_inner(), - payables_turnover_ttm: value.payables_turnover_ttm.into_inner(), - inventory_turnover_ttm: value.inventory_turnover_ttm.into_inner(), - fixed_asset_turnover_ttm: value.fixed_asset_turnover_ttm.into_inner(), - asset_turnover_ttm: value.asset_turnover_ttm.into_inner(), - current_ratio_ttm: value.current_ratio_ttm.into_inner(), - quick_ratio_ttm: value.quick_ratio_ttm.into_inner(), - solvency_ratio_ttm: value.solvency_ratio_ttm.into_inner(), - cash_ratio_ttm: value.cash_ratio_ttm.into_inner(), - price_to_earnings_ratio_ttm: value.price_to_earnings_ratio_ttm.into_inner(), + .map(|value| value.into_inner()), + net_profit_margin_ttm: value.net_profit_margin_ttm.map(|value| value.into_inner()), + bottom_line_profit_margin_ttm: value + .bottom_line_profit_margin_ttm + .map(|value| value.into_inner()), + receivables_turnover_ttm: value + .receivables_turnover_ttm + .map(|value| value.into_inner()), + payables_turnover_ttm: value.payables_turnover_ttm.map(|value| value.into_inner()), + inventory_turnover_ttm: value.inventory_turnover_ttm.map(|value| value.into_inner()), + fixed_asset_turnover_ttm: value + .fixed_asset_turnover_ttm + .map(|value| value.into_inner()), + asset_turnover_ttm: value.asset_turnover_ttm.map(|value| value.into_inner()), + current_ratio_ttm: value.current_ratio_ttm.map(|value| value.into_inner()), + quick_ratio_ttm: value.quick_ratio_ttm.map(|value| value.into_inner()), + solvency_ratio_ttm: value.solvency_ratio_ttm.map(|value| value.into_inner()), + cash_ratio_ttm: value.cash_ratio_ttm.map(|value| value.into_inner()), + price_to_earnings_ratio_ttm: value + .price_to_earnings_ratio_ttm + .map(|value| value.into_inner()), price_to_earnings_growth_ratio_ttm: value .price_to_earnings_growth_ratio_ttm - .into_inner(), + .map(|value| value.into_inner()), forward_price_to_earnings_growth_ratio_ttm: value .forward_price_to_earnings_growth_ratio_ttm - .into_inner(), - price_to_book_ratio_ttm: value.price_to_book_ratio_ttm.into_inner(), - price_to_sales_ratio_ttm: value.price_to_sales_ratio_ttm.into_inner(), - price_to_free_cash_flow_ratio_ttm: value.price_to_free_cash_flow_ratio_ttm.into_inner(), + .map(|value| value.into_inner()), + price_to_book_ratio_ttm: value + .price_to_book_ratio_ttm + .map(|value| value.into_inner()), + price_to_sales_ratio_ttm: value + .price_to_sales_ratio_ttm + .map(|value| value.into_inner()), + price_to_free_cash_flow_ratio_ttm: value + .price_to_free_cash_flow_ratio_ttm + .map(|value| value.into_inner()), price_to_operating_cash_flow_ratio_ttm: value .price_to_operating_cash_flow_ratio_ttm - .into_inner(), - debt_to_assets_ratio_ttm: value.debt_to_assets_ratio_ttm.into_inner(), - debt_to_equity_ratio_ttm: value.debt_to_equity_ratio_ttm.into_inner(), - debt_to_capital_ratio_ttm: value.debt_to_capital_ratio_ttm.into_inner(), + .map(|value| value.into_inner()), + debt_to_assets_ratio_ttm: value + .debt_to_assets_ratio_ttm + .map(|value| value.into_inner()), + debt_to_equity_ratio_ttm: value + .debt_to_equity_ratio_ttm + .map(|value| value.into_inner()), + debt_to_capital_ratio_ttm: value + .debt_to_capital_ratio_ttm + .map(|value| value.into_inner()), long_term_debt_to_capital_ratio_ttm: value .long_term_debt_to_capital_ratio_ttm - .into_inner(), - financial_leverage_ratio_ttm: value.financial_leverage_ratio_ttm.into_inner(), + .map(|value| value.into_inner()), + financial_leverage_ratio_ttm: value + .financial_leverage_ratio_ttm + .map(|value| value.into_inner()), working_capital_turnover_ratio_ttm: value .working_capital_turnover_ratio_ttm - .into_inner(), - operating_cash_flow_ratio_ttm: value.operating_cash_flow_ratio_ttm.into_inner(), + .map(|value| value.into_inner()), + operating_cash_flow_ratio_ttm: value + .operating_cash_flow_ratio_ttm + .map(|value| value.into_inner()), operating_cash_flow_sales_ratio_ttm: value .operating_cash_flow_sales_ratio_ttm - .into_inner(), + .map(|value| value.into_inner()), free_cash_flow_operating_cash_flow_ratio_ttm: value .free_cash_flow_operating_cash_flow_ratio_ttm - .into_inner(), - debt_service_coverage_ratio_ttm: value.debt_service_coverage_ratio_ttm.into_inner(), - interest_coverage_ratio_ttm: value.interest_coverage_ratio_ttm.into_inner(), + .map(|value| value.into_inner()), + debt_service_coverage_ratio_ttm: value + .debt_service_coverage_ratio_ttm + .map(|value| value.into_inner()), + interest_coverage_ratio_ttm: value + .interest_coverage_ratio_ttm + .map(|value| value.into_inner()), short_term_operating_cash_flow_coverage_ratio_ttm: value .short_term_operating_cash_flow_coverage_ratio_ttm - .into_inner(), + .map(|value| value.into_inner()), operating_cash_flow_coverage_ratio_ttm: value .operating_cash_flow_coverage_ratio_ttm - .into_inner(), + .map(|value| value.into_inner()), capital_expenditure_coverage_ratio_ttm: value .capital_expenditure_coverage_ratio_ttm - .into_inner(), + .map(|value| value.into_inner()), dividend_paid_and_capex_coverage_ratio_ttm: value .dividend_paid_and_capex_coverage_ratio_ttm - .into_inner(), - dividend_payout_ratio_ttm: value.dividend_payout_ratio_ttm.into_inner(), - dividend_yield_ttm: value.dividend_yield_ttm.into_inner(), - enterprise_value_ttm: value.enterprise_value_ttm.into_inner(), - revenue_per_share_ttm: value.revenue_per_share_ttm.into_inner(), - net_income_per_share_ttm: value.net_income_per_share_ttm.into_inner(), - interest_debt_per_share_ttm: value.interest_debt_per_share_ttm.into_inner(), - cash_per_share_ttm: value.cash_per_share_ttm.into_inner(), - book_value_per_share_ttm: value.book_value_per_share_ttm.into_inner(), - tangible_book_value_per_share_ttm: value.tangible_book_value_per_share_ttm.into_inner(), - shareholders_equity_per_share_ttm: value.shareholders_equity_per_share_ttm.into_inner(), - operating_cash_flow_per_share_ttm: value.operating_cash_flow_per_share_ttm.into_inner(), - capex_per_share_ttm: value.capex_per_share_ttm.into_inner(), - free_cash_flow_per_share_ttm: value.free_cash_flow_per_share_ttm.into_inner(), - net_income_per_ebt_ttm: value.net_income_per_ebt_ttm.into_inner(), - ebt_per_ebit_ttm: value.ebt_per_ebit_ttm.into_inner(), - price_to_fair_value_ttm: value.price_to_fair_value_ttm.into_inner(), - debt_to_market_cap_ttm: value.debt_to_market_cap_ttm.into_inner(), - effective_tax_rate_ttm: value.effective_tax_rate_ttm.into_inner(), - enterprise_value_multiple_ttm: value.enterprise_value_multiple_ttm.into_inner(), - dividend_per_share_ttm: value.dividend_per_share_ttm.into_inner(), + .map(|value| value.into_inner()), + dividend_payout_ratio_ttm: value + .dividend_payout_ratio_ttm + .map(|value| value.into_inner()), + dividend_yield_ttm: value.dividend_yield_ttm.map(|value| value.into_inner()), + enterprise_value_ttm: value.enterprise_value_ttm.map(|value| value.into_inner()), + revenue_per_share_ttm: value.revenue_per_share_ttm.map(|value| value.into_inner()), + net_income_per_share_ttm: value + .net_income_per_share_ttm + .map(|value| value.into_inner()), + interest_debt_per_share_ttm: value + .interest_debt_per_share_ttm + .map(|value| value.into_inner()), + cash_per_share_ttm: value.cash_per_share_ttm.map(|value| value.into_inner()), + book_value_per_share_ttm: value + .book_value_per_share_ttm + .map(|value| value.into_inner()), + tangible_book_value_per_share_ttm: value + .tangible_book_value_per_share_ttm + .map(|value| value.into_inner()), + shareholders_equity_per_share_ttm: value + .shareholders_equity_per_share_ttm + .map(|value| value.into_inner()), + operating_cash_flow_per_share_ttm: value + .operating_cash_flow_per_share_ttm + .map(|value| value.into_inner()), + capex_per_share_ttm: value.capex_per_share_ttm.map(|value| value.into_inner()), + free_cash_flow_per_share_ttm: value + .free_cash_flow_per_share_ttm + .map(|value| value.into_inner()), + net_income_per_ebt_ttm: value.net_income_per_ebt_ttm.map(|value| value.into_inner()), + ebt_per_ebit_ttm: value.ebt_per_ebit_ttm.map(|value| value.into_inner()), + price_to_fair_value_ttm: value + .price_to_fair_value_ttm + .map(|value| value.into_inner()), + debt_to_market_cap_ttm: value.debt_to_market_cap_ttm.map(|value| value.into_inner()), + effective_tax_rate_ttm: value.effective_tax_rate_ttm.map(|value| value.into_inner()), + enterprise_value_multiple_ttm: value + .enterprise_value_multiple_ttm + .map(|value| value.into_inner()), + dividend_per_share_ttm: value.dividend_per_share_ttm.map(|value| value.into_inner()), } } } @@ -2414,9 +2506,9 @@ pub(crate) struct BulkEarningsSurprise { #[pyo3(get)] pub date: ::chrono::NaiveDate, #[pyo3(get)] - pub eps_actual: String, + pub eps_actual: Option, #[pyo3(get)] - pub eps_estimated: String, + pub eps_estimated: Option, #[pyo3(get)] pub last_updated: ::chrono::NaiveDate, } @@ -2431,8 +2523,8 @@ impl BulkEarningsSurprise { fn new( symbol: String, date: ::chrono::NaiveDate, - eps_actual: String, - eps_estimated: String, + eps_actual: Option, + eps_estimated: Option, last_updated: ::chrono::NaiveDate, ) -> Self { Self { @@ -2535,8 +2627,8 @@ impl From for BulkEarningsSurpris Self { symbol: value.symbol.into_inner(), date: value.date.into_inner(), - eps_actual: value.eps_actual.into_inner(), - eps_estimated: value.eps_estimated.into_inner(), + eps_actual: value.eps_actual.map(|value| value.into_inner()), + eps_estimated: value.eps_estimated.map(|value| value.into_inner()), last_updated: value.last_updated.into_inner(), } } diff --git a/crates/fmp-py/tests/test_bulk_a.py b/crates/fmp-py/tests/test_bulk_a.py index a6e81475..3ffbd52b 100644 --- a/crates/fmp-py/tests/test_bulk_a.py +++ b/crates/fmp-py/tests/test_bulk_a.py @@ -165,13 +165,14 @@ def test_key_metrics_ttm_takes_no_arguments(client: Any, fixture_server: Fixture rows = client.bulk.key_metrics_ttm() assert fixture_server.requests[0].target == "/key-metrics-ttm-bulk" - assert len(rows) == 2 + assert len(rows) == 3 row = rows[0] assert isinstance(row, BulkKeyMetricsTtm) assert row.symbol == "000001.SZ" assert row.market_cap == "224526473551" assert row.ev_to_ebitda_ttm == "29.23198788110799" assert row.earnings_yield_ttm == "0.19355846887948813" + assert rows[2].enterprise_value_ttm is None def test_financial_ratios_ttm_takes_no_arguments(client: Any, fixture_server: FixtureServer) -> None: @@ -180,7 +181,7 @@ def test_financial_ratios_ttm_takes_no_arguments(client: Any, fixture_server: Fi rows = client.bulk.financial_ratios_ttm() assert fixture_server.requests[0].target == "/ratios-ttm-bulk" - assert len(rows) == 2 + assert len(rows) == 3 row = rows[0] assert isinstance(row, BulkFinancialRatiosTtm) assert row.symbol == "000001.SZ" @@ -195,7 +196,7 @@ def test_stock_peers_takes_no_arguments(client: Any, fixture_server: FixtureServ rows = client.bulk.stock_peers() assert fixture_server.requests[0].target == "/peers-bulk" - assert len(rows) == 2 + assert len(rows) == 3 row = rows[0] assert isinstance(row, BulkStockPeer) assert row.symbol == "000001.SZ" @@ -208,7 +209,7 @@ def test_earnings_surprises_encodes_the_required_year(client: Any, fixture_serve rows = client.bulk.earnings_surprises(2026) assert fixture_server.requests[0].target == "/earnings-surprises-bulk?year=2026" - assert len(rows) == 2 + assert len(rows) == 3 row = rows[0] assert isinstance(row, BulkEarningsSurprise) assert row.symbol == "AUTO.OL" @@ -216,6 +217,7 @@ def test_earnings_surprises_encodes_the_required_year(client: Any, fixture_serve assert row.eps_actual == "0.1332" assert row.eps_estimated == "0.1581" assert row.last_updated == datetime.date(2025, 10, 7) + assert rows[2].eps_estimated is None def test_earnings_surprises_accepts_the_year_keyword(client: Any, fixture_server: FixtureServer) -> None: diff --git a/crates/libfmp/src/responses/bulk/metrics.rs b/crates/libfmp/src/responses/bulk/metrics.rs index d27f130d..4ed72ee1 100644 --- a/crates/libfmp/src/responses/bulk/metrics.rs +++ b/crates/libfmp/src/responses/bulk/metrics.rs @@ -13,89 +13,89 @@ use crate::{ pub struct BulkKeyMetricsTtm { pub symbol: Ticker, #[serde(rename = "marketCap")] - pub market_cap: NumericString, + pub market_cap: Option, #[serde(rename = "enterpriseValueTTM")] - pub enterprise_value_ttm: NumericString, + pub enterprise_value_ttm: Option, #[serde(rename = "evToSalesTTM")] - pub ev_to_sales_ttm: NumericString, + pub ev_to_sales_ttm: Option, #[serde(rename = "evToOperatingCashFlowTTM")] - pub ev_to_operating_cash_flow_ttm: NumericString, + pub ev_to_operating_cash_flow_ttm: Option, #[serde(rename = "evToFreeCashFlowTTM")] - pub ev_to_free_cash_flow_ttm: NumericString, + pub ev_to_free_cash_flow_ttm: Option, #[serde(rename = "evToEBITDATTM")] - pub ev_to_ebitda_ttm: NumericString, + pub ev_to_ebitda_ttm: Option, #[serde(rename = "netDebtToEBITDATTM")] - pub net_debt_to_ebitda_ttm: NumericString, + pub net_debt_to_ebitda_ttm: Option, #[serde(rename = "currentRatioTTM")] - pub current_ratio_ttm: NumericString, + pub current_ratio_ttm: Option, #[serde(rename = "incomeQualityTTM")] - pub income_quality_ttm: NumericString, + pub income_quality_ttm: Option, #[serde(rename = "grahamNumberTTM")] - pub graham_number_ttm: NumericString, + pub graham_number_ttm: Option, #[serde(rename = "grahamNetNetTTM")] - pub graham_net_net_ttm: NumericString, + pub graham_net_net_ttm: Option, #[serde(rename = "taxBurdenTTM")] - pub tax_burden_ttm: NumericString, + pub tax_burden_ttm: Option, #[serde(rename = "interestBurdenTTM")] - pub interest_burden_ttm: NumericString, + pub interest_burden_ttm: Option, #[serde(rename = "workingCapitalTTM")] - pub working_capital_ttm: NumericString, + pub working_capital_ttm: Option, #[serde(rename = "investedCapitalTTM")] - pub invested_capital_ttm: NumericString, + pub invested_capital_ttm: Option, #[serde(rename = "returnOnAssetsTTM")] - pub return_on_assets_ttm: NumericString, + pub return_on_assets_ttm: Option, #[serde(rename = "operatingReturnOnAssetsTTM")] - pub operating_return_on_assets_ttm: NumericString, + pub operating_return_on_assets_ttm: Option, #[serde(rename = "returnOnTangibleAssetsTTM")] - pub return_on_tangible_assets_ttm: NumericString, + pub return_on_tangible_assets_ttm: Option, #[serde(rename = "returnOnEquityTTM")] - pub return_on_equity_ttm: NumericString, + pub return_on_equity_ttm: Option, #[serde(rename = "returnOnInvestedCapitalTTM")] - pub return_on_invested_capital_ttm: NumericString, + pub return_on_invested_capital_ttm: Option, #[serde(rename = "returnOnCapitalEmployedTTM")] - pub return_on_capital_employed_ttm: NumericString, + pub return_on_capital_employed_ttm: Option, #[serde(rename = "earningsYieldTTM")] - pub earnings_yield_ttm: NumericString, + pub earnings_yield_ttm: Option, #[serde(rename = "freeCashFlowYieldTTM")] - pub free_cash_flow_yield_ttm: NumericString, + pub free_cash_flow_yield_ttm: Option, #[serde(rename = "capexToOperatingCashFlowTTM")] - pub capex_to_operating_cash_flow_ttm: NumericString, + pub capex_to_operating_cash_flow_ttm: Option, #[serde(rename = "capexToDepreciationTTM")] - pub capex_to_depreciation_ttm: NumericString, + pub capex_to_depreciation_ttm: Option, #[serde(rename = "capexToRevenueTTM")] - pub capex_to_revenue_ttm: NumericString, + pub capex_to_revenue_ttm: Option, #[serde(rename = "salesGeneralAndAdministrativeToRevenueTTM")] - pub sales_general_and_administrative_to_revenue_ttm: NumericString, + pub sales_general_and_administrative_to_revenue_ttm: Option, #[serde(rename = "researchAndDevelopementToRevenueTTM")] - pub research_and_development_to_revenue_ttm: NumericString, + pub research_and_development_to_revenue_ttm: Option, #[serde(rename = "stockBasedCompensationToRevenueTTM")] - pub stock_based_compensation_to_revenue_ttm: NumericString, + pub stock_based_compensation_to_revenue_ttm: Option, #[serde(rename = "intangiblesToTotalAssetsTTM")] - pub intangibles_to_total_assets_ttm: NumericString, + pub intangibles_to_total_assets_ttm: Option, #[serde(rename = "averageReceivablesTTM")] - pub average_receivables_ttm: NumericString, + pub average_receivables_ttm: Option, #[serde(rename = "averagePayablesTTM")] - pub average_payables_ttm: NumericString, + pub average_payables_ttm: Option, #[serde(rename = "averageInventoryTTM")] - pub average_inventory_ttm: NumericString, + pub average_inventory_ttm: Option, #[serde(rename = "daysOfSalesOutstandingTTM")] - pub days_of_sales_outstanding_ttm: NumericString, + pub days_of_sales_outstanding_ttm: Option, #[serde(rename = "daysOfPayablesOutstandingTTM")] - pub days_of_payables_outstanding_ttm: NumericString, + pub days_of_payables_outstanding_ttm: Option, #[serde(rename = "daysOfInventoryOutstandingTTM")] - pub days_of_inventory_outstanding_ttm: NumericString, + pub days_of_inventory_outstanding_ttm: Option, #[serde(rename = "operatingCycleTTM")] - pub operating_cycle_ttm: NumericString, + pub operating_cycle_ttm: Option, #[serde(rename = "cashConversionCycleTTM")] - pub cash_conversion_cycle_ttm: NumericString, + pub cash_conversion_cycle_ttm: Option, #[serde(rename = "freeCashFlowToEquityTTM")] - pub free_cash_flow_to_equity_ttm: NumericString, + pub free_cash_flow_to_equity_ttm: Option, #[serde(rename = "freeCashFlowToFirmTTM")] - pub free_cash_flow_to_firm_ttm: NumericString, + pub free_cash_flow_to_firm_ttm: Option, #[serde(rename = "tangibleAssetValueTTM")] - pub tangible_asset_value_ttm: NumericString, + pub tangible_asset_value_ttm: Option, #[serde(rename = "netCurrentAssetValueTTM")] - pub net_current_asset_value_ttm: NumericString, + pub net_current_asset_value_ttm: Option, } /// One worldwide financial-ratios trailing-twelve-month bulk row. @@ -104,123 +104,123 @@ pub struct BulkKeyMetricsTtm { pub struct BulkFinancialRatiosTtm { pub symbol: Ticker, #[serde(rename = "grossProfitMarginTTM")] - pub gross_profit_margin_ttm: NumericString, + pub gross_profit_margin_ttm: Option, #[serde(rename = "ebitMarginTTM")] - pub ebit_margin_ttm: NumericString, + pub ebit_margin_ttm: Option, #[serde(rename = "ebitdaMarginTTM")] - pub ebitda_margin_ttm: NumericString, + pub ebitda_margin_ttm: Option, #[serde(rename = "operatingProfitMarginTTM")] - pub operating_profit_margin_ttm: NumericString, + pub operating_profit_margin_ttm: Option, #[serde(rename = "pretaxProfitMarginTTM")] - pub pretax_profit_margin_ttm: NumericString, + pub pretax_profit_margin_ttm: Option, #[serde(rename = "continuousOperationsProfitMarginTTM")] - pub continuous_operations_profit_margin_ttm: NumericString, + pub continuous_operations_profit_margin_ttm: Option, #[serde(rename = "netProfitMarginTTM")] - pub net_profit_margin_ttm: NumericString, + pub net_profit_margin_ttm: Option, #[serde(rename = "bottomLineProfitMarginTTM")] - pub bottom_line_profit_margin_ttm: NumericString, + pub bottom_line_profit_margin_ttm: Option, #[serde(rename = "receivablesTurnoverTTM")] - pub receivables_turnover_ttm: NumericString, + pub receivables_turnover_ttm: Option, #[serde(rename = "payablesTurnoverTTM")] - pub payables_turnover_ttm: NumericString, + pub payables_turnover_ttm: Option, #[serde(rename = "inventoryTurnoverTTM")] - pub inventory_turnover_ttm: NumericString, + pub inventory_turnover_ttm: Option, #[serde(rename = "fixedAssetTurnoverTTM")] - pub fixed_asset_turnover_ttm: NumericString, + pub fixed_asset_turnover_ttm: Option, #[serde(rename = "assetTurnoverTTM")] - pub asset_turnover_ttm: NumericString, + pub asset_turnover_ttm: Option, #[serde(rename = "currentRatioTTM")] - pub current_ratio_ttm: NumericString, + pub current_ratio_ttm: Option, #[serde(rename = "quickRatioTTM")] - pub quick_ratio_ttm: NumericString, + pub quick_ratio_ttm: Option, #[serde(rename = "solvencyRatioTTM")] - pub solvency_ratio_ttm: NumericString, + pub solvency_ratio_ttm: Option, #[serde(rename = "cashRatioTTM")] - pub cash_ratio_ttm: NumericString, + pub cash_ratio_ttm: Option, #[serde(rename = "priceToEarningsRatioTTM")] - pub price_to_earnings_ratio_ttm: NumericString, + pub price_to_earnings_ratio_ttm: Option, #[serde(rename = "priceToEarningsGrowthRatioTTM")] - pub price_to_earnings_growth_ratio_ttm: NumericString, + pub price_to_earnings_growth_ratio_ttm: Option, #[serde(rename = "forwardPriceToEarningsGrowthRatioTTM")] - pub forward_price_to_earnings_growth_ratio_ttm: NumericString, + pub forward_price_to_earnings_growth_ratio_ttm: Option, #[serde(rename = "priceToBookRatioTTM")] - pub price_to_book_ratio_ttm: NumericString, + pub price_to_book_ratio_ttm: Option, #[serde(rename = "priceToSalesRatioTTM")] - pub price_to_sales_ratio_ttm: NumericString, + pub price_to_sales_ratio_ttm: Option, #[serde(rename = "priceToFreeCashFlowRatioTTM")] - pub price_to_free_cash_flow_ratio_ttm: NumericString, + pub price_to_free_cash_flow_ratio_ttm: Option, #[serde(rename = "priceToOperatingCashFlowRatioTTM")] - pub price_to_operating_cash_flow_ratio_ttm: NumericString, + pub price_to_operating_cash_flow_ratio_ttm: Option, #[serde(rename = "debtToAssetsRatioTTM")] - pub debt_to_assets_ratio_ttm: NumericString, + pub debt_to_assets_ratio_ttm: Option, #[serde(rename = "debtToEquityRatioTTM")] - pub debt_to_equity_ratio_ttm: NumericString, + pub debt_to_equity_ratio_ttm: Option, #[serde(rename = "debtToCapitalRatioTTM")] - pub debt_to_capital_ratio_ttm: NumericString, + pub debt_to_capital_ratio_ttm: Option, #[serde(rename = "longTermDebtToCapitalRatioTTM")] - pub long_term_debt_to_capital_ratio_ttm: NumericString, + pub long_term_debt_to_capital_ratio_ttm: Option, #[serde(rename = "financialLeverageRatioTTM")] - pub financial_leverage_ratio_ttm: NumericString, + pub financial_leverage_ratio_ttm: Option, #[serde(rename = "workingCapitalTurnoverRatioTTM")] - pub working_capital_turnover_ratio_ttm: NumericString, + pub working_capital_turnover_ratio_ttm: Option, #[serde(rename = "operatingCashFlowRatioTTM")] - pub operating_cash_flow_ratio_ttm: NumericString, + pub operating_cash_flow_ratio_ttm: Option, #[serde(rename = "operatingCashFlowSalesRatioTTM")] - pub operating_cash_flow_sales_ratio_ttm: NumericString, + pub operating_cash_flow_sales_ratio_ttm: Option, #[serde(rename = "freeCashFlowOperatingCashFlowRatioTTM")] - pub free_cash_flow_operating_cash_flow_ratio_ttm: NumericString, + pub free_cash_flow_operating_cash_flow_ratio_ttm: Option, #[serde(rename = "debtServiceCoverageRatioTTM")] - pub debt_service_coverage_ratio_ttm: NumericString, + pub debt_service_coverage_ratio_ttm: Option, #[serde(rename = "interestCoverageRatioTTM")] - pub interest_coverage_ratio_ttm: NumericString, + pub interest_coverage_ratio_ttm: Option, #[serde(rename = "shortTermOperatingCashFlowCoverageRatioTTM")] - pub short_term_operating_cash_flow_coverage_ratio_ttm: NumericString, + pub short_term_operating_cash_flow_coverage_ratio_ttm: Option, #[serde(rename = "operatingCashFlowCoverageRatioTTM")] - pub operating_cash_flow_coverage_ratio_ttm: NumericString, + pub operating_cash_flow_coverage_ratio_ttm: Option, #[serde(rename = "capitalExpenditureCoverageRatioTTM")] - pub capital_expenditure_coverage_ratio_ttm: NumericString, + pub capital_expenditure_coverage_ratio_ttm: Option, #[serde(rename = "dividendPaidAndCapexCoverageRatioTTM")] - pub dividend_paid_and_capex_coverage_ratio_ttm: NumericString, + pub dividend_paid_and_capex_coverage_ratio_ttm: Option, #[serde(rename = "dividendPayoutRatioTTM")] - pub dividend_payout_ratio_ttm: NumericString, + pub dividend_payout_ratio_ttm: Option, #[serde(rename = "dividendYieldTTM")] - pub dividend_yield_ttm: NumericString, + pub dividend_yield_ttm: Option, #[serde(rename = "enterpriseValueTTM")] - pub enterprise_value_ttm: NumericString, + pub enterprise_value_ttm: Option, #[serde(rename = "revenuePerShareTTM")] - pub revenue_per_share_ttm: NumericString, + pub revenue_per_share_ttm: Option, #[serde(rename = "netIncomePerShareTTM")] - pub net_income_per_share_ttm: NumericString, + pub net_income_per_share_ttm: Option, #[serde(rename = "interestDebtPerShareTTM")] - pub interest_debt_per_share_ttm: NumericString, + pub interest_debt_per_share_ttm: Option, #[serde(rename = "cashPerShareTTM")] - pub cash_per_share_ttm: NumericString, + pub cash_per_share_ttm: Option, #[serde(rename = "bookValuePerShareTTM")] - pub book_value_per_share_ttm: NumericString, + pub book_value_per_share_ttm: Option, #[serde(rename = "tangibleBookValuePerShareTTM")] - pub tangible_book_value_per_share_ttm: NumericString, + pub tangible_book_value_per_share_ttm: Option, #[serde(rename = "shareholdersEquityPerShareTTM")] - pub shareholders_equity_per_share_ttm: NumericString, + pub shareholders_equity_per_share_ttm: Option, #[serde(rename = "operatingCashFlowPerShareTTM")] - pub operating_cash_flow_per_share_ttm: NumericString, + pub operating_cash_flow_per_share_ttm: Option, #[serde(rename = "capexPerShareTTM")] - pub capex_per_share_ttm: NumericString, + pub capex_per_share_ttm: Option, #[serde(rename = "freeCashFlowPerShareTTM")] - pub free_cash_flow_per_share_ttm: NumericString, + pub free_cash_flow_per_share_ttm: Option, #[serde(rename = "netIncomePerEBTTTM")] - pub net_income_per_ebt_ttm: NumericString, + pub net_income_per_ebt_ttm: Option, #[serde(rename = "ebtPerEbitTTM")] - pub ebt_per_ebit_ttm: NumericString, + pub ebt_per_ebit_ttm: Option, #[serde(rename = "priceToFairValueTTM")] - pub price_to_fair_value_ttm: NumericString, + pub price_to_fair_value_ttm: Option, #[serde(rename = "debtToMarketCapTTM")] - pub debt_to_market_cap_ttm: NumericString, + pub debt_to_market_cap_ttm: Option, #[serde(rename = "effectiveTaxRateTTM")] - pub effective_tax_rate_ttm: NumericString, + pub effective_tax_rate_ttm: Option, #[serde(rename = "enterpriseValueMultipleTTM")] - pub enterprise_value_multiple_ttm: NumericString, + pub enterprise_value_multiple_ttm: Option, #[serde(rename = "dividendPerShareTTM")] - pub dividend_per_share_ttm: NumericString, + pub dividend_per_share_ttm: Option, } /// One worldwide stock-peer bulk row. @@ -238,7 +238,7 @@ pub struct BulkStockPeer { pub struct BulkEarningsSurprise { pub symbol: Ticker, pub date: Date, - pub eps_actual: NumericString, - pub eps_estimated: NumericString, + pub eps_actual: Option, + pub eps_estimated: Option, pub last_updated: Date, } diff --git a/crates/libfmp/tests/bulk_metrics_endpoints.rs b/crates/libfmp/tests/bulk_metrics_endpoints.rs index dfe75cfc..4a089206 100644 --- a/crates/libfmp/tests/bulk_metrics_endpoints.rs +++ b/crates/libfmp/tests/bulk_metrics_endpoints.rs @@ -110,13 +110,19 @@ async fn custom_proxy_sends_one_exact_request_per_method_with_auth_headers_and_d let peers = client.bulk_stock_peers().await.unwrap(); let surprises = client.bulk_earnings_surprises(Year(2026)).await.unwrap(); - assert_eq!(metrics[0].ev_to_ebitda_ttm.as_str(), "29.23198788110799"); assert_eq!( - ratios[0].net_income_per_ebt_ttm.as_str(), + metrics[0].ev_to_ebitda_ttm.as_ref().unwrap().as_str(), + "29.23198788110799" + ); + assert_eq!( + ratios[0].net_income_per_ebt_ttm.as_ref().unwrap().as_str(), "0.83539656299258" ); assert_eq!(peers[1].symbol.as_str(), "000002.SZ"); - assert_eq!(surprises[1].eps_actual.as_str(), "-0.11675"); + assert_eq!( + surprises[1].eps_actual.as_ref().unwrap().as_str(), + "-0.11675" + ); let requests = executor.requests(); assert_eq!(requests.len(), 4); diff --git a/crates/libfmp/tests/bulk_metrics_responses.rs b/crates/libfmp/tests/bulk_metrics_responses.rs index 2dfb5e2d..da38da0a 100644 --- a/crates/libfmp/tests/bulk_metrics_responses.rs +++ b/crates/libfmp/tests/bulk_metrics_responses.rs @@ -13,7 +13,9 @@ use libfmp::{ query::Year, }; -use bulk_csv::{assert_empty_bodies, assert_members, assert_round_trip, decode, with_cells}; +use bulk_csv::{ + assert_empty_bodies, assert_required_members, assert_round_trip, decode, with_cells, +}; const KEY_METRICS: &[u8] = include_bytes!("fixtures/bulk_key_metrics_ttm.csv"); const RATIOS: &[u8] = include_bytes!("fixtures/bulk_financial_ratios_ttm.csv"); @@ -29,11 +31,15 @@ fn live_csv_fixtures_round_trip_every_cell_with_exact_field_counts() { } #[test] -fn every_column_is_required_and_only_text_members_accept_an_empty_cell() { - assert_members(&bulk_key_metrics_ttm(), KEY_METRICS, &[]); - assert_members(&bulk_financial_ratios_ttm(), RATIOS, &[]); - assert_members(&bulk_stock_peers(), PEERS, &["peers"]); - assert_members(&bulk_earnings_surprises(Year(2024).into()), SURPRISES, &[]); +fn every_column_is_required_and_only_identity_members_reject_an_empty_cell() { + assert_required_members(&bulk_key_metrics_ttm(), KEY_METRICS, &["symbol"]); + assert_required_members(&bulk_financial_ratios_ttm(), RATIOS, &["symbol"]); + assert_required_members(&bulk_stock_peers(), PEERS, &["symbol"]); + assert_required_members( + &bulk_earnings_surprises(Year(2024).into()), + SURPRISES, + &["symbol", "date", "lastUpdated"], + ); } #[test] @@ -58,9 +64,12 @@ fn numeric_cells_keep_their_exact_text_beyond_u64_and_in_exponent_form() { ); let decoded = decode(&bulk_key_metrics_ttm(), body).unwrap().remove(0); - assert_eq!(decoded.market_cap.as_str(), beyond_u64); - assert_eq!(decoded.free_cash_flow_to_firm_ttm.as_str(), high_precision); - assert_eq!(decoded.ev_to_sales_ttm.as_str(), "6.9148336e-9"); + assert_eq!(decoded.market_cap.unwrap().as_str(), beyond_u64); + assert_eq!( + decoded.free_cash_flow_to_firm_ttm.unwrap().as_str(), + high_precision + ); + assert_eq!(decoded.ev_to_sales_ttm.unwrap().as_str(), "6.9148336e-9"); } #[test] @@ -113,3 +122,28 @@ fn first_row(rows: &[R]) -> Map { other => panic!("a row re-encodes as an object, not {other}"), } } + +#[test] +fn live_empty_cells_decode_as_absent_metrics_and_empty_text() { + let metrics = decode(&bulk_key_metrics_ttm(), KEY_METRICS) + .unwrap() + .remove(2); + assert_eq!(metrics.symbol.as_str(), "ADAMO"); + assert_eq!(metrics.enterprise_value_ttm, None); + assert_eq!(metrics.market_cap.unwrap().as_str(), "711942736"); + + let ratios = decode(&bulk_financial_ratios_ttm(), RATIOS) + .unwrap() + .remove(2); + assert_eq!(ratios.price_to_earnings_ratio_ttm, None); + assert_eq!(ratios.gross_profit_margin_ttm.unwrap().as_str(), "0"); + + let surprise = decode(&bulk_earnings_surprises(Year(2024).into()), SURPRISES) + .unwrap() + .remove(2); + assert_eq!(surprise.eps_estimated, None); + assert_eq!(surprise.eps_actual.unwrap().as_str(), "0.00084"); + + let peers = decode(&bulk_stock_peers(), PEERS).unwrap().remove(2); + assert_eq!(peers.peers, ""); +} diff --git a/crates/libfmp/tests/fixtures/bulk_earnings_surprises.csv b/crates/libfmp/tests/fixtures/bulk_earnings_surprises.csv index 7271e075..216a95d5 100644 --- a/crates/libfmp/tests/fixtures/bulk_earnings_surprises.csv +++ b/crates/libfmp/tests/fixtures/bulk_earnings_surprises.csv @@ -1,3 +1,4 @@ "symbol","date","epsActual","epsEstimated","lastUpdated" "AUTO.OL","2024-12-31",0.1332,0.1581,"2025-10-07" "601699.SS","2024-12-31",-0.11675,0.67,"2025-06-17" +"GUD.TO","2024-11-07",0.00084,,"2025-06-17" diff --git a/crates/libfmp/tests/fixtures/bulk_financial_ratios_ttm.csv b/crates/libfmp/tests/fixtures/bulk_financial_ratios_ttm.csv index 103a047c..7832f041 100644 --- a/crates/libfmp/tests/fixtures/bulk_financial_ratios_ttm.csv +++ b/crates/libfmp/tests/fixtures/bulk_financial_ratios_ttm.csv @@ -1,3 +1,4 @@ "symbol","grossProfitMarginTTM","ebitMarginTTM","ebitdaMarginTTM","operatingProfitMarginTTM","pretaxProfitMarginTTM","continuousOperationsProfitMarginTTM","netProfitMarginTTM","bottomLineProfitMarginTTM","receivablesTurnoverTTM","payablesTurnoverTTM","inventoryTurnoverTTM","fixedAssetTurnoverTTM","assetTurnoverTTM","currentRatioTTM","quickRatioTTM","solvencyRatioTTM","cashRatioTTM","priceToEarningsRatioTTM","priceToEarningsGrowthRatioTTM","forwardPriceToEarningsGrowthRatioTTM","priceToEarningsDilutedRatioTTM","priceToEarningsDilutedGrowthRatioTTM","priceToBookRatioTTM","priceToSalesRatioTTM","priceToFreeCashFlowRatioTTM","priceToOperatingCashFlowRatioTTM","debtToAssetsRatioTTM","debtToEquityRatioTTM","debtToCapitalRatioTTM","longTermDebtToCapitalRatioTTM","financialLeverageRatioTTM","workingCapitalTurnoverRatioTTM","operatingCashFlowRatioTTM","operatingCashFlowSalesRatioTTM","freeCashFlowOperatingCashFlowRatioTTM","debtServiceCoverageRatioTTM","interestCoverageRatioTTM","shortTermOperatingCashFlowCoverageRatioTTM","operatingCashFlowCoverageRatioTTM","capitalExpenditureCoverageRatioTTM","dividendPaidAndCapexCoverageRatioTTM","dividendPayoutRatioTTM","dividendYieldTTM","enterpriseValueTTM","revenuePerShareTTM","netIncomePerShareTTM","interestDebtPerShareTTM","cashPerShareTTM","bookValuePerShareTTM","tangibleBookValuePerShareTTM","shareholdersEquityPerShareTTM","operatingCashFlowPerShareTTM","capexPerShareTTM","freeCashFlowPerShareTTM","netIncomePerEBTTTM","ebtPerEbitTTM","priceToFairValueTTM","debtToMarketCapTTM","effectiveTaxRateTTM","enterpriseValueMultipleTTM","dividendPerShareTTM" "000001.SZ",0.5535250166330814,0.2490032117403229,0.2490032117403229,0.2490032117403229,0.2490032117403229,0.20801642726197941,0.20801642726197941,0.19822803834942396,11.876584617133762,0,0,14.59761039686976,0.03463701192046656,0.2105714236740011,0.2105714236740011,0.007811431374385213,0.19800778772590405,5.406542056074767,5.730934579439322,5.49720208631886,5.406542056074767,,0.4795381412345637,1.0746955717759343,16.049068874267334,13.435848917479506,0.26085746254762526,3.360474056734741,0.7706671368780413,0.26820180271423055,12.882414878666593,-0.4366174223979569,0.011935132757014952,0.07998717218470139,0.8371731195021244,0.02949246146924156,0.7086403944912888,0.011935132757014952,0.010620806510145728,6.141492098493201,1.0232059759980408,0.31319174394256655,0.04191876,1520706473551,10.765839575329545,2.239471484935678,84.86236002852326,14.286415102856768,24.12738217279904,23.458359455089937,24.12738217279904,0.861129063824757,0.14021495916864124,0.7209141046561157,0.83539656299258,1,0.4795381412345637,7.000292549657781,0.16460343700741994,29.23198788110799,0.485 "000002.SZ",0.06666835318272966,-0.35160447542859574,-0.3436062587377696,-0.37712505941262214,-0.401972317997405,-0.48904104484834554,-0.4617804342288643,-0.4617804342288643,1.0423766535264125,1.5911361263677661,0.5349196265632267,6.6192161687368785,0.20659150998665363,1.1799592688017517,0.5548431689425369,-0.12094180514091163,0.10669697244200031,-0.5539661898569571,0.009588860380900295,0.04232837042623997,-0.5539661898569571,,0.49738934892132797,0.25578299899413237,35.65731601593227,12.980122127426482,0.18749632297965432,1.7611042833711095,0.6378260661784668,0.5772176041642411,9.392740376899077,1.787639576217643,0.007074995034555664,0.019747535999527614,0.36325375372663976,-2.3997780236250783,-11.174002151242703,0.13552230486057032,0.021758684201507818,1.5704836982740558,0.5419358420167008,-0.051679771183778936,0,171618750294,16.619498177734524,-7.674559085180067,15.644275053834804,4.950954525799006,18.08864773996082,17.017218420148932,8.564718985717171,0.32819413855889606,0.20897646942758963,0.11921775278259993,1.148786654089562,1.065885991833804,0.49738934892132797,3.3203982125782336,-0.21660378825265927,-2.518998346875115,0 +"ARS.AX",0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,,,,,,,,,,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,,,0,0,0,0,0,0,0,0,0,0,0,0,,,0,,0 diff --git a/crates/libfmp/tests/fixtures/bulk_key_metrics_ttm.csv b/crates/libfmp/tests/fixtures/bulk_key_metrics_ttm.csv index c9cfe9de..0dcda6a9 100644 --- a/crates/libfmp/tests/fixtures/bulk_key_metrics_ttm.csv +++ b/crates/libfmp/tests/fixtures/bulk_key_metrics_ttm.csv @@ -1,3 +1,4 @@ "symbol","marketCap","enterpriseValueTTM","evToSalesTTM","evToOperatingCashFlowTTM","evToFreeCashFlowTTM","evToEBITDATTM","netDebtToEBITDATTM","currentRatioTTM","incomeQualityTTM","grahamNumberTTM","grahamNetNetTTM","taxBurdenTTM","interestBurdenTTM","workingCapitalTTM","investedCapitalTTM","returnOnAssetsTTM","operatingReturnOnAssetsTTM","returnOnTangibleAssetsTTM","returnOnEquityTTM","returnOnInvestedCapitalTTM","returnOnCapitalEmployedTTM","earningsYieldTTM","freeCashFlowYieldTTM","capexToOperatingCashFlowTTM","capexToDepreciationTTM","capexToRevenueTTM","salesGeneralAndAdministrativeToRevenueTTM","researchAndDevelopementToRevenueTTM","stockBasedCompensationToRevenueTTM","intangiblesToTotalAssetsTTM","averageReceivablesTTM","averagePayablesTTM","averageInventoryTTM","daysOfSalesOutstandingTTM","daysOfPayablesOutstandingTTM","daysOfInventoryOutstandingTTM","operatingCycleTTM","cashConversionCycleTTM","freeCashFlowToEquityTTM","freeCashFlowToFirmTTM","tangibleAssetValueTTM","netCurrentAssetValueTTM" "000001.SZ",224526473551,1520706473551,7.278858867950086,91.00032754179881,108.69953349185133,29.23198788110799,24.91599707815924,0.2105714236740011,0.39517120696178587,34.86736509399441,-271.7252927689378,0.83539656299258,1,-1105320000000,-1078025000000,0.007205067470726046,0.008623129603290786,0.00722060948264289,0.09289033782530935,0.007205067470726046,0.011232032300027528,0.19355846887948813,0.062308910743312615,0.16282688049787566,0,0.013024061726681377,0.02260663121466966,0,0,0.002152451528393112,8795500000,0,0,30.73274108395039,0,0,30.73274108395039,30.73274108395039,-106379000000,75317297085.8483,455231000000,-5268681000000 "000004.SZ",67513944,-23704249,-0.1354217284200267,-0.841897327678881,-0.8962470352112417,-0.44397584773534005,-1.7084985297809208,1.11454892714022,2.388499808322787,1.1473442645398348,-0.20518332970569364,0.3283065153844871,0.7281077817851126,25605421,62495969,0.03424160485786354,-0.04084017529926973,0.03659920094471008,0.196679553604802,-0.07989108744570404,-0.12787045866623842,0.17475741923272,0.39174637760756503,0.06064143634187946,0.10716974410018917,0.009754358225523015,-0.027096298936874624,0.3538632227145465,0,0.06441659998009591,161005774.5,131472875,5696738.5,281.4264857895316,1445.3568449233546,92.13469677474816,373.56118256427976,-1071.7956623590749,29748351,26643066.309467595,92915054,19968242 +"ADAMO",711942736,,,,,,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,,,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 diff --git a/crates/libfmp/tests/fixtures/bulk_stock_peers.csv b/crates/libfmp/tests/fixtures/bulk_stock_peers.csv index 52d15a19..868f1888 100644 --- a/crates/libfmp/tests/fixtures/bulk_stock_peers.csv +++ b/crates/libfmp/tests/fixtures/bulk_stock_peers.csv @@ -1,3 +1,4 @@ "symbol","peers" "000001.SZ","3698.HK,600000.SS,600015.SS,600016.SS,600036.SS,601166.SS,601658.SS" "000002.SZ","600895.SS,600048.SS,001979.SZ,600376.SS,601588.SS,002244.SZ,600383.SS,600663.SS,600606.SS,600340.SS" +"0126Z0.KS", diff --git a/sdk/go/bulk_models.go b/sdk/go/bulk_models.go index 1de9bd9f..ec59cc82 100644 --- a/sdk/go/bulk_models.go +++ b/sdk/go/bulk_models.go @@ -1624,49 +1624,49 @@ func (m *BulkIncomeStatementGrowth) UnmarshalJSONFrom(dec *jsontext.Decoder) err // BulkKeyMetricsTTM is one worldwide key-metrics trailing-twelve-month bulk // row. type BulkKeyMetricsTTM struct { - Symbol string `json:"symbol"` - MarketCap string `json:"marketCap"` - EnterpriseValueTTM string `json:"enterpriseValueTTM"` - EvToSalesTTM string `json:"evToSalesTTM"` - EvToOperatingCashFlowTTM string `json:"evToOperatingCashFlowTTM"` - EvToFreeCashFlowTTM string `json:"evToFreeCashFlowTTM"` - EvToEbitdaTTM string `json:"evToEBITDATTM"` - NetDebtToEbitdaTTM string `json:"netDebtToEBITDATTM"` - CurrentRatioTTM string `json:"currentRatioTTM"` - IncomeQualityTTM string `json:"incomeQualityTTM"` - GrahamNumberTTM string `json:"grahamNumberTTM"` - GrahamNetNetTTM string `json:"grahamNetNetTTM"` - TaxBurdenTTM string `json:"taxBurdenTTM"` - InterestBurdenTTM string `json:"interestBurdenTTM"` - WorkingCapitalTTM string `json:"workingCapitalTTM"` - InvestedCapitalTTM string `json:"investedCapitalTTM"` - ReturnOnAssetsTTM string `json:"returnOnAssetsTTM"` - OperatingReturnOnAssetsTTM string `json:"operatingReturnOnAssetsTTM"` - ReturnOnTangibleAssetsTTM string `json:"returnOnTangibleAssetsTTM"` - ReturnOnEquityTTM string `json:"returnOnEquityTTM"` - ReturnOnInvestedCapitalTTM string `json:"returnOnInvestedCapitalTTM"` - ReturnOnCapitalEmployedTTM string `json:"returnOnCapitalEmployedTTM"` - EarningsYieldTTM string `json:"earningsYieldTTM"` - FreeCashFlowYieldTTM string `json:"freeCashFlowYieldTTM"` - CapexToOperatingCashFlowTTM string `json:"capexToOperatingCashFlowTTM"` - CapexToDepreciationTTM string `json:"capexToDepreciationTTM"` - CapexToRevenueTTM string `json:"capexToRevenueTTM"` - SalesGeneralAndAdministrativeToRevenueTTM string `json:"salesGeneralAndAdministrativeToRevenueTTM"` - ResearchAndDevelopmentToRevenueTTM string `json:"researchAndDevelopementToRevenueTTM"` - StockBasedCompensationToRevenueTTM string `json:"stockBasedCompensationToRevenueTTM"` - IntangiblesToTotalAssetsTTM string `json:"intangiblesToTotalAssetsTTM"` - AverageReceivablesTTM string `json:"averageReceivablesTTM"` - AveragePayablesTTM string `json:"averagePayablesTTM"` - AverageInventoryTTM string `json:"averageInventoryTTM"` - DaysOfSalesOutstandingTTM string `json:"daysOfSalesOutstandingTTM"` - DaysOfPayablesOutstandingTTM string `json:"daysOfPayablesOutstandingTTM"` - DaysOfInventoryOutstandingTTM string `json:"daysOfInventoryOutstandingTTM"` - OperatingCycleTTM string `json:"operatingCycleTTM"` - CashConversionCycleTTM string `json:"cashConversionCycleTTM"` - FreeCashFlowToEquityTTM string `json:"freeCashFlowToEquityTTM"` - FreeCashFlowToFirmTTM string `json:"freeCashFlowToFirmTTM"` - TangibleAssetValueTTM string `json:"tangibleAssetValueTTM"` - NetCurrentAssetValueTTM string `json:"netCurrentAssetValueTTM"` + Symbol string `json:"symbol"` + MarketCap *string `json:"marketCap"` + EnterpriseValueTTM *string `json:"enterpriseValueTTM"` + EvToSalesTTM *string `json:"evToSalesTTM"` + EvToOperatingCashFlowTTM *string `json:"evToOperatingCashFlowTTM"` + EvToFreeCashFlowTTM *string `json:"evToFreeCashFlowTTM"` + EvToEbitdaTTM *string `json:"evToEBITDATTM"` + NetDebtToEbitdaTTM *string `json:"netDebtToEBITDATTM"` + CurrentRatioTTM *string `json:"currentRatioTTM"` + IncomeQualityTTM *string `json:"incomeQualityTTM"` + GrahamNumberTTM *string `json:"grahamNumberTTM"` + GrahamNetNetTTM *string `json:"grahamNetNetTTM"` + TaxBurdenTTM *string `json:"taxBurdenTTM"` + InterestBurdenTTM *string `json:"interestBurdenTTM"` + WorkingCapitalTTM *string `json:"workingCapitalTTM"` + InvestedCapitalTTM *string `json:"investedCapitalTTM"` + ReturnOnAssetsTTM *string `json:"returnOnAssetsTTM"` + OperatingReturnOnAssetsTTM *string `json:"operatingReturnOnAssetsTTM"` + ReturnOnTangibleAssetsTTM *string `json:"returnOnTangibleAssetsTTM"` + ReturnOnEquityTTM *string `json:"returnOnEquityTTM"` + ReturnOnInvestedCapitalTTM *string `json:"returnOnInvestedCapitalTTM"` + ReturnOnCapitalEmployedTTM *string `json:"returnOnCapitalEmployedTTM"` + EarningsYieldTTM *string `json:"earningsYieldTTM"` + FreeCashFlowYieldTTM *string `json:"freeCashFlowYieldTTM"` + CapexToOperatingCashFlowTTM *string `json:"capexToOperatingCashFlowTTM"` + CapexToDepreciationTTM *string `json:"capexToDepreciationTTM"` + CapexToRevenueTTM *string `json:"capexToRevenueTTM"` + SalesGeneralAndAdministrativeToRevenueTTM *string `json:"salesGeneralAndAdministrativeToRevenueTTM"` + ResearchAndDevelopmentToRevenueTTM *string `json:"researchAndDevelopementToRevenueTTM"` + StockBasedCompensationToRevenueTTM *string `json:"stockBasedCompensationToRevenueTTM"` + IntangiblesToTotalAssetsTTM *string `json:"intangiblesToTotalAssetsTTM"` + AverageReceivablesTTM *string `json:"averageReceivablesTTM"` + AveragePayablesTTM *string `json:"averagePayablesTTM"` + AverageInventoryTTM *string `json:"averageInventoryTTM"` + DaysOfSalesOutstandingTTM *string `json:"daysOfSalesOutstandingTTM"` + DaysOfPayablesOutstandingTTM *string `json:"daysOfPayablesOutstandingTTM"` + DaysOfInventoryOutstandingTTM *string `json:"daysOfInventoryOutstandingTTM"` + OperatingCycleTTM *string `json:"operatingCycleTTM"` + CashConversionCycleTTM *string `json:"cashConversionCycleTTM"` + FreeCashFlowToEquityTTM *string `json:"freeCashFlowToEquityTTM"` + FreeCashFlowToFirmTTM *string `json:"freeCashFlowToFirmTTM"` + TangibleAssetValueTTM *string `json:"tangibleAssetValueTTM"` + NetCurrentAssetValueTTM *string `json:"netCurrentAssetValueTTM"` } // bulkKeyMetricsTTMShadow mirrors BulkKeyMetricsTTM with a pointer or raw @@ -1729,135 +1729,51 @@ func (m *BulkKeyMetricsTTM) UnmarshalJSONFrom(dec *jsontext.Decoder) error { switch { case shadow.Symbol == nil: return missingMemberError("BulkKeyMetricsTTM", "symbol") - case shadow.MarketCap == nil: - return missingMemberError("BulkKeyMetricsTTM", "marketCap") - case shadow.EnterpriseValueTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "enterpriseValueTTM") - case shadow.EvToSalesTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "evToSalesTTM") - case shadow.EvToOperatingCashFlowTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "evToOperatingCashFlowTTM") - case shadow.EvToFreeCashFlowTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "evToFreeCashFlowTTM") - case shadow.EvToEbitdaTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "evToEBITDATTM") - case shadow.NetDebtToEbitdaTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "netDebtToEBITDATTM") - case shadow.CurrentRatioTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "currentRatioTTM") - case shadow.IncomeQualityTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "incomeQualityTTM") - case shadow.GrahamNumberTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "grahamNumberTTM") - case shadow.GrahamNetNetTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "grahamNetNetTTM") - case shadow.TaxBurdenTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "taxBurdenTTM") - case shadow.InterestBurdenTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "interestBurdenTTM") - case shadow.WorkingCapitalTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "workingCapitalTTM") - case shadow.InvestedCapitalTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "investedCapitalTTM") - case shadow.ReturnOnAssetsTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "returnOnAssetsTTM") - case shadow.OperatingReturnOnAssetsTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "operatingReturnOnAssetsTTM") - case shadow.ReturnOnTangibleAssetsTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "returnOnTangibleAssetsTTM") - case shadow.ReturnOnEquityTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "returnOnEquityTTM") - case shadow.ReturnOnInvestedCapitalTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "returnOnInvestedCapitalTTM") - case shadow.ReturnOnCapitalEmployedTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "returnOnCapitalEmployedTTM") - case shadow.EarningsYieldTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "earningsYieldTTM") - case shadow.FreeCashFlowYieldTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "freeCashFlowYieldTTM") - case shadow.CapexToOperatingCashFlowTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "capexToOperatingCashFlowTTM") - case shadow.CapexToDepreciationTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "capexToDepreciationTTM") - case shadow.CapexToRevenueTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "capexToRevenueTTM") - case shadow.SalesGeneralAndAdministrativeToRevenueTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "salesGeneralAndAdministrativeToRevenueTTM") - case shadow.ResearchAndDevelopmentToRevenueTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "researchAndDevelopementToRevenueTTM") - case shadow.StockBasedCompensationToRevenueTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "stockBasedCompensationToRevenueTTM") - case shadow.IntangiblesToTotalAssetsTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "intangiblesToTotalAssetsTTM") - case shadow.AverageReceivablesTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "averageReceivablesTTM") - case shadow.AveragePayablesTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "averagePayablesTTM") - case shadow.AverageInventoryTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "averageInventoryTTM") - case shadow.DaysOfSalesOutstandingTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "daysOfSalesOutstandingTTM") - case shadow.DaysOfPayablesOutstandingTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "daysOfPayablesOutstandingTTM") - case shadow.DaysOfInventoryOutstandingTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "daysOfInventoryOutstandingTTM") - case shadow.OperatingCycleTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "operatingCycleTTM") - case shadow.CashConversionCycleTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "cashConversionCycleTTM") - case shadow.FreeCashFlowToEquityTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "freeCashFlowToEquityTTM") - case shadow.FreeCashFlowToFirmTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "freeCashFlowToFirmTTM") - case shadow.TangibleAssetValueTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "tangibleAssetValueTTM") - case shadow.NetCurrentAssetValueTTM == nil: - return missingMemberError("BulkKeyMetricsTTM", "netCurrentAssetValueTTM") } *m = BulkKeyMetricsTTM{ Symbol: *shadow.Symbol, - MarketCap: *shadow.MarketCap, - EnterpriseValueTTM: *shadow.EnterpriseValueTTM, - EvToSalesTTM: *shadow.EvToSalesTTM, - EvToOperatingCashFlowTTM: *shadow.EvToOperatingCashFlowTTM, - EvToFreeCashFlowTTM: *shadow.EvToFreeCashFlowTTM, - EvToEbitdaTTM: *shadow.EvToEbitdaTTM, - NetDebtToEbitdaTTM: *shadow.NetDebtToEbitdaTTM, - CurrentRatioTTM: *shadow.CurrentRatioTTM, - IncomeQualityTTM: *shadow.IncomeQualityTTM, - GrahamNumberTTM: *shadow.GrahamNumberTTM, - GrahamNetNetTTM: *shadow.GrahamNetNetTTM, - TaxBurdenTTM: *shadow.TaxBurdenTTM, - InterestBurdenTTM: *shadow.InterestBurdenTTM, - WorkingCapitalTTM: *shadow.WorkingCapitalTTM, - InvestedCapitalTTM: *shadow.InvestedCapitalTTM, - ReturnOnAssetsTTM: *shadow.ReturnOnAssetsTTM, - OperatingReturnOnAssetsTTM: *shadow.OperatingReturnOnAssetsTTM, - ReturnOnTangibleAssetsTTM: *shadow.ReturnOnTangibleAssetsTTM, - ReturnOnEquityTTM: *shadow.ReturnOnEquityTTM, - ReturnOnInvestedCapitalTTM: *shadow.ReturnOnInvestedCapitalTTM, - ReturnOnCapitalEmployedTTM: *shadow.ReturnOnCapitalEmployedTTM, - EarningsYieldTTM: *shadow.EarningsYieldTTM, - FreeCashFlowYieldTTM: *shadow.FreeCashFlowYieldTTM, - CapexToOperatingCashFlowTTM: *shadow.CapexToOperatingCashFlowTTM, - CapexToDepreciationTTM: *shadow.CapexToDepreciationTTM, - CapexToRevenueTTM: *shadow.CapexToRevenueTTM, - SalesGeneralAndAdministrativeToRevenueTTM: *shadow.SalesGeneralAndAdministrativeToRevenueTTM, - ResearchAndDevelopmentToRevenueTTM: *shadow.ResearchAndDevelopmentToRevenueTTM, - StockBasedCompensationToRevenueTTM: *shadow.StockBasedCompensationToRevenueTTM, - IntangiblesToTotalAssetsTTM: *shadow.IntangiblesToTotalAssetsTTM, - AverageReceivablesTTM: *shadow.AverageReceivablesTTM, - AveragePayablesTTM: *shadow.AveragePayablesTTM, - AverageInventoryTTM: *shadow.AverageInventoryTTM, - DaysOfSalesOutstandingTTM: *shadow.DaysOfSalesOutstandingTTM, - DaysOfPayablesOutstandingTTM: *shadow.DaysOfPayablesOutstandingTTM, - DaysOfInventoryOutstandingTTM: *shadow.DaysOfInventoryOutstandingTTM, - OperatingCycleTTM: *shadow.OperatingCycleTTM, - CashConversionCycleTTM: *shadow.CashConversionCycleTTM, - FreeCashFlowToEquityTTM: *shadow.FreeCashFlowToEquityTTM, - FreeCashFlowToFirmTTM: *shadow.FreeCashFlowToFirmTTM, - TangibleAssetValueTTM: *shadow.TangibleAssetValueTTM, - NetCurrentAssetValueTTM: *shadow.NetCurrentAssetValueTTM, + MarketCap: shadow.MarketCap, + EnterpriseValueTTM: shadow.EnterpriseValueTTM, + EvToSalesTTM: shadow.EvToSalesTTM, + EvToOperatingCashFlowTTM: shadow.EvToOperatingCashFlowTTM, + EvToFreeCashFlowTTM: shadow.EvToFreeCashFlowTTM, + EvToEbitdaTTM: shadow.EvToEbitdaTTM, + NetDebtToEbitdaTTM: shadow.NetDebtToEbitdaTTM, + CurrentRatioTTM: shadow.CurrentRatioTTM, + IncomeQualityTTM: shadow.IncomeQualityTTM, + GrahamNumberTTM: shadow.GrahamNumberTTM, + GrahamNetNetTTM: shadow.GrahamNetNetTTM, + TaxBurdenTTM: shadow.TaxBurdenTTM, + InterestBurdenTTM: shadow.InterestBurdenTTM, + WorkingCapitalTTM: shadow.WorkingCapitalTTM, + InvestedCapitalTTM: shadow.InvestedCapitalTTM, + ReturnOnAssetsTTM: shadow.ReturnOnAssetsTTM, + OperatingReturnOnAssetsTTM: shadow.OperatingReturnOnAssetsTTM, + ReturnOnTangibleAssetsTTM: shadow.ReturnOnTangibleAssetsTTM, + ReturnOnEquityTTM: shadow.ReturnOnEquityTTM, + ReturnOnInvestedCapitalTTM: shadow.ReturnOnInvestedCapitalTTM, + ReturnOnCapitalEmployedTTM: shadow.ReturnOnCapitalEmployedTTM, + EarningsYieldTTM: shadow.EarningsYieldTTM, + FreeCashFlowYieldTTM: shadow.FreeCashFlowYieldTTM, + CapexToOperatingCashFlowTTM: shadow.CapexToOperatingCashFlowTTM, + CapexToDepreciationTTM: shadow.CapexToDepreciationTTM, + CapexToRevenueTTM: shadow.CapexToRevenueTTM, + SalesGeneralAndAdministrativeToRevenueTTM: shadow.SalesGeneralAndAdministrativeToRevenueTTM, + ResearchAndDevelopmentToRevenueTTM: shadow.ResearchAndDevelopmentToRevenueTTM, + StockBasedCompensationToRevenueTTM: shadow.StockBasedCompensationToRevenueTTM, + IntangiblesToTotalAssetsTTM: shadow.IntangiblesToTotalAssetsTTM, + AverageReceivablesTTM: shadow.AverageReceivablesTTM, + AveragePayablesTTM: shadow.AveragePayablesTTM, + AverageInventoryTTM: shadow.AverageInventoryTTM, + DaysOfSalesOutstandingTTM: shadow.DaysOfSalesOutstandingTTM, + DaysOfPayablesOutstandingTTM: shadow.DaysOfPayablesOutstandingTTM, + DaysOfInventoryOutstandingTTM: shadow.DaysOfInventoryOutstandingTTM, + OperatingCycleTTM: shadow.OperatingCycleTTM, + CashConversionCycleTTM: shadow.CashConversionCycleTTM, + FreeCashFlowToEquityTTM: shadow.FreeCashFlowToEquityTTM, + FreeCashFlowToFirmTTM: shadow.FreeCashFlowToFirmTTM, + TangibleAssetValueTTM: shadow.TangibleAssetValueTTM, + NetCurrentAssetValueTTM: shadow.NetCurrentAssetValueTTM, } return nil } @@ -1865,66 +1781,66 @@ func (m *BulkKeyMetricsTTM) UnmarshalJSONFrom(dec *jsontext.Decoder) error { // BulkFinancialRatiosTTM is one worldwide financial-ratios // trailing-twelve-month bulk row. type BulkFinancialRatiosTTM struct { - Symbol string `json:"symbol"` - GrossProfitMarginTTM string `json:"grossProfitMarginTTM"` - EbitMarginTTM string `json:"ebitMarginTTM"` - EbitdaMarginTTM string `json:"ebitdaMarginTTM"` - OperatingProfitMarginTTM string `json:"operatingProfitMarginTTM"` - PretaxProfitMarginTTM string `json:"pretaxProfitMarginTTM"` - ContinuousOperationsProfitMarginTTM string `json:"continuousOperationsProfitMarginTTM"` - NetProfitMarginTTM string `json:"netProfitMarginTTM"` - BottomLineProfitMarginTTM string `json:"bottomLineProfitMarginTTM"` - ReceivablesTurnoverTTM string `json:"receivablesTurnoverTTM"` - PayablesTurnoverTTM string `json:"payablesTurnoverTTM"` - InventoryTurnoverTTM string `json:"inventoryTurnoverTTM"` - FixedAssetTurnoverTTM string `json:"fixedAssetTurnoverTTM"` - AssetTurnoverTTM string `json:"assetTurnoverTTM"` - CurrentRatioTTM string `json:"currentRatioTTM"` - QuickRatioTTM string `json:"quickRatioTTM"` - SolvencyRatioTTM string `json:"solvencyRatioTTM"` - CashRatioTTM string `json:"cashRatioTTM"` - PriceToEarningsRatioTTM string `json:"priceToEarningsRatioTTM"` - PriceToEarningsGrowthRatioTTM string `json:"priceToEarningsGrowthRatioTTM"` - ForwardPriceToEarningsGrowthRatioTTM string `json:"forwardPriceToEarningsGrowthRatioTTM"` - PriceToBookRatioTTM string `json:"priceToBookRatioTTM"` - PriceToSalesRatioTTM string `json:"priceToSalesRatioTTM"` - PriceToFreeCashFlowRatioTTM string `json:"priceToFreeCashFlowRatioTTM"` - PriceToOperatingCashFlowRatioTTM string `json:"priceToOperatingCashFlowRatioTTM"` - DebtToAssetsRatioTTM string `json:"debtToAssetsRatioTTM"` - DebtToEquityRatioTTM string `json:"debtToEquityRatioTTM"` - DebtToCapitalRatioTTM string `json:"debtToCapitalRatioTTM"` - LongTermDebtToCapitalRatioTTM string `json:"longTermDebtToCapitalRatioTTM"` - FinancialLeverageRatioTTM string `json:"financialLeverageRatioTTM"` - WorkingCapitalTurnoverRatioTTM string `json:"workingCapitalTurnoverRatioTTM"` - OperatingCashFlowRatioTTM string `json:"operatingCashFlowRatioTTM"` - OperatingCashFlowSalesRatioTTM string `json:"operatingCashFlowSalesRatioTTM"` - FreeCashFlowOperatingCashFlowRatioTTM string `json:"freeCashFlowOperatingCashFlowRatioTTM"` - DebtServiceCoverageRatioTTM string `json:"debtServiceCoverageRatioTTM"` - InterestCoverageRatioTTM string `json:"interestCoverageRatioTTM"` - ShortTermOperatingCashFlowCoverageRatioTTM string `json:"shortTermOperatingCashFlowCoverageRatioTTM"` - OperatingCashFlowCoverageRatioTTM string `json:"operatingCashFlowCoverageRatioTTM"` - CapitalExpenditureCoverageRatioTTM string `json:"capitalExpenditureCoverageRatioTTM"` - DividendPaidAndCapexCoverageRatioTTM string `json:"dividendPaidAndCapexCoverageRatioTTM"` - DividendPayoutRatioTTM string `json:"dividendPayoutRatioTTM"` - DividendYieldTTM string `json:"dividendYieldTTM"` - EnterpriseValueTTM string `json:"enterpriseValueTTM"` - RevenuePerShareTTM string `json:"revenuePerShareTTM"` - NetIncomePerShareTTM string `json:"netIncomePerShareTTM"` - InterestDebtPerShareTTM string `json:"interestDebtPerShareTTM"` - CashPerShareTTM string `json:"cashPerShareTTM"` - BookValuePerShareTTM string `json:"bookValuePerShareTTM"` - TangibleBookValuePerShareTTM string `json:"tangibleBookValuePerShareTTM"` - ShareholdersEquityPerShareTTM string `json:"shareholdersEquityPerShareTTM"` - OperatingCashFlowPerShareTTM string `json:"operatingCashFlowPerShareTTM"` - CapexPerShareTTM string `json:"capexPerShareTTM"` - FreeCashFlowPerShareTTM string `json:"freeCashFlowPerShareTTM"` - NetIncomePerEbtTTM string `json:"netIncomePerEBTTTM"` - EbtPerEbitTTM string `json:"ebtPerEbitTTM"` - PriceToFairValueTTM string `json:"priceToFairValueTTM"` - DebtToMarketCapTTM string `json:"debtToMarketCapTTM"` - EffectiveTaxRateTTM string `json:"effectiveTaxRateTTM"` - EnterpriseValueMultipleTTM string `json:"enterpriseValueMultipleTTM"` - DividendPerShareTTM string `json:"dividendPerShareTTM"` + Symbol string `json:"symbol"` + GrossProfitMarginTTM *string `json:"grossProfitMarginTTM"` + EbitMarginTTM *string `json:"ebitMarginTTM"` + EbitdaMarginTTM *string `json:"ebitdaMarginTTM"` + OperatingProfitMarginTTM *string `json:"operatingProfitMarginTTM"` + PretaxProfitMarginTTM *string `json:"pretaxProfitMarginTTM"` + ContinuousOperationsProfitMarginTTM *string `json:"continuousOperationsProfitMarginTTM"` + NetProfitMarginTTM *string `json:"netProfitMarginTTM"` + BottomLineProfitMarginTTM *string `json:"bottomLineProfitMarginTTM"` + ReceivablesTurnoverTTM *string `json:"receivablesTurnoverTTM"` + PayablesTurnoverTTM *string `json:"payablesTurnoverTTM"` + InventoryTurnoverTTM *string `json:"inventoryTurnoverTTM"` + FixedAssetTurnoverTTM *string `json:"fixedAssetTurnoverTTM"` + AssetTurnoverTTM *string `json:"assetTurnoverTTM"` + CurrentRatioTTM *string `json:"currentRatioTTM"` + QuickRatioTTM *string `json:"quickRatioTTM"` + SolvencyRatioTTM *string `json:"solvencyRatioTTM"` + CashRatioTTM *string `json:"cashRatioTTM"` + PriceToEarningsRatioTTM *string `json:"priceToEarningsRatioTTM"` + PriceToEarningsGrowthRatioTTM *string `json:"priceToEarningsGrowthRatioTTM"` + ForwardPriceToEarningsGrowthRatioTTM *string `json:"forwardPriceToEarningsGrowthRatioTTM"` + PriceToBookRatioTTM *string `json:"priceToBookRatioTTM"` + PriceToSalesRatioTTM *string `json:"priceToSalesRatioTTM"` + PriceToFreeCashFlowRatioTTM *string `json:"priceToFreeCashFlowRatioTTM"` + PriceToOperatingCashFlowRatioTTM *string `json:"priceToOperatingCashFlowRatioTTM"` + DebtToAssetsRatioTTM *string `json:"debtToAssetsRatioTTM"` + DebtToEquityRatioTTM *string `json:"debtToEquityRatioTTM"` + DebtToCapitalRatioTTM *string `json:"debtToCapitalRatioTTM"` + LongTermDebtToCapitalRatioTTM *string `json:"longTermDebtToCapitalRatioTTM"` + FinancialLeverageRatioTTM *string `json:"financialLeverageRatioTTM"` + WorkingCapitalTurnoverRatioTTM *string `json:"workingCapitalTurnoverRatioTTM"` + OperatingCashFlowRatioTTM *string `json:"operatingCashFlowRatioTTM"` + OperatingCashFlowSalesRatioTTM *string `json:"operatingCashFlowSalesRatioTTM"` + FreeCashFlowOperatingCashFlowRatioTTM *string `json:"freeCashFlowOperatingCashFlowRatioTTM"` + DebtServiceCoverageRatioTTM *string `json:"debtServiceCoverageRatioTTM"` + InterestCoverageRatioTTM *string `json:"interestCoverageRatioTTM"` + ShortTermOperatingCashFlowCoverageRatioTTM *string `json:"shortTermOperatingCashFlowCoverageRatioTTM"` + OperatingCashFlowCoverageRatioTTM *string `json:"operatingCashFlowCoverageRatioTTM"` + CapitalExpenditureCoverageRatioTTM *string `json:"capitalExpenditureCoverageRatioTTM"` + DividendPaidAndCapexCoverageRatioTTM *string `json:"dividendPaidAndCapexCoverageRatioTTM"` + DividendPayoutRatioTTM *string `json:"dividendPayoutRatioTTM"` + DividendYieldTTM *string `json:"dividendYieldTTM"` + EnterpriseValueTTM *string `json:"enterpriseValueTTM"` + RevenuePerShareTTM *string `json:"revenuePerShareTTM"` + NetIncomePerShareTTM *string `json:"netIncomePerShareTTM"` + InterestDebtPerShareTTM *string `json:"interestDebtPerShareTTM"` + CashPerShareTTM *string `json:"cashPerShareTTM"` + BookValuePerShareTTM *string `json:"bookValuePerShareTTM"` + TangibleBookValuePerShareTTM *string `json:"tangibleBookValuePerShareTTM"` + ShareholdersEquityPerShareTTM *string `json:"shareholdersEquityPerShareTTM"` + OperatingCashFlowPerShareTTM *string `json:"operatingCashFlowPerShareTTM"` + CapexPerShareTTM *string `json:"capexPerShareTTM"` + FreeCashFlowPerShareTTM *string `json:"freeCashFlowPerShareTTM"` + NetIncomePerEbtTTM *string `json:"netIncomePerEBTTTM"` + EbtPerEbitTTM *string `json:"ebtPerEbitTTM"` + PriceToFairValueTTM *string `json:"priceToFairValueTTM"` + DebtToMarketCapTTM *string `json:"debtToMarketCapTTM"` + EffectiveTaxRateTTM *string `json:"effectiveTaxRateTTM"` + EnterpriseValueMultipleTTM *string `json:"enterpriseValueMultipleTTM"` + DividendPerShareTTM *string `json:"dividendPerShareTTM"` } // bulkFinancialRatiosTTMShadow mirrors BulkFinancialRatiosTTM with a pointer @@ -2004,186 +1920,68 @@ func (m *BulkFinancialRatiosTTM) UnmarshalJSONFrom(dec *jsontext.Decoder) error switch { case shadow.Symbol == nil: return missingMemberError("BulkFinancialRatiosTTM", "symbol") - case shadow.GrossProfitMarginTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "grossProfitMarginTTM") - case shadow.EbitMarginTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "ebitMarginTTM") - case shadow.EbitdaMarginTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "ebitdaMarginTTM") - case shadow.OperatingProfitMarginTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "operatingProfitMarginTTM") - case shadow.PretaxProfitMarginTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "pretaxProfitMarginTTM") - case shadow.ContinuousOperationsProfitMarginTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "continuousOperationsProfitMarginTTM") - case shadow.NetProfitMarginTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "netProfitMarginTTM") - case shadow.BottomLineProfitMarginTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "bottomLineProfitMarginTTM") - case shadow.ReceivablesTurnoverTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "receivablesTurnoverTTM") - case shadow.PayablesTurnoverTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "payablesTurnoverTTM") - case shadow.InventoryTurnoverTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "inventoryTurnoverTTM") - case shadow.FixedAssetTurnoverTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "fixedAssetTurnoverTTM") - case shadow.AssetTurnoverTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "assetTurnoverTTM") - case shadow.CurrentRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "currentRatioTTM") - case shadow.QuickRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "quickRatioTTM") - case shadow.SolvencyRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "solvencyRatioTTM") - case shadow.CashRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "cashRatioTTM") - case shadow.PriceToEarningsRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "priceToEarningsRatioTTM") - case shadow.PriceToEarningsGrowthRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "priceToEarningsGrowthRatioTTM") - case shadow.ForwardPriceToEarningsGrowthRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "forwardPriceToEarningsGrowthRatioTTM") - case shadow.PriceToBookRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "priceToBookRatioTTM") - case shadow.PriceToSalesRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "priceToSalesRatioTTM") - case shadow.PriceToFreeCashFlowRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "priceToFreeCashFlowRatioTTM") - case shadow.PriceToOperatingCashFlowRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "priceToOperatingCashFlowRatioTTM") - case shadow.DebtToAssetsRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "debtToAssetsRatioTTM") - case shadow.DebtToEquityRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "debtToEquityRatioTTM") - case shadow.DebtToCapitalRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "debtToCapitalRatioTTM") - case shadow.LongTermDebtToCapitalRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "longTermDebtToCapitalRatioTTM") - case shadow.FinancialLeverageRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "financialLeverageRatioTTM") - case shadow.WorkingCapitalTurnoverRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "workingCapitalTurnoverRatioTTM") - case shadow.OperatingCashFlowRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "operatingCashFlowRatioTTM") - case shadow.OperatingCashFlowSalesRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "operatingCashFlowSalesRatioTTM") - case shadow.FreeCashFlowOperatingCashFlowRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "freeCashFlowOperatingCashFlowRatioTTM") - case shadow.DebtServiceCoverageRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "debtServiceCoverageRatioTTM") - case shadow.InterestCoverageRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "interestCoverageRatioTTM") - case shadow.ShortTermOperatingCashFlowCoverageRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "shortTermOperatingCashFlowCoverageRatioTTM") - case shadow.OperatingCashFlowCoverageRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "operatingCashFlowCoverageRatioTTM") - case shadow.CapitalExpenditureCoverageRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "capitalExpenditureCoverageRatioTTM") - case shadow.DividendPaidAndCapexCoverageRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "dividendPaidAndCapexCoverageRatioTTM") - case shadow.DividendPayoutRatioTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "dividendPayoutRatioTTM") - case shadow.DividendYieldTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "dividendYieldTTM") - case shadow.EnterpriseValueTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "enterpriseValueTTM") - case shadow.RevenuePerShareTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "revenuePerShareTTM") - case shadow.NetIncomePerShareTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "netIncomePerShareTTM") - case shadow.InterestDebtPerShareTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "interestDebtPerShareTTM") - case shadow.CashPerShareTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "cashPerShareTTM") - case shadow.BookValuePerShareTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "bookValuePerShareTTM") - case shadow.TangibleBookValuePerShareTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "tangibleBookValuePerShareTTM") - case shadow.ShareholdersEquityPerShareTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "shareholdersEquityPerShareTTM") - case shadow.OperatingCashFlowPerShareTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "operatingCashFlowPerShareTTM") - case shadow.CapexPerShareTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "capexPerShareTTM") - case shadow.FreeCashFlowPerShareTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "freeCashFlowPerShareTTM") - case shadow.NetIncomePerEbtTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "netIncomePerEBTTTM") - case shadow.EbtPerEbitTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "ebtPerEbitTTM") - case shadow.PriceToFairValueTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "priceToFairValueTTM") - case shadow.DebtToMarketCapTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "debtToMarketCapTTM") - case shadow.EffectiveTaxRateTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "effectiveTaxRateTTM") - case shadow.EnterpriseValueMultipleTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "enterpriseValueMultipleTTM") - case shadow.DividendPerShareTTM == nil: - return missingMemberError("BulkFinancialRatiosTTM", "dividendPerShareTTM") } *m = BulkFinancialRatiosTTM{ Symbol: *shadow.Symbol, - GrossProfitMarginTTM: *shadow.GrossProfitMarginTTM, - EbitMarginTTM: *shadow.EbitMarginTTM, - EbitdaMarginTTM: *shadow.EbitdaMarginTTM, - OperatingProfitMarginTTM: *shadow.OperatingProfitMarginTTM, - PretaxProfitMarginTTM: *shadow.PretaxProfitMarginTTM, - ContinuousOperationsProfitMarginTTM: *shadow.ContinuousOperationsProfitMarginTTM, - NetProfitMarginTTM: *shadow.NetProfitMarginTTM, - BottomLineProfitMarginTTM: *shadow.BottomLineProfitMarginTTM, - ReceivablesTurnoverTTM: *shadow.ReceivablesTurnoverTTM, - PayablesTurnoverTTM: *shadow.PayablesTurnoverTTM, - InventoryTurnoverTTM: *shadow.InventoryTurnoverTTM, - FixedAssetTurnoverTTM: *shadow.FixedAssetTurnoverTTM, - AssetTurnoverTTM: *shadow.AssetTurnoverTTM, - CurrentRatioTTM: *shadow.CurrentRatioTTM, - QuickRatioTTM: *shadow.QuickRatioTTM, - SolvencyRatioTTM: *shadow.SolvencyRatioTTM, - CashRatioTTM: *shadow.CashRatioTTM, - PriceToEarningsRatioTTM: *shadow.PriceToEarningsRatioTTM, - PriceToEarningsGrowthRatioTTM: *shadow.PriceToEarningsGrowthRatioTTM, - ForwardPriceToEarningsGrowthRatioTTM: *shadow.ForwardPriceToEarningsGrowthRatioTTM, - PriceToBookRatioTTM: *shadow.PriceToBookRatioTTM, - PriceToSalesRatioTTM: *shadow.PriceToSalesRatioTTM, - PriceToFreeCashFlowRatioTTM: *shadow.PriceToFreeCashFlowRatioTTM, - PriceToOperatingCashFlowRatioTTM: *shadow.PriceToOperatingCashFlowRatioTTM, - DebtToAssetsRatioTTM: *shadow.DebtToAssetsRatioTTM, - DebtToEquityRatioTTM: *shadow.DebtToEquityRatioTTM, - DebtToCapitalRatioTTM: *shadow.DebtToCapitalRatioTTM, - LongTermDebtToCapitalRatioTTM: *shadow.LongTermDebtToCapitalRatioTTM, - FinancialLeverageRatioTTM: *shadow.FinancialLeverageRatioTTM, - WorkingCapitalTurnoverRatioTTM: *shadow.WorkingCapitalTurnoverRatioTTM, - OperatingCashFlowRatioTTM: *shadow.OperatingCashFlowRatioTTM, - OperatingCashFlowSalesRatioTTM: *shadow.OperatingCashFlowSalesRatioTTM, - FreeCashFlowOperatingCashFlowRatioTTM: *shadow.FreeCashFlowOperatingCashFlowRatioTTM, - DebtServiceCoverageRatioTTM: *shadow.DebtServiceCoverageRatioTTM, - InterestCoverageRatioTTM: *shadow.InterestCoverageRatioTTM, - ShortTermOperatingCashFlowCoverageRatioTTM: *shadow.ShortTermOperatingCashFlowCoverageRatioTTM, - OperatingCashFlowCoverageRatioTTM: *shadow.OperatingCashFlowCoverageRatioTTM, - CapitalExpenditureCoverageRatioTTM: *shadow.CapitalExpenditureCoverageRatioTTM, - DividendPaidAndCapexCoverageRatioTTM: *shadow.DividendPaidAndCapexCoverageRatioTTM, - DividendPayoutRatioTTM: *shadow.DividendPayoutRatioTTM, - DividendYieldTTM: *shadow.DividendYieldTTM, - EnterpriseValueTTM: *shadow.EnterpriseValueTTM, - RevenuePerShareTTM: *shadow.RevenuePerShareTTM, - NetIncomePerShareTTM: *shadow.NetIncomePerShareTTM, - InterestDebtPerShareTTM: *shadow.InterestDebtPerShareTTM, - CashPerShareTTM: *shadow.CashPerShareTTM, - BookValuePerShareTTM: *shadow.BookValuePerShareTTM, - TangibleBookValuePerShareTTM: *shadow.TangibleBookValuePerShareTTM, - ShareholdersEquityPerShareTTM: *shadow.ShareholdersEquityPerShareTTM, - OperatingCashFlowPerShareTTM: *shadow.OperatingCashFlowPerShareTTM, - CapexPerShareTTM: *shadow.CapexPerShareTTM, - FreeCashFlowPerShareTTM: *shadow.FreeCashFlowPerShareTTM, - NetIncomePerEbtTTM: *shadow.NetIncomePerEbtTTM, - EbtPerEbitTTM: *shadow.EbtPerEbitTTM, - PriceToFairValueTTM: *shadow.PriceToFairValueTTM, - DebtToMarketCapTTM: *shadow.DebtToMarketCapTTM, - EffectiveTaxRateTTM: *shadow.EffectiveTaxRateTTM, - EnterpriseValueMultipleTTM: *shadow.EnterpriseValueMultipleTTM, - DividendPerShareTTM: *shadow.DividendPerShareTTM, + GrossProfitMarginTTM: shadow.GrossProfitMarginTTM, + EbitMarginTTM: shadow.EbitMarginTTM, + EbitdaMarginTTM: shadow.EbitdaMarginTTM, + OperatingProfitMarginTTM: shadow.OperatingProfitMarginTTM, + PretaxProfitMarginTTM: shadow.PretaxProfitMarginTTM, + ContinuousOperationsProfitMarginTTM: shadow.ContinuousOperationsProfitMarginTTM, + NetProfitMarginTTM: shadow.NetProfitMarginTTM, + BottomLineProfitMarginTTM: shadow.BottomLineProfitMarginTTM, + ReceivablesTurnoverTTM: shadow.ReceivablesTurnoverTTM, + PayablesTurnoverTTM: shadow.PayablesTurnoverTTM, + InventoryTurnoverTTM: shadow.InventoryTurnoverTTM, + FixedAssetTurnoverTTM: shadow.FixedAssetTurnoverTTM, + AssetTurnoverTTM: shadow.AssetTurnoverTTM, + CurrentRatioTTM: shadow.CurrentRatioTTM, + QuickRatioTTM: shadow.QuickRatioTTM, + SolvencyRatioTTM: shadow.SolvencyRatioTTM, + CashRatioTTM: shadow.CashRatioTTM, + PriceToEarningsRatioTTM: shadow.PriceToEarningsRatioTTM, + PriceToEarningsGrowthRatioTTM: shadow.PriceToEarningsGrowthRatioTTM, + ForwardPriceToEarningsGrowthRatioTTM: shadow.ForwardPriceToEarningsGrowthRatioTTM, + PriceToBookRatioTTM: shadow.PriceToBookRatioTTM, + PriceToSalesRatioTTM: shadow.PriceToSalesRatioTTM, + PriceToFreeCashFlowRatioTTM: shadow.PriceToFreeCashFlowRatioTTM, + PriceToOperatingCashFlowRatioTTM: shadow.PriceToOperatingCashFlowRatioTTM, + DebtToAssetsRatioTTM: shadow.DebtToAssetsRatioTTM, + DebtToEquityRatioTTM: shadow.DebtToEquityRatioTTM, + DebtToCapitalRatioTTM: shadow.DebtToCapitalRatioTTM, + LongTermDebtToCapitalRatioTTM: shadow.LongTermDebtToCapitalRatioTTM, + FinancialLeverageRatioTTM: shadow.FinancialLeverageRatioTTM, + WorkingCapitalTurnoverRatioTTM: shadow.WorkingCapitalTurnoverRatioTTM, + OperatingCashFlowRatioTTM: shadow.OperatingCashFlowRatioTTM, + OperatingCashFlowSalesRatioTTM: shadow.OperatingCashFlowSalesRatioTTM, + FreeCashFlowOperatingCashFlowRatioTTM: shadow.FreeCashFlowOperatingCashFlowRatioTTM, + DebtServiceCoverageRatioTTM: shadow.DebtServiceCoverageRatioTTM, + InterestCoverageRatioTTM: shadow.InterestCoverageRatioTTM, + ShortTermOperatingCashFlowCoverageRatioTTM: shadow.ShortTermOperatingCashFlowCoverageRatioTTM, + OperatingCashFlowCoverageRatioTTM: shadow.OperatingCashFlowCoverageRatioTTM, + CapitalExpenditureCoverageRatioTTM: shadow.CapitalExpenditureCoverageRatioTTM, + DividendPaidAndCapexCoverageRatioTTM: shadow.DividendPaidAndCapexCoverageRatioTTM, + DividendPayoutRatioTTM: shadow.DividendPayoutRatioTTM, + DividendYieldTTM: shadow.DividendYieldTTM, + EnterpriseValueTTM: shadow.EnterpriseValueTTM, + RevenuePerShareTTM: shadow.RevenuePerShareTTM, + NetIncomePerShareTTM: shadow.NetIncomePerShareTTM, + InterestDebtPerShareTTM: shadow.InterestDebtPerShareTTM, + CashPerShareTTM: shadow.CashPerShareTTM, + BookValuePerShareTTM: shadow.BookValuePerShareTTM, + TangibleBookValuePerShareTTM: shadow.TangibleBookValuePerShareTTM, + ShareholdersEquityPerShareTTM: shadow.ShareholdersEquityPerShareTTM, + OperatingCashFlowPerShareTTM: shadow.OperatingCashFlowPerShareTTM, + CapexPerShareTTM: shadow.CapexPerShareTTM, + FreeCashFlowPerShareTTM: shadow.FreeCashFlowPerShareTTM, + NetIncomePerEbtTTM: shadow.NetIncomePerEbtTTM, + EbtPerEbitTTM: shadow.EbtPerEbitTTM, + PriceToFairValueTTM: shadow.PriceToFairValueTTM, + DebtToMarketCapTTM: shadow.DebtToMarketCapTTM, + EffectiveTaxRateTTM: shadow.EffectiveTaxRateTTM, + EnterpriseValueMultipleTTM: shadow.EnterpriseValueMultipleTTM, + DividendPerShareTTM: shadow.DividendPerShareTTM, } return nil } @@ -2225,11 +2023,11 @@ func (m *BulkStockPeer) UnmarshalJSONFrom(dec *jsontext.Decoder) error { // BulkEarningsSurprise is one worldwide annual earnings-surprise bulk row. type BulkEarningsSurprise struct { - Symbol string `json:"symbol"` - Date Date `json:"date"` - EPSActual string `json:"epsActual"` - EPSEstimated string `json:"epsEstimated"` - LastUpdated Date `json:"lastUpdated"` + Symbol string `json:"symbol"` + Date Date `json:"date"` + EPSActual *string `json:"epsActual"` + EPSEstimated *string `json:"epsEstimated"` + LastUpdated Date `json:"lastUpdated"` } // bulkEarningsSurpriseShadow mirrors BulkEarningsSurprise with a pointer or @@ -2256,18 +2054,14 @@ func (m *BulkEarningsSurprise) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("BulkEarningsSurprise", "symbol") case shadow.Date == nil: return missingMemberError("BulkEarningsSurprise", "date") - case shadow.EPSActual == nil: - return missingMemberError("BulkEarningsSurprise", "epsActual") - case shadow.EPSEstimated == nil: - return missingMemberError("BulkEarningsSurprise", "epsEstimated") case shadow.LastUpdated == nil: return missingMemberError("BulkEarningsSurprise", "lastUpdated") } *m = BulkEarningsSurprise{ Symbol: *shadow.Symbol, Date: *shadow.Date, - EPSActual: *shadow.EPSActual, - EPSEstimated: *shadow.EPSEstimated, + EPSActual: shadow.EPSActual, + EPSEstimated: shadow.EPSEstimated, LastUpdated: *shadow.LastUpdated, } return nil diff --git a/sdk/go/bulk_parity_test.go b/sdk/go/bulk_parity_test.go index 0931c52d..df4ea246 100644 --- a/sdk/go/bulk_parity_test.go +++ b/sdk/go/bulk_parity_test.go @@ -82,8 +82,8 @@ func TestBulkSnapshotsDecodeExactValues(t *testing.T) { func TestBulkMetricsPreserveNumericTextIncludingBeyondU64(t *testing.T) { t.Parallel() metrics := assertCSVFixtureParity[BulkKeyMetricsTTM](t, "bulk_key_metrics_ttm.csv", 43) - if metrics[0].Symbol != "000001.SZ" || metrics[0].MarketCap != "224526473551" || - metrics[0].EvToEbitdaTTM != "29.23198788110799" { + if metrics[0].Symbol != "000001.SZ" || cellText(metrics[0].MarketCap) != "224526473551" || + cellText(metrics[0].EvToEbitdaTTM) != "29.23198788110799" { t.Fatalf("bulk_key_metrics_ttm = %+v", metrics[0]) } encoded, err := json.Marshal(metrics[0]) @@ -94,7 +94,7 @@ func TestBulkMetricsPreserveNumericTextIncludingBeyondU64(t *testing.T) { t.Fatalf("re-encoded key metrics = %s, %v", encoded, err) } ratios := assertCSVFixtureParity[BulkFinancialRatiosTTM](t, "bulk_financial_ratios_ttm.csv", 60) - if ratios[0].GrossProfitMarginTTM != "0.5535250166330814" || ratios[0].NetIncomePerEbtTTM != "0.83539656299258" { + if cellText(ratios[0].GrossProfitMarginTTM) != "0.5535250166330814" || cellText(ratios[0].NetIncomePerEbtTTM) != "0.83539656299258" { t.Fatalf("bulk_financial_ratios_ttm = %+v", ratios[0]) } @@ -113,8 +113,8 @@ func TestBulkMetricsPreserveNumericTextIncludingBeyondU64(t *testing.T) { writer := csv.NewWriter(&body) _ = writer.WriteAll([][]string{records[0], row}) decoded, _, kind, err := decodeCSVRows[BulkKeyMetricsTTM]([]byte(body.String())) - if kind != DecodeKindNone || decoded[0].MarketCap != beyondU64 || - decoded[0].FreeCashFlowToFirmTTM != highPrecision || decoded[0].EvToSalesTTM != "6.9148336e-9" { + if kind != DecodeKindNone || cellText(decoded[0].MarketCap) != beyondU64 || + cellText(decoded[0].FreeCashFlowToFirmTTM) != highPrecision || cellText(decoded[0].EvToSalesTTM) != "6.9148336e-9" { t.Fatalf("beyond-u64 decode = %+v, %v", decoded, err) } @@ -124,10 +124,16 @@ func TestBulkMetricsPreserveNumericTextIncludingBeyondU64(t *testing.T) { t.Fatalf("bulk_stock_peers = %+v", peers[0]) } surprises := assertCSVFixtureParity[BulkEarningsSurprise](t, "bulk_earnings_surprises.csv", 5) - if want := (BulkEarningsSurprise{Symbol: "AUTO.OL", Date: mustParseDate(t, "2024-12-31"), EPSActual: "0.1332", - EPSEstimated: "0.1581", LastUpdated: mustParseDate(t, "2025-10-07")}); surprises[0] != want { + if surprises[0].Symbol != "AUTO.OL" || cellText(surprises[0].EPSActual) != "0.1332" || + surprises[0].LastUpdated != mustParseDate(t, "2025-10-07") { t.Fatalf("bulk_earnings_surprises = %+v", surprises[0]) } + if surprises[2].Symbol != "GUD.TO" || surprises[2].EPSEstimated != nil || cellText(surprises[2].EPSActual) != "0.00084" { + t.Fatalf("empty earnings cell = %+v", surprises[2]) + } + if metrics[2].Symbol != "ADAMO" || metrics[2].EnterpriseValueTTM != nil || peers[2].Peers != "" { + t.Fatalf("empty metric cells = %+v, %+v", metrics[2], peers[2]) + } } // Exact values copied from crates/libfmp/tests/bulk_{income,balance,cash_eod}_responses.rs: diff --git a/sdk/go/bulk_test.go b/sdk/go/bulk_test.go index 2be04b87..d4ec7f95 100644 --- a/sdk/go/bulk_test.go +++ b/sdk/go/bulk_test.go @@ -86,19 +86,19 @@ func TestBulkMethodsUseExactPathsAndWireParameterOrder(t *testing.T) { t.Fatalf("UpgradesDowngradesConsensus = %+v, %v", consensus, err) } metrics, err := client.Bulk.KeyMetricsTTM(ctx) - if err != nil || len(metrics) != 2 || metrics[0].MarketCap != "224526473551" { + if err != nil || len(metrics) != 3 || cellText(metrics[0].MarketCap) != "224526473551" { t.Fatalf("KeyMetricsTTM = %+v, %v", metrics, err) } ratios, err := client.Bulk.FinancialRatiosTTM(ctx) - if err != nil || len(ratios) != 2 || ratios[0].GrossProfitMarginTTM != "0.5535250166330814" { + if err != nil || len(ratios) != 3 || cellText(ratios[0].GrossProfitMarginTTM) != "0.5535250166330814" { t.Fatalf("FinancialRatiosTTM = %+v, %v", ratios, err) } peers, err := client.Bulk.StockPeers(ctx) - if err != nil || len(peers) != 2 || peers[0].Peers != "3698.HK,600000.SS,600015.SS,600016.SS,600036.SS,601166.SS,601658.SS" { + if err != nil || len(peers) != 3 || peers[0].Peers != "3698.HK,600000.SS,600015.SS,600016.SS,600036.SS,601166.SS,601658.SS" { t.Fatalf("StockPeers = %+v, %v", peers, err) } surprises, err := client.Bulk.EarningsSurprises(ctx, NewBulkYearQuery(4_294_967_295)) - if err != nil || len(surprises) != 2 || surprises[0].Symbol != "AUTO.OL" { + if err != nil || len(surprises) != 3 || surprises[0].Symbol != "AUTO.OL" { t.Fatalf("EarningsSurprises = %+v, %v", surprises, err) } income, err := client.Bulk.IncomeStatements(ctx, NewBulkStatementQuery(2026, FiscalPeriodQ1)) From b00597d930c2bba41fb7307ecc17587319c601bf Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 11:18:20 +0800 Subject: [PATCH 24/31] fix(bulk)!: make bulk income numeric members optional Under ADR 0035, an empty CSV cell is the only absence marker in bulk rows, and sparse empties vary by period. Every numeric metric member of BulkIncomeStatement and BulkIncomeStatementGrowth now becomes Option (Python Optional[str], Go *string), while identity members (date, symbol, reportedCurrency, cik, filingDate, acceptedDate, fiscalYear, period) stay required. The live FY2024 census saw empty eps, epsDiluted, weightedAverageShsOut(Dil), netIncomeDeductions and others. The fixture gains the live OASMY row with empty cells, and tests assert None where the cells are absent. Regenerated Python models/stubs and Go models are included. BREAKING CHANGE: every numeric member of BulkIncomeStatement and BulkIncomeStatementGrowth is now optional in Rust, Python and Go. --- .../fmp/_native/bulk/income/__init__.pyi | 262 ++++++------ crates/fmp-py/src/models/bulk/income.rs | 394 ++++++++++-------- crates/fmp-py/tests/test_bulk_b.py | 3 +- crates/libfmp/src/responses/bulk/income.rs | 120 +++--- crates/libfmp/tests/bulk_income_responses.rs | 40 +- .../tests/fixtures/bulk_income_statements.csv | 1 + sdk/go/bulk_models.go | 370 ++++++---------- sdk/go/bulk_parity_test.go | 5 +- sdk/go/bulk_test.go | 4 +- 9 files changed, 584 insertions(+), 615 deletions(-) diff --git a/crates/fmp-py/python/fmp/_native/bulk/income/__init__.pyi b/crates/fmp-py/python/fmp/_native/bulk/income/__init__.pyi index bae6c9ef..b13e43a7 100644 --- a/crates/fmp-py/python/fmp/_native/bulk/income/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/bulk/income/__init__.pyi @@ -70,67 +70,75 @@ class BulkIncomeStatement: @property def fiscal_year(self) -> builtins.str: ... @property - def revenue(self) -> builtins.str: ... + def revenue(self) -> typing.Optional[builtins.str]: ... @property - def cost_of_revenue(self) -> builtins.str: ... + def cost_of_revenue(self) -> typing.Optional[builtins.str]: ... @property - def gross_profit(self) -> builtins.str: ... + def gross_profit(self) -> typing.Optional[builtins.str]: ... @property - def research_and_development_expenses(self) -> builtins.str: ... + def research_and_development_expenses(self) -> typing.Optional[builtins.str]: ... @property - def general_and_administrative_expenses(self) -> builtins.str: ... + def general_and_administrative_expenses(self) -> typing.Optional[builtins.str]: ... @property - def selling_and_marketing_expenses(self) -> builtins.str: ... + def selling_and_marketing_expenses(self) -> typing.Optional[builtins.str]: ... @property - def selling_general_and_administrative_expenses(self) -> builtins.str: ... + def selling_general_and_administrative_expenses( + self, + ) -> typing.Optional[builtins.str]: ... @property - def other_expenses(self) -> builtins.str: ... + def other_expenses(self) -> typing.Optional[builtins.str]: ... @property - def operating_expenses(self) -> builtins.str: ... + def operating_expenses(self) -> typing.Optional[builtins.str]: ... @property - def cost_and_expenses(self) -> builtins.str: ... + def cost_and_expenses(self) -> typing.Optional[builtins.str]: ... @property - def net_interest_income(self) -> builtins.str: ... + def net_interest_income(self) -> typing.Optional[builtins.str]: ... @property - def interest_income(self) -> builtins.str: ... + def interest_income(self) -> typing.Optional[builtins.str]: ... @property - def interest_expense(self) -> builtins.str: ... + def interest_expense(self) -> typing.Optional[builtins.str]: ... @property - def depreciation_and_amortization(self) -> builtins.str: ... + def depreciation_and_amortization(self) -> typing.Optional[builtins.str]: ... @property - def ebitda(self) -> builtins.str: ... + def ebitda(self) -> typing.Optional[builtins.str]: ... @property - def ebit(self) -> builtins.str: ... + def ebit(self) -> typing.Optional[builtins.str]: ... @property - def non_operating_income_excluding_interest(self) -> builtins.str: ... + def non_operating_income_excluding_interest( + self, + ) -> typing.Optional[builtins.str]: ... @property - def operating_income(self) -> builtins.str: ... + def operating_income(self) -> typing.Optional[builtins.str]: ... @property - def total_other_income_expenses_net(self) -> builtins.str: ... + def total_other_income_expenses_net(self) -> typing.Optional[builtins.str]: ... @property - def income_before_tax(self) -> builtins.str: ... + def income_before_tax(self) -> typing.Optional[builtins.str]: ... @property - def income_tax_expense(self) -> builtins.str: ... + def income_tax_expense(self) -> typing.Optional[builtins.str]: ... @property - def net_income_from_continuing_operations(self) -> builtins.str: ... + def net_income_from_continuing_operations( + self, + ) -> typing.Optional[builtins.str]: ... @property - def net_income_from_discontinued_operations(self) -> builtins.str: ... + def net_income_from_discontinued_operations( + self, + ) -> typing.Optional[builtins.str]: ... @property - def other_adjustments_to_net_income(self) -> builtins.str: ... + def other_adjustments_to_net_income(self) -> typing.Optional[builtins.str]: ... @property - def net_income(self) -> builtins.str: ... + def net_income(self) -> typing.Optional[builtins.str]: ... @property - def net_income_deductions(self) -> builtins.str: ... + def net_income_deductions(self) -> typing.Optional[builtins.str]: ... @property - def bottom_line_net_income(self) -> builtins.str: ... + def bottom_line_net_income(self) -> typing.Optional[builtins.str]: ... @property - def eps(self) -> builtins.str: ... + def eps(self) -> typing.Optional[builtins.str]: ... @property - def eps_diluted(self) -> builtins.str: ... + def eps_diluted(self) -> typing.Optional[builtins.str]: ... @property - def weighted_average_shs_out(self) -> builtins.str: ... + def weighted_average_shs_out(self) -> typing.Optional[builtins.str]: ... @property - def weighted_average_shs_out_dil(self) -> builtins.str: ... + def weighted_average_shs_out_dil(self) -> typing.Optional[builtins.str]: ... @property def period(self) -> typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... @@ -145,37 +153,37 @@ class BulkIncomeStatement: accepted_date: datetime.datetime, fiscal_year: builtins.str, period: typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"], - revenue: builtins.str, - cost_of_revenue: builtins.str, - gross_profit: builtins.str, - research_and_development_expenses: builtins.str, - general_and_administrative_expenses: builtins.str, - selling_and_marketing_expenses: builtins.str, - selling_general_and_administrative_expenses: builtins.str, - other_expenses: builtins.str, - operating_expenses: builtins.str, - cost_and_expenses: builtins.str, - net_interest_income: builtins.str, - interest_income: builtins.str, - interest_expense: builtins.str, - depreciation_and_amortization: builtins.str, - ebitda: builtins.str, - ebit: builtins.str, - non_operating_income_excluding_interest: builtins.str, - operating_income: builtins.str, - total_other_income_expenses_net: builtins.str, - income_before_tax: builtins.str, - income_tax_expense: builtins.str, - net_income_from_continuing_operations: builtins.str, - net_income_from_discontinued_operations: builtins.str, - other_adjustments_to_net_income: builtins.str, - net_income: builtins.str, - net_income_deductions: builtins.str, - bottom_line_net_income: builtins.str, - eps: builtins.str, - eps_diluted: builtins.str, - weighted_average_shs_out: builtins.str, - weighted_average_shs_out_dil: builtins.str, + revenue: typing.Optional[builtins.str], + cost_of_revenue: typing.Optional[builtins.str], + gross_profit: typing.Optional[builtins.str], + research_and_development_expenses: typing.Optional[builtins.str], + general_and_administrative_expenses: typing.Optional[builtins.str], + selling_and_marketing_expenses: typing.Optional[builtins.str], + selling_general_and_administrative_expenses: typing.Optional[builtins.str], + other_expenses: typing.Optional[builtins.str], + operating_expenses: typing.Optional[builtins.str], + cost_and_expenses: typing.Optional[builtins.str], + net_interest_income: typing.Optional[builtins.str], + interest_income: typing.Optional[builtins.str], + interest_expense: typing.Optional[builtins.str], + depreciation_and_amortization: typing.Optional[builtins.str], + ebitda: typing.Optional[builtins.str], + ebit: typing.Optional[builtins.str], + non_operating_income_excluding_interest: typing.Optional[builtins.str], + operating_income: typing.Optional[builtins.str], + total_other_income_expenses_net: typing.Optional[builtins.str], + income_before_tax: typing.Optional[builtins.str], + income_tax_expense: typing.Optional[builtins.str], + net_income_from_continuing_operations: typing.Optional[builtins.str], + net_income_from_discontinued_operations: typing.Optional[builtins.str], + other_adjustments_to_net_income: typing.Optional[builtins.str], + net_income: typing.Optional[builtins.str], + net_income_deductions: typing.Optional[builtins.str], + bottom_line_net_income: typing.Optional[builtins.str], + eps: typing.Optional[builtins.str], + eps_diluted: typing.Optional[builtins.str], + weighted_average_shs_out: typing.Optional[builtins.str], + weighted_average_shs_out_dil: typing.Optional[builtins.str], ) -> BulkIncomeStatement: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... @@ -235,63 +243,77 @@ class BulkIncomeStatementGrowth: @property def reported_currency(self) -> builtins.str: ... @property - def growth_revenue(self) -> builtins.str: ... + def growth_revenue(self) -> typing.Optional[builtins.str]: ... @property - def growth_cost_of_revenue(self) -> builtins.str: ... + def growth_cost_of_revenue(self) -> typing.Optional[builtins.str]: ... @property - def growth_gross_profit(self) -> builtins.str: ... + def growth_gross_profit(self) -> typing.Optional[builtins.str]: ... @property - def growth_gross_profit_ratio(self) -> builtins.str: ... + def growth_gross_profit_ratio(self) -> typing.Optional[builtins.str]: ... @property - def growth_research_and_development_expenses(self) -> builtins.str: ... + def growth_research_and_development_expenses( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_general_and_administrative_expenses(self) -> builtins.str: ... + def growth_general_and_administrative_expenses( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_selling_and_marketing_expenses(self) -> builtins.str: ... + def growth_selling_and_marketing_expenses( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_other_expenses(self) -> builtins.str: ... + def growth_other_expenses(self) -> typing.Optional[builtins.str]: ... @property - def growth_operating_expenses(self) -> builtins.str: ... + def growth_operating_expenses(self) -> typing.Optional[builtins.str]: ... @property - def growth_cost_and_expenses(self) -> builtins.str: ... + def growth_cost_and_expenses(self) -> typing.Optional[builtins.str]: ... @property - def growth_interest_income(self) -> builtins.str: ... + def growth_interest_income(self) -> typing.Optional[builtins.str]: ... @property - def growth_interest_expense(self) -> builtins.str: ... + def growth_interest_expense(self) -> typing.Optional[builtins.str]: ... @property - def growth_depreciation_and_amortization(self) -> builtins.str: ... + def growth_depreciation_and_amortization(self) -> typing.Optional[builtins.str]: ... @property - def growth_ebitda(self) -> builtins.str: ... + def growth_ebitda(self) -> typing.Optional[builtins.str]: ... @property - def growth_operating_income(self) -> builtins.str: ... + def growth_operating_income(self) -> typing.Optional[builtins.str]: ... @property - def growth_income_before_tax(self) -> builtins.str: ... + def growth_income_before_tax(self) -> typing.Optional[builtins.str]: ... @property - def growth_income_tax_expense(self) -> builtins.str: ... + def growth_income_tax_expense(self) -> typing.Optional[builtins.str]: ... @property - def growth_net_income(self) -> builtins.str: ... + def growth_net_income(self) -> typing.Optional[builtins.str]: ... @property - def growth_eps(self) -> builtins.str: ... + def growth_eps(self) -> typing.Optional[builtins.str]: ... @property - def growth_eps_diluted(self) -> builtins.str: ... + def growth_eps_diluted(self) -> typing.Optional[builtins.str]: ... @property - def growth_weighted_average_shs_out(self) -> builtins.str: ... + def growth_weighted_average_shs_out(self) -> typing.Optional[builtins.str]: ... @property - def growth_weighted_average_shs_out_dil(self) -> builtins.str: ... + def growth_weighted_average_shs_out_dil(self) -> typing.Optional[builtins.str]: ... @property - def growth_ebit(self) -> builtins.str: ... + def growth_ebit(self) -> typing.Optional[builtins.str]: ... @property - def growth_non_operating_income_excluding_interest(self) -> builtins.str: ... + def growth_non_operating_income_excluding_interest( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_net_interest_income(self) -> builtins.str: ... + def growth_net_interest_income(self) -> typing.Optional[builtins.str]: ... @property - def growth_total_other_income_expenses_net(self) -> builtins.str: ... + def growth_total_other_income_expenses_net( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_net_income_from_continuing_operations(self) -> builtins.str: ... + def growth_net_income_from_continuing_operations( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_other_adjustments_to_net_income(self) -> builtins.str: ... + def growth_other_adjustments_to_net_income( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_net_income_deductions(self) -> builtins.str: ... + def growth_net_income_deductions(self) -> typing.Optional[builtins.str]: ... @property def period(self) -> typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... @@ -303,35 +325,35 @@ class BulkIncomeStatementGrowth: fiscal_year: builtins.str, period: typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"], reported_currency: builtins.str, - growth_revenue: builtins.str, - growth_cost_of_revenue: builtins.str, - growth_gross_profit: builtins.str, - growth_gross_profit_ratio: builtins.str, - growth_research_and_development_expenses: builtins.str, - growth_general_and_administrative_expenses: builtins.str, - growth_selling_and_marketing_expenses: builtins.str, - growth_other_expenses: builtins.str, - growth_operating_expenses: builtins.str, - growth_cost_and_expenses: builtins.str, - growth_interest_income: builtins.str, - growth_interest_expense: builtins.str, - growth_depreciation_and_amortization: builtins.str, - growth_ebitda: builtins.str, - growth_operating_income: builtins.str, - growth_income_before_tax: builtins.str, - growth_income_tax_expense: builtins.str, - growth_net_income: builtins.str, - growth_eps: builtins.str, - growth_eps_diluted: builtins.str, - growth_weighted_average_shs_out: builtins.str, - growth_weighted_average_shs_out_dil: builtins.str, - growth_ebit: builtins.str, - growth_non_operating_income_excluding_interest: builtins.str, - growth_net_interest_income: builtins.str, - growth_total_other_income_expenses_net: builtins.str, - growth_net_income_from_continuing_operations: builtins.str, - growth_other_adjustments_to_net_income: builtins.str, - growth_net_income_deductions: builtins.str, + growth_revenue: typing.Optional[builtins.str], + growth_cost_of_revenue: typing.Optional[builtins.str], + growth_gross_profit: typing.Optional[builtins.str], + growth_gross_profit_ratio: typing.Optional[builtins.str], + growth_research_and_development_expenses: typing.Optional[builtins.str], + growth_general_and_administrative_expenses: typing.Optional[builtins.str], + growth_selling_and_marketing_expenses: typing.Optional[builtins.str], + growth_other_expenses: typing.Optional[builtins.str], + growth_operating_expenses: typing.Optional[builtins.str], + growth_cost_and_expenses: typing.Optional[builtins.str], + growth_interest_income: typing.Optional[builtins.str], + growth_interest_expense: typing.Optional[builtins.str], + growth_depreciation_and_amortization: typing.Optional[builtins.str], + growth_ebitda: typing.Optional[builtins.str], + growth_operating_income: typing.Optional[builtins.str], + growth_income_before_tax: typing.Optional[builtins.str], + growth_income_tax_expense: typing.Optional[builtins.str], + growth_net_income: typing.Optional[builtins.str], + growth_eps: typing.Optional[builtins.str], + growth_eps_diluted: typing.Optional[builtins.str], + growth_weighted_average_shs_out: typing.Optional[builtins.str], + growth_weighted_average_shs_out_dil: typing.Optional[builtins.str], + growth_ebit: typing.Optional[builtins.str], + growth_non_operating_income_excluding_interest: typing.Optional[builtins.str], + growth_net_interest_income: typing.Optional[builtins.str], + growth_total_other_income_expenses_net: typing.Optional[builtins.str], + growth_net_income_from_continuing_operations: typing.Optional[builtins.str], + growth_other_adjustments_to_net_income: typing.Optional[builtins.str], + growth_net_income_deductions: typing.Optional[builtins.str], ) -> BulkIncomeStatementGrowth: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... diff --git a/crates/fmp-py/src/models/bulk/income.rs b/crates/fmp-py/src/models/bulk/income.rs index d65af1ad..7a2d6f5b 100644 --- a/crates/fmp-py/src/models/bulk/income.rs +++ b/crates/fmp-py/src/models/bulk/income.rs @@ -25,67 +25,67 @@ pub(crate) struct BulkIncomeStatement { pub fiscal_year: String, pub period: String, #[pyo3(get)] - pub revenue: String, + pub revenue: Option, #[pyo3(get)] - pub cost_of_revenue: String, + pub cost_of_revenue: Option, #[pyo3(get)] - pub gross_profit: String, + pub gross_profit: Option, #[pyo3(get)] - pub research_and_development_expenses: String, + pub research_and_development_expenses: Option, #[pyo3(get)] - pub general_and_administrative_expenses: String, + pub general_and_administrative_expenses: Option, #[pyo3(get)] - pub selling_and_marketing_expenses: String, + pub selling_and_marketing_expenses: Option, #[pyo3(get)] - pub selling_general_and_administrative_expenses: String, + pub selling_general_and_administrative_expenses: Option, #[pyo3(get)] - pub other_expenses: String, + pub other_expenses: Option, #[pyo3(get)] - pub operating_expenses: String, + pub operating_expenses: Option, #[pyo3(get)] - pub cost_and_expenses: String, + pub cost_and_expenses: Option, #[pyo3(get)] - pub net_interest_income: String, + pub net_interest_income: Option, #[pyo3(get)] - pub interest_income: String, + pub interest_income: Option, #[pyo3(get)] - pub interest_expense: String, + pub interest_expense: Option, #[pyo3(get)] - pub depreciation_and_amortization: String, + pub depreciation_and_amortization: Option, #[pyo3(get)] - pub ebitda: String, + pub ebitda: Option, #[pyo3(get)] - pub ebit: String, + pub ebit: Option, #[pyo3(get)] - pub non_operating_income_excluding_interest: String, + pub non_operating_income_excluding_interest: Option, #[pyo3(get)] - pub operating_income: String, + pub operating_income: Option, #[pyo3(get)] - pub total_other_income_expenses_net: String, + pub total_other_income_expenses_net: Option, #[pyo3(get)] - pub income_before_tax: String, + pub income_before_tax: Option, #[pyo3(get)] - pub income_tax_expense: String, + pub income_tax_expense: Option, #[pyo3(get)] - pub net_income_from_continuing_operations: String, + pub net_income_from_continuing_operations: Option, #[pyo3(get)] - pub net_income_from_discontinued_operations: String, + pub net_income_from_discontinued_operations: Option, #[pyo3(get)] - pub other_adjustments_to_net_income: String, + pub other_adjustments_to_net_income: Option, #[pyo3(get)] - pub net_income: String, + pub net_income: Option, #[pyo3(get)] - pub net_income_deductions: String, + pub net_income_deductions: Option, #[pyo3(get)] - pub bottom_line_net_income: String, + pub bottom_line_net_income: Option, #[pyo3(get)] - pub eps: String, + pub eps: Option, #[pyo3(get)] - pub eps_diluted: String, + pub eps_diluted: Option, #[pyo3(get)] - pub weighted_average_shs_out: String, + pub weighted_average_shs_out: Option, #[pyo3(get)] - pub weighted_average_shs_out_dil: String, + pub weighted_average_shs_out_dil: Option, } #[gen_stub_pymethods] @@ -105,37 +105,37 @@ impl BulkIncomeStatement { fiscal_year: String, #[gen_stub(override_type(type_repr = "typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\"]", imports = ("builtins", "typing")))] period: String, - revenue: String, - cost_of_revenue: String, - gross_profit: String, - research_and_development_expenses: String, - general_and_administrative_expenses: String, - selling_and_marketing_expenses: String, - selling_general_and_administrative_expenses: String, - other_expenses: String, - operating_expenses: String, - cost_and_expenses: String, - net_interest_income: String, - interest_income: String, - interest_expense: String, - depreciation_and_amortization: String, - ebitda: String, - ebit: String, - non_operating_income_excluding_interest: String, - operating_income: String, - total_other_income_expenses_net: String, - income_before_tax: String, - income_tax_expense: String, - net_income_from_continuing_operations: String, - net_income_from_discontinued_operations: String, - other_adjustments_to_net_income: String, - net_income: String, - net_income_deductions: String, - bottom_line_net_income: String, - eps: String, - eps_diluted: String, - weighted_average_shs_out: String, - weighted_average_shs_out_dil: String, + revenue: Option, + cost_of_revenue: Option, + gross_profit: Option, + research_and_development_expenses: Option, + general_and_administrative_expenses: Option, + selling_and_marketing_expenses: Option, + selling_general_and_administrative_expenses: Option, + other_expenses: Option, + operating_expenses: Option, + cost_and_expenses: Option, + net_interest_income: Option, + interest_income: Option, + interest_expense: Option, + depreciation_and_amortization: Option, + ebitda: Option, + ebit: Option, + non_operating_income_excluding_interest: Option, + operating_income: Option, + total_other_income_expenses_net: Option, + income_before_tax: Option, + income_tax_expense: Option, + net_income_from_continuing_operations: Option, + net_income_from_discontinued_operations: Option, + other_adjustments_to_net_income: Option, + net_income: Option, + net_income_deductions: Option, + bottom_line_net_income: Option, + eps: Option, + eps_diluted: Option, + weighted_average_shs_out: Option, + weighted_average_shs_out_dil: Option, ) -> Self { Self { date, @@ -764,47 +764,61 @@ impl From for BulkIncomeStatement accepted_date: value.accepted_date.into_inner(), fiscal_year: value.fiscal_year.as_str().to_owned(), period: value.period.to_string(), - revenue: value.revenue.into_inner(), - cost_of_revenue: value.cost_of_revenue.into_inner(), - gross_profit: value.gross_profit.into_inner(), - research_and_development_expenses: value.research_and_development_expenses.into_inner(), + revenue: value.revenue.map(|value| value.into_inner()), + cost_of_revenue: value.cost_of_revenue.map(|value| value.into_inner()), + gross_profit: value.gross_profit.map(|value| value.into_inner()), + research_and_development_expenses: value + .research_and_development_expenses + .map(|value| value.into_inner()), general_and_administrative_expenses: value .general_and_administrative_expenses - .into_inner(), - selling_and_marketing_expenses: value.selling_and_marketing_expenses.into_inner(), + .map(|value| value.into_inner()), + selling_and_marketing_expenses: value + .selling_and_marketing_expenses + .map(|value| value.into_inner()), selling_general_and_administrative_expenses: value .selling_general_and_administrative_expenses - .into_inner(), - other_expenses: value.other_expenses.into_inner(), - operating_expenses: value.operating_expenses.into_inner(), - cost_and_expenses: value.cost_and_expenses.into_inner(), - net_interest_income: value.net_interest_income.into_inner(), - interest_income: value.interest_income.into_inner(), - interest_expense: value.interest_expense.into_inner(), - depreciation_and_amortization: value.depreciation_and_amortization.into_inner(), - ebitda: value.ebitda.into_inner(), - ebit: value.ebit.into_inner(), + .map(|value| value.into_inner()), + other_expenses: value.other_expenses.map(|value| value.into_inner()), + operating_expenses: value.operating_expenses.map(|value| value.into_inner()), + cost_and_expenses: value.cost_and_expenses.map(|value| value.into_inner()), + net_interest_income: value.net_interest_income.map(|value| value.into_inner()), + interest_income: value.interest_income.map(|value| value.into_inner()), + interest_expense: value.interest_expense.map(|value| value.into_inner()), + depreciation_and_amortization: value + .depreciation_and_amortization + .map(|value| value.into_inner()), + ebitda: value.ebitda.map(|value| value.into_inner()), + ebit: value.ebit.map(|value| value.into_inner()), non_operating_income_excluding_interest: value .non_operating_income_excluding_interest - .into_inner(), - operating_income: value.operating_income.into_inner(), - total_other_income_expenses_net: value.total_other_income_expenses_net.into_inner(), - income_before_tax: value.income_before_tax.into_inner(), - income_tax_expense: value.income_tax_expense.into_inner(), + .map(|value| value.into_inner()), + operating_income: value.operating_income.map(|value| value.into_inner()), + total_other_income_expenses_net: value + .total_other_income_expenses_net + .map(|value| value.into_inner()), + income_before_tax: value.income_before_tax.map(|value| value.into_inner()), + income_tax_expense: value.income_tax_expense.map(|value| value.into_inner()), net_income_from_continuing_operations: value .net_income_from_continuing_operations - .into_inner(), + .map(|value| value.into_inner()), net_income_from_discontinued_operations: value .net_income_from_discontinued_operations - .into_inner(), - other_adjustments_to_net_income: value.other_adjustments_to_net_income.into_inner(), - net_income: value.net_income.into_inner(), - net_income_deductions: value.net_income_deductions.into_inner(), - bottom_line_net_income: value.bottom_line_net_income.into_inner(), - eps: value.eps.into_inner(), - eps_diluted: value.eps_diluted.into_inner(), - weighted_average_shs_out: value.weighted_average_shs_out.into_inner(), - weighted_average_shs_out_dil: value.weighted_average_shs_out_dil.into_inner(), + .map(|value| value.into_inner()), + other_adjustments_to_net_income: value + .other_adjustments_to_net_income + .map(|value| value.into_inner()), + net_income: value.net_income.map(|value| value.into_inner()), + net_income_deductions: value.net_income_deductions.map(|value| value.into_inner()), + bottom_line_net_income: value.bottom_line_net_income.map(|value| value.into_inner()), + eps: value.eps.map(|value| value.into_inner()), + eps_diluted: value.eps_diluted.map(|value| value.into_inner()), + weighted_average_shs_out: value + .weighted_average_shs_out + .map(|value| value.into_inner()), + weighted_average_shs_out_dil: value + .weighted_average_shs_out_dil + .map(|value| value.into_inner()), } } } @@ -829,63 +843,63 @@ pub(crate) struct BulkIncomeStatementGrowth { #[pyo3(get)] pub reported_currency: String, #[pyo3(get)] - pub growth_revenue: String, + pub growth_revenue: Option, #[pyo3(get)] - pub growth_cost_of_revenue: String, + pub growth_cost_of_revenue: Option, #[pyo3(get)] - pub growth_gross_profit: String, + pub growth_gross_profit: Option, #[pyo3(get)] - pub growth_gross_profit_ratio: String, + pub growth_gross_profit_ratio: Option, #[pyo3(get)] - pub growth_research_and_development_expenses: String, + pub growth_research_and_development_expenses: Option, #[pyo3(get)] - pub growth_general_and_administrative_expenses: String, + pub growth_general_and_administrative_expenses: Option, #[pyo3(get)] - pub growth_selling_and_marketing_expenses: String, + pub growth_selling_and_marketing_expenses: Option, #[pyo3(get)] - pub growth_other_expenses: String, + pub growth_other_expenses: Option, #[pyo3(get)] - pub growth_operating_expenses: String, + pub growth_operating_expenses: Option, #[pyo3(get)] - pub growth_cost_and_expenses: String, + pub growth_cost_and_expenses: Option, #[pyo3(get)] - pub growth_interest_income: String, + pub growth_interest_income: Option, #[pyo3(get)] - pub growth_interest_expense: String, + pub growth_interest_expense: Option, #[pyo3(get)] - pub growth_depreciation_and_amortization: String, + pub growth_depreciation_and_amortization: Option, #[pyo3(get)] - pub growth_ebitda: String, + pub growth_ebitda: Option, #[pyo3(get)] - pub growth_operating_income: String, + pub growth_operating_income: Option, #[pyo3(get)] - pub growth_income_before_tax: String, + pub growth_income_before_tax: Option, #[pyo3(get)] - pub growth_income_tax_expense: String, + pub growth_income_tax_expense: Option, #[pyo3(get)] - pub growth_net_income: String, + pub growth_net_income: Option, #[pyo3(get)] - pub growth_eps: String, + pub growth_eps: Option, #[pyo3(get)] - pub growth_eps_diluted: String, + pub growth_eps_diluted: Option, #[pyo3(get)] - pub growth_weighted_average_shs_out: String, + pub growth_weighted_average_shs_out: Option, #[pyo3(get)] - pub growth_weighted_average_shs_out_dil: String, + pub growth_weighted_average_shs_out_dil: Option, #[pyo3(get)] - pub growth_ebit: String, + pub growth_ebit: Option, #[pyo3(get)] - pub growth_non_operating_income_excluding_interest: String, + pub growth_non_operating_income_excluding_interest: Option, #[pyo3(get)] - pub growth_net_interest_income: String, + pub growth_net_interest_income: Option, #[pyo3(get)] - pub growth_total_other_income_expenses_net: String, + pub growth_total_other_income_expenses_net: Option, #[pyo3(get)] - pub growth_net_income_from_continuing_operations: String, + pub growth_net_income_from_continuing_operations: Option, #[pyo3(get)] - pub growth_other_adjustments_to_net_income: String, + pub growth_other_adjustments_to_net_income: Option, #[pyo3(get)] - pub growth_net_income_deductions: String, + pub growth_net_income_deductions: Option, } #[gen_stub_pymethods] @@ -902,35 +916,35 @@ impl BulkIncomeStatementGrowth { #[gen_stub(override_type(type_repr = "typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\"]", imports = ("builtins", "typing")))] period: String, reported_currency: String, - growth_revenue: String, - growth_cost_of_revenue: String, - growth_gross_profit: String, - growth_gross_profit_ratio: String, - growth_research_and_development_expenses: String, - growth_general_and_administrative_expenses: String, - growth_selling_and_marketing_expenses: String, - growth_other_expenses: String, - growth_operating_expenses: String, - growth_cost_and_expenses: String, - growth_interest_income: String, - growth_interest_expense: String, - growth_depreciation_and_amortization: String, - growth_ebitda: String, - growth_operating_income: String, - growth_income_before_tax: String, - growth_income_tax_expense: String, - growth_net_income: String, - growth_eps: String, - growth_eps_diluted: String, - growth_weighted_average_shs_out: String, - growth_weighted_average_shs_out_dil: String, - growth_ebit: String, - growth_non_operating_income_excluding_interest: String, - growth_net_interest_income: String, - growth_total_other_income_expenses_net: String, - growth_net_income_from_continuing_operations: String, - growth_other_adjustments_to_net_income: String, - growth_net_income_deductions: String, + growth_revenue: Option, + growth_cost_of_revenue: Option, + growth_gross_profit: Option, + growth_gross_profit_ratio: Option, + growth_research_and_development_expenses: Option, + growth_general_and_administrative_expenses: Option, + growth_selling_and_marketing_expenses: Option, + growth_other_expenses: Option, + growth_operating_expenses: Option, + growth_cost_and_expenses: Option, + growth_interest_income: Option, + growth_interest_expense: Option, + growth_depreciation_and_amortization: Option, + growth_ebitda: Option, + growth_operating_income: Option, + growth_income_before_tax: Option, + growth_income_tax_expense: Option, + growth_net_income: Option, + growth_eps: Option, + growth_eps_diluted: Option, + growth_weighted_average_shs_out: Option, + growth_weighted_average_shs_out_dil: Option, + growth_ebit: Option, + growth_non_operating_income_excluding_interest: Option, + growth_net_interest_income: Option, + growth_total_other_income_expenses_net: Option, + growth_net_income_from_continuing_operations: Option, + growth_other_adjustments_to_net_income: Option, + growth_net_income_deductions: Option, ) -> Self { Self { symbol, @@ -1539,53 +1553,73 @@ impl From for BulkIncomeStat fiscal_year: value.fiscal_year.as_str().to_owned(), period: value.period.to_string(), reported_currency: value.reported_currency.into_inner(), - growth_revenue: value.growth_revenue.into_inner(), - growth_cost_of_revenue: value.growth_cost_of_revenue.into_inner(), - growth_gross_profit: value.growth_gross_profit.into_inner(), - growth_gross_profit_ratio: value.growth_gross_profit_ratio.into_inner(), + growth_revenue: value.growth_revenue.map(|value| value.into_inner()), + growth_cost_of_revenue: value.growth_cost_of_revenue.map(|value| value.into_inner()), + growth_gross_profit: value.growth_gross_profit.map(|value| value.into_inner()), + growth_gross_profit_ratio: value + .growth_gross_profit_ratio + .map(|value| value.into_inner()), growth_research_and_development_expenses: value .growth_research_and_development_expenses - .into_inner(), + .map(|value| value.into_inner()), growth_general_and_administrative_expenses: value .growth_general_and_administrative_expenses - .into_inner(), + .map(|value| value.into_inner()), growth_selling_and_marketing_expenses: value .growth_selling_and_marketing_expenses - .into_inner(), - growth_other_expenses: value.growth_other_expenses.into_inner(), - growth_operating_expenses: value.growth_operating_expenses.into_inner(), - growth_cost_and_expenses: value.growth_cost_and_expenses.into_inner(), - growth_interest_income: value.growth_interest_income.into_inner(), - growth_interest_expense: value.growth_interest_expense.into_inner(), + .map(|value| value.into_inner()), + growth_other_expenses: value.growth_other_expenses.map(|value| value.into_inner()), + growth_operating_expenses: value + .growth_operating_expenses + .map(|value| value.into_inner()), + growth_cost_and_expenses: value + .growth_cost_and_expenses + .map(|value| value.into_inner()), + growth_interest_income: value.growth_interest_income.map(|value| value.into_inner()), + growth_interest_expense: value + .growth_interest_expense + .map(|value| value.into_inner()), growth_depreciation_and_amortization: value .growth_depreciation_and_amortization - .into_inner(), - growth_ebitda: value.growth_ebitda.into_inner(), - growth_operating_income: value.growth_operating_income.into_inner(), - growth_income_before_tax: value.growth_income_before_tax.into_inner(), - growth_income_tax_expense: value.growth_income_tax_expense.into_inner(), - growth_net_income: value.growth_net_income.into_inner(), - growth_eps: value.growth_eps.into_inner(), - growth_eps_diluted: value.growth_eps_diluted.into_inner(), - growth_weighted_average_shs_out: value.growth_weighted_average_shs_out.into_inner(), + .map(|value| value.into_inner()), + growth_ebitda: value.growth_ebitda.map(|value| value.into_inner()), + growth_operating_income: value + .growth_operating_income + .map(|value| value.into_inner()), + growth_income_before_tax: value + .growth_income_before_tax + .map(|value| value.into_inner()), + growth_income_tax_expense: value + .growth_income_tax_expense + .map(|value| value.into_inner()), + growth_net_income: value.growth_net_income.map(|value| value.into_inner()), + growth_eps: value.growth_eps.map(|value| value.into_inner()), + growth_eps_diluted: value.growth_eps_diluted.map(|value| value.into_inner()), + growth_weighted_average_shs_out: value + .growth_weighted_average_shs_out + .map(|value| value.into_inner()), growth_weighted_average_shs_out_dil: value .growth_weighted_average_shs_out_dil - .into_inner(), - growth_ebit: value.growth_ebit.into_inner(), + .map(|value| value.into_inner()), + growth_ebit: value.growth_ebit.map(|value| value.into_inner()), growth_non_operating_income_excluding_interest: value .growth_non_operating_income_excluding_interest - .into_inner(), - growth_net_interest_income: value.growth_net_interest_income.into_inner(), + .map(|value| value.into_inner()), + growth_net_interest_income: value + .growth_net_interest_income + .map(|value| value.into_inner()), growth_total_other_income_expenses_net: value .growth_total_other_income_expenses_net - .into_inner(), + .map(|value| value.into_inner()), growth_net_income_from_continuing_operations: value .growth_net_income_from_continuing_operations - .into_inner(), + .map(|value| value.into_inner()), growth_other_adjustments_to_net_income: value .growth_other_adjustments_to_net_income - .into_inner(), - growth_net_income_deductions: value.growth_net_income_deductions.into_inner(), + .map(|value| value.into_inner()), + growth_net_income_deductions: value + .growth_net_income_deductions + .map(|value| value.into_inner()), } } } diff --git a/crates/fmp-py/tests/test_bulk_b.py b/crates/fmp-py/tests/test_bulk_b.py index 2cbc5730..374cf04a 100644 --- a/crates/fmp-py/tests/test_bulk_b.py +++ b/crates/fmp-py/tests/test_bulk_b.py @@ -32,7 +32,7 @@ def test_income_statements_encodes_year_then_period(client: Any, fixture_server: rows = client.bulk.income_statements(2026, "Q1") assert fixture_server.requests[0].target == "/income-statement-bulk?year=2026&period=Q1" - assert len(rows) == 2 + assert len(rows) == 3 row = rows[0] assert isinstance(row, BulkIncomeStatement) assert row.symbol == "000001.SZ" @@ -43,6 +43,7 @@ def test_income_statements_encodes_year_then_period(client: Any, fixture_server: assert row.revenue == "251641000000" assert row.net_income == "44508000000" assert row.eps == "2.15" + assert (rows[2].symbol, rows[2].eps, rows[2].weighted_average_shs_out_dil) == ("OASMY", None, None) def test_income_statement_growth_with_keyword_arguments(client: Any, fixture_server: FixtureServer) -> None: diff --git a/crates/libfmp/src/responses/bulk/income.rs b/crates/libfmp/src/responses/bulk/income.rs index 35992ec3..53fbbd30 100644 --- a/crates/libfmp/src/responses/bulk/income.rs +++ b/crates/libfmp/src/responses/bulk/income.rs @@ -21,37 +21,37 @@ pub struct BulkIncomeStatement { pub accepted_date: ApiDateTime, pub fiscal_year: FiscalYearString, pub period: FiscalPeriod, - pub revenue: NumericString, - pub cost_of_revenue: NumericString, - pub gross_profit: NumericString, - pub research_and_development_expenses: NumericString, - pub general_and_administrative_expenses: NumericString, - pub selling_and_marketing_expenses: NumericString, - pub selling_general_and_administrative_expenses: NumericString, - pub other_expenses: NumericString, - pub operating_expenses: NumericString, - pub cost_and_expenses: NumericString, - pub net_interest_income: NumericString, - pub interest_income: NumericString, - pub interest_expense: NumericString, - pub depreciation_and_amortization: NumericString, - pub ebitda: NumericString, - pub ebit: NumericString, - pub non_operating_income_excluding_interest: NumericString, - pub operating_income: NumericString, - pub total_other_income_expenses_net: NumericString, - pub income_before_tax: NumericString, - pub income_tax_expense: NumericString, - pub net_income_from_continuing_operations: NumericString, - pub net_income_from_discontinued_operations: NumericString, - pub other_adjustments_to_net_income: NumericString, - pub net_income: NumericString, - pub net_income_deductions: NumericString, - pub bottom_line_net_income: NumericString, - pub eps: NumericString, - pub eps_diluted: NumericString, - pub weighted_average_shs_out: NumericString, - pub weighted_average_shs_out_dil: NumericString, + pub revenue: Option, + pub cost_of_revenue: Option, + pub gross_profit: Option, + pub research_and_development_expenses: Option, + pub general_and_administrative_expenses: Option, + pub selling_and_marketing_expenses: Option, + pub selling_general_and_administrative_expenses: Option, + pub other_expenses: Option, + pub operating_expenses: Option, + pub cost_and_expenses: Option, + pub net_interest_income: Option, + pub interest_income: Option, + pub interest_expense: Option, + pub depreciation_and_amortization: Option, + pub ebitda: Option, + pub ebit: Option, + pub non_operating_income_excluding_interest: Option, + pub operating_income: Option, + pub total_other_income_expenses_net: Option, + pub income_before_tax: Option, + pub income_tax_expense: Option, + pub net_income_from_continuing_operations: Option, + pub net_income_from_discontinued_operations: Option, + pub other_adjustments_to_net_income: Option, + pub net_income: Option, + pub net_income_deductions: Option, + pub bottom_line_net_income: Option, + pub eps: Option, + pub eps_diluted: Option, + pub weighted_average_shs_out: Option, + pub weighted_average_shs_out_dil: Option, } /// One worldwide bulk income-statement-growth row. @@ -64,37 +64,37 @@ pub struct BulkIncomeStatementGrowth { pub fiscal_year: FiscalYearString, pub period: FiscalPeriod, pub reported_currency: CurrencyCode, - pub growth_revenue: NumericString, - pub growth_cost_of_revenue: NumericString, - pub growth_gross_profit: NumericString, - pub growth_gross_profit_ratio: NumericString, - pub growth_research_and_development_expenses: NumericString, - pub growth_general_and_administrative_expenses: NumericString, - pub growth_selling_and_marketing_expenses: NumericString, - pub growth_other_expenses: NumericString, - pub growth_operating_expenses: NumericString, - pub growth_cost_and_expenses: NumericString, - pub growth_interest_income: NumericString, - pub growth_interest_expense: NumericString, - pub growth_depreciation_and_amortization: NumericString, + pub growth_revenue: Option, + pub growth_cost_of_revenue: Option, + pub growth_gross_profit: Option, + pub growth_gross_profit_ratio: Option, + pub growth_research_and_development_expenses: Option, + pub growth_general_and_administrative_expenses: Option, + pub growth_selling_and_marketing_expenses: Option, + pub growth_other_expenses: Option, + pub growth_operating_expenses: Option, + pub growth_cost_and_expenses: Option, + pub growth_interest_income: Option, + pub growth_interest_expense: Option, + pub growth_depreciation_and_amortization: Option, #[serde(rename = "growthEBITDA")] - pub growth_ebitda: NumericString, - pub growth_operating_income: NumericString, - pub growth_income_before_tax: NumericString, - pub growth_income_tax_expense: NumericString, - pub growth_net_income: NumericString, + pub growth_ebitda: Option, + pub growth_operating_income: Option, + pub growth_income_before_tax: Option, + pub growth_income_tax_expense: Option, + pub growth_net_income: Option, #[serde(rename = "growthEPS")] - pub growth_eps: NumericString, + pub growth_eps: Option, #[serde(rename = "growthEPSDiluted")] - pub growth_eps_diluted: NumericString, - pub growth_weighted_average_shs_out: NumericString, - pub growth_weighted_average_shs_out_dil: NumericString, + pub growth_eps_diluted: Option, + pub growth_weighted_average_shs_out: Option, + pub growth_weighted_average_shs_out_dil: Option, #[serde(rename = "growthEBIT")] - pub growth_ebit: NumericString, - pub growth_non_operating_income_excluding_interest: NumericString, - pub growth_net_interest_income: NumericString, - pub growth_total_other_income_expenses_net: NumericString, - pub growth_net_income_from_continuing_operations: NumericString, - pub growth_other_adjustments_to_net_income: NumericString, - pub growth_net_income_deductions: NumericString, + pub growth_ebit: Option, + pub growth_non_operating_income_excluding_interest: Option, + pub growth_net_interest_income: Option, + pub growth_total_other_income_expenses_net: Option, + pub growth_net_income_from_continuing_operations: Option, + pub growth_other_adjustments_to_net_income: Option, + pub growth_net_income_deductions: Option, } diff --git a/crates/libfmp/tests/bulk_income_responses.rs b/crates/libfmp/tests/bulk_income_responses.rs index 6fb1d9b8..db015f44 100644 --- a/crates/libfmp/tests/bulk_income_responses.rs +++ b/crates/libfmp/tests/bulk_income_responses.rs @@ -10,8 +10,8 @@ use libfmp::{ }; use bulk_csv::{ - assert_cell_failure, assert_empty_bodies, assert_members, assert_round_trip, decode, first_row, - with_cells, + assert_cell_failure, assert_empty_bodies, assert_required_members, assert_round_trip, decode, + first_row, with_cells, }; const INCOME: &[u8] = include_bytes!("fixtures/bulk_income_statements.csv"); @@ -28,9 +28,26 @@ fn live_csv_fixtures_round_trip_every_cell_with_exact_field_counts() { } #[test] -fn every_column_is_required_and_no_member_accepts_an_empty_cell() { - assert_members(&bulk_income_statements(query()), INCOME, &[]); - assert_members(&bulk_income_statement_growth(query()), GROWTH, &[]); +fn every_column_is_required_and_only_identity_members_reject_an_empty_cell() { + assert_required_members( + &bulk_income_statements(query()), + INCOME, + &[ + "date", + "symbol", + "reportedCurrency", + "cik", + "filingDate", + "acceptedDate", + "fiscalYear", + "period", + ], + ); + assert_required_members( + &bulk_income_statement_growth(query()), + GROWTH, + &["symbol", "date", "fiscalYear", "period", "reportedCurrency"], + ); } #[test] @@ -51,7 +68,7 @@ fn identity_fields_are_narrow_and_preserve_provider_representations() { assert_eq!(income.accepted_date.to_string(), "2024-12-31 00:00:00"); assert_eq!(income.fiscal_year.as_str(), "2024"); assert_eq!(income.period, FiscalPeriod::FullYear); - assert_eq!(income.revenue.as_str(), "251641000000"); + assert_eq!(income.revenue.unwrap().as_str(), "251641000000"); for (member, cell) in [ ("date", "2024-12-31 00:00:00"), @@ -88,3 +105,14 @@ fn acronym_hazards_keep_exact_provider_wire_names() { assert!(!growth.contains_key(incorrect)); } } + +#[test] +fn live_empty_cells_decode_as_absent_metrics() { + let income = decode(&bulk_income_statements(query()), INCOME) + .unwrap() + .remove(2); + assert_eq!(income.symbol.as_str(), "OASMY"); + assert_eq!(income.eps, None); + assert_eq!(income.weighted_average_shs_out_dil, None); + assert_eq!(income.net_income.unwrap().as_str(), "-39754000"); +} diff --git a/crates/libfmp/tests/fixtures/bulk_income_statements.csv b/crates/libfmp/tests/fixtures/bulk_income_statements.csv index c4ea2d11..792ba89b 100644 --- a/crates/libfmp/tests/fixtures/bulk_income_statements.csv +++ b/crates/libfmp/tests/fixtures/bulk_income_statements.csv @@ -1,3 +1,4 @@ "date","symbol","reportedCurrency","cik","filingDate","acceptedDate","fiscalYear","period","revenue","costOfRevenue","grossProfit","researchAndDevelopmentExpenses","generalAndAdministrativeExpenses","sellingAndMarketingExpenses","sellingGeneralAndAdministrativeExpenses","otherExpenses","operatingExpenses","costAndExpenses","netInterestIncome","interestIncome","interestExpense","depreciationAndAmortization","ebitda","ebit","nonOperatingIncomeExcludingInterest","operatingIncome","totalOtherIncomeExpensesNet","incomeBeforeTax","incomeTaxExpense","netIncomeFromContinuingOperations","netIncomeFromDiscontinuedOperations","otherAdjustmentsToNetIncome","netIncome","netIncomeDeductions","bottomLineNetIncome","eps","epsDiluted","weightedAverageShsOut","weightedAverageShsOutDil" "2024-12-31","000001.SZ","CNY","0000000000","2024-12-31","2024-12-31 00:00:00","2024","FY",251641000000,104954000000,146687000000,0,18449000000,0,18449000000,73500000000,91949000000,196903000000,93427000000,198381000000,104954000000,4667000000,59405000000,54738000000,0,54738000000,0,54738000000,10230000000,44508000000,0,0,44508000000,0,44508000000,2.15,2.15,20701395000,20701395000 "2025-06-30","0002.KL","MYR","0000000000","2025-06-30","2025-06-30 00:00:00","2024","FY",230784000,177777000,53007000,0,0,0,0,1624000,1624000,179401000,2258000,2285000,27000,13605000,64988000,51383000,1649000,53032000,-27000,51356000,8881000,42475000,0,0,42475000,0,42475000,0.29,0.29,148314000,148314000 +"2024-12-31","OASMY","SEK","0001607245","2024-12-31","2024-12-31 00:00:00","2024","FY",0,0,0,0,0,0,37180000,4782000,41962000,41962000,0,3000,3000,3194000,-38768000,-41962000,0,-41962000,2208000,-39754000,0,-39754000,0,0,-39754000,0,-39754000,,,, diff --git a/sdk/go/bulk_models.go b/sdk/go/bulk_models.go index ec59cc82..ed747d0d 100644 --- a/sdk/go/bulk_models.go +++ b/sdk/go/bulk_models.go @@ -1216,37 +1216,37 @@ type BulkIncomeStatement struct { AcceptedDate DateTime `json:"acceptedDate"` FiscalYear string `json:"fiscalYear"` Period string `json:"period"` - Revenue string `json:"revenue"` - CostOfRevenue string `json:"costOfRevenue"` - GrossProfit string `json:"grossProfit"` - ResearchAndDevelopmentExpenses string `json:"researchAndDevelopmentExpenses"` - GeneralAndAdministrativeExpenses string `json:"generalAndAdministrativeExpenses"` - SellingAndMarketingExpenses string `json:"sellingAndMarketingExpenses"` - SellingGeneralAndAdministrativeExpenses string `json:"sellingGeneralAndAdministrativeExpenses"` - OtherExpenses string `json:"otherExpenses"` - OperatingExpenses string `json:"operatingExpenses"` - CostAndExpenses string `json:"costAndExpenses"` - NetInterestIncome string `json:"netInterestIncome"` - InterestIncome string `json:"interestIncome"` - InterestExpense string `json:"interestExpense"` - DepreciationAndAmortization string `json:"depreciationAndAmortization"` - Ebitda string `json:"ebitda"` - Ebit string `json:"ebit"` - NonOperatingIncomeExcludingInterest string `json:"nonOperatingIncomeExcludingInterest"` - OperatingIncome string `json:"operatingIncome"` - TotalOtherIncomeExpensesNet string `json:"totalOtherIncomeExpensesNet"` - IncomeBeforeTax string `json:"incomeBeforeTax"` - IncomeTaxExpense string `json:"incomeTaxExpense"` - NetIncomeFromContinuingOperations string `json:"netIncomeFromContinuingOperations"` - NetIncomeFromDiscontinuedOperations string `json:"netIncomeFromDiscontinuedOperations"` - OtherAdjustmentsToNetIncome string `json:"otherAdjustmentsToNetIncome"` - NetIncome string `json:"netIncome"` - NetIncomeDeductions string `json:"netIncomeDeductions"` - BottomLineNetIncome string `json:"bottomLineNetIncome"` - EPS string `json:"eps"` - EPSDiluted string `json:"epsDiluted"` - WeightedAverageShsOut string `json:"weightedAverageShsOut"` - WeightedAverageShsOutDil string `json:"weightedAverageShsOutDil"` + Revenue *string `json:"revenue"` + CostOfRevenue *string `json:"costOfRevenue"` + GrossProfit *string `json:"grossProfit"` + ResearchAndDevelopmentExpenses *string `json:"researchAndDevelopmentExpenses"` + GeneralAndAdministrativeExpenses *string `json:"generalAndAdministrativeExpenses"` + SellingAndMarketingExpenses *string `json:"sellingAndMarketingExpenses"` + SellingGeneralAndAdministrativeExpenses *string `json:"sellingGeneralAndAdministrativeExpenses"` + OtherExpenses *string `json:"otherExpenses"` + OperatingExpenses *string `json:"operatingExpenses"` + CostAndExpenses *string `json:"costAndExpenses"` + NetInterestIncome *string `json:"netInterestIncome"` + InterestIncome *string `json:"interestIncome"` + InterestExpense *string `json:"interestExpense"` + DepreciationAndAmortization *string `json:"depreciationAndAmortization"` + Ebitda *string `json:"ebitda"` + Ebit *string `json:"ebit"` + NonOperatingIncomeExcludingInterest *string `json:"nonOperatingIncomeExcludingInterest"` + OperatingIncome *string `json:"operatingIncome"` + TotalOtherIncomeExpensesNet *string `json:"totalOtherIncomeExpensesNet"` + IncomeBeforeTax *string `json:"incomeBeforeTax"` + IncomeTaxExpense *string `json:"incomeTaxExpense"` + NetIncomeFromContinuingOperations *string `json:"netIncomeFromContinuingOperations"` + NetIncomeFromDiscontinuedOperations *string `json:"netIncomeFromDiscontinuedOperations"` + OtherAdjustmentsToNetIncome *string `json:"otherAdjustmentsToNetIncome"` + NetIncome *string `json:"netIncome"` + NetIncomeDeductions *string `json:"netIncomeDeductions"` + BottomLineNetIncome *string `json:"bottomLineNetIncome"` + EPS *string `json:"eps"` + EPSDiluted *string `json:"epsDiluted"` + WeightedAverageShsOut *string `json:"weightedAverageShsOut"` + WeightedAverageShsOutDil *string `json:"weightedAverageShsOutDil"` } // bulkIncomeStatementShadow mirrors BulkIncomeStatement with a pointer or raw @@ -1319,68 +1319,6 @@ func (m *BulkIncomeStatement) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("BulkIncomeStatement", "fiscalYear") case shadow.Period == nil: return missingMemberError("BulkIncomeStatement", "period") - case shadow.Revenue == nil: - return missingMemberError("BulkIncomeStatement", "revenue") - case shadow.CostOfRevenue == nil: - return missingMemberError("BulkIncomeStatement", "costOfRevenue") - case shadow.GrossProfit == nil: - return missingMemberError("BulkIncomeStatement", "grossProfit") - case shadow.ResearchAndDevelopmentExpenses == nil: - return missingMemberError("BulkIncomeStatement", "researchAndDevelopmentExpenses") - case shadow.GeneralAndAdministrativeExpenses == nil: - return missingMemberError("BulkIncomeStatement", "generalAndAdministrativeExpenses") - case shadow.SellingAndMarketingExpenses == nil: - return missingMemberError("BulkIncomeStatement", "sellingAndMarketingExpenses") - case shadow.SellingGeneralAndAdministrativeExpenses == nil: - return missingMemberError("BulkIncomeStatement", "sellingGeneralAndAdministrativeExpenses") - case shadow.OtherExpenses == nil: - return missingMemberError("BulkIncomeStatement", "otherExpenses") - case shadow.OperatingExpenses == nil: - return missingMemberError("BulkIncomeStatement", "operatingExpenses") - case shadow.CostAndExpenses == nil: - return missingMemberError("BulkIncomeStatement", "costAndExpenses") - case shadow.NetInterestIncome == nil: - return missingMemberError("BulkIncomeStatement", "netInterestIncome") - case shadow.InterestIncome == nil: - return missingMemberError("BulkIncomeStatement", "interestIncome") - case shadow.InterestExpense == nil: - return missingMemberError("BulkIncomeStatement", "interestExpense") - case shadow.DepreciationAndAmortization == nil: - return missingMemberError("BulkIncomeStatement", "depreciationAndAmortization") - case shadow.Ebitda == nil: - return missingMemberError("BulkIncomeStatement", "ebitda") - case shadow.Ebit == nil: - return missingMemberError("BulkIncomeStatement", "ebit") - case shadow.NonOperatingIncomeExcludingInterest == nil: - return missingMemberError("BulkIncomeStatement", "nonOperatingIncomeExcludingInterest") - case shadow.OperatingIncome == nil: - return missingMemberError("BulkIncomeStatement", "operatingIncome") - case shadow.TotalOtherIncomeExpensesNet == nil: - return missingMemberError("BulkIncomeStatement", "totalOtherIncomeExpensesNet") - case shadow.IncomeBeforeTax == nil: - return missingMemberError("BulkIncomeStatement", "incomeBeforeTax") - case shadow.IncomeTaxExpense == nil: - return missingMemberError("BulkIncomeStatement", "incomeTaxExpense") - case shadow.NetIncomeFromContinuingOperations == nil: - return missingMemberError("BulkIncomeStatement", "netIncomeFromContinuingOperations") - case shadow.NetIncomeFromDiscontinuedOperations == nil: - return missingMemberError("BulkIncomeStatement", "netIncomeFromDiscontinuedOperations") - case shadow.OtherAdjustmentsToNetIncome == nil: - return missingMemberError("BulkIncomeStatement", "otherAdjustmentsToNetIncome") - case shadow.NetIncome == nil: - return missingMemberError("BulkIncomeStatement", "netIncome") - case shadow.NetIncomeDeductions == nil: - return missingMemberError("BulkIncomeStatement", "netIncomeDeductions") - case shadow.BottomLineNetIncome == nil: - return missingMemberError("BulkIncomeStatement", "bottomLineNetIncome") - case shadow.EPS == nil: - return missingMemberError("BulkIncomeStatement", "eps") - case shadow.EPSDiluted == nil: - return missingMemberError("BulkIncomeStatement", "epsDiluted") - case shadow.WeightedAverageShsOut == nil: - return missingMemberError("BulkIncomeStatement", "weightedAverageShsOut") - case shadow.WeightedAverageShsOutDil == nil: - return missingMemberError("BulkIncomeStatement", "weightedAverageShsOutDil") } *m = BulkIncomeStatement{ Date: *shadow.Date, @@ -1391,77 +1329,77 @@ func (m *BulkIncomeStatement) UnmarshalJSONFrom(dec *jsontext.Decoder) error { AcceptedDate: *shadow.AcceptedDate, FiscalYear: *shadow.FiscalYear, Period: *shadow.Period, - Revenue: *shadow.Revenue, - CostOfRevenue: *shadow.CostOfRevenue, - GrossProfit: *shadow.GrossProfit, - ResearchAndDevelopmentExpenses: *shadow.ResearchAndDevelopmentExpenses, - GeneralAndAdministrativeExpenses: *shadow.GeneralAndAdministrativeExpenses, - SellingAndMarketingExpenses: *shadow.SellingAndMarketingExpenses, - SellingGeneralAndAdministrativeExpenses: *shadow.SellingGeneralAndAdministrativeExpenses, - OtherExpenses: *shadow.OtherExpenses, - OperatingExpenses: *shadow.OperatingExpenses, - CostAndExpenses: *shadow.CostAndExpenses, - NetInterestIncome: *shadow.NetInterestIncome, - InterestIncome: *shadow.InterestIncome, - InterestExpense: *shadow.InterestExpense, - DepreciationAndAmortization: *shadow.DepreciationAndAmortization, - Ebitda: *shadow.Ebitda, - Ebit: *shadow.Ebit, - NonOperatingIncomeExcludingInterest: *shadow.NonOperatingIncomeExcludingInterest, - OperatingIncome: *shadow.OperatingIncome, - TotalOtherIncomeExpensesNet: *shadow.TotalOtherIncomeExpensesNet, - IncomeBeforeTax: *shadow.IncomeBeforeTax, - IncomeTaxExpense: *shadow.IncomeTaxExpense, - NetIncomeFromContinuingOperations: *shadow.NetIncomeFromContinuingOperations, - NetIncomeFromDiscontinuedOperations: *shadow.NetIncomeFromDiscontinuedOperations, - OtherAdjustmentsToNetIncome: *shadow.OtherAdjustmentsToNetIncome, - NetIncome: *shadow.NetIncome, - NetIncomeDeductions: *shadow.NetIncomeDeductions, - BottomLineNetIncome: *shadow.BottomLineNetIncome, - EPS: *shadow.EPS, - EPSDiluted: *shadow.EPSDiluted, - WeightedAverageShsOut: *shadow.WeightedAverageShsOut, - WeightedAverageShsOutDil: *shadow.WeightedAverageShsOutDil, + Revenue: shadow.Revenue, + CostOfRevenue: shadow.CostOfRevenue, + GrossProfit: shadow.GrossProfit, + ResearchAndDevelopmentExpenses: shadow.ResearchAndDevelopmentExpenses, + GeneralAndAdministrativeExpenses: shadow.GeneralAndAdministrativeExpenses, + SellingAndMarketingExpenses: shadow.SellingAndMarketingExpenses, + SellingGeneralAndAdministrativeExpenses: shadow.SellingGeneralAndAdministrativeExpenses, + OtherExpenses: shadow.OtherExpenses, + OperatingExpenses: shadow.OperatingExpenses, + CostAndExpenses: shadow.CostAndExpenses, + NetInterestIncome: shadow.NetInterestIncome, + InterestIncome: shadow.InterestIncome, + InterestExpense: shadow.InterestExpense, + DepreciationAndAmortization: shadow.DepreciationAndAmortization, + Ebitda: shadow.Ebitda, + Ebit: shadow.Ebit, + NonOperatingIncomeExcludingInterest: shadow.NonOperatingIncomeExcludingInterest, + OperatingIncome: shadow.OperatingIncome, + TotalOtherIncomeExpensesNet: shadow.TotalOtherIncomeExpensesNet, + IncomeBeforeTax: shadow.IncomeBeforeTax, + IncomeTaxExpense: shadow.IncomeTaxExpense, + NetIncomeFromContinuingOperations: shadow.NetIncomeFromContinuingOperations, + NetIncomeFromDiscontinuedOperations: shadow.NetIncomeFromDiscontinuedOperations, + OtherAdjustmentsToNetIncome: shadow.OtherAdjustmentsToNetIncome, + NetIncome: shadow.NetIncome, + NetIncomeDeductions: shadow.NetIncomeDeductions, + BottomLineNetIncome: shadow.BottomLineNetIncome, + EPS: shadow.EPS, + EPSDiluted: shadow.EPSDiluted, + WeightedAverageShsOut: shadow.WeightedAverageShsOut, + WeightedAverageShsOutDil: shadow.WeightedAverageShsOutDil, } return nil } // BulkIncomeStatementGrowth is one worldwide bulk income-statement-growth row. type BulkIncomeStatementGrowth struct { - Symbol string `json:"symbol"` - Date Date `json:"date"` - FiscalYear string `json:"fiscalYear"` - Period string `json:"period"` - ReportedCurrency string `json:"reportedCurrency"` - GrowthRevenue string `json:"growthRevenue"` - GrowthCostOfRevenue string `json:"growthCostOfRevenue"` - GrowthGrossProfit string `json:"growthGrossProfit"` - GrowthGrossProfitRatio string `json:"growthGrossProfitRatio"` - GrowthResearchAndDevelopmentExpenses string `json:"growthResearchAndDevelopmentExpenses"` - GrowthGeneralAndAdministrativeExpenses string `json:"growthGeneralAndAdministrativeExpenses"` - GrowthSellingAndMarketingExpenses string `json:"growthSellingAndMarketingExpenses"` - GrowthOtherExpenses string `json:"growthOtherExpenses"` - GrowthOperatingExpenses string `json:"growthOperatingExpenses"` - GrowthCostAndExpenses string `json:"growthCostAndExpenses"` - GrowthInterestIncome string `json:"growthInterestIncome"` - GrowthInterestExpense string `json:"growthInterestExpense"` - GrowthDepreciationAndAmortization string `json:"growthDepreciationAndAmortization"` - GrowthEbitda string `json:"growthEBITDA"` - GrowthOperatingIncome string `json:"growthOperatingIncome"` - GrowthIncomeBeforeTax string `json:"growthIncomeBeforeTax"` - GrowthIncomeTaxExpense string `json:"growthIncomeTaxExpense"` - GrowthNetIncome string `json:"growthNetIncome"` - GrowthEPS string `json:"growthEPS"` - GrowthEPSDiluted string `json:"growthEPSDiluted"` - GrowthWeightedAverageShsOut string `json:"growthWeightedAverageShsOut"` - GrowthWeightedAverageShsOutDil string `json:"growthWeightedAverageShsOutDil"` - GrowthEbit string `json:"growthEBIT"` - GrowthNonOperatingIncomeExcludingInterest string `json:"growthNonOperatingIncomeExcludingInterest"` - GrowthNetInterestIncome string `json:"growthNetInterestIncome"` - GrowthTotalOtherIncomeExpensesNet string `json:"growthTotalOtherIncomeExpensesNet"` - GrowthNetIncomeFromContinuingOperations string `json:"growthNetIncomeFromContinuingOperations"` - GrowthOtherAdjustmentsToNetIncome string `json:"growthOtherAdjustmentsToNetIncome"` - GrowthNetIncomeDeductions string `json:"growthNetIncomeDeductions"` + Symbol string `json:"symbol"` + Date Date `json:"date"` + FiscalYear string `json:"fiscalYear"` + Period string `json:"period"` + ReportedCurrency string `json:"reportedCurrency"` + GrowthRevenue *string `json:"growthRevenue"` + GrowthCostOfRevenue *string `json:"growthCostOfRevenue"` + GrowthGrossProfit *string `json:"growthGrossProfit"` + GrowthGrossProfitRatio *string `json:"growthGrossProfitRatio"` + GrowthResearchAndDevelopmentExpenses *string `json:"growthResearchAndDevelopmentExpenses"` + GrowthGeneralAndAdministrativeExpenses *string `json:"growthGeneralAndAdministrativeExpenses"` + GrowthSellingAndMarketingExpenses *string `json:"growthSellingAndMarketingExpenses"` + GrowthOtherExpenses *string `json:"growthOtherExpenses"` + GrowthOperatingExpenses *string `json:"growthOperatingExpenses"` + GrowthCostAndExpenses *string `json:"growthCostAndExpenses"` + GrowthInterestIncome *string `json:"growthInterestIncome"` + GrowthInterestExpense *string `json:"growthInterestExpense"` + GrowthDepreciationAndAmortization *string `json:"growthDepreciationAndAmortization"` + GrowthEbitda *string `json:"growthEBITDA"` + GrowthOperatingIncome *string `json:"growthOperatingIncome"` + GrowthIncomeBeforeTax *string `json:"growthIncomeBeforeTax"` + GrowthIncomeTaxExpense *string `json:"growthIncomeTaxExpense"` + GrowthNetIncome *string `json:"growthNetIncome"` + GrowthEPS *string `json:"growthEPS"` + GrowthEPSDiluted *string `json:"growthEPSDiluted"` + GrowthWeightedAverageShsOut *string `json:"growthWeightedAverageShsOut"` + GrowthWeightedAverageShsOutDil *string `json:"growthWeightedAverageShsOutDil"` + GrowthEbit *string `json:"growthEBIT"` + GrowthNonOperatingIncomeExcludingInterest *string `json:"growthNonOperatingIncomeExcludingInterest"` + GrowthNetInterestIncome *string `json:"growthNetInterestIncome"` + GrowthTotalOtherIncomeExpensesNet *string `json:"growthTotalOtherIncomeExpensesNet"` + GrowthNetIncomeFromContinuingOperations *string `json:"growthNetIncomeFromContinuingOperations"` + GrowthOtherAdjustmentsToNetIncome *string `json:"growthOtherAdjustmentsToNetIncome"` + GrowthNetIncomeDeductions *string `json:"growthNetIncomeDeductions"` } // bulkIncomeStatementGrowthShadow mirrors BulkIncomeStatementGrowth with a @@ -1523,64 +1461,6 @@ func (m *BulkIncomeStatementGrowth) UnmarshalJSONFrom(dec *jsontext.Decoder) err return missingMemberError("BulkIncomeStatementGrowth", "period") case shadow.ReportedCurrency == nil: return missingMemberError("BulkIncomeStatementGrowth", "reportedCurrency") - case shadow.GrowthRevenue == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthRevenue") - case shadow.GrowthCostOfRevenue == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthCostOfRevenue") - case shadow.GrowthGrossProfit == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthGrossProfit") - case shadow.GrowthGrossProfitRatio == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthGrossProfitRatio") - case shadow.GrowthResearchAndDevelopmentExpenses == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthResearchAndDevelopmentExpenses") - case shadow.GrowthGeneralAndAdministrativeExpenses == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthGeneralAndAdministrativeExpenses") - case shadow.GrowthSellingAndMarketingExpenses == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthSellingAndMarketingExpenses") - case shadow.GrowthOtherExpenses == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthOtherExpenses") - case shadow.GrowthOperatingExpenses == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthOperatingExpenses") - case shadow.GrowthCostAndExpenses == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthCostAndExpenses") - case shadow.GrowthInterestIncome == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthInterestIncome") - case shadow.GrowthInterestExpense == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthInterestExpense") - case shadow.GrowthDepreciationAndAmortization == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthDepreciationAndAmortization") - case shadow.GrowthEbitda == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthEBITDA") - case shadow.GrowthOperatingIncome == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthOperatingIncome") - case shadow.GrowthIncomeBeforeTax == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthIncomeBeforeTax") - case shadow.GrowthIncomeTaxExpense == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthIncomeTaxExpense") - case shadow.GrowthNetIncome == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthNetIncome") - case shadow.GrowthEPS == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthEPS") - case shadow.GrowthEPSDiluted == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthEPSDiluted") - case shadow.GrowthWeightedAverageShsOut == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthWeightedAverageShsOut") - case shadow.GrowthWeightedAverageShsOutDil == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthWeightedAverageShsOutDil") - case shadow.GrowthEbit == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthEBIT") - case shadow.GrowthNonOperatingIncomeExcludingInterest == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthNonOperatingIncomeExcludingInterest") - case shadow.GrowthNetInterestIncome == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthNetInterestIncome") - case shadow.GrowthTotalOtherIncomeExpensesNet == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthTotalOtherIncomeExpensesNet") - case shadow.GrowthNetIncomeFromContinuingOperations == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthNetIncomeFromContinuingOperations") - case shadow.GrowthOtherAdjustmentsToNetIncome == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthOtherAdjustmentsToNetIncome") - case shadow.GrowthNetIncomeDeductions == nil: - return missingMemberError("BulkIncomeStatementGrowth", "growthNetIncomeDeductions") } *m = BulkIncomeStatementGrowth{ Symbol: *shadow.Symbol, @@ -1588,35 +1468,35 @@ func (m *BulkIncomeStatementGrowth) UnmarshalJSONFrom(dec *jsontext.Decoder) err FiscalYear: *shadow.FiscalYear, Period: *shadow.Period, ReportedCurrency: *shadow.ReportedCurrency, - GrowthRevenue: *shadow.GrowthRevenue, - GrowthCostOfRevenue: *shadow.GrowthCostOfRevenue, - GrowthGrossProfit: *shadow.GrowthGrossProfit, - GrowthGrossProfitRatio: *shadow.GrowthGrossProfitRatio, - GrowthResearchAndDevelopmentExpenses: *shadow.GrowthResearchAndDevelopmentExpenses, - GrowthGeneralAndAdministrativeExpenses: *shadow.GrowthGeneralAndAdministrativeExpenses, - GrowthSellingAndMarketingExpenses: *shadow.GrowthSellingAndMarketingExpenses, - GrowthOtherExpenses: *shadow.GrowthOtherExpenses, - GrowthOperatingExpenses: *shadow.GrowthOperatingExpenses, - GrowthCostAndExpenses: *shadow.GrowthCostAndExpenses, - GrowthInterestIncome: *shadow.GrowthInterestIncome, - GrowthInterestExpense: *shadow.GrowthInterestExpense, - GrowthDepreciationAndAmortization: *shadow.GrowthDepreciationAndAmortization, - GrowthEbitda: *shadow.GrowthEbitda, - GrowthOperatingIncome: *shadow.GrowthOperatingIncome, - GrowthIncomeBeforeTax: *shadow.GrowthIncomeBeforeTax, - GrowthIncomeTaxExpense: *shadow.GrowthIncomeTaxExpense, - GrowthNetIncome: *shadow.GrowthNetIncome, - GrowthEPS: *shadow.GrowthEPS, - GrowthEPSDiluted: *shadow.GrowthEPSDiluted, - GrowthWeightedAverageShsOut: *shadow.GrowthWeightedAverageShsOut, - GrowthWeightedAverageShsOutDil: *shadow.GrowthWeightedAverageShsOutDil, - GrowthEbit: *shadow.GrowthEbit, - GrowthNonOperatingIncomeExcludingInterest: *shadow.GrowthNonOperatingIncomeExcludingInterest, - GrowthNetInterestIncome: *shadow.GrowthNetInterestIncome, - GrowthTotalOtherIncomeExpensesNet: *shadow.GrowthTotalOtherIncomeExpensesNet, - GrowthNetIncomeFromContinuingOperations: *shadow.GrowthNetIncomeFromContinuingOperations, - GrowthOtherAdjustmentsToNetIncome: *shadow.GrowthOtherAdjustmentsToNetIncome, - GrowthNetIncomeDeductions: *shadow.GrowthNetIncomeDeductions, + GrowthRevenue: shadow.GrowthRevenue, + GrowthCostOfRevenue: shadow.GrowthCostOfRevenue, + GrowthGrossProfit: shadow.GrowthGrossProfit, + GrowthGrossProfitRatio: shadow.GrowthGrossProfitRatio, + GrowthResearchAndDevelopmentExpenses: shadow.GrowthResearchAndDevelopmentExpenses, + GrowthGeneralAndAdministrativeExpenses: shadow.GrowthGeneralAndAdministrativeExpenses, + GrowthSellingAndMarketingExpenses: shadow.GrowthSellingAndMarketingExpenses, + GrowthOtherExpenses: shadow.GrowthOtherExpenses, + GrowthOperatingExpenses: shadow.GrowthOperatingExpenses, + GrowthCostAndExpenses: shadow.GrowthCostAndExpenses, + GrowthInterestIncome: shadow.GrowthInterestIncome, + GrowthInterestExpense: shadow.GrowthInterestExpense, + GrowthDepreciationAndAmortization: shadow.GrowthDepreciationAndAmortization, + GrowthEbitda: shadow.GrowthEbitda, + GrowthOperatingIncome: shadow.GrowthOperatingIncome, + GrowthIncomeBeforeTax: shadow.GrowthIncomeBeforeTax, + GrowthIncomeTaxExpense: shadow.GrowthIncomeTaxExpense, + GrowthNetIncome: shadow.GrowthNetIncome, + GrowthEPS: shadow.GrowthEPS, + GrowthEPSDiluted: shadow.GrowthEPSDiluted, + GrowthWeightedAverageShsOut: shadow.GrowthWeightedAverageShsOut, + GrowthWeightedAverageShsOutDil: shadow.GrowthWeightedAverageShsOutDil, + GrowthEbit: shadow.GrowthEbit, + GrowthNonOperatingIncomeExcludingInterest: shadow.GrowthNonOperatingIncomeExcludingInterest, + GrowthNetInterestIncome: shadow.GrowthNetInterestIncome, + GrowthTotalOtherIncomeExpensesNet: shadow.GrowthTotalOtherIncomeExpensesNet, + GrowthNetIncomeFromContinuingOperations: shadow.GrowthNetIncomeFromContinuingOperations, + GrowthOtherAdjustmentsToNetIncome: shadow.GrowthOtherAdjustmentsToNetIncome, + GrowthNetIncomeDeductions: shadow.GrowthNetIncomeDeductions, } return nil } diff --git a/sdk/go/bulk_parity_test.go b/sdk/go/bulk_parity_test.go index df4ea246..5f7a29b7 100644 --- a/sdk/go/bulk_parity_test.go +++ b/sdk/go/bulk_parity_test.go @@ -145,9 +145,12 @@ func TestBulkStatementsDecodeExactValuesAndKeepProviderTypos(t *testing.T) { if income[0].Symbol != "000001.SZ" || income[0].ReportedCurrency != "CNY" || income[0].CIK != "0000000000" || income[0].Date != mustParseDate(t, "2024-12-31") || income[0].AcceptedDate != mustParseDateTime(t, "2024-12-31 00:00:00") || income[0].FiscalYear != "2024" || - income[0].Period != "FY" || income[0].Revenue != "251641000000" { + income[0].Period != "FY" || cellText(income[0].Revenue) != "251641000000" { t.Fatalf("bulk_income_statements = %+v", income[0]) } + if income[2].Symbol != "OASMY" || income[2].EPS != nil || cellText(income[2].NetIncome) != "-39754000" { + t.Fatalf("empty income cells = %+v", income[2]) + } incomeGrowth := assertCSVFixtureParity[BulkIncomeStatementGrowth](t, "bulk_income_statement_growth.csv", 34) if encoded, err := json.Marshal(incomeGrowth[0]); err != nil || !strings.Contains(string(encoded), `"growthEBITDA":`) || !strings.Contains(string(encoded), `"growthEPSDiluted":`) || !strings.Contains(string(encoded), `"growthEBIT":`) { diff --git a/sdk/go/bulk_test.go b/sdk/go/bulk_test.go index d4ec7f95..3301f223 100644 --- a/sdk/go/bulk_test.go +++ b/sdk/go/bulk_test.go @@ -102,11 +102,11 @@ func TestBulkMethodsUseExactPathsAndWireParameterOrder(t *testing.T) { t.Fatalf("EarningsSurprises = %+v, %v", surprises, err) } income, err := client.Bulk.IncomeStatements(ctx, NewBulkStatementQuery(2026, FiscalPeriodQ1)) - if err != nil || len(income) != 2 || income[0].Revenue != "251641000000" { + if err != nil || len(income) != 3 || cellText(income[0].Revenue) != "251641000000" { t.Fatalf("IncomeStatements = %+v, %v", income, err) } incomeGrowth, err := client.Bulk.IncomeStatementGrowth(ctx, NewBulkStatementQuery(2026, FiscalPeriodQ2)) - if err != nil || len(incomeGrowth) != 2 || incomeGrowth[0].GrowthRevenue != "-0.08220846812871789" { + if err != nil || len(incomeGrowth) != 2 || cellText(incomeGrowth[0].GrowthRevenue) != "-0.08220846812871789" { t.Fatalf("IncomeStatementGrowth = %+v, %v", incomeGrowth, err) } balance, err := client.Bulk.BalanceSheetStatements(ctx, NewBulkStatementQuery(2026, FiscalPeriodQ3)) From d21e08823e03ad046f9b54d5fe4a29bc376692b6 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 11:21:49 +0800 Subject: [PATCH 25/31] fix(bulk)!: make balance-sheet numeric members optional Under ADR 0035, an empty cell is the only absence marker in bulk CSV and sparse empties vary by period, so every numeric metric member of a bulk row must be optional while identity members stay required. This covers the balance-sheet rows: every NumericString member of BulkBalanceSheetStatement and BulkBalanceSheetStatementGrowth becomes Option, with identity members left required. The member-contract test now blanks each column of the live row to check that only identity members reject an empty cell. Regenerated Python models/stubs (Optional[str]) and Go models (*string) are included. BREAKING CHANGE: every numeric member of BulkBalanceSheetStatement and BulkBalanceSheetStatementGrowth is now optional in Rust, Python and Go. --- .../fmp/_native/bulk/balance/__init__.pyi | 436 +++++------ crates/fmp-py/src/models/bulk/balance.rs | 700 ++++++++++-------- crates/libfmp/src/responses/bulk/balance.rs | 208 +++--- crates/libfmp/tests/bulk_balance_responses.rs | 32 +- sdk/go/bulk_models.go | 634 ++++++---------- sdk/go/bulk_parity_test.go | 2 +- sdk/go/bulk_test.go | 2 +- 7 files changed, 962 insertions(+), 1052 deletions(-) diff --git a/crates/fmp-py/python/fmp/_native/bulk/balance/__init__.pyi b/crates/fmp-py/python/fmp/_native/bulk/balance/__init__.pyi index 9a043edb..7a18fdf1 100644 --- a/crates/fmp-py/python/fmp/_native/bulk/balance/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/bulk/balance/__init__.pyi @@ -92,111 +92,115 @@ class BulkBalanceSheetStatement: @property def fiscal_year(self) -> builtins.str: ... @property - def cash_and_cash_equivalents(self) -> builtins.str: ... + def cash_and_cash_equivalents(self) -> typing.Optional[builtins.str]: ... @property - def short_term_investments(self) -> builtins.str: ... + def short_term_investments(self) -> typing.Optional[builtins.str]: ... @property - def cash_and_short_term_investments(self) -> builtins.str: ... + def cash_and_short_term_investments(self) -> typing.Optional[builtins.str]: ... @property - def net_receivables(self) -> builtins.str: ... + def net_receivables(self) -> typing.Optional[builtins.str]: ... @property - def accounts_receivables(self) -> builtins.str: ... + def accounts_receivables(self) -> typing.Optional[builtins.str]: ... @property - def other_receivables(self) -> builtins.str: ... + def other_receivables(self) -> typing.Optional[builtins.str]: ... @property - def inventory(self) -> builtins.str: ... + def inventory(self) -> typing.Optional[builtins.str]: ... @property - def prepaids(self) -> builtins.str: ... + def prepaids(self) -> typing.Optional[builtins.str]: ... @property - def other_current_assets(self) -> builtins.str: ... + def other_current_assets(self) -> typing.Optional[builtins.str]: ... @property - def total_current_assets(self) -> builtins.str: ... + def total_current_assets(self) -> typing.Optional[builtins.str]: ... @property - def property_plant_equipment_net(self) -> builtins.str: ... + def property_plant_equipment_net(self) -> typing.Optional[builtins.str]: ... @property - def goodwill(self) -> builtins.str: ... + def goodwill(self) -> typing.Optional[builtins.str]: ... @property - def intangible_assets(self) -> builtins.str: ... + def intangible_assets(self) -> typing.Optional[builtins.str]: ... @property - def goodwill_and_intangible_assets(self) -> builtins.str: ... + def goodwill_and_intangible_assets(self) -> typing.Optional[builtins.str]: ... @property - def long_term_investments(self) -> builtins.str: ... + def long_term_investments(self) -> typing.Optional[builtins.str]: ... @property - def tax_assets(self) -> builtins.str: ... + def tax_assets(self) -> typing.Optional[builtins.str]: ... @property - def other_non_current_assets(self) -> builtins.str: ... + def other_non_current_assets(self) -> typing.Optional[builtins.str]: ... @property - def total_non_current_assets(self) -> builtins.str: ... + def total_non_current_assets(self) -> typing.Optional[builtins.str]: ... @property - def other_assets(self) -> builtins.str: ... + def other_assets(self) -> typing.Optional[builtins.str]: ... @property - def total_assets(self) -> builtins.str: ... + def total_assets(self) -> typing.Optional[builtins.str]: ... @property - def total_payables(self) -> builtins.str: ... + def total_payables(self) -> typing.Optional[builtins.str]: ... @property - def account_payables(self) -> builtins.str: ... + def account_payables(self) -> typing.Optional[builtins.str]: ... @property - def other_payables(self) -> builtins.str: ... + def other_payables(self) -> typing.Optional[builtins.str]: ... @property - def accrued_expenses(self) -> builtins.str: ... + def accrued_expenses(self) -> typing.Optional[builtins.str]: ... @property - def short_term_debt(self) -> builtins.str: ... + def short_term_debt(self) -> typing.Optional[builtins.str]: ... @property - def capital_lease_obligations_current(self) -> builtins.str: ... + def capital_lease_obligations_current(self) -> typing.Optional[builtins.str]: ... @property - def tax_payables(self) -> builtins.str: ... + def tax_payables(self) -> typing.Optional[builtins.str]: ... @property - def deferred_revenue(self) -> builtins.str: ... + def deferred_revenue(self) -> typing.Optional[builtins.str]: ... @property - def other_current_liabilities(self) -> builtins.str: ... + def other_current_liabilities(self) -> typing.Optional[builtins.str]: ... @property - def total_current_liabilities(self) -> builtins.str: ... + def total_current_liabilities(self) -> typing.Optional[builtins.str]: ... @property - def long_term_debt(self) -> builtins.str: ... + def long_term_debt(self) -> typing.Optional[builtins.str]: ... @property - def capital_lease_obligations_non_current(self) -> builtins.str: ... + def capital_lease_obligations_non_current( + self, + ) -> typing.Optional[builtins.str]: ... @property - def deferred_revenue_non_current(self) -> builtins.str: ... + def deferred_revenue_non_current(self) -> typing.Optional[builtins.str]: ... @property - def deferred_tax_liabilities_non_current(self) -> builtins.str: ... + def deferred_tax_liabilities_non_current(self) -> typing.Optional[builtins.str]: ... @property - def other_non_current_liabilities(self) -> builtins.str: ... + def other_non_current_liabilities(self) -> typing.Optional[builtins.str]: ... @property - def total_non_current_liabilities(self) -> builtins.str: ... + def total_non_current_liabilities(self) -> typing.Optional[builtins.str]: ... @property - def other_liabilities(self) -> builtins.str: ... + def other_liabilities(self) -> typing.Optional[builtins.str]: ... @property - def capital_lease_obligations(self) -> builtins.str: ... + def capital_lease_obligations(self) -> typing.Optional[builtins.str]: ... @property - def total_liabilities(self) -> builtins.str: ... + def total_liabilities(self) -> typing.Optional[builtins.str]: ... @property - def treasury_stock(self) -> builtins.str: ... + def treasury_stock(self) -> typing.Optional[builtins.str]: ... @property - def preferred_stock(self) -> builtins.str: ... + def preferred_stock(self) -> typing.Optional[builtins.str]: ... @property - def common_stock(self) -> builtins.str: ... + def common_stock(self) -> typing.Optional[builtins.str]: ... @property - def retained_earnings(self) -> builtins.str: ... + def retained_earnings(self) -> typing.Optional[builtins.str]: ... @property - def additional_paid_in_capital(self) -> builtins.str: ... + def additional_paid_in_capital(self) -> typing.Optional[builtins.str]: ... @property - def accumulated_other_comprehensive_income_loss(self) -> builtins.str: ... + def accumulated_other_comprehensive_income_loss( + self, + ) -> typing.Optional[builtins.str]: ... @property - def other_total_stockholders_equity(self) -> builtins.str: ... + def other_total_stockholders_equity(self) -> typing.Optional[builtins.str]: ... @property - def total_stockholders_equity(self) -> builtins.str: ... + def total_stockholders_equity(self) -> typing.Optional[builtins.str]: ... @property - def total_equity(self) -> builtins.str: ... + def total_equity(self) -> typing.Optional[builtins.str]: ... @property - def minority_interest(self) -> builtins.str: ... + def minority_interest(self) -> typing.Optional[builtins.str]: ... @property - def total_liabilities_and_total_equity(self) -> builtins.str: ... + def total_liabilities_and_total_equity(self) -> typing.Optional[builtins.str]: ... @property - def total_investments(self) -> builtins.str: ... + def total_investments(self) -> typing.Optional[builtins.str]: ... @property - def total_debt(self) -> builtins.str: ... + def total_debt(self) -> typing.Optional[builtins.str]: ... @property - def net_debt(self) -> builtins.str: ... + def net_debt(self) -> typing.Optional[builtins.str]: ... @property def period(self) -> typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... @@ -211,59 +215,59 @@ class BulkBalanceSheetStatement: accepted_date: datetime.datetime, fiscal_year: builtins.str, period: typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"], - cash_and_cash_equivalents: builtins.str, - short_term_investments: builtins.str, - cash_and_short_term_investments: builtins.str, - net_receivables: builtins.str, - accounts_receivables: builtins.str, - other_receivables: builtins.str, - inventory: builtins.str, - prepaids: builtins.str, - other_current_assets: builtins.str, - total_current_assets: builtins.str, - property_plant_equipment_net: builtins.str, - goodwill: builtins.str, - intangible_assets: builtins.str, - goodwill_and_intangible_assets: builtins.str, - long_term_investments: builtins.str, - tax_assets: builtins.str, - other_non_current_assets: builtins.str, - total_non_current_assets: builtins.str, - other_assets: builtins.str, - total_assets: builtins.str, - total_payables: builtins.str, - account_payables: builtins.str, - other_payables: builtins.str, - accrued_expenses: builtins.str, - short_term_debt: builtins.str, - capital_lease_obligations_current: builtins.str, - tax_payables: builtins.str, - deferred_revenue: builtins.str, - other_current_liabilities: builtins.str, - total_current_liabilities: builtins.str, - long_term_debt: builtins.str, - capital_lease_obligations_non_current: builtins.str, - deferred_revenue_non_current: builtins.str, - deferred_tax_liabilities_non_current: builtins.str, - other_non_current_liabilities: builtins.str, - total_non_current_liabilities: builtins.str, - other_liabilities: builtins.str, - capital_lease_obligations: builtins.str, - total_liabilities: builtins.str, - treasury_stock: builtins.str, - preferred_stock: builtins.str, - common_stock: builtins.str, - retained_earnings: builtins.str, - additional_paid_in_capital: builtins.str, - accumulated_other_comprehensive_income_loss: builtins.str, - other_total_stockholders_equity: builtins.str, - total_stockholders_equity: builtins.str, - total_equity: builtins.str, - minority_interest: builtins.str, - total_liabilities_and_total_equity: builtins.str, - total_investments: builtins.str, - total_debt: builtins.str, - net_debt: builtins.str, + cash_and_cash_equivalents: typing.Optional[builtins.str], + short_term_investments: typing.Optional[builtins.str], + cash_and_short_term_investments: typing.Optional[builtins.str], + net_receivables: typing.Optional[builtins.str], + accounts_receivables: typing.Optional[builtins.str], + other_receivables: typing.Optional[builtins.str], + inventory: typing.Optional[builtins.str], + prepaids: typing.Optional[builtins.str], + other_current_assets: typing.Optional[builtins.str], + total_current_assets: typing.Optional[builtins.str], + property_plant_equipment_net: typing.Optional[builtins.str], + goodwill: typing.Optional[builtins.str], + intangible_assets: typing.Optional[builtins.str], + goodwill_and_intangible_assets: typing.Optional[builtins.str], + long_term_investments: typing.Optional[builtins.str], + tax_assets: typing.Optional[builtins.str], + other_non_current_assets: typing.Optional[builtins.str], + total_non_current_assets: typing.Optional[builtins.str], + other_assets: typing.Optional[builtins.str], + total_assets: typing.Optional[builtins.str], + total_payables: typing.Optional[builtins.str], + account_payables: typing.Optional[builtins.str], + other_payables: typing.Optional[builtins.str], + accrued_expenses: typing.Optional[builtins.str], + short_term_debt: typing.Optional[builtins.str], + capital_lease_obligations_current: typing.Optional[builtins.str], + tax_payables: typing.Optional[builtins.str], + deferred_revenue: typing.Optional[builtins.str], + other_current_liabilities: typing.Optional[builtins.str], + total_current_liabilities: typing.Optional[builtins.str], + long_term_debt: typing.Optional[builtins.str], + capital_lease_obligations_non_current: typing.Optional[builtins.str], + deferred_revenue_non_current: typing.Optional[builtins.str], + deferred_tax_liabilities_non_current: typing.Optional[builtins.str], + other_non_current_liabilities: typing.Optional[builtins.str], + total_non_current_liabilities: typing.Optional[builtins.str], + other_liabilities: typing.Optional[builtins.str], + capital_lease_obligations: typing.Optional[builtins.str], + total_liabilities: typing.Optional[builtins.str], + treasury_stock: typing.Optional[builtins.str], + preferred_stock: typing.Optional[builtins.str], + common_stock: typing.Optional[builtins.str], + retained_earnings: typing.Optional[builtins.str], + additional_paid_in_capital: typing.Optional[builtins.str], + accumulated_other_comprehensive_income_loss: typing.Optional[builtins.str], + other_total_stockholders_equity: typing.Optional[builtins.str], + total_stockholders_equity: typing.Optional[builtins.str], + total_equity: typing.Optional[builtins.str], + minority_interest: typing.Optional[builtins.str], + total_liabilities_and_total_equity: typing.Optional[builtins.str], + total_investments: typing.Optional[builtins.str], + total_debt: typing.Optional[builtins.str], + net_debt: typing.Optional[builtins.str], ) -> BulkBalanceSheetStatement: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... @@ -345,107 +349,121 @@ class BulkBalanceSheetStatementGrowth: @property def reported_currency(self) -> builtins.str: ... @property - def growth_cash_and_cash_equivalents(self) -> builtins.str: ... + def growth_cash_and_cash_equivalents(self) -> typing.Optional[builtins.str]: ... @property - def growth_short_term_investments(self) -> builtins.str: ... + def growth_short_term_investments(self) -> typing.Optional[builtins.str]: ... @property - def growth_cash_and_short_term_investments(self) -> builtins.str: ... + def growth_cash_and_short_term_investments( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_net_receivables(self) -> builtins.str: ... + def growth_net_receivables(self) -> typing.Optional[builtins.str]: ... @property - def growth_inventory(self) -> builtins.str: ... + def growth_inventory(self) -> typing.Optional[builtins.str]: ... @property - def growth_other_current_assets(self) -> builtins.str: ... + def growth_other_current_assets(self) -> typing.Optional[builtins.str]: ... @property - def growth_total_current_assets(self) -> builtins.str: ... + def growth_total_current_assets(self) -> typing.Optional[builtins.str]: ... @property - def growth_property_plant_equipment_net(self) -> builtins.str: ... + def growth_property_plant_equipment_net(self) -> typing.Optional[builtins.str]: ... @property - def growth_goodwill(self) -> builtins.str: ... + def growth_goodwill(self) -> typing.Optional[builtins.str]: ... @property - def growth_intangible_assets(self) -> builtins.str: ... + def growth_intangible_assets(self) -> typing.Optional[builtins.str]: ... @property - def growth_goodwill_and_intangible_assets(self) -> builtins.str: ... + def growth_goodwill_and_intangible_assets( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_long_term_investments(self) -> builtins.str: ... + def growth_long_term_investments(self) -> typing.Optional[builtins.str]: ... @property - def growth_tax_assets(self) -> builtins.str: ... + def growth_tax_assets(self) -> typing.Optional[builtins.str]: ... @property - def growth_other_non_current_assets(self) -> builtins.str: ... + def growth_other_non_current_assets(self) -> typing.Optional[builtins.str]: ... @property - def growth_total_non_current_assets(self) -> builtins.str: ... + def growth_total_non_current_assets(self) -> typing.Optional[builtins.str]: ... @property - def growth_other_assets(self) -> builtins.str: ... + def growth_other_assets(self) -> typing.Optional[builtins.str]: ... @property - def growth_total_assets(self) -> builtins.str: ... + def growth_total_assets(self) -> typing.Optional[builtins.str]: ... @property - def growth_account_payables(self) -> builtins.str: ... + def growth_account_payables(self) -> typing.Optional[builtins.str]: ... @property - def growth_short_term_debt(self) -> builtins.str: ... + def growth_short_term_debt(self) -> typing.Optional[builtins.str]: ... @property - def growth_tax_payables(self) -> builtins.str: ... + def growth_tax_payables(self) -> typing.Optional[builtins.str]: ... @property - def growth_deferred_revenue(self) -> builtins.str: ... + def growth_deferred_revenue(self) -> typing.Optional[builtins.str]: ... @property - def growth_other_current_liabilities(self) -> builtins.str: ... + def growth_other_current_liabilities(self) -> typing.Optional[builtins.str]: ... @property - def growth_total_current_liabilities(self) -> builtins.str: ... + def growth_total_current_liabilities(self) -> typing.Optional[builtins.str]: ... @property - def growth_long_term_debt(self) -> builtins.str: ... + def growth_long_term_debt(self) -> typing.Optional[builtins.str]: ... @property - def growth_deferred_revenue_non_current(self) -> builtins.str: ... + def growth_deferred_revenue_non_current(self) -> typing.Optional[builtins.str]: ... @property - def growth_deferred_tax_liabilities_non_current(self) -> builtins.str: ... + def growth_deferred_tax_liabilities_non_current( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_other_non_current_liabilities(self) -> builtins.str: ... + def growth_other_non_current_liabilities(self) -> typing.Optional[builtins.str]: ... @property - def growth_total_non_current_liabilities(self) -> builtins.str: ... + def growth_total_non_current_liabilities(self) -> typing.Optional[builtins.str]: ... @property - def growth_other_liabilities(self) -> builtins.str: ... + def growth_other_liabilities(self) -> typing.Optional[builtins.str]: ... @property - def growth_total_liabilities(self) -> builtins.str: ... + def growth_total_liabilities(self) -> typing.Optional[builtins.str]: ... @property - def growth_preferred_stock(self) -> builtins.str: ... + def growth_preferred_stock(self) -> typing.Optional[builtins.str]: ... @property - def growth_common_stock(self) -> builtins.str: ... + def growth_common_stock(self) -> typing.Optional[builtins.str]: ... @property - def growth_retained_earnings(self) -> builtins.str: ... + def growth_retained_earnings(self) -> typing.Optional[builtins.str]: ... @property - def growth_accumulated_other_comprehensive_income_loss(self) -> builtins.str: ... + def growth_accumulated_other_comprehensive_income_loss( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_other_total_stockholders_equity(self) -> builtins.str: ... + def growth_other_total_stockholders_equity( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_total_stockholders_equity(self) -> builtins.str: ... + def growth_total_stockholders_equity(self) -> typing.Optional[builtins.str]: ... @property - def growth_minority_interest(self) -> builtins.str: ... + def growth_minority_interest(self) -> typing.Optional[builtins.str]: ... @property - def growth_total_equity(self) -> builtins.str: ... + def growth_total_equity(self) -> typing.Optional[builtins.str]: ... @property - def growth_total_liabilities_and_stockholders_equity(self) -> builtins.str: ... + def growth_total_liabilities_and_stockholders_equity( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_total_investments(self) -> builtins.str: ... + def growth_total_investments(self) -> typing.Optional[builtins.str]: ... @property - def growth_total_debt(self) -> builtins.str: ... + def growth_total_debt(self) -> typing.Optional[builtins.str]: ... @property - def growth_net_debt(self) -> builtins.str: ... + def growth_net_debt(self) -> typing.Optional[builtins.str]: ... @property - def growth_accounts_receivables(self) -> builtins.str: ... + def growth_accounts_receivables(self) -> typing.Optional[builtins.str]: ... @property - def growth_other_receivables(self) -> builtins.str: ... + def growth_other_receivables(self) -> typing.Optional[builtins.str]: ... @property - def growth_prepaids(self) -> builtins.str: ... + def growth_prepaids(self) -> typing.Optional[builtins.str]: ... @property - def growth_total_payables(self) -> builtins.str: ... + def growth_total_payables(self) -> typing.Optional[builtins.str]: ... @property - def growth_other_payables(self) -> builtins.str: ... + def growth_other_payables(self) -> typing.Optional[builtins.str]: ... @property - def growth_accrued_expenses(self) -> builtins.str: ... + def growth_accrued_expenses(self) -> typing.Optional[builtins.str]: ... @property - def growth_capital_lease_obligations_current(self) -> builtins.str: ... + def growth_capital_lease_obligations_current( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_additional_paid_in_capital(self) -> builtins.str: ... + def growth_additional_paid_in_capital(self) -> typing.Optional[builtins.str]: ... @property - def growth_treasury_stock(self) -> builtins.str: ... + def growth_treasury_stock(self) -> typing.Optional[builtins.str]: ... @property def period(self) -> typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... @@ -457,57 +475,59 @@ class BulkBalanceSheetStatementGrowth: fiscal_year: builtins.str, period: typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"], reported_currency: builtins.str, - growth_cash_and_cash_equivalents: builtins.str, - growth_short_term_investments: builtins.str, - growth_cash_and_short_term_investments: builtins.str, - growth_net_receivables: builtins.str, - growth_inventory: builtins.str, - growth_other_current_assets: builtins.str, - growth_total_current_assets: builtins.str, - growth_property_plant_equipment_net: builtins.str, - growth_goodwill: builtins.str, - growth_intangible_assets: builtins.str, - growth_goodwill_and_intangible_assets: builtins.str, - growth_long_term_investments: builtins.str, - growth_tax_assets: builtins.str, - growth_other_non_current_assets: builtins.str, - growth_total_non_current_assets: builtins.str, - growth_other_assets: builtins.str, - growth_total_assets: builtins.str, - growth_account_payables: builtins.str, - growth_short_term_debt: builtins.str, - growth_tax_payables: builtins.str, - growth_deferred_revenue: builtins.str, - growth_other_current_liabilities: builtins.str, - growth_total_current_liabilities: builtins.str, - growth_long_term_debt: builtins.str, - growth_deferred_revenue_non_current: builtins.str, - growth_deferred_tax_liabilities_non_current: builtins.str, - growth_other_non_current_liabilities: builtins.str, - growth_total_non_current_liabilities: builtins.str, - growth_other_liabilities: builtins.str, - growth_total_liabilities: builtins.str, - growth_preferred_stock: builtins.str, - growth_common_stock: builtins.str, - growth_retained_earnings: builtins.str, - growth_accumulated_other_comprehensive_income_loss: builtins.str, - growth_other_total_stockholders_equity: builtins.str, - growth_total_stockholders_equity: builtins.str, - growth_minority_interest: builtins.str, - growth_total_equity: builtins.str, - growth_total_liabilities_and_stockholders_equity: builtins.str, - growth_total_investments: builtins.str, - growth_total_debt: builtins.str, - growth_net_debt: builtins.str, - growth_accounts_receivables: builtins.str, - growth_other_receivables: builtins.str, - growth_prepaids: builtins.str, - growth_total_payables: builtins.str, - growth_other_payables: builtins.str, - growth_accrued_expenses: builtins.str, - growth_capital_lease_obligations_current: builtins.str, - growth_additional_paid_in_capital: builtins.str, - growth_treasury_stock: builtins.str, + growth_cash_and_cash_equivalents: typing.Optional[builtins.str], + growth_short_term_investments: typing.Optional[builtins.str], + growth_cash_and_short_term_investments: typing.Optional[builtins.str], + growth_net_receivables: typing.Optional[builtins.str], + growth_inventory: typing.Optional[builtins.str], + growth_other_current_assets: typing.Optional[builtins.str], + growth_total_current_assets: typing.Optional[builtins.str], + growth_property_plant_equipment_net: typing.Optional[builtins.str], + growth_goodwill: typing.Optional[builtins.str], + growth_intangible_assets: typing.Optional[builtins.str], + growth_goodwill_and_intangible_assets: typing.Optional[builtins.str], + growth_long_term_investments: typing.Optional[builtins.str], + growth_tax_assets: typing.Optional[builtins.str], + growth_other_non_current_assets: typing.Optional[builtins.str], + growth_total_non_current_assets: typing.Optional[builtins.str], + growth_other_assets: typing.Optional[builtins.str], + growth_total_assets: typing.Optional[builtins.str], + growth_account_payables: typing.Optional[builtins.str], + growth_short_term_debt: typing.Optional[builtins.str], + growth_tax_payables: typing.Optional[builtins.str], + growth_deferred_revenue: typing.Optional[builtins.str], + growth_other_current_liabilities: typing.Optional[builtins.str], + growth_total_current_liabilities: typing.Optional[builtins.str], + growth_long_term_debt: typing.Optional[builtins.str], + growth_deferred_revenue_non_current: typing.Optional[builtins.str], + growth_deferred_tax_liabilities_non_current: typing.Optional[builtins.str], + growth_other_non_current_liabilities: typing.Optional[builtins.str], + growth_total_non_current_liabilities: typing.Optional[builtins.str], + growth_other_liabilities: typing.Optional[builtins.str], + growth_total_liabilities: typing.Optional[builtins.str], + growth_preferred_stock: typing.Optional[builtins.str], + growth_common_stock: typing.Optional[builtins.str], + growth_retained_earnings: typing.Optional[builtins.str], + growth_accumulated_other_comprehensive_income_loss: typing.Optional[ + builtins.str + ], + growth_other_total_stockholders_equity: typing.Optional[builtins.str], + growth_total_stockholders_equity: typing.Optional[builtins.str], + growth_minority_interest: typing.Optional[builtins.str], + growth_total_equity: typing.Optional[builtins.str], + growth_total_liabilities_and_stockholders_equity: typing.Optional[builtins.str], + growth_total_investments: typing.Optional[builtins.str], + growth_total_debt: typing.Optional[builtins.str], + growth_net_debt: typing.Optional[builtins.str], + growth_accounts_receivables: typing.Optional[builtins.str], + growth_other_receivables: typing.Optional[builtins.str], + growth_prepaids: typing.Optional[builtins.str], + growth_total_payables: typing.Optional[builtins.str], + growth_other_payables: typing.Optional[builtins.str], + growth_accrued_expenses: typing.Optional[builtins.str], + growth_capital_lease_obligations_current: typing.Optional[builtins.str], + growth_additional_paid_in_capital: typing.Optional[builtins.str], + growth_treasury_stock: typing.Optional[builtins.str], ) -> BulkBalanceSheetStatementGrowth: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... diff --git a/crates/fmp-py/src/models/bulk/balance.rs b/crates/fmp-py/src/models/bulk/balance.rs index a14636b8..1974f6b1 100644 --- a/crates/fmp-py/src/models/bulk/balance.rs +++ b/crates/fmp-py/src/models/bulk/balance.rs @@ -25,111 +25,111 @@ pub(crate) struct BulkBalanceSheetStatement { pub fiscal_year: String, pub period: String, #[pyo3(get)] - pub cash_and_cash_equivalents: String, + pub cash_and_cash_equivalents: Option, #[pyo3(get)] - pub short_term_investments: String, + pub short_term_investments: Option, #[pyo3(get)] - pub cash_and_short_term_investments: String, + pub cash_and_short_term_investments: Option, #[pyo3(get)] - pub net_receivables: String, + pub net_receivables: Option, #[pyo3(get)] - pub accounts_receivables: String, + pub accounts_receivables: Option, #[pyo3(get)] - pub other_receivables: String, + pub other_receivables: Option, #[pyo3(get)] - pub inventory: String, + pub inventory: Option, #[pyo3(get)] - pub prepaids: String, + pub prepaids: Option, #[pyo3(get)] - pub other_current_assets: String, + pub other_current_assets: Option, #[pyo3(get)] - pub total_current_assets: String, + pub total_current_assets: Option, #[pyo3(get)] - pub property_plant_equipment_net: String, + pub property_plant_equipment_net: Option, #[pyo3(get)] - pub goodwill: String, + pub goodwill: Option, #[pyo3(get)] - pub intangible_assets: String, + pub intangible_assets: Option, #[pyo3(get)] - pub goodwill_and_intangible_assets: String, + pub goodwill_and_intangible_assets: Option, #[pyo3(get)] - pub long_term_investments: String, + pub long_term_investments: Option, #[pyo3(get)] - pub tax_assets: String, + pub tax_assets: Option, #[pyo3(get)] - pub other_non_current_assets: String, + pub other_non_current_assets: Option, #[pyo3(get)] - pub total_non_current_assets: String, + pub total_non_current_assets: Option, #[pyo3(get)] - pub other_assets: String, + pub other_assets: Option, #[pyo3(get)] - pub total_assets: String, + pub total_assets: Option, #[pyo3(get)] - pub total_payables: String, + pub total_payables: Option, #[pyo3(get)] - pub account_payables: String, + pub account_payables: Option, #[pyo3(get)] - pub other_payables: String, + pub other_payables: Option, #[pyo3(get)] - pub accrued_expenses: String, + pub accrued_expenses: Option, #[pyo3(get)] - pub short_term_debt: String, + pub short_term_debt: Option, #[pyo3(get)] - pub capital_lease_obligations_current: String, + pub capital_lease_obligations_current: Option, #[pyo3(get)] - pub tax_payables: String, + pub tax_payables: Option, #[pyo3(get)] - pub deferred_revenue: String, + pub deferred_revenue: Option, #[pyo3(get)] - pub other_current_liabilities: String, + pub other_current_liabilities: Option, #[pyo3(get)] - pub total_current_liabilities: String, + pub total_current_liabilities: Option, #[pyo3(get)] - pub long_term_debt: String, + pub long_term_debt: Option, #[pyo3(get)] - pub capital_lease_obligations_non_current: String, + pub capital_lease_obligations_non_current: Option, #[pyo3(get)] - pub deferred_revenue_non_current: String, + pub deferred_revenue_non_current: Option, #[pyo3(get)] - pub deferred_tax_liabilities_non_current: String, + pub deferred_tax_liabilities_non_current: Option, #[pyo3(get)] - pub other_non_current_liabilities: String, + pub other_non_current_liabilities: Option, #[pyo3(get)] - pub total_non_current_liabilities: String, + pub total_non_current_liabilities: Option, #[pyo3(get)] - pub other_liabilities: String, + pub other_liabilities: Option, #[pyo3(get)] - pub capital_lease_obligations: String, + pub capital_lease_obligations: Option, #[pyo3(get)] - pub total_liabilities: String, + pub total_liabilities: Option, #[pyo3(get)] - pub treasury_stock: String, + pub treasury_stock: Option, #[pyo3(get)] - pub preferred_stock: String, + pub preferred_stock: Option, #[pyo3(get)] - pub common_stock: String, + pub common_stock: Option, #[pyo3(get)] - pub retained_earnings: String, + pub retained_earnings: Option, #[pyo3(get)] - pub additional_paid_in_capital: String, + pub additional_paid_in_capital: Option, #[pyo3(get)] - pub accumulated_other_comprehensive_income_loss: String, + pub accumulated_other_comprehensive_income_loss: Option, #[pyo3(get)] - pub other_total_stockholders_equity: String, + pub other_total_stockholders_equity: Option, #[pyo3(get)] - pub total_stockholders_equity: String, + pub total_stockholders_equity: Option, #[pyo3(get)] - pub total_equity: String, + pub total_equity: Option, #[pyo3(get)] - pub minority_interest: String, + pub minority_interest: Option, #[pyo3(get)] - pub total_liabilities_and_total_equity: String, + pub total_liabilities_and_total_equity: Option, #[pyo3(get)] - pub total_investments: String, + pub total_investments: Option, #[pyo3(get)] - pub total_debt: String, + pub total_debt: Option, #[pyo3(get)] - pub net_debt: String, + pub net_debt: Option, } #[gen_stub_pymethods] @@ -149,59 +149,59 @@ impl BulkBalanceSheetStatement { fiscal_year: String, #[gen_stub(override_type(type_repr = "typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\"]", imports = ("builtins", "typing")))] period: String, - cash_and_cash_equivalents: String, - short_term_investments: String, - cash_and_short_term_investments: String, - net_receivables: String, - accounts_receivables: String, - other_receivables: String, - inventory: String, - prepaids: String, - other_current_assets: String, - total_current_assets: String, - property_plant_equipment_net: String, - goodwill: String, - intangible_assets: String, - goodwill_and_intangible_assets: String, - long_term_investments: String, - tax_assets: String, - other_non_current_assets: String, - total_non_current_assets: String, - other_assets: String, - total_assets: String, - total_payables: String, - account_payables: String, - other_payables: String, - accrued_expenses: String, - short_term_debt: String, - capital_lease_obligations_current: String, - tax_payables: String, - deferred_revenue: String, - other_current_liabilities: String, - total_current_liabilities: String, - long_term_debt: String, - capital_lease_obligations_non_current: String, - deferred_revenue_non_current: String, - deferred_tax_liabilities_non_current: String, - other_non_current_liabilities: String, - total_non_current_liabilities: String, - other_liabilities: String, - capital_lease_obligations: String, - total_liabilities: String, - treasury_stock: String, - preferred_stock: String, - common_stock: String, - retained_earnings: String, - additional_paid_in_capital: String, - accumulated_other_comprehensive_income_loss: String, - other_total_stockholders_equity: String, - total_stockholders_equity: String, - total_equity: String, - minority_interest: String, - total_liabilities_and_total_equity: String, - total_investments: String, - total_debt: String, - net_debt: String, + cash_and_cash_equivalents: Option, + short_term_investments: Option, + cash_and_short_term_investments: Option, + net_receivables: Option, + accounts_receivables: Option, + other_receivables: Option, + inventory: Option, + prepaids: Option, + other_current_assets: Option, + total_current_assets: Option, + property_plant_equipment_net: Option, + goodwill: Option, + intangible_assets: Option, + goodwill_and_intangible_assets: Option, + long_term_investments: Option, + tax_assets: Option, + other_non_current_assets: Option, + total_non_current_assets: Option, + other_assets: Option, + total_assets: Option, + total_payables: Option, + account_payables: Option, + other_payables: Option, + accrued_expenses: Option, + short_term_debt: Option, + capital_lease_obligations_current: Option, + tax_payables: Option, + deferred_revenue: Option, + other_current_liabilities: Option, + total_current_liabilities: Option, + long_term_debt: Option, + capital_lease_obligations_non_current: Option, + deferred_revenue_non_current: Option, + deferred_tax_liabilities_non_current: Option, + other_non_current_liabilities: Option, + total_non_current_liabilities: Option, + other_liabilities: Option, + capital_lease_obligations: Option, + total_liabilities: Option, + treasury_stock: Option, + preferred_stock: Option, + common_stock: Option, + retained_earnings: Option, + additional_paid_in_capital: Option, + accumulated_other_comprehensive_income_loss: Option, + other_total_stockholders_equity: Option, + total_stockholders_equity: Option, + total_equity: Option, + minority_interest: Option, + total_liabilities_and_total_equity: Option, + total_investments: Option, + total_debt: Option, + net_debt: Option, ) -> Self { Self { date, @@ -1161,67 +1161,99 @@ impl From for BulkBalanceShe accepted_date: value.accepted_date.into_inner(), fiscal_year: value.fiscal_year.as_str().to_owned(), period: value.period.to_string(), - cash_and_cash_equivalents: value.cash_and_cash_equivalents.into_inner(), - short_term_investments: value.short_term_investments.into_inner(), - cash_and_short_term_investments: value.cash_and_short_term_investments.into_inner(), - net_receivables: value.net_receivables.into_inner(), - accounts_receivables: value.accounts_receivables.into_inner(), - other_receivables: value.other_receivables.into_inner(), - inventory: value.inventory.into_inner(), - prepaids: value.prepaids.into_inner(), - other_current_assets: value.other_current_assets.into_inner(), - total_current_assets: value.total_current_assets.into_inner(), - property_plant_equipment_net: value.property_plant_equipment_net.into_inner(), - goodwill: value.goodwill.into_inner(), - intangible_assets: value.intangible_assets.into_inner(), - goodwill_and_intangible_assets: value.goodwill_and_intangible_assets.into_inner(), - long_term_investments: value.long_term_investments.into_inner(), - tax_assets: value.tax_assets.into_inner(), - other_non_current_assets: value.other_non_current_assets.into_inner(), - total_non_current_assets: value.total_non_current_assets.into_inner(), - other_assets: value.other_assets.into_inner(), - total_assets: value.total_assets.into_inner(), - total_payables: value.total_payables.into_inner(), - account_payables: value.account_payables.into_inner(), - other_payables: value.other_payables.into_inner(), - accrued_expenses: value.accrued_expenses.into_inner(), - short_term_debt: value.short_term_debt.into_inner(), - capital_lease_obligations_current: value.capital_lease_obligations_current.into_inner(), - tax_payables: value.tax_payables.into_inner(), - deferred_revenue: value.deferred_revenue.into_inner(), - other_current_liabilities: value.other_current_liabilities.into_inner(), - total_current_liabilities: value.total_current_liabilities.into_inner(), - long_term_debt: value.long_term_debt.into_inner(), + cash_and_cash_equivalents: value + .cash_and_cash_equivalents + .map(|value| value.into_inner()), + short_term_investments: value.short_term_investments.map(|value| value.into_inner()), + cash_and_short_term_investments: value + .cash_and_short_term_investments + .map(|value| value.into_inner()), + net_receivables: value.net_receivables.map(|value| value.into_inner()), + accounts_receivables: value.accounts_receivables.map(|value| value.into_inner()), + other_receivables: value.other_receivables.map(|value| value.into_inner()), + inventory: value.inventory.map(|value| value.into_inner()), + prepaids: value.prepaids.map(|value| value.into_inner()), + other_current_assets: value.other_current_assets.map(|value| value.into_inner()), + total_current_assets: value.total_current_assets.map(|value| value.into_inner()), + property_plant_equipment_net: value + .property_plant_equipment_net + .map(|value| value.into_inner()), + goodwill: value.goodwill.map(|value| value.into_inner()), + intangible_assets: value.intangible_assets.map(|value| value.into_inner()), + goodwill_and_intangible_assets: value + .goodwill_and_intangible_assets + .map(|value| value.into_inner()), + long_term_investments: value.long_term_investments.map(|value| value.into_inner()), + tax_assets: value.tax_assets.map(|value| value.into_inner()), + other_non_current_assets: value + .other_non_current_assets + .map(|value| value.into_inner()), + total_non_current_assets: value + .total_non_current_assets + .map(|value| value.into_inner()), + other_assets: value.other_assets.map(|value| value.into_inner()), + total_assets: value.total_assets.map(|value| value.into_inner()), + total_payables: value.total_payables.map(|value| value.into_inner()), + account_payables: value.account_payables.map(|value| value.into_inner()), + other_payables: value.other_payables.map(|value| value.into_inner()), + accrued_expenses: value.accrued_expenses.map(|value| value.into_inner()), + short_term_debt: value.short_term_debt.map(|value| value.into_inner()), + capital_lease_obligations_current: value + .capital_lease_obligations_current + .map(|value| value.into_inner()), + tax_payables: value.tax_payables.map(|value| value.into_inner()), + deferred_revenue: value.deferred_revenue.map(|value| value.into_inner()), + other_current_liabilities: value + .other_current_liabilities + .map(|value| value.into_inner()), + total_current_liabilities: value + .total_current_liabilities + .map(|value| value.into_inner()), + long_term_debt: value.long_term_debt.map(|value| value.into_inner()), capital_lease_obligations_non_current: value .capital_lease_obligations_non_current - .into_inner(), - deferred_revenue_non_current: value.deferred_revenue_non_current.into_inner(), + .map(|value| value.into_inner()), + deferred_revenue_non_current: value + .deferred_revenue_non_current + .map(|value| value.into_inner()), deferred_tax_liabilities_non_current: value .deferred_tax_liabilities_non_current - .into_inner(), - other_non_current_liabilities: value.other_non_current_liabilities.into_inner(), - total_non_current_liabilities: value.total_non_current_liabilities.into_inner(), - other_liabilities: value.other_liabilities.into_inner(), - capital_lease_obligations: value.capital_lease_obligations.into_inner(), - total_liabilities: value.total_liabilities.into_inner(), - treasury_stock: value.treasury_stock.into_inner(), - preferred_stock: value.preferred_stock.into_inner(), - common_stock: value.common_stock.into_inner(), - retained_earnings: value.retained_earnings.into_inner(), - additional_paid_in_capital: value.additional_paid_in_capital.into_inner(), + .map(|value| value.into_inner()), + other_non_current_liabilities: value + .other_non_current_liabilities + .map(|value| value.into_inner()), + total_non_current_liabilities: value + .total_non_current_liabilities + .map(|value| value.into_inner()), + other_liabilities: value.other_liabilities.map(|value| value.into_inner()), + capital_lease_obligations: value + .capital_lease_obligations + .map(|value| value.into_inner()), + total_liabilities: value.total_liabilities.map(|value| value.into_inner()), + treasury_stock: value.treasury_stock.map(|value| value.into_inner()), + preferred_stock: value.preferred_stock.map(|value| value.into_inner()), + common_stock: value.common_stock.map(|value| value.into_inner()), + retained_earnings: value.retained_earnings.map(|value| value.into_inner()), + additional_paid_in_capital: value + .additional_paid_in_capital + .map(|value| value.into_inner()), accumulated_other_comprehensive_income_loss: value .accumulated_other_comprehensive_income_loss - .into_inner(), - other_total_stockholders_equity: value.other_total_stockholders_equity.into_inner(), - total_stockholders_equity: value.total_stockholders_equity.into_inner(), - total_equity: value.total_equity.into_inner(), - minority_interest: value.minority_interest.into_inner(), + .map(|value| value.into_inner()), + other_total_stockholders_equity: value + .other_total_stockholders_equity + .map(|value| value.into_inner()), + total_stockholders_equity: value + .total_stockholders_equity + .map(|value| value.into_inner()), + total_equity: value.total_equity.map(|value| value.into_inner()), + minority_interest: value.minority_interest.map(|value| value.into_inner()), total_liabilities_and_total_equity: value .total_liabilities_and_total_equity - .into_inner(), - total_investments: value.total_investments.into_inner(), - total_debt: value.total_debt.into_inner(), - net_debt: value.net_debt.into_inner(), + .map(|value| value.into_inner()), + total_investments: value.total_investments.map(|value| value.into_inner()), + total_debt: value.total_debt.map(|value| value.into_inner()), + net_debt: value.net_debt.map(|value| value.into_inner()), } } } @@ -1246,107 +1278,107 @@ pub(crate) struct BulkBalanceSheetStatementGrowth { #[pyo3(get)] pub reported_currency: String, #[pyo3(get)] - pub growth_cash_and_cash_equivalents: String, + pub growth_cash_and_cash_equivalents: Option, #[pyo3(get)] - pub growth_short_term_investments: String, + pub growth_short_term_investments: Option, #[pyo3(get)] - pub growth_cash_and_short_term_investments: String, + pub growth_cash_and_short_term_investments: Option, #[pyo3(get)] - pub growth_net_receivables: String, + pub growth_net_receivables: Option, #[pyo3(get)] - pub growth_inventory: String, + pub growth_inventory: Option, #[pyo3(get)] - pub growth_other_current_assets: String, + pub growth_other_current_assets: Option, #[pyo3(get)] - pub growth_total_current_assets: String, + pub growth_total_current_assets: Option, #[pyo3(get)] - pub growth_property_plant_equipment_net: String, + pub growth_property_plant_equipment_net: Option, #[pyo3(get)] - pub growth_goodwill: String, + pub growth_goodwill: Option, #[pyo3(get)] - pub growth_intangible_assets: String, + pub growth_intangible_assets: Option, #[pyo3(get)] - pub growth_goodwill_and_intangible_assets: String, + pub growth_goodwill_and_intangible_assets: Option, #[pyo3(get)] - pub growth_long_term_investments: String, + pub growth_long_term_investments: Option, #[pyo3(get)] - pub growth_tax_assets: String, + pub growth_tax_assets: Option, #[pyo3(get)] - pub growth_other_non_current_assets: String, + pub growth_other_non_current_assets: Option, #[pyo3(get)] - pub growth_total_non_current_assets: String, + pub growth_total_non_current_assets: Option, #[pyo3(get)] - pub growth_other_assets: String, + pub growth_other_assets: Option, #[pyo3(get)] - pub growth_total_assets: String, + pub growth_total_assets: Option, #[pyo3(get)] - pub growth_account_payables: String, + pub growth_account_payables: Option, #[pyo3(get)] - pub growth_short_term_debt: String, + pub growth_short_term_debt: Option, #[pyo3(get)] - pub growth_tax_payables: String, + pub growth_tax_payables: Option, #[pyo3(get)] - pub growth_deferred_revenue: String, + pub growth_deferred_revenue: Option, #[pyo3(get)] - pub growth_other_current_liabilities: String, + pub growth_other_current_liabilities: Option, #[pyo3(get)] - pub growth_total_current_liabilities: String, + pub growth_total_current_liabilities: Option, #[pyo3(get)] - pub growth_long_term_debt: String, + pub growth_long_term_debt: Option, #[pyo3(get)] - pub growth_deferred_revenue_non_current: String, + pub growth_deferred_revenue_non_current: Option, #[pyo3(get)] - pub growth_deferred_tax_liabilities_non_current: String, + pub growth_deferred_tax_liabilities_non_current: Option, #[pyo3(get)] - pub growth_other_non_current_liabilities: String, + pub growth_other_non_current_liabilities: Option, #[pyo3(get)] - pub growth_total_non_current_liabilities: String, + pub growth_total_non_current_liabilities: Option, #[pyo3(get)] - pub growth_other_liabilities: String, + pub growth_other_liabilities: Option, #[pyo3(get)] - pub growth_total_liabilities: String, + pub growth_total_liabilities: Option, #[pyo3(get)] - pub growth_preferred_stock: String, + pub growth_preferred_stock: Option, #[pyo3(get)] - pub growth_common_stock: String, + pub growth_common_stock: Option, #[pyo3(get)] - pub growth_retained_earnings: String, + pub growth_retained_earnings: Option, #[pyo3(get)] - pub growth_accumulated_other_comprehensive_income_loss: String, + pub growth_accumulated_other_comprehensive_income_loss: Option, #[pyo3(get)] - pub growth_other_total_stockholders_equity: String, + pub growth_other_total_stockholders_equity: Option, #[pyo3(get)] - pub growth_total_stockholders_equity: String, + pub growth_total_stockholders_equity: Option, #[pyo3(get)] - pub growth_minority_interest: String, + pub growth_minority_interest: Option, #[pyo3(get)] - pub growth_total_equity: String, + pub growth_total_equity: Option, #[pyo3(get)] - pub growth_total_liabilities_and_stockholders_equity: String, + pub growth_total_liabilities_and_stockholders_equity: Option, #[pyo3(get)] - pub growth_total_investments: String, + pub growth_total_investments: Option, #[pyo3(get)] - pub growth_total_debt: String, + pub growth_total_debt: Option, #[pyo3(get)] - pub growth_net_debt: String, + pub growth_net_debt: Option, #[pyo3(get)] - pub growth_accounts_receivables: String, + pub growth_accounts_receivables: Option, #[pyo3(get)] - pub growth_other_receivables: String, + pub growth_other_receivables: Option, #[pyo3(get)] - pub growth_prepaids: String, + pub growth_prepaids: Option, #[pyo3(get)] - pub growth_total_payables: String, + pub growth_total_payables: Option, #[pyo3(get)] - pub growth_other_payables: String, + pub growth_other_payables: Option, #[pyo3(get)] - pub growth_accrued_expenses: String, + pub growth_accrued_expenses: Option, #[pyo3(get)] - pub growth_capital_lease_obligations_current: String, + pub growth_capital_lease_obligations_current: Option, #[pyo3(get)] - pub growth_additional_paid_in_capital: String, + pub growth_additional_paid_in_capital: Option, #[pyo3(get)] - pub growth_treasury_stock: String, + pub growth_treasury_stock: Option, } #[gen_stub_pymethods] @@ -1363,57 +1395,57 @@ impl BulkBalanceSheetStatementGrowth { #[gen_stub(override_type(type_repr = "typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\"]", imports = ("builtins", "typing")))] period: String, reported_currency: String, - growth_cash_and_cash_equivalents: String, - growth_short_term_investments: String, - growth_cash_and_short_term_investments: String, - growth_net_receivables: String, - growth_inventory: String, - growth_other_current_assets: String, - growth_total_current_assets: String, - growth_property_plant_equipment_net: String, - growth_goodwill: String, - growth_intangible_assets: String, - growth_goodwill_and_intangible_assets: String, - growth_long_term_investments: String, - growth_tax_assets: String, - growth_other_non_current_assets: String, - growth_total_non_current_assets: String, - growth_other_assets: String, - growth_total_assets: String, - growth_account_payables: String, - growth_short_term_debt: String, - growth_tax_payables: String, - growth_deferred_revenue: String, - growth_other_current_liabilities: String, - growth_total_current_liabilities: String, - growth_long_term_debt: String, - growth_deferred_revenue_non_current: String, - growth_deferred_tax_liabilities_non_current: String, - growth_other_non_current_liabilities: String, - growth_total_non_current_liabilities: String, - growth_other_liabilities: String, - growth_total_liabilities: String, - growth_preferred_stock: String, - growth_common_stock: String, - growth_retained_earnings: String, - growth_accumulated_other_comprehensive_income_loss: String, - growth_other_total_stockholders_equity: String, - growth_total_stockholders_equity: String, - growth_minority_interest: String, - growth_total_equity: String, - growth_total_liabilities_and_stockholders_equity: String, - growth_total_investments: String, - growth_total_debt: String, - growth_net_debt: String, - growth_accounts_receivables: String, - growth_other_receivables: String, - growth_prepaids: String, - growth_total_payables: String, - growth_other_payables: String, - growth_accrued_expenses: String, - growth_capital_lease_obligations_current: String, - growth_additional_paid_in_capital: String, - growth_treasury_stock: String, + growth_cash_and_cash_equivalents: Option, + growth_short_term_investments: Option, + growth_cash_and_short_term_investments: Option, + growth_net_receivables: Option, + growth_inventory: Option, + growth_other_current_assets: Option, + growth_total_current_assets: Option, + growth_property_plant_equipment_net: Option, + growth_goodwill: Option, + growth_intangible_assets: Option, + growth_goodwill_and_intangible_assets: Option, + growth_long_term_investments: Option, + growth_tax_assets: Option, + growth_other_non_current_assets: Option, + growth_total_non_current_assets: Option, + growth_other_assets: Option, + growth_total_assets: Option, + growth_account_payables: Option, + growth_short_term_debt: Option, + growth_tax_payables: Option, + growth_deferred_revenue: Option, + growth_other_current_liabilities: Option, + growth_total_current_liabilities: Option, + growth_long_term_debt: Option, + growth_deferred_revenue_non_current: Option, + growth_deferred_tax_liabilities_non_current: Option, + growth_other_non_current_liabilities: Option, + growth_total_non_current_liabilities: Option, + growth_other_liabilities: Option, + growth_total_liabilities: Option, + growth_preferred_stock: Option, + growth_common_stock: Option, + growth_retained_earnings: Option, + growth_accumulated_other_comprehensive_income_loss: Option, + growth_other_total_stockholders_equity: Option, + growth_total_stockholders_equity: Option, + growth_minority_interest: Option, + growth_total_equity: Option, + growth_total_liabilities_and_stockholders_equity: Option, + growth_total_investments: Option, + growth_total_debt: Option, + growth_net_debt: Option, + growth_accounts_receivables: Option, + growth_other_receivables: Option, + growth_prepaids: Option, + growth_total_payables: Option, + growth_other_payables: Option, + growth_accrued_expenses: Option, + growth_capital_lease_obligations_current: Option, + growth_additional_paid_in_capital: Option, + growth_treasury_stock: Option, ) -> Self { Self { symbol, @@ -2414,79 +2446,123 @@ impl From fiscal_year: value.fiscal_year.as_str().to_owned(), period: value.period.to_string(), reported_currency: value.reported_currency.into_inner(), - growth_cash_and_cash_equivalents: value.growth_cash_and_cash_equivalents.into_inner(), - growth_short_term_investments: value.growth_short_term_investments.into_inner(), + growth_cash_and_cash_equivalents: value + .growth_cash_and_cash_equivalents + .map(|value| value.into_inner()), + growth_short_term_investments: value + .growth_short_term_investments + .map(|value| value.into_inner()), growth_cash_and_short_term_investments: value .growth_cash_and_short_term_investments - .into_inner(), - growth_net_receivables: value.growth_net_receivables.into_inner(), - growth_inventory: value.growth_inventory.into_inner(), - growth_other_current_assets: value.growth_other_current_assets.into_inner(), - growth_total_current_assets: value.growth_total_current_assets.into_inner(), + .map(|value| value.into_inner()), + growth_net_receivables: value.growth_net_receivables.map(|value| value.into_inner()), + growth_inventory: value.growth_inventory.map(|value| value.into_inner()), + growth_other_current_assets: value + .growth_other_current_assets + .map(|value| value.into_inner()), + growth_total_current_assets: value + .growth_total_current_assets + .map(|value| value.into_inner()), growth_property_plant_equipment_net: value .growth_property_plant_equipment_net - .into_inner(), - growth_goodwill: value.growth_goodwill.into_inner(), - growth_intangible_assets: value.growth_intangible_assets.into_inner(), + .map(|value| value.into_inner()), + growth_goodwill: value.growth_goodwill.map(|value| value.into_inner()), + growth_intangible_assets: value + .growth_intangible_assets + .map(|value| value.into_inner()), growth_goodwill_and_intangible_assets: value .growth_goodwill_and_intangible_assets - .into_inner(), - growth_long_term_investments: value.growth_long_term_investments.into_inner(), - growth_tax_assets: value.growth_tax_assets.into_inner(), - growth_other_non_current_assets: value.growth_other_non_current_assets.into_inner(), - growth_total_non_current_assets: value.growth_total_non_current_assets.into_inner(), - growth_other_assets: value.growth_other_assets.into_inner(), - growth_total_assets: value.growth_total_assets.into_inner(), - growth_account_payables: value.growth_account_payables.into_inner(), - growth_short_term_debt: value.growth_short_term_debt.into_inner(), - growth_tax_payables: value.growth_tax_payables.into_inner(), - growth_deferred_revenue: value.growth_deferred_revenue.into_inner(), - growth_other_current_liabilities: value.growth_other_current_liabilities.into_inner(), - growth_total_current_liabilities: value.growth_total_current_liabilities.into_inner(), - growth_long_term_debt: value.growth_long_term_debt.into_inner(), + .map(|value| value.into_inner()), + growth_long_term_investments: value + .growth_long_term_investments + .map(|value| value.into_inner()), + growth_tax_assets: value.growth_tax_assets.map(|value| value.into_inner()), + growth_other_non_current_assets: value + .growth_other_non_current_assets + .map(|value| value.into_inner()), + growth_total_non_current_assets: value + .growth_total_non_current_assets + .map(|value| value.into_inner()), + growth_other_assets: value.growth_other_assets.map(|value| value.into_inner()), + growth_total_assets: value.growth_total_assets.map(|value| value.into_inner()), + growth_account_payables: value + .growth_account_payables + .map(|value| value.into_inner()), + growth_short_term_debt: value.growth_short_term_debt.map(|value| value.into_inner()), + growth_tax_payables: value.growth_tax_payables.map(|value| value.into_inner()), + growth_deferred_revenue: value + .growth_deferred_revenue + .map(|value| value.into_inner()), + growth_other_current_liabilities: value + .growth_other_current_liabilities + .map(|value| value.into_inner()), + growth_total_current_liabilities: value + .growth_total_current_liabilities + .map(|value| value.into_inner()), + growth_long_term_debt: value.growth_long_term_debt.map(|value| value.into_inner()), growth_deferred_revenue_non_current: value .growth_deferred_revenue_non_current - .into_inner(), + .map(|value| value.into_inner()), growth_deferred_tax_liabilities_non_current: value .growth_deferred_tax_liabilities_non_current - .into_inner(), + .map(|value| value.into_inner()), growth_other_non_current_liabilities: value .growth_other_non_current_liabilities - .into_inner(), + .map(|value| value.into_inner()), growth_total_non_current_liabilities: value .growth_total_non_current_liabilities - .into_inner(), - growth_other_liabilities: value.growth_other_liabilities.into_inner(), - growth_total_liabilities: value.growth_total_liabilities.into_inner(), - growth_preferred_stock: value.growth_preferred_stock.into_inner(), - growth_common_stock: value.growth_common_stock.into_inner(), - growth_retained_earnings: value.growth_retained_earnings.into_inner(), + .map(|value| value.into_inner()), + growth_other_liabilities: value + .growth_other_liabilities + .map(|value| value.into_inner()), + growth_total_liabilities: value + .growth_total_liabilities + .map(|value| value.into_inner()), + growth_preferred_stock: value.growth_preferred_stock.map(|value| value.into_inner()), + growth_common_stock: value.growth_common_stock.map(|value| value.into_inner()), + growth_retained_earnings: value + .growth_retained_earnings + .map(|value| value.into_inner()), growth_accumulated_other_comprehensive_income_loss: value .growth_accumulated_other_comprehensive_income_loss - .into_inner(), + .map(|value| value.into_inner()), growth_other_total_stockholders_equity: value .growth_other_total_stockholders_equity - .into_inner(), - growth_total_stockholders_equity: value.growth_total_stockholders_equity.into_inner(), - growth_minority_interest: value.growth_minority_interest.into_inner(), - growth_total_equity: value.growth_total_equity.into_inner(), + .map(|value| value.into_inner()), + growth_total_stockholders_equity: value + .growth_total_stockholders_equity + .map(|value| value.into_inner()), + growth_minority_interest: value + .growth_minority_interest + .map(|value| value.into_inner()), + growth_total_equity: value.growth_total_equity.map(|value| value.into_inner()), growth_total_liabilities_and_stockholders_equity: value .growth_total_liabilities_and_stockholders_equity - .into_inner(), - growth_total_investments: value.growth_total_investments.into_inner(), - growth_total_debt: value.growth_total_debt.into_inner(), - growth_net_debt: value.growth_net_debt.into_inner(), - growth_accounts_receivables: value.growth_accounts_receivables.into_inner(), - growth_other_receivables: value.growth_other_receivables.into_inner(), - growth_prepaids: value.growth_prepaids.into_inner(), - growth_total_payables: value.growth_total_payables.into_inner(), - growth_other_payables: value.growth_other_payables.into_inner(), - growth_accrued_expenses: value.growth_accrued_expenses.into_inner(), + .map(|value| value.into_inner()), + growth_total_investments: value + .growth_total_investments + .map(|value| value.into_inner()), + growth_total_debt: value.growth_total_debt.map(|value| value.into_inner()), + growth_net_debt: value.growth_net_debt.map(|value| value.into_inner()), + growth_accounts_receivables: value + .growth_accounts_receivables + .map(|value| value.into_inner()), + growth_other_receivables: value + .growth_other_receivables + .map(|value| value.into_inner()), + growth_prepaids: value.growth_prepaids.map(|value| value.into_inner()), + growth_total_payables: value.growth_total_payables.map(|value| value.into_inner()), + growth_other_payables: value.growth_other_payables.map(|value| value.into_inner()), + growth_accrued_expenses: value + .growth_accrued_expenses + .map(|value| value.into_inner()), growth_capital_lease_obligations_current: value .growth_capital_lease_obligations_current - .into_inner(), - growth_additional_paid_in_capital: value.growth_additional_paid_in_capital.into_inner(), - growth_treasury_stock: value.growth_treasury_stock.into_inner(), + .map(|value| value.into_inner()), + growth_additional_paid_in_capital: value + .growth_additional_paid_in_capital + .map(|value| value.into_inner()), + growth_treasury_stock: value.growth_treasury_stock.map(|value| value.into_inner()), } } } diff --git a/crates/libfmp/src/responses/bulk/balance.rs b/crates/libfmp/src/responses/bulk/balance.rs index e2ad8cd7..b1f73753 100644 --- a/crates/libfmp/src/responses/bulk/balance.rs +++ b/crates/libfmp/src/responses/bulk/balance.rs @@ -21,59 +21,59 @@ pub struct BulkBalanceSheetStatement { pub accepted_date: ApiDateTime, pub fiscal_year: FiscalYearString, pub period: FiscalPeriod, - pub cash_and_cash_equivalents: NumericString, - pub short_term_investments: NumericString, - pub cash_and_short_term_investments: NumericString, - pub net_receivables: NumericString, - pub accounts_receivables: NumericString, - pub other_receivables: NumericString, - pub inventory: NumericString, - pub prepaids: NumericString, - pub other_current_assets: NumericString, - pub total_current_assets: NumericString, - pub property_plant_equipment_net: NumericString, - pub goodwill: NumericString, - pub intangible_assets: NumericString, - pub goodwill_and_intangible_assets: NumericString, - pub long_term_investments: NumericString, - pub tax_assets: NumericString, - pub other_non_current_assets: NumericString, - pub total_non_current_assets: NumericString, - pub other_assets: NumericString, - pub total_assets: NumericString, - pub total_payables: NumericString, - pub account_payables: NumericString, - pub other_payables: NumericString, - pub accrued_expenses: NumericString, - pub short_term_debt: NumericString, - pub capital_lease_obligations_current: NumericString, - pub tax_payables: NumericString, - pub deferred_revenue: NumericString, - pub other_current_liabilities: NumericString, - pub total_current_liabilities: NumericString, - pub long_term_debt: NumericString, - pub capital_lease_obligations_non_current: NumericString, - pub deferred_revenue_non_current: NumericString, - pub deferred_tax_liabilities_non_current: NumericString, - pub other_non_current_liabilities: NumericString, - pub total_non_current_liabilities: NumericString, - pub other_liabilities: NumericString, - pub capital_lease_obligations: NumericString, - pub total_liabilities: NumericString, - pub treasury_stock: NumericString, - pub preferred_stock: NumericString, - pub common_stock: NumericString, - pub retained_earnings: NumericString, - pub additional_paid_in_capital: NumericString, - pub accumulated_other_comprehensive_income_loss: NumericString, - pub other_total_stockholders_equity: NumericString, - pub total_stockholders_equity: NumericString, - pub total_equity: NumericString, - pub minority_interest: NumericString, - pub total_liabilities_and_total_equity: NumericString, - pub total_investments: NumericString, - pub total_debt: NumericString, - pub net_debt: NumericString, + pub cash_and_cash_equivalents: Option, + pub short_term_investments: Option, + pub cash_and_short_term_investments: Option, + pub net_receivables: Option, + pub accounts_receivables: Option, + pub other_receivables: Option, + pub inventory: Option, + pub prepaids: Option, + pub other_current_assets: Option, + pub total_current_assets: Option, + pub property_plant_equipment_net: Option, + pub goodwill: Option, + pub intangible_assets: Option, + pub goodwill_and_intangible_assets: Option, + pub long_term_investments: Option, + pub tax_assets: Option, + pub other_non_current_assets: Option, + pub total_non_current_assets: Option, + pub other_assets: Option, + pub total_assets: Option, + pub total_payables: Option, + pub account_payables: Option, + pub other_payables: Option, + pub accrued_expenses: Option, + pub short_term_debt: Option, + pub capital_lease_obligations_current: Option, + pub tax_payables: Option, + pub deferred_revenue: Option, + pub other_current_liabilities: Option, + pub total_current_liabilities: Option, + pub long_term_debt: Option, + pub capital_lease_obligations_non_current: Option, + pub deferred_revenue_non_current: Option, + pub deferred_tax_liabilities_non_current: Option, + pub other_non_current_liabilities: Option, + pub total_non_current_liabilities: Option, + pub other_liabilities: Option, + pub capital_lease_obligations: Option, + pub total_liabilities: Option, + pub treasury_stock: Option, + pub preferred_stock: Option, + pub common_stock: Option, + pub retained_earnings: Option, + pub additional_paid_in_capital: Option, + pub accumulated_other_comprehensive_income_loss: Option, + pub other_total_stockholders_equity: Option, + pub total_stockholders_equity: Option, + pub total_equity: Option, + pub minority_interest: Option, + pub total_liabilities_and_total_equity: Option, + pub total_investments: Option, + pub total_debt: Option, + pub net_debt: Option, } /// One worldwide bulk balance-sheet-statement-growth row. @@ -86,56 +86,56 @@ pub struct BulkBalanceSheetStatementGrowth { pub fiscal_year: FiscalYearString, pub period: FiscalPeriod, pub reported_currency: CurrencyCode, - pub growth_cash_and_cash_equivalents: NumericString, - pub growth_short_term_investments: NumericString, - pub growth_cash_and_short_term_investments: NumericString, - pub growth_net_receivables: NumericString, - pub growth_inventory: NumericString, - pub growth_other_current_assets: NumericString, - pub growth_total_current_assets: NumericString, - pub growth_property_plant_equipment_net: NumericString, - pub growth_goodwill: NumericString, - pub growth_intangible_assets: NumericString, - pub growth_goodwill_and_intangible_assets: NumericString, - pub growth_long_term_investments: NumericString, - pub growth_tax_assets: NumericString, - pub growth_other_non_current_assets: NumericString, - pub growth_total_non_current_assets: NumericString, - pub growth_other_assets: NumericString, - pub growth_total_assets: NumericString, - pub growth_account_payables: NumericString, - pub growth_short_term_debt: NumericString, - pub growth_tax_payables: NumericString, - pub growth_deferred_revenue: NumericString, - pub growth_other_current_liabilities: NumericString, - pub growth_total_current_liabilities: NumericString, - pub growth_long_term_debt: NumericString, - pub growth_deferred_revenue_non_current: NumericString, - pub growth_deferred_tax_liabilities_non_current: NumericString, - pub growth_other_non_current_liabilities: NumericString, - pub growth_total_non_current_liabilities: NumericString, - pub growth_other_liabilities: NumericString, - pub growth_total_liabilities: NumericString, - pub growth_preferred_stock: NumericString, - pub growth_common_stock: NumericString, - pub growth_retained_earnings: NumericString, - pub growth_accumulated_other_comprehensive_income_loss: NumericString, + pub growth_cash_and_cash_equivalents: Option, + pub growth_short_term_investments: Option, + pub growth_cash_and_short_term_investments: Option, + pub growth_net_receivables: Option, + pub growth_inventory: Option, + pub growth_other_current_assets: Option, + pub growth_total_current_assets: Option, + pub growth_property_plant_equipment_net: Option, + pub growth_goodwill: Option, + pub growth_intangible_assets: Option, + pub growth_goodwill_and_intangible_assets: Option, + pub growth_long_term_investments: Option, + pub growth_tax_assets: Option, + pub growth_other_non_current_assets: Option, + pub growth_total_non_current_assets: Option, + pub growth_other_assets: Option, + pub growth_total_assets: Option, + pub growth_account_payables: Option, + pub growth_short_term_debt: Option, + pub growth_tax_payables: Option, + pub growth_deferred_revenue: Option, + pub growth_other_current_liabilities: Option, + pub growth_total_current_liabilities: Option, + pub growth_long_term_debt: Option, + pub growth_deferred_revenue_non_current: Option, + pub growth_deferred_tax_liabilities_non_current: Option, + pub growth_other_non_current_liabilities: Option, + pub growth_total_non_current_liabilities: Option, + pub growth_other_liabilities: Option, + pub growth_total_liabilities: Option, + pub growth_preferred_stock: Option, + pub growth_common_stock: Option, + pub growth_retained_earnings: Option, + pub growth_accumulated_other_comprehensive_income_loss: Option, #[serde(rename = "growthOthertotalStockholdersEquity")] - pub growth_other_total_stockholders_equity: NumericString, - pub growth_total_stockholders_equity: NumericString, - pub growth_minority_interest: NumericString, - pub growth_total_equity: NumericString, - pub growth_total_liabilities_and_stockholders_equity: NumericString, - pub growth_total_investments: NumericString, - pub growth_total_debt: NumericString, - pub growth_net_debt: NumericString, - pub growth_accounts_receivables: NumericString, - pub growth_other_receivables: NumericString, - pub growth_prepaids: NumericString, - pub growth_total_payables: NumericString, - pub growth_other_payables: NumericString, - pub growth_accrued_expenses: NumericString, - pub growth_capital_lease_obligations_current: NumericString, - pub growth_additional_paid_in_capital: NumericString, - pub growth_treasury_stock: NumericString, + pub growth_other_total_stockholders_equity: Option, + pub growth_total_stockholders_equity: Option, + pub growth_minority_interest: Option, + pub growth_total_equity: Option, + pub growth_total_liabilities_and_stockholders_equity: Option, + pub growth_total_investments: Option, + pub growth_total_debt: Option, + pub growth_net_debt: Option, + pub growth_accounts_receivables: Option, + pub growth_other_receivables: Option, + pub growth_prepaids: Option, + pub growth_total_payables: Option, + pub growth_other_payables: Option, + pub growth_accrued_expenses: Option, + pub growth_capital_lease_obligations_current: Option, + pub growth_additional_paid_in_capital: Option, + pub growth_treasury_stock: Option, } diff --git a/crates/libfmp/tests/bulk_balance_responses.rs b/crates/libfmp/tests/bulk_balance_responses.rs index f7ac199a..192e3f99 100644 --- a/crates/libfmp/tests/bulk_balance_responses.rs +++ b/crates/libfmp/tests/bulk_balance_responses.rs @@ -10,7 +10,9 @@ use libfmp::{ query::{FiscalPeriod, Year}, }; -use bulk_csv::{assert_empty_bodies, assert_members, assert_round_trip, decode, first_row}; +use bulk_csv::{ + assert_empty_bodies, assert_required_members, assert_round_trip, decode, first_row, +}; const BALANCE: &[u8] = include_bytes!("fixtures/bulk_balance_sheet_statements.csv"); const GROWTH: &[u8] = include_bytes!("fixtures/bulk_balance_sheet_statement_growth.csv"); @@ -26,9 +28,26 @@ fn live_csv_fixtures_round_trip_every_cell_with_exact_field_counts() { } #[test] -fn every_column_is_required_and_no_member_accepts_an_empty_cell() { - assert_members(&bulk_balance_sheet_statements(query()), BALANCE, &[]); - assert_members(&bulk_balance_sheet_statement_growth(query()), GROWTH, &[]); +fn every_column_is_required_and_only_identity_members_reject_an_empty_cell() { + assert_required_members( + &bulk_balance_sheet_statements(query()), + BALANCE, + &[ + "date", + "symbol", + "reportedCurrency", + "cik", + "filingDate", + "acceptedDate", + "fiscalYear", + "period", + ], + ); + assert_required_members( + &bulk_balance_sheet_statement_growth(query()), + GROWTH, + &["symbol", "date", "fiscalYear", "period", "reportedCurrency"], + ); } #[test] @@ -45,7 +64,10 @@ fn identity_fields_are_narrow_and_preserve_provider_representations() { assert_eq!(balance[1].cik.as_str(), "0000000000"); assert_eq!(balance[1].accepted_date.to_string(), "2025-06-30 00:00:00"); assert_eq!(balance[1].fiscal_year.as_str(), "2024"); - assert_eq!(balance[0].total_assets.as_str(), "5769270000000"); + assert_eq!( + balance[0].total_assets.as_ref().unwrap().as_str(), + "5769270000000" + ); } #[test] diff --git a/sdk/go/bulk_models.go b/sdk/go/bulk_models.go index ed747d0d..2c707c84 100644 --- a/sdk/go/bulk_models.go +++ b/sdk/go/bulk_models.go @@ -20,59 +20,59 @@ type BulkBalanceSheetStatement struct { AcceptedDate DateTime `json:"acceptedDate"` FiscalYear string `json:"fiscalYear"` Period string `json:"period"` - CashAndCashEquivalents string `json:"cashAndCashEquivalents"` - ShortTermInvestments string `json:"shortTermInvestments"` - CashAndShortTermInvestments string `json:"cashAndShortTermInvestments"` - NetReceivables string `json:"netReceivables"` - AccountsReceivables string `json:"accountsReceivables"` - OtherReceivables string `json:"otherReceivables"` - Inventory string `json:"inventory"` - Prepaids string `json:"prepaids"` - OtherCurrentAssets string `json:"otherCurrentAssets"` - TotalCurrentAssets string `json:"totalCurrentAssets"` - PropertyPlantEquipmentNet string `json:"propertyPlantEquipmentNet"` - Goodwill string `json:"goodwill"` - IntangibleAssets string `json:"intangibleAssets"` - GoodwillAndIntangibleAssets string `json:"goodwillAndIntangibleAssets"` - LongTermInvestments string `json:"longTermInvestments"` - TaxAssets string `json:"taxAssets"` - OtherNonCurrentAssets string `json:"otherNonCurrentAssets"` - TotalNonCurrentAssets string `json:"totalNonCurrentAssets"` - OtherAssets string `json:"otherAssets"` - TotalAssets string `json:"totalAssets"` - TotalPayables string `json:"totalPayables"` - AccountPayables string `json:"accountPayables"` - OtherPayables string `json:"otherPayables"` - AccruedExpenses string `json:"accruedExpenses"` - ShortTermDebt string `json:"shortTermDebt"` - CapitalLeaseObligationsCurrent string `json:"capitalLeaseObligationsCurrent"` - TaxPayables string `json:"taxPayables"` - DeferredRevenue string `json:"deferredRevenue"` - OtherCurrentLiabilities string `json:"otherCurrentLiabilities"` - TotalCurrentLiabilities string `json:"totalCurrentLiabilities"` - LongTermDebt string `json:"longTermDebt"` - CapitalLeaseObligationsNonCurrent string `json:"capitalLeaseObligationsNonCurrent"` - DeferredRevenueNonCurrent string `json:"deferredRevenueNonCurrent"` - DeferredTaxLiabilitiesNonCurrent string `json:"deferredTaxLiabilitiesNonCurrent"` - OtherNonCurrentLiabilities string `json:"otherNonCurrentLiabilities"` - TotalNonCurrentLiabilities string `json:"totalNonCurrentLiabilities"` - OtherLiabilities string `json:"otherLiabilities"` - CapitalLeaseObligations string `json:"capitalLeaseObligations"` - TotalLiabilities string `json:"totalLiabilities"` - TreasuryStock string `json:"treasuryStock"` - PreferredStock string `json:"preferredStock"` - CommonStock string `json:"commonStock"` - RetainedEarnings string `json:"retainedEarnings"` - AdditionalPaidInCapital string `json:"additionalPaidInCapital"` - AccumulatedOtherComprehensiveIncomeLoss string `json:"accumulatedOtherComprehensiveIncomeLoss"` - OtherTotalStockholdersEquity string `json:"otherTotalStockholdersEquity"` - TotalStockholdersEquity string `json:"totalStockholdersEquity"` - TotalEquity string `json:"totalEquity"` - MinorityInterest string `json:"minorityInterest"` - TotalLiabilitiesAndTotalEquity string `json:"totalLiabilitiesAndTotalEquity"` - TotalInvestments string `json:"totalInvestments"` - TotalDebt string `json:"totalDebt"` - NetDebt string `json:"netDebt"` + CashAndCashEquivalents *string `json:"cashAndCashEquivalents"` + ShortTermInvestments *string `json:"shortTermInvestments"` + CashAndShortTermInvestments *string `json:"cashAndShortTermInvestments"` + NetReceivables *string `json:"netReceivables"` + AccountsReceivables *string `json:"accountsReceivables"` + OtherReceivables *string `json:"otherReceivables"` + Inventory *string `json:"inventory"` + Prepaids *string `json:"prepaids"` + OtherCurrentAssets *string `json:"otherCurrentAssets"` + TotalCurrentAssets *string `json:"totalCurrentAssets"` + PropertyPlantEquipmentNet *string `json:"propertyPlantEquipmentNet"` + Goodwill *string `json:"goodwill"` + IntangibleAssets *string `json:"intangibleAssets"` + GoodwillAndIntangibleAssets *string `json:"goodwillAndIntangibleAssets"` + LongTermInvestments *string `json:"longTermInvestments"` + TaxAssets *string `json:"taxAssets"` + OtherNonCurrentAssets *string `json:"otherNonCurrentAssets"` + TotalNonCurrentAssets *string `json:"totalNonCurrentAssets"` + OtherAssets *string `json:"otherAssets"` + TotalAssets *string `json:"totalAssets"` + TotalPayables *string `json:"totalPayables"` + AccountPayables *string `json:"accountPayables"` + OtherPayables *string `json:"otherPayables"` + AccruedExpenses *string `json:"accruedExpenses"` + ShortTermDebt *string `json:"shortTermDebt"` + CapitalLeaseObligationsCurrent *string `json:"capitalLeaseObligationsCurrent"` + TaxPayables *string `json:"taxPayables"` + DeferredRevenue *string `json:"deferredRevenue"` + OtherCurrentLiabilities *string `json:"otherCurrentLiabilities"` + TotalCurrentLiabilities *string `json:"totalCurrentLiabilities"` + LongTermDebt *string `json:"longTermDebt"` + CapitalLeaseObligationsNonCurrent *string `json:"capitalLeaseObligationsNonCurrent"` + DeferredRevenueNonCurrent *string `json:"deferredRevenueNonCurrent"` + DeferredTaxLiabilitiesNonCurrent *string `json:"deferredTaxLiabilitiesNonCurrent"` + OtherNonCurrentLiabilities *string `json:"otherNonCurrentLiabilities"` + TotalNonCurrentLiabilities *string `json:"totalNonCurrentLiabilities"` + OtherLiabilities *string `json:"otherLiabilities"` + CapitalLeaseObligations *string `json:"capitalLeaseObligations"` + TotalLiabilities *string `json:"totalLiabilities"` + TreasuryStock *string `json:"treasuryStock"` + PreferredStock *string `json:"preferredStock"` + CommonStock *string `json:"commonStock"` + RetainedEarnings *string `json:"retainedEarnings"` + AdditionalPaidInCapital *string `json:"additionalPaidInCapital"` + AccumulatedOtherComprehensiveIncomeLoss *string `json:"accumulatedOtherComprehensiveIncomeLoss"` + OtherTotalStockholdersEquity *string `json:"otherTotalStockholdersEquity"` + TotalStockholdersEquity *string `json:"totalStockholdersEquity"` + TotalEquity *string `json:"totalEquity"` + MinorityInterest *string `json:"minorityInterest"` + TotalLiabilitiesAndTotalEquity *string `json:"totalLiabilitiesAndTotalEquity"` + TotalInvestments *string `json:"totalInvestments"` + TotalDebt *string `json:"totalDebt"` + NetDebt *string `json:"netDebt"` } // bulkBalanceSheetStatementShadow mirrors BulkBalanceSheetStatement with a @@ -167,112 +167,6 @@ func (m *BulkBalanceSheetStatement) UnmarshalJSONFrom(dec *jsontext.Decoder) err return missingMemberError("BulkBalanceSheetStatement", "fiscalYear") case shadow.Period == nil: return missingMemberError("BulkBalanceSheetStatement", "period") - case shadow.CashAndCashEquivalents == nil: - return missingMemberError("BulkBalanceSheetStatement", "cashAndCashEquivalents") - case shadow.ShortTermInvestments == nil: - return missingMemberError("BulkBalanceSheetStatement", "shortTermInvestments") - case shadow.CashAndShortTermInvestments == nil: - return missingMemberError("BulkBalanceSheetStatement", "cashAndShortTermInvestments") - case shadow.NetReceivables == nil: - return missingMemberError("BulkBalanceSheetStatement", "netReceivables") - case shadow.AccountsReceivables == nil: - return missingMemberError("BulkBalanceSheetStatement", "accountsReceivables") - case shadow.OtherReceivables == nil: - return missingMemberError("BulkBalanceSheetStatement", "otherReceivables") - case shadow.Inventory == nil: - return missingMemberError("BulkBalanceSheetStatement", "inventory") - case shadow.Prepaids == nil: - return missingMemberError("BulkBalanceSheetStatement", "prepaids") - case shadow.OtherCurrentAssets == nil: - return missingMemberError("BulkBalanceSheetStatement", "otherCurrentAssets") - case shadow.TotalCurrentAssets == nil: - return missingMemberError("BulkBalanceSheetStatement", "totalCurrentAssets") - case shadow.PropertyPlantEquipmentNet == nil: - return missingMemberError("BulkBalanceSheetStatement", "propertyPlantEquipmentNet") - case shadow.Goodwill == nil: - return missingMemberError("BulkBalanceSheetStatement", "goodwill") - case shadow.IntangibleAssets == nil: - return missingMemberError("BulkBalanceSheetStatement", "intangibleAssets") - case shadow.GoodwillAndIntangibleAssets == nil: - return missingMemberError("BulkBalanceSheetStatement", "goodwillAndIntangibleAssets") - case shadow.LongTermInvestments == nil: - return missingMemberError("BulkBalanceSheetStatement", "longTermInvestments") - case shadow.TaxAssets == nil: - return missingMemberError("BulkBalanceSheetStatement", "taxAssets") - case shadow.OtherNonCurrentAssets == nil: - return missingMemberError("BulkBalanceSheetStatement", "otherNonCurrentAssets") - case shadow.TotalNonCurrentAssets == nil: - return missingMemberError("BulkBalanceSheetStatement", "totalNonCurrentAssets") - case shadow.OtherAssets == nil: - return missingMemberError("BulkBalanceSheetStatement", "otherAssets") - case shadow.TotalAssets == nil: - return missingMemberError("BulkBalanceSheetStatement", "totalAssets") - case shadow.TotalPayables == nil: - return missingMemberError("BulkBalanceSheetStatement", "totalPayables") - case shadow.AccountPayables == nil: - return missingMemberError("BulkBalanceSheetStatement", "accountPayables") - case shadow.OtherPayables == nil: - return missingMemberError("BulkBalanceSheetStatement", "otherPayables") - case shadow.AccruedExpenses == nil: - return missingMemberError("BulkBalanceSheetStatement", "accruedExpenses") - case shadow.ShortTermDebt == nil: - return missingMemberError("BulkBalanceSheetStatement", "shortTermDebt") - case shadow.CapitalLeaseObligationsCurrent == nil: - return missingMemberError("BulkBalanceSheetStatement", "capitalLeaseObligationsCurrent") - case shadow.TaxPayables == nil: - return missingMemberError("BulkBalanceSheetStatement", "taxPayables") - case shadow.DeferredRevenue == nil: - return missingMemberError("BulkBalanceSheetStatement", "deferredRevenue") - case shadow.OtherCurrentLiabilities == nil: - return missingMemberError("BulkBalanceSheetStatement", "otherCurrentLiabilities") - case shadow.TotalCurrentLiabilities == nil: - return missingMemberError("BulkBalanceSheetStatement", "totalCurrentLiabilities") - case shadow.LongTermDebt == nil: - return missingMemberError("BulkBalanceSheetStatement", "longTermDebt") - case shadow.CapitalLeaseObligationsNonCurrent == nil: - return missingMemberError("BulkBalanceSheetStatement", "capitalLeaseObligationsNonCurrent") - case shadow.DeferredRevenueNonCurrent == nil: - return missingMemberError("BulkBalanceSheetStatement", "deferredRevenueNonCurrent") - case shadow.DeferredTaxLiabilitiesNonCurrent == nil: - return missingMemberError("BulkBalanceSheetStatement", "deferredTaxLiabilitiesNonCurrent") - case shadow.OtherNonCurrentLiabilities == nil: - return missingMemberError("BulkBalanceSheetStatement", "otherNonCurrentLiabilities") - case shadow.TotalNonCurrentLiabilities == nil: - return missingMemberError("BulkBalanceSheetStatement", "totalNonCurrentLiabilities") - case shadow.OtherLiabilities == nil: - return missingMemberError("BulkBalanceSheetStatement", "otherLiabilities") - case shadow.CapitalLeaseObligations == nil: - return missingMemberError("BulkBalanceSheetStatement", "capitalLeaseObligations") - case shadow.TotalLiabilities == nil: - return missingMemberError("BulkBalanceSheetStatement", "totalLiabilities") - case shadow.TreasuryStock == nil: - return missingMemberError("BulkBalanceSheetStatement", "treasuryStock") - case shadow.PreferredStock == nil: - return missingMemberError("BulkBalanceSheetStatement", "preferredStock") - case shadow.CommonStock == nil: - return missingMemberError("BulkBalanceSheetStatement", "commonStock") - case shadow.RetainedEarnings == nil: - return missingMemberError("BulkBalanceSheetStatement", "retainedEarnings") - case shadow.AdditionalPaidInCapital == nil: - return missingMemberError("BulkBalanceSheetStatement", "additionalPaidInCapital") - case shadow.AccumulatedOtherComprehensiveIncomeLoss == nil: - return missingMemberError("BulkBalanceSheetStatement", "accumulatedOtherComprehensiveIncomeLoss") - case shadow.OtherTotalStockholdersEquity == nil: - return missingMemberError("BulkBalanceSheetStatement", "otherTotalStockholdersEquity") - case shadow.TotalStockholdersEquity == nil: - return missingMemberError("BulkBalanceSheetStatement", "totalStockholdersEquity") - case shadow.TotalEquity == nil: - return missingMemberError("BulkBalanceSheetStatement", "totalEquity") - case shadow.MinorityInterest == nil: - return missingMemberError("BulkBalanceSheetStatement", "minorityInterest") - case shadow.TotalLiabilitiesAndTotalEquity == nil: - return missingMemberError("BulkBalanceSheetStatement", "totalLiabilitiesAndTotalEquity") - case shadow.TotalInvestments == nil: - return missingMemberError("BulkBalanceSheetStatement", "totalInvestments") - case shadow.TotalDebt == nil: - return missingMemberError("BulkBalanceSheetStatement", "totalDebt") - case shadow.NetDebt == nil: - return missingMemberError("BulkBalanceSheetStatement", "netDebt") } *m = BulkBalanceSheetStatement{ Date: *shadow.Date, @@ -283,59 +177,59 @@ func (m *BulkBalanceSheetStatement) UnmarshalJSONFrom(dec *jsontext.Decoder) err AcceptedDate: *shadow.AcceptedDate, FiscalYear: *shadow.FiscalYear, Period: *shadow.Period, - CashAndCashEquivalents: *shadow.CashAndCashEquivalents, - ShortTermInvestments: *shadow.ShortTermInvestments, - CashAndShortTermInvestments: *shadow.CashAndShortTermInvestments, - NetReceivables: *shadow.NetReceivables, - AccountsReceivables: *shadow.AccountsReceivables, - OtherReceivables: *shadow.OtherReceivables, - Inventory: *shadow.Inventory, - Prepaids: *shadow.Prepaids, - OtherCurrentAssets: *shadow.OtherCurrentAssets, - TotalCurrentAssets: *shadow.TotalCurrentAssets, - PropertyPlantEquipmentNet: *shadow.PropertyPlantEquipmentNet, - Goodwill: *shadow.Goodwill, - IntangibleAssets: *shadow.IntangibleAssets, - GoodwillAndIntangibleAssets: *shadow.GoodwillAndIntangibleAssets, - LongTermInvestments: *shadow.LongTermInvestments, - TaxAssets: *shadow.TaxAssets, - OtherNonCurrentAssets: *shadow.OtherNonCurrentAssets, - TotalNonCurrentAssets: *shadow.TotalNonCurrentAssets, - OtherAssets: *shadow.OtherAssets, - TotalAssets: *shadow.TotalAssets, - TotalPayables: *shadow.TotalPayables, - AccountPayables: *shadow.AccountPayables, - OtherPayables: *shadow.OtherPayables, - AccruedExpenses: *shadow.AccruedExpenses, - ShortTermDebt: *shadow.ShortTermDebt, - CapitalLeaseObligationsCurrent: *shadow.CapitalLeaseObligationsCurrent, - TaxPayables: *shadow.TaxPayables, - DeferredRevenue: *shadow.DeferredRevenue, - OtherCurrentLiabilities: *shadow.OtherCurrentLiabilities, - TotalCurrentLiabilities: *shadow.TotalCurrentLiabilities, - LongTermDebt: *shadow.LongTermDebt, - CapitalLeaseObligationsNonCurrent: *shadow.CapitalLeaseObligationsNonCurrent, - DeferredRevenueNonCurrent: *shadow.DeferredRevenueNonCurrent, - DeferredTaxLiabilitiesNonCurrent: *shadow.DeferredTaxLiabilitiesNonCurrent, - OtherNonCurrentLiabilities: *shadow.OtherNonCurrentLiabilities, - TotalNonCurrentLiabilities: *shadow.TotalNonCurrentLiabilities, - OtherLiabilities: *shadow.OtherLiabilities, - CapitalLeaseObligations: *shadow.CapitalLeaseObligations, - TotalLiabilities: *shadow.TotalLiabilities, - TreasuryStock: *shadow.TreasuryStock, - PreferredStock: *shadow.PreferredStock, - CommonStock: *shadow.CommonStock, - RetainedEarnings: *shadow.RetainedEarnings, - AdditionalPaidInCapital: *shadow.AdditionalPaidInCapital, - AccumulatedOtherComprehensiveIncomeLoss: *shadow.AccumulatedOtherComprehensiveIncomeLoss, - OtherTotalStockholdersEquity: *shadow.OtherTotalStockholdersEquity, - TotalStockholdersEquity: *shadow.TotalStockholdersEquity, - TotalEquity: *shadow.TotalEquity, - MinorityInterest: *shadow.MinorityInterest, - TotalLiabilitiesAndTotalEquity: *shadow.TotalLiabilitiesAndTotalEquity, - TotalInvestments: *shadow.TotalInvestments, - TotalDebt: *shadow.TotalDebt, - NetDebt: *shadow.NetDebt, + CashAndCashEquivalents: shadow.CashAndCashEquivalents, + ShortTermInvestments: shadow.ShortTermInvestments, + CashAndShortTermInvestments: shadow.CashAndShortTermInvestments, + NetReceivables: shadow.NetReceivables, + AccountsReceivables: shadow.AccountsReceivables, + OtherReceivables: shadow.OtherReceivables, + Inventory: shadow.Inventory, + Prepaids: shadow.Prepaids, + OtherCurrentAssets: shadow.OtherCurrentAssets, + TotalCurrentAssets: shadow.TotalCurrentAssets, + PropertyPlantEquipmentNet: shadow.PropertyPlantEquipmentNet, + Goodwill: shadow.Goodwill, + IntangibleAssets: shadow.IntangibleAssets, + GoodwillAndIntangibleAssets: shadow.GoodwillAndIntangibleAssets, + LongTermInvestments: shadow.LongTermInvestments, + TaxAssets: shadow.TaxAssets, + OtherNonCurrentAssets: shadow.OtherNonCurrentAssets, + TotalNonCurrentAssets: shadow.TotalNonCurrentAssets, + OtherAssets: shadow.OtherAssets, + TotalAssets: shadow.TotalAssets, + TotalPayables: shadow.TotalPayables, + AccountPayables: shadow.AccountPayables, + OtherPayables: shadow.OtherPayables, + AccruedExpenses: shadow.AccruedExpenses, + ShortTermDebt: shadow.ShortTermDebt, + CapitalLeaseObligationsCurrent: shadow.CapitalLeaseObligationsCurrent, + TaxPayables: shadow.TaxPayables, + DeferredRevenue: shadow.DeferredRevenue, + OtherCurrentLiabilities: shadow.OtherCurrentLiabilities, + TotalCurrentLiabilities: shadow.TotalCurrentLiabilities, + LongTermDebt: shadow.LongTermDebt, + CapitalLeaseObligationsNonCurrent: shadow.CapitalLeaseObligationsNonCurrent, + DeferredRevenueNonCurrent: shadow.DeferredRevenueNonCurrent, + DeferredTaxLiabilitiesNonCurrent: shadow.DeferredTaxLiabilitiesNonCurrent, + OtherNonCurrentLiabilities: shadow.OtherNonCurrentLiabilities, + TotalNonCurrentLiabilities: shadow.TotalNonCurrentLiabilities, + OtherLiabilities: shadow.OtherLiabilities, + CapitalLeaseObligations: shadow.CapitalLeaseObligations, + TotalLiabilities: shadow.TotalLiabilities, + TreasuryStock: shadow.TreasuryStock, + PreferredStock: shadow.PreferredStock, + CommonStock: shadow.CommonStock, + RetainedEarnings: shadow.RetainedEarnings, + AdditionalPaidInCapital: shadow.AdditionalPaidInCapital, + AccumulatedOtherComprehensiveIncomeLoss: shadow.AccumulatedOtherComprehensiveIncomeLoss, + OtherTotalStockholdersEquity: shadow.OtherTotalStockholdersEquity, + TotalStockholdersEquity: shadow.TotalStockholdersEquity, + TotalEquity: shadow.TotalEquity, + MinorityInterest: shadow.MinorityInterest, + TotalLiabilitiesAndTotalEquity: shadow.TotalLiabilitiesAndTotalEquity, + TotalInvestments: shadow.TotalInvestments, + TotalDebt: shadow.TotalDebt, + NetDebt: shadow.NetDebt, } return nil } @@ -343,62 +237,62 @@ func (m *BulkBalanceSheetStatement) UnmarshalJSONFrom(dec *jsontext.Decoder) err // BulkBalanceSheetStatementGrowth is one worldwide bulk // balance-sheet-statement-growth row. type BulkBalanceSheetStatementGrowth struct { - Symbol string `json:"symbol"` - Date Date `json:"date"` - FiscalYear string `json:"fiscalYear"` - Period string `json:"period"` - ReportedCurrency string `json:"reportedCurrency"` - GrowthCashAndCashEquivalents string `json:"growthCashAndCashEquivalents"` - GrowthShortTermInvestments string `json:"growthShortTermInvestments"` - GrowthCashAndShortTermInvestments string `json:"growthCashAndShortTermInvestments"` - GrowthNetReceivables string `json:"growthNetReceivables"` - GrowthInventory string `json:"growthInventory"` - GrowthOtherCurrentAssets string `json:"growthOtherCurrentAssets"` - GrowthTotalCurrentAssets string `json:"growthTotalCurrentAssets"` - GrowthPropertyPlantEquipmentNet string `json:"growthPropertyPlantEquipmentNet"` - GrowthGoodwill string `json:"growthGoodwill"` - GrowthIntangibleAssets string `json:"growthIntangibleAssets"` - GrowthGoodwillAndIntangibleAssets string `json:"growthGoodwillAndIntangibleAssets"` - GrowthLongTermInvestments string `json:"growthLongTermInvestments"` - GrowthTaxAssets string `json:"growthTaxAssets"` - GrowthOtherNonCurrentAssets string `json:"growthOtherNonCurrentAssets"` - GrowthTotalNonCurrentAssets string `json:"growthTotalNonCurrentAssets"` - GrowthOtherAssets string `json:"growthOtherAssets"` - GrowthTotalAssets string `json:"growthTotalAssets"` - GrowthAccountPayables string `json:"growthAccountPayables"` - GrowthShortTermDebt string `json:"growthShortTermDebt"` - GrowthTaxPayables string `json:"growthTaxPayables"` - GrowthDeferredRevenue string `json:"growthDeferredRevenue"` - GrowthOtherCurrentLiabilities string `json:"growthOtherCurrentLiabilities"` - GrowthTotalCurrentLiabilities string `json:"growthTotalCurrentLiabilities"` - GrowthLongTermDebt string `json:"growthLongTermDebt"` - GrowthDeferredRevenueNonCurrent string `json:"growthDeferredRevenueNonCurrent"` - GrowthDeferredTaxLiabilitiesNonCurrent string `json:"growthDeferredTaxLiabilitiesNonCurrent"` - GrowthOtherNonCurrentLiabilities string `json:"growthOtherNonCurrentLiabilities"` - GrowthTotalNonCurrentLiabilities string `json:"growthTotalNonCurrentLiabilities"` - GrowthOtherLiabilities string `json:"growthOtherLiabilities"` - GrowthTotalLiabilities string `json:"growthTotalLiabilities"` - GrowthPreferredStock string `json:"growthPreferredStock"` - GrowthCommonStock string `json:"growthCommonStock"` - GrowthRetainedEarnings string `json:"growthRetainedEarnings"` - GrowthAccumulatedOtherComprehensiveIncomeLoss string `json:"growthAccumulatedOtherComprehensiveIncomeLoss"` - GrowthOtherTotalStockholdersEquity string `json:"growthOthertotalStockholdersEquity"` - GrowthTotalStockholdersEquity string `json:"growthTotalStockholdersEquity"` - GrowthMinorityInterest string `json:"growthMinorityInterest"` - GrowthTotalEquity string `json:"growthTotalEquity"` - GrowthTotalLiabilitiesAndStockholdersEquity string `json:"growthTotalLiabilitiesAndStockholdersEquity"` - GrowthTotalInvestments string `json:"growthTotalInvestments"` - GrowthTotalDebt string `json:"growthTotalDebt"` - GrowthNetDebt string `json:"growthNetDebt"` - GrowthAccountsReceivables string `json:"growthAccountsReceivables"` - GrowthOtherReceivables string `json:"growthOtherReceivables"` - GrowthPrepaids string `json:"growthPrepaids"` - GrowthTotalPayables string `json:"growthTotalPayables"` - GrowthOtherPayables string `json:"growthOtherPayables"` - GrowthAccruedExpenses string `json:"growthAccruedExpenses"` - GrowthCapitalLeaseObligationsCurrent string `json:"growthCapitalLeaseObligationsCurrent"` - GrowthAdditionalPaidInCapital string `json:"growthAdditionalPaidInCapital"` - GrowthTreasuryStock string `json:"growthTreasuryStock"` + Symbol string `json:"symbol"` + Date Date `json:"date"` + FiscalYear string `json:"fiscalYear"` + Period string `json:"period"` + ReportedCurrency string `json:"reportedCurrency"` + GrowthCashAndCashEquivalents *string `json:"growthCashAndCashEquivalents"` + GrowthShortTermInvestments *string `json:"growthShortTermInvestments"` + GrowthCashAndShortTermInvestments *string `json:"growthCashAndShortTermInvestments"` + GrowthNetReceivables *string `json:"growthNetReceivables"` + GrowthInventory *string `json:"growthInventory"` + GrowthOtherCurrentAssets *string `json:"growthOtherCurrentAssets"` + GrowthTotalCurrentAssets *string `json:"growthTotalCurrentAssets"` + GrowthPropertyPlantEquipmentNet *string `json:"growthPropertyPlantEquipmentNet"` + GrowthGoodwill *string `json:"growthGoodwill"` + GrowthIntangibleAssets *string `json:"growthIntangibleAssets"` + GrowthGoodwillAndIntangibleAssets *string `json:"growthGoodwillAndIntangibleAssets"` + GrowthLongTermInvestments *string `json:"growthLongTermInvestments"` + GrowthTaxAssets *string `json:"growthTaxAssets"` + GrowthOtherNonCurrentAssets *string `json:"growthOtherNonCurrentAssets"` + GrowthTotalNonCurrentAssets *string `json:"growthTotalNonCurrentAssets"` + GrowthOtherAssets *string `json:"growthOtherAssets"` + GrowthTotalAssets *string `json:"growthTotalAssets"` + GrowthAccountPayables *string `json:"growthAccountPayables"` + GrowthShortTermDebt *string `json:"growthShortTermDebt"` + GrowthTaxPayables *string `json:"growthTaxPayables"` + GrowthDeferredRevenue *string `json:"growthDeferredRevenue"` + GrowthOtherCurrentLiabilities *string `json:"growthOtherCurrentLiabilities"` + GrowthTotalCurrentLiabilities *string `json:"growthTotalCurrentLiabilities"` + GrowthLongTermDebt *string `json:"growthLongTermDebt"` + GrowthDeferredRevenueNonCurrent *string `json:"growthDeferredRevenueNonCurrent"` + GrowthDeferredTaxLiabilitiesNonCurrent *string `json:"growthDeferredTaxLiabilitiesNonCurrent"` + GrowthOtherNonCurrentLiabilities *string `json:"growthOtherNonCurrentLiabilities"` + GrowthTotalNonCurrentLiabilities *string `json:"growthTotalNonCurrentLiabilities"` + GrowthOtherLiabilities *string `json:"growthOtherLiabilities"` + GrowthTotalLiabilities *string `json:"growthTotalLiabilities"` + GrowthPreferredStock *string `json:"growthPreferredStock"` + GrowthCommonStock *string `json:"growthCommonStock"` + GrowthRetainedEarnings *string `json:"growthRetainedEarnings"` + GrowthAccumulatedOtherComprehensiveIncomeLoss *string `json:"growthAccumulatedOtherComprehensiveIncomeLoss"` + GrowthOtherTotalStockholdersEquity *string `json:"growthOthertotalStockholdersEquity"` + GrowthTotalStockholdersEquity *string `json:"growthTotalStockholdersEquity"` + GrowthMinorityInterest *string `json:"growthMinorityInterest"` + GrowthTotalEquity *string `json:"growthTotalEquity"` + GrowthTotalLiabilitiesAndStockholdersEquity *string `json:"growthTotalLiabilitiesAndStockholdersEquity"` + GrowthTotalInvestments *string `json:"growthTotalInvestments"` + GrowthTotalDebt *string `json:"growthTotalDebt"` + GrowthNetDebt *string `json:"growthNetDebt"` + GrowthAccountsReceivables *string `json:"growthAccountsReceivables"` + GrowthOtherReceivables *string `json:"growthOtherReceivables"` + GrowthPrepaids *string `json:"growthPrepaids"` + GrowthTotalPayables *string `json:"growthTotalPayables"` + GrowthOtherPayables *string `json:"growthOtherPayables"` + GrowthAccruedExpenses *string `json:"growthAccruedExpenses"` + GrowthCapitalLeaseObligationsCurrent *string `json:"growthCapitalLeaseObligationsCurrent"` + GrowthAdditionalPaidInCapital *string `json:"growthAdditionalPaidInCapital"` + GrowthTreasuryStock *string `json:"growthTreasuryStock"` } // bulkBalanceSheetStatementGrowthShadow mirrors @@ -482,108 +376,6 @@ func (m *BulkBalanceSheetStatementGrowth) UnmarshalJSONFrom(dec *jsontext.Decode return missingMemberError("BulkBalanceSheetStatementGrowth", "period") case shadow.ReportedCurrency == nil: return missingMemberError("BulkBalanceSheetStatementGrowth", "reportedCurrency") - case shadow.GrowthCashAndCashEquivalents == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthCashAndCashEquivalents") - case shadow.GrowthShortTermInvestments == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthShortTermInvestments") - case shadow.GrowthCashAndShortTermInvestments == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthCashAndShortTermInvestments") - case shadow.GrowthNetReceivables == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthNetReceivables") - case shadow.GrowthInventory == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthInventory") - case shadow.GrowthOtherCurrentAssets == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthOtherCurrentAssets") - case shadow.GrowthTotalCurrentAssets == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTotalCurrentAssets") - case shadow.GrowthPropertyPlantEquipmentNet == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthPropertyPlantEquipmentNet") - case shadow.GrowthGoodwill == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthGoodwill") - case shadow.GrowthIntangibleAssets == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthIntangibleAssets") - case shadow.GrowthGoodwillAndIntangibleAssets == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthGoodwillAndIntangibleAssets") - case shadow.GrowthLongTermInvestments == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthLongTermInvestments") - case shadow.GrowthTaxAssets == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTaxAssets") - case shadow.GrowthOtherNonCurrentAssets == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthOtherNonCurrentAssets") - case shadow.GrowthTotalNonCurrentAssets == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTotalNonCurrentAssets") - case shadow.GrowthOtherAssets == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthOtherAssets") - case shadow.GrowthTotalAssets == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTotalAssets") - case shadow.GrowthAccountPayables == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthAccountPayables") - case shadow.GrowthShortTermDebt == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthShortTermDebt") - case shadow.GrowthTaxPayables == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTaxPayables") - case shadow.GrowthDeferredRevenue == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthDeferredRevenue") - case shadow.GrowthOtherCurrentLiabilities == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthOtherCurrentLiabilities") - case shadow.GrowthTotalCurrentLiabilities == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTotalCurrentLiabilities") - case shadow.GrowthLongTermDebt == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthLongTermDebt") - case shadow.GrowthDeferredRevenueNonCurrent == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthDeferredRevenueNonCurrent") - case shadow.GrowthDeferredTaxLiabilitiesNonCurrent == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthDeferredTaxLiabilitiesNonCurrent") - case shadow.GrowthOtherNonCurrentLiabilities == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthOtherNonCurrentLiabilities") - case shadow.GrowthTotalNonCurrentLiabilities == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTotalNonCurrentLiabilities") - case shadow.GrowthOtherLiabilities == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthOtherLiabilities") - case shadow.GrowthTotalLiabilities == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTotalLiabilities") - case shadow.GrowthPreferredStock == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthPreferredStock") - case shadow.GrowthCommonStock == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthCommonStock") - case shadow.GrowthRetainedEarnings == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthRetainedEarnings") - case shadow.GrowthAccumulatedOtherComprehensiveIncomeLoss == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthAccumulatedOtherComprehensiveIncomeLoss") - case shadow.GrowthOtherTotalStockholdersEquity == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthOthertotalStockholdersEquity") - case shadow.GrowthTotalStockholdersEquity == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTotalStockholdersEquity") - case shadow.GrowthMinorityInterest == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthMinorityInterest") - case shadow.GrowthTotalEquity == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTotalEquity") - case shadow.GrowthTotalLiabilitiesAndStockholdersEquity == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTotalLiabilitiesAndStockholdersEquity") - case shadow.GrowthTotalInvestments == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTotalInvestments") - case shadow.GrowthTotalDebt == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTotalDebt") - case shadow.GrowthNetDebt == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthNetDebt") - case shadow.GrowthAccountsReceivables == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthAccountsReceivables") - case shadow.GrowthOtherReceivables == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthOtherReceivables") - case shadow.GrowthPrepaids == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthPrepaids") - case shadow.GrowthTotalPayables == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTotalPayables") - case shadow.GrowthOtherPayables == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthOtherPayables") - case shadow.GrowthAccruedExpenses == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthAccruedExpenses") - case shadow.GrowthCapitalLeaseObligationsCurrent == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthCapitalLeaseObligationsCurrent") - case shadow.GrowthAdditionalPaidInCapital == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthAdditionalPaidInCapital") - case shadow.GrowthTreasuryStock == nil: - return missingMemberError("BulkBalanceSheetStatementGrowth", "growthTreasuryStock") } *m = BulkBalanceSheetStatementGrowth{ Symbol: *shadow.Symbol, @@ -591,57 +383,57 @@ func (m *BulkBalanceSheetStatementGrowth) UnmarshalJSONFrom(dec *jsontext.Decode FiscalYear: *shadow.FiscalYear, Period: *shadow.Period, ReportedCurrency: *shadow.ReportedCurrency, - GrowthCashAndCashEquivalents: *shadow.GrowthCashAndCashEquivalents, - GrowthShortTermInvestments: *shadow.GrowthShortTermInvestments, - GrowthCashAndShortTermInvestments: *shadow.GrowthCashAndShortTermInvestments, - GrowthNetReceivables: *shadow.GrowthNetReceivables, - GrowthInventory: *shadow.GrowthInventory, - GrowthOtherCurrentAssets: *shadow.GrowthOtherCurrentAssets, - GrowthTotalCurrentAssets: *shadow.GrowthTotalCurrentAssets, - GrowthPropertyPlantEquipmentNet: *shadow.GrowthPropertyPlantEquipmentNet, - GrowthGoodwill: *shadow.GrowthGoodwill, - GrowthIntangibleAssets: *shadow.GrowthIntangibleAssets, - GrowthGoodwillAndIntangibleAssets: *shadow.GrowthGoodwillAndIntangibleAssets, - GrowthLongTermInvestments: *shadow.GrowthLongTermInvestments, - GrowthTaxAssets: *shadow.GrowthTaxAssets, - GrowthOtherNonCurrentAssets: *shadow.GrowthOtherNonCurrentAssets, - GrowthTotalNonCurrentAssets: *shadow.GrowthTotalNonCurrentAssets, - GrowthOtherAssets: *shadow.GrowthOtherAssets, - GrowthTotalAssets: *shadow.GrowthTotalAssets, - GrowthAccountPayables: *shadow.GrowthAccountPayables, - GrowthShortTermDebt: *shadow.GrowthShortTermDebt, - GrowthTaxPayables: *shadow.GrowthTaxPayables, - GrowthDeferredRevenue: *shadow.GrowthDeferredRevenue, - GrowthOtherCurrentLiabilities: *shadow.GrowthOtherCurrentLiabilities, - GrowthTotalCurrentLiabilities: *shadow.GrowthTotalCurrentLiabilities, - GrowthLongTermDebt: *shadow.GrowthLongTermDebt, - GrowthDeferredRevenueNonCurrent: *shadow.GrowthDeferredRevenueNonCurrent, - GrowthDeferredTaxLiabilitiesNonCurrent: *shadow.GrowthDeferredTaxLiabilitiesNonCurrent, - GrowthOtherNonCurrentLiabilities: *shadow.GrowthOtherNonCurrentLiabilities, - GrowthTotalNonCurrentLiabilities: *shadow.GrowthTotalNonCurrentLiabilities, - GrowthOtherLiabilities: *shadow.GrowthOtherLiabilities, - GrowthTotalLiabilities: *shadow.GrowthTotalLiabilities, - GrowthPreferredStock: *shadow.GrowthPreferredStock, - GrowthCommonStock: *shadow.GrowthCommonStock, - GrowthRetainedEarnings: *shadow.GrowthRetainedEarnings, - GrowthAccumulatedOtherComprehensiveIncomeLoss: *shadow.GrowthAccumulatedOtherComprehensiveIncomeLoss, - GrowthOtherTotalStockholdersEquity: *shadow.GrowthOtherTotalStockholdersEquity, - GrowthTotalStockholdersEquity: *shadow.GrowthTotalStockholdersEquity, - GrowthMinorityInterest: *shadow.GrowthMinorityInterest, - GrowthTotalEquity: *shadow.GrowthTotalEquity, - GrowthTotalLiabilitiesAndStockholdersEquity: *shadow.GrowthTotalLiabilitiesAndStockholdersEquity, - GrowthTotalInvestments: *shadow.GrowthTotalInvestments, - GrowthTotalDebt: *shadow.GrowthTotalDebt, - GrowthNetDebt: *shadow.GrowthNetDebt, - GrowthAccountsReceivables: *shadow.GrowthAccountsReceivables, - GrowthOtherReceivables: *shadow.GrowthOtherReceivables, - GrowthPrepaids: *shadow.GrowthPrepaids, - GrowthTotalPayables: *shadow.GrowthTotalPayables, - GrowthOtherPayables: *shadow.GrowthOtherPayables, - GrowthAccruedExpenses: *shadow.GrowthAccruedExpenses, - GrowthCapitalLeaseObligationsCurrent: *shadow.GrowthCapitalLeaseObligationsCurrent, - GrowthAdditionalPaidInCapital: *shadow.GrowthAdditionalPaidInCapital, - GrowthTreasuryStock: *shadow.GrowthTreasuryStock, + GrowthCashAndCashEquivalents: shadow.GrowthCashAndCashEquivalents, + GrowthShortTermInvestments: shadow.GrowthShortTermInvestments, + GrowthCashAndShortTermInvestments: shadow.GrowthCashAndShortTermInvestments, + GrowthNetReceivables: shadow.GrowthNetReceivables, + GrowthInventory: shadow.GrowthInventory, + GrowthOtherCurrentAssets: shadow.GrowthOtherCurrentAssets, + GrowthTotalCurrentAssets: shadow.GrowthTotalCurrentAssets, + GrowthPropertyPlantEquipmentNet: shadow.GrowthPropertyPlantEquipmentNet, + GrowthGoodwill: shadow.GrowthGoodwill, + GrowthIntangibleAssets: shadow.GrowthIntangibleAssets, + GrowthGoodwillAndIntangibleAssets: shadow.GrowthGoodwillAndIntangibleAssets, + GrowthLongTermInvestments: shadow.GrowthLongTermInvestments, + GrowthTaxAssets: shadow.GrowthTaxAssets, + GrowthOtherNonCurrentAssets: shadow.GrowthOtherNonCurrentAssets, + GrowthTotalNonCurrentAssets: shadow.GrowthTotalNonCurrentAssets, + GrowthOtherAssets: shadow.GrowthOtherAssets, + GrowthTotalAssets: shadow.GrowthTotalAssets, + GrowthAccountPayables: shadow.GrowthAccountPayables, + GrowthShortTermDebt: shadow.GrowthShortTermDebt, + GrowthTaxPayables: shadow.GrowthTaxPayables, + GrowthDeferredRevenue: shadow.GrowthDeferredRevenue, + GrowthOtherCurrentLiabilities: shadow.GrowthOtherCurrentLiabilities, + GrowthTotalCurrentLiabilities: shadow.GrowthTotalCurrentLiabilities, + GrowthLongTermDebt: shadow.GrowthLongTermDebt, + GrowthDeferredRevenueNonCurrent: shadow.GrowthDeferredRevenueNonCurrent, + GrowthDeferredTaxLiabilitiesNonCurrent: shadow.GrowthDeferredTaxLiabilitiesNonCurrent, + GrowthOtherNonCurrentLiabilities: shadow.GrowthOtherNonCurrentLiabilities, + GrowthTotalNonCurrentLiabilities: shadow.GrowthTotalNonCurrentLiabilities, + GrowthOtherLiabilities: shadow.GrowthOtherLiabilities, + GrowthTotalLiabilities: shadow.GrowthTotalLiabilities, + GrowthPreferredStock: shadow.GrowthPreferredStock, + GrowthCommonStock: shadow.GrowthCommonStock, + GrowthRetainedEarnings: shadow.GrowthRetainedEarnings, + GrowthAccumulatedOtherComprehensiveIncomeLoss: shadow.GrowthAccumulatedOtherComprehensiveIncomeLoss, + GrowthOtherTotalStockholdersEquity: shadow.GrowthOtherTotalStockholdersEquity, + GrowthTotalStockholdersEquity: shadow.GrowthTotalStockholdersEquity, + GrowthMinorityInterest: shadow.GrowthMinorityInterest, + GrowthTotalEquity: shadow.GrowthTotalEquity, + GrowthTotalLiabilitiesAndStockholdersEquity: shadow.GrowthTotalLiabilitiesAndStockholdersEquity, + GrowthTotalInvestments: shadow.GrowthTotalInvestments, + GrowthTotalDebt: shadow.GrowthTotalDebt, + GrowthNetDebt: shadow.GrowthNetDebt, + GrowthAccountsReceivables: shadow.GrowthAccountsReceivables, + GrowthOtherReceivables: shadow.GrowthOtherReceivables, + GrowthPrepaids: shadow.GrowthPrepaids, + GrowthTotalPayables: shadow.GrowthTotalPayables, + GrowthOtherPayables: shadow.GrowthOtherPayables, + GrowthAccruedExpenses: shadow.GrowthAccruedExpenses, + GrowthCapitalLeaseObligationsCurrent: shadow.GrowthCapitalLeaseObligationsCurrent, + GrowthAdditionalPaidInCapital: shadow.GrowthAdditionalPaidInCapital, + GrowthTreasuryStock: shadow.GrowthTreasuryStock, } return nil } diff --git a/sdk/go/bulk_parity_test.go b/sdk/go/bulk_parity_test.go index 5f7a29b7..3d70c681 100644 --- a/sdk/go/bulk_parity_test.go +++ b/sdk/go/bulk_parity_test.go @@ -159,7 +159,7 @@ func TestBulkStatementsDecodeExactValuesAndKeepProviderTypos(t *testing.T) { balance := assertCSVFixtureParity[BulkBalanceSheetStatement](t, "bulk_balance_sheet_statements.csv", 61) if balance[1].Symbol != "0002.KL" || balance[1].ReportedCurrency != "MYR" || - balance[1].AcceptedDate != mustParseDateTime(t, "2025-06-30 00:00:00") || balance[0].TotalAssets != "5769270000000" { + balance[1].AcceptedDate != mustParseDateTime(t, "2025-06-30 00:00:00") || cellText(balance[0].TotalAssets) != "5769270000000" { t.Fatalf("bulk_balance_sheet_statements = %+v", balance) } balanceGrowth := assertCSVFixtureParity[BulkBalanceSheetStatementGrowth](t, "bulk_balance_sheet_statement_growth.csv", 56) diff --git a/sdk/go/bulk_test.go b/sdk/go/bulk_test.go index 3301f223..cd1ecc2f 100644 --- a/sdk/go/bulk_test.go +++ b/sdk/go/bulk_test.go @@ -114,7 +114,7 @@ func TestBulkMethodsUseExactPathsAndWireParameterOrder(t *testing.T) { t.Fatalf("BalanceSheetStatements = %+v, %v", balance, err) } balanceGrowth, err := client.Bulk.BalanceSheetStatementGrowth(ctx, NewBulkStatementQuery(2026, FiscalPeriodQ4)) - if err != nil || len(balanceGrowth) != 2 || balanceGrowth[0].GrowthShortTermInvestments != "0.3981363245708905" { + if err != nil || len(balanceGrowth) != 2 || cellText(balanceGrowth[0].GrowthShortTermInvestments) != "0.3981363245708905" { t.Fatalf("BalanceSheetStatementGrowth = %+v, %v", balanceGrowth, err) } cashFlow, err := client.Bulk.CashFlowStatements(ctx, NewBulkStatementQuery(4_294_967_295, FiscalPeriodFullYear)) From f7e5a54483dd3a669b9f47bac616ca6c79675c2e Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 11:27:21 +0800 Subject: [PATCH 26/31] fix(bulk)!: make numeric cash-flow and EOD members optional Under ADR 0035 an empty CSV cell is the only absence marker for bulk rows, and sparse empties vary by period, so every numeric metric member becomes optional while identity members stay required. This commit covers the cash-flow and end-of-day rows: every NumericString member of BulkCashFlowStatement, BulkCashFlowStatementGrowth and BulkEodBar becomes Option (Python Optional[str], Go *string). Identity members (symbol, date, and the statement identity columns such as reportedCurrency, cik, filingDate, acceptedDate, fiscalYear and period) stay required. The Go decoders drop the missing-member guards for the now-optional members, and the libfmp tests assert that only identity members reject an empty cell. Regenerated Python models/stubs and Go models are included. BREAKING CHANGE: every numeric member of BulkCashFlowStatement, BulkCashFlowStatementGrowth and BulkEodBar is now optional in Rust, Python and Go. --- .../fmp/_native/bulk/cash_flow/__init__.pyi | 328 ++++++----- .../python/fmp/_native/bulk/eod/__init__.pyi | 24 +- crates/fmp-py/src/models/bulk/cash_flow.rs | 528 ++++++++++-------- crates/fmp-py/src/models/bulk/eod.rs | 36 +- crates/libfmp/src/responses/bulk/cash_flow.rs | 152 ++--- crates/libfmp/src/responses/bulk/eod.rs | 12 +- .../libfmp/tests/bulk_cash_eod_responses.rs | 35 +- sdk/go/bulk_models.go | 506 ++++++----------- sdk/go/bulk_parity_test.go | 6 +- sdk/go/bulk_test.go | 6 +- 10 files changed, 792 insertions(+), 841 deletions(-) diff --git a/crates/fmp-py/python/fmp/_native/bulk/cash_flow/__init__.pyi b/crates/fmp-py/python/fmp/_native/bulk/cash_flow/__init__.pyi index 13cb0a56..a83ff1e1 100644 --- a/crates/fmp-py/python/fmp/_native/bulk/cash_flow/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/bulk/cash_flow/__init__.pyi @@ -78,83 +78,91 @@ class BulkCashFlowStatement: @property def fiscal_year(self) -> builtins.str: ... @property - def net_income(self) -> builtins.str: ... + def net_income(self) -> typing.Optional[builtins.str]: ... @property - def depreciation_and_amortization(self) -> builtins.str: ... + def depreciation_and_amortization(self) -> typing.Optional[builtins.str]: ... @property - def deferred_income_tax(self) -> builtins.str: ... + def deferred_income_tax(self) -> typing.Optional[builtins.str]: ... @property - def stock_based_compensation(self) -> builtins.str: ... + def stock_based_compensation(self) -> typing.Optional[builtins.str]: ... @property - def change_in_working_capital(self) -> builtins.str: ... + def change_in_working_capital(self) -> typing.Optional[builtins.str]: ... @property - def accounts_receivables(self) -> builtins.str: ... + def accounts_receivables(self) -> typing.Optional[builtins.str]: ... @property - def inventory(self) -> builtins.str: ... + def inventory(self) -> typing.Optional[builtins.str]: ... @property - def accounts_payables(self) -> builtins.str: ... + def accounts_payables(self) -> typing.Optional[builtins.str]: ... @property - def other_working_capital(self) -> builtins.str: ... + def other_working_capital(self) -> typing.Optional[builtins.str]: ... @property - def other_non_cash_items(self) -> builtins.str: ... + def other_non_cash_items(self) -> typing.Optional[builtins.str]: ... @property - def net_cash_provided_by_operating_activities(self) -> builtins.str: ... + def net_cash_provided_by_operating_activities( + self, + ) -> typing.Optional[builtins.str]: ... @property - def investments_in_property_plant_and_equipment(self) -> builtins.str: ... + def investments_in_property_plant_and_equipment( + self, + ) -> typing.Optional[builtins.str]: ... @property - def acquisitions_net(self) -> builtins.str: ... + def acquisitions_net(self) -> typing.Optional[builtins.str]: ... @property - def purchases_of_investments(self) -> builtins.str: ... + def purchases_of_investments(self) -> typing.Optional[builtins.str]: ... @property - def sales_maturities_of_investments(self) -> builtins.str: ... + def sales_maturities_of_investments(self) -> typing.Optional[builtins.str]: ... @property - def other_investing_activities(self) -> builtins.str: ... + def other_investing_activities(self) -> typing.Optional[builtins.str]: ... @property - def net_cash_provided_by_investing_activities(self) -> builtins.str: ... + def net_cash_provided_by_investing_activities( + self, + ) -> typing.Optional[builtins.str]: ... @property - def net_debt_issuance(self) -> builtins.str: ... + def net_debt_issuance(self) -> typing.Optional[builtins.str]: ... @property - def long_term_net_debt_issuance(self) -> builtins.str: ... + def long_term_net_debt_issuance(self) -> typing.Optional[builtins.str]: ... @property - def short_term_net_debt_issuance(self) -> builtins.str: ... + def short_term_net_debt_issuance(self) -> typing.Optional[builtins.str]: ... @property - def net_stock_issuance(self) -> builtins.str: ... + def net_stock_issuance(self) -> typing.Optional[builtins.str]: ... @property - def net_common_stock_issuance(self) -> builtins.str: ... + def net_common_stock_issuance(self) -> typing.Optional[builtins.str]: ... @property - def common_stock_issuance(self) -> builtins.str: ... + def common_stock_issuance(self) -> typing.Optional[builtins.str]: ... @property - def common_stock_repurchased(self) -> builtins.str: ... + def common_stock_repurchased(self) -> typing.Optional[builtins.str]: ... @property - def net_preferred_stock_issuance(self) -> builtins.str: ... + def net_preferred_stock_issuance(self) -> typing.Optional[builtins.str]: ... @property - def net_dividends_paid(self) -> builtins.str: ... + def net_dividends_paid(self) -> typing.Optional[builtins.str]: ... @property - def common_dividends_paid(self) -> builtins.str: ... + def common_dividends_paid(self) -> typing.Optional[builtins.str]: ... @property - def preferred_dividends_paid(self) -> builtins.str: ... + def preferred_dividends_paid(self) -> typing.Optional[builtins.str]: ... @property - def other_financing_activities(self) -> builtins.str: ... + def other_financing_activities(self) -> typing.Optional[builtins.str]: ... @property - def net_cash_provided_by_financing_activities(self) -> builtins.str: ... + def net_cash_provided_by_financing_activities( + self, + ) -> typing.Optional[builtins.str]: ... @property - def effect_of_forex_changes_on_cash(self) -> builtins.str: ... + def effect_of_forex_changes_on_cash(self) -> typing.Optional[builtins.str]: ... @property - def net_change_in_cash(self) -> builtins.str: ... + def net_change_in_cash(self) -> typing.Optional[builtins.str]: ... @property - def cash_at_end_of_period(self) -> builtins.str: ... + def cash_at_end_of_period(self) -> typing.Optional[builtins.str]: ... @property - def cash_at_beginning_of_period(self) -> builtins.str: ... + def cash_at_beginning_of_period(self) -> typing.Optional[builtins.str]: ... @property - def operating_cash_flow(self) -> builtins.str: ... + def operating_cash_flow(self) -> typing.Optional[builtins.str]: ... @property - def capital_expenditure(self) -> builtins.str: ... + def capital_expenditure(self) -> typing.Optional[builtins.str]: ... @property - def free_cash_flow(self) -> builtins.str: ... + def free_cash_flow(self) -> typing.Optional[builtins.str]: ... @property - def income_taxes_paid(self) -> builtins.str: ... + def income_taxes_paid(self) -> typing.Optional[builtins.str]: ... @property - def interest_paid(self) -> builtins.str: ... + def interest_paid(self) -> typing.Optional[builtins.str]: ... @property def period(self) -> typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... @@ -169,45 +177,45 @@ class BulkCashFlowStatement: accepted_date: datetime.datetime, fiscal_year: builtins.str, period: typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"], - net_income: builtins.str, - depreciation_and_amortization: builtins.str, - deferred_income_tax: builtins.str, - stock_based_compensation: builtins.str, - change_in_working_capital: builtins.str, - accounts_receivables: builtins.str, - inventory: builtins.str, - accounts_payables: builtins.str, - other_working_capital: builtins.str, - other_non_cash_items: builtins.str, - net_cash_provided_by_operating_activities: builtins.str, - investments_in_property_plant_and_equipment: builtins.str, - acquisitions_net: builtins.str, - purchases_of_investments: builtins.str, - sales_maturities_of_investments: builtins.str, - other_investing_activities: builtins.str, - net_cash_provided_by_investing_activities: builtins.str, - net_debt_issuance: builtins.str, - long_term_net_debt_issuance: builtins.str, - short_term_net_debt_issuance: builtins.str, - net_stock_issuance: builtins.str, - net_common_stock_issuance: builtins.str, - common_stock_issuance: builtins.str, - common_stock_repurchased: builtins.str, - net_preferred_stock_issuance: builtins.str, - net_dividends_paid: builtins.str, - common_dividends_paid: builtins.str, - preferred_dividends_paid: builtins.str, - other_financing_activities: builtins.str, - net_cash_provided_by_financing_activities: builtins.str, - effect_of_forex_changes_on_cash: builtins.str, - net_change_in_cash: builtins.str, - cash_at_end_of_period: builtins.str, - cash_at_beginning_of_period: builtins.str, - operating_cash_flow: builtins.str, - capital_expenditure: builtins.str, - free_cash_flow: builtins.str, - income_taxes_paid: builtins.str, - interest_paid: builtins.str, + net_income: typing.Optional[builtins.str], + depreciation_and_amortization: typing.Optional[builtins.str], + deferred_income_tax: typing.Optional[builtins.str], + stock_based_compensation: typing.Optional[builtins.str], + change_in_working_capital: typing.Optional[builtins.str], + accounts_receivables: typing.Optional[builtins.str], + inventory: typing.Optional[builtins.str], + accounts_payables: typing.Optional[builtins.str], + other_working_capital: typing.Optional[builtins.str], + other_non_cash_items: typing.Optional[builtins.str], + net_cash_provided_by_operating_activities: typing.Optional[builtins.str], + investments_in_property_plant_and_equipment: typing.Optional[builtins.str], + acquisitions_net: typing.Optional[builtins.str], + purchases_of_investments: typing.Optional[builtins.str], + sales_maturities_of_investments: typing.Optional[builtins.str], + other_investing_activities: typing.Optional[builtins.str], + net_cash_provided_by_investing_activities: typing.Optional[builtins.str], + net_debt_issuance: typing.Optional[builtins.str], + long_term_net_debt_issuance: typing.Optional[builtins.str], + short_term_net_debt_issuance: typing.Optional[builtins.str], + net_stock_issuance: typing.Optional[builtins.str], + net_common_stock_issuance: typing.Optional[builtins.str], + common_stock_issuance: typing.Optional[builtins.str], + common_stock_repurchased: typing.Optional[builtins.str], + net_preferred_stock_issuance: typing.Optional[builtins.str], + net_dividends_paid: typing.Optional[builtins.str], + common_dividends_paid: typing.Optional[builtins.str], + preferred_dividends_paid: typing.Optional[builtins.str], + other_financing_activities: typing.Optional[builtins.str], + net_cash_provided_by_financing_activities: typing.Optional[builtins.str], + effect_of_forex_changes_on_cash: typing.Optional[builtins.str], + net_change_in_cash: typing.Optional[builtins.str], + cash_at_end_of_period: typing.Optional[builtins.str], + cash_at_beginning_of_period: typing.Optional[builtins.str], + operating_cash_flow: typing.Optional[builtins.str], + capital_expenditure: typing.Optional[builtins.str], + free_cash_flow: typing.Optional[builtins.str], + income_taxes_paid: typing.Optional[builtins.str], + interest_paid: typing.Optional[builtins.str], ) -> BulkCashFlowStatement: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... @@ -275,79 +283,91 @@ class BulkCashFlowStatementGrowth: @property def reported_currency(self) -> builtins.str: ... @property - def growth_net_income(self) -> builtins.str: ... + def growth_net_income(self) -> typing.Optional[builtins.str]: ... @property - def growth_depreciation_and_amortization(self) -> builtins.str: ... + def growth_depreciation_and_amortization(self) -> typing.Optional[builtins.str]: ... @property - def growth_deferred_income_tax(self) -> builtins.str: ... + def growth_deferred_income_tax(self) -> typing.Optional[builtins.str]: ... @property - def growth_stock_based_compensation(self) -> builtins.str: ... + def growth_stock_based_compensation(self) -> typing.Optional[builtins.str]: ... @property - def growth_change_in_working_capital(self) -> builtins.str: ... + def growth_change_in_working_capital(self) -> typing.Optional[builtins.str]: ... @property - def growth_accounts_receivables(self) -> builtins.str: ... + def growth_accounts_receivables(self) -> typing.Optional[builtins.str]: ... @property - def growth_inventory(self) -> builtins.str: ... + def growth_inventory(self) -> typing.Optional[builtins.str]: ... @property - def growth_accounts_payables(self) -> builtins.str: ... + def growth_accounts_payables(self) -> typing.Optional[builtins.str]: ... @property - def growth_other_working_capital(self) -> builtins.str: ... + def growth_other_working_capital(self) -> typing.Optional[builtins.str]: ... @property - def growth_other_non_cash_items(self) -> builtins.str: ... + def growth_other_non_cash_items(self) -> typing.Optional[builtins.str]: ... @property - def growth_net_cash_provided_by_operating_activities(self) -> builtins.str: ... + def growth_net_cash_provided_by_operating_activities( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_investments_in_property_plant_and_equipment(self) -> builtins.str: ... + def growth_investments_in_property_plant_and_equipment( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_acquisitions_net(self) -> builtins.str: ... + def growth_acquisitions_net(self) -> typing.Optional[builtins.str]: ... @property - def growth_purchases_of_investments(self) -> builtins.str: ... + def growth_purchases_of_investments(self) -> typing.Optional[builtins.str]: ... @property - def growth_sales_maturities_of_investments(self) -> builtins.str: ... + def growth_sales_maturities_of_investments( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_other_investing_activities(self) -> builtins.str: ... + def growth_other_investing_activities(self) -> typing.Optional[builtins.str]: ... @property - def growth_net_cash_used_for_investing_activities(self) -> builtins.str: ... + def growth_net_cash_used_for_investing_activities( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_debt_repayment(self) -> builtins.str: ... + def growth_debt_repayment(self) -> typing.Optional[builtins.str]: ... @property - def growth_common_stock_issued(self) -> builtins.str: ... + def growth_common_stock_issued(self) -> typing.Optional[builtins.str]: ... @property - def growth_common_stock_repurchased(self) -> builtins.str: ... + def growth_common_stock_repurchased(self) -> typing.Optional[builtins.str]: ... @property - def growth_dividends_paid(self) -> builtins.str: ... + def growth_dividends_paid(self) -> typing.Optional[builtins.str]: ... @property - def growth_other_financing_activities(self) -> builtins.str: ... + def growth_other_financing_activities(self) -> typing.Optional[builtins.str]: ... @property - def growth_net_cash_used_provided_by_financing_activities(self) -> builtins.str: ... + def growth_net_cash_used_provided_by_financing_activities( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_effect_of_forex_changes_on_cash(self) -> builtins.str: ... + def growth_effect_of_forex_changes_on_cash( + self, + ) -> typing.Optional[builtins.str]: ... @property - def growth_net_change_in_cash(self) -> builtins.str: ... + def growth_net_change_in_cash(self) -> typing.Optional[builtins.str]: ... @property - def growth_cash_at_end_of_period(self) -> builtins.str: ... + def growth_cash_at_end_of_period(self) -> typing.Optional[builtins.str]: ... @property - def growth_cash_at_beginning_of_period(self) -> builtins.str: ... + def growth_cash_at_beginning_of_period(self) -> typing.Optional[builtins.str]: ... @property - def growth_operating_cash_flow(self) -> builtins.str: ... + def growth_operating_cash_flow(self) -> typing.Optional[builtins.str]: ... @property - def growth_capital_expenditure(self) -> builtins.str: ... + def growth_capital_expenditure(self) -> typing.Optional[builtins.str]: ... @property - def growth_free_cash_flow(self) -> builtins.str: ... + def growth_free_cash_flow(self) -> typing.Optional[builtins.str]: ... @property - def growth_net_debt_issuance(self) -> builtins.str: ... + def growth_net_debt_issuance(self) -> typing.Optional[builtins.str]: ... @property - def growth_long_term_net_debt_issuance(self) -> builtins.str: ... + def growth_long_term_net_debt_issuance(self) -> typing.Optional[builtins.str]: ... @property - def growth_short_term_net_debt_issuance(self) -> builtins.str: ... + def growth_short_term_net_debt_issuance(self) -> typing.Optional[builtins.str]: ... @property - def growth_net_stock_issuance(self) -> builtins.str: ... + def growth_net_stock_issuance(self) -> typing.Optional[builtins.str]: ... @property - def growth_preferred_dividends_paid(self) -> builtins.str: ... + def growth_preferred_dividends_paid(self) -> typing.Optional[builtins.str]: ... @property - def growth_income_taxes_paid(self) -> builtins.str: ... + def growth_income_taxes_paid(self) -> typing.Optional[builtins.str]: ... @property - def growth_interest_paid(self) -> builtins.str: ... + def growth_interest_paid(self) -> typing.Optional[builtins.str]: ... @property def period(self) -> typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... @@ -359,43 +379,47 @@ class BulkCashFlowStatementGrowth: fiscal_year: builtins.str, period: typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"], reported_currency: builtins.str, - growth_net_income: builtins.str, - growth_depreciation_and_amortization: builtins.str, - growth_deferred_income_tax: builtins.str, - growth_stock_based_compensation: builtins.str, - growth_change_in_working_capital: builtins.str, - growth_accounts_receivables: builtins.str, - growth_inventory: builtins.str, - growth_accounts_payables: builtins.str, - growth_other_working_capital: builtins.str, - growth_other_non_cash_items: builtins.str, - growth_net_cash_provided_by_operating_activities: builtins.str, - growth_investments_in_property_plant_and_equipment: builtins.str, - growth_acquisitions_net: builtins.str, - growth_purchases_of_investments: builtins.str, - growth_sales_maturities_of_investments: builtins.str, - growth_other_investing_activities: builtins.str, - growth_net_cash_used_for_investing_activities: builtins.str, - growth_debt_repayment: builtins.str, - growth_common_stock_issued: builtins.str, - growth_common_stock_repurchased: builtins.str, - growth_dividends_paid: builtins.str, - growth_other_financing_activities: builtins.str, - growth_net_cash_used_provided_by_financing_activities: builtins.str, - growth_effect_of_forex_changes_on_cash: builtins.str, - growth_net_change_in_cash: builtins.str, - growth_cash_at_end_of_period: builtins.str, - growth_cash_at_beginning_of_period: builtins.str, - growth_operating_cash_flow: builtins.str, - growth_capital_expenditure: builtins.str, - growth_free_cash_flow: builtins.str, - growth_net_debt_issuance: builtins.str, - growth_long_term_net_debt_issuance: builtins.str, - growth_short_term_net_debt_issuance: builtins.str, - growth_net_stock_issuance: builtins.str, - growth_preferred_dividends_paid: builtins.str, - growth_income_taxes_paid: builtins.str, - growth_interest_paid: builtins.str, + growth_net_income: typing.Optional[builtins.str], + growth_depreciation_and_amortization: typing.Optional[builtins.str], + growth_deferred_income_tax: typing.Optional[builtins.str], + growth_stock_based_compensation: typing.Optional[builtins.str], + growth_change_in_working_capital: typing.Optional[builtins.str], + growth_accounts_receivables: typing.Optional[builtins.str], + growth_inventory: typing.Optional[builtins.str], + growth_accounts_payables: typing.Optional[builtins.str], + growth_other_working_capital: typing.Optional[builtins.str], + growth_other_non_cash_items: typing.Optional[builtins.str], + growth_net_cash_provided_by_operating_activities: typing.Optional[builtins.str], + growth_investments_in_property_plant_and_equipment: typing.Optional[ + builtins.str + ], + growth_acquisitions_net: typing.Optional[builtins.str], + growth_purchases_of_investments: typing.Optional[builtins.str], + growth_sales_maturities_of_investments: typing.Optional[builtins.str], + growth_other_investing_activities: typing.Optional[builtins.str], + growth_net_cash_used_for_investing_activities: typing.Optional[builtins.str], + growth_debt_repayment: typing.Optional[builtins.str], + growth_common_stock_issued: typing.Optional[builtins.str], + growth_common_stock_repurchased: typing.Optional[builtins.str], + growth_dividends_paid: typing.Optional[builtins.str], + growth_other_financing_activities: typing.Optional[builtins.str], + growth_net_cash_used_provided_by_financing_activities: typing.Optional[ + builtins.str + ], + growth_effect_of_forex_changes_on_cash: typing.Optional[builtins.str], + growth_net_change_in_cash: typing.Optional[builtins.str], + growth_cash_at_end_of_period: typing.Optional[builtins.str], + growth_cash_at_beginning_of_period: typing.Optional[builtins.str], + growth_operating_cash_flow: typing.Optional[builtins.str], + growth_capital_expenditure: typing.Optional[builtins.str], + growth_free_cash_flow: typing.Optional[builtins.str], + growth_net_debt_issuance: typing.Optional[builtins.str], + growth_long_term_net_debt_issuance: typing.Optional[builtins.str], + growth_short_term_net_debt_issuance: typing.Optional[builtins.str], + growth_net_stock_issuance: typing.Optional[builtins.str], + growth_preferred_dividends_paid: typing.Optional[builtins.str], + growth_income_taxes_paid: typing.Optional[builtins.str], + growth_interest_paid: typing.Optional[builtins.str], ) -> BulkCashFlowStatementGrowth: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... diff --git a/crates/fmp-py/python/fmp/_native/bulk/eod/__init__.pyi b/crates/fmp-py/python/fmp/_native/bulk/eod/__init__.pyi index 6aa72f7a..dfb9c7f4 100644 --- a/crates/fmp-py/python/fmp/_native/bulk/eod/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/bulk/eod/__init__.pyi @@ -28,29 +28,29 @@ class BulkEodBar: @property def date(self) -> datetime.date: ... @property - def open(self) -> builtins.str: ... + def open(self) -> typing.Optional[builtins.str]: ... @property - def low(self) -> builtins.str: ... + def low(self) -> typing.Optional[builtins.str]: ... @property - def high(self) -> builtins.str: ... + def high(self) -> typing.Optional[builtins.str]: ... @property - def close(self) -> builtins.str: ... + def close(self) -> typing.Optional[builtins.str]: ... @property - def adj_close(self) -> builtins.str: ... + def adj_close(self) -> typing.Optional[builtins.str]: ... @property - def volume(self) -> builtins.str: ... + def volume(self) -> typing.Optional[builtins.str]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... def __new__( cls, *, symbol: builtins.str, date: datetime.date, - open: builtins.str, - low: builtins.str, - high: builtins.str, - close: builtins.str, - adj_close: builtins.str, - volume: builtins.str, + open: typing.Optional[builtins.str], + low: typing.Optional[builtins.str], + high: typing.Optional[builtins.str], + close: typing.Optional[builtins.str], + adj_close: typing.Optional[builtins.str], + volume: typing.Optional[builtins.str], ) -> BulkEodBar: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... diff --git a/crates/fmp-py/src/models/bulk/cash_flow.rs b/crates/fmp-py/src/models/bulk/cash_flow.rs index 0735f241..eb644aa5 100644 --- a/crates/fmp-py/src/models/bulk/cash_flow.rs +++ b/crates/fmp-py/src/models/bulk/cash_flow.rs @@ -25,83 +25,83 @@ pub(crate) struct BulkCashFlowStatement { pub fiscal_year: String, pub period: String, #[pyo3(get)] - pub net_income: String, + pub net_income: Option, #[pyo3(get)] - pub depreciation_and_amortization: String, + pub depreciation_and_amortization: Option, #[pyo3(get)] - pub deferred_income_tax: String, + pub deferred_income_tax: Option, #[pyo3(get)] - pub stock_based_compensation: String, + pub stock_based_compensation: Option, #[pyo3(get)] - pub change_in_working_capital: String, + pub change_in_working_capital: Option, #[pyo3(get)] - pub accounts_receivables: String, + pub accounts_receivables: Option, #[pyo3(get)] - pub inventory: String, + pub inventory: Option, #[pyo3(get)] - pub accounts_payables: String, + pub accounts_payables: Option, #[pyo3(get)] - pub other_working_capital: String, + pub other_working_capital: Option, #[pyo3(get)] - pub other_non_cash_items: String, + pub other_non_cash_items: Option, #[pyo3(get)] - pub net_cash_provided_by_operating_activities: String, + pub net_cash_provided_by_operating_activities: Option, #[pyo3(get)] - pub investments_in_property_plant_and_equipment: String, + pub investments_in_property_plant_and_equipment: Option, #[pyo3(get)] - pub acquisitions_net: String, + pub acquisitions_net: Option, #[pyo3(get)] - pub purchases_of_investments: String, + pub purchases_of_investments: Option, #[pyo3(get)] - pub sales_maturities_of_investments: String, + pub sales_maturities_of_investments: Option, #[pyo3(get)] - pub other_investing_activities: String, + pub other_investing_activities: Option, #[pyo3(get)] - pub net_cash_provided_by_investing_activities: String, + pub net_cash_provided_by_investing_activities: Option, #[pyo3(get)] - pub net_debt_issuance: String, + pub net_debt_issuance: Option, #[pyo3(get)] - pub long_term_net_debt_issuance: String, + pub long_term_net_debt_issuance: Option, #[pyo3(get)] - pub short_term_net_debt_issuance: String, + pub short_term_net_debt_issuance: Option, #[pyo3(get)] - pub net_stock_issuance: String, + pub net_stock_issuance: Option, #[pyo3(get)] - pub net_common_stock_issuance: String, + pub net_common_stock_issuance: Option, #[pyo3(get)] - pub common_stock_issuance: String, + pub common_stock_issuance: Option, #[pyo3(get)] - pub common_stock_repurchased: String, + pub common_stock_repurchased: Option, #[pyo3(get)] - pub net_preferred_stock_issuance: String, + pub net_preferred_stock_issuance: Option, #[pyo3(get)] - pub net_dividends_paid: String, + pub net_dividends_paid: Option, #[pyo3(get)] - pub common_dividends_paid: String, + pub common_dividends_paid: Option, #[pyo3(get)] - pub preferred_dividends_paid: String, + pub preferred_dividends_paid: Option, #[pyo3(get)] - pub other_financing_activities: String, + pub other_financing_activities: Option, #[pyo3(get)] - pub net_cash_provided_by_financing_activities: String, + pub net_cash_provided_by_financing_activities: Option, #[pyo3(get)] - pub effect_of_forex_changes_on_cash: String, + pub effect_of_forex_changes_on_cash: Option, #[pyo3(get)] - pub net_change_in_cash: String, + pub net_change_in_cash: Option, #[pyo3(get)] - pub cash_at_end_of_period: String, + pub cash_at_end_of_period: Option, #[pyo3(get)] - pub cash_at_beginning_of_period: String, + pub cash_at_beginning_of_period: Option, #[pyo3(get)] - pub operating_cash_flow: String, + pub operating_cash_flow: Option, #[pyo3(get)] - pub capital_expenditure: String, + pub capital_expenditure: Option, #[pyo3(get)] - pub free_cash_flow: String, + pub free_cash_flow: Option, #[pyo3(get)] - pub income_taxes_paid: String, + pub income_taxes_paid: Option, #[pyo3(get)] - pub interest_paid: String, + pub interest_paid: Option, } #[gen_stub_pymethods] @@ -121,45 +121,45 @@ impl BulkCashFlowStatement { fiscal_year: String, #[gen_stub(override_type(type_repr = "typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\"]", imports = ("builtins", "typing")))] period: String, - net_income: String, - depreciation_and_amortization: String, - deferred_income_tax: String, - stock_based_compensation: String, - change_in_working_capital: String, - accounts_receivables: String, - inventory: String, - accounts_payables: String, - other_working_capital: String, - other_non_cash_items: String, - net_cash_provided_by_operating_activities: String, - investments_in_property_plant_and_equipment: String, - acquisitions_net: String, - purchases_of_investments: String, - sales_maturities_of_investments: String, - other_investing_activities: String, - net_cash_provided_by_investing_activities: String, - net_debt_issuance: String, - long_term_net_debt_issuance: String, - short_term_net_debt_issuance: String, - net_stock_issuance: String, - net_common_stock_issuance: String, - common_stock_issuance: String, - common_stock_repurchased: String, - net_preferred_stock_issuance: String, - net_dividends_paid: String, - common_dividends_paid: String, - preferred_dividends_paid: String, - other_financing_activities: String, - net_cash_provided_by_financing_activities: String, - effect_of_forex_changes_on_cash: String, - net_change_in_cash: String, - cash_at_end_of_period: String, - cash_at_beginning_of_period: String, - operating_cash_flow: String, - capital_expenditure: String, - free_cash_flow: String, - income_taxes_paid: String, - interest_paid: String, + net_income: Option, + depreciation_and_amortization: Option, + deferred_income_tax: Option, + stock_based_compensation: Option, + change_in_working_capital: Option, + accounts_receivables: Option, + inventory: Option, + accounts_payables: Option, + other_working_capital: Option, + other_non_cash_items: Option, + net_cash_provided_by_operating_activities: Option, + investments_in_property_plant_and_equipment: Option, + acquisitions_net: Option, + purchases_of_investments: Option, + sales_maturities_of_investments: Option, + other_investing_activities: Option, + net_cash_provided_by_investing_activities: Option, + net_debt_issuance: Option, + long_term_net_debt_issuance: Option, + short_term_net_debt_issuance: Option, + net_stock_issuance: Option, + net_common_stock_issuance: Option, + common_stock_issuance: Option, + common_stock_repurchased: Option, + net_preferred_stock_issuance: Option, + net_dividends_paid: Option, + common_dividends_paid: Option, + preferred_dividends_paid: Option, + other_financing_activities: Option, + net_cash_provided_by_financing_activities: Option, + effect_of_forex_changes_on_cash: Option, + net_change_in_cash: Option, + cash_at_end_of_period: Option, + cash_at_beginning_of_period: Option, + operating_cash_flow: Option, + capital_expenditure: Option, + free_cash_flow: Option, + income_taxes_paid: Option, + interest_paid: Option, ) -> Self { Self { date, @@ -937,53 +937,83 @@ impl From for BulkCashFlowStatem accepted_date: value.accepted_date.into_inner(), fiscal_year: value.fiscal_year.as_str().to_owned(), period: value.period.to_string(), - net_income: value.net_income.into_inner(), - depreciation_and_amortization: value.depreciation_and_amortization.into_inner(), - deferred_income_tax: value.deferred_income_tax.into_inner(), - stock_based_compensation: value.stock_based_compensation.into_inner(), - change_in_working_capital: value.change_in_working_capital.into_inner(), - accounts_receivables: value.accounts_receivables.into_inner(), - inventory: value.inventory.into_inner(), - accounts_payables: value.accounts_payables.into_inner(), - other_working_capital: value.other_working_capital.into_inner(), - other_non_cash_items: value.other_non_cash_items.into_inner(), + net_income: value.net_income.map(|value| value.into_inner()), + depreciation_and_amortization: value + .depreciation_and_amortization + .map(|value| value.into_inner()), + deferred_income_tax: value.deferred_income_tax.map(|value| value.into_inner()), + stock_based_compensation: value + .stock_based_compensation + .map(|value| value.into_inner()), + change_in_working_capital: value + .change_in_working_capital + .map(|value| value.into_inner()), + accounts_receivables: value.accounts_receivables.map(|value| value.into_inner()), + inventory: value.inventory.map(|value| value.into_inner()), + accounts_payables: value.accounts_payables.map(|value| value.into_inner()), + other_working_capital: value.other_working_capital.map(|value| value.into_inner()), + other_non_cash_items: value.other_non_cash_items.map(|value| value.into_inner()), net_cash_provided_by_operating_activities: value .net_cash_provided_by_operating_activities - .into_inner(), + .map(|value| value.into_inner()), investments_in_property_plant_and_equipment: value .investments_in_property_plant_and_equipment - .into_inner(), - acquisitions_net: value.acquisitions_net.into_inner(), - purchases_of_investments: value.purchases_of_investments.into_inner(), - sales_maturities_of_investments: value.sales_maturities_of_investments.into_inner(), - other_investing_activities: value.other_investing_activities.into_inner(), + .map(|value| value.into_inner()), + acquisitions_net: value.acquisitions_net.map(|value| value.into_inner()), + purchases_of_investments: value + .purchases_of_investments + .map(|value| value.into_inner()), + sales_maturities_of_investments: value + .sales_maturities_of_investments + .map(|value| value.into_inner()), + other_investing_activities: value + .other_investing_activities + .map(|value| value.into_inner()), net_cash_provided_by_investing_activities: value .net_cash_provided_by_investing_activities - .into_inner(), - net_debt_issuance: value.net_debt_issuance.into_inner(), - long_term_net_debt_issuance: value.long_term_net_debt_issuance.into_inner(), - short_term_net_debt_issuance: value.short_term_net_debt_issuance.into_inner(), - net_stock_issuance: value.net_stock_issuance.into_inner(), - net_common_stock_issuance: value.net_common_stock_issuance.into_inner(), - common_stock_issuance: value.common_stock_issuance.into_inner(), - common_stock_repurchased: value.common_stock_repurchased.into_inner(), - net_preferred_stock_issuance: value.net_preferred_stock_issuance.into_inner(), - net_dividends_paid: value.net_dividends_paid.into_inner(), - common_dividends_paid: value.common_dividends_paid.into_inner(), - preferred_dividends_paid: value.preferred_dividends_paid.into_inner(), - other_financing_activities: value.other_financing_activities.into_inner(), + .map(|value| value.into_inner()), + net_debt_issuance: value.net_debt_issuance.map(|value| value.into_inner()), + long_term_net_debt_issuance: value + .long_term_net_debt_issuance + .map(|value| value.into_inner()), + short_term_net_debt_issuance: value + .short_term_net_debt_issuance + .map(|value| value.into_inner()), + net_stock_issuance: value.net_stock_issuance.map(|value| value.into_inner()), + net_common_stock_issuance: value + .net_common_stock_issuance + .map(|value| value.into_inner()), + common_stock_issuance: value.common_stock_issuance.map(|value| value.into_inner()), + common_stock_repurchased: value + .common_stock_repurchased + .map(|value| value.into_inner()), + net_preferred_stock_issuance: value + .net_preferred_stock_issuance + .map(|value| value.into_inner()), + net_dividends_paid: value.net_dividends_paid.map(|value| value.into_inner()), + common_dividends_paid: value.common_dividends_paid.map(|value| value.into_inner()), + preferred_dividends_paid: value + .preferred_dividends_paid + .map(|value| value.into_inner()), + other_financing_activities: value + .other_financing_activities + .map(|value| value.into_inner()), net_cash_provided_by_financing_activities: value .net_cash_provided_by_financing_activities - .into_inner(), - effect_of_forex_changes_on_cash: value.effect_of_forex_changes_on_cash.into_inner(), - net_change_in_cash: value.net_change_in_cash.into_inner(), - cash_at_end_of_period: value.cash_at_end_of_period.into_inner(), - cash_at_beginning_of_period: value.cash_at_beginning_of_period.into_inner(), - operating_cash_flow: value.operating_cash_flow.into_inner(), - capital_expenditure: value.capital_expenditure.into_inner(), - free_cash_flow: value.free_cash_flow.into_inner(), - income_taxes_paid: value.income_taxes_paid.into_inner(), - interest_paid: value.interest_paid.into_inner(), + .map(|value| value.into_inner()), + effect_of_forex_changes_on_cash: value + .effect_of_forex_changes_on_cash + .map(|value| value.into_inner()), + net_change_in_cash: value.net_change_in_cash.map(|value| value.into_inner()), + cash_at_end_of_period: value.cash_at_end_of_period.map(|value| value.into_inner()), + cash_at_beginning_of_period: value + .cash_at_beginning_of_period + .map(|value| value.into_inner()), + operating_cash_flow: value.operating_cash_flow.map(|value| value.into_inner()), + capital_expenditure: value.capital_expenditure.map(|value| value.into_inner()), + free_cash_flow: value.free_cash_flow.map(|value| value.into_inner()), + income_taxes_paid: value.income_taxes_paid.map(|value| value.into_inner()), + interest_paid: value.interest_paid.map(|value| value.into_inner()), } } } @@ -1008,79 +1038,79 @@ pub(crate) struct BulkCashFlowStatementGrowth { #[pyo3(get)] pub reported_currency: String, #[pyo3(get)] - pub growth_net_income: String, + pub growth_net_income: Option, #[pyo3(get)] - pub growth_depreciation_and_amortization: String, + pub growth_depreciation_and_amortization: Option, #[pyo3(get)] - pub growth_deferred_income_tax: String, + pub growth_deferred_income_tax: Option, #[pyo3(get)] - pub growth_stock_based_compensation: String, + pub growth_stock_based_compensation: Option, #[pyo3(get)] - pub growth_change_in_working_capital: String, + pub growth_change_in_working_capital: Option, #[pyo3(get)] - pub growth_accounts_receivables: String, + pub growth_accounts_receivables: Option, #[pyo3(get)] - pub growth_inventory: String, + pub growth_inventory: Option, #[pyo3(get)] - pub growth_accounts_payables: String, + pub growth_accounts_payables: Option, #[pyo3(get)] - pub growth_other_working_capital: String, + pub growth_other_working_capital: Option, #[pyo3(get)] - pub growth_other_non_cash_items: String, + pub growth_other_non_cash_items: Option, #[pyo3(get)] - pub growth_net_cash_provided_by_operating_activities: String, + pub growth_net_cash_provided_by_operating_activities: Option, #[pyo3(get)] - pub growth_investments_in_property_plant_and_equipment: String, + pub growth_investments_in_property_plant_and_equipment: Option, #[pyo3(get)] - pub growth_acquisitions_net: String, + pub growth_acquisitions_net: Option, #[pyo3(get)] - pub growth_purchases_of_investments: String, + pub growth_purchases_of_investments: Option, #[pyo3(get)] - pub growth_sales_maturities_of_investments: String, + pub growth_sales_maturities_of_investments: Option, #[pyo3(get)] - pub growth_other_investing_activities: String, + pub growth_other_investing_activities: Option, #[pyo3(get)] - pub growth_net_cash_used_for_investing_activities: String, + pub growth_net_cash_used_for_investing_activities: Option, #[pyo3(get)] - pub growth_debt_repayment: String, + pub growth_debt_repayment: Option, #[pyo3(get)] - pub growth_common_stock_issued: String, + pub growth_common_stock_issued: Option, #[pyo3(get)] - pub growth_common_stock_repurchased: String, + pub growth_common_stock_repurchased: Option, #[pyo3(get)] - pub growth_dividends_paid: String, + pub growth_dividends_paid: Option, #[pyo3(get)] - pub growth_other_financing_activities: String, + pub growth_other_financing_activities: Option, #[pyo3(get)] - pub growth_net_cash_used_provided_by_financing_activities: String, + pub growth_net_cash_used_provided_by_financing_activities: Option, #[pyo3(get)] - pub growth_effect_of_forex_changes_on_cash: String, + pub growth_effect_of_forex_changes_on_cash: Option, #[pyo3(get)] - pub growth_net_change_in_cash: String, + pub growth_net_change_in_cash: Option, #[pyo3(get)] - pub growth_cash_at_end_of_period: String, + pub growth_cash_at_end_of_period: Option, #[pyo3(get)] - pub growth_cash_at_beginning_of_period: String, + pub growth_cash_at_beginning_of_period: Option, #[pyo3(get)] - pub growth_operating_cash_flow: String, + pub growth_operating_cash_flow: Option, #[pyo3(get)] - pub growth_capital_expenditure: String, + pub growth_capital_expenditure: Option, #[pyo3(get)] - pub growth_free_cash_flow: String, + pub growth_free_cash_flow: Option, #[pyo3(get)] - pub growth_net_debt_issuance: String, + pub growth_net_debt_issuance: Option, #[pyo3(get)] - pub growth_long_term_net_debt_issuance: String, + pub growth_long_term_net_debt_issuance: Option, #[pyo3(get)] - pub growth_short_term_net_debt_issuance: String, + pub growth_short_term_net_debt_issuance: Option, #[pyo3(get)] - pub growth_net_stock_issuance: String, + pub growth_net_stock_issuance: Option, #[pyo3(get)] - pub growth_preferred_dividends_paid: String, + pub growth_preferred_dividends_paid: Option, #[pyo3(get)] - pub growth_income_taxes_paid: String, + pub growth_income_taxes_paid: Option, #[pyo3(get)] - pub growth_interest_paid: String, + pub growth_interest_paid: Option, } #[gen_stub_pymethods] @@ -1097,43 +1127,43 @@ impl BulkCashFlowStatementGrowth { #[gen_stub(override_type(type_repr = "typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\"]", imports = ("builtins", "typing")))] period: String, reported_currency: String, - growth_net_income: String, - growth_depreciation_and_amortization: String, - growth_deferred_income_tax: String, - growth_stock_based_compensation: String, - growth_change_in_working_capital: String, - growth_accounts_receivables: String, - growth_inventory: String, - growth_accounts_payables: String, - growth_other_working_capital: String, - growth_other_non_cash_items: String, - growth_net_cash_provided_by_operating_activities: String, - growth_investments_in_property_plant_and_equipment: String, - growth_acquisitions_net: String, - growth_purchases_of_investments: String, - growth_sales_maturities_of_investments: String, - growth_other_investing_activities: String, - growth_net_cash_used_for_investing_activities: String, - growth_debt_repayment: String, - growth_common_stock_issued: String, - growth_common_stock_repurchased: String, - growth_dividends_paid: String, - growth_other_financing_activities: String, - growth_net_cash_used_provided_by_financing_activities: String, - growth_effect_of_forex_changes_on_cash: String, - growth_net_change_in_cash: String, - growth_cash_at_end_of_period: String, - growth_cash_at_beginning_of_period: String, - growth_operating_cash_flow: String, - growth_capital_expenditure: String, - growth_free_cash_flow: String, - growth_net_debt_issuance: String, - growth_long_term_net_debt_issuance: String, - growth_short_term_net_debt_issuance: String, - growth_net_stock_issuance: String, - growth_preferred_dividends_paid: String, - growth_income_taxes_paid: String, - growth_interest_paid: String, + growth_net_income: Option, + growth_depreciation_and_amortization: Option, + growth_deferred_income_tax: Option, + growth_stock_based_compensation: Option, + growth_change_in_working_capital: Option, + growth_accounts_receivables: Option, + growth_inventory: Option, + growth_accounts_payables: Option, + growth_other_working_capital: Option, + growth_other_non_cash_items: Option, + growth_net_cash_provided_by_operating_activities: Option, + growth_investments_in_property_plant_and_equipment: Option, + growth_acquisitions_net: Option, + growth_purchases_of_investments: Option, + growth_sales_maturities_of_investments: Option, + growth_other_investing_activities: Option, + growth_net_cash_used_for_investing_activities: Option, + growth_debt_repayment: Option, + growth_common_stock_issued: Option, + growth_common_stock_repurchased: Option, + growth_dividends_paid: Option, + growth_other_financing_activities: Option, + growth_net_cash_used_provided_by_financing_activities: Option, + growth_effect_of_forex_changes_on_cash: Option, + growth_net_change_in_cash: Option, + growth_cash_at_end_of_period: Option, + growth_cash_at_beginning_of_period: Option, + growth_operating_cash_flow: Option, + growth_capital_expenditure: Option, + growth_free_cash_flow: Option, + growth_net_debt_issuance: Option, + growth_long_term_net_debt_issuance: Option, + growth_short_term_net_debt_issuance: Option, + growth_net_stock_issuance: Option, + growth_preferred_dividends_paid: Option, + growth_income_taxes_paid: Option, + growth_interest_paid: Option, ) -> Self { Self { symbol, @@ -1933,63 +1963,105 @@ impl From for BulkCashFlow fiscal_year: value.fiscal_year.as_str().to_owned(), period: value.period.to_string(), reported_currency: value.reported_currency.into_inner(), - growth_net_income: value.growth_net_income.into_inner(), + growth_net_income: value.growth_net_income.map(|value| value.into_inner()), growth_depreciation_and_amortization: value .growth_depreciation_and_amortization - .into_inner(), - growth_deferred_income_tax: value.growth_deferred_income_tax.into_inner(), - growth_stock_based_compensation: value.growth_stock_based_compensation.into_inner(), - growth_change_in_working_capital: value.growth_change_in_working_capital.into_inner(), - growth_accounts_receivables: value.growth_accounts_receivables.into_inner(), - growth_inventory: value.growth_inventory.into_inner(), - growth_accounts_payables: value.growth_accounts_payables.into_inner(), - growth_other_working_capital: value.growth_other_working_capital.into_inner(), - growth_other_non_cash_items: value.growth_other_non_cash_items.into_inner(), + .map(|value| value.into_inner()), + growth_deferred_income_tax: value + .growth_deferred_income_tax + .map(|value| value.into_inner()), + growth_stock_based_compensation: value + .growth_stock_based_compensation + .map(|value| value.into_inner()), + growth_change_in_working_capital: value + .growth_change_in_working_capital + .map(|value| value.into_inner()), + growth_accounts_receivables: value + .growth_accounts_receivables + .map(|value| value.into_inner()), + growth_inventory: value.growth_inventory.map(|value| value.into_inner()), + growth_accounts_payables: value + .growth_accounts_payables + .map(|value| value.into_inner()), + growth_other_working_capital: value + .growth_other_working_capital + .map(|value| value.into_inner()), + growth_other_non_cash_items: value + .growth_other_non_cash_items + .map(|value| value.into_inner()), growth_net_cash_provided_by_operating_activities: value .growth_net_cash_provided_by_operating_activities - .into_inner(), + .map(|value| value.into_inner()), growth_investments_in_property_plant_and_equipment: value .growth_investments_in_property_plant_and_equipment - .into_inner(), - growth_acquisitions_net: value.growth_acquisitions_net.into_inner(), - growth_purchases_of_investments: value.growth_purchases_of_investments.into_inner(), + .map(|value| value.into_inner()), + growth_acquisitions_net: value + .growth_acquisitions_net + .map(|value| value.into_inner()), + growth_purchases_of_investments: value + .growth_purchases_of_investments + .map(|value| value.into_inner()), growth_sales_maturities_of_investments: value .growth_sales_maturities_of_investments - .into_inner(), - growth_other_investing_activities: value.growth_other_investing_activities.into_inner(), + .map(|value| value.into_inner()), + growth_other_investing_activities: value + .growth_other_investing_activities + .map(|value| value.into_inner()), growth_net_cash_used_for_investing_activities: value .growth_net_cash_used_for_investing_activities - .into_inner(), - growth_debt_repayment: value.growth_debt_repayment.into_inner(), - growth_common_stock_issued: value.growth_common_stock_issued.into_inner(), - growth_common_stock_repurchased: value.growth_common_stock_repurchased.into_inner(), - growth_dividends_paid: value.growth_dividends_paid.into_inner(), - growth_other_financing_activities: value.growth_other_financing_activities.into_inner(), + .map(|value| value.into_inner()), + growth_debt_repayment: value.growth_debt_repayment.map(|value| value.into_inner()), + growth_common_stock_issued: value + .growth_common_stock_issued + .map(|value| value.into_inner()), + growth_common_stock_repurchased: value + .growth_common_stock_repurchased + .map(|value| value.into_inner()), + growth_dividends_paid: value.growth_dividends_paid.map(|value| value.into_inner()), + growth_other_financing_activities: value + .growth_other_financing_activities + .map(|value| value.into_inner()), growth_net_cash_used_provided_by_financing_activities: value .growth_net_cash_used_provided_by_financing_activities - .into_inner(), + .map(|value| value.into_inner()), growth_effect_of_forex_changes_on_cash: value .growth_effect_of_forex_changes_on_cash - .into_inner(), - growth_net_change_in_cash: value.growth_net_change_in_cash.into_inner(), - growth_cash_at_end_of_period: value.growth_cash_at_end_of_period.into_inner(), + .map(|value| value.into_inner()), + growth_net_change_in_cash: value + .growth_net_change_in_cash + .map(|value| value.into_inner()), + growth_cash_at_end_of_period: value + .growth_cash_at_end_of_period + .map(|value| value.into_inner()), growth_cash_at_beginning_of_period: value .growth_cash_at_beginning_of_period - .into_inner(), - growth_operating_cash_flow: value.growth_operating_cash_flow.into_inner(), - growth_capital_expenditure: value.growth_capital_expenditure.into_inner(), - growth_free_cash_flow: value.growth_free_cash_flow.into_inner(), - growth_net_debt_issuance: value.growth_net_debt_issuance.into_inner(), + .map(|value| value.into_inner()), + growth_operating_cash_flow: value + .growth_operating_cash_flow + .map(|value| value.into_inner()), + growth_capital_expenditure: value + .growth_capital_expenditure + .map(|value| value.into_inner()), + growth_free_cash_flow: value.growth_free_cash_flow.map(|value| value.into_inner()), + growth_net_debt_issuance: value + .growth_net_debt_issuance + .map(|value| value.into_inner()), growth_long_term_net_debt_issuance: value .growth_long_term_net_debt_issuance - .into_inner(), + .map(|value| value.into_inner()), growth_short_term_net_debt_issuance: value .growth_short_term_net_debt_issuance - .into_inner(), - growth_net_stock_issuance: value.growth_net_stock_issuance.into_inner(), - growth_preferred_dividends_paid: value.growth_preferred_dividends_paid.into_inner(), - growth_income_taxes_paid: value.growth_income_taxes_paid.into_inner(), - growth_interest_paid: value.growth_interest_paid.into_inner(), + .map(|value| value.into_inner()), + growth_net_stock_issuance: value + .growth_net_stock_issuance + .map(|value| value.into_inner()), + growth_preferred_dividends_paid: value + .growth_preferred_dividends_paid + .map(|value| value.into_inner()), + growth_income_taxes_paid: value + .growth_income_taxes_paid + .map(|value| value.into_inner()), + growth_interest_paid: value.growth_interest_paid.map(|value| value.into_inner()), } } } diff --git a/crates/fmp-py/src/models/bulk/eod.rs b/crates/fmp-py/src/models/bulk/eod.rs index 6e35029f..d06b411a 100644 --- a/crates/fmp-py/src/models/bulk/eod.rs +++ b/crates/fmp-py/src/models/bulk/eod.rs @@ -14,17 +14,17 @@ pub(crate) struct BulkEodBar { #[pyo3(get)] pub date: ::chrono::NaiveDate, #[pyo3(get)] - pub open: String, + pub open: Option, #[pyo3(get)] - pub low: String, + pub low: Option, #[pyo3(get)] - pub high: String, + pub high: Option, #[pyo3(get)] - pub close: String, + pub close: Option, #[pyo3(get)] - pub adj_close: String, + pub adj_close: Option, #[pyo3(get)] - pub volume: String, + pub volume: Option, } #[gen_stub_pymethods] @@ -37,12 +37,12 @@ impl BulkEodBar { fn new( symbol: String, date: ::chrono::NaiveDate, - open: String, - low: String, - high: String, - close: String, - adj_close: String, - volume: String, + open: Option, + low: Option, + high: Option, + close: Option, + adj_close: Option, + volume: Option, ) -> Self { Self { symbol, @@ -156,12 +156,12 @@ impl From for BulkEodBar { Self { symbol: value.symbol.into_inner(), date: value.date.into_inner(), - open: value.open.into_inner(), - low: value.low.into_inner(), - high: value.high.into_inner(), - close: value.close.into_inner(), - adj_close: value.adj_close.into_inner(), - volume: value.volume.into_inner(), + open: value.open.map(|value| value.into_inner()), + low: value.low.map(|value| value.into_inner()), + high: value.high.map(|value| value.into_inner()), + close: value.close.map(|value| value.into_inner()), + adj_close: value.adj_close.map(|value| value.into_inner()), + volume: value.volume.map(|value| value.into_inner()), } } } diff --git a/crates/libfmp/src/responses/bulk/cash_flow.rs b/crates/libfmp/src/responses/bulk/cash_flow.rs index d5d9dee1..50bbe7c2 100644 --- a/crates/libfmp/src/responses/bulk/cash_flow.rs +++ b/crates/libfmp/src/responses/bulk/cash_flow.rs @@ -21,45 +21,45 @@ pub struct BulkCashFlowStatement { pub accepted_date: ApiDateTime, pub fiscal_year: FiscalYearString, pub period: FiscalPeriod, - pub net_income: NumericString, - pub depreciation_and_amortization: NumericString, - pub deferred_income_tax: NumericString, - pub stock_based_compensation: NumericString, - pub change_in_working_capital: NumericString, - pub accounts_receivables: NumericString, - pub inventory: NumericString, - pub accounts_payables: NumericString, - pub other_working_capital: NumericString, - pub other_non_cash_items: NumericString, - pub net_cash_provided_by_operating_activities: NumericString, - pub investments_in_property_plant_and_equipment: NumericString, - pub acquisitions_net: NumericString, - pub purchases_of_investments: NumericString, - pub sales_maturities_of_investments: NumericString, - pub other_investing_activities: NumericString, - pub net_cash_provided_by_investing_activities: NumericString, - pub net_debt_issuance: NumericString, - pub long_term_net_debt_issuance: NumericString, - pub short_term_net_debt_issuance: NumericString, - pub net_stock_issuance: NumericString, - pub net_common_stock_issuance: NumericString, - pub common_stock_issuance: NumericString, - pub common_stock_repurchased: NumericString, - pub net_preferred_stock_issuance: NumericString, - pub net_dividends_paid: NumericString, - pub common_dividends_paid: NumericString, - pub preferred_dividends_paid: NumericString, - pub other_financing_activities: NumericString, - pub net_cash_provided_by_financing_activities: NumericString, - pub effect_of_forex_changes_on_cash: NumericString, - pub net_change_in_cash: NumericString, - pub cash_at_end_of_period: NumericString, - pub cash_at_beginning_of_period: NumericString, - pub operating_cash_flow: NumericString, - pub capital_expenditure: NumericString, - pub free_cash_flow: NumericString, - pub income_taxes_paid: NumericString, - pub interest_paid: NumericString, + pub net_income: Option, + pub depreciation_and_amortization: Option, + pub deferred_income_tax: Option, + pub stock_based_compensation: Option, + pub change_in_working_capital: Option, + pub accounts_receivables: Option, + pub inventory: Option, + pub accounts_payables: Option, + pub other_working_capital: Option, + pub other_non_cash_items: Option, + pub net_cash_provided_by_operating_activities: Option, + pub investments_in_property_plant_and_equipment: Option, + pub acquisitions_net: Option, + pub purchases_of_investments: Option, + pub sales_maturities_of_investments: Option, + pub other_investing_activities: Option, + pub net_cash_provided_by_investing_activities: Option, + pub net_debt_issuance: Option, + pub long_term_net_debt_issuance: Option, + pub short_term_net_debt_issuance: Option, + pub net_stock_issuance: Option, + pub net_common_stock_issuance: Option, + pub common_stock_issuance: Option, + pub common_stock_repurchased: Option, + pub net_preferred_stock_issuance: Option, + pub net_dividends_paid: Option, + pub common_dividends_paid: Option, + pub preferred_dividends_paid: Option, + pub other_financing_activities: Option, + pub net_cash_provided_by_financing_activities: Option, + pub effect_of_forex_changes_on_cash: Option, + pub net_change_in_cash: Option, + pub cash_at_end_of_period: Option, + pub cash_at_beginning_of_period: Option, + pub operating_cash_flow: Option, + pub capital_expenditure: Option, + pub free_cash_flow: Option, + pub income_taxes_paid: Option, + pub interest_paid: Option, } /// One worldwide bulk cash-flow-statement-growth row. @@ -72,45 +72,45 @@ pub struct BulkCashFlowStatementGrowth { pub fiscal_year: FiscalYearString, pub period: FiscalPeriod, pub reported_currency: CurrencyCode, - pub growth_net_income: NumericString, - pub growth_depreciation_and_amortization: NumericString, - pub growth_deferred_income_tax: NumericString, - pub growth_stock_based_compensation: NumericString, - pub growth_change_in_working_capital: NumericString, - pub growth_accounts_receivables: NumericString, - pub growth_inventory: NumericString, - pub growth_accounts_payables: NumericString, - pub growth_other_working_capital: NumericString, - pub growth_other_non_cash_items: NumericString, + pub growth_net_income: Option, + pub growth_depreciation_and_amortization: Option, + pub growth_deferred_income_tax: Option, + pub growth_stock_based_compensation: Option, + pub growth_change_in_working_capital: Option, + pub growth_accounts_receivables: Option, + pub growth_inventory: Option, + pub growth_accounts_payables: Option, + pub growth_other_working_capital: Option, + pub growth_other_non_cash_items: Option, #[serde(rename = "growthNetCashProvidedByOperatingActivites")] - pub growth_net_cash_provided_by_operating_activities: NumericString, - pub growth_investments_in_property_plant_and_equipment: NumericString, - pub growth_acquisitions_net: NumericString, - pub growth_purchases_of_investments: NumericString, - pub growth_sales_maturities_of_investments: NumericString, + pub growth_net_cash_provided_by_operating_activities: Option, + pub growth_investments_in_property_plant_and_equipment: Option, + pub growth_acquisitions_net: Option, + pub growth_purchases_of_investments: Option, + pub growth_sales_maturities_of_investments: Option, #[serde(rename = "growthOtherInvestingActivites")] - pub growth_other_investing_activities: NumericString, + pub growth_other_investing_activities: Option, #[serde(rename = "growthNetCashUsedForInvestingActivites")] - pub growth_net_cash_used_for_investing_activities: NumericString, - pub growth_debt_repayment: NumericString, - pub growth_common_stock_issued: NumericString, - pub growth_common_stock_repurchased: NumericString, - pub growth_dividends_paid: NumericString, + pub growth_net_cash_used_for_investing_activities: Option, + pub growth_debt_repayment: Option, + pub growth_common_stock_issued: Option, + pub growth_common_stock_repurchased: Option, + pub growth_dividends_paid: Option, #[serde(rename = "growthOtherFinancingActivites")] - pub growth_other_financing_activities: NumericString, - pub growth_net_cash_used_provided_by_financing_activities: NumericString, - pub growth_effect_of_forex_changes_on_cash: NumericString, - pub growth_net_change_in_cash: NumericString, - pub growth_cash_at_end_of_period: NumericString, - pub growth_cash_at_beginning_of_period: NumericString, - pub growth_operating_cash_flow: NumericString, - pub growth_capital_expenditure: NumericString, - pub growth_free_cash_flow: NumericString, - pub growth_net_debt_issuance: NumericString, - pub growth_long_term_net_debt_issuance: NumericString, - pub growth_short_term_net_debt_issuance: NumericString, - pub growth_net_stock_issuance: NumericString, - pub growth_preferred_dividends_paid: NumericString, - pub growth_income_taxes_paid: NumericString, - pub growth_interest_paid: NumericString, + pub growth_other_financing_activities: Option, + pub growth_net_cash_used_provided_by_financing_activities: Option, + pub growth_effect_of_forex_changes_on_cash: Option, + pub growth_net_change_in_cash: Option, + pub growth_cash_at_end_of_period: Option, + pub growth_cash_at_beginning_of_period: Option, + pub growth_operating_cash_flow: Option, + pub growth_capital_expenditure: Option, + pub growth_free_cash_flow: Option, + pub growth_net_debt_issuance: Option, + pub growth_long_term_net_debt_issuance: Option, + pub growth_short_term_net_debt_issuance: Option, + pub growth_net_stock_issuance: Option, + pub growth_preferred_dividends_paid: Option, + pub growth_income_taxes_paid: Option, + pub growth_interest_paid: Option, } diff --git a/crates/libfmp/src/responses/bulk/eod.rs b/crates/libfmp/src/responses/bulk/eod.rs index 4f592ed2..7deebd15 100644 --- a/crates/libfmp/src/responses/bulk/eod.rs +++ b/crates/libfmp/src/responses/bulk/eod.rs @@ -14,10 +14,10 @@ use crate::{ pub struct BulkEodBar { pub symbol: Ticker, pub date: Date, - pub open: NumericString, - pub low: NumericString, - pub high: NumericString, - pub close: NumericString, - pub adj_close: NumericString, - pub volume: NumericString, + pub open: Option, + pub low: Option, + pub high: Option, + pub close: Option, + pub adj_close: Option, + pub volume: Option, } diff --git a/crates/libfmp/tests/bulk_cash_eod_responses.rs b/crates/libfmp/tests/bulk_cash_eod_responses.rs index d9a36551..52dd8036 100644 --- a/crates/libfmp/tests/bulk_cash_eod_responses.rs +++ b/crates/libfmp/tests/bulk_cash_eod_responses.rs @@ -11,7 +11,9 @@ use libfmp::{ types::Date, }; -use bulk_csv::{assert_empty_bodies, assert_members, assert_round_trip, decode, first_row}; +use bulk_csv::{ + assert_empty_bodies, assert_required_members, assert_round_trip, decode, first_row, +}; const CASH_FLOW: &[u8] = include_bytes!("fixtures/bulk_cash_flow_statements.csv"); const GROWTH: &[u8] = include_bytes!("fixtures/bulk_cash_flow_statement_growth.csv"); @@ -33,10 +35,27 @@ fn live_csv_fixtures_round_trip_every_cell_with_exact_field_counts() { } #[test] -fn every_column_is_required_and_no_member_accepts_an_empty_cell() { - assert_members(&bulk_cash_flow_statements(query()), CASH_FLOW, &[]); - assert_members(&bulk_cash_flow_statement_growth(query()), GROWTH, &[]); - assert_members(&bulk_eod(day().into()), EOD, &[]); +fn every_column_is_required_and_only_identity_members_reject_an_empty_cell() { + assert_required_members( + &bulk_cash_flow_statements(query()), + CASH_FLOW, + &[ + "date", + "symbol", + "reportedCurrency", + "cik", + "filingDate", + "acceptedDate", + "fiscalYear", + "period", + ], + ); + assert_required_members( + &bulk_cash_flow_statement_growth(query()), + GROWTH, + &["symbol", "date", "fiscalYear", "period", "reportedCurrency"], + ); + assert_required_members(&bulk_eod(day().into()), EOD, &["symbol", "date"]); } #[test] @@ -54,13 +73,13 @@ fn identity_fields_and_prices_preserve_provider_representations() { assert_eq!(cash_flow.cik.as_str(), "0000000000"); assert_eq!(cash_flow.accepted_date.to_string(), "2024-03-30 20:00:00"); assert_eq!(cash_flow.period, FiscalPeriod::Q1); - assert_eq!(cash_flow.net_income.as_str(), "14932000000"); + assert_eq!(cash_flow.net_income.unwrap().as_str(), "14932000000"); let eod = decode(&bulk_eod(day().into()), EOD).unwrap(); assert_eq!(eod[1].symbol.as_str(), "HKDCNH"); assert_eq!(eod[1].date, day()); - assert_eq!(eod[1].open.as_str(), "0.91858"); - assert_eq!(eod[1].volume.as_str(), "0"); + assert_eq!(eod[1].open.as_ref().unwrap().as_str(), "0.91858"); + assert_eq!(eod[1].volume.as_ref().unwrap().as_str(), "0"); } #[test] diff --git a/sdk/go/bulk_models.go b/sdk/go/bulk_models.go index 2c707c84..faa541f3 100644 --- a/sdk/go/bulk_models.go +++ b/sdk/go/bulk_models.go @@ -448,45 +448,45 @@ type BulkCashFlowStatement struct { AcceptedDate DateTime `json:"acceptedDate"` FiscalYear string `json:"fiscalYear"` Period string `json:"period"` - NetIncome string `json:"netIncome"` - DepreciationAndAmortization string `json:"depreciationAndAmortization"` - DeferredIncomeTax string `json:"deferredIncomeTax"` - StockBasedCompensation string `json:"stockBasedCompensation"` - ChangeInWorkingCapital string `json:"changeInWorkingCapital"` - AccountsReceivables string `json:"accountsReceivables"` - Inventory string `json:"inventory"` - AccountsPayables string `json:"accountsPayables"` - OtherWorkingCapital string `json:"otherWorkingCapital"` - OtherNonCashItems string `json:"otherNonCashItems"` - NetCashProvidedByOperatingActivities string `json:"netCashProvidedByOperatingActivities"` - InvestmentsInPropertyPlantAndEquipment string `json:"investmentsInPropertyPlantAndEquipment"` - AcquisitionsNet string `json:"acquisitionsNet"` - PurchasesOfInvestments string `json:"purchasesOfInvestments"` - SalesMaturitiesOfInvestments string `json:"salesMaturitiesOfInvestments"` - OtherInvestingActivities string `json:"otherInvestingActivities"` - NetCashProvidedByInvestingActivities string `json:"netCashProvidedByInvestingActivities"` - NetDebtIssuance string `json:"netDebtIssuance"` - LongTermNetDebtIssuance string `json:"longTermNetDebtIssuance"` - ShortTermNetDebtIssuance string `json:"shortTermNetDebtIssuance"` - NetStockIssuance string `json:"netStockIssuance"` - NetCommonStockIssuance string `json:"netCommonStockIssuance"` - CommonStockIssuance string `json:"commonStockIssuance"` - CommonStockRepurchased string `json:"commonStockRepurchased"` - NetPreferredStockIssuance string `json:"netPreferredStockIssuance"` - NetDividendsPaid string `json:"netDividendsPaid"` - CommonDividendsPaid string `json:"commonDividendsPaid"` - PreferredDividendsPaid string `json:"preferredDividendsPaid"` - OtherFinancingActivities string `json:"otherFinancingActivities"` - NetCashProvidedByFinancingActivities string `json:"netCashProvidedByFinancingActivities"` - EffectOfForexChangesOnCash string `json:"effectOfForexChangesOnCash"` - NetChangeInCash string `json:"netChangeInCash"` - CashAtEndOfPeriod string `json:"cashAtEndOfPeriod"` - CashAtBeginningOfPeriod string `json:"cashAtBeginningOfPeriod"` - OperatingCashFlow string `json:"operatingCashFlow"` - CapitalExpenditure string `json:"capitalExpenditure"` - FreeCashFlow string `json:"freeCashFlow"` - IncomeTaxesPaid string `json:"incomeTaxesPaid"` - InterestPaid string `json:"interestPaid"` + NetIncome *string `json:"netIncome"` + DepreciationAndAmortization *string `json:"depreciationAndAmortization"` + DeferredIncomeTax *string `json:"deferredIncomeTax"` + StockBasedCompensation *string `json:"stockBasedCompensation"` + ChangeInWorkingCapital *string `json:"changeInWorkingCapital"` + AccountsReceivables *string `json:"accountsReceivables"` + Inventory *string `json:"inventory"` + AccountsPayables *string `json:"accountsPayables"` + OtherWorkingCapital *string `json:"otherWorkingCapital"` + OtherNonCashItems *string `json:"otherNonCashItems"` + NetCashProvidedByOperatingActivities *string `json:"netCashProvidedByOperatingActivities"` + InvestmentsInPropertyPlantAndEquipment *string `json:"investmentsInPropertyPlantAndEquipment"` + AcquisitionsNet *string `json:"acquisitionsNet"` + PurchasesOfInvestments *string `json:"purchasesOfInvestments"` + SalesMaturitiesOfInvestments *string `json:"salesMaturitiesOfInvestments"` + OtherInvestingActivities *string `json:"otherInvestingActivities"` + NetCashProvidedByInvestingActivities *string `json:"netCashProvidedByInvestingActivities"` + NetDebtIssuance *string `json:"netDebtIssuance"` + LongTermNetDebtIssuance *string `json:"longTermNetDebtIssuance"` + ShortTermNetDebtIssuance *string `json:"shortTermNetDebtIssuance"` + NetStockIssuance *string `json:"netStockIssuance"` + NetCommonStockIssuance *string `json:"netCommonStockIssuance"` + CommonStockIssuance *string `json:"commonStockIssuance"` + CommonStockRepurchased *string `json:"commonStockRepurchased"` + NetPreferredStockIssuance *string `json:"netPreferredStockIssuance"` + NetDividendsPaid *string `json:"netDividendsPaid"` + CommonDividendsPaid *string `json:"commonDividendsPaid"` + PreferredDividendsPaid *string `json:"preferredDividendsPaid"` + OtherFinancingActivities *string `json:"otherFinancingActivities"` + NetCashProvidedByFinancingActivities *string `json:"netCashProvidedByFinancingActivities"` + EffectOfForexChangesOnCash *string `json:"effectOfForexChangesOnCash"` + NetChangeInCash *string `json:"netChangeInCash"` + CashAtEndOfPeriod *string `json:"cashAtEndOfPeriod"` + CashAtBeginningOfPeriod *string `json:"cashAtBeginningOfPeriod"` + OperatingCashFlow *string `json:"operatingCashFlow"` + CapitalExpenditure *string `json:"capitalExpenditure"` + FreeCashFlow *string `json:"freeCashFlow"` + IncomeTaxesPaid *string `json:"incomeTaxesPaid"` + InterestPaid *string `json:"interestPaid"` } // bulkCashFlowStatementShadow mirrors BulkCashFlowStatement with a pointer or @@ -567,84 +567,6 @@ func (m *BulkCashFlowStatement) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("BulkCashFlowStatement", "fiscalYear") case shadow.Period == nil: return missingMemberError("BulkCashFlowStatement", "period") - case shadow.NetIncome == nil: - return missingMemberError("BulkCashFlowStatement", "netIncome") - case shadow.DepreciationAndAmortization == nil: - return missingMemberError("BulkCashFlowStatement", "depreciationAndAmortization") - case shadow.DeferredIncomeTax == nil: - return missingMemberError("BulkCashFlowStatement", "deferredIncomeTax") - case shadow.StockBasedCompensation == nil: - return missingMemberError("BulkCashFlowStatement", "stockBasedCompensation") - case shadow.ChangeInWorkingCapital == nil: - return missingMemberError("BulkCashFlowStatement", "changeInWorkingCapital") - case shadow.AccountsReceivables == nil: - return missingMemberError("BulkCashFlowStatement", "accountsReceivables") - case shadow.Inventory == nil: - return missingMemberError("BulkCashFlowStatement", "inventory") - case shadow.AccountsPayables == nil: - return missingMemberError("BulkCashFlowStatement", "accountsPayables") - case shadow.OtherWorkingCapital == nil: - return missingMemberError("BulkCashFlowStatement", "otherWorkingCapital") - case shadow.OtherNonCashItems == nil: - return missingMemberError("BulkCashFlowStatement", "otherNonCashItems") - case shadow.NetCashProvidedByOperatingActivities == nil: - return missingMemberError("BulkCashFlowStatement", "netCashProvidedByOperatingActivities") - case shadow.InvestmentsInPropertyPlantAndEquipment == nil: - return missingMemberError("BulkCashFlowStatement", "investmentsInPropertyPlantAndEquipment") - case shadow.AcquisitionsNet == nil: - return missingMemberError("BulkCashFlowStatement", "acquisitionsNet") - case shadow.PurchasesOfInvestments == nil: - return missingMemberError("BulkCashFlowStatement", "purchasesOfInvestments") - case shadow.SalesMaturitiesOfInvestments == nil: - return missingMemberError("BulkCashFlowStatement", "salesMaturitiesOfInvestments") - case shadow.OtherInvestingActivities == nil: - return missingMemberError("BulkCashFlowStatement", "otherInvestingActivities") - case shadow.NetCashProvidedByInvestingActivities == nil: - return missingMemberError("BulkCashFlowStatement", "netCashProvidedByInvestingActivities") - case shadow.NetDebtIssuance == nil: - return missingMemberError("BulkCashFlowStatement", "netDebtIssuance") - case shadow.LongTermNetDebtIssuance == nil: - return missingMemberError("BulkCashFlowStatement", "longTermNetDebtIssuance") - case shadow.ShortTermNetDebtIssuance == nil: - return missingMemberError("BulkCashFlowStatement", "shortTermNetDebtIssuance") - case shadow.NetStockIssuance == nil: - return missingMemberError("BulkCashFlowStatement", "netStockIssuance") - case shadow.NetCommonStockIssuance == nil: - return missingMemberError("BulkCashFlowStatement", "netCommonStockIssuance") - case shadow.CommonStockIssuance == nil: - return missingMemberError("BulkCashFlowStatement", "commonStockIssuance") - case shadow.CommonStockRepurchased == nil: - return missingMemberError("BulkCashFlowStatement", "commonStockRepurchased") - case shadow.NetPreferredStockIssuance == nil: - return missingMemberError("BulkCashFlowStatement", "netPreferredStockIssuance") - case shadow.NetDividendsPaid == nil: - return missingMemberError("BulkCashFlowStatement", "netDividendsPaid") - case shadow.CommonDividendsPaid == nil: - return missingMemberError("BulkCashFlowStatement", "commonDividendsPaid") - case shadow.PreferredDividendsPaid == nil: - return missingMemberError("BulkCashFlowStatement", "preferredDividendsPaid") - case shadow.OtherFinancingActivities == nil: - return missingMemberError("BulkCashFlowStatement", "otherFinancingActivities") - case shadow.NetCashProvidedByFinancingActivities == nil: - return missingMemberError("BulkCashFlowStatement", "netCashProvidedByFinancingActivities") - case shadow.EffectOfForexChangesOnCash == nil: - return missingMemberError("BulkCashFlowStatement", "effectOfForexChangesOnCash") - case shadow.NetChangeInCash == nil: - return missingMemberError("BulkCashFlowStatement", "netChangeInCash") - case shadow.CashAtEndOfPeriod == nil: - return missingMemberError("BulkCashFlowStatement", "cashAtEndOfPeriod") - case shadow.CashAtBeginningOfPeriod == nil: - return missingMemberError("BulkCashFlowStatement", "cashAtBeginningOfPeriod") - case shadow.OperatingCashFlow == nil: - return missingMemberError("BulkCashFlowStatement", "operatingCashFlow") - case shadow.CapitalExpenditure == nil: - return missingMemberError("BulkCashFlowStatement", "capitalExpenditure") - case shadow.FreeCashFlow == nil: - return missingMemberError("BulkCashFlowStatement", "freeCashFlow") - case shadow.IncomeTaxesPaid == nil: - return missingMemberError("BulkCashFlowStatement", "incomeTaxesPaid") - case shadow.InterestPaid == nil: - return missingMemberError("BulkCashFlowStatement", "interestPaid") } *m = BulkCashFlowStatement{ Date: *shadow.Date, @@ -655,45 +577,45 @@ func (m *BulkCashFlowStatement) UnmarshalJSONFrom(dec *jsontext.Decoder) error { AcceptedDate: *shadow.AcceptedDate, FiscalYear: *shadow.FiscalYear, Period: *shadow.Period, - NetIncome: *shadow.NetIncome, - DepreciationAndAmortization: *shadow.DepreciationAndAmortization, - DeferredIncomeTax: *shadow.DeferredIncomeTax, - StockBasedCompensation: *shadow.StockBasedCompensation, - ChangeInWorkingCapital: *shadow.ChangeInWorkingCapital, - AccountsReceivables: *shadow.AccountsReceivables, - Inventory: *shadow.Inventory, - AccountsPayables: *shadow.AccountsPayables, - OtherWorkingCapital: *shadow.OtherWorkingCapital, - OtherNonCashItems: *shadow.OtherNonCashItems, - NetCashProvidedByOperatingActivities: *shadow.NetCashProvidedByOperatingActivities, - InvestmentsInPropertyPlantAndEquipment: *shadow.InvestmentsInPropertyPlantAndEquipment, - AcquisitionsNet: *shadow.AcquisitionsNet, - PurchasesOfInvestments: *shadow.PurchasesOfInvestments, - SalesMaturitiesOfInvestments: *shadow.SalesMaturitiesOfInvestments, - OtherInvestingActivities: *shadow.OtherInvestingActivities, - NetCashProvidedByInvestingActivities: *shadow.NetCashProvidedByInvestingActivities, - NetDebtIssuance: *shadow.NetDebtIssuance, - LongTermNetDebtIssuance: *shadow.LongTermNetDebtIssuance, - ShortTermNetDebtIssuance: *shadow.ShortTermNetDebtIssuance, - NetStockIssuance: *shadow.NetStockIssuance, - NetCommonStockIssuance: *shadow.NetCommonStockIssuance, - CommonStockIssuance: *shadow.CommonStockIssuance, - CommonStockRepurchased: *shadow.CommonStockRepurchased, - NetPreferredStockIssuance: *shadow.NetPreferredStockIssuance, - NetDividendsPaid: *shadow.NetDividendsPaid, - CommonDividendsPaid: *shadow.CommonDividendsPaid, - PreferredDividendsPaid: *shadow.PreferredDividendsPaid, - OtherFinancingActivities: *shadow.OtherFinancingActivities, - NetCashProvidedByFinancingActivities: *shadow.NetCashProvidedByFinancingActivities, - EffectOfForexChangesOnCash: *shadow.EffectOfForexChangesOnCash, - NetChangeInCash: *shadow.NetChangeInCash, - CashAtEndOfPeriod: *shadow.CashAtEndOfPeriod, - CashAtBeginningOfPeriod: *shadow.CashAtBeginningOfPeriod, - OperatingCashFlow: *shadow.OperatingCashFlow, - CapitalExpenditure: *shadow.CapitalExpenditure, - FreeCashFlow: *shadow.FreeCashFlow, - IncomeTaxesPaid: *shadow.IncomeTaxesPaid, - InterestPaid: *shadow.InterestPaid, + NetIncome: shadow.NetIncome, + DepreciationAndAmortization: shadow.DepreciationAndAmortization, + DeferredIncomeTax: shadow.DeferredIncomeTax, + StockBasedCompensation: shadow.StockBasedCompensation, + ChangeInWorkingCapital: shadow.ChangeInWorkingCapital, + AccountsReceivables: shadow.AccountsReceivables, + Inventory: shadow.Inventory, + AccountsPayables: shadow.AccountsPayables, + OtherWorkingCapital: shadow.OtherWorkingCapital, + OtherNonCashItems: shadow.OtherNonCashItems, + NetCashProvidedByOperatingActivities: shadow.NetCashProvidedByOperatingActivities, + InvestmentsInPropertyPlantAndEquipment: shadow.InvestmentsInPropertyPlantAndEquipment, + AcquisitionsNet: shadow.AcquisitionsNet, + PurchasesOfInvestments: shadow.PurchasesOfInvestments, + SalesMaturitiesOfInvestments: shadow.SalesMaturitiesOfInvestments, + OtherInvestingActivities: shadow.OtherInvestingActivities, + NetCashProvidedByInvestingActivities: shadow.NetCashProvidedByInvestingActivities, + NetDebtIssuance: shadow.NetDebtIssuance, + LongTermNetDebtIssuance: shadow.LongTermNetDebtIssuance, + ShortTermNetDebtIssuance: shadow.ShortTermNetDebtIssuance, + NetStockIssuance: shadow.NetStockIssuance, + NetCommonStockIssuance: shadow.NetCommonStockIssuance, + CommonStockIssuance: shadow.CommonStockIssuance, + CommonStockRepurchased: shadow.CommonStockRepurchased, + NetPreferredStockIssuance: shadow.NetPreferredStockIssuance, + NetDividendsPaid: shadow.NetDividendsPaid, + CommonDividendsPaid: shadow.CommonDividendsPaid, + PreferredDividendsPaid: shadow.PreferredDividendsPaid, + OtherFinancingActivities: shadow.OtherFinancingActivities, + NetCashProvidedByFinancingActivities: shadow.NetCashProvidedByFinancingActivities, + EffectOfForexChangesOnCash: shadow.EffectOfForexChangesOnCash, + NetChangeInCash: shadow.NetChangeInCash, + CashAtEndOfPeriod: shadow.CashAtEndOfPeriod, + CashAtBeginningOfPeriod: shadow.CashAtBeginningOfPeriod, + OperatingCashFlow: shadow.OperatingCashFlow, + CapitalExpenditure: shadow.CapitalExpenditure, + FreeCashFlow: shadow.FreeCashFlow, + IncomeTaxesPaid: shadow.IncomeTaxesPaid, + InterestPaid: shadow.InterestPaid, } return nil } @@ -701,48 +623,48 @@ func (m *BulkCashFlowStatement) UnmarshalJSONFrom(dec *jsontext.Decoder) error { // BulkCashFlowStatementGrowth is one worldwide bulk cash-flow-statement-growth // row. type BulkCashFlowStatementGrowth struct { - Symbol string `json:"symbol"` - Date Date `json:"date"` - FiscalYear string `json:"fiscalYear"` - Period string `json:"period"` - ReportedCurrency string `json:"reportedCurrency"` - GrowthNetIncome string `json:"growthNetIncome"` - GrowthDepreciationAndAmortization string `json:"growthDepreciationAndAmortization"` - GrowthDeferredIncomeTax string `json:"growthDeferredIncomeTax"` - GrowthStockBasedCompensation string `json:"growthStockBasedCompensation"` - GrowthChangeInWorkingCapital string `json:"growthChangeInWorkingCapital"` - GrowthAccountsReceivables string `json:"growthAccountsReceivables"` - GrowthInventory string `json:"growthInventory"` - GrowthAccountsPayables string `json:"growthAccountsPayables"` - GrowthOtherWorkingCapital string `json:"growthOtherWorkingCapital"` - GrowthOtherNonCashItems string `json:"growthOtherNonCashItems"` - GrowthNetCashProvidedByOperatingActivities string `json:"growthNetCashProvidedByOperatingActivites"` - GrowthInvestmentsInPropertyPlantAndEquipment string `json:"growthInvestmentsInPropertyPlantAndEquipment"` - GrowthAcquisitionsNet string `json:"growthAcquisitionsNet"` - GrowthPurchasesOfInvestments string `json:"growthPurchasesOfInvestments"` - GrowthSalesMaturitiesOfInvestments string `json:"growthSalesMaturitiesOfInvestments"` - GrowthOtherInvestingActivities string `json:"growthOtherInvestingActivites"` - GrowthNetCashUsedForInvestingActivities string `json:"growthNetCashUsedForInvestingActivites"` - GrowthDebtRepayment string `json:"growthDebtRepayment"` - GrowthCommonStockIssued string `json:"growthCommonStockIssued"` - GrowthCommonStockRepurchased string `json:"growthCommonStockRepurchased"` - GrowthDividendsPaid string `json:"growthDividendsPaid"` - GrowthOtherFinancingActivities string `json:"growthOtherFinancingActivites"` - GrowthNetCashUsedProvidedByFinancingActivities string `json:"growthNetCashUsedProvidedByFinancingActivities"` - GrowthEffectOfForexChangesOnCash string `json:"growthEffectOfForexChangesOnCash"` - GrowthNetChangeInCash string `json:"growthNetChangeInCash"` - GrowthCashAtEndOfPeriod string `json:"growthCashAtEndOfPeriod"` - GrowthCashAtBeginningOfPeriod string `json:"growthCashAtBeginningOfPeriod"` - GrowthOperatingCashFlow string `json:"growthOperatingCashFlow"` - GrowthCapitalExpenditure string `json:"growthCapitalExpenditure"` - GrowthFreeCashFlow string `json:"growthFreeCashFlow"` - GrowthNetDebtIssuance string `json:"growthNetDebtIssuance"` - GrowthLongTermNetDebtIssuance string `json:"growthLongTermNetDebtIssuance"` - GrowthShortTermNetDebtIssuance string `json:"growthShortTermNetDebtIssuance"` - GrowthNetStockIssuance string `json:"growthNetStockIssuance"` - GrowthPreferredDividendsPaid string `json:"growthPreferredDividendsPaid"` - GrowthIncomeTaxesPaid string `json:"growthIncomeTaxesPaid"` - GrowthInterestPaid string `json:"growthInterestPaid"` + Symbol string `json:"symbol"` + Date Date `json:"date"` + FiscalYear string `json:"fiscalYear"` + Period string `json:"period"` + ReportedCurrency string `json:"reportedCurrency"` + GrowthNetIncome *string `json:"growthNetIncome"` + GrowthDepreciationAndAmortization *string `json:"growthDepreciationAndAmortization"` + GrowthDeferredIncomeTax *string `json:"growthDeferredIncomeTax"` + GrowthStockBasedCompensation *string `json:"growthStockBasedCompensation"` + GrowthChangeInWorkingCapital *string `json:"growthChangeInWorkingCapital"` + GrowthAccountsReceivables *string `json:"growthAccountsReceivables"` + GrowthInventory *string `json:"growthInventory"` + GrowthAccountsPayables *string `json:"growthAccountsPayables"` + GrowthOtherWorkingCapital *string `json:"growthOtherWorkingCapital"` + GrowthOtherNonCashItems *string `json:"growthOtherNonCashItems"` + GrowthNetCashProvidedByOperatingActivities *string `json:"growthNetCashProvidedByOperatingActivites"` + GrowthInvestmentsInPropertyPlantAndEquipment *string `json:"growthInvestmentsInPropertyPlantAndEquipment"` + GrowthAcquisitionsNet *string `json:"growthAcquisitionsNet"` + GrowthPurchasesOfInvestments *string `json:"growthPurchasesOfInvestments"` + GrowthSalesMaturitiesOfInvestments *string `json:"growthSalesMaturitiesOfInvestments"` + GrowthOtherInvestingActivities *string `json:"growthOtherInvestingActivites"` + GrowthNetCashUsedForInvestingActivities *string `json:"growthNetCashUsedForInvestingActivites"` + GrowthDebtRepayment *string `json:"growthDebtRepayment"` + GrowthCommonStockIssued *string `json:"growthCommonStockIssued"` + GrowthCommonStockRepurchased *string `json:"growthCommonStockRepurchased"` + GrowthDividendsPaid *string `json:"growthDividendsPaid"` + GrowthOtherFinancingActivities *string `json:"growthOtherFinancingActivites"` + GrowthNetCashUsedProvidedByFinancingActivities *string `json:"growthNetCashUsedProvidedByFinancingActivities"` + GrowthEffectOfForexChangesOnCash *string `json:"growthEffectOfForexChangesOnCash"` + GrowthNetChangeInCash *string `json:"growthNetChangeInCash"` + GrowthCashAtEndOfPeriod *string `json:"growthCashAtEndOfPeriod"` + GrowthCashAtBeginningOfPeriod *string `json:"growthCashAtBeginningOfPeriod"` + GrowthOperatingCashFlow *string `json:"growthOperatingCashFlow"` + GrowthCapitalExpenditure *string `json:"growthCapitalExpenditure"` + GrowthFreeCashFlow *string `json:"growthFreeCashFlow"` + GrowthNetDebtIssuance *string `json:"growthNetDebtIssuance"` + GrowthLongTermNetDebtIssuance *string `json:"growthLongTermNetDebtIssuance"` + GrowthShortTermNetDebtIssuance *string `json:"growthShortTermNetDebtIssuance"` + GrowthNetStockIssuance *string `json:"growthNetStockIssuance"` + GrowthPreferredDividendsPaid *string `json:"growthPreferredDividendsPaid"` + GrowthIncomeTaxesPaid *string `json:"growthIncomeTaxesPaid"` + GrowthInterestPaid *string `json:"growthInterestPaid"` } // bulkCashFlowStatementGrowthShadow mirrors BulkCashFlowStatementGrowth with a @@ -812,80 +734,6 @@ func (m *BulkCashFlowStatementGrowth) UnmarshalJSONFrom(dec *jsontext.Decoder) e return missingMemberError("BulkCashFlowStatementGrowth", "period") case shadow.ReportedCurrency == nil: return missingMemberError("BulkCashFlowStatementGrowth", "reportedCurrency") - case shadow.GrowthNetIncome == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthNetIncome") - case shadow.GrowthDepreciationAndAmortization == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthDepreciationAndAmortization") - case shadow.GrowthDeferredIncomeTax == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthDeferredIncomeTax") - case shadow.GrowthStockBasedCompensation == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthStockBasedCompensation") - case shadow.GrowthChangeInWorkingCapital == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthChangeInWorkingCapital") - case shadow.GrowthAccountsReceivables == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthAccountsReceivables") - case shadow.GrowthInventory == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthInventory") - case shadow.GrowthAccountsPayables == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthAccountsPayables") - case shadow.GrowthOtherWorkingCapital == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthOtherWorkingCapital") - case shadow.GrowthOtherNonCashItems == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthOtherNonCashItems") - case shadow.GrowthNetCashProvidedByOperatingActivities == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthNetCashProvidedByOperatingActivites") - case shadow.GrowthInvestmentsInPropertyPlantAndEquipment == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthInvestmentsInPropertyPlantAndEquipment") - case shadow.GrowthAcquisitionsNet == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthAcquisitionsNet") - case shadow.GrowthPurchasesOfInvestments == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthPurchasesOfInvestments") - case shadow.GrowthSalesMaturitiesOfInvestments == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthSalesMaturitiesOfInvestments") - case shadow.GrowthOtherInvestingActivities == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthOtherInvestingActivites") - case shadow.GrowthNetCashUsedForInvestingActivities == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthNetCashUsedForInvestingActivites") - case shadow.GrowthDebtRepayment == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthDebtRepayment") - case shadow.GrowthCommonStockIssued == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthCommonStockIssued") - case shadow.GrowthCommonStockRepurchased == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthCommonStockRepurchased") - case shadow.GrowthDividendsPaid == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthDividendsPaid") - case shadow.GrowthOtherFinancingActivities == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthOtherFinancingActivites") - case shadow.GrowthNetCashUsedProvidedByFinancingActivities == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthNetCashUsedProvidedByFinancingActivities") - case shadow.GrowthEffectOfForexChangesOnCash == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthEffectOfForexChangesOnCash") - case shadow.GrowthNetChangeInCash == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthNetChangeInCash") - case shadow.GrowthCashAtEndOfPeriod == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthCashAtEndOfPeriod") - case shadow.GrowthCashAtBeginningOfPeriod == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthCashAtBeginningOfPeriod") - case shadow.GrowthOperatingCashFlow == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthOperatingCashFlow") - case shadow.GrowthCapitalExpenditure == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthCapitalExpenditure") - case shadow.GrowthFreeCashFlow == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthFreeCashFlow") - case shadow.GrowthNetDebtIssuance == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthNetDebtIssuance") - case shadow.GrowthLongTermNetDebtIssuance == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthLongTermNetDebtIssuance") - case shadow.GrowthShortTermNetDebtIssuance == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthShortTermNetDebtIssuance") - case shadow.GrowthNetStockIssuance == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthNetStockIssuance") - case shadow.GrowthPreferredDividendsPaid == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthPreferredDividendsPaid") - case shadow.GrowthIncomeTaxesPaid == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthIncomeTaxesPaid") - case shadow.GrowthInterestPaid == nil: - return missingMemberError("BulkCashFlowStatementGrowth", "growthInterestPaid") } *m = BulkCashFlowStatementGrowth{ Symbol: *shadow.Symbol, @@ -893,57 +741,57 @@ func (m *BulkCashFlowStatementGrowth) UnmarshalJSONFrom(dec *jsontext.Decoder) e FiscalYear: *shadow.FiscalYear, Period: *shadow.Period, ReportedCurrency: *shadow.ReportedCurrency, - GrowthNetIncome: *shadow.GrowthNetIncome, - GrowthDepreciationAndAmortization: *shadow.GrowthDepreciationAndAmortization, - GrowthDeferredIncomeTax: *shadow.GrowthDeferredIncomeTax, - GrowthStockBasedCompensation: *shadow.GrowthStockBasedCompensation, - GrowthChangeInWorkingCapital: *shadow.GrowthChangeInWorkingCapital, - GrowthAccountsReceivables: *shadow.GrowthAccountsReceivables, - GrowthInventory: *shadow.GrowthInventory, - GrowthAccountsPayables: *shadow.GrowthAccountsPayables, - GrowthOtherWorkingCapital: *shadow.GrowthOtherWorkingCapital, - GrowthOtherNonCashItems: *shadow.GrowthOtherNonCashItems, - GrowthNetCashProvidedByOperatingActivities: *shadow.GrowthNetCashProvidedByOperatingActivities, - GrowthInvestmentsInPropertyPlantAndEquipment: *shadow.GrowthInvestmentsInPropertyPlantAndEquipment, - GrowthAcquisitionsNet: *shadow.GrowthAcquisitionsNet, - GrowthPurchasesOfInvestments: *shadow.GrowthPurchasesOfInvestments, - GrowthSalesMaturitiesOfInvestments: *shadow.GrowthSalesMaturitiesOfInvestments, - GrowthOtherInvestingActivities: *shadow.GrowthOtherInvestingActivities, - GrowthNetCashUsedForInvestingActivities: *shadow.GrowthNetCashUsedForInvestingActivities, - GrowthDebtRepayment: *shadow.GrowthDebtRepayment, - GrowthCommonStockIssued: *shadow.GrowthCommonStockIssued, - GrowthCommonStockRepurchased: *shadow.GrowthCommonStockRepurchased, - GrowthDividendsPaid: *shadow.GrowthDividendsPaid, - GrowthOtherFinancingActivities: *shadow.GrowthOtherFinancingActivities, - GrowthNetCashUsedProvidedByFinancingActivities: *shadow.GrowthNetCashUsedProvidedByFinancingActivities, - GrowthEffectOfForexChangesOnCash: *shadow.GrowthEffectOfForexChangesOnCash, - GrowthNetChangeInCash: *shadow.GrowthNetChangeInCash, - GrowthCashAtEndOfPeriod: *shadow.GrowthCashAtEndOfPeriod, - GrowthCashAtBeginningOfPeriod: *shadow.GrowthCashAtBeginningOfPeriod, - GrowthOperatingCashFlow: *shadow.GrowthOperatingCashFlow, - GrowthCapitalExpenditure: *shadow.GrowthCapitalExpenditure, - GrowthFreeCashFlow: *shadow.GrowthFreeCashFlow, - GrowthNetDebtIssuance: *shadow.GrowthNetDebtIssuance, - GrowthLongTermNetDebtIssuance: *shadow.GrowthLongTermNetDebtIssuance, - GrowthShortTermNetDebtIssuance: *shadow.GrowthShortTermNetDebtIssuance, - GrowthNetStockIssuance: *shadow.GrowthNetStockIssuance, - GrowthPreferredDividendsPaid: *shadow.GrowthPreferredDividendsPaid, - GrowthIncomeTaxesPaid: *shadow.GrowthIncomeTaxesPaid, - GrowthInterestPaid: *shadow.GrowthInterestPaid, + GrowthNetIncome: shadow.GrowthNetIncome, + GrowthDepreciationAndAmortization: shadow.GrowthDepreciationAndAmortization, + GrowthDeferredIncomeTax: shadow.GrowthDeferredIncomeTax, + GrowthStockBasedCompensation: shadow.GrowthStockBasedCompensation, + GrowthChangeInWorkingCapital: shadow.GrowthChangeInWorkingCapital, + GrowthAccountsReceivables: shadow.GrowthAccountsReceivables, + GrowthInventory: shadow.GrowthInventory, + GrowthAccountsPayables: shadow.GrowthAccountsPayables, + GrowthOtherWorkingCapital: shadow.GrowthOtherWorkingCapital, + GrowthOtherNonCashItems: shadow.GrowthOtherNonCashItems, + GrowthNetCashProvidedByOperatingActivities: shadow.GrowthNetCashProvidedByOperatingActivities, + GrowthInvestmentsInPropertyPlantAndEquipment: shadow.GrowthInvestmentsInPropertyPlantAndEquipment, + GrowthAcquisitionsNet: shadow.GrowthAcquisitionsNet, + GrowthPurchasesOfInvestments: shadow.GrowthPurchasesOfInvestments, + GrowthSalesMaturitiesOfInvestments: shadow.GrowthSalesMaturitiesOfInvestments, + GrowthOtherInvestingActivities: shadow.GrowthOtherInvestingActivities, + GrowthNetCashUsedForInvestingActivities: shadow.GrowthNetCashUsedForInvestingActivities, + GrowthDebtRepayment: shadow.GrowthDebtRepayment, + GrowthCommonStockIssued: shadow.GrowthCommonStockIssued, + GrowthCommonStockRepurchased: shadow.GrowthCommonStockRepurchased, + GrowthDividendsPaid: shadow.GrowthDividendsPaid, + GrowthOtherFinancingActivities: shadow.GrowthOtherFinancingActivities, + GrowthNetCashUsedProvidedByFinancingActivities: shadow.GrowthNetCashUsedProvidedByFinancingActivities, + GrowthEffectOfForexChangesOnCash: shadow.GrowthEffectOfForexChangesOnCash, + GrowthNetChangeInCash: shadow.GrowthNetChangeInCash, + GrowthCashAtEndOfPeriod: shadow.GrowthCashAtEndOfPeriod, + GrowthCashAtBeginningOfPeriod: shadow.GrowthCashAtBeginningOfPeriod, + GrowthOperatingCashFlow: shadow.GrowthOperatingCashFlow, + GrowthCapitalExpenditure: shadow.GrowthCapitalExpenditure, + GrowthFreeCashFlow: shadow.GrowthFreeCashFlow, + GrowthNetDebtIssuance: shadow.GrowthNetDebtIssuance, + GrowthLongTermNetDebtIssuance: shadow.GrowthLongTermNetDebtIssuance, + GrowthShortTermNetDebtIssuance: shadow.GrowthShortTermNetDebtIssuance, + GrowthNetStockIssuance: shadow.GrowthNetStockIssuance, + GrowthPreferredDividendsPaid: shadow.GrowthPreferredDividendsPaid, + GrowthIncomeTaxesPaid: shadow.GrowthIncomeTaxesPaid, + GrowthInterestPaid: shadow.GrowthInterestPaid, } return nil } // BulkEodBar is one worldwide bulk end-of-day price row. type BulkEodBar struct { - Symbol string `json:"symbol"` - Date Date `json:"date"` - Open string `json:"open"` - Low string `json:"low"` - High string `json:"high"` - Close string `json:"close"` - AdjClose string `json:"adjClose"` - Volume string `json:"volume"` + Symbol string `json:"symbol"` + Date Date `json:"date"` + Open *string `json:"open"` + Low *string `json:"low"` + High *string `json:"high"` + Close *string `json:"close"` + AdjClose *string `json:"adjClose"` + Volume *string `json:"volume"` } // bulkEodBarShadow mirrors BulkEodBar with a pointer or raw value for every @@ -972,28 +820,16 @@ func (m *BulkEodBar) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("BulkEodBar", "symbol") case shadow.Date == nil: return missingMemberError("BulkEodBar", "date") - case shadow.Open == nil: - return missingMemberError("BulkEodBar", "open") - case shadow.Low == nil: - return missingMemberError("BulkEodBar", "low") - case shadow.High == nil: - return missingMemberError("BulkEodBar", "high") - case shadow.Close == nil: - return missingMemberError("BulkEodBar", "close") - case shadow.AdjClose == nil: - return missingMemberError("BulkEodBar", "adjClose") - case shadow.Volume == nil: - return missingMemberError("BulkEodBar", "volume") } *m = BulkEodBar{ Symbol: *shadow.Symbol, Date: *shadow.Date, - Open: *shadow.Open, - Low: *shadow.Low, - High: *shadow.High, - Close: *shadow.Close, - AdjClose: *shadow.AdjClose, - Volume: *shadow.Volume, + Open: shadow.Open, + Low: shadow.Low, + High: shadow.High, + Close: shadow.Close, + AdjClose: shadow.AdjClose, + Volume: shadow.Volume, } return nil } diff --git a/sdk/go/bulk_parity_test.go b/sdk/go/bulk_parity_test.go index 3d70c681..f0b58ebc 100644 --- a/sdk/go/bulk_parity_test.go +++ b/sdk/go/bulk_parity_test.go @@ -171,7 +171,7 @@ func TestBulkStatementsDecodeExactValuesAndKeepProviderTypos(t *testing.T) { cashFlow := assertCSVFixtureParity[BulkCashFlowStatement](t, "bulk_cash_flow_statements.csv", 47) if cashFlow[0].CIK != "0000000000" || cashFlow[0].AcceptedDate != mustParseDateTime(t, "2024-03-30 20:00:00") || - cashFlow[0].Period != "Q1" || cashFlow[0].NetIncome != "14932000000" { + cashFlow[0].Period != "Q1" || cellText(cashFlow[0].NetIncome) != "14932000000" { t.Fatalf("bulk_cash_flow_statements = %+v", cashFlow[0]) } cashGrowth := assertCSVFixtureParity[BulkCashFlowStatementGrowth](t, "bulk_cash_flow_statement_growth.csv", 42) @@ -184,8 +184,8 @@ func TestBulkStatementsDecodeExactValuesAndKeepProviderTypos(t *testing.T) { } eod := assertCSVFixtureParity[BulkEodBar](t, "bulk_eod.csv", 8) - if want := (BulkEodBar{Symbol: "HKDCNH", Date: mustParseDate(t, "2025-06-02"), Open: "0.91858", Low: "0.91692", - High: "0.9186", Close: "0.91741", AdjClose: "0.91741", Volume: "0"}); eod[1] != want { + if eod[1].Symbol != "HKDCNH" || eod[1].Date != mustParseDate(t, "2025-06-02") || + cellText(eod[1].Open) != "0.91858" || cellText(eod[1].Volume) != "0" { t.Fatalf("bulk_eod = %+v", eod[1]) } } diff --git a/sdk/go/bulk_test.go b/sdk/go/bulk_test.go index cd1ecc2f..4612c00c 100644 --- a/sdk/go/bulk_test.go +++ b/sdk/go/bulk_test.go @@ -118,15 +118,15 @@ func TestBulkMethodsUseExactPathsAndWireParameterOrder(t *testing.T) { t.Fatalf("BalanceSheetStatementGrowth = %+v, %v", balanceGrowth, err) } cashFlow, err := client.Bulk.CashFlowStatements(ctx, NewBulkStatementQuery(4_294_967_295, FiscalPeriodFullYear)) - if err != nil || len(cashFlow) != 2 || cashFlow[0].NetIncome != "14932000000" { + if err != nil || len(cashFlow) != 2 || cellText(cashFlow[0].NetIncome) != "14932000000" { t.Fatalf("CashFlowStatements = %+v, %v", cashFlow, err) } cashGrowth, err := client.Bulk.CashFlowStatementGrowth(ctx, NewBulkStatementQuery(2025, FiscalPeriodFullYear)) - if err != nil || len(cashGrowth) != 2 || cashGrowth[0].GrowthNetIncome != "-0.04191152728446884" { + if err != nil || len(cashGrowth) != 2 || cellText(cashGrowth[0].GrowthNetIncome) != "-0.04191152728446884" { t.Fatalf("CashFlowStatementGrowth = %+v, %v", cashGrowth, err) } eod, err := client.Bulk.Eod(ctx, NewBulkEodQuery(mustParseDate(t, "2024-10-22"))) - if err != nil || len(eod) != 2 || eod[1].Close != "0.91741" { + if err != nil || len(eod) != 2 || cellText(eod[1].Close) != "0.91741" { t.Fatalf("Eod = %+v, %v", eod, err) } From 71d206239b093a52bdc4d33dca4e322bdf8f0999 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 11:33:50 +0800 Subject: [PATCH 27/31] fix(company)!: make profile isin, country and change optional The live profile-bulk census over 46.7k rows surfaced empty cells that the previous non-optional CompanyProfile members could not represent: isin was empty in 33 rows, country in 7, and both change and changePercentage in 2. Since CompanyProfile is shared by company.profile (JSON) and profile-bulk (CSV), these gaps affect both endpoints. isin now decodes as Option and country as Option through the empty_or_null codec, so an empty string or null becomes None. change and change_percentage become Option through required_option, keeping the key required while allowing an absent value. Python exposes these as Optional and Go as pointers, with the Go shadow decoding raw jsontext and mapping empty strings to nil. The bulk fixture gains the live ACCV row (empty change) and CFHAX row (empty isin, country, ceo and website), and the company and bulk tests across the Rust, Python and Go SDKs assert the new optional types. Refs #376 BREAKING CHANGE: CompanyProfile.isin, country, change and change_percentage are now optional in Rust, Python and Go, affecting both company.profile and bulk company_profiles. --- .../python/fmp/_native/company/__init__.pyi | 16 +-- crates/fmp-py/src/models/company.rs | 20 +-- crates/fmp-py/tests/test_bulk_a.py | 4 +- crates/libfmp/src/responses/company.rs | 12 +- .../libfmp/tests/bulk_snapshot_responses.rs | 21 +++ crates/libfmp/tests/company_responses.rs | 8 +- .../tests/fixtures/bulk_company_profiles.csv | 2 + sdk/go/bulk_parity_test.go | 4 + sdk/go/bulk_test.go | 2 +- sdk/go/company_models.go | 132 +++++++++++------- sdk/go/company_parity_test.go | 6 +- 11 files changed, 148 insertions(+), 79 deletions(-) diff --git a/crates/fmp-py/python/fmp/_native/company/__init__.pyi b/crates/fmp-py/python/fmp/_native/company/__init__.pyi index 9315aafa..1feed9e7 100644 --- a/crates/fmp-py/python/fmp/_native/company/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/company/__init__.pyi @@ -322,9 +322,9 @@ class CompanyProfile: @property def range(self) -> builtins.str: ... @property - def change(self) -> builtins.float: ... + def change(self) -> typing.Optional[builtins.float]: ... @property - def change_percentage(self) -> builtins.float: ... + def change_percentage(self) -> typing.Optional[builtins.float]: ... @property def volume(self) -> builtins.float: ... @property @@ -336,7 +336,7 @@ class CompanyProfile: @property def cik(self) -> typing.Optional[builtins.str]: ... @property - def isin(self) -> builtins.str: ... + def isin(self) -> typing.Optional[builtins.str]: ... @property def cusip(self) -> typing.Optional[builtins.str]: ... @property @@ -354,7 +354,7 @@ class CompanyProfile: @property def sector(self) -> builtins.str: ... @property - def country(self) -> builtins.str: ... + def country(self) -> typing.Optional[builtins.str]: ... @property def full_time_employees(self) -> typing.Optional[builtins.str]: ... @property @@ -391,14 +391,14 @@ class CompanyProfile: beta: builtins.float, last_dividend: builtins.float, range: builtins.str, - change: builtins.float, - change_percentage: builtins.float, + change: typing.Optional[builtins.float], + change_percentage: typing.Optional[builtins.float], volume: builtins.float, average_volume: builtins.float, company_name: builtins.str, currency: builtins.str, cik: typing.Optional[builtins.str], - isin: builtins.str, + isin: typing.Optional[builtins.str], cusip: typing.Optional[builtins.str], exchange_full_name: builtins.str, exchange: builtins.str, @@ -407,7 +407,7 @@ class CompanyProfile: description: builtins.str, ceo: builtins.str, sector: builtins.str, - country: builtins.str, + country: typing.Optional[builtins.str], full_time_employees: typing.Optional[builtins.str], phone: typing.Optional[builtins.str], address: builtins.str, diff --git a/crates/fmp-py/src/models/company.rs b/crates/fmp-py/src/models/company.rs index 00397929..166e447f 100644 --- a/crates/fmp-py/src/models/company.rs +++ b/crates/fmp-py/src/models/company.rs @@ -22,9 +22,9 @@ pub(crate) struct CompanyProfile { #[pyo3(get)] pub range: String, #[pyo3(get)] - pub change: f64, + pub change: Option, #[pyo3(get)] - pub change_percentage: f64, + pub change_percentage: Option, #[pyo3(get)] pub volume: f64, #[pyo3(get)] @@ -36,7 +36,7 @@ pub(crate) struct CompanyProfile { #[pyo3(get)] pub cik: Option, #[pyo3(get)] - pub isin: String, + pub isin: Option, #[pyo3(get)] pub cusip: Option, #[pyo3(get)] @@ -54,7 +54,7 @@ pub(crate) struct CompanyProfile { #[pyo3(get)] pub sector: String, #[pyo3(get)] - pub country: String, + pub country: Option, #[pyo3(get)] pub full_time_employees: Option, #[pyo3(get)] @@ -97,14 +97,14 @@ impl CompanyProfile { beta: f64, last_dividend: f64, range: String, - change: f64, - change_percentage: f64, + change: Option, + change_percentage: Option, volume: f64, average_volume: f64, company_name: String, currency: String, cik: Option, - isin: String, + isin: Option, cusip: Option, exchange_full_name: String, exchange: String, @@ -113,7 +113,7 @@ impl CompanyProfile { description: String, ceo: String, sector: String, - country: String, + country: Option, full_time_employees: Option, phone: Option, address: String, @@ -529,7 +529,7 @@ impl From for CompanyProfile { company_name: value.company_name, currency: value.currency.into_inner(), cik: value.cik.map(|value| value.into_inner()), - isin: value.isin.into_inner(), + isin: value.isin.map(|value| value.into_inner()), cusip: value.cusip.map(|value| value.into_inner()), exchange_full_name: value.exchange_full_name, exchange: value.exchange.into_inner(), @@ -538,7 +538,7 @@ impl From for CompanyProfile { description: value.description, ceo: value.ceo, sector: value.sector.into_inner(), - country: value.country.into_inner(), + country: value.country.map(|value| value.into_inner()), full_time_employees: value.full_time_employees.map(|value| value.into_inner()), phone: value.phone, address: value.address, diff --git a/crates/fmp-py/tests/test_bulk_a.py b/crates/fmp-py/tests/test_bulk_a.py index 3ffbd52b..2e78395e 100644 --- a/crates/fmp-py/tests/test_bulk_a.py +++ b/crates/fmp-py/tests/test_bulk_a.py @@ -42,7 +42,7 @@ def test_company_profiles_form_encodes_the_part(client: Any, fixture_server: Fix rows = client.bulk.company_profiles("segment 0/alpha") assert fixture_server.requests[0].target == "/profile-bulk?part=segment+0%2Falpha" - assert len(rows) == 2 + assert len(rows) == 4 row = rows[0] assert isinstance(row, CompanyProfile) assert row.symbol == "WMB" @@ -50,6 +50,8 @@ def test_company_profiles_form_encodes_the_part(client: Any, fixture_server: Fix assert row.price == pytest.approx(67.78) assert row.market_cap == 82_906_291_252 assert row.is_etf is False + assert (rows[2].symbol, rows[2].change, rows[2].change_percentage) == ("ACCV", None, None) + assert (rows[3].symbol, rows[3].isin, rows[3].country) == ("CFHAX", None, None) def test_company_profiles_with_a_numeric_part(client: Any, fixture_server: FixtureServer) -> None: diff --git a/crates/libfmp/src/responses/company.rs b/crates/libfmp/src/responses/company.rs index fdf4a19d..a7de0755 100644 --- a/crates/libfmp/src/responses/company.rs +++ b/crates/libfmp/src/responses/company.rs @@ -37,8 +37,10 @@ pub struct CompanyProfile { #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] pub last_dividend: PerShareAmount, pub range: String, - pub change: Change, - pub change_percentage: Percentage, + #[serde(deserialize_with = "required_option")] + pub change: Option, + #[serde(deserialize_with = "required_option")] + pub change_percentage: Option, #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] pub volume: Volume, #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] @@ -47,7 +49,8 @@ pub struct CompanyProfile { pub currency: CurrencyCode, #[serde(deserialize_with = "required_option")] pub cik: Option, - pub isin: Isin, + #[serde(deserialize_with = "crate::codecs::empty_or_null::deserialize")] + pub isin: Option, #[serde(deserialize_with = "required_option")] pub cusip: Option, pub exchange_full_name: String, @@ -57,7 +60,8 @@ pub struct CompanyProfile { pub description: String, pub ceo: String, pub sector: Sector, - pub country: CountryCode, + #[serde(deserialize_with = "crate::codecs::empty_or_null::deserialize")] + pub country: Option, #[serde(deserialize_with = "required_option")] pub full_time_employees: Option, #[serde(deserialize_with = "required_option")] diff --git a/crates/libfmp/tests/bulk_snapshot_responses.rs b/crates/libfmp/tests/bulk_snapshot_responses.rs index d1b4a14f..bb483028 100644 --- a/crates/libfmp/tests/bulk_snapshot_responses.rs +++ b/crates/libfmp/tests/bulk_snapshot_responses.rs @@ -46,13 +46,17 @@ fn every_column_is_required_and_only_identity_members_reject_an_empty_cell() { PROFILE, &[ "range", + "change", + "changePercentage", "companyName", "cik", + "isin", "cusip", "exchangeFullName", "website", "description", "ceo", + "country", "fullTimeEmployees", "phone", "address", @@ -129,3 +133,20 @@ fn live_empty_cells_decode_as_absent_members_and_empty_text() { assert_eq!(etf.isin, None); assert_eq!(etf.shares_number.unwrap().as_str(), "0"); } + +#[test] +fn live_profile_rows_with_empty_codes_and_changes_decode_as_absent() { + let profiles = decode(&bulk_company_profiles(part()), PROFILE).unwrap(); + let accelevation = &profiles[2]; + assert_eq!(accelevation.symbol, Ticker::new("ACCV").unwrap()); + assert_eq!(accelevation.change, None); + assert_eq!(accelevation.change_percentage, None); + assert_eq!(accelevation.range, ""); + + let catalyst = &profiles[3]; + assert_eq!(catalyst.symbol, Ticker::new("CFHAX").unwrap()); + assert_eq!(catalyst.isin, None); + assert_eq!(catalyst.country, None); + assert_eq!(catalyst.ceo, ""); + assert_eq!(catalyst.website, ""); +} diff --git a/crates/libfmp/tests/company_responses.rs b/crates/libfmp/tests/company_responses.rs index 3fc23030..33b09bc4 100644 --- a/crates/libfmp/tests/company_responses.rs +++ b/crates/libfmp/tests/company_responses.rs @@ -13,14 +13,14 @@ fn documented_company_profile_decodes_every_exact_field_and_wire_type() { assert_eq!(row.beta, 1.097); assert_eq!(row.last_dividend, 1.05); assert_eq!(row.range, "201.5-344.57"); - assert_eq!(row.change, -6.33498); - assert_eq!(row.change_percentage, -1.8732); + assert_eq!(row.change, Some(-6.33498)); + assert_eq!(row.change_percentage, Some(-1.8732)); assert_eq!(row.volume, 28_718_014.0); assert_eq!(row.average_volume, 55_309_000.0); assert_eq!(row.company_name, "Apple Inc."); assert_eq!(row.currency.as_str(), "USD"); assert_eq!(row.cik.as_ref().unwrap().as_str(), "0000320193"); - assert_eq!(row.isin.as_str(), "US0378331005"); + assert_eq!(row.isin.as_ref().unwrap().as_str(), "US0378331005"); assert_eq!(row.cusip.as_ref().unwrap().as_str(), "037833100"); assert_eq!(row.exchange_full_name, "NASDAQ Global Select"); assert_eq!(row.exchange.as_str(), "NASDAQ"); @@ -32,7 +32,7 @@ fn documented_company_profile_decodes_every_exact_field_and_wire_type() { ); assert_eq!(row.ceo, "Timothy D. Cook"); assert_eq!(row.sector.as_str(), "Technology"); - assert_eq!(row.country.as_str(), "US"); + assert_eq!(row.country.as_ref().unwrap().as_str(), "US"); assert_eq!(row.full_time_employees.as_ref().unwrap().as_str(), "166000"); assert_eq!(row.phone.as_deref(), Some("(408) 996-1010")); assert_eq!(row.address, "One Apple Park Way"); diff --git a/crates/libfmp/tests/fixtures/bulk_company_profiles.csv b/crates/libfmp/tests/fixtures/bulk_company_profiles.csv index f3e605ad..7c9d11ba 100644 --- a/crates/libfmp/tests/fixtures/bulk_company_profiles.csv +++ b/crates/libfmp/tests/fixtures/bulk_company_profiles.csv @@ -1,3 +1,5 @@ "symbol","price","marketCap","beta","lastDividend","range","change","changePercentage","volume","averageVolume","companyName","currency","cik","isin","cusip","exchangeFullName","exchange","industry","website","description","ceo","sector","country","fullTimeEmployees","phone","address","city","state","zip","image","ipoDate","defaultImage","isEtf","isActivelyTrading","isAdr","isFund" "WMB",67.78,82906291252,0.618,2.075,"55.56-80.08",-0.85,-1.23853,2020441,7138175,"The Williams Companies, Inc.","USD","0000107263","US9694571004","969457100","New York Stock Exchange","NYSE","Oil & Gas Midstream","https://www.williams.com","The Williams Companies, Inc., alongside its subsidiaries, operates as a prominent energy infrastructure entity, primarily conducting business throughout the United States. The company’s operations are organized into four key segments: Transmission & Gulf of Mexico, Northeast G&P, West, and Gas & NGL Marketing Services. The Transmission & Gulf of Mexico division manages crucial natural gas pipelines such as Transco and Northwest, in addition to natural gas gathering and processing, and crude oil production handling and transportation assets situated in the Gulf Coast. This segment also oversees various petrochemical and feedstock pipelines. Focusing on midstream activities, the Northeast G&P segment handles gathering, processing, and fractionation within the Marcellus Shale region, predominantly in Pennsylvania and New York, and the Utica Shale region of eastern Ohio. The West segment delivers gas gathering, processing, and treating services across the Rocky Mountain areas of Colorado and Wyoming, the Barnett Shale in north-central Texas, the Eagle Ford Shale in South Texas, the Haynesville Shale in northwest Louisiana, and the expansive Mid-Continent region (including the Anadarko, Arkoma, and Permian basins). This segment also operates natural gas liquid (NGL) fractionation and storage facilities located near Conway in central Kansas. The Gas & NGL Marketing Services segment provides comprehensive wholesale marketing, trading, storage, and transportation of natural gas to utilities, municipalities, power generators, and producers, while also offering risk and asset management and NGL marketing services. The company possesses and operates an extensive network, including 30,000 miles of pipelines, 29 processing facilities, 7 fractionation facilities, and an approximate NGL storage capacity of 23 million barrels. The Williams Companies, Inc. was established in 1908 and maintains its headquarters in Tulsa, Oklahoma.","Chad J. Zamarin","Energy","US","5987","800-945-5426","One Williams Center","Tulsa","OK","74172","https://images.financialmodelingprep.com/symbol/WMB.png","1981-12-31",false,false,true,false,false "AMAT",508.787,403956017733,1.597,1.98,"203.4-739.67",-3.223,-0.62948,1689500,8607662,"Applied Materials, Inc.","USD","0000006951","US0382221051","038222105","NASDAQ","NASDAQ","Semiconductors","https://www.appliedmaterials.com","Applied Materials, Inc. engages in provision of materials engineering solutions used to produce semiconductors. The firm also focuses on design, development, production, and servicing of the critical wafer fabrication tools used for customers to manufacture semiconductors. It operates through the following segments: Semiconductor Systems and Applied Global Services (AGS). The Semiconductor Systems segment includes designing, development, manufacturing and sale of equipment used to fabricate semiconductor chips referred to as integrated circuits. The AGS segment engages in provision of services, spares, and factory automation software to customer fabrication plants globally. The company was founded on November 10, 1967 and is headquartered in Santa Clara, CA.","Gary E. Dickerson","Technology","US","36500","408 727 5555","3050 Bowers Ave","Santa Clara","CA","95052-8039","https://images.financialmodelingprep.com/symbol/AMAT.png","1980-03-17",false,false,true,false,false +"ACCV",18,540000000,0,0,,,,0,0,"Accelevation Holdings Corp.","USD",,"US0049661079","004966107","NASDAQ","NASDAQ","Electrical Equipment & Parts","http://www.accelevation.com","Accelevation Holdings Corp. engages in the design, manufacture and installation of power distribution and white space infrastructure products for mission-critical environments. The company was founded by Michael Rubiera and Shawn Rubiera in 2017 and is headquartered in Miamisburg, OH.",,"Industrials","US",,,"9555 N. Springboro Pike, Suite 400","Miamisburg","OH","45342","https://images.financialmodelingprep.com/symbol/ACCV.png","2026-09-30",true,false,true,false,false +"CFHAX",9.8534,0,0,0,"9.51-11.07",-0.0216,-0.21873,0,0,"Catalyst Hedged Commodity Strategy Fund Class A","USD","0001355064",,,"NASDAQ","NASDAQ","Asset Management",,,"","Financial Services",,,"631 549 1859","4221 North 203rd Street, Suite 100","Elkhorn","NE","68022-3474","https://images.financialmodelingprep.com/symbol/CFHAX.png","2019-01-02",true,false,true,false,true diff --git a/sdk/go/bulk_parity_test.go b/sdk/go/bulk_parity_test.go index f0b58ebc..7b312b81 100644 --- a/sdk/go/bulk_parity_test.go +++ b/sdk/go/bulk_parity_test.go @@ -45,6 +45,10 @@ func TestBulkSnapshotsDecodeExactValues(t *testing.T) { profiles[0].CIK == nil || *profiles[0].CIK != "0000107263" || profiles[0].IsETF { t.Fatalf("bulk_company_profiles = %+v", profiles[0]) } + if profiles[2].Symbol != "ACCV" || profiles[2].Change != nil || profiles[2].ChangePercentage != nil || + profiles[3].Symbol != "CFHAX" || profiles[3].ISIN != nil || profiles[3].Country != nil || profiles[3].Ceo != "" { + t.Fatalf("empty profile cells = %+v, %+v", profiles[2], profiles[3]) + } ratings := assertCSVFixtureParity[BulkStockRating](t, "bulk_stock_ratings.csv", 9) if ratings[0].Rating != "B-" || cellText(ratings[0].PriceToBookScore) != "4" { t.Fatalf("bulk_stock_ratings = %+v", ratings[0]) diff --git a/sdk/go/bulk_test.go b/sdk/go/bulk_test.go index 4612c00c..3fde6eaa 100644 --- a/sdk/go/bulk_test.go +++ b/sdk/go/bulk_test.go @@ -58,7 +58,7 @@ func TestBulkMethodsUseExactPathsAndWireParameterOrder(t *testing.T) { ctx := context.Background() profiles, err := client.Bulk.CompanyProfiles(ctx, NewBulkPartQuery("0")) - if err != nil || len(profiles) != 2 || profiles[0].Symbol != "WMB" || profiles[0].MarketCap != 82_906_291_252 { + if err != nil || len(profiles) != 4 || profiles[0].Symbol != "WMB" || profiles[0].MarketCap != 82_906_291_252 { t.Fatalf("CompanyProfiles = %+v, %v", profiles, err) } ratings, err := client.Bulk.StockRatings(ctx) diff --git a/sdk/go/company_models.go b/sdk/go/company_models.go index 0cee64aa..02c63d40 100644 --- a/sdk/go/company_models.go +++ b/sdk/go/company_models.go @@ -13,42 +13,42 @@ import ( // CompanyProfile is a detailed worldwide company profile returned by symbol or // US CIK lookup. type CompanyProfile struct { - Symbol string `json:"symbol"` - Price float64 `json:"price"` - MarketCap float64 `json:"marketCap"` - Beta float64 `json:"beta"` - LastDividend float64 `json:"lastDividend"` - Range string `json:"range"` - Change float64 `json:"change"` - ChangePercentage float64 `json:"changePercentage"` - Volume float64 `json:"volume"` - AverageVolume float64 `json:"averageVolume"` - CompanyName string `json:"companyName"` - Currency string `json:"currency"` - CIK *string `json:"cik"` - ISIN string `json:"isin"` - CUSIP *string `json:"cusip"` - ExchangeFullName string `json:"exchangeFullName"` - Exchange string `json:"exchange"` - Industry string `json:"industry"` - Website string `json:"website"` - Description string `json:"description"` - Ceo string `json:"ceo"` - Sector string `json:"sector"` - Country string `json:"country"` - FullTimeEmployees *string `json:"fullTimeEmployees"` - Phone *string `json:"phone"` - Address string `json:"address"` - City string `json:"city"` - State string `json:"state"` - Zip string `json:"zip"` - Image string `json:"image"` - IPODate *Date `json:"ipoDate"` - DefaultImage bool `json:"defaultImage"` - IsETF bool `json:"isEtf"` - IsActivelyTrading bool `json:"isActivelyTrading"` - IsAdr bool `json:"isAdr"` - IsFund bool `json:"isFund"` + Symbol string `json:"symbol"` + Price float64 `json:"price"` + MarketCap float64 `json:"marketCap"` + Beta float64 `json:"beta"` + LastDividend float64 `json:"lastDividend"` + Range string `json:"range"` + Change *float64 `json:"change"` + ChangePercentage *float64 `json:"changePercentage"` + Volume float64 `json:"volume"` + AverageVolume float64 `json:"averageVolume"` + CompanyName string `json:"companyName"` + Currency string `json:"currency"` + CIK *string `json:"cik"` + ISIN *string `json:"isin"` + CUSIP *string `json:"cusip"` + ExchangeFullName string `json:"exchangeFullName"` + Exchange string `json:"exchange"` + Industry string `json:"industry"` + Website string `json:"website"` + Description string `json:"description"` + Ceo string `json:"ceo"` + Sector string `json:"sector"` + Country *string `json:"country"` + FullTimeEmployees *string `json:"fullTimeEmployees"` + Phone *string `json:"phone"` + Address string `json:"address"` + City string `json:"city"` + State string `json:"state"` + Zip string `json:"zip"` + Image string `json:"image"` + IPODate *Date `json:"ipoDate"` + DefaultImage bool `json:"defaultImage"` + IsETF bool `json:"isEtf"` + IsActivelyTrading bool `json:"isActivelyTrading"` + IsAdr bool `json:"isAdr"` + IsFund bool `json:"isFund"` } // companyProfileShadow mirrors CompanyProfile with a pointer or raw value for @@ -61,14 +61,14 @@ type companyProfileShadow struct { Beta *float64 `json:"beta"` LastDividend *float64 `json:"lastDividend"` Range *string `json:"range"` - Change *float64 `json:"change"` - ChangePercentage *float64 `json:"changePercentage"` + Change jsontext.Value `json:"change"` + ChangePercentage jsontext.Value `json:"changePercentage"` Volume *float64 `json:"volume"` AverageVolume *float64 `json:"averageVolume"` CompanyName *string `json:"companyName"` Currency *string `json:"currency"` CIK jsontext.Value `json:"cik"` - ISIN *string `json:"isin"` + ISIN jsontext.Value `json:"isin"` CUSIP jsontext.Value `json:"cusip"` ExchangeFullName *string `json:"exchangeFullName"` Exchange *string `json:"exchange"` @@ -77,7 +77,7 @@ type companyProfileShadow struct { Description *string `json:"description"` Ceo *string `json:"ceo"` Sector *string `json:"sector"` - Country *string `json:"country"` + Country jsontext.Value `json:"country"` FullTimeEmployees jsontext.Value `json:"fullTimeEmployees"` Phone jsontext.Value `json:"phone"` Address *string `json:"address"` @@ -114,9 +114,9 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("CompanyProfile", "lastDividend") case shadow.Range == nil: return missingMemberError("CompanyProfile", "range") - case shadow.Change == nil: + case len(shadow.Change) == 0: return missingMemberError("CompanyProfile", "change") - case shadow.ChangePercentage == nil: + case len(shadow.ChangePercentage) == 0: return missingMemberError("CompanyProfile", "changePercentage") case shadow.Volume == nil: return missingMemberError("CompanyProfile", "volume") @@ -128,7 +128,7 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("CompanyProfile", "currency") case len(shadow.CIK) == 0: return missingMemberError("CompanyProfile", "cik") - case shadow.ISIN == nil: + case len(shadow.ISIN) == 0: return missingMemberError("CompanyProfile", "isin") case len(shadow.CUSIP) == 0: return missingMemberError("CompanyProfile", "cusip") @@ -146,7 +146,7 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("CompanyProfile", "ceo") case shadow.Sector == nil: return missingMemberError("CompanyProfile", "sector") - case shadow.Country == nil: + case len(shadow.Country) == 0: return missingMemberError("CompanyProfile", "country") case len(shadow.FullTimeEmployees) == 0: return missingMemberError("CompanyProfile", "fullTimeEmployees") @@ -175,6 +175,22 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { case shadow.IsFund == nil: return missingMemberError("CompanyProfile", "isFund") } + var change *float64 + if shadow.Change.Kind() != 'n' { + var value float64 + if err := json.Unmarshal(shadow.Change, &value); err != nil { + return memberDecodeError("CompanyProfile", "change", shadow.Change, err) + } + change = &value + } + var changePercentage *float64 + if shadow.ChangePercentage.Kind() != 'n' { + var value float64 + if err := json.Unmarshal(shadow.ChangePercentage, &value); err != nil { + return memberDecodeError("CompanyProfile", "changePercentage", shadow.ChangePercentage, err) + } + changePercentage = &value + } var cik *string if shadow.CIK.Kind() != 'n' { var value string @@ -183,6 +199,16 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { } cik = &value } + var isin *string + if shadow.ISIN.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.ISIN, &value); err != nil { + return memberDecodeError("CompanyProfile", "isin", shadow.ISIN, err) + } + if value != "" { + isin = &value + } + } var cusip *string if shadow.CUSIP.Kind() != 'n' { var value string @@ -191,6 +217,16 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { } cusip = &value } + var country *string + if shadow.Country.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.Country, &value); err != nil { + return memberDecodeError("CompanyProfile", "country", shadow.Country, err) + } + if value != "" { + country = &value + } + } var fullTimeEmployees *string if shadow.FullTimeEmployees.Kind() != 'n' { var value string @@ -218,14 +254,14 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { Beta: *shadow.Beta, LastDividend: *shadow.LastDividend, Range: *shadow.Range, - Change: *shadow.Change, - ChangePercentage: *shadow.ChangePercentage, + Change: change, + ChangePercentage: changePercentage, Volume: *shadow.Volume, AverageVolume: *shadow.AverageVolume, CompanyName: *shadow.CompanyName, Currency: *shadow.Currency, CIK: cik, - ISIN: *shadow.ISIN, + ISIN: isin, CUSIP: cusip, ExchangeFullName: *shadow.ExchangeFullName, Exchange: *shadow.Exchange, @@ -234,7 +270,7 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { Description: *shadow.Description, Ceo: *shadow.Ceo, Sector: *shadow.Sector, - Country: *shadow.Country, + Country: country, FullTimeEmployees: fullTimeEmployees, Phone: phone, Address: *shadow.Address, diff --git a/sdk/go/company_parity_test.go b/sdk/go/company_parity_test.go index 8c50bbf0..c4c77c47 100644 --- a/sdk/go/company_parity_test.go +++ b/sdk/go/company_parity_test.go @@ -52,11 +52,11 @@ func TestDocumentedCompanyProfileNoteAndPeerDecodeExactValues(t *testing.T) { profile := profiles[0] want := CompanyProfile{ Symbol: "AAPL", Price: 331.85501, MarketCap: 4_874_072_686_740, Beta: 1.097, LastDividend: 1.05, - Range: "201.5-344.57", Change: -6.33498, ChangePercentage: -1.8732, Volume: 28_718_014, + Range: "201.5-344.57", Change: new(-6.33498), ChangePercentage: new(-1.8732), Volume: 28_718_014, AverageVolume: 55_309_000, CompanyName: "Apple Inc.", Currency: "USD", CIK: new("0000320193"), - ISIN: "US0378331005", CUSIP: new("037833100"), ExchangeFullName: "NASDAQ Global Select", Exchange: "NASDAQ", + ISIN: new("US0378331005"), CUSIP: new("037833100"), ExchangeFullName: "NASDAQ Global Select", Exchange: "NASDAQ", Industry: "Consumer Electronics", Website: "https://www.apple.com", Description: profile.Description, - Ceo: "Timothy D. Cook", Sector: "Technology", Country: "US", FullTimeEmployees: new("166000"), + Ceo: "Timothy D. Cook", Sector: "Technology", Country: new("US"), FullTimeEmployees: new("166000"), Phone: new("(408) 996-1010"), Address: "One Apple Park Way", City: "Cupertino", State: "CA", Zip: "95014", Image: "https://images.financialmodelingprep.com/symbol/AAPL.png", IPODate: new(mustParseDate(t, "1980-12-12")), DefaultImage: false, IsETF: false, IsActivelyTrading: true, IsAdr: false, IsFund: false, From 596e9028847907ce2ffaa0867859305c37981b2a Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 11:51:27 +0800 Subject: [PATCH 28/31] test(fmp-py): expect str | None for optional bulk numeric members The mypy type-check contract now expects `str | None` for the bulk numeric members it reads, which became optional under the bulk CSV rule: * `BulkIncomeStatement.revenue` * `BulkEarningsSurprise.eps_actual` * `BulkEodBar.close` Related to issue #376. --- crates/fmp-py/tests/typecheck_contract.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/fmp-py/tests/typecheck_contract.py b/crates/fmp-py/tests/typecheck_contract.py index 1473b119..cae75ef2 100644 --- a/crates/fmp-py/tests/typecheck_contract.py +++ b/crates/fmp-py/tests/typecheck_contract.py @@ -151,12 +151,12 @@ def check_bulk_contract(client: FmpClient) -> None: ipo_date: datetime.date | None = profiles[0].ipo_date statements: list[BulkIncomeStatement] = client.bulk.income_statements(2026, "Q1") accepted: datetime.datetime = statements[0].accepted_date - revenue: str = statements[0].revenue + revenue: str | None = statements[0].revenue surprises: list[BulkEarningsSurprise] = client.bulk.earnings_surprises(year=2026) - eps_actual: str = surprises[0].eps_actual + eps_actual: str | None = surprises[0].eps_actual bars: list[BulkEodBar] = client.bulk.eod(datetime.date(2024, 10, 22)) more_bars: list[BulkEodBar] = client.bulk.eod(date="2024-10-22") - close: str = bars[0].close + close: str | None = bars[0].close _ = (ipo_date, accepted, revenue, eps_actual, close, more_bars) From 08bae144ae205de83fa56e49537de891e6c60d52 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 11:57:44 +0800 Subject: [PATCH 29/31] test(fmp-py): decode empty and header-only csv bulk bodies to no rows Add a parametrized Python test verifying that an empty or header-only ``text/csv`` body on a bulk route decodes to an empty list rather than raising a provider error. Refs #376 --- crates/fmp-py/tests/test_bulk_a.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/fmp-py/tests/test_bulk_a.py b/crates/fmp-py/tests/test_bulk_a.py index 2e78395e..7fa918e0 100644 --- a/crates/fmp-py/tests/test_bulk_a.py +++ b/crates/fmp-py/tests/test_bulk_a.py @@ -230,6 +230,16 @@ def test_earnings_surprises_accepts_the_year_keyword(client: Any, fixture_server assert fixture_server.requests[0].target == "/earnings-surprises-bulk?year=2024" +@pytest.mark.parametrize("body", [b"", b'"symbol","peers"\n'], ids=["empty", "header-only"]) +def test_empty_and_header_only_csv_bodies_decode_to_no_rows( + client: Any, fixture_server: FixtureServer, body: bytes +) -> None: + """An empty or header-only ``text/csv`` body is an empty list, not a provider error.""" + fixture_server.route("/peers-bulk", body, content_type=CSV_CONTENT_TYPE) + + assert client.bulk.stock_peers() == [] + + def test_status_error_carries_the_bulk_endpoint_id( client: Any, fixture_server: FixtureServer, errors: SimpleNamespace ) -> None: From e7da4cf4d82a5341b329490064b4d1e01639859c Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 11:57:54 +0800 Subject: [PATCH 30/31] docs(adr): add ADR 0035 for bulk CSV responses Record that the eighteen bulk routes answer text/csv decoded by header name: the Rust csv crate splits records into a serde cell deserializer, Go uses encoding/csv with the model JSON rules, and the cell rules make every model member a required header column. Add the bulk-CSV-only rule that numeric metric members are optional while identity members stay required, with JSON endpoints keeping ADR 0033 evidence-only. Cover the CompanyProfile widening, the ETF lastUpdated date, and the 256 MiB bulk body-limit default, since ratios-ttm-bulk measured 65.6 MiB and exceeds the 64 MiB general default. Mark ADR 0028 superseded in part, update the bulk module doc, the ADR 0030 raw-key example, and the Python client limit docstring to match. close #376 --- crates/fmp-py/python/fmp/_native/__init__.pyi | 4 +- crates/fmp-py/src/client.rs | 4 +- crates/libfmp/src/endpoints/bulk.rs | 26 ++-- docs/adr/0028-bulk-api-contracts.md | 5 + docs/adr/0030-go-sdk-architecture.md | 2 +- docs/adr/0035-bulk-csv-responses.md | 146 ++++++++++++++++++ 6 files changed, 173 insertions(+), 14 deletions(-) create mode 100644 docs/adr/0035-bulk-csv-responses.md diff --git a/crates/fmp-py/python/fmp/_native/__init__.pyi b/crates/fmp-py/python/fmp/_native/__init__.pyi index 0c09d90f..93a96d76 100644 --- a/crates/fmp-py/python/fmp/_native/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/__init__.pyi @@ -172,7 +172,9 @@ class FmpClient: the default host the constructor raises `FmpConfigError` naming `FMP_API_KEY`. `timeout` and `connect_timeout` are positive finite numbers of seconds. - `max_response_body_bytes` bounds each buffered response. Authenticated + `max_response_body_bytes` bounds each buffered response; when omitted, + `client.bulk` methods buffer up to 256 MiB and every other method up to + 64 MiB. Authenticated non-loopback HTTP requires `danger_allow_insecure_authentication=True`. Redirects are either disabled or restricted to the same origin. diff --git a/crates/fmp-py/src/client.rs b/crates/fmp-py/src/client.rs index b301b435..2e7fce53 100644 --- a/crates/fmp-py/src/client.rs +++ b/crates/fmp-py/src/client.rs @@ -171,7 +171,9 @@ fn positive_duration(value: f64, field: &'static str) -> PyResult { /// the default host the constructor raises `FmpConfigError` naming /// `FMP_API_KEY`. /// `timeout` and `connect_timeout` are positive finite numbers of seconds. -/// `max_response_body_bytes` bounds each buffered response. Authenticated +/// `max_response_body_bytes` bounds each buffered response; when omitted, +/// `client.bulk` methods buffer up to 256 MiB and every other method up to +/// 64 MiB. Authenticated /// non-loopback HTTP requires `danger_allow_insecure_authentication=True`. /// Redirects are either disabled or restricted to the same origin. /// diff --git a/crates/libfmp/src/endpoints/bulk.rs b/crates/libfmp/src/endpoints/bulk.rs index 03f8f1fe..72e2e124 100644 --- a/crates/libfmp/src/endpoints/bulk.rs +++ b/crates/libfmp/src/endpoints/bulk.rs @@ -1,17 +1,21 @@ //! Bulk endpoint contracts. //! -//! Every bulk route answers one `GET` with one bare JSON array, and each -//! convenience method sends exactly one request: the client never paginates -//! a `part`, retries, or fans out on the caller's behalf. Numeric cells arrive -//! as strings on every route except `profile-bulk` and are preserved verbatim -//! as [`crate::codecs::NumericString`]. +//! Every bulk route answers one `GET` with one headed `text/csv` body (ADR +//! 0035), and each convenience method sends exactly one request: the client +//! never paginates a `part`, retries, or fans out on the caller's behalf. Each +//! record decodes into one row by header name; an empty or header-only body is +//! no rows. Numeric cells are preserved verbatim as +//! [`crate::codecs::NumericString`] on every route except `profile-bulk`, and +//! an empty numeric cell is `None`. //! -//! Bulk descriptors and convenience methods inherit the client's finite -//! response-body limit and its single logical timeout. Raise -//! [`crate::client::ClientBuilder::max_response_body_bytes`] and, when needed, -//! [`crate::client::ClientBuilder::timeout`] when a known provider response -//! will exceed the configured limits. The Python binding exposes the same -//! eighteen methods under `client.bulk`. +//! Bulk descriptors buffer up to +//! [`crate::client::DEFAULT_BULK_MAX_RESPONSE_BODY_BYTES`] (256 MiB) while the +//! client limit is unset: whole-market bodies such as `ratios-ttm-bulk` +//! exceed the 64 MiB general default. A limit set with +//! [`crate::client::ClientBuilder::max_response_body_bytes`] applies to bulk +//! routes too, and every bulk call shares the client's single logical +//! timeout, which [`crate::client::ClientBuilder::timeout`] can raise. The +//! Python binding exposes the same eighteen methods under `client.bulk`. use crate::{ Client, Result, diff --git a/docs/adr/0028-bulk-api-contracts.md b/docs/adr/0028-bulk-api-contracts.md index 6d0d8888..61124366 100644 --- a/docs/adr/0028-bulk-api-contracts.md +++ b/docs/adr/0028-bulk-api-contracts.md @@ -5,6 +5,11 @@ Accepted for issue #37's Rust API inventory. All 18 routes are implemented in Rust; the Python facade remains reserved. +Superseded in part by [ADR 0035](0035-bulk-csv-responses.md): the routes +answer `text/csv`, not a bare JSON array; numeric bulk members are optional; +and the ETF key is a clean `lastUpdated` decoded as a date. The names, queries, +single-request rule, and preserved provider spellings below still hold. + ## Decision All 18 documented bulk entries use the following public Rust names. diff --git a/docs/adr/0030-go-sdk-architecture.md b/docs/adr/0030-go-sdk-architecture.md index fcb65715..a5336441 100644 --- a/docs/adr/0030-go-sdk-architecture.md +++ b/docs/adr/0030-go-sdk-architecture.md @@ -147,7 +147,7 @@ that no wire information is invented or lost: | `CalendarQuarter` on a response model (`u8`, validated 1 to 4 in Rust) | `uint8` | no range check on decode: Rust validates, Go parity tests prove the JSON kind only; accepted difference, same policy as `IsoTimestamp` | | `FiscalPeriod` on a response model (`bulk` statement rows) | `string` | the closed Rust enum is not mirrored on the response side; accepted difference, same policy as `IsoTimestamp`. The query-side `FiscalPeriod` stays a typed string | | `pub type` aliases of a model (`FundDisclosureDate = Form13fFilingDate`) | not emitted | the Go method returns the aliased model's Go type (`[]Form13fFilingDate`), so one Rust model is one Go type | -| a wire name Go 1.27.1 `json/v2` cannot spell in a struct tag (a comma, backslash, quote, double quote, or backtick, such as the bulk ETF holdings key `lastUpdated"`) | public field tagged `json:"-"`; the shadow carries a `RawMembers map[string]jsontext.Value` tagged `json:",embed"` fallback, the decoder reads the member through `rawMember`, and a generated `MarshalJSONTo` re-emits the exact key | only plain codecs are bridged; json/v2 allows one embedded fallback per struct, so a model with a raw key cannot also carry an embedded rest map, and the generator fails naming the struct | +| a wire name Go 1.27.1 `json/v2` cannot spell in a struct tag (a comma, backslash, quote, double quote, or backtick; the bulk ETF holdings key `lastUpdated"` was one until ADR 0035) | public field tagged `json:"-"`; the shadow carries a `RawMembers map[string]jsontext.Value` tagged `json:",embed"` fallback, the decoder reads the member through `rawMember`, and a generated `MarshalJSONTo` re-emits the exact key | only plain codecs are bridged; json/v2 allows one embedded fallback per struct, so a model with a raw key cannot also carry an embedded rest map, and the generator fails naming the struct | ### Endpoint metadata (Rust to Go type table) diff --git a/docs/adr/0035-bulk-csv-responses.md b/docs/adr/0035-bulk-csv-responses.md new file mode 100644 index 00000000..4e17a9d8 --- /dev/null +++ b/docs/adr/0035-bulk-csv-responses.md @@ -0,0 +1,146 @@ +# ADR 0035: Bulk CSV responses + +- Status: Accepted +- Date: 2026-10-01 +- Decision owners: libfmp maintainers +- Scope: the eighteen `*-bulk` routes in Rust, Python, and Go; implemented + by [#376](https://github.com/bitbrew-dev/libfmp/issues/376). Supersedes + ADR 0028's "bare JSON array" wire format, its "every documented key is + required and non-null" rule for bulk rows, and its quoted ETF + `lastUpdated"` key. + +## Context + +ADR 0028 typed the bulk routes from the `outer.md` examples, which show JSON +arrays. The 2026-09-30 live audit (#368 rerun) found that every bulk route +answers `200 text/csv` and ignores `Accept: application/json`, so every bulk +call failed to decode in all three SDKs. Live probes on 2026-09-30 and +2026-10-01 established the wire shape: + +- A headed CSV body with LF line endings and no byte-order mark. Strings are + quoted, numbers are bare (including exponent forms such as `6.9e-9`), + quoted cells can carry commas and doubled quotes, and an absent value is an + empty cell. +- A request with no data (for example `earnings-surprises-bulk?year=1900`) + answers `200 text/csv` with an empty body. A bad parameter answers + `400 application/json`, which the existing status path handles. +- The ETF header is a clean `lastUpdated`, not the documented `lastUpdated"`. +- Bodies are large: one `profile-bulk` part is about 30 MB, and the full + `ratios-ttm-bulk` body measured 68,755,170 bytes (65.6 MiB), above the + 64 MiB general default. +- A census of over a million live rows (full bodies for most routes, income + statements for two periods, samples for the other statement routes) found + empty cells on many numeric columns, sparse and varying by period: for example `netIncomeDeductions` empty on 24 of 55,315 FY2024 + income rows and on none of 18,512 Q1 2025 rows; `grahamNumberTTM` empty on + 11,072 of 30,041 key-metrics rows; `dcf` on 1,653 of 35,029 rows. Typed + codes were also empty: `reportedCurrency` (scores, 313 of 62,655), ETF + `asset` and `isin`, and profile `isin`, `country` (with `change` and + `changePercentage` on two rows). + +## Decision + +### Contract + +- `EndpointSpec::get_csv` and `ResponseContract::>::csv()` describe + a CSV route; `ExpectedContentType::Csv` accepts `text/csv` + case-insensitively, parameters ignored. All eighteen bulk descriptors use + it, and gen_go renders them as `getCSV[Row]`. +- A `200` with any other media type, including `application/json`, is the + existing "unexpected content type" decode error. The provider-message check + of ADR 0031 (#381) stays on the JSON path: a header-only CSV body such as + `symbol,price` is data, not an error line. +- An empty body or a header-only body decodes to no rows. + +### Decoding + +- Rust: the `csv` crate (1.4, RFC 4180 records, quoted cells with commas, + doubled quotes and newlines, no trimming, equal field counts) only splits + records. Each record is handed to the row's `Deserialize` as a map from + header name to cell through a small cell deserializer, so serde renames + and field codecs apply unchanged and `serde_path_to_error` reports + `[row].member`, the JSON decoder's path format. The csv crate's own serde + layer was not used: it lost member paths and infers types for + `deserialize_any`. +- Go: `encoding/csv` splits records; each record is written as a JSON object + whose cell kinds come from the model's fields (numbers and booleans raw, + everything else quoted) and decoded with the model's own JSON rules, so + shadows, codecs, and redaction apply and a failure reports + `//`. +- Every model member, `Option` or not, must be a header column, checked on + the first record, so a dropped or renamed provider column is a + `missing_member` error instead of a silent `None`. Columns the model does + not read are ignored. +- Error messages are classified into a `DecodeErrorKind` and dropped; no cell + value reaches an error (ADR 0031). + +### Cell rules + +| Cell | Option member | Plain `String` | Number or bool | Typed code or date, required | +| --- | --- | --- | --- | --- | +| empty | `None` / `None` / `nil` | `""` | `null` decode error | `null` decode error | +| text | the member's own parse | verbatim | parsed; anything else is `wrong_type` | the type's validation | + +`NumericString` keeps the cell text verbatim (`6.9148336e-9`, values beyond +`u64`). Booleans accept exactly `true` and `false`. Go is more lenient on one +point it already had for JSON: a required plain `string` (Go's +`NumericString` and code types) takes an empty cell as `""`. + +### Nullability (bulk CSV only) + +CSV has no null: an empty cell is its only absence marker, and the census +shows sparse empties that change by period, where one empty cell would fail a +50,000-row response. So, for bulk CSV rows only: + +- Every numeric metric member (`NumericString`) is `Option`, + Python `Optional[str]`, Go `*string`. +- Identity members stay required: `symbol`, `date`, and on statement rows + `reportedCurrency`, `cik`, `filingDate`, `acceptedDate`, `fiscalYear`, and + `period`; `lastUpdated` on ETF holdings and earnings surprises. +- Typed codes follow the evidence through `codecs::empty_or_null`: + `BulkFinancialScore.reported_currency`, `BulkEtfHolding.asset` and + `isin`. +- Text members (`BulkStockPeer.peers`, ETF `cusip` and `name`) stay strings; + an empty cell is `""`. + +JSON endpoints keep ADR 0033's evidence-only rule. `CompanyProfile` is shared +with the JSON `company.profile` route; the evidence rule widened its `isin` +and `country` (`empty_or_null`) and `change` and `change_percentage` +(`required_option`), which affects both routes. + +`BulkEtfHolding.last_updated_raw: String` (key `lastUpdated"`) became +`last_updated: Date` (key `lastUpdated`). + +### Body limit and buffering + +CSV bulk descriptors default to `DEFAULT_BULK_MAX_RESPONSE_BODY_BYTES` +(256 MiB) while the client limit is unset; every other endpoint keeps the +64 MiB default. Once a caller sets the client limit +(`ClientBuilder::max_response_body_bytes`, Go `WithMaxResponseBodyBytes`, +Python `max_response_body_bytes=`), it applies to every endpoint, bulk +included, and an endpoint override still wins. Bodies stay buffered, as in +ADR 0028, and decode record by record from the buffer; there is no streaming +API. + +## Alternatives considered + +- **Evidence-only `Option`s for bulk rows.** Rejected: about forty members + today, and any new year or period can add an empty cell that fails a whole + response. +- **Transcode CSV to JSON in Rust and reuse `decode_json`.** Rejected: CSV + cells carry no types, so the transcoder would need the model's schema, + which serde already provides. +- **A separate `BulkCompanyProfile`.** Rejected: a 36-field copy in three + SDKs for the same provider data, where the JSON profile shows the same + gaps. +- **Streaming decode.** Deferred: the bodies fit the buffered limit and every + method returns a `Vec`. + +## Consequences + +- Breaking in all three SDKs: the bulk methods require `text/csv`, most bulk + row members are optional, the ETF field is renamed, and four + `CompanyProfile` members are optional. +- The shared fixtures under `crates/libfmp/tests/fixtures/bulk_*.csv` are + trimmed live captures; the Go fixture check covers `.csv` files. +- A new empty identity cell is a decode error by design and a small model + change under this ADR. From 362c805778e9ffad0f695d44385117fa7a95f595 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Thu, 1 Oct 2026 12:04:35 +0800 Subject: [PATCH 31/31] fix(csv): strip leading UTF-8 BOM before the Go CSV header The Rust csv reader strips a leading byte order mark before parsing the header, but Go's encoding/csv keeps it, so the first header name carried the BOM and the first column failed to decode. Trim the UTF-8 BOM from the body before reading. Add matching tests in both the Rust and Go decoders to confirm a leading byte order mark is not treated as part of the first header. --- crates/libfmp/src/endpoints/csv_rows.rs | 8 ++++++++ sdk/go/csv.go | 6 +++++- sdk/go/csv_test.go | 10 ++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/libfmp/src/endpoints/csv_rows.rs b/crates/libfmp/src/endpoints/csv_rows.rs index 9e414af8..d2218634 100644 --- a/crates/libfmp/src/endpoints/csv_rows.rs +++ b/crates/libfmp/src/endpoints/csv_rows.rs @@ -291,6 +291,14 @@ mod tests { assert_eq!(rows[1].peers, "say \"hi\""); } + #[test] + fn a_leading_byte_order_mark_is_not_part_of_the_first_header() { + let rows = decode(&format!("\u{feff}{HEADER}A,2025-06-02,1,true,1,1,x\r\n")).unwrap(); + + assert_eq!(rows[0].symbol, "A"); + assert_eq!(rows[0].peers, "x"); + } + #[test] fn an_empty_cell_on_a_required_member_is_a_null_at_its_row_and_member() { let body = format!("{HEADER}A,2025-06-02,1,true,1,1,x\nB,2025-06-02,,true,1,1,x\n"); diff --git a/sdk/go/csv.go b/sdk/go/csv.go index 9a3c9469..07c0170c 100644 --- a/sdk/go/csv.go +++ b/sdk/go/csv.go @@ -81,10 +81,14 @@ func getCSV[T any](ctx context.Context, c *Client, endpointID, relativePath stri return rows, nil } +// utf8BOM is the byte order mark the Rust csv reader strips before the +// header; encoding/csv keeps it, so decodeCSVRows drops it first. +var utf8BOM = []byte("\xef\xbb\xbf") + // decodeCSVRows decodes body into rows of T. On failure it returns the JSON // pointer of the row (and member), a DecodeKind, and the cause. func decodeCSVRows[T any](body []byte) ([]T, string, DecodeKind, error) { - reader := csv.NewReader(bytes.NewReader(body)) + reader := csv.NewReader(bytes.NewReader(bytes.TrimPrefix(body, utf8BOM))) reader.ReuseRecord = true header, err := reader.Read() if errors.Is(err, io.EOF) { diff --git a/sdk/go/csv_test.go b/sdk/go/csv_test.go index dbd1534c..3ebbcf33 100644 --- a/sdk/go/csv_test.go +++ b/sdk/go/csv_test.go @@ -84,6 +84,16 @@ func TestGetCSVDecodesCellsByHeaderName(t *testing.T) { } } +// TestGetCSVSkipsALeadingByteOrderMark mirrors +// a_leading_byte_order_mark_is_not_part_of_the_first_header. +func TestGetCSVSkipsALeadingByteOrderMark(t *testing.T) { + t.Parallel() + rows, err := getCSVProbe[csvProbeRow](t, "\ufeff"+csvProbeHeader+"A,2025-06-02,1,true,1,1,x\r\n") + if err != nil || len(rows) != 1 || rows[0].Symbol != "A" || rows[0].Peers != "x" { + t.Fatalf("rows = %+v, err = %v", rows, err) + } +} + // TestGetCSVReportsRowAndMemberOfAFailure mirrors the Rust decoder's // null, missing-member, and syntax tests on a generated model. func TestGetCSVReportsRowAndMemberOfAFailure(t *testing.T) {