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 cbd47e66..d4c50bd3 100644 --- a/crates/fmp-py-gen/src/bin/gen_namespaces/emit.rs +++ b/crates/fmp-py-gen/src/bin/gen_namespaces/emit.rs @@ -94,7 +94,6 @@ pub(crate) fn render(node: &Node, query_modules: &QueryModules) -> Result Result Result,\n}}\n" + "pub(crate) struct {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 {struct_name} {{\n pub(crate) fn new(builder: Arc) -> Self {{\n Self {{ builder }}\n }}\n}}\n" ); out.push_str("#[gen_stub_pymethods]\n#[pymethods]\n"); 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 e68b99fe..bf3af0bb 100644 --- a/crates/fmp-py-gen/src/bin/gen_namespaces/tests.rs +++ b/crates/fmp-py-gen/src/bin/gen_namespaces/tests.rs @@ -65,7 +65,8 @@ fn quote_registry_renders_the_drop_in_namespace() { for needle in [ "#[pyclass(module = \"fmp._native.quote\", frozen)]", "pub(crate) struct QuoteNamespace {", - "pub(crate) fn new(builder: Arc) -> Self", + "pub(crate) fn new(builder: Arc) -> Self", + "use crate::runtime::{ClientHandle, block_on};", "use libfmp::endpoints::quote::{", "QuoteQuery, QuoteShortQuery", "use crate::models::quote::{", @@ -178,7 +179,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::block_on;\n", + "use crate::args;\nuse crate::binary::BinaryPayload;\nuse crate::errors::to_py_error;\nuse crate::runtime::{ClientHandle, block_on};\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)?;", @@ -219,7 +220,7 @@ 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::block_on;\n", + "use crate::args;\nuse crate::convert;\nuse crate::errors::to_py_error;\nuse crate::runtime::{ClientHandle, block_on};\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)?;", diff --git a/crates/fmp-py/README.md b/crates/fmp-py/README.md index 74fac430..3b5d52c3 100644 --- a/crates/fmp-py/README.md +++ b/crates/fmp-py/README.md @@ -163,6 +163,28 @@ except FmpStatusError as error: Bodies attached to errors are redacted before they reach Python: an echoed `apikey` query value shows as `[REDACTED]`. +## Closing a client + +`close()` releases the client's pooled HTTP connections. Use the client as a +context manager to close it deterministically: + +```python +from fmp import FmpClient + +with FmpClient() as client: + rows = client.quote.short("AAPL") +``` + +After `close()`, every call raises `FmpConfigError("the client is closed")`, +including calls through a namespace such as `client.quote` fetched before the +close; calls already in flight finish normally. Closing twice is a no-op, and +`__exit__` never swallows an exception from the block. The process-wide +runtime is shared by every client and stays up. + +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. + ## Custom router or proxy ```python diff --git a/crates/fmp-py/python/fmp/_native/__init__.pyi b/crates/fmp-py/python/fmp/_native/__init__.pyi index f36b81fd..0c09d90f 100644 --- a/crates/fmp-py/python/fmp/_native/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/__init__.pyi @@ -1,6 +1,7 @@ # This file is automatically generated by pyo3_stub_gen import builtins +import types import typing from fmp._native import analyst import fmp._native.analyst @@ -174,6 +175,11 @@ class FmpClient: `max_response_body_bytes` bounds each buffered response. Authenticated non-loopback HTTP requires `danger_allow_insecure_authentication=True`. Redirects are either disabled or restricted to the same origin. + + `close()` releases the client's pooled connections; afterwards every call, + including through a namespace fetched before the close, raises + `FmpConfigError`. Closing twice is a no-op, and `with FmpClient(...) as + client:` closes the client when the block exits. """ @property @@ -387,6 +393,26 @@ class FmpClient: danger_allow_insecure_authentication: builtins.bool = False, follow_redirects: typing.Optional[builtins.bool] = None, ) -> FmpClient: ... + def close(self) -> None: + r""" + Releases the pooled HTTP connections and rejects every later call with + `FmpConfigError`. Calls already in flight finish normally. Idempotent. + """ + + def __enter__(self) -> FmpClient: + r""" + Returns the client itself for use in a `with` block. + """ + + def __exit__( + 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. + """ def reveal_secret_urls() -> builtins.bool: diff --git a/crates/fmp-py/src/client.rs b/crates/fmp-py/src/client.rs index a8407fd0..b301b435 100644 --- a/crates/fmp-py/src/client.rs +++ b/crates/fmp-py/src/client.rs @@ -11,11 +11,14 @@ use libfmp::{ }; use pyo3::{ prelude::*, - types::{PyInt, PyMapping}, + types::{PyAny, PyInt, PyMapping}, }; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; -use crate::errors::to_py_error; +use crate::{ + errors::to_py_error, + runtime::{self, ClientHandle}, +}; fn invalid_configuration(message: &'static str) -> PyErr { to_py_error(libfmp::Error::configuration(message)) @@ -171,10 +174,15 @@ fn positive_duration(value: f64, field: &'static str) -> PyResult { /// `max_response_body_bytes` bounds each buffered response. Authenticated /// non-loopback HTTP requires `danger_allow_insecure_authentication=True`. /// Redirects are either disabled or restricted to the same origin. +/// +/// `close()` releases the client's pooled connections; afterwards every call, +/// including through a namespace fetched before the close, raises +/// `FmpConfigError`. Closing twice is a no-op, and `with FmpClient(...) as +/// client:` closes the client when the block exits. #[gen_stub_pyclass] #[pyclass(module = "fmp._native", frozen)] pub(crate) struct FmpClient { - pub(crate) builder: Arc, + pub(crate) builder: Arc, } #[gen_stub_pymethods] @@ -240,7 +248,33 @@ impl FmpClient { .build() .map_err(|error| build_error(auth_mode, error))?; Ok(Self { - builder: Arc::new(builder), + builder: ClientHandle::new(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) + } + + /// Returns the client itself for use in a `with` block. + fn __enter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + + /// Closes the client and lets any exception from the block propagate. + #[pyo3(signature = (exc_type, exc_value, traceback))] + fn __exit__( + &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); + self.close() + } } diff --git a/crates/fmp-py/src/namespaces/analyst.rs b/crates/fmp-py/src/namespaces/analyst.rs index 0bbdcaef..0a981c76 100644 --- a/crates/fmp-py/src/namespaces/analyst.rs +++ b/crates/fmp-py/src/namespaces/analyst.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::analyst::{ FinancialEstimatesQuery, HistoricalRatingsQuery, HistoricalStockGradesQuery, PriceTargetConsensusQuery, PriceTargetSummaryQuery, RatingsSnapshotQuery, StockGradesQuery, @@ -17,17 +16,17 @@ use crate::models::analyst::{ FinancialEstimate, HistoricalRating, HistoricalStockGrade, PriceTargetConsensus, PriceTargetSummary, RatingSnapshot, StockGrade, StockGradesSummary, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Analyst endpoints for a single client, exposed as `client.analyst`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.analyst", frozen)] pub(crate) struct AnalystNamespace { - builder: Arc, + builder: Arc, } impl AnalystNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/bulk.rs b/crates/fmp-py/src/namespaces/bulk.rs index 90dffbac..6ceadff5 100644 --- a/crates/fmp-py/src/namespaces/bulk.rs +++ b/crates/fmp-py/src/namespaces/bulk.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::bulk::{BulkEodQuery, BulkPartQuery, BulkStatementQuery, BulkYearQuery}; use pyo3::prelude::*; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -21,17 +20,17 @@ use crate::models::bulk::snapshots::{ BulkUpgradesDowngradesConsensus, }; use crate::models::company::CompanyProfile; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Bulk endpoints for a single client, exposed as `client.bulk`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.bulk", frozen)] pub(crate) struct BulkNamespace { - builder: Arc, + builder: Arc, } impl BulkNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/calendar.rs b/crates/fmp-py/src/namespaces/calendar.rs index a8646d59..c5ecd186 100644 --- a/crates/fmp-py/src/namespaces/calendar.rs +++ b/crates/fmp-py/src/namespaces/calendar.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::calendar::{ DividendsCalendarQuery, DividendsQuery, EarningsCalendarQuery, EarningsQuery, IposCalendarQuery, IposDisclosureQuery, IposProspectusQuery, StockSplitsCalendarQuery, @@ -16,17 +15,17 @@ use crate::errors::to_py_error; use crate::models::calendar::{ DividendEvent, EarningsEvent, IpoCalendarEvent, IpoDisclosure, IpoProspectus, StockSplitEvent, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Calendar endpoints for a single client, exposed as `client.calendar`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.calendar", frozen)] pub(crate) struct CalendarNamespace { - builder: Arc, + builder: Arc, } impl CalendarNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/chart.rs b/crates/fmp-py/src/namespaces/chart.rs index 279d622d..1836c93f 100644 --- a/crates/fmp-py/src/namespaces/chart.rs +++ b/crates/fmp-py/src/namespaces/chart.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::chart::{StockChartEodQuery, StockChartIntradayQuery}; use pyo3::prelude::*; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -12,17 +11,17 @@ use crate::errors::to_py_error; use crate::models::chart::{ StockChartAdjustedBar, StockChartFullBar, StockChartIntradayBar, StockChartLightBar, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Chart endpoints for a single client, exposed as `client.chart`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.chart", frozen)] pub(crate) struct ChartNamespace { - builder: Arc, + builder: Arc, } impl ChartNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/commitment_of_traders.rs b/crates/fmp-py/src/namespaces/commitment_of_traders.rs index f03fb9e2..4e0d6ed3 100644 --- a/crates/fmp-py/src/namespaces/commitment_of_traders.rs +++ b/crates/fmp-py/src/namespaces/commitment_of_traders.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::commitment_of_traders::CotQuery; use pyo3::prelude::*; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -10,17 +9,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Commitment of traders endpoints for a single client, exposed as `client.commitment_of_traders`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.commitment_of_traders", frozen)] pub(crate) struct CommitmentOfTradersNamespace { - builder: Arc, + builder: Arc, } impl CommitmentOfTradersNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/commodities.rs b/crates/fmp-py/src/namespaces/commodities.rs index 356d4a5a..88fe7994 100644 --- a/crates/fmp-py/src/namespaces/commodities.rs +++ b/crates/fmp-py/src/namespaces/commodities.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::asset_chart::AssetChartQuery; use libfmp::endpoints::quote::{QuoteQuery, QuoteShortQuery}; use pyo3::prelude::*; @@ -13,17 +12,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Commodities endpoints for a single client, exposed as `client.commodities`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.commodities", frozen)] pub(crate) struct CommoditiesNamespace { - builder: Arc, + builder: Arc, } impl CommoditiesNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/company.rs b/crates/fmp-py/src/namespaces/company.rs index f26af25e..5983d72d 100644 --- a/crates/fmp-py/src/namespaces/company.rs +++ b/crates/fmp-py/src/namespaces/company.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::company::{ AllSharesFloatQuery, BatchMarketCapitalizationQuery, CompanyNotesQuery, DelistedCompaniesQuery, EmployeeCountQuery, ExecutiveCompensationBenchmarkQuery, ExecutiveCompensationQuery, @@ -20,17 +19,17 @@ use crate::models::company::{ DelistedCompany, EmployeeCount, ExecutiveCompensation, ExecutiveCompensationBenchmark, MergerAcquisition, ShareFloat, StockPeer, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Company endpoints for a single client, exposed as `client.company`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.company", frozen)] pub(crate) struct CompanyNamespace { - builder: Arc, + builder: Arc, } impl CompanyNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/congressional.rs b/crates/fmp-py/src/namespaces/congressional.rs index 7317d70e..a8a3019a 100644 --- a/crates/fmp-py/src/namespaces/congressional.rs +++ b/crates/fmp-py/src/namespaces/congressional.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::congressional::{ CongressionalNetWorthAggregatedQuery, CongressionalNetWorthQuery, CongressionalPositionsQuery, CongressionalProfilesQuery, CongressionalTradesByMemberIdQuery, CongressionalTradesByNameQuery, @@ -17,17 +16,17 @@ use crate::models::congressional::{ CongressionalMemberNetWorth, CongressionalMemberNetWorthAggregate, CongressionalMemberPosition, CongressionalMemberProfile, CongressionalTrade, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Congressional endpoints for a single client, exposed as `client.congressional`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.congressional", frozen)] pub(crate) struct CongressionalNamespace { - builder: Arc, + builder: Arc, } impl CongressionalNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/crypto.rs b/crates/fmp-py/src/namespaces/crypto.rs index 57348cc3..f1fd4e33 100644 --- a/crates/fmp-py/src/namespaces/crypto.rs +++ b/crates/fmp-py/src/namespaces/crypto.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::asset_chart::AssetChartQuery; use libfmp::endpoints::quote::{QuoteQuery, QuoteShortQuery}; use pyo3::prelude::*; @@ -13,17 +12,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Crypto endpoints for a single client, exposed as `client.crypto`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.crypto", frozen)] pub(crate) struct CryptoNamespace { - builder: Arc, + builder: Arc, } impl CryptoNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/dcf.rs b/crates/fmp-py/src/namespaces/dcf.rs index 7e968e8f..c16a1f9f 100644 --- a/crates/fmp-py/src/namespaces/dcf.rs +++ b/crates/fmp-py/src/namespaces/dcf.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::dcf::{CustomDcfQuery, DcfAssumptions, DcfQuery}; use pyo3::prelude::*; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -10,17 +9,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Dcf endpoints for a single client, exposed as `client.dcf`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.dcf", frozen)] pub(crate) struct DcfNamespace { - builder: Arc, + builder: Arc, } impl DcfNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/directory.rs b/crates/fmp-py/src/namespaces/directory.rs index e06f7cc5..8f4fd49d 100644 --- a/crates/fmp-py/src/namespaces/directory.rs +++ b/crates/fmp-py/src/namespaces/directory.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::directory::{AvailableExchangesQuery, CikListQuery, SymbolChangesQuery}; use pyo3::prelude::*; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -14,17 +13,17 @@ use crate::models::directory::{ CikListing, CompanySymbol, EarningsTranscriptAvailability, EtfSymbol, FinancialStatementSymbol, SymbolChange, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Directory endpoints for a single client, exposed as `client.directory`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.directory", frozen)] pub(crate) struct DirectoryNamespace { - builder: Arc, + builder: Arc, } impl DirectoryNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/economics.rs b/crates/fmp-py/src/namespaces/economics.rs index 20099e15..821fda59 100644 --- a/crates/fmp-py/src/namespaces/economics.rs +++ b/crates/fmp-py/src/namespaces/economics.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::economics::{ EconomicCalendarQuery, EconomicIndicatorsQuery, TreasuryRatesQuery, }; @@ -14,17 +13,17 @@ use crate::errors::to_py_error; use crate::models::economics::{ EconomicCalendarEvent, EconomicIndicatorObservation, MarketRiskPremium, TreasuryRate, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Economics endpoints for a single client, exposed as `client.economics`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.economics", frozen)] pub(crate) struct EconomicsNamespace { - builder: Arc, + builder: Arc, } impl EconomicsNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/esg.rs b/crates/fmp-py/src/namespaces/esg.rs index b79758a3..76ddb946 100644 --- a/crates/fmp-py/src/namespaces/esg.rs +++ b/crates/fmp-py/src/namespaces/esg.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::esg::{EsgBenchmarkQuery, EsgSymbolQuery}; use pyo3::prelude::*; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -10,17 +9,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Esg endpoints for a single client, exposed as `client.esg`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.esg", frozen)] pub(crate) struct EsgNamespace { - builder: Arc, + builder: Arc, } impl EsgNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/forex.rs b/crates/fmp-py/src/namespaces/forex.rs index 2333532b..57087541 100644 --- a/crates/fmp-py/src/namespaces/forex.rs +++ b/crates/fmp-py/src/namespaces/forex.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::asset_chart::AssetChartQuery; use libfmp::endpoints::quote::{QuoteQuery, QuoteShortQuery}; use pyo3::prelude::*; @@ -13,17 +12,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Forex endpoints for a single client, exposed as `client.forex`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.forex", frozen)] pub(crate) struct ForexNamespace { - builder: Arc, + builder: Arc, } impl ForexNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/fundraising.rs b/crates/fmp-py/src/namespaces/fundraising.rs index d760334d..a3d46b4e 100644 --- a/crates/fmp-py/src/namespaces/fundraising.rs +++ b/crates/fmp-py/src/namespaces/fundraising.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::fundraising::{ LatestCrowdfundingOfferingsQuery, LatestRegulationDOfferingsQuery, OfferingByCikQuery, OfferingSearchQuery, @@ -16,17 +15,17 @@ use crate::models::fundraising::{ CrowdfundingOffering, CrowdfundingOfferingSearchResult, RegulationDOffering, RegulationDOfferingSearchResult, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Fundraising endpoints for a single client, exposed as `client.fundraising`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.fundraising", frozen)] pub(crate) struct FundraisingNamespace { - builder: Arc, + builder: Arc, } impl FundraisingNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/funds.rs b/crates/fmp-py/src/namespaces/funds.rs index 9bb3f83a..e09401cd 100644 --- a/crates/fmp-py/src/namespaces/funds.rs +++ b/crates/fmp-py/src/namespaces/funds.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::funds::{ EtfAssetExposureQuery, EtfCountryWeightingsQuery, EtfHoldingsQuery, EtfInfoQuery, EtfSectorWeightingsQuery, FundDisclosureDatesQuery, FundDisclosureHolderSearchQuery, @@ -18,17 +17,17 @@ use crate::models::funds::{ FundDisclosure, FundDisclosureHolder, FundDisclosureSearchResult, }; use crate::models::institutional_ownership::Form13fFilingDate; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Funds endpoints for a single client, exposed as `client.funds`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.funds", frozen)] pub(crate) struct FundsNamespace { - builder: Arc, + builder: Arc, } impl FundsNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/indexes.rs b/crates/fmp-py/src/namespaces/indexes.rs index 3faccdb5..20b8a524 100644 --- a/crates/fmp-py/src/namespaces/indexes.rs +++ b/crates/fmp-py/src/namespaces/indexes.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::indexes::IndexChartQuery; use libfmp::endpoints::quote::{QuoteQuery, QuoteShortQuery}; use pyo3::prelude::*; @@ -13,17 +12,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Indexes endpoints for a single client, exposed as `client.indexes`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.indexes", frozen)] pub(crate) struct IndexesNamespace { - builder: Arc, + builder: Arc, } impl IndexesNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/insider_trading.rs b/crates/fmp-py/src/namespaces/insider_trading.rs index 2a33337e..3020d55d 100644 --- a/crates/fmp-py/src/namespaces/insider_trading.rs +++ b/crates/fmp-py/src/namespaces/insider_trading.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::insider_trading::{ BeneficialOwnershipAcquisitionsQuery, InsiderReportingNameSearchQuery, InsiderTradeStatisticsQuery, InsiderTradesSearchQuery, LatestInsiderTradesQuery, @@ -16,17 +15,17 @@ use crate::models::insider_trading::{ BeneficialOwnershipAcquisition, InsiderReportingName, InsiderTrade, InsiderTradeStatistics, InsiderTransactionType, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Insider trading endpoints for a single client, exposed as `client.insider_trading`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.insider_trading", frozen)] pub(crate) struct InsiderTradingNamespace { - builder: Arc, + builder: Arc, } impl InsiderTradingNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/institutional_ownership.rs b/crates/fmp-py/src/namespaces/institutional_ownership.rs index b0c06ca6..0a23fea0 100644 --- a/crates/fmp-py/src/namespaces/institutional_ownership.rs +++ b/crates/fmp-py/src/namespaces/institutional_ownership.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::institutional_ownership::{ Form13fFilingDatesQuery, HolderIndustryBreakdownQuery, HolderPerformanceSummaryQuery, InstitutionalHolderAnalyticsQuery, InstitutionalIndustrySummaryQuery, @@ -19,17 +18,17 @@ use crate::models::institutional_ownership::{ InstitutionalHolderAnalytics, InstitutionalHolding, InstitutionalIndustrySummary, InstitutionalOwnershipFiling, InstitutionalPositionSummary, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Institutional ownership endpoints for a single client, exposed as `client.institutional_ownership`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.institutional_ownership", frozen)] pub(crate) struct InstitutionalOwnershipNamespace { - builder: Arc, + builder: Arc, } impl InstitutionalOwnershipNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/market.rs b/crates/fmp-py/src/namespaces/market.rs index 280ce507..3c95375f 100644 --- a/crates/fmp-py/src/namespaces/market.rs +++ b/crates/fmp-py/src/namespaces/market.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::market::{ HistoricalIndustryPeQuery, HistoricalIndustryPerformanceQuery, HistoricalSectorPeQuery, HistoricalSectorPerformanceQuery, IndustryPeSnapshotQuery, IndustryPerformanceSnapshotQuery, @@ -16,17 +15,17 @@ use crate::errors::to_py_error; use crate::models::market::{ IndustryPe, IndustryPerformance, MarketMover, SectorPe, SectorPerformance, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Market endpoints for a single client, exposed as `client.market`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.market", frozen)] pub(crate) struct MarketNamespace { - builder: Arc, + builder: Arc, } impl MarketNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/market_hours.rs b/crates/fmp-py/src/namespaces/market_hours.rs index 0f2aeb1b..58f8b80e 100644 --- a/crates/fmp-py/src/namespaces/market_hours.rs +++ b/crates/fmp-py/src/namespaces/market_hours.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::market_hours::{ AllExchangeMarketHoursQuery, ExchangeMarketHoursQuery, HolidaysByExchangeQuery, }; @@ -12,17 +11,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Market hours endpoints for a single client, exposed as `client.market_hours`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.market_hours", frozen)] pub(crate) struct MarketHoursNamespace { - builder: Arc, + builder: Arc, } impl MarketHoursNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/news.rs b/crates/fmp-py/src/namespaces/news.rs index 9f1e68e2..8c5ecbe0 100644 --- a/crates/fmp-py/src/namespaces/news.rs +++ b/crates/fmp-py/src/namespaces/news.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::news::{ ArticlesQuery, LatestCryptoNewsQuery, LatestForexNewsQuery, LatestGeneralNewsQuery, LatestPressReleasesQuery, LatestStockNewsQuery, SearchCryptoNewsQuery, SearchForexNewsQuery, @@ -14,17 +13,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// News endpoints for a single client, exposed as `client.news`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.news", frozen)] pub(crate) struct NewsNamespace { - builder: Arc, + builder: Arc, } impl NewsNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/quote.rs b/crates/fmp-py/src/namespaces/quote.rs index 45029e65..188824bc 100644 --- a/crates/fmp-py/src/namespaces/quote.rs +++ b/crates/fmp-py/src/namespaces/quote.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::quote::{ AftermarketQuoteQuery, AftermarketTradeQuery, BatchAftermarketQuoteQuery, BatchAftermarketTradeQuery, BatchQuoteQuery, BatchQuoteShortQuery, ExchangeQuotesQuery, @@ -16,17 +15,17 @@ use crate::errors::to_py_error; use crate::models::quote::{ AftermarketQuote, AftermarketTrade, Quote, QuoteShort, StockPriceChange, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Quote endpoints for a single client, exposed as `client.quote`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.quote", frozen)] pub(crate) struct QuoteNamespace { - builder: Arc, + builder: Arc, } impl QuoteNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/screener.rs b/crates/fmp-py/src/namespaces/screener.rs index efb0be23..f03bf3d3 100644 --- a/crates/fmp-py/src/namespaces/screener.rs +++ b/crates/fmp-py/src/namespaces/screener.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::screener::CompanyScreenerQuery; use pyo3::prelude::*; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -10,17 +9,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Screener endpoints for a single client, exposed as `client.screener`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.screener", frozen)] pub(crate) struct ScreenerNamespace { - builder: Arc, + builder: Arc, } impl ScreenerNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/search.rs b/crates/fmp-py/src/namespaces/search.rs index 619c878b..8a42da3a 100644 --- a/crates/fmp-py/src/namespaces/search.rs +++ b/crates/fmp-py/src/namespaces/search.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::search::{ CikSearchQuery, CusipSearchQuery, ExchangeVariantsQuery, IsinSearchQuery, NameSearchQuery, SymbolSearchQuery, @@ -16,17 +15,17 @@ use crate::models::search::{ CikSearchResult, CusipSearchResult, ExchangeVariant, IsinSearchResult, NameSearchResult, SymbolSearchResult, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Search endpoints for a single client, exposed as `client.search`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.search", frozen)] pub(crate) struct SearchNamespace { - builder: Arc, + builder: Arc, } impl SearchNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/sec_filings.rs b/crates/fmp-py/src/namespaces/sec_filings.rs index 6493108a..6fc4016a 100644 --- a/crates/fmp-py/src/namespaces/sec_filings.rs +++ b/crates/fmp-py/src/namespaces/sec_filings.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::sec_filings::{ AllIndustryClassificationsQuery, IndustryClassificationSearchQuery, IndustryClassificationsQuery, Latest8kSecFilingsQuery, LatestSecFilingsQuery, @@ -19,17 +18,17 @@ use crate::errors::to_py_error; use crate::models::sec_filings::{ SecCompanyProfile, SecCompanySearchResult, SecFiling, SicClassification, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Sec filings endpoints for a single client, exposed as `client.sec_filings`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.sec_filings", frozen)] pub(crate) struct SecFilingsNamespace { - builder: Arc, + builder: Arc, } impl SecFilingsNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/statements/as_reported.rs b/crates/fmp-py/src/namespaces/statements/as_reported.rs index 69c2d151..f62ec2e6 100644 --- a/crates/fmp-py/src/namespaces/statements/as_reported.rs +++ b/crates/fmp-py/src/namespaces/statements/as_reported.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::statements::as_reported::{ BalanceSheetStatementAsReportedQuery, CashFlowStatementAsReportedQuery, FinancialStatementFullAsReportedQuery, IncomeStatementAsReportedQuery, @@ -13,17 +12,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Statements as reported endpoints for a single client, exposed as `client.statements.as_reported`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.statements.as_reported", frozen)] pub(crate) struct StatementsAsReportedNamespace { - builder: Arc, + builder: Arc, } impl StatementsAsReportedNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/statements/balance.rs b/crates/fmp-py/src/namespaces/statements/balance.rs index 74bfd739..012c6c0a 100644 --- a/crates/fmp-py/src/namespaces/statements/balance.rs +++ b/crates/fmp-py/src/namespaces/statements/balance.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::statements::{BalanceSheetStatementQuery, BalanceSheetStatementTtmQuery}; use pyo3::prelude::*; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -10,17 +9,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Statements balance endpoints for a single client, exposed as `client.statements.balance`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.statements.balance", frozen)] pub(crate) struct StatementsBalanceNamespace { - builder: Arc, + builder: Arc, } impl StatementsBalanceNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/statements/cash_flow.rs b/crates/fmp-py/src/namespaces/statements/cash_flow.rs index b3f72feb..71ffcb5f 100644 --- a/crates/fmp-py/src/namespaces/statements/cash_flow.rs +++ b/crates/fmp-py/src/namespaces/statements/cash_flow.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::statements::{CashFlowStatementQuery, CashFlowStatementTtmQuery}; use pyo3::prelude::*; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -10,17 +9,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Statements cash flow endpoints for a single client, exposed as `client.statements.cash_flow`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.statements.cash_flow", frozen)] pub(crate) struct StatementsCashFlowNamespace { - builder: Arc, + builder: Arc, } impl StatementsCashFlowNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/statements/growth.rs b/crates/fmp-py/src/namespaces/statements/growth.rs index 14be0d6c..25eeb4d5 100644 --- a/crates/fmp-py/src/namespaces/statements/growth.rs +++ b/crates/fmp-py/src/namespaces/statements/growth.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::statements::growth::{ BalanceSheetStatementGrowthQuery, CashFlowStatementGrowthQuery, FinancialStatementGrowthQuery, IncomeStatementGrowthQuery, @@ -16,17 +15,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Statements growth endpoints for a single client, exposed as `client.statements.growth`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.statements.growth", frozen)] pub(crate) struct StatementsGrowthNamespace { - builder: Arc, + builder: Arc, } impl StatementsGrowthNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/statements/income.rs b/crates/fmp-py/src/namespaces/statements/income.rs index f0f28e28..e161d7e7 100644 --- a/crates/fmp-py/src/namespaces/statements/income.rs +++ b/crates/fmp-py/src/namespaces/statements/income.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::statements::{IncomeStatementQuery, IncomeStatementTtmQuery}; use pyo3::prelude::*; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -10,17 +9,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Statements income endpoints for a single client, exposed as `client.statements.income`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.statements.income", frozen)] pub(crate) struct StatementsIncomeNamespace { - builder: Arc, + builder: Arc, } impl StatementsIncomeNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/statements/metrics.rs b/crates/fmp-py/src/namespaces/statements/metrics.rs index aa5604cb..50bae88b 100644 --- a/crates/fmp-py/src/namespaces/statements/metrics.rs +++ b/crates/fmp-py/src/namespaces/statements/metrics.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::statements::{KeyMetricsQuery, KeyMetricsTtmQuery}; use pyo3::prelude::*; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -10,17 +9,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Statements metrics endpoints for a single client, exposed as `client.statements.metrics`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.statements.metrics", frozen)] pub(crate) struct StatementsMetricsNamespace { - builder: Arc, + builder: Arc, } impl StatementsMetricsNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/statements/mod.rs b/crates/fmp-py/src/namespaces/statements/mod.rs index a49b5273..b64cd49e 100644 --- a/crates/fmp-py/src/namespaces/statements/mod.rs +++ b/crates/fmp-py/src/namespaces/statements/mod.rs @@ -13,10 +13,11 @@ pub(crate) mod summaries; use std::sync::Arc; -use libfmp::ClientBuilder; use pyo3::prelude::*; 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; @@ -32,11 +33,11 @@ use self::summaries::StatementsSummariesNamespace; #[gen_stub_pyclass] #[pyclass(module = "fmp._native.statements", frozen)] pub(crate) struct StatementsNamespace { - builder: Arc, + builder: Arc, } impl StatementsNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/statements/ratios.rs b/crates/fmp-py/src/namespaces/statements/ratios.rs index 278ef15e..dd1648fb 100644 --- a/crates/fmp-py/src/namespaces/statements/ratios.rs +++ b/crates/fmp-py/src/namespaces/statements/ratios.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::statements::{FinancialRatiosQuery, FinancialRatiosTtmQuery}; use pyo3::prelude::*; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -10,17 +9,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Statements ratios endpoints for a single client, exposed as `client.statements.ratios`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.statements.ratios", frozen)] pub(crate) struct StatementsRatiosNamespace { - builder: Arc, + builder: Arc, } impl StatementsRatiosNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/statements/reports.rs b/crates/fmp-py/src/namespaces/statements/reports.rs index 5c8b4ce8..1b2643de 100644 --- a/crates/fmp-py/src/namespaces/statements/reports.rs +++ b/crates/fmp-py/src/namespaces/statements/reports.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::statements::reports::{ FinancialReportsDatesQuery, FinancialReportsJsonQuery, FinancialReportsXlsxQuery, }; @@ -13,17 +12,17 @@ use crate::args; use crate::binary::BinaryPayload; use crate::errors::to_py_error; use crate::models::statements::reports::{FinancialReportDate, FinancialReportJson}; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Statements reports endpoints for a single client, exposed as `client.statements.reports`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.statements.reports", frozen)] pub(crate) struct StatementsReportsNamespace { - builder: Arc, + builder: Arc, } impl StatementsReportsNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/statements/segmentation.rs b/crates/fmp-py/src/namespaces/statements/segmentation.rs index c217e8e1..f28a7e4b 100644 --- a/crates/fmp-py/src/namespaces/statements/segmentation.rs +++ b/crates/fmp-py/src/namespaces/statements/segmentation.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::statements::segmentation::{ RevenueGeographicSegmentationQuery, RevenueProductSegmentationQuery, }; @@ -12,17 +11,17 @@ 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::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Statements segmentation endpoints for a single client, exposed as `client.statements.segmentation`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.statements.segmentation", frozen)] pub(crate) struct StatementsSegmentationNamespace { - builder: Arc, + builder: Arc, } impl StatementsSegmentationNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/statements/summaries.rs b/crates/fmp-py/src/namespaces/statements/summaries.rs index bf006ff8..25390ffe 100644 --- a/crates/fmp-py/src/namespaces/statements/summaries.rs +++ b/crates/fmp-py/src/namespaces/statements/summaries.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::statements::{ EnterpriseValuesQuery, FinancialScoresQuery, LatestFinancialStatementsQuery, OwnerEarningsQuery, }; @@ -14,17 +13,17 @@ use crate::errors::to_py_error; use crate::models::statements::summaries::{ EnterpriseValue, FinancialScore, LatestFinancialStatement, OwnerEarnings, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Statements summaries endpoints for a single client, exposed as `client.statements.summaries`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.statements.summaries", frozen)] pub(crate) struct StatementsSummariesNamespace { - builder: Arc, + builder: Arc, } impl StatementsSummariesNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/technical_indicators.rs b/crates/fmp-py/src/namespaces/technical_indicators.rs index 11e9a0f3..cbf4b2f5 100644 --- a/crates/fmp-py/src/namespaces/technical_indicators.rs +++ b/crates/fmp-py/src/namespaces/technical_indicators.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::technical_indicators::TechnicalIndicatorQuery; use pyo3::prelude::*; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -14,17 +13,17 @@ use crate::models::technical_indicators::{ RelativeStrengthIndexBar, SimpleMovingAverageBar, StandardDeviationBar, TripleExponentialMovingAverageBar, WeightedMovingAverageBar, WilliamsBar, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Technical indicators endpoints for a single client, exposed as `client.technical_indicators`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.technical_indicators", frozen)] pub(crate) struct TechnicalIndicatorsNamespace { - builder: Arc, + builder: Arc, } impl TechnicalIndicatorsNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/tipranks.rs b/crates/fmp-py/src/namespaces/tipranks.rs index 749a441c..707e1516 100644 --- a/crates/fmp-py/src/namespaces/tipranks.rs +++ b/crates/fmp-py/src/namespaces/tipranks.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::tipranks::{ PointInTimeRatingsByAnalystQuery, PointInTimeRatingsBySymbolQuery, TipRanksAnalystSummaryQuery, TipRanksAnalystsQuery, TipRanksFirmSummaryQuery, TipRanksSearchQuery, @@ -17,17 +16,17 @@ use crate::models::tipranks::{ TipRanksAnalystProfile, TipRanksAnalystSummary, TipRanksFirmSummary, TipRanksPointInTimeRating, TipRanksRatingSearchResult, TipRanksSymbolSummary, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Tipranks endpoints for a single client, exposed as `client.tipranks`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.tipranks", frozen)] pub(crate) struct TipranksNamespace { - builder: Arc, + builder: Arc, } impl TipranksNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/namespaces/transcripts.rs b/crates/fmp-py/src/namespaces/transcripts.rs index db97b60b..7ae3b163 100644 --- a/crates/fmp-py/src/namespaces/transcripts.rs +++ b/crates/fmp-py/src/namespaces/transcripts.rs @@ -2,7 +2,6 @@ use std::sync::Arc; -use libfmp::ClientBuilder; use libfmp::endpoints::transcripts::{ EarningsTranscriptDatesQuery, EarningsTranscriptQuery, LatestEarningsTranscriptsQuery, }; @@ -14,17 +13,17 @@ use crate::errors::to_py_error; use crate::models::transcripts::{ EarningsTranscript, EarningsTranscriptDate, LatestEarningsTranscript, }; -use crate::runtime::block_on; +use crate::runtime::{ClientHandle, block_on}; /// Transcripts endpoints for a single client, exposed as `client.transcripts`. #[gen_stub_pyclass] #[pyclass(module = "fmp._native.transcripts", frozen)] pub(crate) struct TranscriptsNamespace { - builder: Arc, + builder: Arc, } impl TranscriptsNamespace { - pub(crate) fn new(builder: Arc) -> Self { + pub(crate) fn new(builder: Arc) -> Self { Self { builder } } } diff --git a/crates/fmp-py/src/runtime.rs b/crates/fmp-py/src/runtime.rs index be8e44b6..9408b498 100644 --- a/crates/fmp-py/src/runtime.rs +++ b/crates/fmp-py/src/runtime.rs @@ -13,7 +13,10 @@ use std::{ collections::HashMap, future::Future, mem, process, - sync::{Arc, Mutex, OnceLock, RwLock, Weak}, + sync::{ + Arc, Mutex, OnceLock, RwLock, Weak, + atomic::{AtomicBool, Ordering}, + }, }; use libfmp::{Client, ClientBuilder}; @@ -37,9 +40,32 @@ fn poisoned() -> pyo3::PyErr { internal_error("the fmp runtime lock was poisoned by a panicking thread") } -/// A client built for one `ClientBuilder`, retained while that builder lives. +/// The message raised by every call on a closed `FmpClient`. +const CLOSED_MESSAGE: &str = "the client is closed"; + +/// The configuration shared by one `FmpClient` and every namespace it hands +/// out, plus the flag `close()` sets so that all of them stop issuing requests. +pub(crate) struct ClientHandle { + builder: ClientBuilder, + closed: AtomicBool, +} + +impl ClientHandle { + pub(crate) fn new(builder: ClientBuilder) -> Arc { + Arc::new(Self { + builder, + closed: AtomicBool::new(false), + }) + } + + fn is_closed(&self) -> bool { + self.closed.load(Ordering::SeqCst) + } +} + +/// A client built for one `ClientHandle`, retained while that handle lives. struct CachedClient { - owner: Weak, + owner: Weak, client: Client, } @@ -67,29 +93,42 @@ impl Inner { })) } - /// Return a cached client for `builder`, building and caching one on miss. + /// Return a cached client for `handle`, building and caching one on miss. /// /// The cache mutex is held only for the lookup and insert, never across the /// `block_on` that follows, so concurrent callers on other threads are not - /// serialised by their network work. - fn client(&self, builder: &Arc) -> PyResult { + /// serialised by their network work. The closed flag is read under the same + /// mutex [`close`] takes to evict, so a closed handle is never re-cached. + fn client(&self, handle: &Arc) -> PyResult { let mut clients = self.clients.lock().map_err(|_| poisoned())?; + if handle.is_closed() { + return Err(configuration_error(CLOSED_MESSAGE)); + } clients.retain(|_, cached| cached.owner.upgrade().is_some()); - let id = Arc::as_ptr(builder) as usize; + let id = Arc::as_ptr(handle) as usize; if let Some(cached) = clients.get(&id) { return Ok(cached.client.clone()); } - let client = builder.as_ref().clone().build().map_err(to_py_error)?; + let client = handle.builder.clone().build().map_err(to_py_error)?; clients.insert( id, CachedClient { - owner: Arc::downgrade(builder), + owner: Arc::downgrade(handle), client: client.clone(), }, ); Ok(client) } + fn evict(&self, handle: &Arc) -> PyResult<()> { + let evicted = { + let mut clients = self.clients.lock().map_err(|_| poisoned())?; + clients.remove(&(Arc::as_ptr(handle) as usize)) + }; + drop(evicted); + Ok(()) + } + /// Drive `future` to completion on the shared runtime. /// /// The caller must not be a runtime worker thread; [`block_on`] enforces @@ -163,24 +202,50 @@ fn current() -> PyResult> { /// Run `operation` against a cached client on the shared runtime. /// -/// Returns a configuration error when called from inside a runtime worker -/// thread, where `Runtime::block_on` would panic. Callers release the GIL (via -/// `Python::detach`) around this so other Python threads make progress. -pub(crate) fn block_on(builder: Arc, operation: F) -> PyResult +/// Returns a configuration error when `handle` is closed, or when called from +/// inside a runtime worker thread, where `Runtime::block_on` would panic. +/// Callers release the GIL (via `Python::detach`) around this so other Python +/// threads make progress. +pub(crate) fn block_on(handle: Arc, operation: F) -> PyResult where F: FnOnce(Client) -> Fut, Fut: Future, { + if handle.is_closed() { + return Err(configuration_error(CLOSED_MESSAGE)); + } if Handle::try_current().is_ok() { return Err(configuration_error( "cannot call a synchronous fmp method from inside the fmp runtime", )); } let inner = current()?; - let client = inner.client(&builder)?; + let client = inner.client(&handle)?; Ok(inner.block_on(operation(client))) } +/// 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 +/// stays up. Only this process generation's pool is touched: a forked child +/// rebuilds its pool lazily and sees the closed flag before any request. +pub(crate) fn close(handle: &Arc) -> PyResult<()> { + if handle.closed.swap(true, Ordering::SeqCst) { + return Ok(()); + } + let inner = { + let guard = slot().read().map_err(|_| poisoned())?; + guard + .as_ref() + .filter(|inner| inner.owner_pid == process::id()) + .cloned() + }; + match inner { + Some(inner) => inner.evict(handle), + None => Ok(()), + } +} + #[cfg(all(test, unix))] mod fork_tests { use super::*; diff --git a/crates/fmp-py/tests/test_client.py b/crates/fmp-py/tests/test_client.py index b17021f0..f3953007 100644 --- a/crates/fmp-py/tests/test_client.py +++ b/crates/fmp-py/tests/test_client.py @@ -311,3 +311,70 @@ def test_quote_short_decodes_the_fractional_volume_observed_live( assert rows[0].volume == pytest.approx(20_201_922.82733) assert rows[0].price == pytest.approx(342.395) assert rows[0].change == pytest.approx(3.415) + + +def _proxy(fixture_server: FixtureServer) -> FmpClient: + fixture_server.route(QUOTE_SHORT_PATH, load_fixture("quote_short.json")) + return FmpClient(base_url=fixture_server.base_url, path_prefix="", auth_mode="none") + + +def test_with_block_returns_the_client_and_closes_it( + fixture_server: FixtureServer, errors: SimpleNamespace +) -> None: + """``with`` yields the client itself and closes it on exit.""" + client = _proxy(fixture_server) + with client as entered: + assert entered is client + assert entered.quote.short("AAPL")[0].symbol == "AAPL" + + with pytest.raises(errors.FmpConfigError) as raised: + client.quote.short("AAPL") + error = raised.value + assert str(error) == "the client is closed" + assert error.category == "configuration" + assert (error.endpoint, error.status, error.body) == (None, None, None) + assert len(fixture_server.requests) == 1 + + +def test_close_rejects_namespaces_fetched_before_it(fixture_server: FixtureServer, errors: SimpleNamespace) -> None: + """A namespace captured before ``close()`` shares the closed state.""" + client = _proxy(fixture_server) + quote = client.quote + assert quote.short("AAPL")[0].symbol == "AAPL" + client.close() + + with pytest.raises(errors.FmpConfigError, match="the client is closed"): + quote.short("AAPL") + assert len(fixture_server.requests) == 1 + + +def test_close_is_idempotent_and_works_before_any_call( + fixture_server: FixtureServer, errors: SimpleNamespace +) -> None: + """Closing an unused client twice is a no-op; calls still raise.""" + client = _proxy(fixture_server) + assert client.close() is None + assert client.close() is None + assert client.__exit__(None, None, None) is None + with pytest.raises(errors.FmpConfigError, match="the client is closed"): + client.quote.short("AAPL") + assert fixture_server.requests == [] + + +def test_with_block_propagates_exceptions_and_still_closes( + fixture_server: FixtureServer, errors: SimpleNamespace +) -> None: + """``__exit__`` never swallows the block's exception.""" + client = _proxy(fixture_server) + with pytest.raises(LookupError), client: + raise LookupError("from the block") + with pytest.raises(errors.FmpConfigError, match="the client is closed"): + client.quote.short("AAPL") + + +def test_closing_one_client_leaves_others_usable(fixture_server: FixtureServer) -> None: + """Close is per client: the shared runtime keeps serving other clients.""" + closed = _proxy(fixture_server) + other = FmpClient(base_url=fixture_server.base_url, path_prefix="", auth_mode="none") + closed.close() + assert other.quote.short("AAPL")[0].symbol == "AAPL" diff --git a/crates/libfmp/src/query.rs b/crates/libfmp/src/query.rs index 2e5ccca3..eccb18f6 100644 --- a/crates/libfmp/src/query.rs +++ b/crates/libfmp/src/query.rs @@ -6,6 +6,8 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use crate::types::StringValueError; +/// Every wire enum is deliberately `#[non_exhaustive]`, closed-looking sets included, +/// so the provider can add values without a breaking release. macro_rules! wire_enum { ( $(#[$meta:meta])* diff --git a/sdk/go/README.md b/sdk/go/README.md index 9b4985c4..f2b46a9c 100644 --- a/sdk/go/README.md +++ b/sdk/go/README.md @@ -75,6 +75,11 @@ append the secret as the last query pair of every request. `WithBaseURL` and `WithPathPrefix` route the client through a proxy; `WithHTTPClient` injects a caller-owned `*http.Client` for tests and custom transports. +`client.CloseIdleConnections()` closes the idle keep-alive connections of the +transport `NewClient` built; the client stays usable. It is a no-op with +`WithHTTPClient`, whose transport stays with the caller. There is no `Close`: +a `Client` owns nothing beyond that pool. + `CustomHeaderWithPrefix(name, prefix, secret)` places non-secret text immediately before the secret, byte for byte: no separator is inserted, so a `"Bearer "` prefix carries its own trailing space. It mirrors the Rust diff --git a/sdk/go/client.go b/sdk/go/client.go index b203ad72..d701d440 100644 --- a/sdk/go/client.go +++ b/sdk/go/client.go @@ -162,6 +162,7 @@ type Client struct { redactor *Redactor topology []string httpClient *http.Client + ownsTransport bool timeout time.Duration maxResponseBodyBytes int64 redirectPolicy RedirectPolicy @@ -235,7 +236,8 @@ func NewClient(opts ...Option) (*Client, error) { } httpClient := cfg.httpClient - if httpClient == nil { + ownsTransport := httpClient == nil + if ownsTransport { httpClient = newHTTPClient(cfg.connectTimeout) } else { httpClient = installRedirectPolicy(httpClient) @@ -249,6 +251,7 @@ func NewClient(opts ...Option) (*Client, error) { redactor: redactor, topology: networkTopology(baseURL, transportProxy(httpClient)), httpClient: httpClient, + ownsTransport: ownsTransport, timeout: cfg.timeout, maxResponseBodyBytes: cfg.maxResponseBodyBytes, redirectPolicy: cfg.redirectPolicy, @@ -257,6 +260,18 @@ func NewClient(opts ...Option) (*Client, error) { return client, nil } +// CloseIdleConnections closes the idle keep-alive connections of the +// transport NewClient built for this client. It is a no-op when WithHTTPClient +// supplied the *http.Client, whose transport and lifecycle stay with the +// caller. The client remains usable: a later request dials a new connection. +// There is no Close: a Client owns no resources beyond that connection pool, +// which net/http also reclaims after its idle timeout. +func (c *Client) CloseIdleConnections() { + if c.ownsTransport { + c.httpClient.CloseIdleConnections() + } +} + // Format prints the configuration shape without URLs, header values, or secrets. func (c Client) Format(f fmt.State, _ rune) { names := make([]string, 0, len(c.defaultHeaders)) diff --git a/sdk/go/client_test.go b/sdk/go/client_test.go index 9d56380c..084b1028 100644 --- a/sdk/go/client_test.go +++ b/sdk/go/client_test.go @@ -6,11 +6,13 @@ import ( "fmt" "io" "log" + "net" "net/http" "net/http/httptest" "strings" "sync" "testing" + "time" ) const probeEndpoint = "endpoint-contract-test" @@ -327,3 +329,85 @@ func TestClientFormattingNeverPrintsConfiguration(t *testing.T) { } } } + +func TestCloseIdleConnectionsClosesTheOwnedPool(t *testing.T) { + t.Parallel() + var mu sync.Mutex + dials := 0 + closed := make(chan struct{}, 1) + server := httptest.NewUnstartedServer(jsonHandler(`[{"symbol":"AAPL"}]`)) + server.Config.ConnState = func(_ net.Conn, state http.ConnState) { + switch state { + case http.StateNew: + mu.Lock() + dials++ + mu.Unlock() + case http.StateClosed: + select { + case closed <- struct{}{}: + default: + } + } + } + server.Start() + t.Cleanup(server.Close) + client, err := NewClient(WithBaseURL(server.URL+"/router"), WithPathPrefix("stable")) + if err != nil { + t.Fatalf("NewClient: %v", err) + } + dialCount := func() int { + mu.Lock() + defer mu.Unlock() + return dials + } + + for range 2 { + if _, err := probe(t, client); err != nil { + t.Fatal(err) + } + } + if got := dialCount(); got != 1 { + t.Fatalf("dials before CloseIdleConnections = %d, want 1 reused connection", got) + } + client.CloseIdleConnections() + select { + case <-closed: + case <-time.After(5 * time.Second): + t.Fatal("the idle connection was not closed") + } + if _, err := probe(t, client); err != nil { + t.Fatalf("client unusable after CloseIdleConnections: %v", err) + } + if got := dialCount(); got != 2 { + t.Fatalf("dials after CloseIdleConnections = %d, want a fresh connection", got) + } +} + +type idleCountingTransport struct { + http.RoundTripper + mu sync.Mutex + calls int +} + +func (t *idleCountingTransport) CloseIdleConnections() { + t.mu.Lock() + defer t.mu.Unlock() + t.calls++ +} + +func TestCloseIdleConnectionsLeavesACallerTransportAlone(t *testing.T) { + t.Parallel() + server, _ := newServer(t, jsonHandler(`[{"symbol":"AAPL"}]`)) + transport := &idleCountingTransport{RoundTripper: server.Client().Transport} + client := newClient(t, server, WithHTTPClient(&http.Client{Transport: transport})) + + if _, err := probe(t, client); err != nil { + t.Fatal(err) + } + client.CloseIdleConnections() + transport.mu.Lock() + defer transport.mu.Unlock() + if transport.calls != 0 { + t.Fatalf("caller transport CloseIdleConnections called %d times", transport.calls) + } +} diff --git a/wiki/go.md b/wiki/go.md index 37f7692c..d03abc9e 100644 --- a/wiki/go.md +++ b/wiki/go.md @@ -29,6 +29,11 @@ append the secret as the last query pair. See [Building a client](../sdk/go/README.md#building-a-client) for the option list and the environment variables the live tests read. +`client.CloseIdleConnections()` releases the idle connections of the +SDK-built transport and leaves the client usable; with `WithHTTPClient` it is +a no-op, because the caller owns that transport. A `Client` owns no other +resources, so there is no `Close`. + ## Custom routers and proxies `WithBaseURL` and `WithPathPrefix` route the client through a proxy, and diff --git a/wiki/python.md b/wiki/python.md index 76c40a3b..ba3a1e31 100644 --- a/wiki/python.md +++ b/wiki/python.md @@ -89,6 +89,28 @@ provider and transport failures map to the hierarchy under `fmp.errors`, with `FmpError` as the base class. Bodies attached to errors are redacted before they reach Python. +## Closing a client + +`close()` releases the client's pooled HTTP connections. Use the client as a +context manager to close it deterministically: + +```python +from fmp import FmpClient + +with FmpClient() as client: + rows = client.quote.short("AAPL") +``` + +After `close()`, every call raises `FmpConfigError("the client is closed")`, +including calls through a namespace such as `client.quote` fetched before the +close; calls already in flight finish normally. Closing twice is a no-op, and +`__exit__` never swallows an exception from the block. The process-wide +runtime is shared by every client and stays up. + +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. + ## Custom routers and proxies Endpoint code is independent of transport configuration. The client supports