diff --git a/Cargo.toml b/Cargo.toml index d04cad0a..f1e10756 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ http = "1.5.0" libc = "0.2.189" libfmp = { path = "crates/libfmp" } proc-macro2 = "1.0" -pyo3 = { version = "0.29", features = ["abi3-py310", "chrono", "multiple-pymethods"] } +pyo3 = { version = "0.29", features = ["abi3-py310", "chrono", "experimental-async", "multiple-pymethods"] } quote = "1.0" pyo3-stub-gen = { version = "0.23", default-features = false, features = ["infer_signature"] } reqwest = { version = "0.13.4", default-features = false } diff --git a/crates/fmp-py-gen/src/bin/gen_namespaces/bindings.rs b/crates/fmp-py-gen/src/bin/gen_namespaces/bindings.rs index 0502ec22..bdba7d88 100644 --- a/crates/fmp-py-gen/src/bin/gen_namespaces/bindings.rs +++ b/crates/fmp-py-gen/src/bin/gen_namespaces/bindings.rs @@ -1,15 +1,15 @@ //! The per-module wiring that used to be hand-written in `lib.rs`, //! `client.rs`, and `facade.rs`: one binding per `fmp._native.` //! submodule, unioning the model inventory with the registry tree, rendered -//! as `registration.rs`, `namespaces/mod.rs`, `client_namespaces.rs`, and -//! `facade_domains.rs`. +//! as `registration.rs`, `namespaces/mod.rs`, `client_namespaces.rs` (the +//! getters of both `FmpClient` and `AsyncFmpClient`), and `facade_domains.rs`. use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write as _; use crate::GENERATED_HEADER; use crate::models::ModelModules; -use crate::plan::Node; +use crate::plan::{Node, async_struct_name_for}; /// One `fmp._native.` submodule and everything registered into it. #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -18,7 +18,8 @@ pub(crate) struct Binding { pub(crate) path: Vec, /// The generated model structs the module holds, sorted. pub(crate) models: Vec, - /// The namespace struct name when the registry defines this path. + /// The sync namespace struct name when the registry defines this path; + /// its `Async` twin is registered alongside it. pub(crate) namespace: Option, /// The last path segment of every direct child, sorted. pub(crate) children: BTreeSet, @@ -104,10 +105,15 @@ pub(crate) fn render_registration(bindings: &BTreeMap, Binding>) -> ); } if let Some(namespace) = &binding.namespace { + let module = binding.path.join("::"); let _ = writeln!( out, - " module.add_class::()?;", - binding.path.join("::") + " module.add_class::()?;" + ); + let _ = writeln!( + out, + " module.add_class::()?;", + async_struct_name_for(&binding.path) ); } for child in &binding.children { @@ -137,37 +143,55 @@ pub(crate) fn render_namespaces_mod(tree: &BTreeMap, Node>) -> Strin out } -/// Renders `client_namespaces.rs`: the `FmpClient` getter for every domain. +/// Renders `client_namespaces.rs`: the getter for every domain on +/// `FmpClient`, then the same getters on `AsyncFmpClient` returning the +/// `Async` twins. pub(crate) fn render_client_namespaces(tree: &BTreeMap, Node>) -> String { let domains: Vec<&Node> = tree.values().filter(|node| node.path.len() == 1).collect(); let mut out = String::new(); out.push_str(GENERATED_HEADER); out.push_str("\n\nuse pyo3::prelude::*;\nuse pyo3_stub_gen::derive::gen_stub_pymethods;\n\n"); - out.push_str("use crate::client::FmpClient;\n"); + out.push_str("use crate::client::{AsyncFmpClient, FmpClient};\n"); for node in &domains { let _ = writeln!( out, - "use crate::namespaces::{}::{};", + "use crate::namespaces::{}::{{{}, {}}};", node.dotted(), + async_struct_name_for(&node.path), node.struct_name() ); } - out.push_str("\n#[gen_stub_pymethods]\n#[pymethods]\nimpl FmpClient {\n"); + render_getters(&mut out, "FmpClient", &domains, |node| node.struct_name()); + render_getters(&mut out, "AsyncFmpClient", &domains, |node| { + async_struct_name_for(&node.path) + }); + out +} + +fn render_getters( + out: &mut String, + client: &str, + domains: &[&Node], + struct_name: impl Fn(&Node) -> String, +) { + let _ = writeln!( + out, + "\n#[gen_stub_pymethods]\n#[pymethods]\nimpl {client} {{" + ); let mut first = true; - for node in &domains { + for node in domains { if !first { out.push('\n'); } first = false; let domain = node.dotted(); - let struct_name = node.struct_name(); + let struct_name = struct_name(node); let _ = writeln!( out, " /// The `{domain}` endpoints, reached as `client.{domain}`.\n #[getter]\n fn {domain}(&self) -> {struct_name} {{\n {struct_name}::new(self.builder.clone())\n }}" ); } out.push_str("}\n"); - out } /// Renders `facade_domains.rs`: a wildcard `reexport_module_members!` from diff --git a/crates/fmp-py-gen/src/bin/gen_namespaces/emit.rs b/crates/fmp-py-gen/src/bin/gen_namespaces/emit.rs index d4c50bd3..3831cb0d 100644 --- a/crates/fmp-py-gen/src/bin/gen_namespaces/emit.rs +++ b/crates/fmp-py-gen/src/bin/gen_namespaces/emit.rs @@ -1,18 +1,20 @@ //! Rendering one namespace node into Rust source. //! +//! Each node renders two classes in one file: the sync `Namespace` for +//! `FmpClient` and its `AsyncNamespace` twin for `AsyncFmpClient`. //! Every method follows the same shape: //! a free `_query` function converts the Python arguments and builds -//! the `libfmp` query (the part an async twin will share), and the -//! `#[pymethods]` body only detaches from the interpreter, runs the call on -//! the shared runtime, and maps rows into the generated models (or a +//! the `libfmp` query (shared by both twins), and the `#[pymethods]` body +//! only hands the call to the shared runtime (detached `block_on`, or an +//! awaited `spawn`) and maps rows into the generated models (or a //! `BinaryResponse` into the hand-written `BinaryPayload`). use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write as _; use crate::GENERATED_HEADER; -use crate::method::{render_method, render_query_fn}; -use crate::plan::{Node, struct_name_for}; +use crate::method::{render_async_method, render_method, render_query_fn}; +use crate::plan::{Node, async_struct_name_for, struct_name_for}; /// Where the types of one verified entry live, as segments under /// `libfmp::endpoints`: the query type's module and each nested builder's @@ -117,42 +119,80 @@ pub(crate) fn render(node: &Node, query_modules: &QueryModules) -> Result String, &str) = match flavor { + Flavor::Sync => (struct_name_for, "client"), + Flavor::Async => (async_struct_name_for, "async client"), + }; + let name = struct_name(&node.path); let _ = write!( out, - "\n/// {} endpoints for a single client, exposed as `client.{dotted}`.\n", + "\n/// {} endpoints for a single {client}, exposed as `client.{dotted}`.\n", humanize(&node.path) ); out.push_str("#[gen_stub_pyclass]\n"); let _ = writeln!(out, "#[pyclass(module = \"fmp._native.{dotted}\", frozen)]"); let _ = writeln!( out, - "pub(crate) struct {struct_name} {{\n builder: Arc,\n}}\n" + "pub(crate) struct {name} {{\n builder: Arc,\n}}\n" ); let _ = writeln!( out, - "impl {struct_name} {{\n pub(crate) fn new(builder: Arc) -> Self {{\n Self {{ builder }}\n }}\n}}\n" + "impl {name} {{\n pub(crate) fn new(builder: Arc) -> Self {{\n Self {{ builder }}\n }}\n}}\n" ); out.push_str("#[gen_stub_pymethods]\n#[pymethods]\n"); - let _ = writeln!(out, "impl {struct_name} {{"); + let _ = writeln!(out, "impl {name} {{"); let mut first = true; - for endpoint in methods { + for endpoint in &node.endpoints { if !first { out.push('\n'); } first = false; - render_method(&mut out, endpoint); + match flavor { + Flavor::Sync => render_method(out, endpoint), + Flavor::Async => render_async_method(out, endpoint), + } } for child in &node.children { if !first { @@ -161,7 +201,7 @@ pub(crate) fn render(node: &Node, query_modules: &QueryModules) -> Result Result::::{A, B};` with the braces dropped for one name. diff --git a/crates/fmp-py-gen/src/bin/gen_namespaces/main.rs b/crates/fmp-py-gen/src/bin/gen_namespaces/main.rs index 664d941d..8c097b2c 100644 --- a/crates/fmp-py-gen/src/bin/gen_namespaces/main.rs +++ b/crates/fmp-py-gen/src/bin/gen_namespaces/main.rs @@ -2,7 +2,8 @@ //! //! Reads the endpoint registry under `crates/fmp-py-gen/registry`, validates //! it against the real `libfmp` signatures (refusing to emit on any error), -//! and writes one frozen `pyclass` namespace per registry path under +//! and writes one frozen `pyclass` namespace per registry path, with its +//! `Async` twin in the same file, under //! `crates/fmp-py/src/namespaces/`: flat domains as `.rs`, nested //! ones as `/mod.rs` plus `/.rs`, each formatted with //! `rustfmt`. Run it with `cargo run -p fmp-py-gen --bin gen_namespaces`. @@ -11,8 +12,9 @@ //! extension, so adding a domain never edits `lib.rs` or `client.rs`: //! `namespaces/mod.rs`, `registration.rs` (every `fmp._native.` //! submodule with its models and namespace classes, read back from the -//! generated model files), `client_namespaces.rs` (one `FmpClient` getter -//! per domain), and `facade_domains.rs` (the public `fmp.` re-exports). +//! generated model files), `client_namespaces.rs` (one getter per domain on +//! both `FmpClient` and `AsyncFmpClient`), and `facade_domains.rs` (the +//! public `fmp.` re-exports). mod bindings; mod emit; diff --git a/crates/fmp-py-gen/src/bin/gen_namespaces/method.rs b/crates/fmp-py-gen/src/bin/gen_namespaces/method.rs index 7fcecaa8..c190176c 100644 --- a/crates/fmp-py-gen/src/bin/gen_namespaces/method.rs +++ b/crates/fmp-py-gen/src/bin/gen_namespaces/method.rs @@ -1,9 +1,11 @@ -//! Rendering one endpoint: the `#[pymethods]` wrapper and the free -//! `_query` builder it calls. +//! Rendering one endpoint: the `#[pymethods]` wrappers and the free +//! `_query` builder they call. //! -//! The wrapper holds only the runtime hand-off (detach, `block_on`, result -//! mapping); argument conversion and query construction live in the builder -//! so an async twin can reuse them without touching this template. A +//! The sync wrapper holds only the runtime hand-off (detach, `block_on`, +//! result mapping); its async twin on the `Async*Namespace` awaits `spawn` +//! instead and takes owned `String`s where the sync one borrows `&str`, since +//! a coroutine's arguments must be `'static`. Argument conversion and query +//! construction live in the shared builder. A //! `binary = true` entry returns one `BinaryPayload` instead of a `Vec` of //! models, with the same hand-off; a `single = true` entry returns one model; //! a `response = "dynamic"` entry returns @@ -29,25 +31,25 @@ fn params(endpoint: &Endpoint) -> Vec<&Arg> { required.chain(optional).collect() } +/// Whether a parameter is declared borrowed (`&str`, the sync wrapper and +/// the query builder) or owned (`String`, the async wrapper). +#[derive(Clone, Copy, PartialEq, Eq)] +enum Ownership { + Borrowed, + Owned, +} + /// `name: &str` or `name: Option`, with `args::` prefixed onto the -/// crate-defined input enums (`SymbolsArg`, `DateArg`, ...). With `stub`, a -/// closed-vocabulary kind also carries its `typing.Literal` stub override. -fn param_decl(arg: &Arg, stub: bool) -> String { - let mut ty = String::new(); - let mut word = String::new(); - for c in arg.kind.input_type().chars().chain(std::iter::once(' ')) { - if c.is_ascii_alphanumeric() || c == '_' { - word.push(c); - continue; - } - if word.starts_with(|w: char| w.is_ascii_uppercase()) { - ty.push_str("args::"); - } - ty.push_str(&word); - word.clear(); - ty.push(c); - } - let ty = ty.trim_end(); +/// crate-defined input enums (`SymbolsArg`, `DateArg`, ...); `Owned` declares +/// `&str` as `String`. With `stub`, a closed-vocabulary kind also carries its +/// `typing.Literal` stub override. +fn param_decl(arg: &Arg, stub: bool, ownership: Ownership) -> String { + let input = arg.kind.input_type(); + let ty = if ownership == Ownership::Owned && input == "&str" { + "String".to_owned() + } else { + prefixed_type(input) + }; let literal = arg.kind.python_literal().filter(|_| stub); let override_attr = literal.map_or_else(String::new, |literal| { let repr = if arg.required { @@ -64,18 +66,46 @@ fn param_decl(arg: &Arg, stub: bool) -> String { } } -fn param_list(endpoint: &Endpoint, stub: bool) -> String { +fn param_list(endpoint: &Endpoint, stub: bool, ownership: Ownership) -> String { params(endpoint) .iter() - .map(|arg| param_decl(arg, stub)) + .map(|arg| param_decl(arg, stub, ownership)) .collect::>() .join(", ") } -fn name_list(endpoint: &Endpoint) -> String { +/// `input` with `args::` prefixed onto every capitalised type name. +fn prefixed_type(input: &str) -> String { + let mut ty = String::new(); + let mut word = String::new(); + for c in input.chars().chain(std::iter::once(' ')) { + if c.is_ascii_alphanumeric() || c == '_' { + word.push(c); + continue; + } + if word.starts_with(|w: char| w.is_ascii_uppercase()) { + ty.push_str("args::"); + } + ty.push_str(&word); + word.clear(); + ty.push(c); + } + ty.trim_end().to_owned() +} + +/// The query-builder call arguments; `Owned` lends each owned `String` back +/// as the `&str` the builder takes. +fn name_list(endpoint: &Endpoint, ownership: Ownership) -> String { params(endpoint) .iter() - .map(|arg| arg.python_name()) + .map(|arg| { + let name = arg.python_name(); + match (ownership, arg.kind.input_type() == "&str", arg.required) { + (Ownership::Owned, true, true) => format!("&{name}"), + (Ownership::Owned, true, false) => format!("{name}.as_deref()"), + _ => name, + } + }) .collect::>() .join(", ") } @@ -91,8 +121,9 @@ fn push_doc(out: &mut String, indent: &str, doc: &str) { } } -pub(crate) fn render_method(out: &mut String, endpoint: &Endpoint) { - let name = &endpoint.python_name; +/// The doc comment and attributes shared by both wrappers; `receivers` is how +/// many non-Python parameters precede the arguments (`&self, py` or `&self`). +fn render_header(out: &mut String, endpoint: &Endpoint, receivers: usize) { let args = params(endpoint); push_doc(out, " ", &endpoint.doc); if !args.is_empty() { @@ -112,26 +143,38 @@ pub(crate) fn render_method(out: &mut String, endpoint: &Endpoint) { out.push_str(DYNAMIC_STUB_OVERRIDE); out.push('\n'); } - if args.len() + 2 > MAX_PARAMS { + if args.len() + receivers > MAX_PARAMS { out.push_str(" #[allow(clippy::too_many_arguments)]\n"); } - let (result, binding, mapping) = result_shape(endpoint); - let mut decl = String::from("&self, py: Python<'_>"); - if !args.is_empty() { - decl.push_str(", "); - decl.push_str(¶m_list(endpoint, true)); - } - let _ = writeln!(out, " fn {name}({decl}) -> PyResult<{result}> {{"); - let call = if endpoint.query_type.is_some() { +} + +/// The `libfmp` call the spawned or blocked-on future makes, after binding +/// `query` from the builder when the entry has one. +fn render_call(out: &mut String, endpoint: &Endpoint, ownership: Ownership) -> String { + let name = &endpoint.python_name; + if endpoint.query_type.is_some() { let _ = writeln!( out, " let query = {name}_query({})?;", - name_list(endpoint) + name_list(endpoint, ownership) ); format!("client.{}(query).await", endpoint.libfmp_method) } else { format!("client.{}().await", endpoint.libfmp_method) - }; + } +} + +pub(crate) fn render_method(out: &mut String, endpoint: &Endpoint) { + let name = &endpoint.python_name; + render_header(out, endpoint, 2); + let (result, binding, mapping) = result_shape(endpoint); + let mut decl = String::from("&self, py: Python<'_>"); + if !endpoint.args.is_empty() { + decl.push_str(", "); + decl.push_str(¶m_list(endpoint, true, Ownership::Borrowed)); + } + let _ = writeln!(out, " fn {name}({decl}) -> PyResult<{result}> {{"); + let call = render_call(out, endpoint, Ownership::Borrowed); out.push_str(" let builder = self.builder.clone();\n"); let _ = writeln!( out, @@ -141,6 +184,32 @@ pub(crate) fn render_method(out: &mut String, endpoint: &Endpoint) { out.push_str(" }\n"); } +/// The coroutine twin of [`render_method`]: the request runs as a task on +/// the shared runtime and the rows are mapped once it resolves, back on the +/// event loop thread, attaching for the `dict` conversion of dynamic rows. +pub(crate) fn render_async_method(out: &mut String, endpoint: &Endpoint) { + let name = &endpoint.python_name; + render_header(out, endpoint, 1); + let (result, binding, mapping) = result_shape(endpoint); + let mut decl = String::from("&self"); + if !endpoint.args.is_empty() { + decl.push_str(", "); + decl.push_str(¶m_list(endpoint, true, Ownership::Owned)); + } + let _ = writeln!(out, " async fn {name}({decl}) -> PyResult<{result}> {{"); + let call = render_call(out, endpoint, Ownership::Owned); + let _ = writeln!( + out, + " let {binding} = spawn(self.builder.clone(), |client| async move {{ {call} }}).await?;" + ); + if endpoint.dynamic { + let _ = writeln!(out, " Python::attach(|py| {mapping})"); + } else { + let _ = writeln!(out, " {mapping}"); + } + out.push_str(" }\n"); +} + /// The Python return type, the local the detached call binds, and the /// expression mapping that local into the return type. fn result_shape(endpoint: &Endpoint) -> (String, &'static str, String) { @@ -193,7 +262,7 @@ pub(crate) fn render_query_fn(out: &mut String, endpoint: &Endpoint, struct_name let _ = writeln!( out, "fn {name}_query({}) -> PyResult<{query}> {{", - param_list(endpoint, false) + param_list(endpoint, false, Ownership::Borrowed) ); for arg in params(endpoint) { let python = arg.python_name(); diff --git a/crates/fmp-py-gen/src/bin/gen_namespaces/plan.rs b/crates/fmp-py-gen/src/bin/gen_namespaces/plan.rs index 2ca98683..81d064d0 100644 --- a/crates/fmp-py-gen/src/bin/gen_namespaces/plan.rs +++ b/crates/fmp-py-gen/src/bin/gen_namespaces/plan.rs @@ -88,3 +88,9 @@ pub(crate) fn struct_name_for(path: &[String]) -> String { name.push_str("Namespace"); name } + +/// `["statements", "income"]` -> `AsyncStatementsIncomeNamespace`, the +/// `AsyncFmpClient` twin of [`struct_name_for`]. +pub(crate) fn async_struct_name_for(path: &[String]) -> String { + format!("Async{}", struct_name_for(path)) +} diff --git a/crates/fmp-py-gen/src/bin/gen_namespaces/tests.rs b/crates/fmp-py-gen/src/bin/gen_namespaces/tests.rs index 4d8a07a8..7445a09f 100644 --- a/crates/fmp-py-gen/src/bin/gen_namespaces/tests.rs +++ b/crates/fmp-py-gen/src/bin/gen_namespaces/tests.rs @@ -66,7 +66,7 @@ fn quote_registry_renders_the_drop_in_namespace() { "#[pyclass(module = \"fmp._native.quote\", frozen)]", "pub(crate) struct QuoteNamespace {", "pub(crate) fn new(builder: Arc) -> Self", - "use crate::runtime::{ClientHandle, block_on};", + "use crate::runtime::{ClientHandle, block_on, spawn};", "use libfmp::endpoints::quote::{", "QuoteQuery, QuoteShortQuery", "use crate::models::quote::{", @@ -78,9 +78,13 @@ fn quote_registry_renders_the_drop_in_namespace() { "fn mutual_funds(&self, py: Python<'_>) -> PyResult> {", "client.mutual_fund_quotes().await", "fn short_query(symbol: &str) -> PyResult {\n let symbol = args::ticker(\"symbol\", symbol)?;\n Ok(QuoteShortQuery::new(symbol))\n}", + "/// Quote endpoints for a single async client, exposed as `client.quote`.\n#[gen_stub_pyclass]\n#[pyclass(module = \"fmp._native.quote\", frozen)]\npub(crate) struct AsyncQuoteNamespace {", + " /// The full quote for a symbol.\n #[pyo3(signature = (symbol))]\n async fn full(&self, symbol: String) -> PyResult> {\n let query = full_query(&symbol)?;\n let rows = spawn(self.builder.clone(), |client| async move { client.quote(query).await }).await?;\n rows.map(|items| items.into_iter().map(Quote::from).collect()).map_err(to_py_error)\n }", + "async fn mutual_funds(&self) -> PyResult> {", ] { assert!(quote.contains(needle), "missing {needle:?} in:\n{quote}"); } + assert_eq!(quote.matches("fn short_query(").count(), 1); assert!(!quote.contains("#[getter]")); assert!(!quote.contains("BinaryPayload")); } @@ -146,11 +150,13 @@ fn nested_fixture_renders_parent_getters_and_children() { let parent = source(&files, "statements/mod.rs"); for needle in [ "pub(crate) mod income;\npub(crate) mod reports;", - "use self::income::StatementsIncomeNamespace;", + "use self::income::{AsyncStatementsIncomeNamespace, StatementsIncomeNamespace};", "#[pyclass(module = \"fmp._native.statements\", frozen)]", "pub(crate) struct StatementsNamespace {", " #[getter]\n fn income(&self) -> StatementsIncomeNamespace {\n StatementsIncomeNamespace::new(self.builder.clone())\n }", " #[getter]\n fn reports(&self) -> StatementsReportsNamespace {", + "pub(crate) struct AsyncStatementsNamespace {", + " #[getter]\n fn income(&self) -> AsyncStatementsIncomeNamespace {\n AsyncStatementsIncomeNamespace::new(self.builder.clone())\n }", ] { assert!(parent.contains(needle), "missing {needle:?} in:\n{parent}"); } @@ -179,7 +185,7 @@ fn nested_fixture_renders_parent_getters_and_children() { let reports = source(&files, "statements/reports.rs"); for needle in [ "use libfmp::endpoints::statements::reports::FinancialReportsXlsxQuery;", - "use crate::args;\nuse crate::binary::BinaryPayload;\nuse crate::errors::to_py_error;\nuse crate::runtime::{ClientHandle, block_on};\n", + "use crate::args;\nuse crate::binary::BinaryPayload;\nuse crate::errors::to_py_error;\nuse crate::runtime::{ClientHandle, block_on, spawn};\n", "pub(crate) struct StatementsReportsNamespace {", " /// Downloads one financial report as an XLSX workbook.\n #[pyo3(signature = (symbol, year, period))]\n fn financial_reports_xlsx(&self, py: Python<'_>, symbol: &str, year: i64, #[gen_stub(override_type(type_repr = \"typing.Literal[\\\"Q1\\\", \\\"Q2\\\", \\\"Q3\\\", \\\"Q4\\\", \\\"FY\\\"]\", imports = (\"typing\",)))] period: &str) -> PyResult {", "let query = financial_reports_xlsx_query(symbol, year, period)?;", @@ -188,6 +194,7 @@ fn nested_fixture_renders_parent_getters_and_children() { "fn financial_reports_xlsx_query(symbol: &str, year: i64, period: &str) -> PyResult {", "let period = args::fiscal_period(\"period\", period)?;", "Ok(FinancialReportsXlsxQuery::new(symbol, year, period))", + "period: String) -> PyResult {\n let query = financial_reports_xlsx_query(&symbol, year, &period)?;\n let response = spawn(self.builder.clone(), |client| async move { client.financial_reports_xlsx(query).await }).await?;\n response.map(BinaryPayload::from).map_err(to_py_error)\n }", ] { assert!( reports.contains(needle), @@ -220,13 +227,15 @@ fn dynamic_fixture_renders_dict_rows_with_the_stub_override() { let files = render_all(&manifest().join("tests/fixtures/dynamic_rows")); let sec = source(&files, "sec_filings.rs"); for needle in [ - "use crate::args;\nuse crate::convert;\nuse crate::errors::to_py_error;\nuse crate::runtime::{ClientHandle, block_on};\n", + "use crate::args;\nuse crate::convert;\nuse crate::errors::to_py_error;\nuse crate::runtime::{ClientHandle, block_on, spawn};\n", "#[pyo3(signature = (*, symbol=None, cik=None, sic_code=None))]\n #[gen_stub(override_return_type(type_repr = \"list[dict[str, typing.Any]]\", imports = (\"typing\")))]\n fn search_industry_classifications(", "sic_code: Option<&str>) -> PyResult>> {", "let query = search_industry_classifications_query(symbol, cik, sic_code)?;", "let rows = py.detach(move || block_on(builder, |client| async move { client.search_industry_classifications(query).await }))?;", " rows.map_err(to_py_error)?.iter().map(|row| convert::dynamic_object_to_py(py, row).map(Bound::unbind)).collect()\n }", "let mut query = IndustryClassificationSearchQuery::new();", + " async fn search_industry_classifications(&self, symbol: Option, cik: Option, sic_code: Option) -> PyResult>> {\n let query = search_industry_classifications_query(symbol.as_deref(), cik.as_deref(), sic_code.as_deref())?;", + " Python::attach(|py| rows.map_err(to_py_error)?.iter().map(|row| convert::dynamic_object_to_py(py, row).map(Bound::unbind)).collect())\n }", ] { assert!(sec.contains(needle), "missing {needle:?} in:\n{sec}"); } @@ -331,6 +340,8 @@ fn wide_signatures_get_the_clippy_allow_and_prefixed_input_types() { "symbols: args::SymbolsArg, range: (args::DateArg, args::DateArg), page: Option, since: Option, active: Option, party: Option<&str>", "let party = args::optional(\"party\", party, args::text)?;", "Ok(WideQuery::new(symbols, range, page, since, active, party))", + "party=None))]\n async fn wide(&self, symbols: args::SymbolsArg, range: (args::DateArg, args::DateArg), page: Option, since: Option, active: Option, party: Option) -> PyResult<", + "let query = wide_query(symbols, range, page, since, active, party.as_deref())?;", ] { assert!(source.contains(needle), "missing {needle:?} in:\n{source}"); } @@ -449,8 +460,8 @@ fn binding_files_render_the_wiring_and_parse() { syn::parse_file(®istration).expect("registration parses"); for needle in [ "pub(crate) fn register_namespaces(parent: &Bound<'_, PyModule>) -> PyResult<()> {\n register_analyst(parent)?;", - "fn register_statements(parent: &Bound<'_, PyModule>) -> PyResult<()> {\n let module = PyModule::new(parent.py(), \"statements\")?;\n module.add_class::()?;\n register_statements_as_reported(&module)?;", - " module.add_class::()?;\n module.add_class::()?;\n add_submodule(parent, \"fmp._native.statements.income\", &module)\n}", + "fn register_statements(parent: &Bound<'_, PyModule>) -> PyResult<()> {\n let module = PyModule::new(parent.py(), \"statements\")?;\n module.add_class::()?;\n module.add_class::()?;\n register_statements_as_reported(&module)?;", + " module.add_class::()?;\n module.add_class::()?;\n module.add_class::()?;\n add_submodule(parent, \"fmp._native.statements.income\", &module)\n}", ] { assert!( registration.contains(needle), @@ -460,8 +471,15 @@ fn binding_files_render_the_wiring_and_parse() { let getters = render_client_namespaces(&tree); syn::parse_file(&getters).expect("client getters parse"); - assert!(getters.contains("use crate::namespaces::statements::StatementsNamespace;")); - assert!(getters.contains(" #[getter]\n fn statements(&self) -> StatementsNamespace {")); + assert!(getters.contains( + "use crate::namespaces::statements::{AsyncStatementsNamespace, StatementsNamespace};" + )); + assert!(getters.contains( + "impl FmpClient {\n /// The `statements` endpoints, reached as `client.statements`.\n #[getter]\n fn statements(&self) -> StatementsNamespace {" + )); + assert!(getters.contains( + "impl AsyncFmpClient {\n /// The `statements` endpoints, reached as `client.statements`.\n #[getter]\n fn statements(&self) -> AsyncStatementsNamespace {" + )); assert!(!getters.contains("fn income(")); let namespaces_mod = render_namespaces_mod(&tree); diff --git a/crates/fmp-py/README.md b/crates/fmp-py/README.md index 90aa5037..127cb8f1 100644 --- a/crates/fmp-py/README.md +++ b/crates/fmp-py/README.md @@ -28,8 +28,8 @@ naming `FMP_API_KEY`, while a custom `base_url` selects no auth. When [Using a proxy](#using-a-proxy)). `FmpClient` is synchronous: each call releases the Python GIL while the async -Rust transport waits, so threads keep running. A Python async facade is not -part of this release. +Rust transport waits, so threads keep running. For asyncio code, use +[`AsyncFmpClient`](#asyncio-client), the same surface with `async def` methods. ## Examples @@ -203,6 +203,75 @@ A client that is never closed keeps its pooled connections until the next request made through any client after it and its namespaces are garbage-collected, or until the process exits. +## Asyncio client + +`AsyncFmpClient` takes the same keywords and environment variables as +`FmpClient` and exposes the same namespaces and methods, but every endpoint +method is a coroutine, xlsx and bulk CSV included: + +```python +import asyncio + +from fmp import AsyncFmpClient + + +async def main() -> None: + async with AsyncFmpClient() as client: + rows = await client.quote.short("AAPL") + quotes, profiles = await asyncio.gather( + client.quote.full("MSFT"), + client.company.profile("NVDA"), + ) + + +asyncio.run(main()) +``` + +| Topic | Behavior | +|-------|----------| +| Event loop | Requests run on the binding's process-wide runtime (shared with `FmpClient`); the coroutine only waits for the result, so the loop never blocks | +| Concurrency | `asyncio.gather` on one client overlaps the requests; bound a large fan-out with a semaphore to stay inside your plan's rate limit | +| Cancellation | Cancelling the awaiting task (`task.cancel()`, `asyncio.wait_for`, a task group) aborts the HTTP request | +| Lazy start | A coroutine sends nothing until it is awaited | +| Errors | The same exception classes as `FmpClient`, raised from the `await` | +| `on_response` | A plain synchronous callable, run on the event loop thread before the coroutine returns; an exception it raises propagates from the `await` | +| Closing | `close()`, `await aclose()`, or `async with`; afterwards every call raises `FmpConfigError`, and calls in flight finish | +| Runtimes | asyncio, and anyio on its asyncio backend; trio is not supported | +| `fork()` | Supported as for `FmpClient`: a child process rebuilds the runtime and awaits on its own event loop | + +### Airflow deferrable operators + +An Airflow trigger runs `async def run()` in the triggerer's shared event loop, +where a blocking call would stall every other trigger. `AsyncFmpClient` awaits +without blocking, so it fits a trigger directly: + +```python +from collections.abc import AsyncIterator +from typing import Any + +from airflow.triggers.base import BaseTrigger, TriggerEvent +from fmp import AsyncFmpClient + + +class QuoteTrigger(BaseTrigger): + def __init__(self, symbol: str) -> None: + super().__init__() + self.symbol = symbol + + def serialize(self) -> tuple[str, dict[str, Any]]: + return ("my_dags.triggers.QuoteTrigger", {"symbol": self.symbol}) + + async def run(self) -> AsyncIterator[TriggerEvent]: + async with AsyncFmpClient() as client: + rows = await client.quote.short(self.symbol) + yield TriggerEvent({"symbol": self.symbol, "price": rows[0].price}) +``` + +The client reads `FMP_API_KEY` on the triggerer host, so keep the key in the +triggerer's environment (or a secrets backend) rather than in the serialized +trigger kwargs. If Airflow cancels the trigger, the in-flight request is +aborted. + ## Using a proxy A proxy that issues its own keys and forwards to FMP, such as diff --git a/crates/fmp-py/examples/README.md b/crates/fmp-py/examples/README.md index 4a824afc..f2145e74 100644 --- a/crates/fmp-py/examples/README.md +++ b/crates/fmp-py/examples/README.md @@ -21,6 +21,10 @@ uv run quickstart.py directory, whose path is printed. - Scripts marked "offline" stage their scenarios against a loopback server in `_local_server.py`, so they need neither a key nor network access. +- `async_client.py` needs a release that ships `AsyncFmpClient`. For an + Airflow deferrable trigger built on it, see the + [Asyncio client](../README.md#airflow-deferrable-operators) section of the + package README. ## Scripts @@ -31,6 +35,7 @@ uv run quickstart.py | `timeouts_and_limits.py` | `timeout` and `connect_timeout`; bulk defaults (600 s, 256 MiB) vs the rest (30 s, 64 MiB); an explicit value applies to bulk too | offline | | `errors.py` | config, validation, transport, status (including a provider error inside a 200 body) and decode errors; `decode_path` and `decode_kind` | offline, one call needs network | | `nullable_fields.py` | `Optional` members such as `CompanyProfile.isin` / `country`; congressional net-worth columns that may be `None` and the `additional_columns` dict | key | +| `async_client.py` | `async with AsyncFmpClient() as client:`, awaited calls vs an `asyncio.gather` fan-out with a semaphore, a `wait_for` timeout that aborts the request | key (the cancellation part is offline) | | `lifecycle_and_threads.py` | `with FmpClient() as client:`, `close()` semantics, a `ThreadPoolExecutor` fan-out on one shared client | key | | `bulk.py` | one small bulk endpoint, its row count, and a CSV written with the `csv` module | key with bulk access | | `binary_and_dicts.py` | `statements.reports.xlsx` bytes saved to a file; `to_dict()` for JSON or a DataFrame hand-off | key | diff --git a/crates/fmp-py/examples/async_client.py b/crates/fmp-py/examples/async_client.py new file mode 100644 index 00000000..8d1a6baa --- /dev/null +++ b/crates/fmp-py/examples/async_client.py @@ -0,0 +1,69 @@ +"""The asyncio client: awaited calls, gather fan-out, and cancellation. + +``AsyncFmpClient`` takes the same keywords as ``FmpClient`` and exposes the +same namespaces, but every endpoint method is a coroutine. Requests run on the +binding's own runtime, so awaiting one never blocks the event loop. Run with +``uv run async_client.py``; needs ``FMP_API_KEY`` (the cancellation part is +offline). +""" + +# standard library +import asyncio +import time + +# fmp library +from fmp import AsyncFmpClient + +# local +from _local_server import serve # isort: skip + +SYMBOLS = ["AAPL", "MSFT", "NVDA", "AMZN", "GOOGL", "META"] + + +async def fan_out(client: AsyncFmpClient) -> None: + """Fetches one profile per symbol, awaited one by one and then gathered.""" + started = time.perf_counter() + for symbol in SYMBOLS: + await client.company.profile(symbol) + sequential = time.perf_counter() - started + + started = time.perf_counter() + # gather overlaps the requests on one shared client. A semaphore keeps a + # large fan-out inside your FMP plan's rate limit. + limit = asyncio.Semaphore(4) + + async def profile(symbol: str) -> str: + async with limit: + rows = await client.company.profile(symbol) + return f"{rows[0].symbol}: {rows[0].company_name}" + + lines = await asyncio.gather(*(profile(symbol) for symbol in SYMBOLS)) + gathered = time.perf_counter() - started + + for line in lines: + print(f" {line}") + print(f" sequential {sequential:.2f}s, gathered {gathered:.2f}s") + + +async def cancellation() -> None: + """Shows that a timeout cancels the coroutine and aborts its HTTP request.""" + with serve(b"[]", delay=5.0) as base_url: + async with AsyncFmpClient(base_url=base_url, path_prefix="", auth_mode="none") as client: + started = time.perf_counter() + try: + await asyncio.wait_for(client.quote.short("AAPL"), timeout=0.5) + except asyncio.TimeoutError: + print(f" gave up after {time.perf_counter() - started:.2f}s; the request was aborted") + + +async def main() -> None: + """Uses the client as an async context manager, then demonstrates cancellation.""" + # async with closes the client on exit, releasing pooled connections; it + # never swallows an exception raised inside the block. + async with AsyncFmpClient() as client: + await fan_out(client) + await cancellation() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/crates/fmp-py/python/fmp/__init__.py b/crates/fmp-py/python/fmp/__init__.py index 0646cc88..0018721c 100644 --- a/crates/fmp-py/python/fmp/__init__.py +++ b/crates/fmp-py/python/fmp/__init__.py @@ -1,9 +1,10 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native import BinaryPayload, FmpClient, __version__, reveal_secret_urls, set_reveal_secret_urls +from fmp._native import AsyncFmpClient, BinaryPayload, FmpClient, __version__, reveal_secret_urls, set_reveal_secret_urls from fmp._native.errors import FmpConfigError, FmpDecodeError, FmpError, FmpStatusError, FmpTransportError, FmpValidationError __all__ = [ + "AsyncFmpClient", "BinaryPayload", "FmpClient", "FmpConfigError", diff --git a/crates/fmp-py/python/fmp/_native/__init__.pyi b/crates/fmp-py/python/fmp/_native/__init__.pyi index 26e33dc0..1d7a5569 100644 --- a/crates/fmp-py/python/fmp/_native/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/__init__.pyi @@ -67,6 +67,7 @@ from fmp._native import transcripts import fmp._native.transcripts __all__ = [ + "AsyncFmpClient", "BinaryPayload", "FmpClient", "analyst", @@ -107,6 +108,277 @@ __all__ = [ __version__: builtins.str +@typing.final +class AsyncFmpClient: + r""" + An asyncio FMP client: the same namespaces and methods as `FmpClient`, but + every endpoint method is a coroutine to `await`. + + The constructor takes the same keywords, environment fallbacks + (`FMP_API_KEY`, `FMP_BASE_URL`), and defaults as `FmpClient`, and raises + the same exceptions. Requests run on the process-wide runtime shared with + `FmpClient`, so awaiting one never blocks the event loop and concurrent + coroutines (`asyncio.gather`) run their requests in parallel. Cancelling + the awaiting task aborts its request. Coroutines need a running asyncio + loop (anyio on its asyncio backend works); trio is not supported. + + `on_response` behaves as on `FmpClient`: a plain, synchronous callable run + on the event loop thread after the response decoded and before the + coroutine returns; if it raises, the coroutine raises that exception. + + `close()` and `await aclose()` release the pooled connections; afterwards + every call, including through a namespace fetched before the close, raises + `FmpConfigError`. Calls already in flight finish normally. Closing twice + is a no-op, and `async with AsyncFmpClient(...) as client:` closes the + client when the block exits. + """ + + @property + def analyst(self) -> fmp._native.analyst.AsyncAnalystNamespace: + r""" + The `analyst` endpoints, reached as `client.analyst`. + """ + + @property + def bulk(self) -> fmp._native.bulk.AsyncBulkNamespace: + r""" + The `bulk` endpoints, reached as `client.bulk`. + """ + + @property + def calendar(self) -> fmp._native.calendar.AsyncCalendarNamespace: + r""" + The `calendar` endpoints, reached as `client.calendar`. + """ + + @property + def chart(self) -> fmp._native.chart.AsyncChartNamespace: + r""" + The `chart` endpoints, reached as `client.chart`. + """ + + @property + def commitment_of_traders( + self, + ) -> fmp._native.commitment_of_traders.AsyncCommitmentOfTradersNamespace: + r""" + The `commitment_of_traders` endpoints, reached as `client.commitment_of_traders`. + """ + + @property + def commodities(self) -> fmp._native.commodities.AsyncCommoditiesNamespace: + r""" + The `commodities` endpoints, reached as `client.commodities`. + """ + + @property + def company(self) -> fmp._native.company.AsyncCompanyNamespace: + r""" + The `company` endpoints, reached as `client.company`. + """ + + @property + def congressional(self) -> fmp._native.congressional.AsyncCongressionalNamespace: + r""" + The `congressional` endpoints, reached as `client.congressional`. + """ + + @property + def crypto(self) -> fmp._native.crypto.AsyncCryptoNamespace: + r""" + The `crypto` endpoints, reached as `client.crypto`. + """ + + @property + def dcf(self) -> fmp._native.dcf.AsyncDcfNamespace: + r""" + The `dcf` endpoints, reached as `client.dcf`. + """ + + @property + def directory(self) -> fmp._native.directory.AsyncDirectoryNamespace: + r""" + The `directory` endpoints, reached as `client.directory`. + """ + + @property + def economics(self) -> fmp._native.economics.AsyncEconomicsNamespace: + r""" + The `economics` endpoints, reached as `client.economics`. + """ + + @property + def esg(self) -> fmp._native.esg.AsyncEsgNamespace: + r""" + The `esg` endpoints, reached as `client.esg`. + """ + + @property + def forex(self) -> fmp._native.forex.AsyncForexNamespace: + r""" + The `forex` endpoints, reached as `client.forex`. + """ + + @property + def fundraising(self) -> fmp._native.fundraising.AsyncFundraisingNamespace: + r""" + The `fundraising` endpoints, reached as `client.fundraising`. + """ + + @property + def funds(self) -> fmp._native.funds.AsyncFundsNamespace: + r""" + The `funds` endpoints, reached as `client.funds`. + """ + + @property + def indexes(self) -> fmp._native.indexes.AsyncIndexesNamespace: + r""" + The `indexes` endpoints, reached as `client.indexes`. + """ + + @property + def insider_trading( + self, + ) -> fmp._native.insider_trading.AsyncInsiderTradingNamespace: + r""" + The `insider_trading` endpoints, reached as `client.insider_trading`. + """ + + @property + def institutional_ownership( + self, + ) -> fmp._native.institutional_ownership.AsyncInstitutionalOwnershipNamespace: + r""" + The `institutional_ownership` endpoints, reached as `client.institutional_ownership`. + """ + + @property + def market(self) -> fmp._native.market.AsyncMarketNamespace: + r""" + The `market` endpoints, reached as `client.market`. + """ + + @property + def market_hours(self) -> fmp._native.market_hours.AsyncMarketHoursNamespace: + r""" + The `market_hours` endpoints, reached as `client.market_hours`. + """ + + @property + def news(self) -> fmp._native.news.AsyncNewsNamespace: + r""" + The `news` endpoints, reached as `client.news`. + """ + + @property + def quote(self) -> fmp._native.quote.AsyncQuoteNamespace: + r""" + The `quote` endpoints, reached as `client.quote`. + """ + + @property + def screener(self) -> fmp._native.screener.AsyncScreenerNamespace: + r""" + The `screener` endpoints, reached as `client.screener`. + """ + + @property + def search(self) -> fmp._native.search.AsyncSearchNamespace: + r""" + The `search` endpoints, reached as `client.search`. + """ + + @property + def sec_filings(self) -> fmp._native.sec_filings.AsyncSecFilingsNamespace: + r""" + The `sec_filings` endpoints, reached as `client.sec_filings`. + """ + + @property + def statements(self) -> fmp._native.statements.AsyncStatementsNamespace: + r""" + The `statements` endpoints, reached as `client.statements`. + """ + + @property + def technical_indicators( + self, + ) -> fmp._native.technical_indicators.AsyncTechnicalIndicatorsNamespace: + r""" + The `technical_indicators` endpoints, reached as `client.technical_indicators`. + """ + + @property + def tipranks(self) -> fmp._native.tipranks.AsyncTipranksNamespace: + r""" + The `tipranks` endpoints, reached as `client.tipranks`. + """ + + @property + def transcripts(self) -> fmp._native.transcripts.AsyncTranscriptsNamespace: + r""" + The `transcripts` endpoints, reached as `client.transcripts`. + """ + + def __new__( + cls, + *, + token: typing.Optional[builtins.str] = None, + base_url: typing.Optional[builtins.str] = None, + path_prefix: typing.Optional[builtins.str] = None, + auth_mode: typing.Optional[ + typing.Literal[ + "none", + "fmp_header", + "fmp_query", + "bearer", + "custom_header", + "custom_query", + ] + ] = None, + auth_name: typing.Optional[builtins.str] = None, + auth_prefix: typing.Optional[builtins.str] = None, + headers: typing.Optional[typing.Mapping[builtins.str, builtins.str]] = None, + timeout: typing.Optional[builtins.float] = None, + connect_timeout: typing.Optional[builtins.float] = None, + max_response_body_bytes: typing.Optional[builtins.int] = None, + danger_allow_insecure_authentication: builtins.bool = False, + follow_redirects: typing.Optional[builtins.bool] = None, + on_response: typing.Optional[ + typing.Callable[ + [builtins.str, builtins.int, builtins.dict[builtins.str, builtins.str]], + None, + ] + ] = None, + ) -> AsyncFmpClient: ... + def close(self) -> None: + r""" + Releases the pooled HTTP connections and rejects every later call with + `FmpConfigError`. Calls already in flight finish normally. Idempotent. + """ + + async def aclose(self) -> None: + r""" + The awaitable form of `close()`, for `async` cleanup code. + """ + + async def __aenter__(self) -> AsyncFmpClient: + r""" + Returns the client itself for use in an `async with` block. + """ + + async def __aexit__( + self, + exc_type: typing.Optional[type[builtins.BaseException]], + exc_value: typing.Optional[builtins.BaseException], + traceback: typing.Optional[types.TracebackType], + ) -> None: + r""" + Closes the client and lets any exception from the block propagate. + """ + + @typing.final class BinaryPayload: r""" diff --git a/crates/fmp-py/python/fmp/_native/analyst/__init__.pyi b/crates/fmp-py/python/fmp/_native/analyst/__init__.pyi index 3c3095ab..d1f2e5ff 100644 --- a/crates/fmp-py/python/fmp/_native/analyst/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/analyst/__init__.pyi @@ -6,6 +6,7 @@ import typing __all__ = [ "AnalystNamespace", + "AsyncAnalystNamespace", "FinancialEstimate", "HistoricalRating", "HistoricalStockGrade", @@ -81,6 +82,72 @@ class AnalystNamespace: """ +@typing.final +class AsyncAnalystNamespace: + r""" + Analyst endpoints for a single async client, exposed as `client.analyst`. + """ + + async def financial_estimates( + self, + symbol: builtins.str, + period: typing.Literal["annual", "quarter"], + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[FinancialEstimate]: + r""" + Retrieves worldwide analyst financial estimates for one ticker. + """ + + async def ratings_snapshot( + self, symbol: builtins.str + ) -> builtins.list[RatingSnapshot]: + r""" + Retrieves the worldwide financial-rating snapshot for one ticker. + """ + + async def historical_ratings( + self, symbol: builtins.str, *, limit: typing.Optional[builtins.int] = None + ) -> builtins.list[HistoricalRating]: + r""" + Retrieves worldwide historical financial ratings for one ticker. + """ + + async def price_target_summary( + self, symbol: builtins.str + ) -> builtins.list[PriceTargetSummary]: + r""" + Retrieves the US-only price-target summary for one ticker. + """ + + async def price_target_consensus( + self, symbol: builtins.str + ) -> builtins.list[PriceTargetConsensus]: + r""" + Retrieves the US-only price-target consensus for one ticker. + """ + + async def stock_grades(self, symbol: builtins.str) -> builtins.list[StockGrade]: + r""" + Retrieves current worldwide stock-grade actions for one ticker. + """ + + async def historical_stock_grades( + self, symbol: builtins.str, *, limit: typing.Optional[builtins.int] = None + ) -> builtins.list[HistoricalStockGrade]: + r""" + Retrieves worldwide historical stock-grade counts for one ticker. + """ + + async def stock_grades_summary( + self, symbol: builtins.str + ) -> builtins.list[StockGradesSummary]: + r""" + Retrieves the worldwide stock-grades summary for one ticker. + """ + + @typing.final class FinancialEstimate: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/bulk/__init__.pyi b/crates/fmp-py/python/fmp/_native/bulk/__init__.pyi index e7eebfdd..63eefd08 100644 --- a/crates/fmp-py/python/fmp/_native/bulk/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/bulk/__init__.pyi @@ -19,6 +19,7 @@ from fmp._native.bulk import snapshots import fmp._native.bulk.snapshots __all__ = [ + "AsyncBulkNamespace", "BulkNamespace", "balance", "cash_flow", @@ -29,6 +30,139 @@ __all__ = [ ] +@typing.final +class AsyncBulkNamespace: + r""" + Bulk endpoints for a single async client, exposed as `client.bulk`. + """ + + async def company_profiles( + self, part: builtins.str + ) -> builtins.list[fmp._native.company.CompanyProfile]: + r""" + Retrieves one provider partition of worldwide company profiles. + """ + + async def stock_ratings( + self, + ) -> builtins.list[fmp._native.bulk.snapshots.BulkStockRating]: + r""" + Retrieves worldwide stock ratings in one provider bulk response. + """ + + async def dcf_valuations( + self, + ) -> builtins.list[fmp._native.bulk.snapshots.BulkDcfValuation]: + r""" + Retrieves worldwide discounted-cash-flow valuations in one provider bulk response. + """ + + async def financial_scores( + self, + ) -> builtins.list[fmp._native.bulk.snapshots.BulkFinancialScore]: + r""" + Retrieves worldwide financial scores in one provider bulk response. + """ + + async def price_target_summaries( + self, + ) -> builtins.list[fmp._native.bulk.snapshots.BulkPriceTargetSummary]: + r""" + Retrieves US price-target summaries in one provider bulk response. + """ + + async def etf_holdings( + self, part: builtins.str + ) -> builtins.list[fmp._native.bulk.snapshots.BulkEtfHolding]: + r""" + Retrieves one provider partition of worldwide ETF holdings. + """ + + async def upgrades_downgrades_consensus( + self, + ) -> builtins.list[fmp._native.bulk.snapshots.BulkUpgradesDowngradesConsensus]: + r""" + Retrieves worldwide upgrades/downgrades consensus in one provider bulk response. + """ + + async def key_metrics_ttm( + self, + ) -> builtins.list[fmp._native.bulk.metrics.BulkKeyMetricsTtm]: + r""" + Retrieves worldwide trailing-twelve-month key metrics in one bulk response. + """ + + async def financial_ratios_ttm( + self, + ) -> builtins.list[fmp._native.bulk.metrics.BulkFinancialRatiosTtm]: + r""" + Retrieves worldwide trailing-twelve-month financial ratios in one bulk response. + """ + + async def stock_peers( + self, + ) -> builtins.list[fmp._native.bulk.metrics.BulkStockPeer]: + r""" + Retrieves worldwide stock peers in one provider bulk response. + """ + + async def earnings_surprises( + self, year: builtins.int + ) -> builtins.list[fmp._native.bulk.metrics.BulkEarningsSurprise]: + r""" + Retrieves worldwide annual earnings surprises for one required provider year. + """ + + async def income_statements( + self, year: builtins.int, period: typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"] + ) -> builtins.list[fmp._native.bulk.income.BulkIncomeStatement]: + r""" + Retrieves worldwide bulk income statements for one year and fiscal period. + """ + + async def income_statement_growth( + self, year: builtins.int, period: typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"] + ) -> builtins.list[fmp._native.bulk.income.BulkIncomeStatementGrowth]: + r""" + Retrieves worldwide bulk income-statement growth for one year and fiscal period. + """ + + async def balance_sheet_statements( + self, year: builtins.int, period: typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"] + ) -> builtins.list[fmp._native.bulk.balance.BulkBalanceSheetStatement]: + r""" + Retrieves worldwide bulk balance sheets for one year and fiscal period. + """ + + async def balance_sheet_statement_growth( + self, year: builtins.int, period: typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"] + ) -> builtins.list[fmp._native.bulk.balance.BulkBalanceSheetStatementGrowth]: + r""" + Retrieves worldwide bulk balance-sheet growth for one year and fiscal period. + """ + + async def cash_flow_statements( + self, year: builtins.int, period: typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"] + ) -> builtins.list[fmp._native.bulk.cash_flow.BulkCashFlowStatement]: + r""" + Retrieves worldwide bulk cash-flow statements for one year and fiscal period. + """ + + async def cash_flow_statement_growth( + self, year: builtins.int, period: typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"] + ) -> builtins.list[fmp._native.bulk.cash_flow.BulkCashFlowStatementGrowth]: + r""" + Retrieves worldwide bulk cash-flow growth for one year and fiscal period. + """ + + async def eod( + self, date: datetime.date | builtins.str + ) -> builtins.list[fmp._native.bulk.eod.BulkEodBar]: + r""" + Retrieves worldwide bulk end-of-day prices for one required date. + """ + + @typing.final class BulkNamespace: r""" diff --git a/crates/fmp-py/python/fmp/_native/calendar/__init__.pyi b/crates/fmp-py/python/fmp/_native/calendar/__init__.pyi index 6fdbb4a7..d7783e97 100644 --- a/crates/fmp-py/python/fmp/_native/calendar/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/calendar/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncCalendarNamespace", "CalendarNamespace", "DividendEvent", "EarningsEvent", @@ -15,6 +16,102 @@ __all__ = [ ] +@typing.final +class AsyncCalendarNamespace: + r""" + Calendar endpoints for a single async client, exposed as `client.calendar`. + """ + + async def dividends( + self, symbol: builtins.str, *, limit: typing.Optional[builtins.int] = None + ) -> builtins.list[DividendEvent]: + r""" + Retrieves worldwide dividend events for one company. + """ + + async def dividends_calendar( + self, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + page: typing.Optional[builtins.int] = None, + ) -> builtins.list[DividendEvent]: + r""" + Retrieves the worldwide dividend calendar. + """ + + async def earnings( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + include_report_times: typing.Optional[builtins.bool] = None, + ) -> builtins.list[EarningsEvent]: + r""" + Retrieves worldwide earnings events for one company. + """ + + async def earnings_calendar( + self, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + page: typing.Optional[builtins.int] = None, + include_report_times: typing.Optional[builtins.bool] = None, + ) -> builtins.list[EarningsEvent]: + r""" + Retrieves the worldwide earnings calendar. + """ + + async def ipos_calendar( + self, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[IpoCalendarEvent]: + r""" + Retrieves the worldwide IPO calendar. + """ + + async def ipos_disclosure( + self, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[IpoDisclosure]: + r""" + Retrieves US IPO disclosure filings. + """ + + async def ipos_prospectus( + self, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[IpoProspectus]: + r""" + Retrieves US IPO prospectus filings and documented offering values. + """ + + async def stock_splits( + self, symbol: builtins.str, *, limit: typing.Optional[builtins.int] = None + ) -> builtins.list[StockSplitEvent]: + r""" + Retrieves worldwide stock-split events for one company. + """ + + async def stock_splits_calendar( + self, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + page: typing.Optional[builtins.int] = None, + ) -> builtins.list[StockSplitEvent]: + r""" + Retrieves the worldwide stock-splits calendar. + """ + + @typing.final class CalendarNamespace: r""" diff --git a/crates/fmp-py/python/fmp/_native/chart/__init__.pyi b/crates/fmp-py/python/fmp/_native/chart/__init__.pyi index 024be28b..cc475ab3 100644 --- a/crates/fmp-py/python/fmp/_native/chart/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/chart/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncChartNamespace", "ChartNamespace", "StockChartAdjustedBar", "StockChartFullBar", @@ -13,6 +14,135 @@ __all__ = [ ] +@typing.final +class AsyncChartNamespace: + r""" + Chart endpoints for a single async client, exposed as `client.chart`. + """ + + async def light( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[StockChartLightBar]: + r""" + Retrieves compact worldwide end-of-day stock chart rows. + """ + + async def full( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[StockChartFullBar]: + r""" + Retrieves detailed worldwide end-of-day stock chart rows. + """ + + async def non_split_adjusted( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[StockChartAdjustedBar]: + r""" + Retrieves split-unadjusted worldwide end-of-day stock chart rows. + """ + + async def dividend_adjusted( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[StockChartAdjustedBar]: + r""" + Retrieves dividend-adjusted worldwide end-of-day stock chart rows. + """ + + async def one_minute( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + nonadjusted: typing.Optional[builtins.bool] = None, + extended: typing.Optional[builtins.bool] = None, + ) -> builtins.list[StockChartIntradayBar]: + r""" + Retrieves one-minute worldwide intraday stock chart rows. + """ + + async def five_minutes( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + nonadjusted: typing.Optional[builtins.bool] = None, + extended: typing.Optional[builtins.bool] = None, + ) -> builtins.list[StockChartIntradayBar]: + r""" + Retrieves five-minute worldwide intraday stock chart rows. + """ + + async def fifteen_minutes( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + nonadjusted: typing.Optional[builtins.bool] = None, + extended: typing.Optional[builtins.bool] = None, + ) -> builtins.list[StockChartIntradayBar]: + r""" + Retrieves fifteen-minute worldwide intraday stock chart rows. + """ + + async def thirty_minutes( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + nonadjusted: typing.Optional[builtins.bool] = None, + extended: typing.Optional[builtins.bool] = None, + ) -> builtins.list[StockChartIntradayBar]: + r""" + Retrieves thirty-minute worldwide intraday stock chart rows. + """ + + async def one_hour( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + nonadjusted: typing.Optional[builtins.bool] = None, + extended: typing.Optional[builtins.bool] = None, + ) -> builtins.list[StockChartIntradayBar]: + r""" + Retrieves one-hour worldwide intraday stock chart rows. + """ + + async def four_hours( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + nonadjusted: typing.Optional[builtins.bool] = None, + extended: typing.Optional[builtins.bool] = None, + ) -> builtins.list[StockChartIntradayBar]: + r""" + Retrieves four-hour worldwide intraday stock chart rows. + """ + + @typing.final class ChartNamespace: r""" diff --git a/crates/fmp-py/python/fmp/_native/commitment_of_traders/__init__.pyi b/crates/fmp-py/python/fmp/_native/commitment_of_traders/__init__.pyi index 676a969b..6e2b226c 100644 --- a/crates/fmp-py/python/fmp/_native/commitment_of_traders/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/commitment_of_traders/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncCommitmentOfTradersNamespace", "CommitmentOfTradersNamespace", "CotAnalysis", "CotReport", @@ -12,6 +13,40 @@ __all__ = [ ] +@typing.final +class AsyncCommitmentOfTradersNamespace: + r""" + Commitment of traders endpoints for a single async client, exposed as `client.commitment_of_traders`. + """ + + async def report( + self, + *, + symbol: typing.Optional[builtins.str] = None, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[CotReport]: + r""" + Retrieves detailed Commitment of Traders reports. + """ + + async def analysis( + self, + *, + symbol: typing.Optional[builtins.str] = None, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[CotAnalysis]: + r""" + Retrieves derived Commitment of Traders market analysis. + """ + + async def report_list(self) -> builtins.list[CotReportListing]: + r""" + Lists the Commitment of Traders reports available from the provider. + """ + + @typing.final class CommitmentOfTradersNamespace: r""" diff --git a/crates/fmp-py/python/fmp/_native/commodities/__init__.pyi b/crates/fmp-py/python/fmp/_native/commodities/__init__.pyi index 00f77a10..344016e9 100644 --- a/crates/fmp-py/python/fmp/_native/commodities/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/commodities/__init__.pyi @@ -9,11 +9,93 @@ import fmp._native.quote import typing __all__ = [ + "AsyncCommoditiesNamespace", "CommoditiesNamespace", "CommodityListing", ] +@typing.final +class AsyncCommoditiesNamespace: + r""" + Commodities endpoints for a single async client, exposed as `client.commodities`. + """ + + async def list(self) -> builtins.list[CommodityListing]: + r""" + Lists the provider's documented commodity catalog. + """ + + async def quote( + self, symbol: builtins.str + ) -> builtins.list[fmp._native.quote.Quote]: + r""" + Retrieves a detailed US-only quote for one commodity. + """ + + async def quote_short( + self, symbol: builtins.str + ) -> builtins.list[fmp._native.quote.QuoteShort]: + r""" + Retrieves a compact US-only quote for one commodity. + """ + + async def chart_light( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartLightBar]: + r""" + Retrieves compact end-of-day commodity history. + """ + + async def chart_full( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartFullBar]: + r""" + Retrieves detailed end-of-day commodity history. + """ + + async def chart_one_minute( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartIntradayBar]: + r""" + Retrieves one-minute commodity history. + """ + + async def chart_five_minutes( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartIntradayBar]: + r""" + Retrieves five-minute commodity history. + """ + + async def chart_one_hour( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartIntradayBar]: + r""" + Retrieves one-hour commodity history. + """ + + @typing.final class CommoditiesNamespace: r""" diff --git a/crates/fmp-py/python/fmp/_native/company/__init__.pyi b/crates/fmp-py/python/fmp/_native/company/__init__.pyi index 1feed9e7..24368d2b 100644 --- a/crates/fmp-py/python/fmp/_native/company/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/company/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncCompanyNamespace", "CompanyExecutive", "CompanyMarketCapitalization", "CompanyNamespace", @@ -21,6 +22,138 @@ __all__ = [ ] +@typing.final +class AsyncCompanyNamespace: + r""" + Company endpoints for a single async client, exposed as `client.company`. + """ + + async def profile(self, symbol: builtins.str) -> builtins.list[CompanyProfile]: + r""" + Retrieves worldwide company profiles by provider ticker. + """ + + async def profile_by_cik(self, cik: builtins.str) -> builtins.list[CompanyProfile]: + r""" + Retrieves US company profiles by representation-preserving CIK. + """ + + async def notes(self, symbol: builtins.str) -> builtins.list[CompanyNote]: + r""" + Retrieves company-issued notes for a US company. + """ + + async def stock_peers(self, symbol: builtins.str) -> builtins.list[StockPeer]: + r""" + Retrieves worldwide stock peers for a company. + """ + + async def delisted_companies( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[DelistedCompany]: + r""" + Retrieves delisted US companies with optional provider pagination. + """ + + async def employee_count( + self, symbol: builtins.str, *, limit: typing.Optional[builtins.int] = None + ) -> builtins.list[EmployeeCount]: + r""" + Retrieves current employee-count filings for a US company. + """ + + async def historical_employee_count( + self, symbol: builtins.str, *, limit: typing.Optional[builtins.int] = None + ) -> builtins.list[EmployeeCount]: + r""" + Retrieves historical employee-count filings for a US company. + """ + + async def market_capitalization( + self, symbol: builtins.str + ) -> builtins.list[CompanyMarketCapitalization]: + r""" + Retrieves current worldwide market capitalization for one company. + """ + + async def batch_market_capitalization( + self, symbols: builtins.str | typing.Sequence[builtins.str] + ) -> builtins.list[CompanyMarketCapitalization]: + r""" + Retrieves current worldwide market capitalization for multiple companies. + """ + + async def historical_market_capitalization( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[CompanyMarketCapitalization]: + r""" + Retrieves historical worldwide market capitalization for one company. + """ + + async def shares_float( + self, symbol: builtins.str + ) -> builtins.list[CompanyShareFloat]: + r""" + Retrieves current worldwide share-float data for one company. + """ + + async def all_shares_float( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[ShareFloat]: + r""" + Retrieves paginated worldwide share-float data for all companies. + """ + + async def latest_mergers_acquisitions( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[MergerAcquisition]: + r""" + Retrieves the latest US mergers and acquisitions with optional pagination. + """ + + async def search_mergers_acquisitions( + self, name: builtins.str + ) -> builtins.list[MergerAcquisition]: + r""" + Searches US mergers and acquisitions by representation-preserving company name. + """ + + async def key_executives( + self, symbol: builtins.str + ) -> builtins.list[CompanyExecutive]: + r""" + Retrieves worldwide company executives by provider ticker. + """ + + async def executive_compensation( + self, symbol: builtins.str + ) -> builtins.list[ExecutiveCompensation]: + r""" + Retrieves executive compensation for a US company. + """ + + async def executive_compensation_benchmark( + self, *, year: typing.Optional[builtins.str] = None + ) -> builtins.list[ExecutiveCompensationBenchmark]: + r""" + Retrieves US executive-compensation benchmarks with an optional year. + """ + + @typing.final class CompanyExecutive: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/congressional/__init__.pyi b/crates/fmp-py/python/fmp/_native/congressional/__init__.pyi index 9e9810f5..9fed8b52 100644 --- a/crates/fmp-py/python/fmp/_native/congressional/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/congressional/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncCongressionalNamespace", "CongressionalDebtDetails", "CongressionalMemberNetWorth", "CongressionalMemberNetWorthAggregate", @@ -16,6 +17,139 @@ __all__ = [ ] +@typing.final +class AsyncCongressionalNamespace: + r""" + Congressional endpoints for a single async client, exposed as `client.congressional`. + """ + + async def latest_senate_disclosures( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[CongressionalTrade]: + r""" + Retrieves the latest Senate financial disclosures. + """ + + async def latest_house_disclosures( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[CongressionalTrade]: + r""" + Retrieves the latest House financial disclosures. + """ + + async def senate_trades( + self, + symbol: builtins.str, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[CongressionalTrade]: + r""" + Retrieves Senate trades for one ticker. + """ + + async def senate_trades_by_name( + self, name: builtins.str + ) -> builtins.list[CongressionalTrade]: + r""" + Searches Senate trades by member name. + """ + + async def senate_trades_by_member_id( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + member_id: typing.Optional[builtins.str] = None, + ) -> builtins.list[CongressionalTrade]: + r""" + Looks up Senate trades by an optional member ID and pagination. + """ + + async def house_trades( + self, + symbol: builtins.str, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[CongressionalTrade]: + r""" + Retrieves House trades for one ticker. + """ + + async def house_trades_by_name( + self, name: builtins.str + ) -> builtins.list[CongressionalTrade]: + r""" + Searches House trades by member name. + """ + + async def house_trades_by_member_id( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + member_id: typing.Optional[builtins.str] = None, + ) -> builtins.list[CongressionalTrade]: + r""" + Looks up House trades by an optional member ID and pagination. + """ + + async def profiles( + self, + *, + active: typing.Optional[builtins.bool] = None, + member_id: typing.Optional[builtins.str] = None, + latest_party: typing.Optional[builtins.str] = None, + latest_position: typing.Optional[builtins.str] = None, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[CongressionalMemberProfile]: + r""" + Retrieves congressional profiles using optional provider filters. + """ + + async def positions( + self, + *, + member_id: typing.Optional[builtins.str] = None, + party: typing.Optional[builtins.str] = None, + position: typing.Optional[builtins.str] = None, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[CongressionalMemberPosition]: + r""" + Retrieves congressional position history using optional provider filters. + """ + + async def net_worth( + self, + member_id: builtins.str, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[CongressionalMemberNetWorth]: + r""" + Retrieves itemized congressional net-worth disclosures. + """ + + async def net_worth_aggregated( + self, + member_id: builtins.str, + *, + totals_col: typing.Optional[builtins.str] = None, + ) -> builtins.list[CongressionalMemberNetWorthAggregate]: + r""" + Retrieves aggregated congressional net-worth totals by year. + """ + + @typing.final class CongressionalDebtDetails: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/crypto/__init__.pyi b/crates/fmp-py/python/fmp/_native/crypto/__init__.pyi index f94db3d4..3c7dec5b 100644 --- a/crates/fmp-py/python/fmp/_native/crypto/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/crypto/__init__.pyi @@ -9,11 +9,93 @@ import fmp._native.quote import typing __all__ = [ + "AsyncCryptoNamespace", "CryptoNamespace", "CryptocurrencyListing", ] +@typing.final +class AsyncCryptoNamespace: + r""" + Crypto endpoints for a single async client, exposed as `client.crypto`. + """ + + async def list(self) -> builtins.list[CryptocurrencyListing]: + r""" + Lists the provider's documented cryptocurrency catalog. + """ + + async def quote( + self, symbol: builtins.str + ) -> builtins.list[fmp._native.quote.Quote]: + r""" + Retrieves a detailed quote for one cryptocurrency. + """ + + async def quote_short( + self, symbol: builtins.str + ) -> builtins.list[fmp._native.quote.QuoteShort]: + r""" + Retrieves a compact quote for one cryptocurrency. + """ + + async def chart_light( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartLightBar]: + r""" + Retrieves compact end-of-day cryptocurrency history. + """ + + async def chart_full( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartFullBar]: + r""" + Retrieves detailed end-of-day cryptocurrency history. + """ + + async def chart_one_minute( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartIntradayBar]: + r""" + Retrieves one-minute cryptocurrency history. + """ + + async def chart_five_minutes( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartIntradayBar]: + r""" + Retrieves five-minute cryptocurrency history. + """ + + async def chart_one_hour( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartIntradayBar]: + r""" + Retrieves one-hour cryptocurrency history. + """ + + @typing.final class CryptoNamespace: r""" diff --git a/crates/fmp-py/python/fmp/_native/dcf/__init__.pyi b/crates/fmp-py/python/fmp/_native/dcf/__init__.pyi index 62c0e25e..40684ef2 100644 --- a/crates/fmp-py/python/fmp/_native/dcf/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/dcf/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncDcfNamespace", "CustomDcfValuation", "CustomLeveredDcfValuation", "DcfNamespace", @@ -12,6 +13,81 @@ __all__ = [ ] +@typing.final +class AsyncDcfNamespace: + r""" + Dcf endpoints for a single async client, exposed as `client.dcf`. + """ + + async def standard(self, symbol: builtins.str) -> builtins.list[DcfValuation]: + r""" + Retrieves the standard discounted-cash-flow valuation for one company. + """ + + async def levered(self, symbol: builtins.str) -> builtins.list[DcfValuation]: + r""" + Retrieves the levered discounted-cash-flow valuation for one company. + """ + + async def custom( + self, + symbol: builtins.str, + *, + revenue_growth_pct: typing.Optional[builtins.float] = None, + ebitda_pct: typing.Optional[builtins.float] = None, + depreciation_and_amortization_pct: typing.Optional[builtins.float] = None, + cash_and_short_term_investments_pct: typing.Optional[builtins.float] = None, + receivables_pct: typing.Optional[builtins.float] = None, + inventories_pct: typing.Optional[builtins.float] = None, + payable_pct: typing.Optional[builtins.float] = None, + ebit_pct: typing.Optional[builtins.float] = None, + capital_expenditure_pct: typing.Optional[builtins.float] = None, + operating_cash_flow_pct: typing.Optional[builtins.float] = None, + selling_general_and_administrative_expenses_pct: typing.Optional[ + builtins.float + ] = None, + tax_rate: typing.Optional[builtins.float] = None, + long_term_growth_rate: typing.Optional[builtins.float] = None, + cost_of_debt: typing.Optional[builtins.float] = None, + cost_of_equity: typing.Optional[builtins.float] = None, + market_risk_premium: typing.Optional[builtins.float] = None, + beta: typing.Optional[builtins.float] = None, + risk_free_rate: typing.Optional[builtins.float] = None, + ) -> builtins.list[CustomDcfValuation]: + r""" + Retrieves a custom unlevered discounted-cash-flow valuation. + """ + + async def custom_levered( + self, + symbol: builtins.str, + *, + revenue_growth_pct: typing.Optional[builtins.float] = None, + ebitda_pct: typing.Optional[builtins.float] = None, + depreciation_and_amortization_pct: typing.Optional[builtins.float] = None, + cash_and_short_term_investments_pct: typing.Optional[builtins.float] = None, + receivables_pct: typing.Optional[builtins.float] = None, + inventories_pct: typing.Optional[builtins.float] = None, + payable_pct: typing.Optional[builtins.float] = None, + ebit_pct: typing.Optional[builtins.float] = None, + capital_expenditure_pct: typing.Optional[builtins.float] = None, + operating_cash_flow_pct: typing.Optional[builtins.float] = None, + selling_general_and_administrative_expenses_pct: typing.Optional[ + builtins.float + ] = None, + tax_rate: typing.Optional[builtins.float] = None, + long_term_growth_rate: typing.Optional[builtins.float] = None, + cost_of_debt: typing.Optional[builtins.float] = None, + cost_of_equity: typing.Optional[builtins.float] = None, + market_risk_premium: typing.Optional[builtins.float] = None, + beta: typing.Optional[builtins.float] = None, + risk_free_rate: typing.Optional[builtins.float] = None, + ) -> builtins.list[CustomLeveredDcfValuation]: + r""" + Retrieves a custom levered discounted-cash-flow valuation. + """ + + @typing.final class CustomDcfValuation: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/directory/__init__.pyi b/crates/fmp-py/python/fmp/_native/directory/__init__.pyi index 23e0b435..6779b7f0 100644 --- a/crates/fmp-py/python/fmp/_native/directory/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/directory/__init__.pyi @@ -6,6 +6,7 @@ import typing __all__ = [ "ActivelyTradingSymbol", + "AsyncDirectoryNamespace", "AvailableCountry", "AvailableExchange", "AvailableIndustry", @@ -42,6 +43,84 @@ class ActivelyTradingSymbol: """ +@typing.final +class AsyncDirectoryNamespace: + r""" + Directory endpoints for a single async client, exposed as `client.directory`. + """ + + async def company_symbols(self) -> builtins.list[CompanySymbol]: + r""" + Lists worldwide company and instrument symbols. + """ + + async def financial_statement_symbols( + self, + ) -> builtins.list[FinancialStatementSymbol]: + r""" + Lists worldwide companies with financial statements available. + """ + + async def cik_list( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[CikListing]: + r""" + Lists US SEC entities with optional provider pagination. + """ + + async def symbol_changes( + self, + *, + invalid: typing.Optional[builtins.bool] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[SymbolChange]: + r""" + Lists US symbol changes without interpreting the provider's `invalid` flag. + """ + + async def etf_symbols(self) -> builtins.list[EtfSymbol]: + r""" + Lists worldwide exchange-traded fund symbols. + """ + + async def actively_trading(self) -> builtins.list[ActivelyTradingSymbol]: + r""" + Lists worldwide actively trading companies and instruments. + """ + + async def earnings_transcript_list( + self, + ) -> builtins.list[EarningsTranscriptAvailability]: + r""" + Lists US companies with their available earnings-transcript counts. + """ + + async def available_exchanges( + self, *, extended: typing.Optional[builtins.bool] = None + ) -> builtins.list[AvailableExchange]: + r""" + Lists supported worldwide stock exchanges. + """ + + async def available_sectors(self) -> builtins.list[AvailableSector]: + r""" + Lists the provider's available sectors. + """ + + async def available_industries(self) -> builtins.list[AvailableIndustry]: + r""" + Lists the provider's available industries. + """ + + async def available_countries(self) -> builtins.list[AvailableCountry]: + r""" + Lists the provider's available countries. + """ + + @typing.final class AvailableCountry: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/economics/__init__.pyi b/crates/fmp-py/python/fmp/_native/economics/__init__.pyi index 8fbbecd9..f2795172 100644 --- a/crates/fmp-py/python/fmp/_native/economics/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/economics/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncEconomicsNamespace", "EconomicCalendarEvent", "EconomicIndicatorObservation", "EconomicsNamespace", @@ -13,6 +14,50 @@ __all__ = [ ] +@typing.final +class AsyncEconomicsNamespace: + r""" + Economics endpoints for a single async client, exposed as `client.economics`. + """ + + async def treasury_rates( + self, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[TreasuryRate]: + r""" + Retrieves Treasury-rate observations. + """ + + async def indicators( + self, + name: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[EconomicIndicatorObservation]: + r""" + Retrieves observations for one economic indicator. + """ + + async def calendar( + self, + *, + country: typing.Optional[builtins.str] = None, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[EconomicCalendarEvent]: + r""" + Retrieves economic-calendar events. + """ + + async def market_risk_premium(self) -> builtins.list[MarketRiskPremium]: + r""" + Retrieves the provider's queryless country risk-premium rows. + """ + + @typing.final class EconomicCalendarEvent: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/esg/__init__.pyi b/crates/fmp-py/python/fmp/_native/esg/__init__.pyi index c78237ae..734a57bc 100644 --- a/crates/fmp-py/python/fmp/_native/esg/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/esg/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncEsgNamespace", "EsgBenchmark", "EsgDisclosure", "EsgNamespace", @@ -12,6 +13,30 @@ __all__ = [ ] +@typing.final +class AsyncEsgNamespace: + r""" + Esg endpoints for a single async client, exposed as `client.esg`. + """ + + async def disclosures(self, symbol: builtins.str) -> builtins.list[EsgDisclosure]: + r""" + Retrieves ESG disclosure filings for one company. + """ + + async def ratings(self, symbol: builtins.str) -> builtins.list[EsgRating]: + r""" + Retrieves ESG ratings for one company. + """ + + async def benchmark( + self, *, year: typing.Optional[builtins.str] = None + ) -> builtins.list[EsgBenchmark]: + r""" + Retrieves sector ESG benchmarks, optionally for one provider year string. + """ + + @typing.final class EsgBenchmark: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/forex/__init__.pyi b/crates/fmp-py/python/fmp/_native/forex/__init__.pyi index 14795fe3..ceeae5b9 100644 --- a/crates/fmp-py/python/fmp/_native/forex/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/forex/__init__.pyi @@ -9,11 +9,93 @@ import fmp._native.quote import typing __all__ = [ + "AsyncForexNamespace", "ForexNamespace", "ForexPair", ] +@typing.final +class AsyncForexNamespace: + r""" + Forex endpoints for a single async client, exposed as `client.forex`. + """ + + async def list(self) -> builtins.list[ForexPair]: + r""" + Lists the provider's documented forex pairs. + """ + + async def quote( + self, symbol: builtins.str + ) -> builtins.list[fmp._native.quote.Quote]: + r""" + Retrieves a detailed US-only quote for one forex pair. + """ + + async def quote_short( + self, symbol: builtins.str + ) -> builtins.list[fmp._native.quote.QuoteShort]: + r""" + Retrieves a compact US-only quote for one forex pair. + """ + + async def chart_light( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartLightBar]: + r""" + Retrieves compact end-of-day forex history. + """ + + async def chart_full( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartFullBar]: + r""" + Retrieves detailed end-of-day forex history. + """ + + async def chart_one_minute( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartIntradayBar]: + r""" + Retrieves one-minute forex history. + """ + + async def chart_five_minutes( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartIntradayBar]: + r""" + Retrieves five-minute forex history. + """ + + async def chart_one_hour( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartIntradayBar]: + r""" + Retrieves one-hour forex history. + """ + + @typing.final class ForexNamespace: r""" diff --git a/crates/fmp-py/python/fmp/_native/fundraising/__init__.pyi b/crates/fmp-py/python/fmp/_native/fundraising/__init__.pyi index d6b14b17..1a5fb3e4 100644 --- a/crates/fmp-py/python/fmp/_native/fundraising/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/fundraising/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncFundraisingNamespace", "CrowdfundingOffering", "CrowdfundingOfferingSearchResult", "FundraisingNamespace", @@ -13,6 +14,62 @@ __all__ = [ ] +@typing.final +class AsyncFundraisingNamespace: + r""" + Fundraising endpoints for a single async client, exposed as `client.fundraising`. + """ + + async def latest_crowdfunding_offerings( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[CrowdfundingOffering]: + r""" + Retrieves the latest US crowdfunding offerings. + """ + + async def crowdfunding_offerings_by_cik( + self, cik: builtins.str + ) -> builtins.list[CrowdfundingOffering]: + r""" + Retrieves US crowdfunding offerings for one issuer CIK. + """ + + async def search_crowdfunding_offerings( + self, name: builtins.str + ) -> builtins.list[CrowdfundingOfferingSearchResult]: + r""" + Searches US crowdfunding offerings by company, campaign, or platform text. + """ + + async def search_regulation_d_offerings( + self, name: builtins.str + ) -> builtins.list[RegulationDOfferingSearchResult]: + r""" + Searches US Regulation D offerings by company name or stock symbol. + """ + + async def latest_regulation_d_offerings( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + cik: typing.Optional[builtins.str] = None, + ) -> builtins.list[RegulationDOffering]: + r""" + Retrieves the latest US Regulation D offerings. + """ + + async def regulation_d_offerings_by_cik( + self, cik: builtins.str + ) -> builtins.list[RegulationDOffering]: + r""" + Retrieves US Regulation D offerings for one issuer CIK. + """ + + @typing.final class CrowdfundingOffering: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/funds/__init__.pyi b/crates/fmp-py/python/fmp/_native/funds/__init__.pyi index ba09ff8d..e3f4eb5d 100644 --- a/crates/fmp-py/python/fmp/_native/funds/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/funds/__init__.pyi @@ -7,6 +7,7 @@ import fmp._native.institutional_ownership import typing __all__ = [ + "AsyncFundsNamespace", "EtfAssetExposure", "EtfCountryWeighting", "EtfFundHolding", @@ -20,6 +21,77 @@ __all__ = [ ] +@typing.final +class AsyncFundsNamespace: + r""" + Funds endpoints for a single async client, exposed as `client.funds`. + """ + + async def etf_holdings(self, symbol: builtins.str) -> builtins.list[EtfFundHolding]: + r""" + Retrieves the worldwide holdings of one ETF or mutual fund. + """ + + async def etf_info(self, symbol: builtins.str) -> builtins.list[EtfFundInfo]: + r""" + Retrieves worldwide descriptive and trading information for one fund. + """ + + async def etf_country_weightings( + self, symbol: builtins.str + ) -> builtins.list[EtfCountryWeighting]: + r""" + Retrieves the worldwide country allocation for one ETF or mutual fund. + """ + + async def etf_asset_exposure( + self, symbol: builtins.str + ) -> builtins.list[EtfAssetExposure]: + r""" + Retrieves the worldwide ETF exposure to one requested asset. + """ + + async def etf_sector_weightings( + self, symbol: builtins.str + ) -> builtins.list[EtfSectorWeighting]: + r""" + Retrieves the worldwide sector allocation for one ETF or mutual fund. + """ + + async def latest_disclosure_holders( + self, symbol: builtins.str + ) -> builtins.list[FundDisclosureHolder]: + r""" + Retrieves the latest US fund disclosures holding one requested asset. + """ + + async def disclosures( + self, + symbol: builtins.str, + year: builtins.int, + quarter: builtins.int, + *, + cik: typing.Optional[builtins.str] = None, + ) -> builtins.list[FundDisclosure]: + r""" + Retrieves positions from one US mutual-fund disclosure period. + """ + + async def search_disclosure_holders( + self, name: builtins.str + ) -> builtins.list[FundDisclosureSearchResult]: + r""" + Searches US mutual-fund and ETF disclosure holders by exact name text. + """ + + async def disclosure_dates( + self, symbol: builtins.str, *, cik: typing.Optional[builtins.str] = None + ) -> builtins.list[fmp._native.institutional_ownership.Form13fFilingDate]: + r""" + Retrieves available US fund-disclosure reporting dates. + """ + + @typing.final class EtfAssetExposure: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/indexes/__init__.pyi b/crates/fmp-py/python/fmp/_native/indexes/__init__.pyi index cad7e343..14008d91 100644 --- a/crates/fmp-py/python/fmp/_native/indexes/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/indexes/__init__.pyi @@ -9,6 +9,7 @@ import fmp._native.quote import typing __all__ = [ + "AsyncIndexesNamespace", "HistoricalIndexConstituent", "IndexConstituent", "IndexListing", @@ -16,6 +17,123 @@ __all__ = [ ] +@typing.final +class AsyncIndexesNamespace: + r""" + Indexes endpoints for a single async client, exposed as `client.indexes`. + """ + + async def list(self) -> builtins.list[IndexListing]: + r""" + Lists worldwide stock-market indexes. + """ + + async def quote( + self, symbol: builtins.str + ) -> builtins.list[fmp._native.quote.Quote]: + r""" + Retrieves a detailed quote for one stock-market index. + """ + + async def quote_short( + self, symbol: builtins.str + ) -> builtins.list[fmp._native.quote.QuoteShort]: + r""" + Retrieves a compact quote for one stock-market index. + """ + + async def chart_light( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartLightBar]: + r""" + Retrieves compact end-of-day history for one stock-market index. + """ + + async def chart_full( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartFullBar]: + r""" + Retrieves detailed end-of-day history for one stock-market index. + """ + + async def chart_one_minute( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartIntradayBar]: + r""" + Retrieves one-minute history for one stock-market index. + """ + + async def chart_five_minutes( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartIntradayBar]: + r""" + Retrieves five-minute history for one stock-market index. + """ + + async def chart_one_hour( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[fmp._native.chart.StockChartIntradayBar]: + r""" + Retrieves one-hour history for one stock-market index. + """ + + async def sp500_constituents(self) -> builtins.list[IndexConstituent]: + r""" + Lists the current S&P 500 constituents. + """ + + async def nasdaq_constituents(self) -> builtins.list[IndexConstituent]: + r""" + Lists the current Nasdaq constituents. + """ + + async def dow_jones_constituents(self) -> builtins.list[IndexConstituent]: + r""" + Lists the current Dow Jones constituents. + """ + + async def historical_sp500_constituents( + self, + ) -> builtins.list[HistoricalIndexConstituent]: + r""" + Lists historical S&P 500 constituent changes. + """ + + async def historical_nasdaq_constituents( + self, + ) -> builtins.list[HistoricalIndexConstituent]: + r""" + Lists historical Nasdaq constituent changes. + """ + + async def historical_dow_jones_constituents( + self, + ) -> builtins.list[HistoricalIndexConstituent]: + r""" + Lists historical Dow Jones constituent changes. + """ + + @typing.final class HistoricalIndexConstituent: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/insider_trading/__init__.pyi b/crates/fmp-py/python/fmp/_native/insider_trading/__init__.pyi index 7ddc59ea..922b2c37 100644 --- a/crates/fmp-py/python/fmp/_native/insider_trading/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/insider_trading/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncInsiderTradingNamespace", "BeneficialOwnershipAcquisition", "InsiderReportingName", "InsiderTrade", @@ -14,6 +15,64 @@ __all__ = [ ] +@typing.final +class AsyncInsiderTradingNamespace: + r""" + Insider trading endpoints for a single async client, exposed as `client.insider_trading`. + """ + + async def latest_trades( + self, + *, + date: typing.Optional[datetime.date | builtins.str] = None, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[InsiderTrade]: + r""" + Retrieves the latest US insider trades with optional date and pagination filters. + """ + + async def search_trades( + self, + *, + symbol: typing.Optional[builtins.str] = None, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + reporting_cik: typing.Optional[builtins.str] = None, + company_cik: typing.Optional[builtins.str] = None, + transaction_type: typing.Optional[builtins.str] = None, + ) -> builtins.list[InsiderTrade]: + r""" + Searches US insider trades using independently optional filters. + """ + + async def search_reporting_names( + self, name: builtins.str + ) -> builtins.list[InsiderReportingName]: + r""" + Searches US insider-reporting identities by name. + """ + + async def transaction_types(self) -> builtins.list[InsiderTransactionType]: + r""" + Retrieves the US insider transaction-type taxonomy. + """ + + async def trade_statistics( + self, symbol: builtins.str + ) -> builtins.list[InsiderTradeStatistics]: + r""" + Retrieves quarterly US insider-trade statistics for one ticker. + """ + + async def beneficial_ownership_acquisitions( + self, symbol: builtins.str, *, limit: typing.Optional[builtins.int] = None + ) -> builtins.list[BeneficialOwnershipAcquisition]: + r""" + Retrieves US beneficial-ownership acquisition filings for one ticker. + """ + + @typing.final class BeneficialOwnershipAcquisition: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/institutional_ownership/__init__.pyi b/crates/fmp-py/python/fmp/_native/institutional_ownership/__init__.pyi index 1a43806c..5878c0b4 100644 --- a/crates/fmp-py/python/fmp/_native/institutional_ownership/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/institutional_ownership/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncInstitutionalOwnershipNamespace", "Form13fFilingDate", "HolderIndustryBreakdown", "HolderPerformanceSummary", @@ -17,6 +18,78 @@ __all__ = [ ] +@typing.final +class AsyncInstitutionalOwnershipNamespace: + r""" + Institutional ownership endpoints for a single async client, exposed as `client.institutional_ownership`. + """ + + async def latest_filings( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[InstitutionalOwnershipFiling]: + r""" + Retrieves the latest US institutional-ownership filings. + """ + + async def extract( + self, cik: builtins.str, year: builtins.int, quarter: builtins.int + ) -> builtins.list[InstitutionalHolding]: + r""" + Extracts the positions from one US institutional-ownership filing period. + """ + + async def form_13f_filing_dates( + self, cik: builtins.str + ) -> builtins.list[Form13fFilingDate]: + r""" + Retrieves available Form 13F filing dates for one US institutional holder. + """ + + async def holder_analytics( + self, + symbol: builtins.str, + year: builtins.int, + quarter: builtins.int, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[InstitutionalHolderAnalytics]: + r""" + Retrieves holder-level analytics for one US security and filing period. + """ + + async def holder_performance_summary( + self, cik: builtins.str, *, page: typing.Optional[builtins.int] = None + ) -> builtins.list[HolderPerformanceSummary]: + r""" + Retrieves a US institutional holder's portfolio performance summary. + """ + + async def holder_industry_breakdown( + self, cik: builtins.str, year: builtins.int, quarter: builtins.int + ) -> builtins.list[HolderIndustryBreakdown]: + r""" + Retrieves a US institutional holder's industry breakdown for one period. + """ + + async def positions_summary( + self, symbol: builtins.str, year: builtins.int, quarter: builtins.int + ) -> builtins.list[InstitutionalPositionSummary]: + r""" + Retrieves the US institutional position summary for one security and period. + """ + + async def industry_summary( + self, year: builtins.int, quarter: builtins.int + ) -> builtins.list[InstitutionalIndustrySummary]: + r""" + Retrieves US institutional industry values for one reporting period. + """ + + @typing.final class Form13fFilingDate: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/market/__init__.pyi b/crates/fmp-py/python/fmp/_native/market/__init__.pyi index a3c0edb4..9937ae3f 100644 --- a/crates/fmp-py/python/fmp/_native/market/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/market/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncMarketNamespace", "IndustryPe", "IndustryPerformance", "MarketMover", @@ -14,6 +15,120 @@ __all__ = [ ] +@typing.final +class AsyncMarketNamespace: + r""" + Market endpoints for a single async client, exposed as `client.market`. + """ + + async def sector_performance_snapshot( + self, + date: datetime.date | builtins.str, + *, + exchange: typing.Optional[builtins.str] = None, + sector: typing.Optional[builtins.str] = None, + ) -> builtins.list[SectorPerformance]: + r""" + Retrieves a worldwide dated sector-performance snapshot. + """ + + async def industry_performance_snapshot( + self, + date: datetime.date | builtins.str, + *, + exchange: typing.Optional[builtins.str] = None, + industry: typing.Optional[builtins.str] = None, + ) -> builtins.list[IndustryPerformance]: + r""" + Retrieves a worldwide dated industry-performance snapshot. + """ + + async def sector_pe_snapshot( + self, + date: datetime.date | builtins.str, + *, + exchange: typing.Optional[builtins.str] = None, + sector: typing.Optional[builtins.str] = None, + ) -> builtins.list[SectorPe]: + r""" + Retrieves a worldwide dated sector P/E snapshot. + """ + + async def industry_pe_snapshot( + self, + date: datetime.date | builtins.str, + *, + exchange: typing.Optional[builtins.str] = None, + industry: typing.Optional[builtins.str] = None, + ) -> builtins.list[IndustryPe]: + r""" + Retrieves a worldwide dated industry P/E snapshot. + """ + + async def historical_sector_performance( + self, + sector: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + exchange: typing.Optional[builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[SectorPerformance]: + r""" + Retrieves worldwide historical sector performance. + """ + + async def historical_industry_performance( + self, + industry: builtins.str, + *, + exchange: typing.Optional[builtins.str] = None, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[IndustryPerformance]: + r""" + Retrieves worldwide historical industry performance. + """ + + async def historical_sector_pe( + self, + sector: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + exchange: typing.Optional[builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[SectorPe]: + r""" + Retrieves worldwide historical sector P/E ratios. + """ + + async def historical_industry_pe( + self, + industry: builtins.str, + *, + exchange: typing.Optional[builtins.str] = None, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[IndustryPe]: + r""" + Retrieves worldwide historical industry P/E ratios. + """ + + async def biggest_gainers(self) -> builtins.list[MarketMover]: + r""" + Retrieves the US-only list of biggest stock gainers. + """ + + async def biggest_losers(self) -> builtins.list[MarketMover]: + r""" + Retrieves the US-only list of biggest stock losers. + """ + + async def most_actives(self) -> builtins.list[MarketMover]: + r""" + Retrieves the US-only list of most actively traded stocks. + """ + + @typing.final class IndustryPe: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/market_hours/__init__.pyi b/crates/fmp-py/python/fmp/_native/market_hours/__init__.pyi index 91db6683..61642feb 100644 --- a/crates/fmp-py/python/fmp/_native/market_hours/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/market_hours/__init__.pyi @@ -5,12 +5,45 @@ import datetime import typing __all__ = [ + "AsyncMarketHoursNamespace", "ExchangeHoliday", "ExchangeMarketHours", "MarketHoursNamespace", ] +@typing.final +class AsyncMarketHoursNamespace: + r""" + Market hours endpoints for a single async client, exposed as `client.market_hours`. + """ + + async def exchange( + self, exchange: builtins.str, *, timestamp: typing.Optional[builtins.str] = None + ) -> builtins.list[ExchangeMarketHours]: + r""" + Retrieves trading hours for one exchange. + """ + + async def holidays( + self, + exchange: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[ExchangeHoliday]: + r""" + Retrieves holidays for one exchange. + """ + + async def all_exchanges( + self, *, timestamp: typing.Optional[builtins.str] = None + ) -> builtins.list[ExchangeMarketHours]: + r""" + Retrieves trading hours for all exchanges. + """ + + @typing.final class ExchangeHoliday: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/news/__init__.pyi b/crates/fmp-py/python/fmp/_native/news/__init__.pyi index bf9fb4fd..848222de 100644 --- a/crates/fmp-py/python/fmp/_native/news/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/news/__init__.pyi @@ -6,6 +6,7 @@ import typing __all__ = [ "Article", + "AsyncNewsNamespace", "NewsArticle", "NewsNamespace", ] @@ -63,6 +64,135 @@ class Article: """ +@typing.final +class AsyncNewsNamespace: + r""" + News endpoints for a single async client, exposed as `client.news`. + """ + + async def articles( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[Article]: + r""" + Retrieves US-authored Financial Modeling Prep articles. + """ + + async def latest_general_news( + self, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[NewsArticle]: + r""" + Retrieves the latest worldwide general-news articles. + """ + + async def latest_press_releases( + self, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[NewsArticle]: + r""" + Retrieves the latest US press releases. + """ + + async def latest_stock_news( + self, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[NewsArticle]: + r""" + Retrieves the latest stock-news articles. + """ + + async def latest_crypto_news( + self, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[NewsArticle]: + r""" + Retrieves the latest cryptocurrency-news articles. + """ + + async def latest_forex_news( + self, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[NewsArticle]: + r""" + Retrieves the latest foreign-exchange-news articles. + """ + + async def search_press_releases( + self, + symbols: builtins.str | typing.Sequence[builtins.str], + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[NewsArticle]: + r""" + Searches US press releases for an ordered non-empty ticker list. + """ + + async def search_stock_news( + self, + symbols: builtins.str | typing.Sequence[builtins.str], + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[NewsArticle]: + r""" + Searches stock news for an ordered non-empty ticker list. + """ + + async def search_crypto_news( + self, + symbols: builtins.str | typing.Sequence[builtins.str], + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[NewsArticle]: + r""" + Searches cryptocurrency news for an ordered non-empty ticker list. + """ + + async def search_forex_news( + self, + symbols: builtins.str | typing.Sequence[builtins.str], + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[NewsArticle]: + r""" + Searches foreign-exchange news for an ordered non-empty ticker list. + """ + + @typing.final class NewsArticle: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/quote/__init__.pyi b/crates/fmp-py/python/fmp/_native/quote/__init__.pyi index 74c0b7ca..c4853004 100644 --- a/crates/fmp-py/python/fmp/_native/quote/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/quote/__init__.pyi @@ -6,6 +6,7 @@ import typing __all__ = [ "AftermarketQuote", "AftermarketTrade", + "AsyncQuoteNamespace", "Quote", "QuoteNamespace", "QuoteShort", @@ -97,6 +98,107 @@ class AftermarketTrade: """ +@typing.final +class AsyncQuoteNamespace: + r""" + Quote endpoints for a single async client, exposed as `client.quote`. + """ + + async def full(self, symbol: builtins.str) -> builtins.list[Quote]: + r""" + The full quote for a symbol. + """ + + async def short(self, symbol: builtins.str) -> builtins.list[QuoteShort]: + r""" + The compact quote array for a symbol. + """ + + async def mutual_funds(self) -> builtins.list[QuoteShort]: + r""" + Compact quotes for every mutual fund, without a query. + """ + + async def aftermarket_trade( + self, symbol: builtins.str + ) -> builtins.list[AftermarketTrade]: + r""" + Retrieves US aftermarket trades for one ticker. + """ + + async def aftermarket_quote( + self, symbol: builtins.str + ) -> builtins.list[AftermarketQuote]: + r""" + Retrieves US aftermarket bid-and-ask quotes for one ticker. + """ + + async def stock_price_change( + self, symbol: builtins.str + ) -> builtins.list[StockPriceChange]: + r""" + Retrieves worldwide percentage price changes for one stock. + """ + + async def batch( + self, symbols: builtins.str | typing.Sequence[builtins.str] + ) -> builtins.list[Quote]: + r""" + Retrieves detailed worldwide stock quotes for multiple tickers. + """ + + async def batch_short( + self, symbols: builtins.str | typing.Sequence[builtins.str] + ) -> builtins.list[QuoteShort]: + r""" + Retrieves compact worldwide stock quotes for multiple tickers. + """ + + async def batch_aftermarket_trade( + self, symbols: builtins.str | typing.Sequence[builtins.str] + ) -> builtins.list[AftermarketTrade]: + r""" + Retrieves US aftermarket trades for multiple tickers. + """ + + async def batch_aftermarket_quote( + self, symbols: builtins.str | typing.Sequence[builtins.str] + ) -> builtins.list[AftermarketQuote]: + r""" + Retrieves US aftermarket bid-and-ask quotes for multiple tickers. + """ + + async def exchange(self, exchange: builtins.str) -> builtins.list[QuoteShort]: + r""" + Retrieves compact quotes for every stock on one exchange. + """ + + async def etfs(self) -> builtins.list[QuoteShort]: + r""" + Retrieves compact quotes for the documented ETF universe. + """ + + async def commodities(self) -> builtins.list[QuoteShort]: + r""" + Retrieves compact quotes for the documented commodity universe. + """ + + async def cryptocurrencies(self) -> builtins.list[QuoteShort]: + r""" + Retrieves compact quotes for the documented cryptocurrency universe. + """ + + async def forex(self) -> builtins.list[QuoteShort]: + r""" + Retrieves compact quotes for the documented forex universe. + """ + + async def indexes(self) -> builtins.list[QuoteShort]: + r""" + Retrieves compact quotes for the documented index universe. + """ + + @typing.final class Quote: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/screener/__init__.pyi b/crates/fmp-py/python/fmp/_native/screener/__init__.pyi index ec5ba1fe..70f4bbdb 100644 --- a/crates/fmp-py/python/fmp/_native/screener/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/screener/__init__.pyi @@ -4,11 +4,47 @@ import builtins import typing __all__ = [ + "AsyncScreenerNamespace", "CompanyScreenerResult", "ScreenerNamespace", ] +@typing.final +class AsyncScreenerNamespace: + r""" + Screener endpoints for a single async client, exposed as `client.screener`. + """ + + async def companies( + self, + *, + market_cap_more_than: typing.Optional[builtins.int] = None, + market_cap_lower_than: typing.Optional[builtins.int] = None, + sector: typing.Optional[builtins.str] = None, + industry: typing.Optional[builtins.str] = None, + beta_more_than: typing.Optional[builtins.float] = None, + beta_lower_than: typing.Optional[builtins.float] = None, + price_more_than: typing.Optional[builtins.float] = None, + price_lower_than: typing.Optional[builtins.float] = None, + dividend_more_than: typing.Optional[builtins.float] = None, + dividend_lower_than: typing.Optional[builtins.float] = None, + volume_more_than: typing.Optional[builtins.int] = None, + volume_lower_than: typing.Optional[builtins.int] = None, + exchange: typing.Optional[builtins.str] = None, + country: typing.Optional[builtins.str] = None, + is_etf: typing.Optional[builtins.bool] = None, + is_fund: typing.Optional[builtins.bool] = None, + is_actively_trading: typing.Optional[builtins.bool] = None, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + include_all_share_classes: typing.Optional[builtins.bool] = None, + ) -> builtins.list[CompanyScreenerResult]: + r""" + Screens worldwide companies using the supplied optional filters. + """ + + @typing.final class CompanyScreenerResult: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/search/__init__.pyi b/crates/fmp-py/python/fmp/_native/search/__init__.pyi index db96cf2f..cef02cb5 100644 --- a/crates/fmp-py/python/fmp/_native/search/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/search/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncSearchNamespace", "CikSearchResult", "CusipSearchResult", "ExchangeVariant", @@ -15,6 +16,59 @@ __all__ = [ ] +@typing.final +class AsyncSearchNamespace: + r""" + Search endpoints for a single async client, exposed as `client.search`. + """ + + async def symbol( + self, + query: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + exchange: typing.Optional[builtins.str] = None, + ) -> builtins.list[SymbolSearchResult]: + r""" + Searches worldwide companies and instruments by symbol. + """ + + async def name( + self, + query: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + exchange: typing.Optional[builtins.str] = None, + ) -> builtins.list[NameSearchResult]: + r""" + Searches worldwide companies and instruments by name. + """ + + async def cik( + self, cik: builtins.str, *, limit: typing.Optional[builtins.int] = None + ) -> builtins.list[CikSearchResult]: + r""" + Searches US companies by Central Index Key. + """ + + async def cusip(self, cusip: builtins.str) -> builtins.list[CusipSearchResult]: + r""" + Searches worldwide securities by CUSIP without constraining its length. + """ + + async def isin(self, isin: builtins.str) -> builtins.list[IsinSearchResult]: + r""" + Searches worldwide securities by ISIN without constraining its length. + """ + + async def exchange_variants( + self, symbol: builtins.str + ) -> builtins.list[ExchangeVariant]: + r""" + Finds worldwide exchange listings for a provider ticker. + """ + + @typing.final class CikSearchResult: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/sec_filings/__init__.pyi b/crates/fmp-py/python/fmp/_native/sec_filings/__init__.pyi index 81f6e4cc..d2163214 100644 --- a/crates/fmp-py/python/fmp/_native/sec_filings/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/sec_filings/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncSecFilingsNamespace", "SecCompanyProfile", "SecCompanySearchResult", "SecFiling", @@ -13,6 +14,135 @@ __all__ = [ ] +@typing.final +class AsyncSecFilingsNamespace: + r""" + Sec filings endpoints for a single async client, exposed as `client.sec_filings`. + """ + + async def latest_8k( + self, + from_: datetime.date | builtins.str, + to: datetime.date | builtins.str, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[SecFiling]: + r""" + Retrieves the latest US 8-K SEC filings within the required date range. + """ + + async def latest( + self, + from_: datetime.date | builtins.str, + to: datetime.date | builtins.str, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[SecFiling]: + r""" + Retrieves the latest US financial SEC filings within the required date range. + """ + + async def by_form_type( + self, + form_type: builtins.str, + from_: datetime.date | builtins.str, + to: datetime.date | builtins.str, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[SecFiling]: + r""" + Retrieves US SEC filings by their open form type. + """ + + async def by_symbol( + self, + symbol: builtins.str, + from_: datetime.date | builtins.str, + to: datetime.date | builtins.str, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[SecFiling]: + r""" + Retrieves US SEC filings for a ticker. + """ + + async def by_cik( + self, + cik: builtins.str, + from_: datetime.date | builtins.str, + to: datetime.date | builtins.str, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[SecFiling]: + r""" + Retrieves US SEC filings for a string-backed CIK. + """ + + async def search_companies_by_name( + self, company: builtins.str + ) -> builtins.list[SecCompanySearchResult]: + r""" + Searches US SEC companies by company-name text. + """ + + async def search_companies_by_symbol( + self, symbol: builtins.str + ) -> builtins.list[SecCompanySearchResult]: + r""" + Searches US SEC companies by ticker. + """ + + async def search_companies_by_cik( + self, cik: builtins.str + ) -> builtins.list[SecCompanySearchResult]: + r""" + Searches US SEC companies by string-backed CIK. + """ + + async def company_profile( + self, symbol: builtins.str, *, cik_a: typing.Optional[builtins.str] = None + ) -> builtins.list[SecCompanyProfile]: + r""" + Retrieves the full US SEC company profile for a ticker. + """ + + async def industry_classifications( + self, + *, + industry_title: typing.Optional[builtins.str] = None, + sic_code: typing.Optional[builtins.str] = None, + ) -> builtins.list[SicClassification]: + r""" + Retrieves the US Standard Industrial Classification directory. + """ + + async def all_industry_classifications( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[SecCompanySearchResult]: + r""" + Retrieves the paginated US company industry-classification feed. + """ + + async def search_industry_classifications( + self, + *, + symbol: typing.Optional[builtins.str] = None, + cik: typing.Optional[builtins.str] = None, + sic_code: typing.Optional[builtins.str] = None, + ) -> list[dict[str, typing.Any]]: + r""" + Searches US industry classifications while preserving raw documented rows. + """ + + @typing.final class SecCompanyProfile: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/statements/__init__.pyi b/crates/fmp-py/python/fmp/_native/statements/__init__.pyi index aa5c659e..7508225d 100644 --- a/crates/fmp-py/python/fmp/_native/statements/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/statements/__init__.pyi @@ -23,6 +23,7 @@ from fmp._native.statements import summaries import fmp._native.statements.summaries __all__ = [ + "AsyncStatementsNamespace", "StatementsNamespace", "as_reported", "balance", @@ -37,6 +38,81 @@ __all__ = [ ] +@typing.final +class AsyncStatementsNamespace: + r""" + Statements endpoints for a single async client, exposed as `client.statements`. + """ + + @property + def as_reported( + self, + ) -> fmp._native.statements.as_reported.AsyncStatementsAsReportedNamespace: + r""" + The `statements.as_reported` endpoints, reached as `client.statements.as_reported`. + """ + + @property + def balance(self) -> fmp._native.statements.balance.AsyncStatementsBalanceNamespace: + r""" + The `statements.balance` endpoints, reached as `client.statements.balance`. + """ + + @property + def cash_flow( + self, + ) -> fmp._native.statements.cash_flow.AsyncStatementsCashFlowNamespace: + r""" + The `statements.cash_flow` endpoints, reached as `client.statements.cash_flow`. + """ + + @property + def growth(self) -> fmp._native.statements.growth.AsyncStatementsGrowthNamespace: + r""" + The `statements.growth` endpoints, reached as `client.statements.growth`. + """ + + @property + def income(self) -> fmp._native.statements.income.AsyncStatementsIncomeNamespace: + r""" + The `statements.income` endpoints, reached as `client.statements.income`. + """ + + @property + def metrics(self) -> fmp._native.statements.metrics.AsyncStatementsMetricsNamespace: + r""" + The `statements.metrics` endpoints, reached as `client.statements.metrics`. + """ + + @property + def ratios(self) -> fmp._native.statements.ratios.AsyncStatementsRatiosNamespace: + r""" + The `statements.ratios` endpoints, reached as `client.statements.ratios`. + """ + + @property + def reports(self) -> fmp._native.statements.reports.AsyncStatementsReportsNamespace: + r""" + The `statements.reports` endpoints, reached as `client.statements.reports`. + """ + + @property + def segmentation( + self, + ) -> fmp._native.statements.segmentation.AsyncStatementsSegmentationNamespace: + r""" + The `statements.segmentation` endpoints, reached as `client.statements.segmentation`. + """ + + @property + def summaries( + self, + ) -> fmp._native.statements.summaries.AsyncStatementsSummariesNamespace: + r""" + The `statements.summaries` endpoints, reached as `client.statements.summaries`. + """ + + @typing.final class StatementsNamespace: r""" diff --git a/crates/fmp-py/python/fmp/_native/statements/as_reported/__init__.pyi b/crates/fmp-py/python/fmp/_native/statements/as_reported/__init__.pyi index a16c9d82..0e08929b 100644 --- a/crates/fmp-py/python/fmp/_native/statements/as_reported/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/statements/as_reported/__init__.pyi @@ -6,6 +6,7 @@ import typing __all__ = [ "AsReportedFinancialStatement", + "AsyncStatementsAsReportedNamespace", "StatementsAsReportedNamespace", ] @@ -54,6 +55,57 @@ class AsReportedFinancialStatement: """ +@typing.final +class AsyncStatementsAsReportedNamespace: + r""" + Statements as reported endpoints for a single async client, exposed as `client.statements.as_reported`. + """ + + async def income( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + period: typing.Optional[typing.Literal["annual", "quarter"]] = None, + ) -> builtins.list[AsReportedFinancialStatement]: + r""" + Retrieves income statements as reported by one company. + """ + + async def balance_sheet( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + period: typing.Optional[typing.Literal["annual", "quarter"]] = None, + ) -> builtins.list[AsReportedFinancialStatement]: + r""" + Retrieves balance sheets as reported by one company. + """ + + async def cash_flow( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + period: typing.Optional[typing.Literal["annual", "quarter"]] = None, + ) -> builtins.list[AsReportedFinancialStatement]: + r""" + Retrieves cash-flow statements as reported by one company. + """ + + async def full( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + period: typing.Optional[typing.Literal["annual", "quarter"]] = None, + ) -> builtins.list[AsReportedFinancialStatement]: + r""" + Retrieves complete financial statements as reported by one company. + """ + + @typing.final class StatementsAsReportedNamespace: r""" diff --git a/crates/fmp-py/python/fmp/_native/statements/balance/__init__.pyi b/crates/fmp-py/python/fmp/_native/statements/balance/__init__.pyi index 3dea836f..a9cbdd47 100644 --- a/crates/fmp-py/python/fmp/_native/statements/balance/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/statements/balance/__init__.pyi @@ -5,12 +5,40 @@ import datetime import typing __all__ = [ + "AsyncStatementsBalanceNamespace", "BalanceSheetStatement", "BalanceSheetStatementTtm", "StatementsBalanceNamespace", ] +@typing.final +class AsyncStatementsBalanceNamespace: + r""" + Statements balance endpoints for a single async client, exposed as `client.statements.balance`. + """ + + async def statement( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + period: typing.Optional[ + typing.Literal["Q1", "Q2", "Q3", "Q4", "FY", "annual", "quarter"] + ] = None, + ) -> builtins.list[BalanceSheetStatement]: + r""" + Retrieves historical worldwide balance sheets for one company. + """ + + async def statement_ttm( + self, symbol: builtins.str, *, limit: typing.Optional[builtins.int] = None + ) -> builtins.list[BalanceSheetStatementTtm]: + r""" + Retrieves trailing-twelve-month worldwide balance sheets for one company. + """ + + @typing.final class BalanceSheetStatement: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/statements/cash_flow/__init__.pyi b/crates/fmp-py/python/fmp/_native/statements/cash_flow/__init__.pyi index e9680f33..566c8f0b 100644 --- a/crates/fmp-py/python/fmp/_native/statements/cash_flow/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/statements/cash_flow/__init__.pyi @@ -5,11 +5,39 @@ import datetime import typing __all__ = [ + "AsyncStatementsCashFlowNamespace", "CashFlowStatement", "StatementsCashFlowNamespace", ] +@typing.final +class AsyncStatementsCashFlowNamespace: + r""" + Statements cash flow endpoints for a single async client, exposed as `client.statements.cash_flow`. + """ + + async def statement( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + period: typing.Optional[ + typing.Literal["Q1", "Q2", "Q3", "Q4", "FY", "annual", "quarter"] + ] = None, + ) -> builtins.list[CashFlowStatement]: + r""" + Retrieves historical worldwide cash-flow statements for one company. + """ + + async def statement_ttm( + self, symbol: builtins.str, *, limit: typing.Optional[builtins.int] = None + ) -> builtins.list[CashFlowStatement]: + r""" + Retrieves trailing-twelve-month worldwide cash-flow statements for one company. + """ + + @typing.final class CashFlowStatement: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/statements/growth/__init__.pyi b/crates/fmp-py/python/fmp/_native/statements/growth/__init__.pyi index 76317cb3..19cde633 100644 --- a/crates/fmp-py/python/fmp/_native/statements/growth/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/statements/growth/__init__.pyi @@ -12,6 +12,7 @@ from fmp._native.statements.growth import income import fmp._native.statements.growth.income __all__ = [ + "AsyncStatementsGrowthNamespace", "StatementsGrowthNamespace", "balance", "cash_flow", @@ -20,6 +21,67 @@ __all__ = [ ] +@typing.final +class AsyncStatementsGrowthNamespace: + r""" + Statements growth endpoints for a single async client, exposed as `client.statements.growth`. + """ + + async def income( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + period: typing.Optional[ + typing.Literal["Q1", "Q2", "Q3", "Q4", "FY", "annual", "quarter"] + ] = None, + ) -> builtins.list[fmp._native.statements.growth.income.IncomeStatementGrowth]: + r""" + Retrieves worldwide income-statement growth for one company. + """ + + async def balance_sheet( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + period: typing.Optional[ + typing.Literal["Q1", "Q2", "Q3", "Q4", "FY", "annual", "quarter"] + ] = None, + ) -> builtins.list[ + fmp._native.statements.growth.balance.BalanceSheetStatementGrowth + ]: + r""" + Retrieves worldwide balance-sheet-statement growth for one company. + """ + + async def cash_flow( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + period: typing.Optional[ + typing.Literal["Q1", "Q2", "Q3", "Q4", "FY", "annual", "quarter"] + ] = None, + ) -> builtins.list[fmp._native.statements.growth.cash_flow.CashFlowStatementGrowth]: + r""" + Retrieves worldwide cash-flow-statement growth for one company. + """ + + async def financial( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + period: typing.Optional[ + typing.Literal["Q1", "Q2", "Q3", "Q4", "FY", "annual", "quarter"] + ] = None, + ) -> builtins.list[fmp._native.statements.growth.combined.FinancialStatementGrowth]: + r""" + Retrieves combined worldwide financial-statement growth for one company. + """ + + @typing.final class StatementsGrowthNamespace: r""" diff --git a/crates/fmp-py/python/fmp/_native/statements/income/__init__.pyi b/crates/fmp-py/python/fmp/_native/statements/income/__init__.pyi index 88fa7881..84bc4382 100644 --- a/crates/fmp-py/python/fmp/_native/statements/income/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/statements/income/__init__.pyi @@ -5,11 +5,39 @@ import datetime import typing __all__ = [ + "AsyncStatementsIncomeNamespace", "IncomeStatement", "StatementsIncomeNamespace", ] +@typing.final +class AsyncStatementsIncomeNamespace: + r""" + Statements income endpoints for a single async client, exposed as `client.statements.income`. + """ + + async def statement( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + period: typing.Optional[ + typing.Literal["Q1", "Q2", "Q3", "Q4", "FY", "annual", "quarter"] + ] = None, + ) -> builtins.list[IncomeStatement]: + r""" + Retrieves historical worldwide income statements for one company. + """ + + async def statement_ttm( + self, symbol: builtins.str, *, limit: typing.Optional[builtins.int] = None + ) -> builtins.list[IncomeStatement]: + r""" + Retrieves trailing-twelve-month worldwide income statements for one company. + """ + + @typing.final class IncomeStatement: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/statements/metrics/__init__.pyi b/crates/fmp-py/python/fmp/_native/statements/metrics/__init__.pyi index 6d13d3c1..98bbdf6e 100644 --- a/crates/fmp-py/python/fmp/_native/statements/metrics/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/statements/metrics/__init__.pyi @@ -5,12 +5,40 @@ import datetime import typing __all__ = [ + "AsyncStatementsMetricsNamespace", "KeyMetrics", "KeyMetricsTtm", "StatementsMetricsNamespace", ] +@typing.final +class AsyncStatementsMetricsNamespace: + r""" + Statements metrics endpoints for a single async client, exposed as `client.statements.metrics`. + """ + + async def key_metrics( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + period: typing.Optional[ + typing.Literal["Q1", "Q2", "Q3", "Q4", "FY", "annual", "quarter"] + ] = None, + ) -> builtins.list[KeyMetrics]: + r""" + Retrieves historical worldwide key metrics for one company. + """ + + async def key_metrics_ttm( + self, symbol: builtins.str + ) -> builtins.list[KeyMetricsTtm]: + r""" + Retrieves worldwide trailing-twelve-month key metrics for one company. + """ + + @typing.final class KeyMetrics: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/statements/ratios/__init__.pyi b/crates/fmp-py/python/fmp/_native/statements/ratios/__init__.pyi index 6efd80eb..758f4e20 100644 --- a/crates/fmp-py/python/fmp/_native/statements/ratios/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/statements/ratios/__init__.pyi @@ -5,12 +5,40 @@ import datetime import typing __all__ = [ + "AsyncStatementsRatiosNamespace", "FinancialRatios", "FinancialRatiosTtm", "StatementsRatiosNamespace", ] +@typing.final +class AsyncStatementsRatiosNamespace: + r""" + Statements ratios endpoints for a single async client, exposed as `client.statements.ratios`. + """ + + async def financial_ratios( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + period: typing.Optional[ + typing.Literal["Q1", "Q2", "Q3", "Q4", "FY", "annual", "quarter"] + ] = None, + ) -> builtins.list[FinancialRatios]: + r""" + Retrieves historical worldwide financial ratios for one company. + """ + + async def financial_ratios_ttm( + self, symbol: builtins.str + ) -> builtins.list[FinancialRatiosTtm]: + r""" + Retrieves trailing-twelve-month worldwide financial ratios for one company. + """ + + @typing.final class FinancialRatios: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/statements/reports/__init__.pyi b/crates/fmp-py/python/fmp/_native/statements/reports/__init__.pyi index febda1d2..7af6e570 100644 --- a/crates/fmp-py/python/fmp/_native/statements/reports/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/statements/reports/__init__.pyi @@ -6,12 +6,45 @@ import fmp._native import typing __all__ = [ + "AsyncStatementsReportsNamespace", "FinancialReportDate", "FinancialReportJson", "StatementsReportsNamespace", ] +@typing.final +class AsyncStatementsReportsNamespace: + r""" + Statements reports endpoints for a single async client, exposed as `client.statements.reports`. + """ + + async def dates(self, symbol: builtins.str) -> builtins.list[FinancialReportDate]: + r""" + Retrieves available financial-report periods and protected download links. + """ + + async def json( + self, + symbol: builtins.str, + year: builtins.int, + period: typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"], + ) -> FinancialReportJson: + r""" + Retrieves one dynamic JSON financial report as a single object. + """ + + async def xlsx( + self, + symbol: builtins.str, + year: builtins.int, + period: typing.Literal["Q1", "Q2", "Q3", "Q4", "FY"], + ) -> fmp._native.BinaryPayload: + r""" + Downloads one financial report using the endpoint's XLSX MIME policy. + """ + + @typing.final class FinancialReportDate: r""" diff --git a/crates/fmp-py/python/fmp/_native/statements/segmentation/__init__.pyi b/crates/fmp-py/python/fmp/_native/statements/segmentation/__init__.pyi index f7082d97..d3387049 100644 --- a/crates/fmp-py/python/fmp/_native/statements/segmentation/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/statements/segmentation/__init__.pyi @@ -5,11 +5,41 @@ import datetime import typing __all__ = [ + "AsyncStatementsSegmentationNamespace", "RevenueSegmentation", "StatementsSegmentationNamespace", ] +@typing.final +class AsyncStatementsSegmentationNamespace: + r""" + Statements segmentation endpoints for a single async client, exposed as `client.statements.segmentation`. + """ + + async def revenue_product( + self, + symbol: builtins.str, + *, + period: typing.Optional[typing.Literal["annual", "quarter"]] = None, + structure: typing.Optional[typing.Literal["flat"]] = None, + ) -> builtins.list[RevenueSegmentation]: + r""" + Retrieves product revenue segmentation for one company. + """ + + async def revenue_geographic( + self, + symbol: builtins.str, + *, + period: typing.Optional[typing.Literal["annual", "quarter"]] = None, + structure: typing.Optional[typing.Literal["flat"]] = None, + ) -> builtins.list[RevenueSegmentation]: + r""" + Retrieves geographic revenue segmentation for one company. + """ + + @typing.final class RevenueSegmentation: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/statements/summaries/__init__.pyi b/crates/fmp-py/python/fmp/_native/statements/summaries/__init__.pyi index a3dacffe..c2c666fe 100644 --- a/crates/fmp-py/python/fmp/_native/statements/summaries/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/statements/summaries/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncStatementsSummariesNamespace", "EnterpriseValue", "FinancialScore", "LatestFinancialStatement", @@ -13,6 +14,50 @@ __all__ = [ ] +@typing.final +class AsyncStatementsSummariesNamespace: + r""" + Statements summaries endpoints for a single async client, exposed as `client.statements.summaries`. + """ + + async def latest_financial_statements( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[LatestFinancialStatement]: + r""" + Retrieves the latest worldwide financial-statement filings. + """ + + async def financial_scores( + self, symbol: builtins.str + ) -> builtins.list[FinancialScore]: + r""" + Retrieves worldwide financial-health scores for one company. + """ + + async def owner_earnings( + self, symbol: builtins.str, *, limit: typing.Optional[builtins.int] = None + ) -> builtins.list[OwnerEarnings]: + r""" + Retrieves worldwide owner earnings for one company. + """ + + async def enterprise_values( + self, + symbol: builtins.str, + *, + limit: typing.Optional[builtins.int] = None, + period: typing.Optional[ + typing.Literal["Q1", "Q2", "Q3", "Q4", "FY", "annual", "quarter"] + ] = None, + ) -> builtins.list[EnterpriseValue]: + r""" + Retrieves worldwide enterprise values for one company. + """ + + @typing.final class EnterpriseValue: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/technical_indicators/__init__.pyi b/crates/fmp-py/python/fmp/_native/technical_indicators/__init__.pyi index 49d280df..d29535d1 100644 --- a/crates/fmp-py/python/fmp/_native/technical_indicators/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/technical_indicators/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncTechnicalIndicatorsNamespace", "AverageDirectionalIndexBar", "DoubleExponentialMovingAverageBar", "ExponentialMovingAverageBar", @@ -18,6 +19,148 @@ __all__ = [ ] +@typing.final +class AsyncTechnicalIndicatorsNamespace: + r""" + Technical indicators endpoints for a single async client, exposed as `client.technical_indicators`. + """ + + async def simple_moving_average( + self, + symbol: builtins.str, + period_length: builtins.int, + timeframe: typing.Literal[ + "1min", "5min", "15min", "30min", "1hour", "4hour", "1day" + ], + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[SimpleMovingAverageBar]: + r""" + Retrieves worldwide simple-moving-average bars. + """ + + async def exponential_moving_average( + self, + symbol: builtins.str, + period_length: builtins.int, + timeframe: typing.Literal[ + "1min", "5min", "15min", "30min", "1hour", "4hour", "1day" + ], + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[ExponentialMovingAverageBar]: + r""" + Retrieves worldwide exponential-moving-average bars. + """ + + async def weighted_moving_average( + self, + symbol: builtins.str, + period_length: builtins.int, + timeframe: typing.Literal[ + "1min", "5min", "15min", "30min", "1hour", "4hour", "1day" + ], + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[WeightedMovingAverageBar]: + r""" + Retrieves worldwide weighted-moving-average bars. + """ + + async def double_exponential_moving_average( + self, + symbol: builtins.str, + period_length: builtins.int, + timeframe: typing.Literal[ + "1min", "5min", "15min", "30min", "1hour", "4hour", "1day" + ], + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[DoubleExponentialMovingAverageBar]: + r""" + Retrieves worldwide double-exponential-moving-average bars. + """ + + async def triple_exponential_moving_average( + self, + symbol: builtins.str, + period_length: builtins.int, + timeframe: typing.Literal[ + "1min", "5min", "15min", "30min", "1hour", "4hour", "1day" + ], + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[TripleExponentialMovingAverageBar]: + r""" + Retrieves worldwide triple-exponential-moving-average bars. + """ + + async def relative_strength_index( + self, + symbol: builtins.str, + period_length: builtins.int, + timeframe: typing.Literal[ + "1min", "5min", "15min", "30min", "1hour", "4hour", "1day" + ], + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[RelativeStrengthIndexBar]: + r""" + Retrieves worldwide relative-strength-index bars. + """ + + async def standard_deviation( + self, + symbol: builtins.str, + period_length: builtins.int, + timeframe: typing.Literal[ + "1min", "5min", "15min", "30min", "1hour", "4hour", "1day" + ], + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[StandardDeviationBar]: + r""" + Retrieves worldwide standard-deviation bars. + """ + + async def williams( + self, + symbol: builtins.str, + period_length: builtins.int, + timeframe: typing.Literal[ + "1min", "5min", "15min", "30min", "1hour", "4hour", "1day" + ], + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[WilliamsBar]: + r""" + Retrieves worldwide Williams bars. + """ + + async def average_directional_index( + self, + symbol: builtins.str, + period_length: builtins.int, + timeframe: typing.Literal[ + "1min", "5min", "15min", "30min", "1hour", "4hour", "1day" + ], + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[AverageDirectionalIndexBar]: + r""" + Retrieves worldwide average-directional-index bars. + """ + + @typing.final class AverageDirectionalIndexBar: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/tipranks/__init__.pyi b/crates/fmp-py/python/fmp/_native/tipranks/__init__.pyi index 14ac1f75..adba2c6a 100644 --- a/crates/fmp-py/python/fmp/_native/tipranks/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/tipranks/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncTipranksNamespace", "TipRanksAnalystActionCounts", "TipRanksAnalystProfile", "TipRanksAnalystSummary", @@ -17,6 +18,100 @@ __all__ = [ ] +@typing.final +class AsyncTipranksNamespace: + r""" + Tipranks endpoints for a single async client, exposed as `client.tipranks`. + """ + + async def search_ratings( + self, + *, + expert_uid: typing.Optional[builtins.str] = None, + symbol: typing.Optional[builtins.str] = None, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + limit: typing.Optional[builtins.int] = None, + page: typing.Optional[builtins.int] = None, + nonadjusted: typing.Optional[builtins.bool] = None, + ) -> builtins.list[TipRanksRatingSearchResult]: + r""" + Retrieves individual analyst ratings from the TipRanks add-on. + """ + + async def point_in_time_ratings_by_symbol( + self, + symbol: builtins.str, + *, + date: typing.Optional[datetime.date | builtins.str] = None, + limit: typing.Optional[builtins.int] = None, + page: typing.Optional[builtins.int] = None, + nonadjusted: typing.Optional[builtins.bool] = None, + ) -> builtins.list[TipRanksPointInTimeRating]: + r""" + Retrieves a ticker's analyst ratings as of an optional snapshot date. + """ + + async def point_in_time_ratings_by_analyst( + self, + *, + expert_uid: typing.Optional[builtins.str] = None, + analyst_name: typing.Optional[builtins.str] = None, + date: typing.Optional[datetime.date | builtins.str] = None, + limit: typing.Optional[builtins.int] = None, + page: typing.Optional[builtins.int] = None, + nonadjusted: typing.Optional[builtins.bool] = None, + ) -> builtins.list[TipRanksPointInTimeRating]: + r""" + Retrieves an analyst's active coverage as of an optional snapshot date. + """ + + async def symbol_summary( + self, + symbol: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[TipRanksSymbolSummary]: + r""" + Retrieves a ticker's aggregate TipRanks ratings summary. + """ + + async def analyst_summary( + self, + expert_uid: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[TipRanksAnalystSummary]: + r""" + Retrieves an analyst's aggregate TipRanks ratings summary. + """ + + async def firm_summary( + self, + firm_name: builtins.str, + *, + from_: typing.Optional[datetime.date | builtins.str] = None, + to: typing.Optional[datetime.date | builtins.str] = None, + ) -> builtins.list[TipRanksFirmSummary]: + r""" + Retrieves a firm's aggregate TipRanks ratings summary. + """ + + async def analysts( + self, + *, + page: typing.Optional[builtins.int] = None, + limit: typing.Optional[builtins.int] = None, + firm_name: typing.Optional[builtins.str] = None, + analyst_name: typing.Optional[builtins.str] = None, + ) -> builtins.list[TipRanksAnalystProfile]: + r""" + Retrieves analyst profiles from the TipRanks directory. + """ + + @typing.final class TipRanksAnalystActionCounts: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/_native/transcripts/__init__.pyi b/crates/fmp-py/python/fmp/_native/transcripts/__init__.pyi index 3a1bb8e7..76849742 100644 --- a/crates/fmp-py/python/fmp/_native/transcripts/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/transcripts/__init__.pyi @@ -5,6 +5,7 @@ import datetime import typing __all__ = [ + "AsyncTranscriptsNamespace", "EarningsTranscript", "EarningsTranscriptDate", "LatestEarningsTranscript", @@ -12,6 +13,42 @@ __all__ = [ ] +@typing.final +class AsyncTranscriptsNamespace: + r""" + Transcripts endpoints for a single async client, exposed as `client.transcripts`. + """ + + async def latest( + self, + *, + limit: typing.Optional[builtins.int] = None, + page: typing.Optional[builtins.int] = None, + ) -> builtins.list[LatestEarningsTranscript]: + r""" + Retrieves the latest worldwide earnings-transcript metadata. + """ + + async def by_quarter( + self, + symbol: builtins.str, + year: builtins.int, + quarter: builtins.int, + *, + limit: typing.Optional[builtins.int] = None, + ) -> builtins.list[EarningsTranscript]: + r""" + Retrieves complete worldwide earnings-call transcripts. + """ + + async def dates( + self, symbol: builtins.str + ) -> builtins.list[EarningsTranscriptDate]: + r""" + Retrieves available worldwide transcript dates for one ticker. + """ + + @typing.final class EarningsTranscript: __hash__: typing.ClassVar[None] # type: ignore[assignment] diff --git a/crates/fmp-py/python/fmp/analyst/__init__.py b/crates/fmp-py/python/fmp/analyst/__init__.py index 8d495606..7b1173b8 100644 --- a/crates/fmp-py/python/fmp/analyst/__init__.py +++ b/crates/fmp-py/python/fmp/analyst/__init__.py @@ -1,9 +1,10 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.analyst import AnalystNamespace, FinancialEstimate, HistoricalRating, HistoricalStockGrade, PriceTargetConsensus, PriceTargetSummary, RatingSnapshot, StockGrade, StockGradesSummary +from fmp._native.analyst import AnalystNamespace, AsyncAnalystNamespace, FinancialEstimate, HistoricalRating, HistoricalStockGrade, PriceTargetConsensus, PriceTargetSummary, RatingSnapshot, StockGrade, StockGradesSummary __all__ = [ "AnalystNamespace", + "AsyncAnalystNamespace", "FinancialEstimate", "HistoricalRating", "HistoricalStockGrade", diff --git a/crates/fmp-py/python/fmp/bulk/__init__.py b/crates/fmp-py/python/fmp/bulk/__init__.py index 31a85d42..d966f9f8 100644 --- a/crates/fmp-py/python/fmp/bulk/__init__.py +++ b/crates/fmp-py/python/fmp/bulk/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.bulk import BulkNamespace, balance, cash_flow, eod, income, metrics, snapshots +from fmp._native.bulk import AsyncBulkNamespace, BulkNamespace, balance, cash_flow, eod, income, metrics, snapshots __all__ = [ + "AsyncBulkNamespace", "BulkNamespace", "balance", "cash_flow", diff --git a/crates/fmp-py/python/fmp/calendar/__init__.py b/crates/fmp-py/python/fmp/calendar/__init__.py index 4e8f678e..d062d16c 100644 --- a/crates/fmp-py/python/fmp/calendar/__init__.py +++ b/crates/fmp-py/python/fmp/calendar/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.calendar import CalendarNamespace, DividendEvent, EarningsEvent, IpoCalendarEvent, IpoDisclosure, IpoProspectus, StockSplitEvent +from fmp._native.calendar import AsyncCalendarNamespace, CalendarNamespace, DividendEvent, EarningsEvent, IpoCalendarEvent, IpoDisclosure, IpoProspectus, StockSplitEvent __all__ = [ + "AsyncCalendarNamespace", "CalendarNamespace", "DividendEvent", "EarningsEvent", diff --git a/crates/fmp-py/python/fmp/chart/__init__.py b/crates/fmp-py/python/fmp/chart/__init__.py index e8e416a1..3d088ee8 100644 --- a/crates/fmp-py/python/fmp/chart/__init__.py +++ b/crates/fmp-py/python/fmp/chart/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.chart import ChartNamespace, StockChartAdjustedBar, StockChartFullBar, StockChartIntradayBar, StockChartLightBar +from fmp._native.chart import AsyncChartNamespace, ChartNamespace, StockChartAdjustedBar, StockChartFullBar, StockChartIntradayBar, StockChartLightBar __all__ = [ + "AsyncChartNamespace", "ChartNamespace", "StockChartAdjustedBar", "StockChartFullBar", diff --git a/crates/fmp-py/python/fmp/client/__init__.py b/crates/fmp-py/python/fmp/client/__init__.py index fbf6a133..7f70025e 100644 --- a/crates/fmp-py/python/fmp/client/__init__.py +++ b/crates/fmp-py/python/fmp/client/__init__.py @@ -1,7 +1,8 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native import FmpClient +from fmp._native import AsyncFmpClient, FmpClient __all__ = [ + "AsyncFmpClient", "FmpClient", ] diff --git a/crates/fmp-py/python/fmp/commitment_of_traders/__init__.py b/crates/fmp-py/python/fmp/commitment_of_traders/__init__.py index f71dd105..e28b3171 100644 --- a/crates/fmp-py/python/fmp/commitment_of_traders/__init__.py +++ b/crates/fmp-py/python/fmp/commitment_of_traders/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.commitment_of_traders import CommitmentOfTradersNamespace, CotAnalysis, CotReport, CotReportListing +from fmp._native.commitment_of_traders import AsyncCommitmentOfTradersNamespace, CommitmentOfTradersNamespace, CotAnalysis, CotReport, CotReportListing __all__ = [ + "AsyncCommitmentOfTradersNamespace", "CommitmentOfTradersNamespace", "CotAnalysis", "CotReport", diff --git a/crates/fmp-py/python/fmp/commodities/__init__.py b/crates/fmp-py/python/fmp/commodities/__init__.py index 9e519efe..a1c39af7 100644 --- a/crates/fmp-py/python/fmp/commodities/__init__.py +++ b/crates/fmp-py/python/fmp/commodities/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.commodities import CommoditiesNamespace, CommodityListing +from fmp._native.commodities import AsyncCommoditiesNamespace, CommoditiesNamespace, CommodityListing __all__ = [ + "AsyncCommoditiesNamespace", "CommoditiesNamespace", "CommodityListing", ] diff --git a/crates/fmp-py/python/fmp/company/__init__.py b/crates/fmp-py/python/fmp/company/__init__.py index 5cc9f645..0282125f 100644 --- a/crates/fmp-py/python/fmp/company/__init__.py +++ b/crates/fmp-py/python/fmp/company/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.company import CompanyExecutive, CompanyMarketCapitalization, CompanyNamespace, CompanyNote, CompanyProfile, CompanyShareFloat, DelistedCompany, EmployeeCount, ExecutiveCompensation, ExecutiveCompensationBenchmark, MergerAcquisition, ShareFloat, StockPeer +from fmp._native.company import AsyncCompanyNamespace, CompanyExecutive, CompanyMarketCapitalization, CompanyNamespace, CompanyNote, CompanyProfile, CompanyShareFloat, DelistedCompany, EmployeeCount, ExecutiveCompensation, ExecutiveCompensationBenchmark, MergerAcquisition, ShareFloat, StockPeer __all__ = [ + "AsyncCompanyNamespace", "CompanyExecutive", "CompanyMarketCapitalization", "CompanyNamespace", diff --git a/crates/fmp-py/python/fmp/congressional/__init__.py b/crates/fmp-py/python/fmp/congressional/__init__.py index 99106c91..7ae6af41 100644 --- a/crates/fmp-py/python/fmp/congressional/__init__.py +++ b/crates/fmp-py/python/fmp/congressional/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.congressional import CongressionalDebtDetails, CongressionalMemberNetWorth, CongressionalMemberNetWorthAggregate, CongressionalMemberPosition, CongressionalMemberProfile, CongressionalNamespace, CongressionalNetWorthRange, CongressionalTrade +from fmp._native.congressional import AsyncCongressionalNamespace, CongressionalDebtDetails, CongressionalMemberNetWorth, CongressionalMemberNetWorthAggregate, CongressionalMemberPosition, CongressionalMemberProfile, CongressionalNamespace, CongressionalNetWorthRange, CongressionalTrade __all__ = [ + "AsyncCongressionalNamespace", "CongressionalDebtDetails", "CongressionalMemberNetWorth", "CongressionalMemberNetWorthAggregate", diff --git a/crates/fmp-py/python/fmp/crypto/__init__.py b/crates/fmp-py/python/fmp/crypto/__init__.py index 55b4a4c1..a2225b4e 100644 --- a/crates/fmp-py/python/fmp/crypto/__init__.py +++ b/crates/fmp-py/python/fmp/crypto/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.crypto import CryptoNamespace, CryptocurrencyListing +from fmp._native.crypto import AsyncCryptoNamespace, CryptoNamespace, CryptocurrencyListing __all__ = [ + "AsyncCryptoNamespace", "CryptoNamespace", "CryptocurrencyListing", ] diff --git a/crates/fmp-py/python/fmp/dcf/__init__.py b/crates/fmp-py/python/fmp/dcf/__init__.py index 916a1792..62120f76 100644 --- a/crates/fmp-py/python/fmp/dcf/__init__.py +++ b/crates/fmp-py/python/fmp/dcf/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.dcf import CustomDcfValuation, CustomLeveredDcfValuation, DcfNamespace, DcfValuation +from fmp._native.dcf import AsyncDcfNamespace, CustomDcfValuation, CustomLeveredDcfValuation, DcfNamespace, DcfValuation __all__ = [ + "AsyncDcfNamespace", "CustomDcfValuation", "CustomLeveredDcfValuation", "DcfNamespace", diff --git a/crates/fmp-py/python/fmp/directory/__init__.py b/crates/fmp-py/python/fmp/directory/__init__.py index 393ad7d1..6810b487 100644 --- a/crates/fmp-py/python/fmp/directory/__init__.py +++ b/crates/fmp-py/python/fmp/directory/__init__.py @@ -1,9 +1,10 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.directory import ActivelyTradingSymbol, AvailableCountry, AvailableExchange, AvailableIndustry, AvailableSector, CikListing, CompanySymbol, DirectoryNamespace, EarningsTranscriptAvailability, EtfSymbol, FinancialStatementSymbol, SymbolChange +from fmp._native.directory import ActivelyTradingSymbol, AsyncDirectoryNamespace, AvailableCountry, AvailableExchange, AvailableIndustry, AvailableSector, CikListing, CompanySymbol, DirectoryNamespace, EarningsTranscriptAvailability, EtfSymbol, FinancialStatementSymbol, SymbolChange __all__ = [ "ActivelyTradingSymbol", + "AsyncDirectoryNamespace", "AvailableCountry", "AvailableExchange", "AvailableIndustry", diff --git a/crates/fmp-py/python/fmp/economics/__init__.py b/crates/fmp-py/python/fmp/economics/__init__.py index 50f4a0f8..a12301f6 100644 --- a/crates/fmp-py/python/fmp/economics/__init__.py +++ b/crates/fmp-py/python/fmp/economics/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.economics import EconomicCalendarEvent, EconomicIndicatorObservation, EconomicsNamespace, MarketRiskPremium, TreasuryRate +from fmp._native.economics import AsyncEconomicsNamespace, EconomicCalendarEvent, EconomicIndicatorObservation, EconomicsNamespace, MarketRiskPremium, TreasuryRate __all__ = [ + "AsyncEconomicsNamespace", "EconomicCalendarEvent", "EconomicIndicatorObservation", "EconomicsNamespace", diff --git a/crates/fmp-py/python/fmp/esg/__init__.py b/crates/fmp-py/python/fmp/esg/__init__.py index 19460742..c3d4b951 100644 --- a/crates/fmp-py/python/fmp/esg/__init__.py +++ b/crates/fmp-py/python/fmp/esg/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.esg import EsgBenchmark, EsgDisclosure, EsgNamespace, EsgRating +from fmp._native.esg import AsyncEsgNamespace, EsgBenchmark, EsgDisclosure, EsgNamespace, EsgRating __all__ = [ + "AsyncEsgNamespace", "EsgBenchmark", "EsgDisclosure", "EsgNamespace", diff --git a/crates/fmp-py/python/fmp/forex/__init__.py b/crates/fmp-py/python/fmp/forex/__init__.py index 80d547ec..2502db2b 100644 --- a/crates/fmp-py/python/fmp/forex/__init__.py +++ b/crates/fmp-py/python/fmp/forex/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.forex import ForexNamespace, ForexPair +from fmp._native.forex import AsyncForexNamespace, ForexNamespace, ForexPair __all__ = [ + "AsyncForexNamespace", "ForexNamespace", "ForexPair", ] diff --git a/crates/fmp-py/python/fmp/fundraising/__init__.py b/crates/fmp-py/python/fmp/fundraising/__init__.py index eb949b35..d5a92c66 100644 --- a/crates/fmp-py/python/fmp/fundraising/__init__.py +++ b/crates/fmp-py/python/fmp/fundraising/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.fundraising import CrowdfundingOffering, CrowdfundingOfferingSearchResult, FundraisingNamespace, RegulationDOffering, RegulationDOfferingSearchResult +from fmp._native.fundraising import AsyncFundraisingNamespace, CrowdfundingOffering, CrowdfundingOfferingSearchResult, FundraisingNamespace, RegulationDOffering, RegulationDOfferingSearchResult __all__ = [ + "AsyncFundraisingNamespace", "CrowdfundingOffering", "CrowdfundingOfferingSearchResult", "FundraisingNamespace", diff --git a/crates/fmp-py/python/fmp/funds/__init__.py b/crates/fmp-py/python/fmp/funds/__init__.py index b63ebbbe..c6721672 100644 --- a/crates/fmp-py/python/fmp/funds/__init__.py +++ b/crates/fmp-py/python/fmp/funds/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.funds import EtfAssetExposure, EtfCountryWeighting, EtfFundHolding, EtfFundInfo, EtfSectorExposure, EtfSectorWeighting, FundDisclosure, FundDisclosureHolder, FundDisclosureSearchResult, FundsNamespace +from fmp._native.funds import AsyncFundsNamespace, EtfAssetExposure, EtfCountryWeighting, EtfFundHolding, EtfFundInfo, EtfSectorExposure, EtfSectorWeighting, FundDisclosure, FundDisclosureHolder, FundDisclosureSearchResult, FundsNamespace __all__ = [ + "AsyncFundsNamespace", "EtfAssetExposure", "EtfCountryWeighting", "EtfFundHolding", diff --git a/crates/fmp-py/python/fmp/indexes/__init__.py b/crates/fmp-py/python/fmp/indexes/__init__.py index 2a9b921a..3344a652 100644 --- a/crates/fmp-py/python/fmp/indexes/__init__.py +++ b/crates/fmp-py/python/fmp/indexes/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.indexes import HistoricalIndexConstituent, IndexConstituent, IndexListing, IndexesNamespace +from fmp._native.indexes import AsyncIndexesNamespace, HistoricalIndexConstituent, IndexConstituent, IndexListing, IndexesNamespace __all__ = [ + "AsyncIndexesNamespace", "HistoricalIndexConstituent", "IndexConstituent", "IndexListing", diff --git a/crates/fmp-py/python/fmp/insider_trading/__init__.py b/crates/fmp-py/python/fmp/insider_trading/__init__.py index a9adf358..7d6b9a29 100644 --- a/crates/fmp-py/python/fmp/insider_trading/__init__.py +++ b/crates/fmp-py/python/fmp/insider_trading/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.insider_trading import BeneficialOwnershipAcquisition, InsiderReportingName, InsiderTrade, InsiderTradeStatistics, InsiderTradingNamespace, InsiderTransactionType +from fmp._native.insider_trading import AsyncInsiderTradingNamespace, BeneficialOwnershipAcquisition, InsiderReportingName, InsiderTrade, InsiderTradeStatistics, InsiderTradingNamespace, InsiderTransactionType __all__ = [ + "AsyncInsiderTradingNamespace", "BeneficialOwnershipAcquisition", "InsiderReportingName", "InsiderTrade", diff --git a/crates/fmp-py/python/fmp/institutional_ownership/__init__.py b/crates/fmp-py/python/fmp/institutional_ownership/__init__.py index 1fdcbb7d..dc91ff7c 100644 --- a/crates/fmp-py/python/fmp/institutional_ownership/__init__.py +++ b/crates/fmp-py/python/fmp/institutional_ownership/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.institutional_ownership import Form13fFilingDate, HolderIndustryBreakdown, HolderPerformanceSummary, InstitutionalHolderAnalytics, InstitutionalHolding, InstitutionalIndustrySummary, InstitutionalOwnershipFiling, InstitutionalOwnershipNamespace, InstitutionalPositionSummary +from fmp._native.institutional_ownership import AsyncInstitutionalOwnershipNamespace, Form13fFilingDate, HolderIndustryBreakdown, HolderPerformanceSummary, InstitutionalHolderAnalytics, InstitutionalHolding, InstitutionalIndustrySummary, InstitutionalOwnershipFiling, InstitutionalOwnershipNamespace, InstitutionalPositionSummary __all__ = [ + "AsyncInstitutionalOwnershipNamespace", "Form13fFilingDate", "HolderIndustryBreakdown", "HolderPerformanceSummary", diff --git a/crates/fmp-py/python/fmp/market/__init__.py b/crates/fmp-py/python/fmp/market/__init__.py index 7b6f5f87..74eefe93 100644 --- a/crates/fmp-py/python/fmp/market/__init__.py +++ b/crates/fmp-py/python/fmp/market/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.market import IndustryPe, IndustryPerformance, MarketMover, MarketNamespace, SectorPe, SectorPerformance +from fmp._native.market import AsyncMarketNamespace, IndustryPe, IndustryPerformance, MarketMover, MarketNamespace, SectorPe, SectorPerformance __all__ = [ + "AsyncMarketNamespace", "IndustryPe", "IndustryPerformance", "MarketMover", diff --git a/crates/fmp-py/python/fmp/market_hours/__init__.py b/crates/fmp-py/python/fmp/market_hours/__init__.py index 355ba13e..d53d014b 100644 --- a/crates/fmp-py/python/fmp/market_hours/__init__.py +++ b/crates/fmp-py/python/fmp/market_hours/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.market_hours import ExchangeHoliday, ExchangeMarketHours, MarketHoursNamespace +from fmp._native.market_hours import AsyncMarketHoursNamespace, ExchangeHoliday, ExchangeMarketHours, MarketHoursNamespace __all__ = [ + "AsyncMarketHoursNamespace", "ExchangeHoliday", "ExchangeMarketHours", "MarketHoursNamespace", diff --git a/crates/fmp-py/python/fmp/news/__init__.py b/crates/fmp-py/python/fmp/news/__init__.py index e1691dc8..5515d7f2 100644 --- a/crates/fmp-py/python/fmp/news/__init__.py +++ b/crates/fmp-py/python/fmp/news/__init__.py @@ -1,9 +1,10 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.news import Article, NewsArticle, NewsNamespace +from fmp._native.news import Article, AsyncNewsNamespace, NewsArticle, NewsNamespace __all__ = [ "Article", + "AsyncNewsNamespace", "NewsArticle", "NewsNamespace", ] diff --git a/crates/fmp-py/python/fmp/quote/__init__.py b/crates/fmp-py/python/fmp/quote/__init__.py index 001197cd..6f67e2a2 100644 --- a/crates/fmp-py/python/fmp/quote/__init__.py +++ b/crates/fmp-py/python/fmp/quote/__init__.py @@ -1,10 +1,11 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.quote import AftermarketQuote, AftermarketTrade, Quote, QuoteNamespace, QuoteShort, StockPriceChange +from fmp._native.quote import AftermarketQuote, AftermarketTrade, AsyncQuoteNamespace, Quote, QuoteNamespace, QuoteShort, StockPriceChange __all__ = [ "AftermarketQuote", "AftermarketTrade", + "AsyncQuoteNamespace", "Quote", "QuoteNamespace", "QuoteShort", diff --git a/crates/fmp-py/python/fmp/screener/__init__.py b/crates/fmp-py/python/fmp/screener/__init__.py index 7edecaf7..4261f653 100644 --- a/crates/fmp-py/python/fmp/screener/__init__.py +++ b/crates/fmp-py/python/fmp/screener/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.screener import CompanyScreenerResult, ScreenerNamespace +from fmp._native.screener import AsyncScreenerNamespace, CompanyScreenerResult, ScreenerNamespace __all__ = [ + "AsyncScreenerNamespace", "CompanyScreenerResult", "ScreenerNamespace", ] diff --git a/crates/fmp-py/python/fmp/search/__init__.py b/crates/fmp-py/python/fmp/search/__init__.py index e604330e..4463d3bd 100644 --- a/crates/fmp-py/python/fmp/search/__init__.py +++ b/crates/fmp-py/python/fmp/search/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.search import CikSearchResult, CusipSearchResult, ExchangeVariant, IsinSearchResult, NameSearchResult, SearchNamespace, SymbolSearchResult +from fmp._native.search import AsyncSearchNamespace, CikSearchResult, CusipSearchResult, ExchangeVariant, IsinSearchResult, NameSearchResult, SearchNamespace, SymbolSearchResult __all__ = [ + "AsyncSearchNamespace", "CikSearchResult", "CusipSearchResult", "ExchangeVariant", diff --git a/crates/fmp-py/python/fmp/sec_filings/__init__.py b/crates/fmp-py/python/fmp/sec_filings/__init__.py index 55d118c8..2e2e59f6 100644 --- a/crates/fmp-py/python/fmp/sec_filings/__init__.py +++ b/crates/fmp-py/python/fmp/sec_filings/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.sec_filings import SecCompanyProfile, SecCompanySearchResult, SecFiling, SecFilingsNamespace, SicClassification +from fmp._native.sec_filings import AsyncSecFilingsNamespace, SecCompanyProfile, SecCompanySearchResult, SecFiling, SecFilingsNamespace, SicClassification __all__ = [ + "AsyncSecFilingsNamespace", "SecCompanyProfile", "SecCompanySearchResult", "SecFiling", diff --git a/crates/fmp-py/python/fmp/statements/__init__.py b/crates/fmp-py/python/fmp/statements/__init__.py index b0a75d47..e3bb3f69 100644 --- a/crates/fmp-py/python/fmp/statements/__init__.py +++ b/crates/fmp-py/python/fmp/statements/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.statements import StatementsNamespace, as_reported, balance, cash_flow, growth, income, metrics, ratios, reports, segmentation, summaries +from fmp._native.statements import AsyncStatementsNamespace, StatementsNamespace, as_reported, balance, cash_flow, growth, income, metrics, ratios, reports, segmentation, summaries __all__ = [ + "AsyncStatementsNamespace", "StatementsNamespace", "as_reported", "balance", diff --git a/crates/fmp-py/python/fmp/statements/as_reported/__init__.py b/crates/fmp-py/python/fmp/statements/as_reported/__init__.py index 092fd952..069a0f13 100644 --- a/crates/fmp-py/python/fmp/statements/as_reported/__init__.py +++ b/crates/fmp-py/python/fmp/statements/as_reported/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.statements.as_reported import AsReportedFinancialStatement, StatementsAsReportedNamespace +from fmp._native.statements.as_reported import AsReportedFinancialStatement, AsyncStatementsAsReportedNamespace, StatementsAsReportedNamespace __all__ = [ "AsReportedFinancialStatement", + "AsyncStatementsAsReportedNamespace", "StatementsAsReportedNamespace", ] diff --git a/crates/fmp-py/python/fmp/statements/balance/__init__.py b/crates/fmp-py/python/fmp/statements/balance/__init__.py index a48288c7..ef2b9e19 100644 --- a/crates/fmp-py/python/fmp/statements/balance/__init__.py +++ b/crates/fmp-py/python/fmp/statements/balance/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.statements.balance import BalanceSheetStatement, BalanceSheetStatementTtm, StatementsBalanceNamespace +from fmp._native.statements.balance import AsyncStatementsBalanceNamespace, BalanceSheetStatement, BalanceSheetStatementTtm, StatementsBalanceNamespace __all__ = [ + "AsyncStatementsBalanceNamespace", "BalanceSheetStatement", "BalanceSheetStatementTtm", "StatementsBalanceNamespace", diff --git a/crates/fmp-py/python/fmp/statements/cash_flow/__init__.py b/crates/fmp-py/python/fmp/statements/cash_flow/__init__.py index 3712199d..dd24452a 100644 --- a/crates/fmp-py/python/fmp/statements/cash_flow/__init__.py +++ b/crates/fmp-py/python/fmp/statements/cash_flow/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.statements.cash_flow import CashFlowStatement, StatementsCashFlowNamespace +from fmp._native.statements.cash_flow import AsyncStatementsCashFlowNamespace, CashFlowStatement, StatementsCashFlowNamespace __all__ = [ + "AsyncStatementsCashFlowNamespace", "CashFlowStatement", "StatementsCashFlowNamespace", ] diff --git a/crates/fmp-py/python/fmp/statements/growth/__init__.py b/crates/fmp-py/python/fmp/statements/growth/__init__.py index c7e3ec4d..bf68db8d 100644 --- a/crates/fmp-py/python/fmp/statements/growth/__init__.py +++ b/crates/fmp-py/python/fmp/statements/growth/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.statements.growth import StatementsGrowthNamespace, balance, cash_flow, combined, income +from fmp._native.statements.growth import AsyncStatementsGrowthNamespace, StatementsGrowthNamespace, balance, cash_flow, combined, income __all__ = [ + "AsyncStatementsGrowthNamespace", "StatementsGrowthNamespace", "balance", "cash_flow", diff --git a/crates/fmp-py/python/fmp/statements/income/__init__.py b/crates/fmp-py/python/fmp/statements/income/__init__.py index ad185e42..48ce62fd 100644 --- a/crates/fmp-py/python/fmp/statements/income/__init__.py +++ b/crates/fmp-py/python/fmp/statements/income/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.statements.income import IncomeStatement, StatementsIncomeNamespace +from fmp._native.statements.income import AsyncStatementsIncomeNamespace, IncomeStatement, StatementsIncomeNamespace __all__ = [ + "AsyncStatementsIncomeNamespace", "IncomeStatement", "StatementsIncomeNamespace", ] diff --git a/crates/fmp-py/python/fmp/statements/metrics/__init__.py b/crates/fmp-py/python/fmp/statements/metrics/__init__.py index 0981bb40..10228cc2 100644 --- a/crates/fmp-py/python/fmp/statements/metrics/__init__.py +++ b/crates/fmp-py/python/fmp/statements/metrics/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.statements.metrics import KeyMetrics, KeyMetricsTtm, StatementsMetricsNamespace +from fmp._native.statements.metrics import AsyncStatementsMetricsNamespace, KeyMetrics, KeyMetricsTtm, StatementsMetricsNamespace __all__ = [ + "AsyncStatementsMetricsNamespace", "KeyMetrics", "KeyMetricsTtm", "StatementsMetricsNamespace", diff --git a/crates/fmp-py/python/fmp/statements/ratios/__init__.py b/crates/fmp-py/python/fmp/statements/ratios/__init__.py index 6d9e06dc..7d8bdb6c 100644 --- a/crates/fmp-py/python/fmp/statements/ratios/__init__.py +++ b/crates/fmp-py/python/fmp/statements/ratios/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.statements.ratios import FinancialRatios, FinancialRatiosTtm, StatementsRatiosNamespace +from fmp._native.statements.ratios import AsyncStatementsRatiosNamespace, FinancialRatios, FinancialRatiosTtm, StatementsRatiosNamespace __all__ = [ + "AsyncStatementsRatiosNamespace", "FinancialRatios", "FinancialRatiosTtm", "StatementsRatiosNamespace", diff --git a/crates/fmp-py/python/fmp/statements/reports/__init__.py b/crates/fmp-py/python/fmp/statements/reports/__init__.py index defa2fa4..1a2b180c 100644 --- a/crates/fmp-py/python/fmp/statements/reports/__init__.py +++ b/crates/fmp-py/python/fmp/statements/reports/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.statements.reports import FinancialReportDate, FinancialReportJson, StatementsReportsNamespace +from fmp._native.statements.reports import AsyncStatementsReportsNamespace, FinancialReportDate, FinancialReportJson, StatementsReportsNamespace __all__ = [ + "AsyncStatementsReportsNamespace", "FinancialReportDate", "FinancialReportJson", "StatementsReportsNamespace", diff --git a/crates/fmp-py/python/fmp/statements/segmentation/__init__.py b/crates/fmp-py/python/fmp/statements/segmentation/__init__.py index 60db31a6..5fd0f498 100644 --- a/crates/fmp-py/python/fmp/statements/segmentation/__init__.py +++ b/crates/fmp-py/python/fmp/statements/segmentation/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.statements.segmentation import RevenueSegmentation, StatementsSegmentationNamespace +from fmp._native.statements.segmentation import AsyncStatementsSegmentationNamespace, RevenueSegmentation, StatementsSegmentationNamespace __all__ = [ + "AsyncStatementsSegmentationNamespace", "RevenueSegmentation", "StatementsSegmentationNamespace", ] diff --git a/crates/fmp-py/python/fmp/statements/summaries/__init__.py b/crates/fmp-py/python/fmp/statements/summaries/__init__.py index ef09d9c7..fefc7578 100644 --- a/crates/fmp-py/python/fmp/statements/summaries/__init__.py +++ b/crates/fmp-py/python/fmp/statements/summaries/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.statements.summaries import EnterpriseValue, FinancialScore, LatestFinancialStatement, OwnerEarnings, StatementsSummariesNamespace +from fmp._native.statements.summaries import AsyncStatementsSummariesNamespace, EnterpriseValue, FinancialScore, LatestFinancialStatement, OwnerEarnings, StatementsSummariesNamespace __all__ = [ + "AsyncStatementsSummariesNamespace", "EnterpriseValue", "FinancialScore", "LatestFinancialStatement", diff --git a/crates/fmp-py/python/fmp/technical_indicators/__init__.py b/crates/fmp-py/python/fmp/technical_indicators/__init__.py index 021500eb..36a55b4a 100644 --- a/crates/fmp-py/python/fmp/technical_indicators/__init__.py +++ b/crates/fmp-py/python/fmp/technical_indicators/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.technical_indicators import AverageDirectionalIndexBar, DoubleExponentialMovingAverageBar, ExponentialMovingAverageBar, RelativeStrengthIndexBar, SimpleMovingAverageBar, StandardDeviationBar, TechnicalIndicatorsNamespace, TripleExponentialMovingAverageBar, WeightedMovingAverageBar, WilliamsBar +from fmp._native.technical_indicators import AsyncTechnicalIndicatorsNamespace, AverageDirectionalIndexBar, DoubleExponentialMovingAverageBar, ExponentialMovingAverageBar, RelativeStrengthIndexBar, SimpleMovingAverageBar, StandardDeviationBar, TechnicalIndicatorsNamespace, TripleExponentialMovingAverageBar, WeightedMovingAverageBar, WilliamsBar __all__ = [ + "AsyncTechnicalIndicatorsNamespace", "AverageDirectionalIndexBar", "DoubleExponentialMovingAverageBar", "ExponentialMovingAverageBar", diff --git a/crates/fmp-py/python/fmp/tipranks/__init__.py b/crates/fmp-py/python/fmp/tipranks/__init__.py index 65d1499c..59cebbcd 100644 --- a/crates/fmp-py/python/fmp/tipranks/__init__.py +++ b/crates/fmp-py/python/fmp/tipranks/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.tipranks import TipRanksAnalystActionCounts, TipRanksAnalystProfile, TipRanksAnalystSummary, TipRanksFirmSummary, TipRanksPointInTimeRating, TipRanksRatingSearchResult, TipRanksRecommendationCounts, TipRanksSymbolSummary, TipranksNamespace +from fmp._native.tipranks import AsyncTipranksNamespace, TipRanksAnalystActionCounts, TipRanksAnalystProfile, TipRanksAnalystSummary, TipRanksFirmSummary, TipRanksPointInTimeRating, TipRanksRatingSearchResult, TipRanksRecommendationCounts, TipRanksSymbolSummary, TipranksNamespace __all__ = [ + "AsyncTipranksNamespace", "TipRanksAnalystActionCounts", "TipRanksAnalystProfile", "TipRanksAnalystSummary", diff --git a/crates/fmp-py/python/fmp/transcripts/__init__.py b/crates/fmp-py/python/fmp/transcripts/__init__.py index 992f24f1..e2faa4c6 100644 --- a/crates/fmp-py/python/fmp/transcripts/__init__.py +++ b/crates/fmp-py/python/fmp/transcripts/__init__.py @@ -1,8 +1,9 @@ # This file is automatically generated by pyo3_stub_gen # ruff: noqa: F401 -from fmp._native.transcripts import EarningsTranscript, EarningsTranscriptDate, LatestEarningsTranscript, TranscriptsNamespace +from fmp._native.transcripts import AsyncTranscriptsNamespace, EarningsTranscript, EarningsTranscriptDate, LatestEarningsTranscript, TranscriptsNamespace __all__ = [ + "AsyncTranscriptsNamespace", "EarningsTranscript", "EarningsTranscriptDate", "LatestEarningsTranscript", diff --git a/crates/fmp-py/src/client.rs b/crates/fmp-py/src/client.rs index 095e13d8..f0de1870 100644 --- a/crates/fmp-py/src/client.rs +++ b/crates/fmp-py/src/client.rs @@ -167,6 +167,74 @@ fn positive_duration(value: f64, field: &'static str) -> PyResult { Duration::try_from_secs_f64(value).map_err(|_| invalid_configuration(message)) } +/// Validates the constructor keywords shared by `FmpClient` and +/// `AsyncFmpClient` and builds the handle both hand to their namespaces. +#[allow(clippy::too_many_arguments)] +fn build_handle( + token: Option, + base_url: Option, + path_prefix: Option, + auth_mode: Option<&str>, + auth_name: Option, + auth_prefix: Option, + headers: Option<&Bound<'_, PyMapping>>, + timeout: Option, + connect_timeout: Option, + max_response_body_bytes: Option>, + danger_allow_insecure_authentication: bool, + follow_redirects: Option, + on_response: Option>, +) -> PyResult> { + let auth = authentication(auth_mode, token, auth_name, auth_prefix)?; + let mut builder = ClientBuilder::default().authentication(auth); + + if let Some(base_url) = resolve_base_url(base_url) { + builder = builder.base_url(base_url); + } + if let Some(path_prefix) = path_prefix { + builder = builder.path_prefix(path_prefix); + } + if let Some(headers) = headers { + for item in headers.items()?.iter() { + let (name, value): (Bound<'_, PyAny>, Bound<'_, PyAny>) = item.extract()?; + builder = builder.default_header(header_text(&name)?, header_text(&value)?); + } + } + if let Some(timeout) = timeout { + builder = builder.timeout(positive_duration(timeout, "timeout")?); + } + if let Some(connect_timeout) = connect_timeout { + builder = builder.connect_timeout(positive_duration(connect_timeout, "connect_timeout")?); + } + if let Some(max_response_body_bytes) = max_response_body_bytes { + builder = builder.max_response_body_bytes(body_limit(&max_response_body_bytes)?); + } + builder = builder.danger_allow_insecure_authentication(danger_allow_insecure_authentication); + if let Some(follow_redirects) = follow_redirects { + builder = builder.redirect_policy(if follow_redirects { + RedirectPolicy::SameOrigin + } else { + RedirectPolicy::None + }); + } + + let on_response = on_response.filter(|callback| !callback.is_none()); + if let Some(callback) = &on_response { + if !callback.is_callable() { + return Err(invalid_configuration( + "on_response must be callable or None", + )); + } + builder = builder.on_response(response_hook::record); + } + + builder + .clone() + .build() + .map_err(|error| build_error(auth_mode, error))?; + Ok(ClientHandle::new(builder, on_response.map(Bound::unbind))) +} + /// A synchronous FMP client with proxy-ready transport configuration. /// /// When `token` is omitted, the `FMP_API_KEY` environment variable is read @@ -242,58 +310,22 @@ impl FmpClient { #[gen_stub(override_type(type_repr = "typing.Optional[typing.Callable[[builtins.str, builtins.int, builtins.dict[builtins.str, builtins.str]], None]]", imports = ("builtins", "typing")))] on_response: Option>, ) -> PyResult { - let auth = authentication(auth_mode, token, auth_name, auth_prefix)?; - let mut builder = ClientBuilder::default().authentication(auth); - - if let Some(base_url) = resolve_base_url(base_url) { - builder = builder.base_url(base_url); - } - if let Some(path_prefix) = path_prefix { - builder = builder.path_prefix(path_prefix); - } - if let Some(headers) = headers { - for item in headers.items()?.iter() { - let (name, value): (Bound<'_, PyAny>, Bound<'_, PyAny>) = item.extract()?; - builder = builder.default_header(header_text(&name)?, header_text(&value)?); - } - } - if let Some(timeout) = timeout { - builder = builder.timeout(positive_duration(timeout, "timeout")?); - } - if let Some(connect_timeout) = connect_timeout { - builder = - builder.connect_timeout(positive_duration(connect_timeout, "connect_timeout")?); - } - if let Some(max_response_body_bytes) = max_response_body_bytes { - builder = builder.max_response_body_bytes(body_limit(&max_response_body_bytes)?); - } - builder = - builder.danger_allow_insecure_authentication(danger_allow_insecure_authentication); - if let Some(follow_redirects) = follow_redirects { - builder = builder.redirect_policy(if follow_redirects { - RedirectPolicy::SameOrigin - } else { - RedirectPolicy::None - }); - } - - let on_response = on_response.filter(|callback| !callback.is_none()); - if let Some(callback) = &on_response { - if !callback.is_callable() { - return Err(invalid_configuration( - "on_response must be callable or None", - )); - } - builder = builder.on_response(response_hook::record); - } - - builder - .clone() - .build() - .map_err(|error| build_error(auth_mode, error))?; - Ok(Self { - builder: ClientHandle::new(builder, on_response.map(Bound::unbind)), - }) + let builder = build_handle( + token, + base_url, + path_prefix, + auth_mode, + auth_name, + auth_prefix, + headers, + timeout, + connect_timeout, + max_response_body_bytes, + danger_allow_insecure_authentication, + follow_redirects, + on_response, + )?; + Ok(Self { builder }) } /// Releases the pooled HTTP connections and rejects every later call with @@ -322,3 +354,104 @@ impl FmpClient { self.close() } } + +/// An asyncio FMP client: the same namespaces and methods as `FmpClient`, but +/// every endpoint method is a coroutine to `await`. +/// +/// The constructor takes the same keywords, environment fallbacks +/// (`FMP_API_KEY`, `FMP_BASE_URL`), and defaults as `FmpClient`, and raises +/// the same exceptions. Requests run on the process-wide runtime shared with +/// `FmpClient`, so awaiting one never blocks the event loop and concurrent +/// coroutines (`asyncio.gather`) run their requests in parallel. Cancelling +/// the awaiting task aborts its request. Coroutines need a running asyncio +/// loop (anyio on its asyncio backend works); trio is not supported. +/// +/// `on_response` behaves as on `FmpClient`: a plain, synchronous callable run +/// on the event loop thread after the response decoded and before the +/// coroutine returns; if it raises, the coroutine raises that exception. +/// +/// `close()` and `await aclose()` release the pooled connections; afterwards +/// every call, including through a namespace fetched before the close, raises +/// `FmpConfigError`. Calls already in flight finish normally. Closing twice +/// is a no-op, and `async with AsyncFmpClient(...) as client:` closes the +/// client when the block exits. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native", frozen)] +pub(crate) struct AsyncFmpClient { + pub(crate) builder: Arc, +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncFmpClient { + #[new] + #[pyo3(signature = (*, token=None, base_url=None, path_prefix=None, auth_mode=None, auth_name=None, auth_prefix=None, headers=None, timeout=None, connect_timeout=None, max_response_body_bytes=None, danger_allow_insecure_authentication=false, follow_redirects=None, on_response=None))] + #[allow(clippy::too_many_arguments)] + fn new( + token: Option, + base_url: Option, + path_prefix: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"none\", \"fmp_header\", \"fmp_query\", \"bearer\", \"custom_header\", \"custom_query\"]]", imports = ("typing",)))] + auth_mode: Option<&str>, + auth_name: Option, + auth_prefix: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Mapping[builtins.str, builtins.str]]", imports = ("builtins", "typing")))] + headers: Option<&Bound<'_, PyMapping>>, + timeout: Option, + connect_timeout: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[builtins.int]", imports = ("builtins", "typing")))] + max_response_body_bytes: Option>, + danger_allow_insecure_authentication: bool, + follow_redirects: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Callable[[builtins.str, builtins.int, builtins.dict[builtins.str, builtins.str]], None]]", imports = ("builtins", "typing")))] + on_response: Option>, + ) -> PyResult { + let builder = build_handle( + token, + base_url, + path_prefix, + auth_mode, + auth_name, + auth_prefix, + headers, + timeout, + connect_timeout, + max_response_body_bytes, + danger_allow_insecure_authentication, + follow_redirects, + on_response, + )?; + Ok(Self { builder }) + } + + /// Releases the pooled HTTP connections and rejects every later call with + /// `FmpConfigError`. Calls already in flight finish normally. Idempotent. + fn close(&self) -> PyResult<()> { + runtime::close(&self.builder) + } + + /// The awaitable form of `close()`, for `async` cleanup code. + async fn aclose(&self) -> PyResult<()> { + runtime::close(&self.builder) + } + + /// Returns the client itself for use in an `async with` block. + async fn __aenter__(slf: Py) -> Py { + slf + } + + /// Closes the client and lets any exception from the block propagate. + #[pyo3(signature = (exc_type, exc_value, traceback))] + async fn __aexit__( + &self, + #[gen_stub(override_type(type_repr = "typing.Optional[type[builtins.BaseException]]", imports = ("builtins", "typing")))] + exc_type: Option>, + #[gen_stub(override_type(type_repr = "typing.Optional[builtins.BaseException]", imports = ("builtins", "typing")))] + exc_value: Option>, + #[gen_stub(override_type(type_repr = "typing.Optional[types.TracebackType]", imports = ("types", "typing")))] + traceback: Option>, + ) -> PyResult<()> { + let _ = (exc_type, exc_value, traceback); + runtime::close(&self.builder) + } +} diff --git a/crates/fmp-py/src/client_namespaces.rs b/crates/fmp-py/src/client_namespaces.rs index a6f39b82..b52136ca 100644 --- a/crates/fmp-py/src/client_namespaces.rs +++ b/crates/fmp-py/src/client_namespaces.rs @@ -3,37 +3,43 @@ use pyo3::prelude::*; use pyo3_stub_gen::derive::gen_stub_pymethods; -use crate::client::FmpClient; -use crate::namespaces::analyst::AnalystNamespace; -use crate::namespaces::bulk::BulkNamespace; -use crate::namespaces::calendar::CalendarNamespace; -use crate::namespaces::chart::ChartNamespace; -use crate::namespaces::commitment_of_traders::CommitmentOfTradersNamespace; -use crate::namespaces::commodities::CommoditiesNamespace; -use crate::namespaces::company::CompanyNamespace; -use crate::namespaces::congressional::CongressionalNamespace; -use crate::namespaces::crypto::CryptoNamespace; -use crate::namespaces::dcf::DcfNamespace; -use crate::namespaces::directory::DirectoryNamespace; -use crate::namespaces::economics::EconomicsNamespace; -use crate::namespaces::esg::EsgNamespace; -use crate::namespaces::forex::ForexNamespace; -use crate::namespaces::fundraising::FundraisingNamespace; -use crate::namespaces::funds::FundsNamespace; -use crate::namespaces::indexes::IndexesNamespace; -use crate::namespaces::insider_trading::InsiderTradingNamespace; -use crate::namespaces::institutional_ownership::InstitutionalOwnershipNamespace; -use crate::namespaces::market::MarketNamespace; -use crate::namespaces::market_hours::MarketHoursNamespace; -use crate::namespaces::news::NewsNamespace; -use crate::namespaces::quote::QuoteNamespace; -use crate::namespaces::screener::ScreenerNamespace; -use crate::namespaces::search::SearchNamespace; -use crate::namespaces::sec_filings::SecFilingsNamespace; -use crate::namespaces::statements::StatementsNamespace; -use crate::namespaces::technical_indicators::TechnicalIndicatorsNamespace; -use crate::namespaces::tipranks::TipranksNamespace; -use crate::namespaces::transcripts::TranscriptsNamespace; +use crate::client::{AsyncFmpClient, FmpClient}; +use crate::namespaces::analyst::{AnalystNamespace, AsyncAnalystNamespace}; +use crate::namespaces::bulk::{AsyncBulkNamespace, BulkNamespace}; +use crate::namespaces::calendar::{AsyncCalendarNamespace, CalendarNamespace}; +use crate::namespaces::chart::{AsyncChartNamespace, ChartNamespace}; +use crate::namespaces::commitment_of_traders::{ + AsyncCommitmentOfTradersNamespace, CommitmentOfTradersNamespace, +}; +use crate::namespaces::commodities::{AsyncCommoditiesNamespace, CommoditiesNamespace}; +use crate::namespaces::company::{AsyncCompanyNamespace, CompanyNamespace}; +use crate::namespaces::congressional::{AsyncCongressionalNamespace, CongressionalNamespace}; +use crate::namespaces::crypto::{AsyncCryptoNamespace, CryptoNamespace}; +use crate::namespaces::dcf::{AsyncDcfNamespace, DcfNamespace}; +use crate::namespaces::directory::{AsyncDirectoryNamespace, DirectoryNamespace}; +use crate::namespaces::economics::{AsyncEconomicsNamespace, EconomicsNamespace}; +use crate::namespaces::esg::{AsyncEsgNamespace, EsgNamespace}; +use crate::namespaces::forex::{AsyncForexNamespace, ForexNamespace}; +use crate::namespaces::fundraising::{AsyncFundraisingNamespace, FundraisingNamespace}; +use crate::namespaces::funds::{AsyncFundsNamespace, FundsNamespace}; +use crate::namespaces::indexes::{AsyncIndexesNamespace, IndexesNamespace}; +use crate::namespaces::insider_trading::{AsyncInsiderTradingNamespace, InsiderTradingNamespace}; +use crate::namespaces::institutional_ownership::{ + AsyncInstitutionalOwnershipNamespace, InstitutionalOwnershipNamespace, +}; +use crate::namespaces::market::{AsyncMarketNamespace, MarketNamespace}; +use crate::namespaces::market_hours::{AsyncMarketHoursNamespace, MarketHoursNamespace}; +use crate::namespaces::news::{AsyncNewsNamespace, NewsNamespace}; +use crate::namespaces::quote::{AsyncQuoteNamespace, QuoteNamespace}; +use crate::namespaces::screener::{AsyncScreenerNamespace, ScreenerNamespace}; +use crate::namespaces::search::{AsyncSearchNamespace, SearchNamespace}; +use crate::namespaces::sec_filings::{AsyncSecFilingsNamespace, SecFilingsNamespace}; +use crate::namespaces::statements::{AsyncStatementsNamespace, StatementsNamespace}; +use crate::namespaces::technical_indicators::{ + AsyncTechnicalIndicatorsNamespace, TechnicalIndicatorsNamespace, +}; +use crate::namespaces::tipranks::{AsyncTipranksNamespace, TipranksNamespace}; +use crate::namespaces::transcripts::{AsyncTranscriptsNamespace, TranscriptsNamespace}; #[gen_stub_pymethods] #[pymethods] @@ -218,3 +224,187 @@ impl FmpClient { TranscriptsNamespace::new(self.builder.clone()) } } + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncFmpClient { + /// The `analyst` endpoints, reached as `client.analyst`. + #[getter] + fn analyst(&self) -> AsyncAnalystNamespace { + AsyncAnalystNamespace::new(self.builder.clone()) + } + + /// The `bulk` endpoints, reached as `client.bulk`. + #[getter] + fn bulk(&self) -> AsyncBulkNamespace { + AsyncBulkNamespace::new(self.builder.clone()) + } + + /// The `calendar` endpoints, reached as `client.calendar`. + #[getter] + fn calendar(&self) -> AsyncCalendarNamespace { + AsyncCalendarNamespace::new(self.builder.clone()) + } + + /// The `chart` endpoints, reached as `client.chart`. + #[getter] + fn chart(&self) -> AsyncChartNamespace { + AsyncChartNamespace::new(self.builder.clone()) + } + + /// The `commitment_of_traders` endpoints, reached as `client.commitment_of_traders`. + #[getter] + fn commitment_of_traders(&self) -> AsyncCommitmentOfTradersNamespace { + AsyncCommitmentOfTradersNamespace::new(self.builder.clone()) + } + + /// The `commodities` endpoints, reached as `client.commodities`. + #[getter] + fn commodities(&self) -> AsyncCommoditiesNamespace { + AsyncCommoditiesNamespace::new(self.builder.clone()) + } + + /// The `company` endpoints, reached as `client.company`. + #[getter] + fn company(&self) -> AsyncCompanyNamespace { + AsyncCompanyNamespace::new(self.builder.clone()) + } + + /// The `congressional` endpoints, reached as `client.congressional`. + #[getter] + fn congressional(&self) -> AsyncCongressionalNamespace { + AsyncCongressionalNamespace::new(self.builder.clone()) + } + + /// The `crypto` endpoints, reached as `client.crypto`. + #[getter] + fn crypto(&self) -> AsyncCryptoNamespace { + AsyncCryptoNamespace::new(self.builder.clone()) + } + + /// The `dcf` endpoints, reached as `client.dcf`. + #[getter] + fn dcf(&self) -> AsyncDcfNamespace { + AsyncDcfNamespace::new(self.builder.clone()) + } + + /// The `directory` endpoints, reached as `client.directory`. + #[getter] + fn directory(&self) -> AsyncDirectoryNamespace { + AsyncDirectoryNamespace::new(self.builder.clone()) + } + + /// The `economics` endpoints, reached as `client.economics`. + #[getter] + fn economics(&self) -> AsyncEconomicsNamespace { + AsyncEconomicsNamespace::new(self.builder.clone()) + } + + /// The `esg` endpoints, reached as `client.esg`. + #[getter] + fn esg(&self) -> AsyncEsgNamespace { + AsyncEsgNamespace::new(self.builder.clone()) + } + + /// The `forex` endpoints, reached as `client.forex`. + #[getter] + fn forex(&self) -> AsyncForexNamespace { + AsyncForexNamespace::new(self.builder.clone()) + } + + /// The `fundraising` endpoints, reached as `client.fundraising`. + #[getter] + fn fundraising(&self) -> AsyncFundraisingNamespace { + AsyncFundraisingNamespace::new(self.builder.clone()) + } + + /// The `funds` endpoints, reached as `client.funds`. + #[getter] + fn funds(&self) -> AsyncFundsNamespace { + AsyncFundsNamespace::new(self.builder.clone()) + } + + /// The `indexes` endpoints, reached as `client.indexes`. + #[getter] + fn indexes(&self) -> AsyncIndexesNamespace { + AsyncIndexesNamespace::new(self.builder.clone()) + } + + /// The `insider_trading` endpoints, reached as `client.insider_trading`. + #[getter] + fn insider_trading(&self) -> AsyncInsiderTradingNamespace { + AsyncInsiderTradingNamespace::new(self.builder.clone()) + } + + /// The `institutional_ownership` endpoints, reached as `client.institutional_ownership`. + #[getter] + fn institutional_ownership(&self) -> AsyncInstitutionalOwnershipNamespace { + AsyncInstitutionalOwnershipNamespace::new(self.builder.clone()) + } + + /// The `market` endpoints, reached as `client.market`. + #[getter] + fn market(&self) -> AsyncMarketNamespace { + AsyncMarketNamespace::new(self.builder.clone()) + } + + /// The `market_hours` endpoints, reached as `client.market_hours`. + #[getter] + fn market_hours(&self) -> AsyncMarketHoursNamespace { + AsyncMarketHoursNamespace::new(self.builder.clone()) + } + + /// The `news` endpoints, reached as `client.news`. + #[getter] + fn news(&self) -> AsyncNewsNamespace { + AsyncNewsNamespace::new(self.builder.clone()) + } + + /// The `quote` endpoints, reached as `client.quote`. + #[getter] + fn quote(&self) -> AsyncQuoteNamespace { + AsyncQuoteNamespace::new(self.builder.clone()) + } + + /// The `screener` endpoints, reached as `client.screener`. + #[getter] + fn screener(&self) -> AsyncScreenerNamespace { + AsyncScreenerNamespace::new(self.builder.clone()) + } + + /// The `search` endpoints, reached as `client.search`. + #[getter] + fn search(&self) -> AsyncSearchNamespace { + AsyncSearchNamespace::new(self.builder.clone()) + } + + /// The `sec_filings` endpoints, reached as `client.sec_filings`. + #[getter] + fn sec_filings(&self) -> AsyncSecFilingsNamespace { + AsyncSecFilingsNamespace::new(self.builder.clone()) + } + + /// The `statements` endpoints, reached as `client.statements`. + #[getter] + fn statements(&self) -> AsyncStatementsNamespace { + AsyncStatementsNamespace::new(self.builder.clone()) + } + + /// The `technical_indicators` endpoints, reached as `client.technical_indicators`. + #[getter] + fn technical_indicators(&self) -> AsyncTechnicalIndicatorsNamespace { + AsyncTechnicalIndicatorsNamespace::new(self.builder.clone()) + } + + /// The `tipranks` endpoints, reached as `client.tipranks`. + #[getter] + fn tipranks(&self) -> AsyncTipranksNamespace { + AsyncTipranksNamespace::new(self.builder.clone()) + } + + /// The `transcripts` endpoints, reached as `client.transcripts`. + #[getter] + fn transcripts(&self) -> AsyncTranscriptsNamespace { + AsyncTranscriptsNamespace::new(self.builder.clone()) + } +} diff --git a/crates/fmp-py/src/facade.rs b/crates/fmp-py/src/facade.rs index b209fe72..28670431 100644 --- a/crates/fmp-py/src/facade.rs +++ b/crates/fmp-py/src/facade.rs @@ -16,14 +16,14 @@ pyo3_stub_gen::module_variable!("fmp._native", "__version__", String); pyo3_stub_gen::reexport_module_members!( "fmp" from "fmp._native"; - "FmpClient", "BinaryPayload", "__version__", "reveal_secret_urls", "set_reveal_secret_urls" + "FmpClient", "AsyncFmpClient", "BinaryPayload", "__version__", "reveal_secret_urls", "set_reveal_secret_urls" ); pyo3_stub_gen::reexport_module_members!( "fmp" from "fmp._native.errors"; "FmpError", "FmpValidationError", "FmpConfigError", "FmpTransportError", "FmpStatusError", "FmpDecodeError" ); -pyo3_stub_gen::reexport_module_members!("fmp.client" from "fmp._native"; "FmpClient"); +pyo3_stub_gen::reexport_module_members!("fmp.client" from "fmp._native"; "FmpClient", "AsyncFmpClient"); pyo3_stub_gen::reexport_module_members!( "fmp.errors" from "fmp._native.errors"; "FmpError", "FmpValidationError", "FmpConfigError", "FmpTransportError", "FmpStatusError", diff --git a/crates/fmp-py/src/lib.rs b/crates/fmp-py/src/lib.rs index 2338a1b0..290cb721 100644 --- a/crates/fmp-py/src/lib.rs +++ b/crates/fmp-py/src/lib.rs @@ -25,6 +25,7 @@ mod _native { fn init(module: &Bound<'_, PyModule>) -> PyResult<()> { module.setattr("__version__", libfmp::VERSION)?; module.add_class::()?; + module.add_class::()?; module.add_class::()?; super::errors::register(module)?; super::secrets::register(module)?; diff --git a/crates/fmp-py/src/namespaces/analyst.rs b/crates/fmp-py/src/namespaces/analyst.rs index 0a981c76..492c2abf 100644 --- a/crates/fmp-py/src/namespaces/analyst.rs +++ b/crates/fmp-py/src/namespaces/analyst.rs @@ -16,7 +16,7 @@ use crate::models::analyst::{ FinancialEstimate, HistoricalRating, HistoricalStockGrade, PriceTargetConsensus, PriceTargetSummary, RatingSnapshot, StockGrade, StockGradesSummary, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Analyst endpoints for a single client, exposed as `client.analyst`. #[gen_stub_pyclass] @@ -178,6 +178,134 @@ impl AnalystNamespace { } } +/// Analyst endpoints for a single async client, exposed as `client.analyst`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.analyst", frozen)] +pub(crate) struct AsyncAnalystNamespace { + builder: Arc, +} + +impl AsyncAnalystNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncAnalystNamespace { + /// Retrieves worldwide analyst financial estimates for one ticker. + #[pyo3(signature = (symbol, period, *, page=None, limit=None))] + async fn financial_estimates( + &self, + symbol: String, + #[gen_stub(override_type(type_repr = "typing.Literal[\"annual\", \"quarter\"]", imports = ("typing",)))] + period: String, + page: Option, + limit: Option, + ) -> PyResult> { + let query = financial_estimates_query(&symbol, &period, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.financial_estimates(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(FinancialEstimate::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the worldwide financial-rating snapshot for one ticker. + #[pyo3(signature = (symbol))] + async fn ratings_snapshot(&self, symbol: String) -> PyResult> { + let query = ratings_snapshot_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.ratings_snapshot(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(RatingSnapshot::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide historical financial ratings for one ticker. + #[pyo3(signature = (symbol, *, limit=None))] + async fn historical_ratings( + &self, + symbol: String, + limit: Option, + ) -> PyResult> { + let query = historical_ratings_query(&symbol, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.historical_ratings(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(HistoricalRating::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the US-only price-target summary for one ticker. + #[pyo3(signature = (symbol))] + async fn price_target_summary(&self, symbol: String) -> PyResult> { + let query = price_target_summary_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.price_target_summary(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(PriceTargetSummary::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the US-only price-target consensus for one ticker. + #[pyo3(signature = (symbol))] + async fn price_target_consensus(&self, symbol: String) -> PyResult> { + let query = price_target_consensus_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.price_target_consensus(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(PriceTargetConsensus::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves current worldwide stock-grade actions for one ticker. + #[pyo3(signature = (symbol))] + async fn stock_grades(&self, symbol: String) -> PyResult> { + let query = stock_grades_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_grades(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockGrade::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide historical stock-grade counts for one ticker. + #[pyo3(signature = (symbol, *, limit=None))] + async fn historical_stock_grades( + &self, + symbol: String, + limit: Option, + ) -> PyResult> { + let query = historical_stock_grades_query(&symbol, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.historical_stock_grades(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(HistoricalStockGrade::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the worldwide stock-grades summary for one ticker. + #[pyo3(signature = (symbol))] + async fn stock_grades_summary(&self, symbol: String) -> PyResult> { + let query = stock_grades_summary_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_grades_summary(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockGradesSummary::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `FinancialEstimatesQuery` for `AnalystNamespace::financial_estimates` from validated Python arguments. fn financial_estimates_query( symbol: &str, diff --git a/crates/fmp-py/src/namespaces/bulk.rs b/crates/fmp-py/src/namespaces/bulk.rs index 6ceadff5..106787ac 100644 --- a/crates/fmp-py/src/namespaces/bulk.rs +++ b/crates/fmp-py/src/namespaces/bulk.rs @@ -20,7 +20,7 @@ use crate::models::bulk::snapshots::{ BulkUpgradesDowngradesConsensus, }; use crate::models::company::CompanyProfile; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Bulk endpoints for a single client, exposed as `client.bulk`. #[gen_stub_pyclass] @@ -351,6 +351,290 @@ impl BulkNamespace { } } +/// Bulk endpoints for a single async client, exposed as `client.bulk`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.bulk", frozen)] +pub(crate) struct AsyncBulkNamespace { + builder: Arc, +} + +impl AsyncBulkNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncBulkNamespace { + /// Retrieves one provider partition of worldwide company profiles. + #[pyo3(signature = (part))] + async fn company_profiles(&self, part: String) -> PyResult> { + let query = company_profiles_query(&part)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_company_profiles(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CompanyProfile::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide stock ratings in one provider bulk response. + async fn stock_ratings(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_stock_ratings().await + }) + .await?; + rows.map(|items| items.into_iter().map(BulkStockRating::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide discounted-cash-flow valuations in one provider bulk response. + async fn dcf_valuations(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_dcf_valuations().await + }) + .await?; + rows.map(|items| items.into_iter().map(BulkDcfValuation::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide financial scores in one provider bulk response. + async fn financial_scores(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_financial_scores().await + }) + .await?; + rows.map(|items| items.into_iter().map(BulkFinancialScore::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves US price-target summaries in one provider bulk response. + async fn price_target_summaries(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_price_target_summaries().await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(BulkPriceTargetSummary::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves one provider partition of worldwide ETF holdings. + #[pyo3(signature = (part))] + async fn etf_holdings(&self, part: String) -> PyResult> { + let query = etf_holdings_query(&part)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_etf_holdings(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(BulkEtfHolding::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide upgrades/downgrades consensus in one provider bulk response. + async fn upgrades_downgrades_consensus( + &self, + ) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_upgrades_downgrades_consensus().await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(BulkUpgradesDowngradesConsensus::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves worldwide trailing-twelve-month key metrics in one bulk response. + async fn key_metrics_ttm(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_key_metrics_ttm().await + }) + .await?; + rows.map(|items| items.into_iter().map(BulkKeyMetricsTtm::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide trailing-twelve-month financial ratios in one bulk response. + async fn financial_ratios_ttm(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_financial_ratios_ttm().await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(BulkFinancialRatiosTtm::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves worldwide stock peers in one provider bulk response. + async fn stock_peers(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_stock_peers().await + }) + .await?; + rows.map(|items| items.into_iter().map(BulkStockPeer::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide annual earnings surprises for one required provider year. + #[pyo3(signature = (year))] + async fn earnings_surprises(&self, year: i64) -> PyResult> { + let query = earnings_surprises_query(year)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_earnings_surprises(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(BulkEarningsSurprise::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide bulk income statements for one year and fiscal period. + #[pyo3(signature = (year, period))] + async fn income_statements( + &self, + year: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\"]", imports = ("typing",)))] + period: String, + ) -> PyResult> { + let query = income_statements_query(year, &period)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_income_statements(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(BulkIncomeStatement::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide bulk income-statement growth for one year and fiscal period. + #[pyo3(signature = (year, period))] + async fn income_statement_growth( + &self, + year: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\"]", imports = ("typing",)))] + period: String, + ) -> PyResult> { + let query = income_statement_growth_query(year, &period)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_income_statement_growth(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(BulkIncomeStatementGrowth::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves worldwide bulk balance sheets for one year and fiscal period. + #[pyo3(signature = (year, period))] + async fn balance_sheet_statements( + &self, + year: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\"]", imports = ("typing",)))] + period: String, + ) -> PyResult> { + let query = balance_sheet_statements_query(year, &period)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_balance_sheet_statements(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(BulkBalanceSheetStatement::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves worldwide bulk balance-sheet growth for one year and fiscal period. + #[pyo3(signature = (year, period))] + async fn balance_sheet_statement_growth( + &self, + year: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\"]", imports = ("typing",)))] + period: String, + ) -> PyResult> { + let query = balance_sheet_statement_growth_query(year, &period)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_balance_sheet_statement_growth(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(BulkBalanceSheetStatementGrowth::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves worldwide bulk cash-flow statements for one year and fiscal period. + #[pyo3(signature = (year, period))] + async fn cash_flow_statements( + &self, + year: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\"]", imports = ("typing",)))] + period: String, + ) -> PyResult> { + let query = cash_flow_statements_query(year, &period)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_cash_flow_statements(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(BulkCashFlowStatement::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide bulk cash-flow growth for one year and fiscal period. + #[pyo3(signature = (year, period))] + async fn cash_flow_statement_growth( + &self, + year: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\"]", imports = ("typing",)))] + period: String, + ) -> PyResult> { + let query = cash_flow_statement_growth_query(year, &period)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_cash_flow_statement_growth(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(BulkCashFlowStatementGrowth::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves worldwide bulk end-of-day prices for one required date. + #[pyo3(signature = (date))] + async fn eod(&self, date: args::DateArg) -> PyResult> { + let query = eod_query(date)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.bulk_eod(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(BulkEodBar::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `BulkPartQuery` for `BulkNamespace::company_profiles` from validated Python arguments. fn company_profiles_query(part: &str) -> PyResult { let part = args::bulk_part("part", part)?; diff --git a/crates/fmp-py/src/namespaces/calendar.rs b/crates/fmp-py/src/namespaces/calendar.rs index c5ecd186..2b602ab1 100644 --- a/crates/fmp-py/src/namespaces/calendar.rs +++ b/crates/fmp-py/src/namespaces/calendar.rs @@ -15,7 +15,7 @@ use crate::errors::to_py_error; use crate::models::calendar::{ DividendEvent, EarningsEvent, IpoCalendarEvent, IpoDisclosure, IpoProspectus, StockSplitEvent, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Calendar endpoints for a single client, exposed as `client.calendar`. #[gen_stub_pyclass] @@ -213,6 +213,168 @@ impl CalendarNamespace { } } +/// Calendar endpoints for a single async client, exposed as `client.calendar`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.calendar", frozen)] +pub(crate) struct AsyncCalendarNamespace { + builder: Arc, +} + +impl AsyncCalendarNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncCalendarNamespace { + /// Retrieves worldwide dividend events for one company. + #[pyo3(signature = (symbol, *, limit=None))] + async fn dividends(&self, symbol: String, limit: Option) -> PyResult> { + let query = dividends_query(&symbol, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.dividends(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(DividendEvent::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the worldwide dividend calendar. + #[pyo3(signature = (*, from_=None, to=None, page=None))] + async fn dividends_calendar( + &self, + from_: Option, + to: Option, + page: Option, + ) -> PyResult> { + let query = dividends_calendar_query(from_, to, page)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.dividends_calendar(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(DividendEvent::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide earnings events for one company. + #[pyo3(signature = (symbol, *, limit=None, include_report_times=None))] + async fn earnings( + &self, + symbol: String, + limit: Option, + include_report_times: Option, + ) -> PyResult> { + let query = earnings_query(&symbol, limit, include_report_times)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.earnings(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EarningsEvent::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the worldwide earnings calendar. + #[pyo3(signature = (*, from_=None, to=None, page=None, include_report_times=None))] + async fn earnings_calendar( + &self, + from_: Option, + to: Option, + page: Option, + include_report_times: Option, + ) -> PyResult> { + let query = earnings_calendar_query(from_, to, page, include_report_times)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.earnings_calendar(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EarningsEvent::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the worldwide IPO calendar. + #[pyo3(signature = (*, from_=None, to=None))] + async fn ipos_calendar( + &self, + from_: Option, + to: Option, + ) -> PyResult> { + let query = ipos_calendar_query(from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.ipos_calendar(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(IpoCalendarEvent::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves US IPO disclosure filings. + #[pyo3(signature = (*, from_=None, to=None))] + async fn ipos_disclosure( + &self, + from_: Option, + to: Option, + ) -> PyResult> { + let query = ipos_disclosure_query(from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.ipos_disclosure(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(IpoDisclosure::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves US IPO prospectus filings and documented offering values. + #[pyo3(signature = (*, from_=None, to=None))] + async fn ipos_prospectus( + &self, + from_: Option, + to: Option, + ) -> PyResult> { + let query = ipos_prospectus_query(from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.ipos_prospectus(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(IpoProspectus::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide stock-split events for one company. + #[pyo3(signature = (symbol, *, limit=None))] + async fn stock_splits( + &self, + symbol: String, + limit: Option, + ) -> PyResult> { + let query = stock_splits_query(&symbol, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_splits(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockSplitEvent::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the worldwide stock-splits calendar. + #[pyo3(signature = (*, from_=None, to=None, page=None))] + async fn stock_splits_calendar( + &self, + from_: Option, + to: Option, + page: Option, + ) -> PyResult> { + let query = stock_splits_calendar_query(from_, to, page)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_splits_calendar(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockSplitEvent::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `DividendsQuery` for `CalendarNamespace::dividends` from validated Python arguments. fn dividends_query(symbol: &str, limit: Option) -> PyResult { let symbol = args::ticker("symbol", symbol)?; diff --git a/crates/fmp-py/src/namespaces/chart.rs b/crates/fmp-py/src/namespaces/chart.rs index 1836c93f..a0367887 100644 --- a/crates/fmp-py/src/namespaces/chart.rs +++ b/crates/fmp-py/src/namespaces/chart.rs @@ -11,7 +11,7 @@ use crate::errors::to_py_error; use crate::models::chart::{ StockChartAdjustedBar, StockChartFullBar, StockChartIntradayBar, StockChartLightBar, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Chart endpoints for a single client, exposed as `client.chart`. #[gen_stub_pyclass] @@ -242,6 +242,205 @@ impl ChartNamespace { } } +/// Chart endpoints for a single async client, exposed as `client.chart`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.chart", frozen)] +pub(crate) struct AsyncChartNamespace { + builder: Arc, +} + +impl AsyncChartNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncChartNamespace { + /// Retrieves compact worldwide end-of-day stock chart rows. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn light( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = light_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_chart_light(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartLightBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves detailed worldwide end-of-day stock chart rows. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn full( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = full_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_chart_full(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartFullBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves split-unadjusted worldwide end-of-day stock chart rows. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn non_split_adjusted( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = non_split_adjusted_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_chart_non_split_adjusted(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartAdjustedBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves dividend-adjusted worldwide end-of-day stock chart rows. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn dividend_adjusted( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = dividend_adjusted_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_chart_dividend_adjusted(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartAdjustedBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves one-minute worldwide intraday stock chart rows. + #[pyo3(signature = (symbol, *, from_=None, to=None, nonadjusted=None, extended=None))] + async fn one_minute( + &self, + symbol: String, + from_: Option, + to: Option, + nonadjusted: Option, + extended: Option, + ) -> PyResult> { + let query = one_minute_query(&symbol, from_, to, nonadjusted, extended)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_chart_one_minute(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves five-minute worldwide intraday stock chart rows. + #[pyo3(signature = (symbol, *, from_=None, to=None, nonadjusted=None, extended=None))] + async fn five_minutes( + &self, + symbol: String, + from_: Option, + to: Option, + nonadjusted: Option, + extended: Option, + ) -> PyResult> { + let query = five_minutes_query(&symbol, from_, to, nonadjusted, extended)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_chart_five_minutes(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves fifteen-minute worldwide intraday stock chart rows. + #[pyo3(signature = (symbol, *, from_=None, to=None, nonadjusted=None, extended=None))] + async fn fifteen_minutes( + &self, + symbol: String, + from_: Option, + to: Option, + nonadjusted: Option, + extended: Option, + ) -> PyResult> { + let query = fifteen_minutes_query(&symbol, from_, to, nonadjusted, extended)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_chart_fifteen_minutes(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves thirty-minute worldwide intraday stock chart rows. + #[pyo3(signature = (symbol, *, from_=None, to=None, nonadjusted=None, extended=None))] + async fn thirty_minutes( + &self, + symbol: String, + from_: Option, + to: Option, + nonadjusted: Option, + extended: Option, + ) -> PyResult> { + let query = thirty_minutes_query(&symbol, from_, to, nonadjusted, extended)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_chart_thirty_minutes(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves one-hour worldwide intraday stock chart rows. + #[pyo3(signature = (symbol, *, from_=None, to=None, nonadjusted=None, extended=None))] + async fn one_hour( + &self, + symbol: String, + from_: Option, + to: Option, + nonadjusted: Option, + extended: Option, + ) -> PyResult> { + let query = one_hour_query(&symbol, from_, to, nonadjusted, extended)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_chart_one_hour(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves four-hour worldwide intraday stock chart rows. + #[pyo3(signature = (symbol, *, from_=None, to=None, nonadjusted=None, extended=None))] + async fn four_hours( + &self, + symbol: String, + from_: Option, + to: Option, + nonadjusted: Option, + extended: Option, + ) -> PyResult> { + let query = four_hours_query(&symbol, from_, to, nonadjusted, extended)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_chart_four_hours(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `StockChartEodQuery` for `ChartNamespace::light` from validated Python arguments. fn light_query( symbol: &str, diff --git a/crates/fmp-py/src/namespaces/commitment_of_traders.rs b/crates/fmp-py/src/namespaces/commitment_of_traders.rs index 4e0d6ed3..a278d4f2 100644 --- a/crates/fmp-py/src/namespaces/commitment_of_traders.rs +++ b/crates/fmp-py/src/namespaces/commitment_of_traders.rs @@ -9,7 +9,7 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::args; use crate::errors::to_py_error; use crate::models::commitment_of_traders::{CotAnalysis, CotReport, CotReportListing}; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Commitment of traders endpoints for a single client, exposed as `client.commitment_of_traders`. #[gen_stub_pyclass] @@ -83,6 +83,67 @@ impl CommitmentOfTradersNamespace { } } +/// Commitment of traders endpoints for a single async client, exposed as `client.commitment_of_traders`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.commitment_of_traders", frozen)] +pub(crate) struct AsyncCommitmentOfTradersNamespace { + builder: Arc, +} + +impl AsyncCommitmentOfTradersNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncCommitmentOfTradersNamespace { + /// Retrieves detailed Commitment of Traders reports. + #[pyo3(signature = (*, symbol=None, from_=None, to=None))] + async fn report( + &self, + symbol: Option, + from_: Option, + to: Option, + ) -> PyResult> { + let query = report_query(symbol.as_deref(), from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.cot_report(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CotReport::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves derived Commitment of Traders market analysis. + #[pyo3(signature = (*, symbol=None, from_=None, to=None))] + async fn analysis( + &self, + symbol: Option, + from_: Option, + to: Option, + ) -> PyResult> { + let query = analysis_query(symbol.as_deref(), from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.cot_analysis(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CotAnalysis::from).collect()) + .map_err(to_py_error) + } + + /// Lists the Commitment of Traders reports available from the provider. + async fn report_list(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.cot_report_list().await + }) + .await?; + rows.map(|items| items.into_iter().map(CotReportListing::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `CotQuery` for `CommitmentOfTradersNamespace::report` from validated Python arguments. fn report_query( symbol: Option<&str>, diff --git a/crates/fmp-py/src/namespaces/commodities.rs b/crates/fmp-py/src/namespaces/commodities.rs index 88fe7994..f3df32e3 100644 --- a/crates/fmp-py/src/namespaces/commodities.rs +++ b/crates/fmp-py/src/namespaces/commodities.rs @@ -12,7 +12,7 @@ use crate::errors::to_py_error; use crate::models::chart::{StockChartFullBar, StockChartIntradayBar, StockChartLightBar}; use crate::models::commodities::CommodityListing; use crate::models::quote::{Quote, QuoteShort}; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Commodities endpoints for a single client, exposed as `client.commodities`. #[gen_stub_pyclass] @@ -172,6 +172,142 @@ impl CommoditiesNamespace { } } +/// Commodities endpoints for a single async client, exposed as `client.commodities`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.commodities", frozen)] +pub(crate) struct AsyncCommoditiesNamespace { + builder: Arc, +} + +impl AsyncCommoditiesNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncCommoditiesNamespace { + /// Lists the provider's documented commodity catalog. + async fn list(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.commodities_list().await + }) + .await?; + rows.map(|items| items.into_iter().map(CommodityListing::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves a detailed US-only quote for one commodity. + #[pyo3(signature = (symbol))] + async fn quote(&self, symbol: String) -> PyResult> { + let query = quote_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.commodity_quote(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(Quote::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves a compact US-only quote for one commodity. + #[pyo3(signature = (symbol))] + async fn quote_short(&self, symbol: String) -> PyResult> { + let query = quote_short_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.commodity_quote_short(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(QuoteShort::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves compact end-of-day commodity history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_light( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_light_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.commodity_chart_light(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartLightBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves detailed end-of-day commodity history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_full( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_full_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.commodity_chart_full(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartFullBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves one-minute commodity history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_one_minute( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_one_minute_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.commodity_chart_one_minute(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves five-minute commodity history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_five_minutes( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_five_minutes_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.commodity_chart_five_minutes(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves one-hour commodity history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_one_hour( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_one_hour_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.commodity_chart_one_hour(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `QuoteQuery` for `CommoditiesNamespace::quote` from validated Python arguments. fn quote_query(symbol: &str) -> PyResult { let symbol = args::ticker("symbol", symbol)?; diff --git a/crates/fmp-py/src/namespaces/company.rs b/crates/fmp-py/src/namespaces/company.rs index 5983d72d..a92da143 100644 --- a/crates/fmp-py/src/namespaces/company.rs +++ b/crates/fmp-py/src/namespaces/company.rs @@ -19,7 +19,7 @@ use crate::models::company::{ DelistedCompany, EmployeeCount, ExecutiveCompensation, ExecutiveCompensationBenchmark, MergerAcquisition, ShareFloat, StockPeer, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Company endpoints for a single client, exposed as `client.company`. #[gen_stub_pyclass] @@ -348,6 +348,282 @@ impl CompanyNamespace { } } +/// Company endpoints for a single async client, exposed as `client.company`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.company", frozen)] +pub(crate) struct AsyncCompanyNamespace { + builder: Arc, +} + +impl AsyncCompanyNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncCompanyNamespace { + /// Retrieves worldwide company profiles by provider ticker. + #[pyo3(signature = (symbol))] + async fn profile(&self, symbol: String) -> PyResult> { + let query = profile_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.profile(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CompanyProfile::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves US company profiles by representation-preserving CIK. + #[pyo3(signature = (cik))] + async fn profile_by_cik(&self, cik: String) -> PyResult> { + let query = profile_by_cik_query(&cik)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.profile_by_cik(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CompanyProfile::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves company-issued notes for a US company. + #[pyo3(signature = (symbol))] + async fn notes(&self, symbol: String) -> PyResult> { + let query = notes_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.company_notes(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CompanyNote::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide stock peers for a company. + #[pyo3(signature = (symbol))] + async fn stock_peers(&self, symbol: String) -> PyResult> { + let query = stock_peers_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_peers(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockPeer::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves delisted US companies with optional provider pagination. + #[pyo3(signature = (*, page=None, limit=None))] + async fn delisted_companies( + &self, + page: Option, + limit: Option, + ) -> PyResult> { + let query = delisted_companies_query(page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.delisted_companies(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(DelistedCompany::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves current employee-count filings for a US company. + #[pyo3(signature = (symbol, *, limit=None))] + async fn employee_count( + &self, + symbol: String, + limit: Option, + ) -> PyResult> { + let query = employee_count_query(&symbol, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.employee_count(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EmployeeCount::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves historical employee-count filings for a US company. + #[pyo3(signature = (symbol, *, limit=None))] + async fn historical_employee_count( + &self, + symbol: String, + limit: Option, + ) -> PyResult> { + let query = historical_employee_count_query(&symbol, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.historical_employee_count(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EmployeeCount::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves current worldwide market capitalization for one company. + #[pyo3(signature = (symbol))] + async fn market_capitalization( + &self, + symbol: String, + ) -> PyResult> { + let query = market_capitalization_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.market_capitalization(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(CompanyMarketCapitalization::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves current worldwide market capitalization for multiple companies. + #[pyo3(signature = (symbols))] + async fn batch_market_capitalization( + &self, + symbols: args::SymbolsArg, + ) -> PyResult> { + let query = batch_market_capitalization_query(symbols)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.batch_market_capitalization(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(CompanyMarketCapitalization::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves historical worldwide market capitalization for one company. + #[pyo3(signature = (symbol, *, limit=None, from_=None, to=None))] + async fn historical_market_capitalization( + &self, + symbol: String, + limit: Option, + from_: Option, + to: Option, + ) -> PyResult> { + let query = historical_market_capitalization_query(&symbol, limit, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.historical_market_capitalization(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(CompanyMarketCapitalization::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves current worldwide share-float data for one company. + #[pyo3(signature = (symbol))] + async fn shares_float(&self, symbol: String) -> PyResult> { + let query = shares_float_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.shares_float(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CompanyShareFloat::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves paginated worldwide share-float data for all companies. + #[pyo3(signature = (*, page=None, limit=None))] + async fn all_shares_float( + &self, + page: Option, + limit: Option, + ) -> PyResult> { + let query = all_shares_float_query(page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.all_shares_float(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(ShareFloat::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the latest US mergers and acquisitions with optional pagination. + #[pyo3(signature = (*, page=None, limit=None))] + async fn latest_mergers_acquisitions( + &self, + page: Option, + limit: Option, + ) -> PyResult> { + let query = latest_mergers_acquisitions_query(page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_mergers_acquisitions(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(MergerAcquisition::from).collect()) + .map_err(to_py_error) + } + + /// Searches US mergers and acquisitions by representation-preserving company name. + #[pyo3(signature = (name))] + async fn search_mergers_acquisitions(&self, name: String) -> PyResult> { + let query = search_mergers_acquisitions_query(&name)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_mergers_acquisitions(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(MergerAcquisition::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide company executives by provider ticker. + #[pyo3(signature = (symbol))] + async fn key_executives(&self, symbol: String) -> PyResult> { + let query = key_executives_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.key_executives(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CompanyExecutive::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves executive compensation for a US company. + #[pyo3(signature = (symbol))] + async fn executive_compensation(&self, symbol: String) -> PyResult> { + let query = executive_compensation_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.executive_compensation(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(ExecutiveCompensation::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves US executive-compensation benchmarks with an optional year. + #[pyo3(signature = (*, year=None))] + async fn executive_compensation_benchmark( + &self, + year: Option, + ) -> PyResult> { + let query = executive_compensation_benchmark_query(year.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.executive_compensation_benchmark(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(ExecutiveCompensationBenchmark::from) + .collect() + }) + .map_err(to_py_error) + } +} + /// Builds the `ProfileQuery` for `CompanyNamespace::profile` from validated Python arguments. fn profile_query(symbol: &str) -> PyResult { let symbol = args::ticker("symbol", symbol)?; diff --git a/crates/fmp-py/src/namespaces/congressional.rs b/crates/fmp-py/src/namespaces/congressional.rs index a8a3019a..a1bc2660 100644 --- a/crates/fmp-py/src/namespaces/congressional.rs +++ b/crates/fmp-py/src/namespaces/congressional.rs @@ -16,7 +16,7 @@ use crate::models::congressional::{ CongressionalMemberNetWorth, CongressionalMemberNetWorthAggregate, CongressionalMemberPosition, CongressionalMemberProfile, CongressionalTrade, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Congressional endpoints for a single client, exposed as `client.congressional`. #[gen_stub_pyclass] @@ -302,6 +302,252 @@ impl CongressionalNamespace { } } +/// Congressional endpoints for a single async client, exposed as `client.congressional`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.congressional", frozen)] +pub(crate) struct AsyncCongressionalNamespace { + builder: Arc, +} + +impl AsyncCongressionalNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncCongressionalNamespace { + /// Retrieves the latest Senate financial disclosures. + #[pyo3(signature = (*, page=None, limit=None))] + async fn latest_senate_disclosures( + &self, + page: Option, + limit: Option, + ) -> PyResult> { + let query = latest_senate_disclosures_query(page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_senate_disclosures(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CongressionalTrade::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the latest House financial disclosures. + #[pyo3(signature = (*, page=None, limit=None))] + async fn latest_house_disclosures( + &self, + page: Option, + limit: Option, + ) -> PyResult> { + let query = latest_house_disclosures_query(page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_house_disclosures(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CongressionalTrade::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves Senate trades for one ticker. + #[pyo3(signature = (symbol, *, page=None, limit=None))] + async fn senate_trades( + &self, + symbol: String, + page: Option, + limit: Option, + ) -> PyResult> { + let query = senate_trades_query(&symbol, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.senate_trades(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CongressionalTrade::from).collect()) + .map_err(to_py_error) + } + + /// Searches Senate trades by member name. + #[pyo3(signature = (name))] + async fn senate_trades_by_name(&self, name: String) -> PyResult> { + let query = senate_trades_by_name_query(&name)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.senate_trades_by_name(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CongressionalTrade::from).collect()) + .map_err(to_py_error) + } + + /// Looks up Senate trades by an optional member ID and pagination. + #[pyo3(signature = (*, page=None, limit=None, member_id=None))] + async fn senate_trades_by_member_id( + &self, + page: Option, + limit: Option, + member_id: Option, + ) -> PyResult> { + let query = senate_trades_by_member_id_query(page, limit, member_id.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.senate_trades_by_member_id(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CongressionalTrade::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves House trades for one ticker. + #[pyo3(signature = (symbol, *, page=None, limit=None))] + async fn house_trades( + &self, + symbol: String, + page: Option, + limit: Option, + ) -> PyResult> { + let query = house_trades_query(&symbol, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.house_trades(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CongressionalTrade::from).collect()) + .map_err(to_py_error) + } + + /// Searches House trades by member name. + #[pyo3(signature = (name))] + async fn house_trades_by_name(&self, name: String) -> PyResult> { + let query = house_trades_by_name_query(&name)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.house_trades_by_name(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CongressionalTrade::from).collect()) + .map_err(to_py_error) + } + + /// Looks up House trades by an optional member ID and pagination. + #[pyo3(signature = (*, page=None, limit=None, member_id=None))] + async fn house_trades_by_member_id( + &self, + page: Option, + limit: Option, + member_id: Option, + ) -> PyResult> { + let query = house_trades_by_member_id_query(page, limit, member_id.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.house_trades_by_member_id(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CongressionalTrade::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves congressional profiles using optional provider filters. + #[pyo3(signature = (*, active=None, member_id=None, latest_party=None, latest_position=None, page=None, limit=None))] + async fn profiles( + &self, + active: Option, + member_id: Option, + latest_party: Option, + latest_position: Option, + page: Option, + limit: Option, + ) -> PyResult> { + let query = profiles_query( + active, + member_id.as_deref(), + latest_party.as_deref(), + latest_position.as_deref(), + page, + limit, + )?; + let rows = spawn(self.builder.clone(), |client| async move { + client.congressional_profiles(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(CongressionalMemberProfile::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves congressional position history using optional provider filters. + #[pyo3(signature = (*, member_id=None, party=None, position=None, page=None, limit=None))] + async fn positions( + &self, + member_id: Option, + party: Option, + position: Option, + page: Option, + limit: Option, + ) -> PyResult> { + let query = positions_query( + member_id.as_deref(), + party.as_deref(), + position.as_deref(), + page, + limit, + )?; + let rows = spawn(self.builder.clone(), |client| async move { + client.congressional_positions(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(CongressionalMemberPosition::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves itemized congressional net-worth disclosures. + #[pyo3(signature = (member_id, *, page=None, limit=None))] + async fn net_worth( + &self, + member_id: String, + page: Option, + limit: Option, + ) -> PyResult> { + let query = net_worth_query(&member_id, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.congressional_net_worth(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(CongressionalMemberNetWorth::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves aggregated congressional net-worth totals by year. + #[pyo3(signature = (member_id, *, totals_col=None))] + async fn net_worth_aggregated( + &self, + member_id: String, + totals_col: Option, + ) -> PyResult> { + let query = net_worth_aggregated_query(&member_id, totals_col.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.congressional_net_worth_aggregated(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(CongressionalMemberNetWorthAggregate::from) + .collect() + }) + .map_err(to_py_error) + } +} + /// Builds the `LatestCongressionalDisclosuresQuery` for `CongressionalNamespace::latest_senate_disclosures` from validated Python arguments. fn latest_senate_disclosures_query( page: Option, diff --git a/crates/fmp-py/src/namespaces/crypto.rs b/crates/fmp-py/src/namespaces/crypto.rs index f1fd4e33..6de43864 100644 --- a/crates/fmp-py/src/namespaces/crypto.rs +++ b/crates/fmp-py/src/namespaces/crypto.rs @@ -12,7 +12,7 @@ use crate::errors::to_py_error; use crate::models::chart::{StockChartFullBar, StockChartIntradayBar, StockChartLightBar}; use crate::models::crypto::CryptocurrencyListing; use crate::models::quote::{Quote, QuoteShort}; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Crypto endpoints for a single client, exposed as `client.crypto`. #[gen_stub_pyclass] @@ -171,6 +171,142 @@ impl CryptoNamespace { } } +/// Crypto endpoints for a single async client, exposed as `client.crypto`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.crypto", frozen)] +pub(crate) struct AsyncCryptoNamespace { + builder: Arc, +} + +impl AsyncCryptoNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncCryptoNamespace { + /// Lists the provider's documented cryptocurrency catalog. + async fn list(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.cryptocurrency_list().await + }) + .await?; + rows.map(|items| items.into_iter().map(CryptocurrencyListing::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves a detailed quote for one cryptocurrency. + #[pyo3(signature = (symbol))] + async fn quote(&self, symbol: String) -> PyResult> { + let query = quote_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.cryptocurrency_quote(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(Quote::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves a compact quote for one cryptocurrency. + #[pyo3(signature = (symbol))] + async fn quote_short(&self, symbol: String) -> PyResult> { + let query = quote_short_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.cryptocurrency_quote_short(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(QuoteShort::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves compact end-of-day cryptocurrency history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_light( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_light_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.cryptocurrency_chart_light(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartLightBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves detailed end-of-day cryptocurrency history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_full( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_full_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.cryptocurrency_chart_full(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartFullBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves one-minute cryptocurrency history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_one_minute( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_one_minute_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.cryptocurrency_chart_one_minute(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves five-minute cryptocurrency history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_five_minutes( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_five_minutes_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.cryptocurrency_chart_five_minutes(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves one-hour cryptocurrency history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_one_hour( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_one_hour_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.cryptocurrency_chart_one_hour(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `QuoteQuery` for `CryptoNamespace::quote` from validated Python arguments. fn quote_query(symbol: &str) -> PyResult { let symbol = args::ticker("symbol", symbol)?; diff --git a/crates/fmp-py/src/namespaces/dcf.rs b/crates/fmp-py/src/namespaces/dcf.rs index c16a1f9f..3d6ab024 100644 --- a/crates/fmp-py/src/namespaces/dcf.rs +++ b/crates/fmp-py/src/namespaces/dcf.rs @@ -9,7 +9,7 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::args; use crate::errors::to_py_error; use crate::models::dcf::{CustomDcfValuation, CustomLeveredDcfValuation, DcfValuation}; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Dcf endpoints for a single client, exposed as `client.dcf`. #[gen_stub_pyclass] @@ -175,6 +175,160 @@ impl DcfNamespace { } } +/// Dcf endpoints for a single async client, exposed as `client.dcf`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.dcf", frozen)] +pub(crate) struct AsyncDcfNamespace { + builder: Arc, +} + +impl AsyncDcfNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncDcfNamespace { + /// Retrieves the standard discounted-cash-flow valuation for one company. + #[pyo3(signature = (symbol))] + async fn standard(&self, symbol: String) -> PyResult> { + let query = standard_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.discounted_cash_flow(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(DcfValuation::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the levered discounted-cash-flow valuation for one company. + #[pyo3(signature = (symbol))] + async fn levered(&self, symbol: String) -> PyResult> { + let query = levered_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.levered_discounted_cash_flow(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(DcfValuation::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves a custom unlevered discounted-cash-flow valuation. + #[pyo3(signature = (symbol, *, revenue_growth_pct=None, ebitda_pct=None, depreciation_and_amortization_pct=None, cash_and_short_term_investments_pct=None, receivables_pct=None, inventories_pct=None, payable_pct=None, ebit_pct=None, capital_expenditure_pct=None, operating_cash_flow_pct=None, selling_general_and_administrative_expenses_pct=None, tax_rate=None, long_term_growth_rate=None, cost_of_debt=None, cost_of_equity=None, market_risk_premium=None, beta=None, risk_free_rate=None))] + #[allow(clippy::too_many_arguments)] + async fn custom( + &self, + symbol: String, + revenue_growth_pct: Option, + ebitda_pct: Option, + depreciation_and_amortization_pct: Option, + cash_and_short_term_investments_pct: Option, + receivables_pct: Option, + inventories_pct: Option, + payable_pct: Option, + ebit_pct: Option, + capital_expenditure_pct: Option, + operating_cash_flow_pct: Option, + selling_general_and_administrative_expenses_pct: Option, + tax_rate: Option, + long_term_growth_rate: Option, + cost_of_debt: Option, + cost_of_equity: Option, + market_risk_premium: Option, + beta: Option, + risk_free_rate: Option, + ) -> PyResult> { + let query = custom_query( + &symbol, + revenue_growth_pct, + ebitda_pct, + depreciation_and_amortization_pct, + cash_and_short_term_investments_pct, + receivables_pct, + inventories_pct, + payable_pct, + ebit_pct, + capital_expenditure_pct, + operating_cash_flow_pct, + selling_general_and_administrative_expenses_pct, + tax_rate, + long_term_growth_rate, + cost_of_debt, + cost_of_equity, + market_risk_premium, + beta, + risk_free_rate, + )?; + let rows = spawn(self.builder.clone(), |client| async move { + client.custom_discounted_cash_flow(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CustomDcfValuation::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves a custom levered discounted-cash-flow valuation. + #[pyo3(signature = (symbol, *, revenue_growth_pct=None, ebitda_pct=None, depreciation_and_amortization_pct=None, cash_and_short_term_investments_pct=None, receivables_pct=None, inventories_pct=None, payable_pct=None, ebit_pct=None, capital_expenditure_pct=None, operating_cash_flow_pct=None, selling_general_and_administrative_expenses_pct=None, tax_rate=None, long_term_growth_rate=None, cost_of_debt=None, cost_of_equity=None, market_risk_premium=None, beta=None, risk_free_rate=None))] + #[allow(clippy::too_many_arguments)] + async fn custom_levered( + &self, + symbol: String, + revenue_growth_pct: Option, + ebitda_pct: Option, + depreciation_and_amortization_pct: Option, + cash_and_short_term_investments_pct: Option, + receivables_pct: Option, + inventories_pct: Option, + payable_pct: Option, + ebit_pct: Option, + capital_expenditure_pct: Option, + operating_cash_flow_pct: Option, + selling_general_and_administrative_expenses_pct: Option, + tax_rate: Option, + long_term_growth_rate: Option, + cost_of_debt: Option, + cost_of_equity: Option, + market_risk_premium: Option, + beta: Option, + risk_free_rate: Option, + ) -> PyResult> { + let query = custom_levered_query( + &symbol, + revenue_growth_pct, + ebitda_pct, + depreciation_and_amortization_pct, + cash_and_short_term_investments_pct, + receivables_pct, + inventories_pct, + payable_pct, + ebit_pct, + capital_expenditure_pct, + operating_cash_flow_pct, + selling_general_and_administrative_expenses_pct, + tax_rate, + long_term_growth_rate, + cost_of_debt, + cost_of_equity, + market_risk_premium, + beta, + risk_free_rate, + )?; + let rows = spawn(self.builder.clone(), |client| async move { + client.custom_levered_discounted_cash_flow(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(CustomLeveredDcfValuation::from) + .collect() + }) + .map_err(to_py_error) + } +} + /// Builds the `DcfQuery` for `DcfNamespace::standard` from validated Python arguments. fn standard_query(symbol: &str) -> PyResult { let symbol = args::ticker("symbol", symbol)?; diff --git a/crates/fmp-py/src/namespaces/directory.rs b/crates/fmp-py/src/namespaces/directory.rs index 8f4fd49d..927fe394 100644 --- a/crates/fmp-py/src/namespaces/directory.rs +++ b/crates/fmp-py/src/namespaces/directory.rs @@ -13,7 +13,7 @@ use crate::models::directory::{ CikListing, CompanySymbol, EarningsTranscriptAvailability, EtfSymbol, FinancialStatementSymbol, SymbolChange, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Directory endpoints for a single client, exposed as `client.directory`. #[gen_stub_pyclass] @@ -202,6 +202,156 @@ impl DirectoryNamespace { } } +/// Directory endpoints for a single async client, exposed as `client.directory`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.directory", frozen)] +pub(crate) struct AsyncDirectoryNamespace { + builder: Arc, +} + +impl AsyncDirectoryNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncDirectoryNamespace { + /// Lists worldwide company and instrument symbols. + async fn company_symbols(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.company_symbols().await + }) + .await?; + rows.map(|items| items.into_iter().map(CompanySymbol::from).collect()) + .map_err(to_py_error) + } + + /// Lists worldwide companies with financial statements available. + async fn financial_statement_symbols(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.financial_statement_symbols().await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(FinancialStatementSymbol::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Lists US SEC entities with optional provider pagination. + #[pyo3(signature = (*, page=None, limit=None))] + async fn cik_list(&self, page: Option, limit: Option) -> PyResult> { + let query = cik_list_query(page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.cik_list(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CikListing::from).collect()) + .map_err(to_py_error) + } + + /// Lists US symbol changes without interpreting the provider's `invalid` flag. + #[pyo3(signature = (*, invalid=None, limit=None))] + async fn symbol_changes( + &self, + invalid: Option, + limit: Option, + ) -> PyResult> { + let query = symbol_changes_query(invalid, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.symbol_changes(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(SymbolChange::from).collect()) + .map_err(to_py_error) + } + + /// Lists worldwide exchange-traded fund symbols. + async fn etf_symbols(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.etf_symbols().await + }) + .await?; + rows.map(|items| items.into_iter().map(EtfSymbol::from).collect()) + .map_err(to_py_error) + } + + /// Lists worldwide actively trading companies and instruments. + async fn actively_trading(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.actively_trading().await + }) + .await?; + rows.map(|items| items.into_iter().map(ActivelyTradingSymbol::from).collect()) + .map_err(to_py_error) + } + + /// Lists US companies with their available earnings-transcript counts. + async fn earnings_transcript_list(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.earnings_transcript_list().await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(EarningsTranscriptAvailability::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Lists supported worldwide stock exchanges. + #[pyo3(signature = (*, extended=None))] + async fn available_exchanges( + &self, + extended: Option, + ) -> PyResult> { + let query = available_exchanges_query(extended)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.available_exchanges(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(AvailableExchange::from).collect()) + .map_err(to_py_error) + } + + /// Lists the provider's available sectors. + async fn available_sectors(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.available_sectors().await + }) + .await?; + rows.map(|items| items.into_iter().map(AvailableSector::from).collect()) + .map_err(to_py_error) + } + + /// Lists the provider's available industries. + async fn available_industries(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.available_industries().await + }) + .await?; + rows.map(|items| items.into_iter().map(AvailableIndustry::from).collect()) + .map_err(to_py_error) + } + + /// Lists the provider's available countries. + async fn available_countries(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.available_countries().await + }) + .await?; + rows.map(|items| items.into_iter().map(AvailableCountry::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `CikListQuery` for `DirectoryNamespace::cik_list` from validated Python arguments. fn cik_list_query(page: Option, limit: Option) -> PyResult { let page = args::optional("page", page, args::page)?; diff --git a/crates/fmp-py/src/namespaces/economics.rs b/crates/fmp-py/src/namespaces/economics.rs index 821fda59..81331920 100644 --- a/crates/fmp-py/src/namespaces/economics.rs +++ b/crates/fmp-py/src/namespaces/economics.rs @@ -13,7 +13,7 @@ use crate::errors::to_py_error; use crate::models::economics::{ EconomicCalendarEvent, EconomicIndicatorObservation, MarketRiskPremium, TreasuryRate, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Economics endpoints for a single client, exposed as `client.economics`. #[gen_stub_pyclass] @@ -108,6 +108,88 @@ impl EconomicsNamespace { } } +/// Economics endpoints for a single async client, exposed as `client.economics`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.economics", frozen)] +pub(crate) struct AsyncEconomicsNamespace { + builder: Arc, +} + +impl AsyncEconomicsNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncEconomicsNamespace { + /// Retrieves Treasury-rate observations. + #[pyo3(signature = (*, from_=None, to=None))] + async fn treasury_rates( + &self, + from_: Option, + to: Option, + ) -> PyResult> { + let query = treasury_rates_query(from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.treasury_rates(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(TreasuryRate::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves observations for one economic indicator. + #[pyo3(signature = (name, *, from_=None, to=None))] + async fn indicators( + &self, + name: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = indicators_query(&name, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.economic_indicators(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(EconomicIndicatorObservation::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves economic-calendar events. + #[pyo3(signature = (*, country=None, from_=None, to=None))] + async fn calendar( + &self, + country: Option, + from_: Option, + to: Option, + ) -> PyResult> { + let query = calendar_query(country.as_deref(), from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.economic_calendar(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EconomicCalendarEvent::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the provider's queryless country risk-premium rows. + async fn market_risk_premium(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.market_risk_premium().await + }) + .await?; + rows.map(|items| items.into_iter().map(MarketRiskPremium::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `TreasuryRatesQuery` for `EconomicsNamespace::treasury_rates` from validated Python arguments. fn treasury_rates_query( from_: Option, diff --git a/crates/fmp-py/src/namespaces/esg.rs b/crates/fmp-py/src/namespaces/esg.rs index 76ddb946..225e061a 100644 --- a/crates/fmp-py/src/namespaces/esg.rs +++ b/crates/fmp-py/src/namespaces/esg.rs @@ -9,7 +9,7 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::args; use crate::errors::to_py_error; use crate::models::esg::{EsgBenchmark, EsgDisclosure, EsgRating}; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Esg endpoints for a single client, exposed as `client.esg`. #[gen_stub_pyclass] @@ -71,6 +71,59 @@ impl EsgNamespace { } } +/// Esg endpoints for a single async client, exposed as `client.esg`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.esg", frozen)] +pub(crate) struct AsyncEsgNamespace { + builder: Arc, +} + +impl AsyncEsgNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncEsgNamespace { + /// Retrieves ESG disclosure filings for one company. + #[pyo3(signature = (symbol))] + async fn disclosures(&self, symbol: String) -> PyResult> { + let query = disclosures_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.esg_disclosures(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EsgDisclosure::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves ESG ratings for one company. + #[pyo3(signature = (symbol))] + async fn ratings(&self, symbol: String) -> PyResult> { + let query = ratings_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.esg_ratings(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EsgRating::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves sector ESG benchmarks, optionally for one provider year string. + #[pyo3(signature = (*, year=None))] + async fn benchmark(&self, year: Option) -> PyResult> { + let query = benchmark_query(year.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.esg_benchmark(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EsgBenchmark::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `EsgSymbolQuery` for `EsgNamespace::disclosures` from validated Python arguments. fn disclosures_query(symbol: &str) -> PyResult { let symbol = args::ticker("symbol", symbol)?; diff --git a/crates/fmp-py/src/namespaces/forex.rs b/crates/fmp-py/src/namespaces/forex.rs index 57087541..b9c07029 100644 --- a/crates/fmp-py/src/namespaces/forex.rs +++ b/crates/fmp-py/src/namespaces/forex.rs @@ -12,7 +12,7 @@ use crate::errors::to_py_error; use crate::models::chart::{StockChartFullBar, StockChartIntradayBar, StockChartLightBar}; use crate::models::forex::ForexPair; use crate::models::quote::{Quote, QuoteShort}; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Forex endpoints for a single client, exposed as `client.forex`. #[gen_stub_pyclass] @@ -169,6 +169,142 @@ impl ForexNamespace { } } +/// Forex endpoints for a single async client, exposed as `client.forex`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.forex", frozen)] +pub(crate) struct AsyncForexNamespace { + builder: Arc, +} + +impl AsyncForexNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncForexNamespace { + /// Lists the provider's documented forex pairs. + async fn list(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.forex_list().await + }) + .await?; + rows.map(|items| items.into_iter().map(ForexPair::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves a detailed US-only quote for one forex pair. + #[pyo3(signature = (symbol))] + async fn quote(&self, symbol: String) -> PyResult> { + let query = quote_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.forex_quote(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(Quote::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves a compact US-only quote for one forex pair. + #[pyo3(signature = (symbol))] + async fn quote_short(&self, symbol: String) -> PyResult> { + let query = quote_short_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.forex_quote_short(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(QuoteShort::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves compact end-of-day forex history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_light( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_light_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.forex_chart_light(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartLightBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves detailed end-of-day forex history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_full( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_full_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.forex_chart_full(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartFullBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves one-minute forex history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_one_minute( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_one_minute_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.forex_chart_one_minute(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves five-minute forex history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_five_minutes( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_five_minutes_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.forex_chart_five_minutes(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves one-hour forex history. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_one_hour( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_one_hour_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.forex_chart_one_hour(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `QuoteQuery` for `ForexNamespace::quote` from validated Python arguments. fn quote_query(symbol: &str) -> PyResult { let symbol = args::ticker("symbol", symbol)?; diff --git a/crates/fmp-py/src/namespaces/fundraising.rs b/crates/fmp-py/src/namespaces/fundraising.rs index a3d46b4e..bf8f925f 100644 --- a/crates/fmp-py/src/namespaces/fundraising.rs +++ b/crates/fmp-py/src/namespaces/fundraising.rs @@ -15,7 +15,7 @@ use crate::models::fundraising::{ CrowdfundingOffering, CrowdfundingOfferingSearchResult, RegulationDOffering, RegulationDOfferingSearchResult, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Fundraising endpoints for a single client, exposed as `client.fundraising`. #[gen_stub_pyclass] @@ -155,6 +155,126 @@ impl FundraisingNamespace { } } +/// Fundraising endpoints for a single async client, exposed as `client.fundraising`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.fundraising", frozen)] +pub(crate) struct AsyncFundraisingNamespace { + builder: Arc, +} + +impl AsyncFundraisingNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncFundraisingNamespace { + /// Retrieves the latest US crowdfunding offerings. + #[pyo3(signature = (*, page=None, limit=None))] + async fn latest_crowdfunding_offerings( + &self, + page: Option, + limit: Option, + ) -> PyResult> { + let query = latest_crowdfunding_offerings_query(page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_crowdfunding_offerings(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CrowdfundingOffering::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves US crowdfunding offerings for one issuer CIK. + #[pyo3(signature = (cik))] + async fn crowdfunding_offerings_by_cik( + &self, + cik: String, + ) -> PyResult> { + let query = crowdfunding_offerings_by_cik_query(&cik)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.crowdfunding_offerings_by_cik(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CrowdfundingOffering::from).collect()) + .map_err(to_py_error) + } + + /// Searches US crowdfunding offerings by company, campaign, or platform text. + #[pyo3(signature = (name))] + async fn search_crowdfunding_offerings( + &self, + name: String, + ) -> PyResult> { + let query = search_crowdfunding_offerings_query(&name)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_crowdfunding_offerings(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(CrowdfundingOfferingSearchResult::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Searches US Regulation D offerings by company name or stock symbol. + #[pyo3(signature = (name))] + async fn search_regulation_d_offerings( + &self, + name: String, + ) -> PyResult> { + let query = search_regulation_d_offerings_query(&name)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_regulation_d_offerings(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(RegulationDOfferingSearchResult::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves the latest US Regulation D offerings. + #[pyo3(signature = (*, page=None, limit=None, cik=None))] + async fn latest_regulation_d_offerings( + &self, + page: Option, + limit: Option, + cik: Option, + ) -> PyResult> { + let query = latest_regulation_d_offerings_query(page, limit, cik.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_regulation_d_offerings(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(RegulationDOffering::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves US Regulation D offerings for one issuer CIK. + #[pyo3(signature = (cik))] + async fn regulation_d_offerings_by_cik( + &self, + cik: String, + ) -> PyResult> { + let query = regulation_d_offerings_by_cik_query(&cik)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.regulation_d_offerings_by_cik(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(RegulationDOffering::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `LatestCrowdfundingOfferingsQuery` for `FundraisingNamespace::latest_crowdfunding_offerings` from validated Python arguments. fn latest_crowdfunding_offerings_query( page: Option, diff --git a/crates/fmp-py/src/namespaces/funds.rs b/crates/fmp-py/src/namespaces/funds.rs index e09401cd..be43d47b 100644 --- a/crates/fmp-py/src/namespaces/funds.rs +++ b/crates/fmp-py/src/namespaces/funds.rs @@ -17,7 +17,7 @@ use crate::models::funds::{ FundDisclosure, FundDisclosureHolder, FundDisclosureSearchResult, }; use crate::models::institutional_ownership::Form13fFilingDate; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Funds endpoints for a single client, exposed as `client.funds`. #[gen_stub_pyclass] @@ -197,6 +197,152 @@ impl FundsNamespace { } } +/// Funds endpoints for a single async client, exposed as `client.funds`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.funds", frozen)] +pub(crate) struct AsyncFundsNamespace { + builder: Arc, +} + +impl AsyncFundsNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncFundsNamespace { + /// Retrieves the worldwide holdings of one ETF or mutual fund. + #[pyo3(signature = (symbol))] + async fn etf_holdings(&self, symbol: String) -> PyResult> { + let query = etf_holdings_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.etf_holdings(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EtfFundHolding::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide descriptive and trading information for one fund. + #[pyo3(signature = (symbol))] + async fn etf_info(&self, symbol: String) -> PyResult> { + let query = etf_info_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.etf_info(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EtfFundInfo::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the worldwide country allocation for one ETF or mutual fund. + #[pyo3(signature = (symbol))] + async fn etf_country_weightings(&self, symbol: String) -> PyResult> { + let query = etf_country_weightings_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.etf_country_weightings(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EtfCountryWeighting::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the worldwide ETF exposure to one requested asset. + #[pyo3(signature = (symbol))] + async fn etf_asset_exposure(&self, symbol: String) -> PyResult> { + let query = etf_asset_exposure_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.etf_asset_exposure(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EtfAssetExposure::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the worldwide sector allocation for one ETF or mutual fund. + #[pyo3(signature = (symbol))] + async fn etf_sector_weightings(&self, symbol: String) -> PyResult> { + let query = etf_sector_weightings_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.etf_sector_weightings(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EtfSectorWeighting::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the latest US fund disclosures holding one requested asset. + #[pyo3(signature = (symbol))] + async fn latest_disclosure_holders( + &self, + symbol: String, + ) -> PyResult> { + let query = latest_disclosure_holders_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_fund_disclosure_holders(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(FundDisclosureHolder::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves positions from one US mutual-fund disclosure period. + #[pyo3(signature = (symbol, year, quarter, *, cik=None))] + async fn disclosures( + &self, + symbol: String, + year: i64, + quarter: i64, + cik: Option, + ) -> PyResult> { + let query = disclosures_query(&symbol, year, quarter, cik.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.fund_disclosures(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(FundDisclosure::from).collect()) + .map_err(to_py_error) + } + + /// Searches US mutual-fund and ETF disclosure holders by exact name text. + #[pyo3(signature = (name))] + async fn search_disclosure_holders( + &self, + name: String, + ) -> PyResult> { + let query = search_disclosure_holders_query(&name)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_fund_disclosure_holders(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(FundDisclosureSearchResult::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves available US fund-disclosure reporting dates. + #[pyo3(signature = (symbol, *, cik=None))] + async fn disclosure_dates( + &self, + symbol: String, + cik: Option, + ) -> PyResult> { + let query = disclosure_dates_query(&symbol, cik.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.fund_disclosure_dates(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(Form13fFilingDate::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `EtfHoldingsQuery` for `FundsNamespace::etf_holdings` from validated Python arguments. fn etf_holdings_query(symbol: &str) -> PyResult { let symbol = args::ticker("symbol", symbol)?; diff --git a/crates/fmp-py/src/namespaces/indexes.rs b/crates/fmp-py/src/namespaces/indexes.rs index 20b8a524..9c9fc103 100644 --- a/crates/fmp-py/src/namespaces/indexes.rs +++ b/crates/fmp-py/src/namespaces/indexes.rs @@ -12,7 +12,7 @@ use crate::errors::to_py_error; use crate::models::chart::{StockChartFullBar, StockChartIntradayBar, StockChartLightBar}; use crate::models::indexes::{HistoricalIndexConstituent, IndexConstituent, IndexListing}; use crate::models::quote::{Quote, QuoteShort}; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Indexes endpoints for a single client, exposed as `client.indexes`. #[gen_stub_pyclass] @@ -265,6 +265,217 @@ impl IndexesNamespace { } } +/// Indexes endpoints for a single async client, exposed as `client.indexes`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.indexes", frozen)] +pub(crate) struct AsyncIndexesNamespace { + builder: Arc, +} + +impl AsyncIndexesNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncIndexesNamespace { + /// Lists worldwide stock-market indexes. + async fn list(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.index_list().await + }) + .await?; + rows.map(|items| items.into_iter().map(IndexListing::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves a detailed quote for one stock-market index. + #[pyo3(signature = (symbol))] + async fn quote(&self, symbol: String) -> PyResult> { + let query = quote_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.index_quote(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(Quote::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves a compact quote for one stock-market index. + #[pyo3(signature = (symbol))] + async fn quote_short(&self, symbol: String) -> PyResult> { + let query = quote_short_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.index_quote_short(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(QuoteShort::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves compact end-of-day history for one stock-market index. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_light( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_light_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.index_chart_light(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartLightBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves detailed end-of-day history for one stock-market index. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_full( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_full_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.index_chart_full(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartFullBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves one-minute history for one stock-market index. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_one_minute( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_one_minute_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.index_chart_one_minute(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves five-minute history for one stock-market index. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_five_minutes( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_five_minutes_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.index_chart_five_minutes(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves one-hour history for one stock-market index. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn chart_one_hour( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = chart_one_hour_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.index_chart_one_hour(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockChartIntradayBar::from).collect()) + .map_err(to_py_error) + } + + /// Lists the current S&P 500 constituents. + async fn sp500_constituents(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.sp500_constituents().await + }) + .await?; + rows.map(|items| items.into_iter().map(IndexConstituent::from).collect()) + .map_err(to_py_error) + } + + /// Lists the current Nasdaq constituents. + async fn nasdaq_constituents(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.nasdaq_constituents().await + }) + .await?; + rows.map(|items| items.into_iter().map(IndexConstituent::from).collect()) + .map_err(to_py_error) + } + + /// Lists the current Dow Jones constituents. + async fn dow_jones_constituents(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.dow_jones_constituents().await + }) + .await?; + rows.map(|items| items.into_iter().map(IndexConstituent::from).collect()) + .map_err(to_py_error) + } + + /// Lists historical S&P 500 constituent changes. + async fn historical_sp500_constituents(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.historical_sp500_constituents().await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(HistoricalIndexConstituent::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Lists historical Nasdaq constituent changes. + async fn historical_nasdaq_constituents(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.historical_nasdaq_constituents().await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(HistoricalIndexConstituent::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Lists historical Dow Jones constituent changes. + async fn historical_dow_jones_constituents(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.historical_dow_jones_constituents().await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(HistoricalIndexConstituent::from) + .collect() + }) + .map_err(to_py_error) + } +} + /// Builds the `QuoteQuery` for `IndexesNamespace::quote` from validated Python arguments. fn quote_query(symbol: &str) -> PyResult { let symbol = args::ticker("symbol", symbol)?; diff --git a/crates/fmp-py/src/namespaces/insider_trading.rs b/crates/fmp-py/src/namespaces/insider_trading.rs index 3020d55d..8500735c 100644 --- a/crates/fmp-py/src/namespaces/insider_trading.rs +++ b/crates/fmp-py/src/namespaces/insider_trading.rs @@ -15,7 +15,7 @@ use crate::models::insider_trading::{ BeneficialOwnershipAcquisition, InsiderReportingName, InsiderTrade, InsiderTradeStatistics, InsiderTransactionType, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Insider trading endpoints for a single client, exposed as `client.insider_trading`. #[gen_stub_pyclass] @@ -167,6 +167,132 @@ impl InsiderTradingNamespace { } } +/// Insider trading endpoints for a single async client, exposed as `client.insider_trading`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.insider_trading", frozen)] +pub(crate) struct AsyncInsiderTradingNamespace { + builder: Arc, +} + +impl AsyncInsiderTradingNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncInsiderTradingNamespace { + /// Retrieves the latest US insider trades with optional date and pagination filters. + #[pyo3(signature = (*, date=None, page=None, limit=None))] + async fn latest_trades( + &self, + date: Option, + page: Option, + limit: Option, + ) -> PyResult> { + let query = latest_trades_query(date, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_insider_trades(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(InsiderTrade::from).collect()) + .map_err(to_py_error) + } + + /// Searches US insider trades using independently optional filters. + #[pyo3(signature = (*, symbol=None, page=None, limit=None, reporting_cik=None, company_cik=None, transaction_type=None))] + async fn search_trades( + &self, + symbol: Option, + page: Option, + limit: Option, + reporting_cik: Option, + company_cik: Option, + transaction_type: Option, + ) -> PyResult> { + let query = search_trades_query( + symbol.as_deref(), + page, + limit, + reporting_cik.as_deref(), + company_cik.as_deref(), + transaction_type.as_deref(), + )?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_insider_trades(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(InsiderTrade::from).collect()) + .map_err(to_py_error) + } + + /// Searches US insider-reporting identities by name. + #[pyo3(signature = (name))] + async fn search_reporting_names(&self, name: String) -> PyResult> { + let query = search_reporting_names_query(&name)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_insider_reporting_names(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(InsiderReportingName::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the US insider transaction-type taxonomy. + async fn transaction_types(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.insider_transaction_types().await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(InsiderTransactionType::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves quarterly US insider-trade statistics for one ticker. + #[pyo3(signature = (symbol))] + async fn trade_statistics(&self, symbol: String) -> PyResult> { + let query = trade_statistics_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.insider_trade_statistics(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(InsiderTradeStatistics::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves US beneficial-ownership acquisition filings for one ticker. + #[pyo3(signature = (symbol, *, limit=None))] + async fn beneficial_ownership_acquisitions( + &self, + symbol: String, + limit: Option, + ) -> PyResult> { + let query = beneficial_ownership_acquisitions_query(&symbol, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.beneficial_ownership_acquisitions(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(BeneficialOwnershipAcquisition::from) + .collect() + }) + .map_err(to_py_error) + } +} + /// Builds the `LatestInsiderTradesQuery` for `InsiderTradingNamespace::latest_trades` from validated Python arguments. fn latest_trades_query( date: Option, diff --git a/crates/fmp-py/src/namespaces/institutional_ownership.rs b/crates/fmp-py/src/namespaces/institutional_ownership.rs index 0a23fea0..21aa4814 100644 --- a/crates/fmp-py/src/namespaces/institutional_ownership.rs +++ b/crates/fmp-py/src/namespaces/institutional_ownership.rs @@ -18,7 +18,7 @@ use crate::models::institutional_ownership::{ InstitutionalHolderAnalytics, InstitutionalHolding, InstitutionalIndustrySummary, InstitutionalOwnershipFiling, InstitutionalPositionSummary, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Institutional ownership endpoints for a single client, exposed as `client.institutional_ownership`. #[gen_stub_pyclass] @@ -220,6 +220,183 @@ impl InstitutionalOwnershipNamespace { } } +/// Institutional ownership endpoints for a single async client, exposed as `client.institutional_ownership`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.institutional_ownership", frozen)] +pub(crate) struct AsyncInstitutionalOwnershipNamespace { + builder: Arc, +} + +impl AsyncInstitutionalOwnershipNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncInstitutionalOwnershipNamespace { + /// Retrieves the latest US institutional-ownership filings. + #[pyo3(signature = (*, page=None, limit=None))] + async fn latest_filings( + &self, + page: Option, + limit: Option, + ) -> PyResult> { + let query = latest_filings_query(page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_institutional_ownership_filings(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(InstitutionalOwnershipFiling::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Extracts the positions from one US institutional-ownership filing period. + #[pyo3(signature = (cik, year, quarter))] + async fn extract( + &self, + cik: String, + year: i64, + quarter: i64, + ) -> PyResult> { + let query = extract_query(&cik, year, quarter)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.institutional_ownership_extract(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(InstitutionalHolding::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves available Form 13F filing dates for one US institutional holder. + #[pyo3(signature = (cik))] + async fn form_13f_filing_dates(&self, cik: String) -> PyResult> { + let query = form_13f_filing_dates_query(&cik)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.form_13f_filing_dates(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(Form13fFilingDate::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves holder-level analytics for one US security and filing period. + #[pyo3(signature = (symbol, year, quarter, *, page=None, limit=None))] + async fn holder_analytics( + &self, + symbol: String, + year: i64, + quarter: i64, + page: Option, + limit: Option, + ) -> PyResult> { + let query = holder_analytics_query(&symbol, year, quarter, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.institutional_holder_analytics(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(InstitutionalHolderAnalytics::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves a US institutional holder's portfolio performance summary. + #[pyo3(signature = (cik, *, page=None))] + async fn holder_performance_summary( + &self, + cik: String, + page: Option, + ) -> PyResult> { + let query = holder_performance_summary_query(&cik, page)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.holder_performance_summary(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(HolderPerformanceSummary::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves a US institutional holder's industry breakdown for one period. + #[pyo3(signature = (cik, year, quarter))] + async fn holder_industry_breakdown( + &self, + cik: String, + year: i64, + quarter: i64, + ) -> PyResult> { + let query = holder_industry_breakdown_query(&cik, year, quarter)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.holder_industry_breakdown(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(HolderIndustryBreakdown::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves the US institutional position summary for one security and period. + #[pyo3(signature = (symbol, year, quarter))] + async fn positions_summary( + &self, + symbol: String, + year: i64, + quarter: i64, + ) -> PyResult> { + let query = positions_summary_query(&symbol, year, quarter)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.institutional_positions_summary(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(InstitutionalPositionSummary::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves US institutional industry values for one reporting period. + #[pyo3(signature = (year, quarter))] + async fn industry_summary( + &self, + year: i64, + quarter: i64, + ) -> PyResult> { + let query = industry_summary_query(year, quarter)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.institutional_industry_summary(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(InstitutionalIndustrySummary::from) + .collect() + }) + .map_err(to_py_error) + } +} + /// Builds the `LatestInstitutionalOwnershipFilingsQuery` for `InstitutionalOwnershipNamespace::latest_filings` from validated Python arguments. fn latest_filings_query( page: Option, diff --git a/crates/fmp-py/src/namespaces/market.rs b/crates/fmp-py/src/namespaces/market.rs index 3c95375f..1c565fed 100644 --- a/crates/fmp-py/src/namespaces/market.rs +++ b/crates/fmp-py/src/namespaces/market.rs @@ -15,7 +15,7 @@ use crate::errors::to_py_error; use crate::models::market::{ IndustryPe, IndustryPerformance, MarketMover, SectorPe, SectorPerformance, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Market endpoints for a single client, exposed as `client.market`. #[gen_stub_pyclass] @@ -234,6 +234,196 @@ impl MarketNamespace { } } +/// Market endpoints for a single async client, exposed as `client.market`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.market", frozen)] +pub(crate) struct AsyncMarketNamespace { + builder: Arc, +} + +impl AsyncMarketNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncMarketNamespace { + /// Retrieves a worldwide dated sector-performance snapshot. + #[pyo3(signature = (date, *, exchange=None, sector=None))] + async fn sector_performance_snapshot( + &self, + date: args::DateArg, + exchange: Option, + sector: Option, + ) -> PyResult> { + let query = + sector_performance_snapshot_query(date, exchange.as_deref(), sector.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.sector_performance_snapshot(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(SectorPerformance::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves a worldwide dated industry-performance snapshot. + #[pyo3(signature = (date, *, exchange=None, industry=None))] + async fn industry_performance_snapshot( + &self, + date: args::DateArg, + exchange: Option, + industry: Option, + ) -> PyResult> { + let query = + industry_performance_snapshot_query(date, exchange.as_deref(), industry.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.industry_performance_snapshot(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(IndustryPerformance::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves a worldwide dated sector P/E snapshot. + #[pyo3(signature = (date, *, exchange=None, sector=None))] + async fn sector_pe_snapshot( + &self, + date: args::DateArg, + exchange: Option, + sector: Option, + ) -> PyResult> { + let query = sector_pe_snapshot_query(date, exchange.as_deref(), sector.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.sector_pe_snapshot(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(SectorPe::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves a worldwide dated industry P/E snapshot. + #[pyo3(signature = (date, *, exchange=None, industry=None))] + async fn industry_pe_snapshot( + &self, + date: args::DateArg, + exchange: Option, + industry: Option, + ) -> PyResult> { + let query = industry_pe_snapshot_query(date, exchange.as_deref(), industry.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.industry_pe_snapshot(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(IndustryPe::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide historical sector performance. + #[pyo3(signature = (sector, *, from_=None, exchange=None, to=None))] + async fn historical_sector_performance( + &self, + sector: String, + from_: Option, + exchange: Option, + to: Option, + ) -> PyResult> { + let query = historical_sector_performance_query(§or, from_, exchange.as_deref(), to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.historical_sector_performance(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(SectorPerformance::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide historical industry performance. + #[pyo3(signature = (industry, *, exchange=None, from_=None, to=None))] + async fn historical_industry_performance( + &self, + industry: String, + exchange: Option, + from_: Option, + to: Option, + ) -> PyResult> { + let query = + historical_industry_performance_query(&industry, exchange.as_deref(), from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.historical_industry_performance(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(IndustryPerformance::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide historical sector P/E ratios. + #[pyo3(signature = (sector, *, from_=None, exchange=None, to=None))] + async fn historical_sector_pe( + &self, + sector: String, + from_: Option, + exchange: Option, + to: Option, + ) -> PyResult> { + let query = historical_sector_pe_query(§or, from_, exchange.as_deref(), to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.historical_sector_pe(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(SectorPe::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide historical industry P/E ratios. + #[pyo3(signature = (industry, *, exchange=None, from_=None, to=None))] + async fn historical_industry_pe( + &self, + industry: String, + exchange: Option, + from_: Option, + to: Option, + ) -> PyResult> { + let query = historical_industry_pe_query(&industry, exchange.as_deref(), from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.historical_industry_pe(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(IndustryPe::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the US-only list of biggest stock gainers. + async fn biggest_gainers(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.biggest_gainers().await + }) + .await?; + rows.map(|items| items.into_iter().map(MarketMover::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the US-only list of biggest stock losers. + async fn biggest_losers(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.biggest_losers().await + }) + .await?; + rows.map(|items| items.into_iter().map(MarketMover::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the US-only list of most actively traded stocks. + async fn most_actives(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.most_actives().await + }) + .await?; + rows.map(|items| items.into_iter().map(MarketMover::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `SectorPerformanceSnapshotQuery` for `MarketNamespace::sector_performance_snapshot` from validated Python arguments. fn sector_performance_snapshot_query( date: args::DateArg, diff --git a/crates/fmp-py/src/namespaces/market_hours.rs b/crates/fmp-py/src/namespaces/market_hours.rs index 58f8b80e..faa68569 100644 --- a/crates/fmp-py/src/namespaces/market_hours.rs +++ b/crates/fmp-py/src/namespaces/market_hours.rs @@ -11,7 +11,7 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::args; use crate::errors::to_py_error; use crate::models::market_hours::{ExchangeHoliday, ExchangeMarketHours}; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Market hours endpoints for a single client, exposed as `client.market_hours`. #[gen_stub_pyclass] @@ -87,6 +87,68 @@ impl MarketHoursNamespace { } } +/// Market hours endpoints for a single async client, exposed as `client.market_hours`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.market_hours", frozen)] +pub(crate) struct AsyncMarketHoursNamespace { + builder: Arc, +} + +impl AsyncMarketHoursNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncMarketHoursNamespace { + /// Retrieves trading hours for one exchange. + #[pyo3(signature = (exchange, *, timestamp=None))] + async fn exchange( + &self, + exchange: String, + timestamp: Option, + ) -> PyResult> { + let query = exchange_query(&exchange, timestamp.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.exchange_market_hours(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(ExchangeMarketHours::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves holidays for one exchange. + #[pyo3(signature = (exchange, *, from_=None, to=None))] + async fn holidays( + &self, + exchange: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = holidays_query(&exchange, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.holidays_by_exchange(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(ExchangeHoliday::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves trading hours for all exchanges. + #[pyo3(signature = (*, timestamp=None))] + async fn all_exchanges(&self, timestamp: Option) -> PyResult> { + let query = all_exchanges_query(timestamp.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.all_exchange_market_hours(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(ExchangeMarketHours::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `ExchangeMarketHoursQuery` for `MarketHoursNamespace::exchange` from validated Python arguments. fn exchange_query(exchange: &str, timestamp: Option<&str>) -> PyResult { let exchange = args::exchange_code("exchange", exchange)?; diff --git a/crates/fmp-py/src/namespaces/news.rs b/crates/fmp-py/src/namespaces/news.rs index 8c5ecbe0..5d180941 100644 --- a/crates/fmp-py/src/namespaces/news.rs +++ b/crates/fmp-py/src/namespaces/news.rs @@ -13,7 +13,7 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::args; use crate::errors::to_py_error; use crate::models::news::{Article, NewsArticle}; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// News endpoints for a single client, exposed as `client.news`. #[gen_stub_pyclass] @@ -245,6 +245,201 @@ impl NewsNamespace { } } +/// News endpoints for a single async client, exposed as `client.news`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.news", frozen)] +pub(crate) struct AsyncNewsNamespace { + builder: Arc, +} + +impl AsyncNewsNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncNewsNamespace { + /// Retrieves US-authored Financial Modeling Prep articles. + #[pyo3(signature = (*, page=None, limit=None))] + async fn articles(&self, page: Option, limit: Option) -> PyResult> { + let query = articles_query(page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.articles(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(Article::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the latest worldwide general-news articles. + #[pyo3(signature = (*, from_=None, to=None, page=None, limit=None))] + async fn latest_general_news( + &self, + from_: Option, + to: Option, + page: Option, + limit: Option, + ) -> PyResult> { + let query = latest_general_news_query(from_, to, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_general_news(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(NewsArticle::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the latest US press releases. + #[pyo3(signature = (*, from_=None, to=None, page=None, limit=None))] + async fn latest_press_releases( + &self, + from_: Option, + to: Option, + page: Option, + limit: Option, + ) -> PyResult> { + let query = latest_press_releases_query(from_, to, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_press_releases(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(NewsArticle::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the latest stock-news articles. + #[pyo3(signature = (*, from_=None, to=None, page=None, limit=None))] + async fn latest_stock_news( + &self, + from_: Option, + to: Option, + page: Option, + limit: Option, + ) -> PyResult> { + let query = latest_stock_news_query(from_, to, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_stock_news(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(NewsArticle::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the latest cryptocurrency-news articles. + #[pyo3(signature = (*, from_=None, to=None, page=None, limit=None))] + async fn latest_crypto_news( + &self, + from_: Option, + to: Option, + page: Option, + limit: Option, + ) -> PyResult> { + let query = latest_crypto_news_query(from_, to, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_crypto_news(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(NewsArticle::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the latest foreign-exchange-news articles. + #[pyo3(signature = (*, from_=None, to=None, page=None, limit=None))] + async fn latest_forex_news( + &self, + from_: Option, + to: Option, + page: Option, + limit: Option, + ) -> PyResult> { + let query = latest_forex_news_query(from_, to, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_forex_news(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(NewsArticle::from).collect()) + .map_err(to_py_error) + } + + /// Searches US press releases for an ordered non-empty ticker list. + #[pyo3(signature = (symbols, *, from_=None, to=None, page=None, limit=None))] + async fn search_press_releases( + &self, + symbols: args::SymbolsArg, + from_: Option, + to: Option, + page: Option, + limit: Option, + ) -> PyResult> { + let query = search_press_releases_query(symbols, from_, to, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_press_releases(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(NewsArticle::from).collect()) + .map_err(to_py_error) + } + + /// Searches stock news for an ordered non-empty ticker list. + #[pyo3(signature = (symbols, *, from_=None, to=None, page=None, limit=None))] + async fn search_stock_news( + &self, + symbols: args::SymbolsArg, + from_: Option, + to: Option, + page: Option, + limit: Option, + ) -> PyResult> { + let query = search_stock_news_query(symbols, from_, to, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_stock_news(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(NewsArticle::from).collect()) + .map_err(to_py_error) + } + + /// Searches cryptocurrency news for an ordered non-empty ticker list. + #[pyo3(signature = (symbols, *, from_=None, to=None, page=None, limit=None))] + async fn search_crypto_news( + &self, + symbols: args::SymbolsArg, + from_: Option, + to: Option, + page: Option, + limit: Option, + ) -> PyResult> { + let query = search_crypto_news_query(symbols, from_, to, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_crypto_news(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(NewsArticle::from).collect()) + .map_err(to_py_error) + } + + /// Searches foreign-exchange news for an ordered non-empty ticker list. + #[pyo3(signature = (symbols, *, from_=None, to=None, page=None, limit=None))] + async fn search_forex_news( + &self, + symbols: args::SymbolsArg, + from_: Option, + to: Option, + page: Option, + limit: Option, + ) -> PyResult> { + let query = search_forex_news_query(symbols, from_, to, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_forex_news(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(NewsArticle::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `ArticlesQuery` for `NewsNamespace::articles` from validated Python arguments. fn articles_query(page: Option, limit: Option) -> PyResult { let page = args::optional("page", page, args::page)?; diff --git a/crates/fmp-py/src/namespaces/quote.rs b/crates/fmp-py/src/namespaces/quote.rs index 188824bc..6f38949f 100644 --- a/crates/fmp-py/src/namespaces/quote.rs +++ b/crates/fmp-py/src/namespaces/quote.rs @@ -15,7 +15,7 @@ use crate::errors::to_py_error; use crate::models::quote::{ AftermarketQuote, AftermarketTrade, Quote, QuoteShort, StockPriceChange, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Quote endpoints for a single client, exposed as `client.quote`. #[gen_stub_pyclass] @@ -247,6 +247,209 @@ impl QuoteNamespace { } } +/// Quote endpoints for a single async client, exposed as `client.quote`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.quote", frozen)] +pub(crate) struct AsyncQuoteNamespace { + builder: Arc, +} + +impl AsyncQuoteNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncQuoteNamespace { + /// The full quote for a symbol. + #[pyo3(signature = (symbol))] + async fn full(&self, symbol: String) -> PyResult> { + let query = full_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.quote(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(Quote::from).collect()) + .map_err(to_py_error) + } + + /// The compact quote array for a symbol. + #[pyo3(signature = (symbol))] + async fn short(&self, symbol: String) -> PyResult> { + let query = short_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.quote_short(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(QuoteShort::from).collect()) + .map_err(to_py_error) + } + + /// Compact quotes for every mutual fund, without a query. + async fn mutual_funds(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.mutual_fund_quotes().await + }) + .await?; + rows.map(|items| items.into_iter().map(QuoteShort::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves US aftermarket trades for one ticker. + #[pyo3(signature = (symbol))] + async fn aftermarket_trade(&self, symbol: String) -> PyResult> { + let query = aftermarket_trade_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.aftermarket_trade(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(AftermarketTrade::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves US aftermarket bid-and-ask quotes for one ticker. + #[pyo3(signature = (symbol))] + async fn aftermarket_quote(&self, symbol: String) -> PyResult> { + let query = aftermarket_quote_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.aftermarket_quote(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(AftermarketQuote::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide percentage price changes for one stock. + #[pyo3(signature = (symbol))] + async fn stock_price_change(&self, symbol: String) -> PyResult> { + let query = stock_price_change_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.stock_price_change(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StockPriceChange::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves detailed worldwide stock quotes for multiple tickers. + #[pyo3(signature = (symbols))] + async fn batch(&self, symbols: args::SymbolsArg) -> PyResult> { + let query = batch_query(symbols)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.batch_quote(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(Quote::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves compact worldwide stock quotes for multiple tickers. + #[pyo3(signature = (symbols))] + async fn batch_short(&self, symbols: args::SymbolsArg) -> PyResult> { + let query = batch_short_query(symbols)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.batch_quote_short(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(QuoteShort::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves US aftermarket trades for multiple tickers. + #[pyo3(signature = (symbols))] + async fn batch_aftermarket_trade( + &self, + symbols: args::SymbolsArg, + ) -> PyResult> { + let query = batch_aftermarket_trade_query(symbols)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.batch_aftermarket_trade(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(AftermarketTrade::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves US aftermarket bid-and-ask quotes for multiple tickers. + #[pyo3(signature = (symbols))] + async fn batch_aftermarket_quote( + &self, + symbols: args::SymbolsArg, + ) -> PyResult> { + let query = batch_aftermarket_quote_query(symbols)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.batch_aftermarket_quote(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(AftermarketQuote::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves compact quotes for every stock on one exchange. + #[pyo3(signature = (exchange))] + async fn exchange(&self, exchange: String) -> PyResult> { + let query = exchange_query(&exchange)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.exchange_quotes(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(QuoteShort::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves compact quotes for the documented ETF universe. + async fn etfs(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.etf_quotes().await + }) + .await?; + rows.map(|items| items.into_iter().map(QuoteShort::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves compact quotes for the documented commodity universe. + async fn commodities(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.commodity_quotes().await + }) + .await?; + rows.map(|items| items.into_iter().map(QuoteShort::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves compact quotes for the documented cryptocurrency universe. + async fn cryptocurrencies(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.cryptocurrency_quotes().await + }) + .await?; + rows.map(|items| items.into_iter().map(QuoteShort::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves compact quotes for the documented forex universe. + async fn forex(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.forex_quotes().await + }) + .await?; + rows.map(|items| items.into_iter().map(QuoteShort::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves compact quotes for the documented index universe. + async fn indexes(&self) -> PyResult> { + let rows = spawn(self.builder.clone(), |client| async move { + client.index_quotes().await + }) + .await?; + rows.map(|items| items.into_iter().map(QuoteShort::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `QuoteQuery` for `QuoteNamespace::full` from validated Python arguments. fn full_query(symbol: &str) -> PyResult { let symbol = args::ticker("symbol", symbol)?; diff --git a/crates/fmp-py/src/namespaces/screener.rs b/crates/fmp-py/src/namespaces/screener.rs index f03bf3d3..23b28c03 100644 --- a/crates/fmp-py/src/namespaces/screener.rs +++ b/crates/fmp-py/src/namespaces/screener.rs @@ -9,7 +9,7 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::args; use crate::errors::to_py_error; use crate::models::screener::CompanyScreenerResult; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Screener endpoints for a single client, exposed as `client.screener`. #[gen_stub_pyclass] @@ -87,6 +87,79 @@ impl ScreenerNamespace { } } +/// Screener endpoints for a single async client, exposed as `client.screener`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.screener", frozen)] +pub(crate) struct AsyncScreenerNamespace { + builder: Arc, +} + +impl AsyncScreenerNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncScreenerNamespace { + /// Screens worldwide companies using the supplied optional filters. + #[pyo3(signature = (*, market_cap_more_than=None, market_cap_lower_than=None, sector=None, industry=None, beta_more_than=None, beta_lower_than=None, price_more_than=None, price_lower_than=None, dividend_more_than=None, dividend_lower_than=None, volume_more_than=None, volume_lower_than=None, exchange=None, country=None, is_etf=None, is_fund=None, is_actively_trading=None, page=None, limit=None, include_all_share_classes=None))] + #[allow(clippy::too_many_arguments)] + async fn companies( + &self, + market_cap_more_than: Option, + market_cap_lower_than: Option, + sector: Option, + industry: Option, + beta_more_than: Option, + beta_lower_than: Option, + price_more_than: Option, + price_lower_than: Option, + dividend_more_than: Option, + dividend_lower_than: Option, + volume_more_than: Option, + volume_lower_than: Option, + exchange: Option, + country: Option, + is_etf: Option, + is_fund: Option, + is_actively_trading: Option, + page: Option, + limit: Option, + include_all_share_classes: Option, + ) -> PyResult> { + let query = companies_query( + market_cap_more_than, + market_cap_lower_than, + sector.as_deref(), + industry.as_deref(), + beta_more_than, + beta_lower_than, + price_more_than, + price_lower_than, + dividend_more_than, + dividend_lower_than, + volume_more_than, + volume_lower_than, + exchange.as_deref(), + country.as_deref(), + is_etf, + is_fund, + is_actively_trading, + page, + limit, + include_all_share_classes, + )?; + let rows = spawn(self.builder.clone(), |client| async move { + client.company_screener(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CompanyScreenerResult::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `CompanyScreenerQuery` for `ScreenerNamespace::companies` from validated Python arguments. #[allow(clippy::too_many_arguments)] fn companies_query( diff --git a/crates/fmp-py/src/namespaces/search.rs b/crates/fmp-py/src/namespaces/search.rs index 8a42da3a..c4d5f79d 100644 --- a/crates/fmp-py/src/namespaces/search.rs +++ b/crates/fmp-py/src/namespaces/search.rs @@ -15,7 +15,7 @@ use crate::models::search::{ CikSearchResult, CusipSearchResult, ExchangeVariant, IsinSearchResult, NameSearchResult, SymbolSearchResult, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Search endpoints for a single client, exposed as `client.search`. #[gen_stub_pyclass] @@ -134,6 +134,105 @@ impl SearchNamespace { } } +/// Search endpoints for a single async client, exposed as `client.search`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.search", frozen)] +pub(crate) struct AsyncSearchNamespace { + builder: Arc, +} + +impl AsyncSearchNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncSearchNamespace { + /// Searches worldwide companies and instruments by symbol. + #[pyo3(signature = (query, *, limit=None, exchange=None))] + async fn symbol( + &self, + query: String, + limit: Option, + exchange: Option, + ) -> PyResult> { + let query = symbol_query(&query, limit, exchange.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_symbol(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(SymbolSearchResult::from).collect()) + .map_err(to_py_error) + } + + /// Searches worldwide companies and instruments by name. + #[pyo3(signature = (query, *, limit=None, exchange=None))] + async fn name( + &self, + query: String, + limit: Option, + exchange: Option, + ) -> PyResult> { + let query = name_query(&query, limit, exchange.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_name(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(NameSearchResult::from).collect()) + .map_err(to_py_error) + } + + /// Searches US companies by Central Index Key. + #[pyo3(signature = (cik, *, limit=None))] + async fn cik(&self, cik: String, limit: Option) -> PyResult> { + let query = cik_query(&cik, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_cik(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CikSearchResult::from).collect()) + .map_err(to_py_error) + } + + /// Searches worldwide securities by CUSIP without constraining its length. + #[pyo3(signature = (cusip))] + async fn cusip(&self, cusip: String) -> PyResult> { + let query = cusip_query(&cusip)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_cusip(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CusipSearchResult::from).collect()) + .map_err(to_py_error) + } + + /// Searches worldwide securities by ISIN without constraining its length. + #[pyo3(signature = (isin))] + async fn isin(&self, isin: String) -> PyResult> { + let query = isin_query(&isin)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_isin(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(IsinSearchResult::from).collect()) + .map_err(to_py_error) + } + + /// Finds worldwide exchange listings for a provider ticker. + #[pyo3(signature = (symbol))] + async fn exchange_variants(&self, symbol: String) -> PyResult> { + let query = exchange_variants_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_exchange_variants(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(ExchangeVariant::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `SymbolSearchQuery` for `SearchNamespace::symbol` from validated Python arguments. fn symbol_query( query: &str, diff --git a/crates/fmp-py/src/namespaces/sec_filings.rs b/crates/fmp-py/src/namespaces/sec_filings.rs index 6fc4016a..0195d3fa 100644 --- a/crates/fmp-py/src/namespaces/sec_filings.rs +++ b/crates/fmp-py/src/namespaces/sec_filings.rs @@ -18,7 +18,7 @@ use crate::errors::to_py_error; use crate::models::sec_filings::{ SecCompanyProfile, SecCompanySearchResult, SecFiling, SicClassification, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Sec filings endpoints for a single client, exposed as `client.sec_filings`. #[gen_stub_pyclass] @@ -299,6 +299,252 @@ impl SecFilingsNamespace { } } +/// Sec filings endpoints for a single async client, exposed as `client.sec_filings`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.sec_filings", frozen)] +pub(crate) struct AsyncSecFilingsNamespace { + builder: Arc, +} + +impl AsyncSecFilingsNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncSecFilingsNamespace { + /// Retrieves the latest US 8-K SEC filings within the required date range. + #[pyo3(signature = (from_, to, *, page=None, limit=None))] + async fn latest_8k( + &self, + from_: args::DateArg, + to: args::DateArg, + page: Option, + limit: Option, + ) -> PyResult> { + let query = latest_8k_query(from_, to, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_8k_sec_filings(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(SecFiling::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the latest US financial SEC filings within the required date range. + #[pyo3(signature = (from_, to, *, page=None, limit=None))] + async fn latest( + &self, + from_: args::DateArg, + to: args::DateArg, + page: Option, + limit: Option, + ) -> PyResult> { + let query = latest_query(from_, to, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_sec_filings(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(SecFiling::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves US SEC filings by their open form type. + #[pyo3(signature = (form_type, from_, to, *, page=None, limit=None))] + async fn by_form_type( + &self, + form_type: String, + from_: args::DateArg, + to: args::DateArg, + page: Option, + limit: Option, + ) -> PyResult> { + let query = by_form_type_query(&form_type, from_, to, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.sec_filings_by_form_type(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(SecFiling::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves US SEC filings for a ticker. + #[pyo3(signature = (symbol, from_, to, *, page=None, limit=None))] + async fn by_symbol( + &self, + symbol: String, + from_: args::DateArg, + to: args::DateArg, + page: Option, + limit: Option, + ) -> PyResult> { + let query = by_symbol_query(&symbol, from_, to, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.sec_filings_by_symbol(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(SecFiling::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves US SEC filings for a string-backed CIK. + #[pyo3(signature = (cik, from_, to, *, page=None, limit=None))] + async fn by_cik( + &self, + cik: String, + from_: args::DateArg, + to: args::DateArg, + page: Option, + limit: Option, + ) -> PyResult> { + let query = by_cik_query(&cik, from_, to, page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.sec_filings_by_cik(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(SecFiling::from).collect()) + .map_err(to_py_error) + } + + /// Searches US SEC companies by company-name text. + #[pyo3(signature = (company))] + async fn search_companies_by_name( + &self, + company: String, + ) -> PyResult> { + let query = search_companies_by_name_query(&company)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_sec_companies_by_name(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(SecCompanySearchResult::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Searches US SEC companies by ticker. + #[pyo3(signature = (symbol))] + async fn search_companies_by_symbol( + &self, + symbol: String, + ) -> PyResult> { + let query = search_companies_by_symbol_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_sec_companies_by_symbol(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(SecCompanySearchResult::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Searches US SEC companies by string-backed CIK. + #[pyo3(signature = (cik))] + async fn search_companies_by_cik(&self, cik: String) -> PyResult> { + let query = search_companies_by_cik_query(&cik)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_sec_companies_by_cik(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(SecCompanySearchResult::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves the full US SEC company profile for a ticker. + #[pyo3(signature = (symbol, *, cik_a=None))] + async fn company_profile( + &self, + symbol: String, + cik_a: Option, + ) -> PyResult> { + let query = company_profile_query(&symbol, cik_a.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.sec_company_profile(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(SecCompanyProfile::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the US Standard Industrial Classification directory. + #[pyo3(signature = (*, industry_title=None, sic_code=None))] + async fn industry_classifications( + &self, + industry_title: Option, + sic_code: Option, + ) -> PyResult> { + let query = industry_classifications_query(industry_title.as_deref(), sic_code.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.industry_classifications(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(SicClassification::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves the paginated US company industry-classification feed. + #[pyo3(signature = (*, page=None, limit=None))] + async fn all_industry_classifications( + &self, + page: Option, + limit: Option, + ) -> PyResult> { + let query = all_industry_classifications_query(page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.all_industry_classifications(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(SecCompanySearchResult::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Searches US industry classifications while preserving raw documented rows. + #[pyo3(signature = (*, symbol=None, cik=None, sic_code=None))] + #[gen_stub(override_return_type(type_repr = "list[dict[str, typing.Any]]", imports = ("typing")))] + async fn search_industry_classifications( + &self, + symbol: Option, + cik: Option, + sic_code: Option, + ) -> PyResult>> { + let query = search_industry_classifications_query( + symbol.as_deref(), + cik.as_deref(), + sic_code.as_deref(), + )?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_industry_classifications(query).await + }) + .await?; + Python::attach(|py| { + rows.map_err(to_py_error)? + .iter() + .map(|row| convert::dynamic_object_to_py(py, row).map(Bound::unbind)) + .collect() + }) + } +} + /// Builds the `Latest8kSecFilingsQuery` for `SecFilingsNamespace::latest_8k` from validated Python arguments. fn latest_8k_query( from_: args::DateArg, diff --git a/crates/fmp-py/src/namespaces/statements/as_reported.rs b/crates/fmp-py/src/namespaces/statements/as_reported.rs index f62ec2e6..f5dfce04 100644 --- a/crates/fmp-py/src/namespaces/statements/as_reported.rs +++ b/crates/fmp-py/src/namespaces/statements/as_reported.rs @@ -12,7 +12,7 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::args; use crate::errors::to_py_error; use crate::models::statements::as_reported::AsReportedFinancialStatement; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Statements as reported endpoints for a single client, exposed as `client.statements.as_reported`. #[gen_stub_pyclass] @@ -135,6 +135,115 @@ impl StatementsAsReportedNamespace { } } +/// Statements as reported endpoints for a single async client, exposed as `client.statements.as_reported`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.statements.as_reported", frozen)] +pub(crate) struct AsyncStatementsAsReportedNamespace { + builder: Arc, +} + +impl AsyncStatementsAsReportedNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncStatementsAsReportedNamespace { + /// Retrieves income statements as reported by one company. + #[pyo3(signature = (symbol, *, limit=None, period=None))] + async fn income( + &self, + symbol: String, + limit: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + ) -> PyResult> { + let query = income_query(&symbol, limit, period.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.income_statement_as_reported(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(AsReportedFinancialStatement::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves balance sheets as reported by one company. + #[pyo3(signature = (symbol, *, limit=None, period=None))] + async fn balance_sheet( + &self, + symbol: String, + limit: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + ) -> PyResult> { + let query = balance_sheet_query(&symbol, limit, period.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.balance_sheet_statement_as_reported(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(AsReportedFinancialStatement::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves cash-flow statements as reported by one company. + #[pyo3(signature = (symbol, *, limit=None, period=None))] + async fn cash_flow( + &self, + symbol: String, + limit: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + ) -> PyResult> { + let query = cash_flow_query(&symbol, limit, period.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.cash_flow_statement_as_reported(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(AsReportedFinancialStatement::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves complete financial statements as reported by one company. + #[pyo3(signature = (symbol, *, limit=None, period=None))] + async fn full( + &self, + symbol: String, + limit: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + ) -> PyResult> { + let query = full_query(&symbol, limit, period.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.financial_statement_full_as_reported(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(AsReportedFinancialStatement::from) + .collect() + }) + .map_err(to_py_error) + } +} + /// Builds the `IncomeStatementAsReportedQuery` for `StatementsAsReportedNamespace::income` from validated Python arguments. fn income_query( symbol: &str, diff --git a/crates/fmp-py/src/namespaces/statements/balance.rs b/crates/fmp-py/src/namespaces/statements/balance.rs index 012c6c0a..47ca614e 100644 --- a/crates/fmp-py/src/namespaces/statements/balance.rs +++ b/crates/fmp-py/src/namespaces/statements/balance.rs @@ -9,7 +9,7 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::args; use crate::errors::to_py_error; use crate::models::statements::balance::{BalanceSheetStatement, BalanceSheetStatementTtm}; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Statements balance endpoints for a single client, exposed as `client.statements.balance`. #[gen_stub_pyclass] @@ -73,6 +73,62 @@ impl StatementsBalanceNamespace { } } +/// Statements balance endpoints for a single async client, exposed as `client.statements.balance`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.statements.balance", frozen)] +pub(crate) struct AsyncStatementsBalanceNamespace { + builder: Arc, +} + +impl AsyncStatementsBalanceNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncStatementsBalanceNamespace { + /// Retrieves historical worldwide balance sheets for one company. + #[pyo3(signature = (symbol, *, limit=None, period=None))] + async fn statement( + &self, + symbol: String, + limit: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\", \"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + ) -> PyResult> { + let query = statement_query(&symbol, limit, period.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.balance_sheet_statement(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(BalanceSheetStatement::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves trailing-twelve-month worldwide balance sheets for one company. + #[pyo3(signature = (symbol, *, limit=None))] + async fn statement_ttm( + &self, + symbol: String, + limit: Option, + ) -> PyResult> { + let query = statement_ttm_query(&symbol, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.balance_sheet_statement_ttm(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(BalanceSheetStatementTtm::from) + .collect() + }) + .map_err(to_py_error) + } +} + /// Builds the `BalanceSheetStatementQuery` for `StatementsBalanceNamespace::statement` from validated Python arguments. fn statement_query( symbol: &str, diff --git a/crates/fmp-py/src/namespaces/statements/cash_flow.rs b/crates/fmp-py/src/namespaces/statements/cash_flow.rs index 71ffcb5f..9374abb0 100644 --- a/crates/fmp-py/src/namespaces/statements/cash_flow.rs +++ b/crates/fmp-py/src/namespaces/statements/cash_flow.rs @@ -9,7 +9,7 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::args; use crate::errors::to_py_error; use crate::models::statements::cash_flow::CashFlowStatement; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Statements cash flow endpoints for a single client, exposed as `client.statements.cash_flow`. #[gen_stub_pyclass] @@ -68,6 +68,57 @@ impl StatementsCashFlowNamespace { } } +/// Statements cash flow endpoints for a single async client, exposed as `client.statements.cash_flow`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.statements.cash_flow", frozen)] +pub(crate) struct AsyncStatementsCashFlowNamespace { + builder: Arc, +} + +impl AsyncStatementsCashFlowNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncStatementsCashFlowNamespace { + /// Retrieves historical worldwide cash-flow statements for one company. + #[pyo3(signature = (symbol, *, limit=None, period=None))] + async fn statement( + &self, + symbol: String, + limit: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\", \"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + ) -> PyResult> { + let query = statement_query(&symbol, limit, period.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.cash_flow_statement(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CashFlowStatement::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves trailing-twelve-month worldwide cash-flow statements for one company. + #[pyo3(signature = (symbol, *, limit=None))] + async fn statement_ttm( + &self, + symbol: String, + limit: Option, + ) -> PyResult> { + let query = statement_ttm_query(&symbol, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.cash_flow_statement_ttm(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(CashFlowStatement::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `CashFlowStatementQuery` for `StatementsCashFlowNamespace::statement` from validated Python arguments. fn statement_query( symbol: &str, diff --git a/crates/fmp-py/src/namespaces/statements/growth.rs b/crates/fmp-py/src/namespaces/statements/growth.rs index 25eeb4d5..03ee90ad 100644 --- a/crates/fmp-py/src/namespaces/statements/growth.rs +++ b/crates/fmp-py/src/namespaces/statements/growth.rs @@ -15,7 +15,7 @@ use crate::models::statements::growth::balance::BalanceSheetStatementGrowth; use crate::models::statements::growth::cash_flow::CashFlowStatementGrowth; use crate::models::statements::growth::combined::FinancialStatementGrowth; use crate::models::statements::growth::income::IncomeStatementGrowth; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Statements growth endpoints for a single client, exposed as `client.statements.growth`. #[gen_stub_pyclass] @@ -133,6 +133,110 @@ impl StatementsGrowthNamespace { } } +/// Statements growth endpoints for a single async client, exposed as `client.statements.growth`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.statements.growth", frozen)] +pub(crate) struct AsyncStatementsGrowthNamespace { + builder: Arc, +} + +impl AsyncStatementsGrowthNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncStatementsGrowthNamespace { + /// Retrieves worldwide income-statement growth for one company. + #[pyo3(signature = (symbol, *, limit=None, period=None))] + async fn income( + &self, + symbol: String, + limit: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\", \"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + ) -> PyResult> { + let query = income_query(&symbol, limit, period.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.income_statement_growth(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(IncomeStatementGrowth::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide balance-sheet-statement growth for one company. + #[pyo3(signature = (symbol, *, limit=None, period=None))] + async fn balance_sheet( + &self, + symbol: String, + limit: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\", \"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + ) -> PyResult> { + let query = balance_sheet_query(&symbol, limit, period.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.balance_sheet_statement_growth(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(BalanceSheetStatementGrowth::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves worldwide cash-flow-statement growth for one company. + #[pyo3(signature = (symbol, *, limit=None, period=None))] + async fn cash_flow( + &self, + symbol: String, + limit: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\", \"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + ) -> PyResult> { + let query = cash_flow_query(&symbol, limit, period.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.cash_flow_statement_growth(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(CashFlowStatementGrowth::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves combined worldwide financial-statement growth for one company. + #[pyo3(signature = (symbol, *, limit=None, period=None))] + async fn financial( + &self, + symbol: String, + limit: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\", \"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + ) -> PyResult> { + let query = financial_query(&symbol, limit, period.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.financial_statement_growth(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(FinancialStatementGrowth::from) + .collect() + }) + .map_err(to_py_error) + } +} + /// Builds the `IncomeStatementGrowthQuery` for `StatementsGrowthNamespace::income` from validated Python arguments. fn income_query( symbol: &str, diff --git a/crates/fmp-py/src/namespaces/statements/income.rs b/crates/fmp-py/src/namespaces/statements/income.rs index e161d7e7..8be980a2 100644 --- a/crates/fmp-py/src/namespaces/statements/income.rs +++ b/crates/fmp-py/src/namespaces/statements/income.rs @@ -9,7 +9,7 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::args; use crate::errors::to_py_error; use crate::models::statements::income::IncomeStatement; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Statements income endpoints for a single client, exposed as `client.statements.income`. #[gen_stub_pyclass] @@ -68,6 +68,57 @@ impl StatementsIncomeNamespace { } } +/// Statements income endpoints for a single async client, exposed as `client.statements.income`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.statements.income", frozen)] +pub(crate) struct AsyncStatementsIncomeNamespace { + builder: Arc, +} + +impl AsyncStatementsIncomeNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncStatementsIncomeNamespace { + /// Retrieves historical worldwide income statements for one company. + #[pyo3(signature = (symbol, *, limit=None, period=None))] + async fn statement( + &self, + symbol: String, + limit: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\", \"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + ) -> PyResult> { + let query = statement_query(&symbol, limit, period.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.income_statement(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(IncomeStatement::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves trailing-twelve-month worldwide income statements for one company. + #[pyo3(signature = (symbol, *, limit=None))] + async fn statement_ttm( + &self, + symbol: String, + limit: Option, + ) -> PyResult> { + let query = statement_ttm_query(&symbol, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.income_statement_ttm(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(IncomeStatement::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `IncomeStatementQuery` for `StatementsIncomeNamespace::statement` from validated Python arguments. fn statement_query( symbol: &str, diff --git a/crates/fmp-py/src/namespaces/statements/metrics.rs b/crates/fmp-py/src/namespaces/statements/metrics.rs index 50bae88b..afc2af13 100644 --- a/crates/fmp-py/src/namespaces/statements/metrics.rs +++ b/crates/fmp-py/src/namespaces/statements/metrics.rs @@ -9,7 +9,7 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::args; use crate::errors::to_py_error; use crate::models::statements::metrics::{KeyMetrics, KeyMetricsTtm}; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Statements metrics endpoints for a single client, exposed as `client.statements.metrics`. #[gen_stub_pyclass] @@ -64,6 +64,53 @@ impl StatementsMetricsNamespace { } } +/// Statements metrics endpoints for a single async client, exposed as `client.statements.metrics`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.statements.metrics", frozen)] +pub(crate) struct AsyncStatementsMetricsNamespace { + builder: Arc, +} + +impl AsyncStatementsMetricsNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncStatementsMetricsNamespace { + /// Retrieves historical worldwide key metrics for one company. + #[pyo3(signature = (symbol, *, limit=None, period=None))] + async fn key_metrics( + &self, + symbol: String, + limit: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\", \"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + ) -> PyResult> { + let query = key_metrics_query(&symbol, limit, period.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.key_metrics(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(KeyMetrics::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide trailing-twelve-month key metrics for one company. + #[pyo3(signature = (symbol))] + async fn key_metrics_ttm(&self, symbol: String) -> PyResult> { + let query = key_metrics_ttm_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.key_metrics_ttm(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(KeyMetricsTtm::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `KeyMetricsQuery` for `StatementsMetricsNamespace::key_metrics` from validated Python arguments. fn key_metrics_query( symbol: &str, diff --git a/crates/fmp-py/src/namespaces/statements/mod.rs b/crates/fmp-py/src/namespaces/statements/mod.rs index b64cd49e..95a8ba82 100644 --- a/crates/fmp-py/src/namespaces/statements/mod.rs +++ b/crates/fmp-py/src/namespaces/statements/mod.rs @@ -18,16 +18,16 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::runtime::ClientHandle; -use self::as_reported::StatementsAsReportedNamespace; -use self::balance::StatementsBalanceNamespace; -use self::cash_flow::StatementsCashFlowNamespace; -use self::growth::StatementsGrowthNamespace; -use self::income::StatementsIncomeNamespace; -use self::metrics::StatementsMetricsNamespace; -use self::ratios::StatementsRatiosNamespace; -use self::reports::StatementsReportsNamespace; -use self::segmentation::StatementsSegmentationNamespace; -use self::summaries::StatementsSummariesNamespace; +use self::as_reported::{AsyncStatementsAsReportedNamespace, StatementsAsReportedNamespace}; +use self::balance::{AsyncStatementsBalanceNamespace, StatementsBalanceNamespace}; +use self::cash_flow::{AsyncStatementsCashFlowNamespace, StatementsCashFlowNamespace}; +use self::growth::{AsyncStatementsGrowthNamespace, StatementsGrowthNamespace}; +use self::income::{AsyncStatementsIncomeNamespace, StatementsIncomeNamespace}; +use self::metrics::{AsyncStatementsMetricsNamespace, StatementsMetricsNamespace}; +use self::ratios::{AsyncStatementsRatiosNamespace, StatementsRatiosNamespace}; +use self::reports::{AsyncStatementsReportsNamespace, StatementsReportsNamespace}; +use self::segmentation::{AsyncStatementsSegmentationNamespace, StatementsSegmentationNamespace}; +use self::summaries::{AsyncStatementsSummariesNamespace, StatementsSummariesNamespace}; /// Statements endpoints for a single client, exposed as `client.statements`. #[gen_stub_pyclass] @@ -105,3 +105,80 @@ impl StatementsNamespace { StatementsSummariesNamespace::new(self.builder.clone()) } } + +/// Statements endpoints for a single async client, exposed as `client.statements`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.statements", frozen)] +pub(crate) struct AsyncStatementsNamespace { + builder: Arc, +} + +impl AsyncStatementsNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncStatementsNamespace { + /// The `statements.as_reported` endpoints, reached as `client.statements.as_reported`. + #[getter] + fn as_reported(&self) -> AsyncStatementsAsReportedNamespace { + AsyncStatementsAsReportedNamespace::new(self.builder.clone()) + } + + /// The `statements.balance` endpoints, reached as `client.statements.balance`. + #[getter] + fn balance(&self) -> AsyncStatementsBalanceNamespace { + AsyncStatementsBalanceNamespace::new(self.builder.clone()) + } + + /// The `statements.cash_flow` endpoints, reached as `client.statements.cash_flow`. + #[getter] + fn cash_flow(&self) -> AsyncStatementsCashFlowNamespace { + AsyncStatementsCashFlowNamespace::new(self.builder.clone()) + } + + /// The `statements.growth` endpoints, reached as `client.statements.growth`. + #[getter] + fn growth(&self) -> AsyncStatementsGrowthNamespace { + AsyncStatementsGrowthNamespace::new(self.builder.clone()) + } + + /// The `statements.income` endpoints, reached as `client.statements.income`. + #[getter] + fn income(&self) -> AsyncStatementsIncomeNamespace { + AsyncStatementsIncomeNamespace::new(self.builder.clone()) + } + + /// The `statements.metrics` endpoints, reached as `client.statements.metrics`. + #[getter] + fn metrics(&self) -> AsyncStatementsMetricsNamespace { + AsyncStatementsMetricsNamespace::new(self.builder.clone()) + } + + /// The `statements.ratios` endpoints, reached as `client.statements.ratios`. + #[getter] + fn ratios(&self) -> AsyncStatementsRatiosNamespace { + AsyncStatementsRatiosNamespace::new(self.builder.clone()) + } + + /// The `statements.reports` endpoints, reached as `client.statements.reports`. + #[getter] + fn reports(&self) -> AsyncStatementsReportsNamespace { + AsyncStatementsReportsNamespace::new(self.builder.clone()) + } + + /// The `statements.segmentation` endpoints, reached as `client.statements.segmentation`. + #[getter] + fn segmentation(&self) -> AsyncStatementsSegmentationNamespace { + AsyncStatementsSegmentationNamespace::new(self.builder.clone()) + } + + /// The `statements.summaries` endpoints, reached as `client.statements.summaries`. + #[getter] + fn summaries(&self) -> AsyncStatementsSummariesNamespace { + AsyncStatementsSummariesNamespace::new(self.builder.clone()) + } +} diff --git a/crates/fmp-py/src/namespaces/statements/ratios.rs b/crates/fmp-py/src/namespaces/statements/ratios.rs index dd1648fb..1041f16e 100644 --- a/crates/fmp-py/src/namespaces/statements/ratios.rs +++ b/crates/fmp-py/src/namespaces/statements/ratios.rs @@ -9,7 +9,7 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::args; use crate::errors::to_py_error; use crate::models::statements::ratios::{FinancialRatios, FinancialRatiosTtm}; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Statements ratios endpoints for a single client, exposed as `client.statements.ratios`. #[gen_stub_pyclass] @@ -67,6 +67,53 @@ impl StatementsRatiosNamespace { } } +/// Statements ratios endpoints for a single async client, exposed as `client.statements.ratios`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.statements.ratios", frozen)] +pub(crate) struct AsyncStatementsRatiosNamespace { + builder: Arc, +} + +impl AsyncStatementsRatiosNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncStatementsRatiosNamespace { + /// Retrieves historical worldwide financial ratios for one company. + #[pyo3(signature = (symbol, *, limit=None, period=None))] + async fn financial_ratios( + &self, + symbol: String, + limit: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\", \"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + ) -> PyResult> { + let query = financial_ratios_query(&symbol, limit, period.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.financial_ratios(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(FinancialRatios::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves trailing-twelve-month worldwide financial ratios for one company. + #[pyo3(signature = (symbol))] + async fn financial_ratios_ttm(&self, symbol: String) -> PyResult> { + let query = financial_ratios_ttm_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.financial_ratios_ttm(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(FinancialRatiosTtm::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `FinancialRatiosQuery` for `StatementsRatiosNamespace::financial_ratios` from validated Python arguments. fn financial_ratios_query( symbol: &str, diff --git a/crates/fmp-py/src/namespaces/statements/reports.rs b/crates/fmp-py/src/namespaces/statements/reports.rs index 3b7ce0ba..d2ba7f54 100644 --- a/crates/fmp-py/src/namespaces/statements/reports.rs +++ b/crates/fmp-py/src/namespaces/statements/reports.rs @@ -12,7 +12,7 @@ use crate::args; use crate::binary::BinaryPayload; use crate::errors::to_py_error; use crate::models::statements::reports::{FinancialReportDate, FinancialReportJson}; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Statements reports endpoints for a single client, exposed as `client.statements.reports`. #[gen_stub_pyclass] @@ -85,6 +85,69 @@ impl StatementsReportsNamespace { } } +/// Statements reports endpoints for a single async client, exposed as `client.statements.reports`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.statements.reports", frozen)] +pub(crate) struct AsyncStatementsReportsNamespace { + builder: Arc, +} + +impl AsyncStatementsReportsNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncStatementsReportsNamespace { + /// Retrieves available financial-report periods and protected download links. + #[pyo3(signature = (symbol))] + async fn dates(&self, symbol: String) -> PyResult> { + let query = dates_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.financial_reports_dates(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(FinancialReportDate::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves one dynamic JSON financial report as a single object. + #[pyo3(signature = (symbol, year, period))] + async fn json( + &self, + symbol: String, + year: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\"]", imports = ("typing",)))] + period: String, + ) -> PyResult { + let query = json_query(&symbol, year, &period)?; + let row = spawn(self.builder.clone(), |client| async move { + client.financial_reports_json(query).await + }) + .await?; + row.map(FinancialReportJson::from).map_err(to_py_error) + } + + /// Downloads one financial report using the endpoint's XLSX MIME policy. + #[pyo3(signature = (symbol, year, period))] + async fn xlsx( + &self, + symbol: String, + year: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\"]", imports = ("typing",)))] + period: String, + ) -> PyResult { + let query = xlsx_query(&symbol, year, &period)?; + let response = spawn(self.builder.clone(), |client| async move { + client.financial_reports_xlsx(query).await + }) + .await?; + response.map(BinaryPayload::from).map_err(to_py_error) + } +} + /// Builds the `FinancialReportsDatesQuery` for `StatementsReportsNamespace::dates` from validated Python arguments. fn dates_query(symbol: &str) -> PyResult { let symbol = args::ticker("symbol", symbol)?; diff --git a/crates/fmp-py/src/namespaces/statements/segmentation.rs b/crates/fmp-py/src/namespaces/statements/segmentation.rs index f28a7e4b..6dcd9335 100644 --- a/crates/fmp-py/src/namespaces/statements/segmentation.rs +++ b/crates/fmp-py/src/namespaces/statements/segmentation.rs @@ -11,7 +11,7 @@ use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use crate::args; use crate::errors::to_py_error; use crate::models::statements::segmentation::RevenueSegmentation; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Statements segmentation endpoints for a single client, exposed as `client.statements.segmentation`. #[gen_stub_pyclass] @@ -74,6 +74,61 @@ impl StatementsSegmentationNamespace { } } +/// Statements segmentation endpoints for a single async client, exposed as `client.statements.segmentation`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.statements.segmentation", frozen)] +pub(crate) struct AsyncStatementsSegmentationNamespace { + builder: Arc, +} + +impl AsyncStatementsSegmentationNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncStatementsSegmentationNamespace { + /// Retrieves product revenue segmentation for one company. + #[pyo3(signature = (symbol, *, period=None, structure=None))] + async fn revenue_product( + &self, + symbol: String, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"flat\"]]", imports = ("typing",)))] + structure: Option, + ) -> PyResult> { + let query = revenue_product_query(&symbol, period.as_deref(), structure.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.revenue_product_segmentation(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(RevenueSegmentation::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves geographic revenue segmentation for one company. + #[pyo3(signature = (symbol, *, period=None, structure=None))] + async fn revenue_geographic( + &self, + symbol: String, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"flat\"]]", imports = ("typing",)))] + structure: Option, + ) -> PyResult> { + let query = revenue_geographic_query(&symbol, period.as_deref(), structure.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.revenue_geographic_segmentation(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(RevenueSegmentation::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `RevenueProductSegmentationQuery` for `StatementsSegmentationNamespace::revenue_product` from validated Python arguments. fn revenue_product_query( symbol: &str, diff --git a/crates/fmp-py/src/namespaces/statements/summaries.rs b/crates/fmp-py/src/namespaces/statements/summaries.rs index 25390ffe..e322f85b 100644 --- a/crates/fmp-py/src/namespaces/statements/summaries.rs +++ b/crates/fmp-py/src/namespaces/statements/summaries.rs @@ -13,7 +13,7 @@ use crate::errors::to_py_error; use crate::models::statements::summaries::{ EnterpriseValue, FinancialScore, LatestFinancialStatement, OwnerEarnings, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Statements summaries endpoints for a single client, exposed as `client.statements.summaries`. #[gen_stub_pyclass] @@ -110,6 +110,90 @@ impl StatementsSummariesNamespace { } } +/// Statements summaries endpoints for a single async client, exposed as `client.statements.summaries`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.statements.summaries", frozen)] +pub(crate) struct AsyncStatementsSummariesNamespace { + builder: Arc, +} + +impl AsyncStatementsSummariesNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncStatementsSummariesNamespace { + /// Retrieves the latest worldwide financial-statement filings. + #[pyo3(signature = (*, page=None, limit=None))] + async fn latest_financial_statements( + &self, + page: Option, + limit: Option, + ) -> PyResult> { + let query = latest_financial_statements_query(page, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_financial_statements(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(LatestFinancialStatement::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves worldwide financial-health scores for one company. + #[pyo3(signature = (symbol))] + async fn financial_scores(&self, symbol: String) -> PyResult> { + let query = financial_scores_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.financial_scores(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(FinancialScore::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide owner earnings for one company. + #[pyo3(signature = (symbol, *, limit=None))] + async fn owner_earnings( + &self, + symbol: String, + limit: Option, + ) -> PyResult> { + let query = owner_earnings_query(&symbol, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.owner_earnings(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(OwnerEarnings::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide enterprise values for one company. + #[pyo3(signature = (symbol, *, limit=None, period=None))] + async fn enterprise_values( + &self, + symbol: String, + limit: Option, + #[gen_stub(override_type(type_repr = "typing.Optional[typing.Literal[\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"FY\", \"annual\", \"quarter\"]]", imports = ("typing",)))] + period: Option, + ) -> PyResult> { + let query = enterprise_values_query(&symbol, limit, period.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.enterprise_values(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EnterpriseValue::from).collect()) + .map_err(to_py_error) + } +} + /// Builds the `LatestFinancialStatementsQuery` for `StatementsSummariesNamespace::latest_financial_statements` from validated Python arguments. fn latest_financial_statements_query( page: Option, diff --git a/crates/fmp-py/src/namespaces/technical_indicators.rs b/crates/fmp-py/src/namespaces/technical_indicators.rs index cbf4b2f5..d0734f00 100644 --- a/crates/fmp-py/src/namespaces/technical_indicators.rs +++ b/crates/fmp-py/src/namespaces/technical_indicators.rs @@ -13,7 +13,7 @@ use crate::models::technical_indicators::{ RelativeStrengthIndexBar, SimpleMovingAverageBar, StandardDeviationBar, TripleExponentialMovingAverageBar, WeightedMovingAverageBar, WilliamsBar, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Technical indicators endpoints for a single client, exposed as `client.technical_indicators`. #[gen_stub_pyclass] @@ -277,6 +277,241 @@ impl TechnicalIndicatorsNamespace { } } +/// Technical indicators endpoints for a single async client, exposed as `client.technical_indicators`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.technical_indicators", frozen)] +pub(crate) struct AsyncTechnicalIndicatorsNamespace { + builder: Arc, +} + +impl AsyncTechnicalIndicatorsNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncTechnicalIndicatorsNamespace { + /// Retrieves worldwide simple-moving-average bars. + #[pyo3(signature = (symbol, period_length, timeframe, *, from_=None, to=None))] + async fn simple_moving_average( + &self, + symbol: String, + period_length: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"1min\", \"5min\", \"15min\", \"30min\", \"1hour\", \"4hour\", \"1day\"]", imports = ("typing",)))] + timeframe: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = simple_moving_average_query(&symbol, period_length, &timeframe, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.simple_moving_average(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(SimpleMovingAverageBar::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves worldwide exponential-moving-average bars. + #[pyo3(signature = (symbol, period_length, timeframe, *, from_=None, to=None))] + async fn exponential_moving_average( + &self, + symbol: String, + period_length: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"1min\", \"5min\", \"15min\", \"30min\", \"1hour\", \"4hour\", \"1day\"]", imports = ("typing",)))] + timeframe: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = + exponential_moving_average_query(&symbol, period_length, &timeframe, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.exponential_moving_average(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(ExponentialMovingAverageBar::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves worldwide weighted-moving-average bars. + #[pyo3(signature = (symbol, period_length, timeframe, *, from_=None, to=None))] + async fn weighted_moving_average( + &self, + symbol: String, + period_length: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"1min\", \"5min\", \"15min\", \"30min\", \"1hour\", \"4hour\", \"1day\"]", imports = ("typing",)))] + timeframe: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = weighted_moving_average_query(&symbol, period_length, &timeframe, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.weighted_moving_average(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(WeightedMovingAverageBar::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves worldwide double-exponential-moving-average bars. + #[pyo3(signature = (symbol, period_length, timeframe, *, from_=None, to=None))] + async fn double_exponential_moving_average( + &self, + symbol: String, + period_length: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"1min\", \"5min\", \"15min\", \"30min\", \"1hour\", \"4hour\", \"1day\"]", imports = ("typing",)))] + timeframe: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = + double_exponential_moving_average_query(&symbol, period_length, &timeframe, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.double_exponential_moving_average(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(DoubleExponentialMovingAverageBar::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves worldwide triple-exponential-moving-average bars. + #[pyo3(signature = (symbol, period_length, timeframe, *, from_=None, to=None))] + async fn triple_exponential_moving_average( + &self, + symbol: String, + period_length: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"1min\", \"5min\", \"15min\", \"30min\", \"1hour\", \"4hour\", \"1day\"]", imports = ("typing",)))] + timeframe: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = + triple_exponential_moving_average_query(&symbol, period_length, &timeframe, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.triple_exponential_moving_average(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(TripleExponentialMovingAverageBar::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves worldwide relative-strength-index bars. + #[pyo3(signature = (symbol, period_length, timeframe, *, from_=None, to=None))] + async fn relative_strength_index( + &self, + symbol: String, + period_length: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"1min\", \"5min\", \"15min\", \"30min\", \"1hour\", \"4hour\", \"1day\"]", imports = ("typing",)))] + timeframe: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = relative_strength_index_query(&symbol, period_length, &timeframe, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.relative_strength_index(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(RelativeStrengthIndexBar::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves worldwide standard-deviation bars. + #[pyo3(signature = (symbol, period_length, timeframe, *, from_=None, to=None))] + async fn standard_deviation( + &self, + symbol: String, + period_length: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"1min\", \"5min\", \"15min\", \"30min\", \"1hour\", \"4hour\", \"1day\"]", imports = ("typing",)))] + timeframe: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = standard_deviation_query(&symbol, period_length, &timeframe, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.standard_deviation(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(StandardDeviationBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide Williams bars. + #[pyo3(signature = (symbol, period_length, timeframe, *, from_=None, to=None))] + async fn williams( + &self, + symbol: String, + period_length: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"1min\", \"5min\", \"15min\", \"30min\", \"1hour\", \"4hour\", \"1day\"]", imports = ("typing",)))] + timeframe: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = williams_query(&symbol, period_length, &timeframe, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.williams(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(WilliamsBar::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves worldwide average-directional-index bars. + #[pyo3(signature = (symbol, period_length, timeframe, *, from_=None, to=None))] + async fn average_directional_index( + &self, + symbol: String, + period_length: i64, + #[gen_stub(override_type(type_repr = "typing.Literal[\"1min\", \"5min\", \"15min\", \"30min\", \"1hour\", \"4hour\", \"1day\"]", imports = ("typing",)))] + timeframe: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = average_directional_index_query(&symbol, period_length, &timeframe, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.average_directional_index(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(AverageDirectionalIndexBar::from) + .collect() + }) + .map_err(to_py_error) + } +} + /// Builds the `TechnicalIndicatorQuery` for `TechnicalIndicatorsNamespace::simple_moving_average` from validated Python arguments. fn simple_moving_average_query( symbol: &str, diff --git a/crates/fmp-py/src/namespaces/tipranks.rs b/crates/fmp-py/src/namespaces/tipranks.rs index 707e1516..5f393a73 100644 --- a/crates/fmp-py/src/namespaces/tipranks.rs +++ b/crates/fmp-py/src/namespaces/tipranks.rs @@ -16,7 +16,7 @@ use crate::models::tipranks::{ TipRanksAnalystProfile, TipRanksAnalystSummary, TipRanksFirmSummary, TipRanksPointInTimeRating, TipRanksRatingSearchResult, TipRanksSymbolSummary, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Tipranks endpoints for a single client, exposed as `client.tipranks`. #[gen_stub_pyclass] @@ -221,6 +221,195 @@ impl TipranksNamespace { } } +/// Tipranks endpoints for a single async client, exposed as `client.tipranks`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.tipranks", frozen)] +pub(crate) struct AsyncTipranksNamespace { + builder: Arc, +} + +impl AsyncTipranksNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncTipranksNamespace { + /// Retrieves individual analyst ratings from the TipRanks add-on. + #[pyo3(signature = (*, expert_uid=None, symbol=None, from_=None, to=None, limit=None, page=None, nonadjusted=None))] + #[allow(clippy::too_many_arguments)] + async fn search_ratings( + &self, + expert_uid: Option, + symbol: Option, + from_: Option, + to: Option, + limit: Option, + page: Option, + nonadjusted: Option, + ) -> PyResult> { + let query = search_ratings_query( + expert_uid.as_deref(), + symbol.as_deref(), + from_, + to, + limit, + page, + nonadjusted, + )?; + let rows = spawn(self.builder.clone(), |client| async move { + client.search_tipranks_ratings(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(TipRanksRatingSearchResult::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves a ticker's analyst ratings as of an optional snapshot date. + #[pyo3(signature = (symbol, *, date=None, limit=None, page=None, nonadjusted=None))] + async fn point_in_time_ratings_by_symbol( + &self, + symbol: String, + date: Option, + limit: Option, + page: Option, + nonadjusted: Option, + ) -> PyResult> { + let query = point_in_time_ratings_by_symbol_query(&symbol, date, limit, page, nonadjusted)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.tipranks_point_in_time_ratings_by_symbol(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(TipRanksPointInTimeRating::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves an analyst's active coverage as of an optional snapshot date. + #[pyo3(signature = (*, expert_uid=None, analyst_name=None, date=None, limit=None, page=None, nonadjusted=None))] + async fn point_in_time_ratings_by_analyst( + &self, + expert_uid: Option, + analyst_name: Option, + date: Option, + limit: Option, + page: Option, + nonadjusted: Option, + ) -> PyResult> { + let query = point_in_time_ratings_by_analyst_query( + expert_uid.as_deref(), + analyst_name.as_deref(), + date, + limit, + page, + nonadjusted, + )?; + let rows = spawn(self.builder.clone(), |client| async move { + client + .tipranks_point_in_time_ratings_by_analyst(query) + .await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(TipRanksPointInTimeRating::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves a ticker's aggregate TipRanks ratings summary. + #[pyo3(signature = (symbol, *, from_=None, to=None))] + async fn symbol_summary( + &self, + symbol: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = symbol_summary_query(&symbol, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.tipranks_symbol_summary(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(TipRanksSymbolSummary::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves an analyst's aggregate TipRanks ratings summary. + #[pyo3(signature = (expert_uid, *, from_=None, to=None))] + async fn analyst_summary( + &self, + expert_uid: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = analyst_summary_query(&expert_uid, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.tipranks_analyst_summary(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(TipRanksAnalystSummary::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves a firm's aggregate TipRanks ratings summary. + #[pyo3(signature = (firm_name, *, from_=None, to=None))] + async fn firm_summary( + &self, + firm_name: String, + from_: Option, + to: Option, + ) -> PyResult> { + let query = firm_summary_query(&firm_name, from_, to)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.tipranks_firm_summary(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(TipRanksFirmSummary::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves analyst profiles from the TipRanks directory. + #[pyo3(signature = (*, page=None, limit=None, firm_name=None, analyst_name=None))] + async fn analysts( + &self, + page: Option, + limit: Option, + firm_name: Option, + analyst_name: Option, + ) -> PyResult> { + let query = analysts_query(page, limit, firm_name.as_deref(), analyst_name.as_deref())?; + let rows = spawn(self.builder.clone(), |client| async move { + client.tipranks_analysts(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(TipRanksAnalystProfile::from) + .collect() + }) + .map_err(to_py_error) + } +} + /// Builds the `TipRanksSearchQuery` for `TipranksNamespace::search_ratings` from validated Python arguments. fn search_ratings_query( expert_uid: Option<&str>, diff --git a/crates/fmp-py/src/namespaces/transcripts.rs b/crates/fmp-py/src/namespaces/transcripts.rs index 7ae3b163..abe9a655 100644 --- a/crates/fmp-py/src/namespaces/transcripts.rs +++ b/crates/fmp-py/src/namespaces/transcripts.rs @@ -13,7 +13,7 @@ use crate::errors::to_py_error; use crate::models::transcripts::{ EarningsTranscript, EarningsTranscriptDate, LatestEarningsTranscript, }; -use crate::runtime::{ClientHandle, block_on}; +use crate::runtime::{ClientHandle, block_on, spawn}; /// Transcripts endpoints for a single client, exposed as `client.transcripts`. #[gen_stub_pyclass] @@ -96,6 +96,79 @@ impl TranscriptsNamespace { } } +/// Transcripts endpoints for a single async client, exposed as `client.transcripts`. +#[gen_stub_pyclass] +#[pyclass(module = "fmp._native.transcripts", frozen)] +pub(crate) struct AsyncTranscriptsNamespace { + builder: Arc, +} + +impl AsyncTranscriptsNamespace { + pub(crate) fn new(builder: Arc) -> Self { + Self { builder } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl AsyncTranscriptsNamespace { + /// Retrieves the latest worldwide earnings-transcript metadata. + #[pyo3(signature = (*, limit=None, page=None))] + async fn latest( + &self, + limit: Option, + page: Option, + ) -> PyResult> { + let query = latest_query(limit, page)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.latest_earnings_transcripts(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(LatestEarningsTranscript::from) + .collect() + }) + .map_err(to_py_error) + } + + /// Retrieves complete worldwide earnings-call transcripts. + #[pyo3(signature = (symbol, year, quarter, *, limit=None))] + async fn by_quarter( + &self, + symbol: String, + year: i64, + quarter: i64, + limit: Option, + ) -> PyResult> { + let query = by_quarter_query(&symbol, year, quarter, limit)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.earnings_transcript(query).await + }) + .await?; + rows.map(|items| items.into_iter().map(EarningsTranscript::from).collect()) + .map_err(to_py_error) + } + + /// Retrieves available worldwide transcript dates for one ticker. + #[pyo3(signature = (symbol))] + async fn dates(&self, symbol: String) -> PyResult> { + let query = dates_query(&symbol)?; + let rows = spawn(self.builder.clone(), |client| async move { + client.earnings_transcript_dates(query).await + }) + .await?; + rows.map(|items| { + items + .into_iter() + .map(EarningsTranscriptDate::from) + .collect() + }) + .map_err(to_py_error) + } +} + /// Builds the `LatestEarningsTranscriptsQuery` for `TranscriptsNamespace::latest` from validated Python arguments. fn latest_query(limit: Option, page: Option) -> PyResult { let limit = args::optional("limit", limit, args::limit)?; diff --git a/crates/fmp-py/src/registration.rs b/crates/fmp-py/src/registration.rs index 96a5d7a0..d38c736e 100644 --- a/crates/fmp-py/src/registration.rs +++ b/crates/fmp-py/src/registration.rs @@ -53,12 +53,14 @@ fn register_analyst(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.analyst", &module) } fn register_bulk(parent: &Bound<'_, PyModule>) -> PyResult<()> { let module = PyModule::new(parent.py(), "bulk")?; module.add_class::()?; + module.add_class::()?; register_bulk_balance(&module)?; register_bulk_cash_flow(&module)?; register_bulk_eod(&module)?; @@ -124,6 +126,7 @@ fn register_calendar(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.calendar", &module) } @@ -134,6 +137,7 @@ fn register_chart(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.chart", &module) } @@ -143,6 +147,9 @@ fn register_commitment_of_traders(parent: &Bound<'_, PyModule>) -> PyResult<()> module.add_class::()?; module.add_class::()?; module.add_class::()?; + module + .add_class::( + )?; add_submodule(parent, "fmp._native.commitment_of_traders", &module) } @@ -150,6 +157,7 @@ fn register_commodities(parent: &Bound<'_, PyModule>) -> PyResult<()> { let module = PyModule::new(parent.py(), "commodities")?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.commodities", &module) } @@ -168,6 +176,7 @@ fn register_company(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.company", &module) } @@ -181,6 +190,7 @@ fn register_congressional(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.congressional", &module) } @@ -188,6 +198,7 @@ fn register_crypto(parent: &Bound<'_, PyModule>) -> PyResult<()> { let module = PyModule::new(parent.py(), "crypto")?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.crypto", &module) } @@ -197,6 +208,7 @@ fn register_dcf(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.dcf", &module) } @@ -214,6 +226,7 @@ fn register_directory(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.directory", &module) } @@ -224,6 +237,7 @@ fn register_economics(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.economics", &module) } @@ -233,6 +247,7 @@ fn register_esg(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.esg", &module) } @@ -240,6 +255,7 @@ fn register_forex(parent: &Bound<'_, PyModule>) -> PyResult<()> { let module = PyModule::new(parent.py(), "forex")?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.forex", &module) } @@ -250,6 +266,7 @@ fn register_fundraising(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.fundraising", &module) } @@ -265,6 +282,7 @@ fn register_funds(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.funds", &module) } @@ -274,6 +292,7 @@ fn register_indexes(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.indexes", &module) } @@ -285,6 +304,7 @@ fn register_insider_trading(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.insider_trading", &module) } @@ -301,6 +321,7 @@ fn register_institutional_ownership(parent: &Bound<'_, PyModule>) -> PyResult<() module .add_class::( )?; + module.add_class::()?; add_submodule(parent, "fmp._native.institutional_ownership", &module) } @@ -312,6 +333,7 @@ fn register_market(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.market", &module) } @@ -320,6 +342,7 @@ fn register_market_hours(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.market_hours", &module) } @@ -328,6 +351,7 @@ fn register_news(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.news", &module) } @@ -339,6 +363,7 @@ fn register_quote(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.quote", &module) } @@ -346,6 +371,7 @@ fn register_screener(parent: &Bound<'_, PyModule>) -> PyResult<()> { let module = PyModule::new(parent.py(), "screener")?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.screener", &module) } @@ -358,6 +384,7 @@ fn register_search(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.search", &module) } @@ -368,12 +395,14 @@ fn register_sec_filings(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.sec_filings", &module) } fn register_statements(parent: &Bound<'_, PyModule>) -> PyResult<()> { let module = PyModule::new(parent.py(), "statements")?; module.add_class::()?; + module.add_class::()?; register_statements_as_reported(&module)?; register_statements_balance(&module)?; register_statements_cash_flow(&module)?; @@ -392,6 +421,7 @@ fn register_statements_as_reported(parent: &Bound<'_, PyModule>) -> PyResult<()> module.add_class::()?; module .add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.statements.as_reported", &module) } @@ -400,6 +430,8 @@ fn register_statements_balance(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module + .add_class::()?; add_submodule(parent, "fmp._native.statements.balance", &module) } @@ -407,12 +439,16 @@ fn register_statements_cash_flow(parent: &Bound<'_, PyModule>) -> PyResult<()> { let module = PyModule::new(parent.py(), "cash_flow")?; module.add_class::()?; module.add_class::()?; + module + .add_class::( + )?; add_submodule(parent, "fmp._native.statements.cash_flow", &module) } fn register_statements_growth(parent: &Bound<'_, PyModule>) -> PyResult<()> { let module = PyModule::new(parent.py(), "growth")?; module.add_class::()?; + module.add_class::()?; register_statements_growth_balance(&module)?; register_statements_growth_cash_flow(&module)?; register_statements_growth_combined(&module)?; @@ -449,6 +485,7 @@ fn register_statements_income(parent: &Bound<'_, PyModule>) -> PyResult<()> { let module = PyModule::new(parent.py(), "income")?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.statements.income", &module) } @@ -457,6 +494,8 @@ fn register_statements_metrics(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module + .add_class::()?; add_submodule(parent, "fmp._native.statements.metrics", &module) } @@ -465,6 +504,7 @@ fn register_statements_ratios(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.statements.ratios", &module) } @@ -473,6 +513,8 @@ fn register_statements_reports(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module + .add_class::()?; add_submodule(parent, "fmp._native.statements.reports", &module) } @@ -482,6 +524,7 @@ fn register_statements_segmentation(parent: &Bound<'_, PyModule>) -> PyResult<() module .add_class::( )?; + module.add_class::()?; add_submodule(parent, "fmp._native.statements.segmentation", &module) } @@ -492,6 +535,9 @@ fn register_statements_summaries(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module + .add_class::( + )?; add_submodule(parent, "fmp._native.statements.summaries", &module) } @@ -507,6 +553,9 @@ fn register_technical_indicators(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module + .add_class::( + )?; add_submodule(parent, "fmp._native.technical_indicators", &module) } @@ -521,6 +570,7 @@ fn register_tipranks(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.tipranks", &module) } @@ -530,5 +580,6 @@ fn register_transcripts(parent: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + module.add_class::()?; add_submodule(parent, "fmp._native.transcripts", &module) } diff --git a/crates/fmp-py/src/runtime.rs b/crates/fmp-py/src/runtime.rs index f5e87174..1c5f0b2c 100644 --- a/crates/fmp-py/src/runtime.rs +++ b/crates/fmp-py/src/runtime.rs @@ -1,26 +1,32 @@ -//! Process-wide, fork-safe async runtime backing the synchronous FMP clients. +//! Process-wide, fork-safe async runtime backing the FMP clients. //! //! A single multi-thread Tokio runtime and a builder-keyed client pool are -//! shared by every `FmpClient` in the process. Both live behind [`current`], -//! which rebuilds them the first time it runs in a new process generation so -//! that a runtime inherited across `fork()` is never used or dropped in the -//! child. This topology is deliberately process-global rather than -//! thread-local: it is the shape a future asynchronous client needs, where a -//! spawned future must outlive the calling thread and deliver its result to the -//! Python event loop. +//! shared by every `FmpClient` and `AsyncFmpClient` in the process. Both live +//! behind [`current`], which rebuilds them the first time it runs in a new +//! process generation so that a runtime inherited across `fork()` is never +//! used or dropped in the child. This topology is deliberately process-global +//! rather than thread-local: `AsyncFmpClient` spawns each request as a task +//! that outlives the polling call and delivers its result to the Python event +//! loop through [`spawn`]. use std::{ collections::HashMap, future::Future, - mem, process, + mem, + pin::Pin, + process, sync::{ Arc, Mutex, OnceLock, RwLock, Weak, atomic::{AtomicBool, Ordering}, }, + task::{Context, Poll}, }; use libfmp::{Client, ClientBuilder}; -use tokio::runtime::{Builder as RuntimeBuilder, Handle, Runtime}; +use tokio::{ + runtime::{Builder as RuntimeBuilder, Handle, Runtime}, + task::JoinHandle, +}; use pyo3::{Py, PyResult, types::PyAny}; @@ -237,6 +243,62 @@ where Ok(output) } +/// A spawned task that is aborted when this guard is dropped. +/// +/// Dropping a bare `JoinHandle` detaches its task, so a cancelled Python +/// coroutine would leave the request running. Aborting after the task already +/// finished is a no-op. +struct AbortOnDrop(JoinHandle); + +impl Drop for AbortOnDrop { + fn drop(&mut self) { + self.0.abort(); + } +} + +impl Future for AbortOnDrop { + type Output = PyResult; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.0) + .poll(cx) + .map(|joined| joined.map_err(|_| internal_error("an fmp request task panicked"))) + } +} + +/// Run `operation` against a cached client as a task on the shared runtime and +/// await it, the asynchronous twin of [`block_on`]. +/// +/// The awaiting coroutine only polls the task's handle, so it never blocks +/// the event loop thread. Dropping the returned future (an asyncio +/// cancellation) aborts the task and with it the in-flight request. Returns a +/// configuration error when `handle` is closed. With an `on_response` +/// callback, it runs here after the task finishes, with the GIL, and its +/// exception is returned. +pub(crate) async fn spawn(handle: Arc, operation: F) -> PyResult +where + F: FnOnce(Client) -> Fut, + Fut: Future + Send + 'static, + T: Send + 'static, +{ + if handle.is_closed() { + return Err(configuration_error(CLOSED_MESSAGE)); + } + let inner = current()?; + let client = inner.client(&handle)?; + let Some(callback) = &handle.on_response else { + return AbortOnDrop(inner.runtime.spawn(operation(client))).await; + }; + let task = AbortOnDrop( + inner + .runtime + .spawn(response_hook::capture(operation(client))), + ); + let (output, seen) = task.await?; + response_hook::deliver(callback, seen)?; + Ok(output) +} + /// Mark `handle` closed and drop its pooled client, releasing its idle /// connections. Idempotent. Calls already in flight keep their own client /// clone and finish; the process-wide runtime is shared by every client and @@ -323,3 +385,49 @@ mod fork_tests { ); } } + +#[cfg(test)] +mod abort_tests { + use super::*; + use std::{sync::mpsc, time::Duration}; + + /// Sends once when dropped, so a test can observe a task being torn down. + struct DropSignal(mpsc::Sender<()>); + + impl Drop for DropSignal { + fn drop(&mut self) { + let _ = self.0.send(()); + } + } + + #[test] + fn dropping_the_guard_aborts_the_task() { + let inner = current().expect("runtime builds"); + let (sender, dropped) = mpsc::channel(); + let signal = DropSignal(sender); + let guard = AbortOnDrop(inner.runtime.spawn(async move { + let _signal = signal; + std::future::pending::<()>().await + })); + + drop(guard); + + dropped + .recv_timeout(Duration::from_secs(5)) + .expect("the pending task was aborted and its state dropped"); + } + + #[test] + fn the_guard_resolves_to_the_task_output() { + let inner = current().expect("runtime builds"); + let output = inner.block_on(AbortOnDrop(inner.runtime.spawn(async { 21 * 2 }))); + assert_eq!(output.ok(), Some(42)); + } + + #[test] + fn a_panicking_task_becomes_an_error() { + let inner = current().expect("runtime builds"); + let output = inner.block_on(AbortOnDrop(inner.runtime.spawn(async { panic!("boom") }))); + assert!(output.is_err()); + } +} diff --git a/crates/fmp-py/tests/conftest.py b/crates/fmp-py/tests/conftest.py index 73ef04df..afb734c5 100644 --- a/crates/fmp-py/tests/conftest.py +++ b/crates/fmp-py/tests/conftest.py @@ -32,6 +32,9 @@ def test_profile_decodes_documented_fixture(client, fixture_server): prove the error mapping stays structured. The ``client`` fixture targets the server with ``path_prefix=""`` and ``auth_mode="none"``; build your own ``FmpClient(base_url=fixture_server.base_url, ...)`` for other configurations. +The ``async_client`` fixture is the same configuration as an ``AsyncFmpClient``. +A route's ``delay`` holds the response back for that many seconds; a client +that hung up meanwhile is counted in ``disconnects`` and gets no response. ``FMP_API_KEY`` and ``FMP_BASE_URL`` are removed from the environment for every test (the autouse ``isolated_api_key_env`` fixture), so values exported @@ -41,7 +44,10 @@ def test_profile_decodes_documented_fixture(client, fixture_server): """ import json +import select +import socket import threading +import time from collections.abc import Iterator from dataclasses import dataclass, field from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer @@ -96,16 +102,18 @@ def _errors_namespace() -> SimpleNamespace: @dataclass(frozen=True) class Route: - """One canned response: HTTP status, body, content type, and extra headers. + """One canned response: HTTP status, body, content type, extra headers, and delay. ``body`` is sent verbatim when it is ``bytes``; any other value is JSON - encoded. ``headers`` are sent in order after the content type. + encoded. ``headers`` are sent in order after the content type. ``delay`` + is how many seconds the server waits before answering. """ body: Any = field(default_factory=list) status: int = 200 content_type: str = JSON_CONTENT_TYPE headers: tuple[tuple[str, str], ...] = () + delay: float = 0.0 def payload(self) -> bytes: """Return the encoded response body.""" @@ -130,12 +138,25 @@ def target(self) -> str: return f"{self.path}?{self.raw_query}" if self.raw_query else self.path +class _Server(ThreadingHTTPServer): + """A threading server whose listen backlog absorbs a burst of concurrent connections.""" + + request_queue_size = 64 + + +def _peer_closed(connection: socket.socket) -> bool: + """Whether the client closed ``connection``: it is readable and a peek reads EOF.""" + readable, _, _ = select.select([connection], [], [], 0) + return bool(readable) and connection.recv(1, socket.MSG_PEEK) == b"" + + class FixtureServer: """A loopback HTTP server that answers from a route table. ``routes`` maps a request path (or a full ``path?query`` string) to a :class:`Route`; unmatched requests receive ``default``. Every request is - appended to ``requests`` in arrival order. + appended to ``requests`` in arrival order, and ``disconnects`` counts the + delayed requests whose client hung up before the answer. """ def __init__(self, default: Route | None = None) -> None: @@ -143,7 +164,8 @@ def __init__(self, default: Route | None = None) -> None: self.routes: dict[str, Route] = {} self.default: Route = default if default is not None else Route() self.requests: list[RecordedRequest] = [] - self._server = ThreadingHTTPServer(("127.0.0.1", 0), self._handler_class()) + self.disconnects = 0 + self._server = _Server(("127.0.0.1", 0), self._handler_class()) self._thread = threading.Thread(target=self._server.serve_forever, daemon=True) @property @@ -159,10 +181,15 @@ def route( status: int = 200, content_type: str = JSON_CONTENT_TYPE, headers: tuple[tuple[str, str], ...] = (), + delay: float = 0.0, ) -> Route: """Register the response for ``path`` and return the stored route.""" stored = Route( - body=[] if body is None else body, status=status, content_type=content_type, headers=headers + body=[] if body is None else body, + status=status, + content_type=content_type, + headers=headers, + delay=delay, ) self.routes[path] = stored return stored @@ -207,6 +234,12 @@ def do_GET(self) -> None: """Answer a GET from the route table.""" server._record(self) route = server.lookup(self.path) + if route.delay: + time.sleep(route.delay) + if _peer_closed(self.connection): + server.disconnects += 1 + self.close_connection = True + return payload = route.payload() self.send_response(route.status) self.send_header("Content-Type", route.content_type) @@ -248,6 +281,14 @@ def client(fixture_server: FixtureServer) -> Any: return FmpClient(base_url=fixture_server.base_url, path_prefix="", auth_mode="none") +@pytest.fixture +def async_client(fixture_server: FixtureServer) -> Any: + """An unauthenticated ``AsyncFmpClient`` pointed at ``fixture_server``.""" + from fmp import AsyncFmpClient + + return AsyncFmpClient(base_url=fixture_server.base_url, path_prefix="", auth_mode="none") + + @pytest.fixture def errors() -> SimpleNamespace: """The exception hierarchy under its public names (see ``_errors_namespace``).""" diff --git a/crates/fmp-py/tests/test_async_client.py b/crates/fmp-py/tests/test_async_client.py new file mode 100644 index 00000000..d49d2f83 --- /dev/null +++ b/crates/fmp-py/tests/test_async_client.py @@ -0,0 +1,362 @@ +"""Issue #422: ``AsyncFmpClient``, the asyncio twin of ``FmpClient``. + +Each test drives its coroutines with ``asyncio.run`` inside a plain test +function, so the suite needs no async pytest plugin. +""" + +import asyncio +import os +import signal +import time +from collections.abc import Callable, Coroutine +from types import SimpleNamespace +from typing import Any, TypeVar + +import pytest +from conftest import CSV_CONTENT_TYPE, FixtureServer, load_csv_fixture, load_fixture +from fmp import AsyncFmpClient, BinaryPayload, FmpClient +from fmp.quote import AsyncQuoteNamespace, Quote +from fmp.statements import AsyncStatementsNamespace +from fmp.statements.income import AsyncStatementsIncomeNamespace + +T = TypeVar("T") + +XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" +WORKBOOK = b"PK\x03\x04\xff\xfe\x80\x00 not utf-8" +QUOTE_SHORT_ROW = {"symbol": "AAPL", "price": 232.8, "change": 2.1, "volume": 44000000} + + +def run(coroutine: Coroutine[Any, Any, T]) -> T: + """Run ``coroutine`` to completion on a fresh event loop.""" + return asyncio.run(coroutine) + + +def wait_until(condition: Callable[[], bool], timeout: float = 5.0) -> bool: + """Poll ``condition`` until it holds or ``timeout`` seconds pass.""" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if condition(): + return True + time.sleep(0.02) + return condition() + + +def public_names(obj: Any) -> set[str]: + """The public attribute names of ``obj``'s type.""" + return {name for name in dir(type(obj)) if not name.startswith("_")} + + +def test_endpoint_methods_are_coroutines_returning_models(async_client: Any, fixture_server: FixtureServer) -> None: + """``await client.quote.full`` sends the same request and returns the same models as the sync client.""" + fixture_server.route("/quote", load_fixture("quote.json")) + coroutine = async_client.quote.full("AAPL") + assert asyncio.iscoroutine(coroutine) + + rows = run(coroutine) + + assert fixture_server.requests[0].target == "/quote?symbol=AAPL" + assert isinstance(rows[0], Quote) + assert rows[0].symbol == "AAPL" + + +def test_namespaces_are_the_async_generated_types(async_client: Any) -> None: + """Domain getters return the ``Async*Namespace`` classes, nested ones included.""" + assert isinstance(async_client.quote, AsyncQuoteNamespace) + assert isinstance(async_client.statements, AsyncStatementsNamespace) + assert isinstance(async_client.statements.income, AsyncStatementsIncomeNamespace) + + +def test_every_sync_method_has_an_async_twin(client: Any, async_client: Any) -> None: + """Both clients expose the same namespaces and methods, at every depth.""" + + def compare(sync: Any, asynchronous: Any, path: str) -> int: + assert public_names(asynchronous) - {"aclose"} == public_names(sync), path + compared = 0 + for name in sorted(public_names(sync)): + if name == "close": + continue + sync_member = getattr(sync, name) + async_member = getattr(asynchronous, name) + if callable(sync_member): + assert callable(async_member), f"{path}.{name}" + compared += 1 + else: + compared += compare(sync_member, async_member, f"{path}.{name}") + return compared + + assert compare(client, async_client, "client") > 250 + + +def test_constructor_signature_matches_the_sync_client() -> None: + """``AsyncFmpClient`` takes exactly the keywords ``FmpClient`` takes.""" + assert AsyncFmpClient.__text_signature__ == FmpClient.__text_signature__ + + +def test_missing_credential_raises_the_sync_config_error(errors: SimpleNamespace) -> None: + """Without a token or ``FMP_API_KEY`` the default host is rejected, naming the variable.""" + with pytest.raises(errors.FmpConfigError, match="FMP_API_KEY"): + AsyncFmpClient() + + +def test_gather_runs_requests_concurrently(async_client: Any, fixture_server: FixtureServer) -> None: + """Ten gathered calls against a 0.5 second route finish in about one route delay.""" + fixture_server.route("/quote-short", [QUOTE_SHORT_ROW], delay=0.5) + + async def fan_out() -> list[Any]: + return await asyncio.gather(*(async_client.quote.short("AAPL") for _ in range(10))) + + started = time.monotonic() + results = run(fan_out()) + elapsed = time.monotonic() - started + + assert [rows[0].symbol for rows in results] == ["AAPL"] * 10 + assert len(fixture_server.requests) == 10 + assert elapsed < 2.5, f"10 gathered calls took {elapsed:.2f}s; sequential would take 5s" + + +def test_event_loop_stays_responsive_while_a_request_is_in_flight( + async_client: Any, fixture_server: FixtureServer +) -> None: + """A ticker coroutine keeps running while one slow request is awaited.""" + fixture_server.route("/quote-short", [QUOTE_SHORT_ROW], delay=0.5) + + async def scenario() -> int: + ticks = 0 + + async def tick() -> None: + nonlocal ticks + while True: + await asyncio.sleep(0.01) + ticks += 1 + + ticker = asyncio.ensure_future(tick()) + await async_client.quote.short("AAPL") + ticker.cancel() + return ticks + + assert run(scenario()) >= 10 + + +def test_cancellation_aborts_the_in_flight_request(async_client: Any, fixture_server: FixtureServer) -> None: + """Cancelling the awaiting task raises ``CancelledError`` at once and drops the connection.""" + fixture_server.route("/quote-short", [QUOTE_SHORT_ROW], delay=1.0) + + async def scenario() -> float: + task = asyncio.ensure_future(async_client.quote.short("AAPL")) + while not fixture_server.requests: + await asyncio.sleep(0.01) + task.cancel() + started = time.monotonic() + with pytest.raises(asyncio.CancelledError): + await task + return time.monotonic() - started + + assert run(scenario()) < 0.5 + assert wait_until(lambda: fixture_server.disconnects == 1), "the server never saw the client hang up" + + +def test_wait_for_timeout_cancels_like_a_task_cancel(async_client: Any, fixture_server: FixtureServer) -> None: + """``asyncio.wait_for`` times out promptly and the request is aborted.""" + fixture_server.route("/quote-short", [QUOTE_SHORT_ROW], delay=1.0) + + async def scenario() -> None: + await asyncio.wait_for(async_client.quote.short("AAPL"), timeout=0.2) + + with pytest.raises(asyncio.TimeoutError): + run(scenario()) + assert wait_until(lambda: fixture_server.disconnects == 1) + + +def test_an_unawaited_coroutine_sends_nothing(async_client: Any, fixture_server: FixtureServer) -> None: + """The request starts on the first ``await``, not when the method is called.""" + coroutine = async_client.quote.short("AAPL") + coroutine.close() + + time.sleep(0.1) + assert fixture_server.requests == [] + + +def test_on_response_is_called_after_each_awaited_success(fixture_server: FixtureServer) -> None: + """The callback sees the endpoint, status and allowlisted headers, once per call.""" + fixture_server.route("/quote-short", [QUOTE_SHORT_ROW], headers=(("X-Proxy-Cache", "HIT"),)) + calls: list[tuple[str, int, dict[str, str]]] = [] + + def on_response(endpoint_id: str, status: int, headers: dict[str, str]) -> None: + calls.append((endpoint_id, status, headers)) + + client = AsyncFmpClient(base_url=fixture_server.base_url, path_prefix="", auth_mode="none", on_response=on_response) + + async def scenario() -> None: + await asyncio.gather(client.quote.short("AAPL"), client.quote.short("MSFT")) + + run(scenario()) + assert calls == [("quote-short", 200, {"x-proxy-cache": "HIT"})] * 2 + + +def test_on_response_exception_propagates_from_the_await(fixture_server: FixtureServer) -> None: + """A raising callback makes the coroutine raise that exception instead of returning data.""" + fixture_server.route("/quote-short", [QUOTE_SHORT_ROW]) + + def on_response(endpoint_id: str, status: int, headers: dict[str, str]) -> None: + raise LookupError(f"rejected {endpoint_id}") + + client = AsyncFmpClient(base_url=fixture_server.base_url, path_prefix="", auth_mode="none", on_response=on_response) + + with pytest.raises(LookupError, match="rejected quote-short"): + run(client.quote.short("AAPL")) + + +def test_errors_raise_the_sync_exception_classes( + async_client: Any, fixture_server: FixtureServer, errors: SimpleNamespace +) -> None: + """Status, decode and validation failures surface as the same structured exceptions.""" + fixture_server.route("/quote-short", {"Error Message": "denied"}, status=401) + fixture_server.route("/quote", b"not-json", content_type="text/plain") + + with pytest.raises(errors.FmpStatusError) as status: + run(async_client.quote.short("AAPL")) + assert status.value.status == 401 + with pytest.raises(errors.FmpDecodeError): + run(async_client.quote.full("AAPL")) + with pytest.raises(errors.FmpValidationError): + run(async_client.quote.full("")) + assert len(fixture_server.requests) == 2 + + +def test_closed_client_rejects_calls_through_existing_namespaces( + async_client: Any, fixture_server: FixtureServer, errors: SimpleNamespace +) -> None: + """After ``close()`` every call raises ``FmpConfigError``, even via a namespace fetched before.""" + namespace = async_client.quote + async_client.close() + async_client.close() + + with pytest.raises(errors.FmpConfigError, match="closed"): + run(namespace.short("AAPL")) + assert fixture_server.requests == [] + + +def test_aclose_is_awaitable_and_idempotent(async_client: Any, errors: SimpleNamespace) -> None: + """``await aclose()`` closes the client; a second close is a no-op.""" + + async def scenario() -> None: + await async_client.aclose() + await async_client.aclose() + await async_client.quote.short("AAPL") + + with pytest.raises(errors.FmpConfigError, match="closed"): + run(scenario()) + + +def test_async_with_yields_the_client_and_closes_it( + async_client: Any, fixture_server: FixtureServer, errors: SimpleNamespace +) -> None: + """``async with`` binds the client itself and closes it on exit, letting exceptions propagate.""" + fixture_server.route("/quote-short", [QUOTE_SHORT_ROW]) + + async def scenario() -> None: + async with async_client as bound: + assert bound is async_client + assert (await bound.quote.short("AAPL"))[0].symbol == "AAPL" + with pytest.raises(errors.FmpConfigError, match="closed"): + await async_client.quote.short("AAPL") + + run(scenario()) + + async def raising() -> None: + async with AsyncFmpClient(base_url=fixture_server.base_url, auth_mode="none"): + raise KeyError("from the block") + + with pytest.raises(KeyError, match="from the block"): + run(raising()) + + +def test_close_lets_an_in_flight_call_finish(async_client: Any, fixture_server: FixtureServer) -> None: + """A call already awaiting its response completes after ``close()``, like the sync client.""" + fixture_server.route("/quote-short", [QUOTE_SHORT_ROW], delay=0.3) + + async def scenario() -> Any: + task = asyncio.ensure_future(async_client.quote.short("AAPL")) + while not fixture_server.requests: + await asyncio.sleep(0.01) + await async_client.aclose() + return await task + + assert run(scenario())[0].symbol == "AAPL" + + +def test_xlsx_endpoint_awaits_a_binary_payload(async_client: Any, fixture_server: FixtureServer) -> None: + """``await statements.reports.xlsx`` returns the workbook bytes and media type.""" + fixture_server.route("/financial-reports-xlsx", WORKBOOK, content_type=XLSX) + + payload = run(async_client.statements.reports.xlsx("AAPL", 2022, "FY")) + + assert fixture_server.requests[0].target == "/financial-reports-xlsx?symbol=AAPL&year=2022&period=FY" + assert isinstance(payload, BinaryPayload) + assert payload.data == WORKBOOK + assert payload.content_type == XLSX + + +def test_bulk_csv_endpoint_awaits_decoded_rows(async_client: Any, fixture_server: FixtureServer) -> None: + """A bulk CSV method decodes its rows the same way as the sync client.""" + fixture_server.route("/profile-bulk", load_csv_fixture("bulk_company_profiles.csv"), content_type=CSV_CONTENT_TYPE) + + rows = run(async_client.bulk.company_profiles("0")) + + assert fixture_server.requests[0].target == "/profile-bulk?part=0" + assert len(rows) == 4 + assert rows[0].symbol == "WMB" + + +def test_dynamic_rows_are_awaited_as_dicts(async_client: Any, fixture_server: FixtureServer) -> None: + """An untyped-row endpoint returns plain ``dict`` rows from the coroutine.""" + fixture_server.route("/industry-classification-search", [{"symbol": "AAPL", "sicCode": "3571"}]) + + rows = run(async_client.sec_filings.search_industry_classifications(symbol="AAPL")) + + assert rows == [{"symbol": "AAPL", "sicCode": "3571"}] + + +def test_sync_and_async_clients_share_one_process( + client: Any, async_client: Any, fixture_server: FixtureServer +) -> None: + """A sync call inside a coroutine and an awaited call both succeed on the shared runtime.""" + fixture_server.route("/quote-short", [QUOTE_SHORT_ROW]) + + async def scenario() -> tuple[str, str]: + sync_rows = client.quote.short("AAPL") + async_rows = await async_client.quote.short("AAPL") + return sync_rows[0].symbol, async_rows[0].symbol + + assert run(scenario()) == ("AAPL", "AAPL") + + +@pytest.mark.skipif(not hasattr(os, "fork"), reason="os.fork is unavailable on this platform") +@pytest.mark.filterwarnings("ignore:This process .* is multi-threaded:DeprecationWarning") +def test_async_client_used_in_parent_works_after_fork(async_client: Any, fixture_server: FixtureServer) -> None: + """A forked child awaits on a fresh event loop with the client its parent already used.""" + fixture_server.route("/quote-short", [QUOTE_SHORT_ROW]) + assert run(async_client.quote.short("AAPL"))[0].symbol == "AAPL" + + read_fd, write_fd = os.pipe() + child_pid = os.fork() + if child_pid == 0: + os.close(read_fd) + try: + payload = asyncio.run(async_client.quote.short("AAPL"))[0].symbol.encode() + exit_code = 0 + except BaseException as error: + payload = f"{type(error).__name__}: {error}".encode() + exit_code = 1 + os.write(write_fd, payload) + os.close(write_fd) + os._exit(exit_code) + + os.close(write_fd) + if not wait_until(lambda: os.waitpid(child_pid, os.WNOHANG)[0] == child_pid, timeout=10): + os.kill(child_pid, signal.SIGKILL) + os.waitpid(child_pid, 0) + raise AssertionError("the post-fork await did not complete within 10 seconds") + payload = os.read(read_fd, 65_536) + os.close(read_fd) + assert payload == b"AAPL", payload.decode(errors="replace") diff --git a/wiki/python.md b/wiki/python.md index ba3a1e31..92efbc9b 100644 --- a/wiki/python.md +++ b/wiki/python.md @@ -41,9 +41,11 @@ valuation = client.dcf.custom("AAPL", beta=1.2, tax_rate=0.21) rows = client.sec_filings.search_industry_classifications(symbol="AAPL") ``` -The client is synchronous: each call releases the Python GIL while the async -Rust transport waits, so other threads keep running. A Python async facade is -not part of the current release. +`FmpClient` is synchronous: each call releases the Python GIL while the async +Rust transport waits, so other threads keep running. `AsyncFmpClient` is the +asyncio twin, with the same namespaces and `async def` methods (see the +[Asyncio client](../crates/fmp-py/README.md#asyncio-client) section of the +fmp-py README). ## Responses