libfmp 1.4.0

Async, typed Rust client for the Financial Modeling Prep API
Documentation
# libfmp

`libfmp` is an async, typed Rust client for the [Financial Modeling Prep (FMP)](https://financialmodelingprep.com/) data API.

The crate implements all 276 endpoint entries in the repository's captured API
documentation oracle through transport-independent descriptors, typed queries,
response models, and 271 `Client` methods. This is coverage of that pinned
oracle, not a claim that every endpoint currently or historically offered by
the provider is covered. The
separately distributed synchronous Python client (`fmp-py-sdk`, imported as
`fmp`) exposes all 271 methods as `client.<domain>.<method>(...)` namespaces
generated from these signatures; see
[crates/fmp-py/README.md](../fmp-py/README.md).

```rust
use std::env;

use libfmp::{Client, config::Authentication, types::Ticker};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::builder()
        .authentication(Authentication::fmp_header(env::var("FMP_API_KEY")?))
        .build()?;
    let rows = client.quote_short(Ticker::new("AAPL")?).await?;
    println!("{rows:#?}");
    Ok(())
}
```

The Rust builder never reads the environment on its own. When the key lives in
`FMP_API_KEY`, `Authentication::fmp_header_from_env()` returns the same header
authentication as `Option<Authentication>` (`None` when the variable is unset,
empty, or whitespace-only), so the `env::var` line above is the explicit form.

The documented bare JSON array remains a `Vec<QuoteShort>`; empty and multi-row responses keep their original shape.

## Using a proxy

A proxy that issues its own keys and forwards to FMP, such as
[valet](https://valet.bitbrew.app), needs two environment variables and no
code change:

```sh
export FMP_API_KEY=vk_...
export FMP_BASE_URL=https://valet.bitbrew.app/fmp
```

```rust
use libfmp::{Client, types::Ticker};

# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_env()?;
let rows = client.quote_short(Ticker::new("AAPL")?).await?;
# let _ = rows;
# Ok(())
# }
```

`Client::from_env()` (or `ClientBuilder::from_env()?` to keep configuring)
sends the key in the `apikey` header and keeps the base URL's path, so the
quote endpoint goes to `https://valet.bitbrew.app/fmp/stable/quote`. Without
`FMP_BASE_URL` it targets `https://financialmodelingprep.com`, and without
`FMP_API_KEY` it returns a `MissingCredential` configuration error. Both values
are trimmed and a blank value counts as unset. A plaintext `http://` base URL
to a non-loopback host is still refused unless the builder opts in with
`danger_allow_insecure_authentication(true)`. `Client::builder()` itself never
reads either variable.

To read the proxy's cache status and remaining daily budget on a success, run
the endpoint spec through `execute_with_metadata`. It returns a `Response`
with the decoded `data`, the HTTP `status` and the allowlisted, redacted
`headers` (the same allowlist as status errors, below):

```rust
use libfmp::{Client, endpoints::quote::{QuoteShortQuery, quote_short}, types::Ticker};

# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_env()?;
let spec = quote_short(QuoteShortQuery::new(Ticker::new("AAPL")?));
let response = client.execute_with_metadata(&spec).await?;
let cache = response.headers.get("x-proxy-cache"); // Some("HIT") or Some("MISS")
let remaining = response.headers.get("x-proxy-daily-remaining");
# let _ = (response.data, cache, remaining);
# Ok(())
# }
```

Every endpoint method has a public spec function under `libfmp::endpoints`,
and `execute` returns the same data without the metadata. On failure nothing
changes: the error already carries its headers.

To watch every call, endpoint methods included, register
`ClientBuilder::on_response(|info| ...)`. After each decoded success it gets a
`ResponseInfo` with the endpoint id, status and the same allowlisted headers.
It never fires on errors, cannot fail the call, and runs inline on the request
task, so keep it cheap and non-blocking.

## Custom router or proxy

Transport choices do not change endpoint code. A caller can select a custom base URL and path prefix, no auth, FMP header or query auth, bearer auth, custom secret header or query auth, and arbitrary default headers.

```rust
use std::env;

use libfmp::{Client, config::Authentication, types::Ticker};

# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
    .base_url(env::var("FMP_PROXY_BASE_URL")?)
    .path_prefix("router/stable")
    .authentication(Authentication::custom_header(
        "X-Proxy-Token",
        Some("Bearer ".to_owned()),
        env::var("FMP_PROXY_TOKEN")?,
    ))
    .default_header("X-Tenant", env::var("FMP_TENANT")?)
    .build()?;
let rows = client.quote_short(Ticker::new("AAPL")?).await?;
# let _ = rows;
# Ok(())
# }
```

A status error keeps an allowlisted set of response headers: `Retry-After`,
`X-Proxy-*` and `X-RateLimit-*`. `err.retry_after()` returns the delay as a
`Duration` (delta-seconds or HTTP-date, a past date is zero),
`err.proxy_error()` returns the proxy's `X-Proxy-Error` reason, and
`err.headers()` lists every retained header. No other header is stored, and
headers never appear in the error's `Display` text.

Rust 1.96 or newer is supported. The repository pins Rust 1.96.0 for development and release validation. This project is available under the [MIT License](LICENSE).