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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions crates/fmp-py-gen/src/bin/gen_namespaces/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ pub(crate) fn render(node: &Node, query_modules: &QueryModules) -> Result<Render
}

out.push_str("use std::sync::Arc;\n\n");
out.push_str("use libfmp::ClientBuilder;\n");
for (module, names) in &query_uses {
let _ = writeln!(out, "use {};", use_line("libfmp::endpoints", module, names));
}
Expand All @@ -115,8 +114,10 @@ pub(crate) fn render(node: &Node, query_modules: &QueryModules) -> Result<Render
for (module, names) in &model_uses {
let _ = writeln!(out, "use {};", use_line("crate::models", module, names));
}
if !methods.is_empty() {
out.push_str("use crate::runtime::block_on;\n");
if methods.is_empty() {
out.push_str("use crate::runtime::ClientHandle;\n");
} else {
out.push_str("use crate::runtime::{ClientHandle, block_on};\n");
}
if !node.children.is_empty() {
out.push('\n');
Expand All @@ -136,11 +137,11 @@ pub(crate) fn render(node: &Node, query_modules: &QueryModules) -> Result<Render
let _ = writeln!(out, "#[pyclass(module = \"fmp._native.{dotted}\", frozen)]");
let _ = writeln!(
out,
"pub(crate) struct {struct_name} {{\n builder: Arc<ClientBuilder>,\n}}\n"
"pub(crate) struct {struct_name} {{\n builder: Arc<ClientHandle>,\n}}\n"
);
let _ = writeln!(
out,
"impl {struct_name} {{\n pub(crate) fn new(builder: Arc<ClientBuilder>) -> Self {{\n Self {{ builder }}\n }}\n}}\n"
"impl {struct_name} {{\n pub(crate) fn new(builder: Arc<ClientHandle>) -> Self {{\n Self {{ builder }}\n }}\n}}\n"
);

out.push_str("#[gen_stub_pymethods]\n#[pymethods]\n");
Expand Down
7 changes: 4 additions & 3 deletions crates/fmp-py-gen/src/bin/gen_namespaces/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClientBuilder>) -> Self",
"pub(crate) fn new(builder: Arc<ClientHandle>) -> Self",
"use crate::runtime::{ClientHandle, block_on};",
"use libfmp::endpoints::quote::{",
"QuoteQuery, QuoteShortQuery",
"use crate::models::quote::{",
Expand Down Expand Up @@ -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<BinaryPayload> {",
"let query = financial_reports_xlsx_query(symbol, year, period)?;",
Expand Down Expand Up @@ -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<Vec<Py<PyAny>>> {",
"let query = search_industry_classifications_query(symbol, cik, sic_code)?;",
Expand Down
22 changes: 22 additions & 0 deletions crates/fmp-py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions crates/fmp-py/python/fmp/_native/__init__.pyi
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
42 changes: 38 additions & 4 deletions crates/fmp-py/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -171,10 +174,15 @@ fn positive_duration(value: f64, field: &'static str) -> PyResult<Duration> {
/// `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<ClientBuilder>,
pub(crate) builder: Arc<ClientHandle>,
}

#[gen_stub_pymethods]
Expand Down Expand Up @@ -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<Bound<'_, PyAny>>,
#[gen_stub(override_type(type_repr = "typing.Optional[builtins.BaseException]", imports = ("builtins", "typing")))]
exc_value: Option<Bound<'_, PyAny>>,
#[gen_stub(override_type(type_repr = "typing.Optional[types.TracebackType]", imports = ("types", "typing")))]
traceback: Option<Bound<'_, PyAny>>,
) -> PyResult<()> {
let _ = (exc_type, exc_value, traceback);
self.close()
}
}
7 changes: 3 additions & 4 deletions crates/fmp-py/src/namespaces/analyst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

use std::sync::Arc;

use libfmp::ClientBuilder;
use libfmp::endpoints::analyst::{
FinancialEstimatesQuery, HistoricalRatingsQuery, HistoricalStockGradesQuery,
PriceTargetConsensusQuery, PriceTargetSummaryQuery, RatingsSnapshotQuery, StockGradesQuery,
Expand All @@ -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<ClientBuilder>,
builder: Arc<ClientHandle>,
}

impl AnalystNamespace {
pub(crate) fn new(builder: Arc<ClientBuilder>) -> Self {
pub(crate) fn new(builder: Arc<ClientHandle>) -> Self {
Self { builder }
}
}
Expand Down
7 changes: 3 additions & 4 deletions crates/fmp-py/src/namespaces/bulk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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<ClientBuilder>,
builder: Arc<ClientHandle>,
}

impl BulkNamespace {
pub(crate) fn new(builder: Arc<ClientBuilder>) -> Self {
pub(crate) fn new(builder: Arc<ClientHandle>) -> Self {
Self { builder }
}
}
Expand Down
7 changes: 3 additions & 4 deletions crates/fmp-py/src/namespaces/calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

use std::sync::Arc;

use libfmp::ClientBuilder;
use libfmp::endpoints::calendar::{
DividendsCalendarQuery, DividendsQuery, EarningsCalendarQuery, EarningsQuery,
IposCalendarQuery, IposDisclosureQuery, IposProspectusQuery, StockSplitsCalendarQuery,
Expand All @@ -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<ClientBuilder>,
builder: Arc<ClientHandle>,
}

impl CalendarNamespace {
pub(crate) fn new(builder: Arc<ClientBuilder>) -> Self {
pub(crate) fn new(builder: Arc<ClientHandle>) -> Self {
Self { builder }
}
}
Expand Down
7 changes: 3 additions & 4 deletions crates/fmp-py/src/namespaces/chart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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<ClientBuilder>,
builder: Arc<ClientHandle>,
}

impl ChartNamespace {
pub(crate) fn new(builder: Arc<ClientBuilder>) -> Self {
pub(crate) fn new(builder: Arc<ClientHandle>) -> Self {
Self { builder }
}
}
Expand Down
7 changes: 3 additions & 4 deletions crates/fmp-py/src/namespaces/commitment_of_traders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,24 @@

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};

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<ClientBuilder>,
builder: Arc<ClientHandle>,
}

impl CommitmentOfTradersNamespace {
pub(crate) fn new(builder: Arc<ClientBuilder>) -> Self {
pub(crate) fn new(builder: Arc<ClientHandle>) -> Self {
Self { builder }
}
}
Expand Down
7 changes: 3 additions & 4 deletions crates/fmp-py/src/namespaces/commodities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -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<ClientBuilder>,
builder: Arc<ClientHandle>,
}

impl CommoditiesNamespace {
pub(crate) fn new(builder: Arc<ClientBuilder>) -> Self {
pub(crate) fn new(builder: Arc<ClientHandle>) -> Self {
Self { builder }
}
}
Expand Down
Loading
Loading