diff --git a/CLAUDE.md b/CLAUDE.md index 2d15621..7fc106d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,12 +103,34 @@ pub struct SomeRequest { ... } - `rust` feature — `bon` builder pattern for ergonomic Rust usage ### Error Handling -`SdkError` (`crates/core/src/errors.rs`) uses `thiserror` with three variants: -- `Http` — wraps `reqwest::Error` +`SdkError` (`crates/core/src/errors.rs`) uses `thiserror` with five variants: +- `Http` — wraps `reqwest::Error` (further classified via `SdkError::http_kind()` → `HttpKind::{Timeout, Connect, Other}`) - `Api` — non-2xx response with status code and raw body - `Decode` — JSON parse failure with raw body for debugging - -Language bindings convert `SdkError` to native exceptions: `PyValueError` (Python), `napi::Error` (Node.js), `magnus::Error` / `RuntimeError` (Ruby). +- `UrlParse` — invalid URL (wraps `url::ParseError`) +- `Config` — invalid configuration (string message) + +Each binding exposes a typed exception hierarchy rooted at a shared base class so callers can `rescue` / `catch` / `except` by category. The mapping is: + +| `SdkError` variant | Python / Ruby class | Node class | Base | +|---|---|---|---| +| `Config`, `UrlParse` | `ConfigError` | `ConfigError` | `QuickNodeError` | +| `Http` + `HttpKind::Timeout` | `TimeoutError` | `TimeoutError` | `HttpError` | +| `Http` + `HttpKind::Connect` | `ConnectionError` | `ConnectionError` | `HttpError` | +| `Http` + `HttpKind::Other` | `HttpError` | `HttpError` | `QuickNodeError` | +| `Api { status, body }` | `ApiError` (with `.status`, `.body`) | `ApiError` (with `.status`, `.body`) | `QuickNodeError` | +| `Decode { body, .. }` | `DecodeError` (with `.body`) | `DecodeError` (with `.body`) | `QuickNodeError` | + +Each binding owns its mapping in a dedicated `errors.rs` file: +- **Python** — `crates/python/src/errors.rs` uses `create_exception!` macros; `map_sdk_err` sets `.status` / `.body` attributes via `setattr` on the exception instance. Exceptions are registered on the module in `add_to_module`. +- **Node** — `crates/node/src/errors.rs` encodes the variant, status, and body into a tagged message (`[||]\x1f`) because napi-rs only supports plain `napi::Error`. The JS wrapper `npm/errors.js` (`fromNapiError` + `wrapClient` Proxy) parses the prefix and rethrows as the typed subclass. All client methods must be wrapped via `wrapClient` so sync throws and rejected promises are both re-tagged. +- **Ruby** — `crates/ruby/src/errors.rs` uses `module.define_error` to build the class hierarchy under `QuickNodeSdk::Error`; `map_err` instantiates the class and sets `@status` / `@body` ivars (exposed via `attr_reader`-style methods). Classes are captured once in a `OnceLock>`. + +When adding a new `SdkError` variant: +1. Add the variant to `crates/core/src/errors.rs` and update `http_kind()` if it's transport-level. +2. Update the `match` in each binding's `map_*_err` function — the compiler will flag missing arms in Python and Ruby (Node's match is also exhaustive on the kind string). +3. If the new variant should surface as a new exception class, add it to all three bindings + `npm/errors.js` + the exports in `python/sdk/__init__.py` + `npm/sdk.d.ts` + `npm/sdk.mjs`. +4. Update examples in all four languages to demonstrate the new class if user-facing. ### Python Binding Pattern `crates/python/src/lib.rs` wraps core async methods using `pyo3_async_runtimes::tokio::future_into_py`. The Python API accepts individual keyword arguments instead of structs. @@ -148,6 +170,9 @@ Core clients are tested using mocked API calls with wiremock. All functions maki ### Error handling - Library constructors should return `Result`, not panic — use `.unwrap()` or `.expect()` only in examples and tests, never in library code - Validate numeric config values before casting between signed/unsigned types (e.g., check `>= 0` before `i64 as u64`) +- Map `SdkError` at the binding boundary only — keep core code returning `Result<_, SdkError>`, never a language-specific exception type. See the Error Handling section above for the typed exception hierarchy and how to add a new variant. +- When a binding needs new error metadata (status, body, retry info, etc.), add it to the `SdkError` variant first, then surface it on the exception class in each binding (PyO3 `setattr`, Ruby `ivar_set`, Node tagged-message prefix). +- Exception-raising tests belong in each language's example script (`crates/core/examples/admin_e2e.rs`, `python/examples/admin.py`, `npm/examples/admin.ts`, `ruby/examples/admin_e2e.rb`) — assert on the typed class, `status`, and `body` so regressions in the mapping layer fail loudly. ### Backwards Compatability - If the release is still an 0.1.z release, we don't need to worry about backwards compatability as this is a greenfield project diff --git a/README.md b/README.md index 4b6f24d..b58edbf 100644 --- a/README.md +++ b/README.md @@ -3300,52 +3300,74 @@ qn.kvstore.delete_list(key: "my-list") ## Error Handling -The core SDK defines `SdkError` (`crates/core/src/errors.rs`) with these variants: - -- `Http` — transport failure (wraps `reqwest::Error`). -- `Api { status, body }` — non-2xx HTTP response, carrying the status code and raw response body. -- `Decode { source, body }` — response was 2xx but JSON parsing failed; `body` holds the raw payload for debugging. -- `Config` — misconfiguration surfaced at construction time. - -Each language binding maps these to its native exception type: - -- **Rust**: `Result` — pattern-match on the variants. -- **Python**: raises `ValueError` (`PyValueError`) with the error message. -- **Node.js**: rejects with a napi-wrapped `Error` carrying the message. -- **Ruby**: raises `RuntimeError` for SDK errors and `ArgumentError` for missing/unknown Hash keys or bad types. - -```rust -// Rust -match qn.streams.get_stream("missing").await { - Ok(stream) => println!("{}", stream.name), - Err(SdkError::Api { status, body }) => eprintln!("api {status}: {body}"), - Err(e) => eprintln!("other error: {e}"), +Every binding exposes a typed exception hierarchy derived from the core `SdkError` +enum (`crates/core/src/errors.rs`). Catch the base class (`QuickNodeError` / +`QuickNodeSdk::Error` / `SdkError`) for any SDK-originated failure, or a specific +subclass to branch on transport vs. API semantics. + +| Logical class | When it fires | Extra fields | +|----------------------|-------------------------------------------------------------|----------------------| +| `QuickNodeError` | base class; catches everything below | — | +| `ConfigError` | invalid config or URL surfaced at construction time | — | +| `HttpError` | transport failure that isn't a timeout/connect | — | +| `TimeoutError` | request timed out (subclass of `HttpError`) | — | +| `ConnectionError` | connection refused / DNS / TLS (subclass of `HttpError`) | — | +| `ApiError` | non-2xx HTTP response | `status`, `body` | +| `DecodeError` | 2xx response but JSON parse failed | `body` | + +Per-language names: + +- **Rust** — pattern-match on `SdkError { Http, Api, Decode, UrlParse, Config }`; use `err.http_kind()` to classify `Http` into `Timeout`, `Connect`, or `Other`. +- **Python** — `QuickNodeError`, `ConfigError`, `HttpError`, `TimeoutError`, `ConnectionError`, `ApiError`, `DecodeError` (importable from `sdk`). +- **Node.js** — same class names, importable from `@quicknode/sdk`, all extend `Error`. +- **Ruby** — `QuickNodeSdk::Error`, `QuickNodeSdk::ConfigError`, `QuickNodeSdk::HttpError`, `QuickNodeSdk::TimeoutError`, `QuickNodeSdk::ConnectionError`, `QuickNodeSdk::ApiError`, `QuickNodeSdk::DecodeError`; all extend `StandardError`. Hash-key validation still raises `ArgumentError`. + +```rust +// Rust +match qn.admin.show_endpoint("missing").await { + Ok(resp) => println!("{:?}", resp.data), + Err(SdkError::Api { status, body }) if status.as_u16() == 404 => { + eprintln!("not found: {body}") + } + Err(e) if matches!(e.http_kind(), Some(HttpKind::Timeout)) => eprintln!("timed out"), + Err(e) => eprintln!("other: {e}"), } ``` ```python # Python +from sdk import ApiError, TimeoutError try: - await qn.streams.get_stream("missing") -except ValueError as e: - print(f"sdk error: {e}") + await qn.admin.show_endpoint("missing") +except ApiError as e: + if e.status == 404: + print(f"not found: {e.body}") + else: + raise +except TimeoutError: + print("timed out") ``` ```typescript // Node.js +import { ApiError, TimeoutError } from "@quicknode/sdk"; try { - await qn.streams.getStream("missing"); + await qn.admin.showEndpoint("missing"); } catch (e) { - console.error("sdk error:", e); + if (e instanceof ApiError && e.status === 404) console.error("not found:", e.body); + else if (e instanceof TimeoutError) console.error("timed out"); + else throw e; } ``` ```ruby # Ruby begin - qn.streams.get_stream(id: "missing") -rescue => e - warn "sdk error: #{e.message}" + qn.admin.show_endpoint(id: "missing") +rescue QuickNodeSdk::ApiError => e + warn "api #{e.status}: #{e.body}" if e.status == 404 +rescue QuickNodeSdk::TimeoutError + warn "timed out" end ``` diff --git a/crates/core/examples/admin_e2e.rs b/crates/core/examples/admin_e2e.rs index 7a0f4c9..f6c1d3e 100644 --- a/crates/core/examples/admin_e2e.rs +++ b/crates/core/examples/admin_e2e.rs @@ -10,7 +10,8 @@ use sdk_core::{ UpdateRateLimitsRequest, UpdateRequestFilterRequest, UpdateSecurityOptionsRequest, UpdateTeamEndpointsRequest, }, - QuickNodeSdk, SdkFullConfig, + errors::SdkError, + AdminConfig, HttpConfig, QuickNodeSdk, SdkFullConfig, }; #[tokio::main] @@ -539,7 +540,7 @@ async fn main() { &endpoint_id, &UpdateRateLimitsRequest { rate_limits: RateLimitSettings { - rps: Some(10), + rps: Some(3), ..Default::default() }, }, @@ -793,4 +794,42 @@ async fn main() { Ok(()) => println!("archive_endpoint: ok"), Err(e) => eprintln!("archive_endpoint error: {e}"), } + + // --- Error handling --- + + // 1) API error path — 404 on a bogus endpoint id. + match qn.admin.show_endpoint("does-not-exist").await { + Err(SdkError::Api { status, body }) => { + println!("api error {status}: {}", &body[..body.len().min(80)]); + assert_eq!(status.as_u16(), 404); + } + other => eprintln!("expected Api 404, got {other:?}"), + } + + // 2) Timeout path — unreachable base URL + 1s timeout forces a timeout + // from reqwest, which maps to SdkError::Http with http_kind() == Timeout. + let blackhole = SdkFullConfig { + api_key: config.api_key.clone(), + http: Some(HttpConfig { + timeout_secs: Some(1), + pool_max_idle_per_host: None, + }), + admin: Some(AdminConfig { + base_url: Some("http://10.255.255.1/".to_string()), + }), + streams: None, + webhooks: None, + kvstore: None, + }; + let tiny = QuickNodeSdk::new(&blackhole).expect("build tiny sdk"); + match tiny + .admin + .get_endpoints(&GetEndpointsRequest::default()) + .await + { + Err(e) if matches!(e.http_kind(), Some(sdk_core::errors::HttpKind::Timeout)) => { + println!("timed out as expected"); + } + other => eprintln!("expected timeout, got {other:?}"), + } } diff --git a/crates/core/src/errors.rs b/crates/core/src/errors.rs index 2ef8d5b..70f17bb 100644 --- a/crates/core/src/errors.rs +++ b/crates/core/src/errors.rs @@ -22,3 +22,57 @@ pub enum SdkError { #[error("Configuration error: {0}")] Config(String), } + +// Classifies a transport-level HTTP failure. Bindings use this to pick a +// typed exception subclass (TimeoutError / ConnectionError / HttpError) so the +// reqwest predicate logic lives in one place. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HttpKind { + Timeout, + Connect, + Other, +} + +impl SdkError { + pub fn http_kind(&self) -> Option { + match self { + SdkError::Http(e) if e.is_timeout() => Some(HttpKind::Timeout), + SdkError::Http(e) if e.is_connect() => Some(HttpKind::Connect), + SdkError::Http(_) => Some(HttpKind::Other), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn api_error_display_includes_status_and_body() { + let err = SdkError::Api { + status: reqwest::StatusCode::NOT_FOUND, + body: "not found".to_string(), + }; + let s = err.to_string(); + assert!(s.contains("404"), "expected 404 in {s}"); + assert!(s.contains("not found"), "expected body in {s}"); + } + + #[test] + fn config_error_display() { + let err = SdkError::Config("missing api key".to_string()); + assert!(err.to_string().contains("missing api key")); + } + + #[test] + #[allow(clippy::unwrap_used)] + fn http_kind_none_for_non_http_variants() { + assert!(SdkError::Config("x".to_string()).http_kind().is_none()); + let decode_err = SdkError::Decode { + source: serde_json::from_str::("bad").unwrap_err(), + body: "bad".to_string(), + }; + assert!(decode_err.http_kind().is_none()); + } +} diff --git a/crates/node/src/errors.rs b/crates/node/src/errors.rs new file mode 100644 index 0000000..65db478 --- /dev/null +++ b/crates/node/src/errors.rs @@ -0,0 +1,36 @@ +use napi::{bindgen_prelude::Error, Status}; +use sdk_core::errors::{HttpKind, SdkError}; + +// napi-rs can only throw plain napi::Error with a status + message. To give +// callers a typed class hierarchy in JS, we encode the variant as a structured +// prefix in the message; the JS-side wrapper (npm/sdk.js) parses the prefix +// and rethrows as the matching typed class (ApiError / TimeoutError / ...). +// +// Wire format: "[||]" +// - kind: one of Config | Http | Timeout | Connect | Api | Decode +// - status: u16 for Api, "-" otherwise +// - body_len: byte length of body blob appended after message (for Api/Decode), "-" otherwise +// The body bytes are appended after a "\x1f" (unit separator) so JS can split cleanly. +#[allow(clippy::needless_pass_by_value)] +pub fn map_sdk_err(e: SdkError) -> Error { + let msg = e.to_string(); + let (kind, status, body) = match &e { + SdkError::Config(_) | SdkError::UrlParse(_) => ("Config", None, None), + SdkError::Api { status, body } => ("Api", Some(status.as_u16()), Some(body.clone())), + SdkError::Decode { body, .. } => ("Decode", None, Some(body.clone())), + SdkError::Http(_) => match e.http_kind() { + Some(HttpKind::Timeout) => ("Timeout", None, None), + Some(HttpKind::Connect) => ("Connect", None, None), + _ => ("Http", None, None), + }, + }; + let status_s = status.map_or("-".to_string(), |s| s.to_string()); + let body_s = body.as_deref().unwrap_or(""); + let body_len = if body.is_some() { + body_s.len().to_string() + } else { + "-".to_string() + }; + let tagged = format!("[{kind}|{status_s}|{body_len}]{msg}\u{001f}{body_s}"); + Error::new(Status::GenericFailure, tagged) +} diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 1512f40..467b83d 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -2,6 +2,7 @@ use napi::bindgen_prelude::*; use napi_derive::napi; use sdk_core as core; +mod errors; mod streams_destination; // ── Top-level SDK ────────────────────────────────────────────── @@ -20,8 +21,7 @@ impl QuickNodeSdk { #[napi(constructor)] #[allow(clippy::needless_pass_by_value)] pub fn new(config: core::SdkFullConfig) -> Result { - let sdk_config = - core::SdkConfig::new(&config).map_err(|e| Error::from_reason(e.to_string()))?; + let sdk_config = core::SdkConfig::new(&config).map_err(errors::map_sdk_err)?; Ok(Self { admin: AdminApiClient { inner: core::admin::AdminApiClient::new(sdk_config.clone()), @@ -74,7 +74,7 @@ impl QuickNodeSdk { }, kvstore: KvStoreApiClient { inner: sdk.kvstore }, }) - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } } @@ -102,7 +102,7 @@ impl AdminApiClient { self.inner .get_endpoints(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Creates a new endpoint for a given blockchain and network. Requires @@ -118,7 +118,7 @@ impl AdminApiClient { self.inner .create_endpoint(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns details for a specific endpoint by ID. @@ -127,7 +127,7 @@ impl AdminApiClient { self.inner .show_endpoint(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Updates editable fields on an endpoint (e.g. its label). Returns a @@ -142,7 +142,7 @@ impl AdminApiClient { self.inner .update_endpoint(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Archives an endpoint. The API uses `DELETE` but the effect is archival @@ -152,7 +152,7 @@ impl AdminApiClient { self.inner .archive_endpoint(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Pauses or unpauses an endpoint by setting its status to `active` or @@ -166,7 +166,7 @@ impl AdminApiClient { self.inner .update_endpoint_status(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Creates a new tag on a specific endpoint from a label. Returns the new @@ -181,7 +181,7 @@ impl AdminApiClient { self.inner .create_tag(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Removes a tag from a specific endpoint by tag id. @@ -190,7 +190,7 @@ impl AdminApiClient { self.inner .delete_tag(&id, &tag_id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns account RPC usage totals for an optional time range. The @@ -205,7 +205,7 @@ impl AdminApiClient { self.inner .get_usage(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns RPC usage broken down per endpoint over an optional time range. @@ -220,7 +220,7 @@ impl AdminApiClient { self.inner .get_usage_by_endpoint(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns RPC usage grouped by method over an optional time range. Each @@ -235,7 +235,7 @@ impl AdminApiClient { self.inner .get_usage_by_method(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns RPC usage grouped by chain over an optional time range. Each @@ -249,7 +249,7 @@ impl AdminApiClient { self.inner .get_usage_by_chain(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns activity logs for a specific endpoint. Supports filtering by @@ -265,7 +265,7 @@ impl AdminApiClient { self.inner .get_endpoint_logs(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns the raw request and response payloads for a specific log entry @@ -280,7 +280,7 @@ impl AdminApiClient { self.inner .get_log_details(&id, &request_id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns the security options for an endpoint — an object of security @@ -293,7 +293,7 @@ impl AdminApiClient { self.inner .get_security_options(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Updates which security features are enabled on an endpoint. Each option @@ -309,7 +309,7 @@ impl AdminApiClient { self.inner .update_security_options(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Generates a new authentication token for an endpoint. @@ -318,7 +318,7 @@ impl AdminApiClient { self.inner .create_token(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Revokes a token on an endpoint by token id. @@ -331,7 +331,7 @@ impl AdminApiClient { self.inner .delete_token(&id, &token_id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Adds a referrer to an endpoint's security settings, specifying which @@ -346,7 +346,7 @@ impl AdminApiClient { self.inner .create_referrer(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Removes a referrer from an endpoint's security settings by referrer id. @@ -359,7 +359,7 @@ impl AdminApiClient { self.inner .delete_referrer(&id, &referrer_id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Adds an IP address to an endpoint's security whitelist. @@ -373,7 +373,7 @@ impl AdminApiClient { self.inner .create_ip(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Removes an IP address from an endpoint's security whitelist by ip id. @@ -386,7 +386,7 @@ impl AdminApiClient { self.inner .delete_ip(&id, &ip_id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Adds a domain mask to an endpoint — a custom domain used to hide the @@ -402,7 +402,7 @@ impl AdminApiClient { self.inner .create_domain_mask(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Removes a domain mask from an endpoint by domain mask id. @@ -415,7 +415,7 @@ impl AdminApiClient { self.inner .delete_domain_mask(&id, &domain_mask_id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Creates a new JWT for endpoint authentication. Accepts a public key, @@ -430,7 +430,7 @@ impl AdminApiClient { self.inner .create_jwt(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Removes a JWT from an endpoint's security configuration by jwt id, @@ -440,7 +440,7 @@ impl AdminApiClient { self.inner .delete_jwt(&id, &jwt_id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Creates a request filter on an endpoint — a method whitelist that @@ -456,7 +456,7 @@ impl AdminApiClient { self.inner .create_request_filter(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Updates an existing request filter on an endpoint, replacing the @@ -472,7 +472,7 @@ impl AdminApiClient { self.inner .update_request_filter(&id, &request_filter_id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Removes a request filter from an endpoint's security configuration by @@ -482,7 +482,7 @@ impl AdminApiClient { self.inner .delete_request_filter(&id, &request_filter_id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Enables multichain functionality on an endpoint, allowing a single @@ -492,7 +492,7 @@ impl AdminApiClient { self.inner .enable_multichain(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Disables multichain functionality on an endpoint. @@ -501,7 +501,7 @@ impl AdminApiClient { self.inner .disable_multichain(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Sets the custom HTTP header used to identify the client IP for an @@ -517,7 +517,7 @@ impl AdminApiClient { self.inner .create_or_update_ip_custom_header(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Removes the custom IP header configuration from an endpoint. @@ -529,7 +529,7 @@ impl AdminApiClient { self.inner .delete_ip_custom_header(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns the method rate limits configured on an endpoint, including @@ -542,7 +542,7 @@ impl AdminApiClient { self.inner .get_method_rate_limits(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Creates a per-method rate limit on an endpoint. A method rate limit @@ -557,7 +557,7 @@ impl AdminApiClient { self.inner .create_method_rate_limit(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Updates an existing method rate limit on an endpoint. Accepts the @@ -573,7 +573,7 @@ impl AdminApiClient { self.inner .update_method_rate_limit(&id, &method_rate_limit_id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Removes a method rate limit from an endpoint by method rate limit id. @@ -586,7 +586,7 @@ impl AdminApiClient { self.inner .delete_method_rate_limit(&id, &method_rate_limit_id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Updates the overall rate limits on an endpoint. Accepts `rps` @@ -601,7 +601,7 @@ impl AdminApiClient { self.inner .update_rate_limits(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns time-series metrics for a specific endpoint. Requires a @@ -616,7 +616,7 @@ impl AdminApiClient { self.inner .get_endpoint_metrics(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns aggregated metrics across all endpoints on the account. Accepts @@ -630,17 +630,14 @@ impl AdminApiClient { self.inner .get_account_metrics(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns all chains supported by QuickNode along with their networks. /// Each entry includes the chain slug and its network slugs and names. #[napi] pub async fn list_chains(&self) -> Result { - self.inner - .list_chains() - .await - .map_err(|e| Error::from_reason(e.to_string())) + self.inner.list_chains().await.map_err(errors::map_sdk_err) } /// Returns the account's invoices, including id, status, billing reason, @@ -651,7 +648,7 @@ impl AdminApiClient { self.inner .list_invoices() .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns all payments on the account, including amount, status, card @@ -661,17 +658,14 @@ impl AdminApiClient { self.inner .list_payments() .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns all teams on the account. Each team includes its id, name, /// member count, and member details (roles, contact info, account status). #[napi] pub async fn list_teams(&self) -> Result { - self.inner - .list_teams() - .await - .map_err(|e| Error::from_reason(e.to_string())) + self.inner.list_teams().await.map_err(errors::map_sdk_err) } /// Creates a new team. Requires a `name`; returns the new team with its @@ -684,17 +678,14 @@ impl AdminApiClient { self.inner .create_team(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns a specific team by id, including active members with their /// roles and contact info plus any pending invites. #[napi] pub async fn get_team(&self, id: i64) -> Result { - self.inner - .get_team(id) - .await - .map_err(|e| Error::from_reason(e.to_string())) + self.inner.get_team(id).await.map_err(errors::map_sdk_err) } /// Deletes a team by id. The team must have no members before it can be @@ -704,7 +695,7 @@ impl AdminApiClient { self.inner .delete_team(id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns the endpoints accessible to a given team. Each entry includes @@ -717,7 +708,7 @@ impl AdminApiClient { self.inner .list_team_endpoints(id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Assigns or unassigns endpoints for a team. Pass an array of endpoint ids @@ -732,7 +723,7 @@ impl AdminApiClient { self.inner .update_team_endpoints(id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Invites a user to a team by email. For new users, `full_name` and @@ -747,7 +738,7 @@ impl AdminApiClient { self.inner .invite_team_member(id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Removes a user from a team by team id and user id. @@ -762,7 +753,7 @@ impl AdminApiClient { self.inner .remove_team_member(id, user_id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Resends the invitation email to a pending team member, identified by @@ -776,7 +767,7 @@ impl AdminApiClient { self.inner .resend_team_invite(id, user_id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Pauses or unpauses multiple endpoints in a single call. Accepts an @@ -790,7 +781,7 @@ impl AdminApiClient { self.inner .bulk_update_endpoint_status(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Applies a single tag label to multiple endpoints in one call. Returns @@ -804,7 +795,7 @@ impl AdminApiClient { self.inner .bulk_add_tag(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Removes a tag from multiple endpoints in one call, identified by an @@ -817,17 +808,14 @@ impl AdminApiClient { self.inner .bulk_remove_tag(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns all account-level tags, including tags with zero associated /// endpoints. Each tag includes its id, label, and endpoint usage count. #[napi] pub async fn list_tags(&self) -> Result { - self.inner - .list_tags() - .await - .map_err(|e| Error::from_reason(e.to_string())) + self.inner.list_tags().await.map_err(errors::map_sdk_err) } /// Updates the label of an account tag. Because the tag is shared across @@ -841,7 +829,7 @@ impl AdminApiClient { self.inner .rename_tag(id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Deletes an account-level tag. The tag must first be removed from all @@ -854,7 +842,7 @@ impl AdminApiClient { self.inner .delete_account_tag(id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns RPC usage grouped by endpoint tag over an optional time range. @@ -869,7 +857,7 @@ impl AdminApiClient { self.inner .get_usage_by_tag(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns the full security configuration for an endpoint in a single @@ -884,7 +872,7 @@ impl AdminApiClient { self.inner .get_endpoint_security(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } } @@ -915,7 +903,7 @@ impl StreamsApiClient { .inner .create_stream(&core_params) .await - .map_err(|e| Error::from_reason(e.to_string()))?; + .map_err(errors::map_sdk_err)?; streams_destination::StreamNode::from_core(stream) } @@ -937,7 +925,7 @@ impl StreamsApiClient { .inner .list_streams(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string()))?; + .map_err(errors::map_sdk_err)?; streams_destination::ListStreamsResponseNode::from_core(resp) } @@ -948,7 +936,7 @@ impl StreamsApiClient { self.inner .delete_all_streams() .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns a single stream by ID, including its full configuration and @@ -959,7 +947,7 @@ impl StreamsApiClient { .inner .get_stream(&id) .await - .map_err(|e| Error::from_reason(e.to_string()))?; + .map_err(errors::map_sdk_err)?; streams_destination::StreamNode::from_core(stream) } @@ -976,7 +964,7 @@ impl StreamsApiClient { .inner .update_stream(&id, &core_params) .await - .map_err(|e| Error::from_reason(e.to_string()))?; + .map_err(errors::map_sdk_err)?; streams_destination::StreamNode::from_core(stream) } @@ -986,7 +974,7 @@ impl StreamsApiClient { self.inner .delete_stream(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Activates a stream by ID, resuming delivery from its current position. @@ -995,7 +983,7 @@ impl StreamsApiClient { self.inner .activate_stream(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Pauses a stream by ID, halting delivery until it is activated again. @@ -1004,7 +992,7 @@ impl StreamsApiClient { self.inner .pause_stream(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Runs a filter function against a specified block on a given network and @@ -1018,7 +1006,7 @@ impl StreamsApiClient { self.inner .test_filter(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns the total count of currently enabled (active) streams on the @@ -1031,7 +1019,7 @@ impl StreamsApiClient { self.inner .get_enabled_count(stream_type.as_deref()) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } } @@ -1059,7 +1047,7 @@ impl WebhooksApiClient { self.inner .list_webhooks(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Removes every webhook on the account. Destructive and takes no @@ -1069,7 +1057,7 @@ impl WebhooksApiClient { self.inner .delete_all_webhooks() .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Fetches a single webhook's full configuration and status by ID. Returns @@ -1082,7 +1070,7 @@ impl WebhooksApiClient { self.inner .get_webhook(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Modifies an existing webhook's configuration. Supports updating the @@ -1101,7 +1089,7 @@ impl WebhooksApiClient { self.inner .update_webhook(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Permanently removes a single webhook by ID. @@ -1110,7 +1098,7 @@ impl WebhooksApiClient { self.inner .delete_webhook(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Pauses a webhook by ID so it stops delivering events until reactivated. @@ -1119,7 +1107,7 @@ impl WebhooksApiClient { self.inner .pause_webhook(&id) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Activates a previously created or paused webhook so it begins (or @@ -1135,7 +1123,7 @@ impl WebhooksApiClient { self.inner .activate_webhook(&id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns the total number of enabled webhooks currently configured on @@ -1145,7 +1133,7 @@ impl WebhooksApiClient { self.inner .get_enabled_count() .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Creates a new webhook from a predefined filter template. Requires a @@ -1163,7 +1151,7 @@ impl WebhooksApiClient { self.inner .create_webhook_from_template(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Updates an existing template-backed webhook, modifying its template @@ -1181,7 +1169,7 @@ impl WebhooksApiClient { self.inner .update_webhook_template(&webhook_id, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } } @@ -1201,7 +1189,7 @@ impl KvStoreApiClient { self.inner .create_set(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns a paginated page of key/value entries from the store. Use the @@ -1215,16 +1203,13 @@ impl KvStoreApiClient { self.inner .get_sets(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns the string value stored for a single set by key. #[napi] pub async fn get_set(&self, key: String) -> Result { - self.inner - .get_set(&key) - .await - .map_err(|e| Error::from_reason(e.to_string())) + self.inner.get_set(&key).await.map_err(errors::map_sdk_err) } /// Adds and removes multiple sets in a single request. Either `add_sets`, @@ -1234,7 +1219,7 @@ impl KvStoreApiClient { self.inner .bulk_sets(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Removes a single set by key. @@ -1243,7 +1228,7 @@ impl KvStoreApiClient { self.inner .delete_set(&key) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Creates a new list under the given key, seeded with the provided items. @@ -1252,7 +1237,7 @@ impl KvStoreApiClient { self.inner .create_list(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns a paginated page of list keys from the store. Use the response @@ -1266,7 +1251,7 @@ impl KvStoreApiClient { self.inner .get_lists(¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Returns a paginated page of items from the list identified by `key`. @@ -1281,7 +1266,7 @@ impl KvStoreApiClient { self.inner .get_list(&key, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Updates an existing list by adding and/or removing items in a single @@ -1295,7 +1280,7 @@ impl KvStoreApiClient { self.inner .update_list(&key, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Appends a single item to the list identified by `key`. @@ -1308,7 +1293,7 @@ impl KvStoreApiClient { self.inner .add_list_item(&key, ¶ms) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Checks whether the specified list contains the given item. @@ -1321,7 +1306,7 @@ impl KvStoreApiClient { self.inner .list_contains_item(&key, &item) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Removes a specific item from the list identified by `key`. @@ -1330,7 +1315,7 @@ impl KvStoreApiClient { self.inner .delete_list_item(&key, &item) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } /// Removes a list and all of its items by key. @@ -1339,6 +1324,6 @@ impl KvStoreApiClient { self.inner .delete_list(&key) .await - .map_err(|e| Error::from_reason(e.to_string())) + .map_err(errors::map_sdk_err) } } diff --git a/crates/python/src/errors.rs b/crates/python/src/errors.rs new file mode 100644 index 0000000..2c65d2d --- /dev/null +++ b/crates/python/src/errors.rs @@ -0,0 +1,68 @@ +use pyo3::{ + create_exception, + exceptions::{PyException, PyValueError}, + prelude::*, +}; +use sdk_core::errors::{HttpKind, SdkError}; + +// Invalid-argument parse errors for user-supplied strings (enum values, etc.) +// stay as PyValueError — they're argument errors, not SDK errors. +#[allow(clippy::needless_pass_by_value)] +pub fn map_parse_err(e: serde_json::Error) -> PyErr { + PyValueError::new_err(e.to_string()) +} + +create_exception!(_core, QuickNodeError, PyException); +create_exception!(_core, ConfigError, QuickNodeError); +create_exception!(_core, HttpError, QuickNodeError); +create_exception!(_core, TimeoutError, HttpError); +create_exception!(_core, ConnectionError, HttpError); +create_exception!(_core, ApiError, QuickNodeError); +create_exception!(_core, DecodeError, QuickNodeError); + +#[allow(clippy::needless_pass_by_value)] +pub fn map_sdk_err(e: SdkError) -> PyErr { + let msg = e.to_string(); + match &e { + SdkError::Config(_) | SdkError::UrlParse(_) => ConfigError::new_err(msg), + SdkError::Api { status, body } => { + // Stash status/body on the exception instance so callers can + // branch on them without regex-parsing the message. + let status = status.as_u16(); + let body = body.clone(); + Python::attach(|py| { + let err = ApiError::new_err(msg); + let val = err.value(py); + let _ = val.setattr("status", status); + let _ = val.setattr("body", body); + err + }) + } + SdkError::Decode { body, .. } => { + let body = body.clone(); + Python::attach(|py| { + let err = DecodeError::new_err(msg); + let val = err.value(py); + let _ = val.setattr("body", body); + err + }) + } + SdkError::Http(_) => match e.http_kind() { + Some(HttpKind::Timeout) => TimeoutError::new_err(msg), + Some(HttpKind::Connect) => ConnectionError::new_err(msg), + _ => HttpError::new_err(msg), + }, + } +} + +pub fn add_to_module(m: &Bound<'_, PyModule>) -> PyResult<()> { + let py = m.py(); + m.add("QuickNodeError", py.get_type::())?; + m.add("ConfigError", py.get_type::())?; + m.add("HttpError", py.get_type::())?; + m.add("TimeoutError", py.get_type::())?; + m.add("ConnectionError", py.get_type::())?; + m.add("ApiError", py.get_type::())?; + m.add("DecodeError", py.get_type::())?; + Ok(()) +} diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs index f694c95..beac9f3 100644 --- a/crates/python/src/lib.rs +++ b/crates/python/src/lib.rs @@ -1,10 +1,11 @@ -use pyo3::{exceptions::PyValueError, prelude::*}; +use pyo3::prelude::*; use pyo3_stub_gen::{ define_stub_info_gatherer, derive::{gen_stub_pyclass, gen_stub_pymethods}, }; use sdk_core as core; +mod errors; mod streams_destination; // ── Top-level SDK ────────────────────────────────────────────── @@ -29,8 +30,7 @@ impl QuickNodeSdk { #[new] #[allow(clippy::needless_pass_by_value)] fn new(config: core::SdkFullConfig) -> PyResult { - let sdk_config = - core::SdkConfig::new(&config).map_err(|e| PyValueError::new_err(e.to_string()))?; + let sdk_config = core::SdkConfig::new(&config).map_err(errors::map_sdk_err)?; Ok(Self { admin: AdminApiClient { inner: core::admin::AdminApiClient::new(sdk_config.clone()), @@ -59,7 +59,7 @@ impl QuickNodeSdk { }, kvstore: KvStoreApiClient { inner: sdk.kvstore }, }) - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) } } @@ -139,7 +139,7 @@ impl AdminApiClient { client .get_endpoints(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -163,7 +163,7 @@ impl AdminApiClient { client .create_endpoint(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -174,10 +174,7 @@ impl AdminApiClient { fn show_endpoint<'py>(&self, py: Python<'py>, id: String) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .show_endpoint(&id) - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.show_endpoint(&id).await.map_err(errors::map_sdk_err) }) } @@ -197,7 +194,7 @@ impl AdminApiClient { client .update_endpoint(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -210,7 +207,7 @@ impl AdminApiClient { client .archive_endpoint(&id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -231,7 +228,7 @@ impl AdminApiClient { client .update_endpoint_status(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -251,7 +248,7 @@ impl AdminApiClient { client .create_tag(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -268,7 +265,7 @@ impl AdminApiClient { client .delete_tag(&id, &tag_id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -291,10 +288,7 @@ impl AdminApiClient { end_time, }; pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .get_usage(¶ms) - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.get_usage(¶ms).await.map_err(errors::map_sdk_err) }) } @@ -320,7 +314,7 @@ impl AdminApiClient { client .get_usage_by_endpoint(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -346,7 +340,7 @@ impl AdminApiClient { client .get_usage_by_method(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -371,7 +365,7 @@ impl AdminApiClient { client .get_usage_by_chain(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -406,7 +400,7 @@ impl AdminApiClient { client .get_endpoint_logs(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -427,7 +421,7 @@ impl AdminApiClient { client .get_log_details(&id, &request_id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -446,7 +440,7 @@ impl AdminApiClient { client .get_security_options(&id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -491,7 +485,7 @@ impl AdminApiClient { client .update_security_options(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -500,10 +494,7 @@ impl AdminApiClient { fn create_token<'py>(&self, py: Python<'py>, id: String) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .create_token(&id) - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.create_token(&id).await.map_err(errors::map_sdk_err) }) } @@ -522,7 +513,7 @@ impl AdminApiClient { client .delete_token(&id, &token_id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -542,7 +533,7 @@ impl AdminApiClient { client .create_referrer(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -561,7 +552,7 @@ impl AdminApiClient { client .delete_referrer(&id, &referrer_id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -580,7 +571,7 @@ impl AdminApiClient { client .create_ip(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -599,7 +590,7 @@ impl AdminApiClient { client .delete_ip(&id, &ip_id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -620,7 +611,7 @@ impl AdminApiClient { client .create_domain_mask(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -639,7 +630,7 @@ impl AdminApiClient { client .delete_domain_mask(&id, &domain_mask_id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -665,7 +656,7 @@ impl AdminApiClient { client .create_jwt(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -683,7 +674,7 @@ impl AdminApiClient { client .delete_jwt(&id, &jwt_id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -706,7 +697,7 @@ impl AdminApiClient { client .create_request_filter(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -727,7 +718,7 @@ impl AdminApiClient { client .update_request_filter(&id, &request_filter_id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -745,7 +736,7 @@ impl AdminApiClient { client .delete_request_filter(&id, &request_filter_id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -758,7 +749,7 @@ impl AdminApiClient { client .enable_multichain(&id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -770,7 +761,7 @@ impl AdminApiClient { client .disable_multichain(&id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -793,7 +784,7 @@ impl AdminApiClient { client .create_or_update_ip_custom_header(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -811,7 +802,7 @@ impl AdminApiClient { client .delete_ip_custom_header(&id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -830,7 +821,7 @@ impl AdminApiClient { client .get_method_rate_limits(&id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -858,7 +849,7 @@ impl AdminApiClient { client .create_method_rate_limit(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -887,7 +878,7 @@ impl AdminApiClient { client .update_method_rate_limit(&id, &method_rate_limit_id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -904,7 +895,7 @@ impl AdminApiClient { client .delete_method_rate_limit(&id, &method_rate_limit_id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -929,7 +920,7 @@ impl AdminApiClient { client .update_rate_limits(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -952,7 +943,7 @@ impl AdminApiClient { client .get_endpoint_metrics(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -980,7 +971,7 @@ impl AdminApiClient { client .get_account_metrics(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -992,10 +983,7 @@ impl AdminApiClient { fn list_chains<'py>(&self, py: Python<'py>) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .list_chains() - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.list_chains().await.map_err(errors::map_sdk_err) }) } @@ -1008,10 +996,7 @@ impl AdminApiClient { fn list_invoices<'py>(&self, py: Python<'py>) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .list_invoices() - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.list_invoices().await.map_err(errors::map_sdk_err) }) } @@ -1023,10 +1008,7 @@ impl AdminApiClient { fn list_payments<'py>(&self, py: Python<'py>) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .list_payments() - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.list_payments().await.map_err(errors::map_sdk_err) }) } @@ -1038,10 +1020,7 @@ impl AdminApiClient { fn list_teams<'py>(&self, py: Python<'py>) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .list_teams() - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.list_teams().await.map_err(errors::map_sdk_err) }) } @@ -1058,7 +1037,7 @@ impl AdminApiClient { client .create_team(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1071,10 +1050,7 @@ impl AdminApiClient { fn get_team<'py>(&self, py: Python<'py>, id: i64) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .get_team(id) - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.get_team(id).await.map_err(errors::map_sdk_err) }) } @@ -1087,10 +1063,7 @@ impl AdminApiClient { fn delete_team<'py>(&self, py: Python<'py>, id: i64) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .delete_team(id) - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.delete_team(id).await.map_err(errors::map_sdk_err) }) } @@ -1106,7 +1079,7 @@ impl AdminApiClient { client .list_team_endpoints(id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1129,7 +1102,7 @@ impl AdminApiClient { client .update_team_endpoints(id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1158,7 +1131,7 @@ impl AdminApiClient { client .invite_team_member(id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1180,7 +1153,7 @@ impl AdminApiClient { client .remove_team_member(id, user_id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1201,7 +1174,7 @@ impl AdminApiClient { client .resend_team_invite(id, user_id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1223,7 +1196,7 @@ impl AdminApiClient { client .bulk_update_endpoint_status(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1245,7 +1218,7 @@ impl AdminApiClient { client .bulk_add_tag(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1266,7 +1239,7 @@ impl AdminApiClient { client .bulk_remove_tag(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1278,10 +1251,7 @@ impl AdminApiClient { fn list_tags<'py>(&self, py: Python<'py>) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .list_tags() - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.list_tags().await.map_err(errors::map_sdk_err) }) } @@ -1302,7 +1272,7 @@ impl AdminApiClient { client .rename_tag(id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1317,7 +1287,7 @@ impl AdminApiClient { client .delete_account_tag(id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1343,7 +1313,7 @@ impl AdminApiClient { client .get_usage_by_tag(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1364,7 +1334,7 @@ impl AdminApiClient { client .get_endpoint_security(&id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } } @@ -1454,17 +1424,17 @@ impl StreamsApiClient { let dataset = serde_json::from_value::( serde_json::Value::String(dataset), ) - .map_err(|e| PyValueError::new_err(e.to_string()))?; + .map_err(errors::map_parse_err)?; let region = serde_json::from_value::( serde_json::Value::String(region), ) - .map_err(|e| PyValueError::new_err(e.to_string()))?; + .map_err(errors::map_parse_err)?; let filter_language = filter_language .map(|s| { serde_json::from_value::(serde_json::Value::String( s, )) - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_parse_err) }) .transpose()?; let include_stream_metadata = include_stream_metadata @@ -1472,19 +1442,19 @@ impl StreamsApiClient { serde_json::from_value::( serde_json::Value::String(s), ) - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_parse_err) }) .transpose()?; let product_type = product_type .map(|s| { serde_json::from_value::(serde_json::Value::String(s)) - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_parse_err) }) .transpose()?; let status = status .map(|s| { serde_json::from_value::(serde_json::Value::String(s)) - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_parse_err) }) .transpose()?; pyo3_async_runtimes::tokio::future_into_py(py, async move { @@ -1518,7 +1488,7 @@ impl StreamsApiClient { let stream = client .create_stream(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string()))?; + .map_err(errors::map_sdk_err)?; Python::attach(|py| streams_destination::PyStream::from_core(stream, py)) }) } @@ -1556,7 +1526,7 @@ impl StreamsApiClient { let resp = client .list_streams(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string()))?; + .map_err(errors::map_sdk_err)?; Python::attach(|py| streams_destination::PyListStreamsResponse::from_core(resp, py)) }) } @@ -1570,7 +1540,7 @@ impl StreamsApiClient { client .delete_all_streams() .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1582,10 +1552,7 @@ impl StreamsApiClient { fn get_stream<'py>(&self, py: Python<'py>, id: String) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - let stream = client - .get_stream(&id) - .await - .map_err(|e| PyValueError::new_err(e.to_string()))?; + let stream = client.get_stream(&id).await.map_err(errors::map_sdk_err)?; Python::attach(|py| streams_destination::PyStream::from_core(stream, py)) }) } @@ -1661,13 +1628,13 @@ impl StreamsApiClient { let dataset = dataset .map(|s| { serde_json::from_value::(serde_json::Value::String(s)) - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_parse_err) }) .transpose()?; let region = region .map(|s| { serde_json::from_value::(serde_json::Value::String(s)) - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_parse_err) }) .transpose()?; let filter_language = filter_language @@ -1675,7 +1642,7 @@ impl StreamsApiClient { serde_json::from_value::(serde_json::Value::String( s, )) - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_parse_err) }) .transpose()?; let include_stream_metadata = include_stream_metadata @@ -1683,13 +1650,13 @@ impl StreamsApiClient { serde_json::from_value::( serde_json::Value::String(s), ) - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_parse_err) }) .transpose()?; let status = status .map(|s| { serde_json::from_value::(serde_json::Value::String(s)) - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_parse_err) }) .transpose()?; pyo3_async_runtimes::tokio::future_into_py(py, async move { @@ -1723,7 +1690,7 @@ impl StreamsApiClient { let stream = client .update_stream(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string()))?; + .map_err(errors::map_sdk_err)?; Python::attach(|py| streams_destination::PyStream::from_core(stream, py)) }) } @@ -1733,10 +1700,7 @@ impl StreamsApiClient { fn delete_stream<'py>(&self, py: Python<'py>, id: String) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .delete_stream(&id) - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.delete_stream(&id).await.map_err(errors::map_sdk_err) }) } @@ -1748,7 +1712,7 @@ impl StreamsApiClient { client .activate_stream(&id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1757,10 +1721,7 @@ impl StreamsApiClient { fn pause_stream<'py>(&self, py: Python<'py>, id: String) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .pause_stream(&id) - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.pause_stream(&id).await.map_err(errors::map_sdk_err) }) } @@ -1785,13 +1746,13 @@ impl StreamsApiClient { let dataset = serde_json::from_value::( serde_json::Value::String(dataset), ) - .map_err(|e| PyValueError::new_err(e.to_string()))?; + .map_err(errors::map_parse_err)?; let filter_language = filter_language .map(|s| { serde_json::from_value::( serde_json::Value::String(s), ) - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_parse_err) }) .transpose()?; let params = core::streams::TestFilterParams { @@ -1805,7 +1766,7 @@ impl StreamsApiClient { client .test_filter(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1825,7 +1786,7 @@ impl StreamsApiClient { client .get_enabled_count(stream_type.as_deref()) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } } @@ -1863,7 +1824,7 @@ impl WebhooksApiClient { client .list_webhooks(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1876,7 +1837,7 @@ impl WebhooksApiClient { client .delete_all_webhooks() .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1891,10 +1852,7 @@ impl WebhooksApiClient { fn get_webhook<'py>(&self, py: Python<'py>, id: String) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .get_webhook(&id) - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.get_webhook(&id).await.map_err(errors::map_sdk_err) }) } @@ -1926,7 +1884,7 @@ impl WebhooksApiClient { client .update_webhook(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1938,7 +1896,7 @@ impl WebhooksApiClient { client .delete_webhook(&id) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1947,10 +1905,7 @@ impl WebhooksApiClient { fn pause_webhook<'py>(&self, py: Python<'py>, id: String) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .pause_webhook(&id) - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.pause_webhook(&id).await.map_err(errors::map_sdk_err) }) } @@ -1969,13 +1924,13 @@ impl WebhooksApiClient { let start_from = serde_json::from_value::( serde_json::Value::String(start_from), ) - .map_err(|e| PyValueError::new_err(e.to_string()))?; + .map_err(errors::map_parse_err)?; let params = core::webhooks::ActivateWebhookParams { start_from }; pyo3_async_runtimes::tokio::future_into_py(py, async move { client .activate_webhook(&id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -1990,7 +1945,7 @@ impl WebhooksApiClient { client .get_enabled_count() .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -2026,7 +1981,7 @@ impl WebhooksApiClient { client .create_webhook_from_template(¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -2060,7 +2015,7 @@ impl WebhooksApiClient { client .update_webhook_template(&webhook_id, ¶ms) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } } @@ -2091,7 +2046,7 @@ impl KvStoreApiClient { client .create_set(&core::kvstore::CreateSetParams { key, value }) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -2112,7 +2067,7 @@ impl KvStoreApiClient { client .get_sets(&core::kvstore::GetSetsParams { limit, cursor }) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -2124,10 +2079,7 @@ impl KvStoreApiClient { fn get_set<'py>(&self, py: Python<'py>, key: String) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .get_set(&key) - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.get_set(&key).await.map_err(errors::map_sdk_err) }) } @@ -2149,7 +2101,7 @@ impl KvStoreApiClient { delete_sets, }) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -2159,10 +2111,7 @@ impl KvStoreApiClient { fn delete_set<'py>(&self, py: Python<'py>, key: String) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .delete_set(&key) - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.delete_set(&key).await.map_err(errors::map_sdk_err) }) } @@ -2180,7 +2129,7 @@ impl KvStoreApiClient { client .create_list(&core::kvstore::CreateListParams { key, items }) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -2201,7 +2150,7 @@ impl KvStoreApiClient { client .get_lists(&core::kvstore::GetListsParams { limit, cursor }) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -2223,7 +2172,7 @@ impl KvStoreApiClient { client .get_list(&key, &core::kvstore::GetListParams { limit, cursor }) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -2249,7 +2198,7 @@ impl KvStoreApiClient { }, ) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -2267,7 +2216,7 @@ impl KvStoreApiClient { client .add_list_item(&key, &core::kvstore::AddListItemParams { item }) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -2287,7 +2236,7 @@ impl KvStoreApiClient { client .list_contains_item(&key, &item) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -2305,7 +2254,7 @@ impl KvStoreApiClient { client .delete_list_item(&key, &item) .await - .map_err(|e| PyValueError::new_err(e.to_string())) + .map_err(errors::map_sdk_err) }) } @@ -2315,10 +2264,7 @@ impl KvStoreApiClient { fn delete_list<'py>(&self, py: Python<'py>, key: String) -> PyResult> { let client = self.inner.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - client - .delete_list(&key) - .await - .map_err(|e| PyValueError::new_err(e.to_string())) + client.delete_list(&key).await.map_err(errors::map_sdk_err) }) } } @@ -2327,6 +2273,7 @@ impl KvStoreApiClient { #[pymodule] fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> { + errors::add_to_module(m)?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/ruby/src/errors.rs b/crates/ruby/src/errors.rs new file mode 100644 index 0000000..291436e --- /dev/null +++ b/crates/ruby/src/errors.rs @@ -0,0 +1,141 @@ +use magnus::{ + exception::ExceptionClass, prelude::*, value::Opaque, Object, RModule, RObject, Ruby, Value, +}; +use sdk_core::errors::{HttpKind, SdkError}; +use std::sync::OnceLock; + +// Class handles are captured at #[magnus::init] time. OnceLock> is +// the Magnus-blessed pattern for stashing Ruby values across calls — the +// Opaque wrapper asserts Send/Sync at access time via Ruby::get_inner. +struct ErrorClasses { + // Held solely to register the base class on the Ruby side; never read + // back because subclasses dispatch directly. + #[allow(dead_code)] + base: Opaque, + config: Opaque, + http: Opaque, + timeout: Opaque, + connection: Opaque, + api: Opaque, + decode: Opaque, +} + +static CLASSES: OnceLock = OnceLock::new(); + +pub fn init(ruby: &Ruby, module: &RModule) -> Result<(), magnus::Error> { + let std_err = ruby.exception_standard_error(); + let base = module.define_error("Error", std_err)?; + let config = module.define_error("ConfigError", base)?; + let http = module.define_error("HttpError", base)?; + let timeout = module.define_error("TimeoutError", http)?; + let connection = module.define_error("ConnectionError", http)?; + let api = module.define_error("ApiError", base)?; + let decode = module.define_error("DecodeError", base)?; + + // attr_reader :status, :body on ApiError; :body on DecodeError + api.define_method("status", magnus::method!(read_status, 0))?; + api.define_method("body", magnus::method!(read_body, 0))?; + decode.define_method("body", magnus::method!(read_body, 0))?; + + CLASSES + .set(ErrorClasses { + base: base.into(), + config: config.into(), + http: http.into(), + timeout: timeout.into(), + connection: connection.into(), + api: api.into(), + decode: decode.into(), + }) + .map_err(|_| { + magnus::Error::new( + ruby.exception_runtime_error(), + "error classes already initialized", + ) + })?; + Ok(()) +} + +fn read_status(rb_self: magnus::Exception) -> Result { + as_object(rb_self)?.ivar_get("@status") +} + +fn read_body(rb_self: magnus::Exception) -> Result { + as_object(rb_self)?.ivar_get("@body") +} + +fn as_object(exc: magnus::Exception) -> Result { + RObject::from_value(exc.as_value()).ok_or_else(|| { + let ruby = Ruby::get().expect("read_ivar called outside a Ruby thread"); + magnus::Error::new( + ruby.exception_runtime_error(), + "exception is not a plain Ruby object", + ) + }) +} + +fn classes(ruby: &Ruby) -> &ErrorClasses { + let _ = ruby; + CLASSES + .get() + .expect("QuickNodeSdk error classes not initialized") +} + +#[allow(clippy::needless_pass_by_value)] +pub fn map_err(e: SdkError) -> magnus::Error { + // map_err is only called from within Ruby-initiated SDK calls, so Ruby + // is always attached to this thread here. + let ruby = Ruby::get().expect("map_err called outside a Ruby thread"); + let c = classes(&ruby); + let msg = e.to_string(); + match &e { + SdkError::Config(_) | SdkError::UrlParse(_) => { + magnus::Error::new(ruby.get_inner(c.config), msg) + } + SdkError::Api { status, body } => build_with_ivars( + &ruby, + ruby.get_inner(c.api), + &msg, + Some(status.as_u16()), + Some(body.clone()), + ), + SdkError::Decode { body, .. } => build_with_ivars( + &ruby, + ruby.get_inner(c.decode), + &msg, + None, + Some(body.clone()), + ), + SdkError::Http(_) => { + let cls = match e.http_kind() { + Some(HttpKind::Timeout) => ruby.get_inner(c.timeout), + Some(HttpKind::Connect) => ruby.get_inner(c.connection), + _ => ruby.get_inner(c.http), + }; + magnus::Error::new(cls, msg) + } + } +} + +fn build_with_ivars( + ruby: &Ruby, + class: ExceptionClass, + msg: &str, + status: Option, + body: Option, +) -> magnus::Error { + match class.new_instance((msg,)) { + Ok(exc) => { + if let Some(obj) = RObject::from_value(exc.as_value()) { + if let Some(s) = status { + let _ = obj.ivar_set("@status", s); + } + if let Some(b) = body { + let _ = obj.ivar_set("@body", b); + } + } + magnus::Error::from(exc) + } + Err(_) => magnus::Error::new(ruby.exception_runtime_error(), msg.to_string()), + } +} diff --git a/crates/ruby/src/lib.rs b/crates/ruby/src/lib.rs index 8ca1b94..6a3fd23 100644 --- a/crates/ruby/src/lib.rs +++ b/crates/ruby/src/lib.rs @@ -4,6 +4,8 @@ use magnus::{ }; use sdk_core as core; +mod errors; + // ── Tokio runtime ─────────────────────────────────────────────────────────── // // A single shared runtime for all blocking HTTP calls. GVL is held during @@ -22,7 +24,7 @@ fn ruby() -> Ruby { #[allow(clippy::needless_pass_by_value)] fn map_err(e: core::errors::SdkError) -> Error { - Error::new(ruby().exception_runtime_error(), e.to_string()) + errors::map_err(e) } #[allow(clippy::needless_pass_by_value)] @@ -1798,6 +1800,10 @@ impl KvStoreApiClient { fn init(ruby: &Ruby) -> Result<(), Error> { let module = ruby.define_module("QuickNodeSdk")?; + // Typed exception hierarchy — register before client classes so map_err + // can use them from the first call onward. + errors::init(ruby, &module)?; + // ── SDK root ────────────────────────────────────────────── let sdk = module.define_class("SDK", ruby.class_object())?; sdk.define_singleton_method("from_env", function!(QuickNodeSdk::from_env, 0))?; diff --git a/npm/errors.js b/npm/errors.js new file mode 100644 index 0000000..9fef90d --- /dev/null +++ b/npm/errors.js @@ -0,0 +1,121 @@ +// Typed error classes. The Rust binding throws a plain napi Error whose +// message is tagged "[||]\x1f"; parseAndRethrow +// decodes that and throws an instance of the matching subclass below. + +class QuickNodeError extends Error { + constructor(message) { + super(message); + this.name = "QuickNodeError"; + } +} + +class ConfigError extends QuickNodeError { + constructor(message) { + super(message); + this.name = "ConfigError"; + } +} + +class HttpError extends QuickNodeError { + constructor(message) { + super(message); + this.name = "HttpError"; + } +} + +class TimeoutError extends HttpError { + constructor(message) { + super(message); + this.name = "TimeoutError"; + } +} + +class ConnectionError extends HttpError { + constructor(message) { + super(message); + this.name = "ConnectionError"; + } +} + +class ApiError extends QuickNodeError { + constructor(message, status, body) { + super(message); + this.name = "ApiError"; + this.status = status; + this.body = body; + } +} + +class DecodeError extends QuickNodeError { + constructor(message, body) { + super(message); + this.name = "DecodeError"; + this.body = body; + } +} + +const TAG_RE = /^\[(Config|Http|Timeout|Connect|Api|Decode)\|([^|]+)\|([^\]]+)\](.*)$/s; + +function fromNapiError(err) { + if (!(err instanceof Error)) return err; + const m = err.message.match(TAG_RE); + if (!m) return err; + const [, kind, statusStr, bodyLenStr, rest] = m; + // rest = "\x1f". Use body_len (byte length from Rust) to split + // deterministically — the body may itself contain \x1f, and Api messages + // embed the body in msg, so scanning for the first separator is unsafe. + let msg = rest; + let body = ""; + if (bodyLenStr !== "-") { + const bodyLen = Number(bodyLenStr); + const bodyBytes = Buffer.from(rest, "utf8"); + const bodyStart = bodyBytes.length - bodyLen; + if (bodyStart >= 1 && bodyBytes[bodyStart - 1] === 0x1f) { + msg = bodyBytes.slice(0, bodyStart - 1).toString("utf8"); + body = bodyBytes.slice(bodyStart).toString("utf8"); + } + } + switch (kind) { + case "Config": return new ConfigError(msg); + case "Timeout": return new TimeoutError(msg); + case "Connect": return new ConnectionError(msg); + case "Http": return new HttpError(msg); + case "Api": return new ApiError(msg, Number(statusStr), body); + case "Decode": return new DecodeError(msg, body); + default: return err; + } +} + +// Wraps an object's methods so thrown napi errors get retagged as typed +// subclasses. Handles both sync throws and rejected promises. +function wrapClient(client) { + return new Proxy(client, { + get(target, prop) { + const val = target[prop]; + if (typeof val !== "function") return val; + return function (...args) { + try { + const result = val.apply(target, args); + if (result && typeof result.then === "function") { + return result.catch((e) => { throw fromNapiError(e); }); + } + return result; + } catch (e) { + throw fromNapiError(e); + } + }; + }, + }); +} + +module.exports = { + QuickNodeError, + ConfigError, + HttpError, + TimeoutError, + ConnectionError, + ApiError, + DecodeError, + fromNapiError, + wrapClient, +}; diff --git a/npm/examples/admin.ts b/npm/examples/admin.ts index 0c4f292..e21d3aa 100644 --- a/npm/examples/admin.ts +++ b/npm/examples/admin.ts @@ -1,4 +1,9 @@ -import { QuickNodeSdk } from ".."; +import { + QuickNodeSdk, + ApiError, + TimeoutError, + QuickNodeError, +} from "../sdk"; async function main() { const qn = QuickNodeSdk.fromEnv(); @@ -28,6 +33,30 @@ async function main() { const sec = await qn.admin.getEndpointSecurity(response.data[0].id); console.log(`getEndpointSecurity: has_data=${sec.data !== undefined && sec.data !== null}`); } + + // ── Error handling ────────────────────────────────────────────────── + // 1) API error path — 404 on a bogus endpoint id. + try { + await qn.admin.showEndpoint("does-not-exist"); + } catch (e) { + if (!(e instanceof ApiError)) throw e; + console.assert(e instanceof QuickNodeError); + console.assert(e.status === 404); + console.log(`api error ${e.status}: ${e.body.slice(0, 80)}`); + } + + // 2) Timeout path — unreachable base URL + 1s timeout forces a timeout. + const blackhole = new QuickNodeSdk({ + apiKey: process.env.QN_SDK__API_KEY ?? "", + http: { timeoutSecs: 1 }, + admin: { baseUrl: "http://10.255.255.1/" }, + }); + try { + await blackhole.admin.getEndpoints(); + } catch (e) { + if (!(e instanceof TimeoutError)) throw e; + console.log("timed out as expected"); + } } main(); diff --git a/npm/sdk.d.ts b/npm/sdk.d.ts index b80b502..e0ed244 100644 --- a/npm/sdk.d.ts +++ b/npm/sdk.d.ts @@ -322,3 +322,18 @@ export class TemplateArgs { static hyperliquidWalletEventsFilter(attrs: HyperliquidWalletEventsFilterTemplate): TemplateArgs; static stellarWalletTransactionsFilter(attrs: StellarWalletTransactionsFilterTemplate): TemplateArgs; } + +// Typed error hierarchy. Any SDK call can throw one of these; catch +// QuickNodeError to handle them all, or a specific subclass for finer control. +export class QuickNodeError extends Error {} +export class ConfigError extends QuickNodeError {} +export class HttpError extends QuickNodeError {} +export class TimeoutError extends HttpError {} +export class ConnectionError extends HttpError {} +export class ApiError extends QuickNodeError { + status: number; + body: string; +} +export class DecodeError extends QuickNodeError { + body: string; +} diff --git a/npm/sdk.js b/npm/sdk.js index a74da06..0f42b47 100644 --- a/npm/sdk.js +++ b/npm/sdk.js @@ -2,23 +2,32 @@ // const _index = require("./index.js"); const { QuickNodeSdk: _QuickNodeSdk } = _index; +const errors = require("./errors.js"); class QuickNodeSdk { constructor(config) { - this._inner = new _QuickNodeSdk(config); - this.admin = this._inner.admin; - this.streams = this._inner.streams; - this.webhooks = this._inner.webhooks; - this.kvstore = this._inner.kvstore; + try { + this._inner = new _QuickNodeSdk(config); + } catch (e) { + throw errors.fromNapiError(e); + } + this.admin = errors.wrapClient(this._inner.admin); + this.streams = errors.wrapClient(this._inner.streams); + this.webhooks = errors.wrapClient(this._inner.webhooks); + this.kvstore = errors.wrapClient(this._inner.kvstore); } static fromEnv() { const instance = Object.create(QuickNodeSdk.prototype); - instance._inner = _QuickNodeSdk.fromEnv(); - instance.admin = instance._inner.admin; - instance.streams = instance._inner.streams; - instance.webhooks = instance._inner.webhooks; - instance.kvstore = instance._inner.kvstore; + try { + instance._inner = _QuickNodeSdk.fromEnv(); + } catch (e) { + throw errors.fromNapiError(e); + } + instance.admin = errors.wrapClient(instance._inner.admin); + instance.streams = errors.wrapClient(instance._inner.streams); + instance.webhooks = errors.wrapClient(instance._inner.webhooks); + instance.kvstore = errors.wrapClient(instance._inner.kvstore); return instance; } } @@ -79,4 +88,15 @@ class TemplateArgs { } } -module.exports = { ..._index, QuickNodeSdk, TemplateArgs }; +module.exports = { + ..._index, + QuickNodeSdk, + TemplateArgs, + QuickNodeError: errors.QuickNodeError, + ConfigError: errors.ConfigError, + HttpError: errors.HttpError, + TimeoutError: errors.TimeoutError, + ConnectionError: errors.ConnectionError, + ApiError: errors.ApiError, + DecodeError: errors.DecodeError, +}; diff --git a/npm/sdk.mjs b/npm/sdk.mjs index 1dd9144..1fa7f1e 100644 --- a/npm/sdk.mjs +++ b/npm/sdk.mjs @@ -24,4 +24,11 @@ export const { StreamsApiClient, WebhooksApiClient, KvStoreApiClient, + QuickNodeError, + ConfigError, + HttpError, + TimeoutError, + ConnectionError, + ApiError, + DecodeError, } = cjs; diff --git a/python/examples/admin.py b/python/examples/admin.py index f8afa7c..8fce9f9 100644 --- a/python/examples/admin.py +++ b/python/examples/admin.py @@ -1,5 +1,14 @@ import asyncio -from sdk import QuickNodeSdk +import os +from sdk import ( + QuickNodeSdk, + SdkFullConfig, + HttpConfig, + AdminConfig, + ApiError, + TimeoutError, + QuickNodeError, +) async def main(): @@ -27,5 +36,27 @@ async def main(): sec = await qn.admin.get_endpoint_security(response.data[0].id) print(f"get_endpoint_security: has_data={sec.data is not None}") + # ── Error handling ────────────────────────────────────────────────── + # 1) API error path — 404 on a bogus endpoint id. + try: + await qn.admin.show_endpoint("does-not-exist") + except ApiError as e: + assert isinstance(e, QuickNodeError) + assert e.status == 404 + print(f"api error {e.status}: {e.body[:80]}") + + # 2) Timeout path — unreachable base URL + 1s timeout forces a timeout. + blackhole = QuickNodeSdk( + SdkFullConfig( + api_key=os.environ["QN_SDK__API_KEY"], + http=HttpConfig(timeout_secs=1), + admin=AdminConfig(base_url="http://10.255.255.1/"), + ) + ) + try: + await blackhole.admin.get_endpoints() + except TimeoutError: + print("timed out as expected") + asyncio.run(main()) diff --git a/python/sdk/__init__.py b/python/sdk/__init__.py index 9e4e3b2..9686ae0 100644 --- a/python/sdk/__init__.py +++ b/python/sdk/__init__.py @@ -184,6 +184,13 @@ XrplWalletFilterTemplate, HyperliquidWalletEventsFilterTemplate, StellarWalletTransactionsFilterTemplate, + QuickNodeError, + ConfigError, + HttpError, + TimeoutError, + ConnectionError, + ApiError, + DecodeError, ) __all__ = [ @@ -371,4 +378,11 @@ "XrplWalletFilterTemplate", "HyperliquidWalletEventsFilterTemplate", "StellarWalletTransactionsFilterTemplate", + "QuickNodeError", + "ConfigError", + "HttpError", + "TimeoutError", + "ConnectionError", + "ApiError", + "DecodeError", ] diff --git a/python/sdk/__init__.pyi b/python/sdk/__init__.pyi index 4b07896..01bbfeb 100644 --- a/python/sdk/__init__.pyi +++ b/python/sdk/__init__.pyi @@ -1,6 +1,390 @@ -# This file is automatically generated by pyo3_stub_gen -# ruff: noqa: E501, F401, F403, F405 - -from . import _core -__all__ = [] +## Source of truth for sdk/__init__.pyi +## python/sdk/__init__.pyi is a build artifact — overwritten by `just python-build`. +## Edit this file, not __init__.pyi directly. +from sdk._core import ( + QuickNodeSdk, + AdminApiClient, + StreamsApiClient, + Stream, + ListStreamsResponse, + PageInfo, + TestFilterResponse, + EnabledCountResponse, + WebhookAttributes, + S3Attributes, + AzureAttributes, + PostgresAttributes, + MysqlAttributes, + MongoAttributes, + ClickhouseAttributes, + SnowflakeAttributes, + KafkaAttributes, + RedisAttributes, + StreamWebhookDestination, + StreamS3Destination, + StreamAzureDestination, + StreamPostgresDestination, + StreamMysqlDestination, + StreamMongoDestination, + StreamClickhouseDestination, + StreamSnowflakeDestination, + StreamKafkaDestination, + StreamRedisDestination, + AddressBookConfig, + Endpoint, + EndpointTag, + GetEndpointsRequest, + GetEndpointsResponse, + CreateEndpointRequest, + CreateEndpointResponse, + SingleEndpoint, + ShowEndpointResponse, + UpdateEndpointRequest, + UpdateEndpointStatusRequest, + UpdateEndpointStatusResponse, + CreateTagRequest, + GetUsageRequest, + UsageData, + GetUsageResponse, + EndpointUsage, + MethodUsage, + ChainUsage, + UsageByEndpointData, + GetUsageByEndpointResponse, + UsageByMethodData, + GetUsageByMethodResponse, + UsageByChainData, + GetUsageByChainResponse, + GetEndpointLogsRequest, + LogDetails, + EndpointLog, + GetEndpointLogsResponse, + GetLogDetailsResponse, + SecurityOption, + GetSecurityOptionsResponse, + SecurityOptionsUpdate, + UpdateSecurityOptionsRequest, + UpdateSecurityOptionsResponse, + CreateReferrerRequest, + CreateIpRequest, + CreateDomainMaskRequest, + CreateJwtRequest, + CreateRequestFilterRequest, + CreateRequestFilterData, + CreateRequestFilterResponse, + UpdateRequestFilterRequest, + CreateOrUpdateIpCustomHeaderRequest, + IpCustomHeaderData, + CreateOrUpdateIpCustomHeaderResponse, + DeleteBoolResponse, + MethodRateLimiter, + GetMethodRateLimitsData, + GetMethodRateLimitsResponse, + CreateMethodRateLimitRequest, + CreateMethodRateLimitResponse, + UpdateMethodRateLimitRequest, + UpdateMethodRateLimitResponse, + RateLimitSettings, + UpdateRateLimitsRequest, + GetEndpointMetricsRequest, + GetAccountMetricsRequest, + EndpointMetric, + GetEndpointMetricsResponse, + GetAccountMetricsResponse, + ChainNetwork, + Chain, + ListChainsResponse, + InvoiceLine, + Invoice, + ListInvoicesData, + ListInvoicesResponse, + Payment, + ListPaymentsData, + ListPaymentsResponse, + EndpointRateLimits, + EndpointSecurity, + EndpointSecurityOptions, + EndpointIpCustomHeaderOption, + EndpointToken, + EndpointJwt, + EndpointReferrer, + EndpointDomainMask, + EndpointIp, + EndpointRequestFilter, + HttpConfig, + AdminConfig, + StreamsConfig, + KvStoreConfig, + SdkFullConfig, + KvStoreApiClient, + KvSetEntry, + GetSetsResponse, + GetSetResponse, + GetListsData, + GetListsResponse, + GetListData, + GetListResponse, + ListContainsItemResponse, + TeamUser, + TeamSummary, + TeamDetail, + ListTeamsResponse, + CreateTeamRequest, + CreateTeamData, + CreateTeamResponse, + GetTeamResponse, + DeleteTeamData, + DeleteTeamResponse, + TeamEndpoint, + ListTeamEndpointsResponse, + UpdateTeamEndpointsRequest, + UpdateTeamEndpointsData, + UpdateTeamEndpointsResponse, + InviteTeamMemberRequest, + InviteTeamMemberResponse, + RemoveTeamMemberRequest, + TeamMessageData, + RemoveTeamMemberResponse, + ResendTeamInviteResponse, + Pagination, + GetEndpointSecurityResponse, + BulkOperationResult, + BulkUpdateEndpointStatusRequest, + BulkUpdateEndpointStatusData, + BulkUpdateEndpointStatusResponse, + BulkTag, + BulkAddTagRequest, + BulkAddTagData, + BulkAddTagResponse, + BulkRemoveTagRequest, + BulkRemoveTagData, + BulkRemoveTagResponse, + AccountTag, + ListTagsData, + ListTagsResponse, + RenameTagRequest, + RenameTagResponse, + DeleteAccountTagData, + DeleteAccountTagResponse, + TagUsage, + UsageByTagData, + GetUsageByTagResponse, + WebhooksApiClient, + Webhook, + ListWebhooksResponse, + WebhookEnabledCountResponse, + WebhookDestinationAttributes, + TemplateArgs, + WebhooksConfig, + GetWebhooksParams, + UpdateWebhookParams, + EvmWalletFilterTemplate, + EvmContractEventsTemplate, + EvmAbiFilterTemplate, + SolanaWalletFilterTemplate, + BitcoinWalletFilterTemplate, + XrplWalletFilterTemplate, + HyperliquidWalletEventsFilterTemplate, + StellarWalletTransactionsFilterTemplate, + QuickNodeError, + ConfigError, + HttpError, + TimeoutError, + ConnectionError, + ApiError, + DecodeError, +) +__all__ = [ + "QuickNodeSdk", + "AdminApiClient", + "StreamsApiClient", + "Stream", + "ListStreamsResponse", + "PageInfo", + "TestFilterResponse", + "EnabledCountResponse", + "WebhookAttributes", + "S3Attributes", + "AzureAttributes", + "PostgresAttributes", + "MysqlAttributes", + "MongoAttributes", + "ClickhouseAttributes", + "SnowflakeAttributes", + "KafkaAttributes", + "RedisAttributes", + "StreamWebhookDestination", + "StreamS3Destination", + "StreamAzureDestination", + "StreamPostgresDestination", + "StreamMysqlDestination", + "StreamMongoDestination", + "StreamClickhouseDestination", + "StreamSnowflakeDestination", + "StreamKafkaDestination", + "StreamRedisDestination", + "AddressBookConfig", + "StreamsConfig", + "Endpoint", + "EndpointTag", + "GetEndpointsRequest", + "GetEndpointsResponse", + "CreateEndpointRequest", + "CreateEndpointResponse", + "SingleEndpoint", + "ShowEndpointResponse", + "UpdateEndpointRequest", + "UpdateEndpointStatusRequest", + "UpdateEndpointStatusResponse", + "CreateTagRequest", + "GetUsageRequest", + "UsageData", + "GetUsageResponse", + "EndpointUsage", + "MethodUsage", + "ChainUsage", + "UsageByEndpointData", + "GetUsageByEndpointResponse", + "UsageByMethodData", + "GetUsageByMethodResponse", + "UsageByChainData", + "GetUsageByChainResponse", + "GetEndpointLogsRequest", + "LogDetails", + "EndpointLog", + "GetEndpointLogsResponse", + "GetLogDetailsResponse", + "SecurityOption", + "GetSecurityOptionsResponse", + "SecurityOptionsUpdate", + "UpdateSecurityOptionsRequest", + "UpdateSecurityOptionsResponse", + "CreateReferrerRequest", + "CreateIpRequest", + "CreateDomainMaskRequest", + "CreateJwtRequest", + "CreateRequestFilterRequest", + "CreateRequestFilterData", + "CreateRequestFilterResponse", + "UpdateRequestFilterRequest", + "CreateOrUpdateIpCustomHeaderRequest", + "IpCustomHeaderData", + "CreateOrUpdateIpCustomHeaderResponse", + "DeleteBoolResponse", + "MethodRateLimiter", + "GetMethodRateLimitsData", + "GetMethodRateLimitsResponse", + "CreateMethodRateLimitRequest", + "CreateMethodRateLimitResponse", + "UpdateMethodRateLimitRequest", + "UpdateMethodRateLimitResponse", + "RateLimitSettings", + "UpdateRateLimitsRequest", + "GetEndpointMetricsRequest", + "GetAccountMetricsRequest", + "EndpointMetric", + "GetEndpointMetricsResponse", + "GetAccountMetricsResponse", + "ChainNetwork", + "Chain", + "ListChainsResponse", + "InvoiceLine", + "Invoice", + "ListInvoicesData", + "ListInvoicesResponse", + "Payment", + "ListPaymentsData", + "ListPaymentsResponse", + "EndpointRateLimits", + "EndpointSecurity", + "EndpointSecurityOptions", + "EndpointIpCustomHeaderOption", + "EndpointToken", + "EndpointJwt", + "EndpointReferrer", + "EndpointDomainMask", + "EndpointIp", + "EndpointRequestFilter", + "HttpConfig", + "AdminConfig", + "KvStoreConfig", + "SdkFullConfig", + "KvStoreApiClient", + "KvSetEntry", + "GetSetsResponse", + "GetSetResponse", + "GetListsData", + "GetListsResponse", + "GetListData", + "GetListResponse", + "ListContainsItemResponse", + "TeamUser", + "TeamSummary", + "TeamDetail", + "ListTeamsResponse", + "CreateTeamRequest", + "CreateTeamData", + "CreateTeamResponse", + "GetTeamResponse", + "DeleteTeamData", + "DeleteTeamResponse", + "TeamEndpoint", + "ListTeamEndpointsResponse", + "UpdateTeamEndpointsRequest", + "UpdateTeamEndpointsData", + "UpdateTeamEndpointsResponse", + "InviteTeamMemberRequest", + "InviteTeamMemberResponse", + "RemoveTeamMemberRequest", + "TeamMessageData", + "RemoveTeamMemberResponse", + "ResendTeamInviteResponse", + "Pagination", + "GetEndpointSecurityResponse", + "BulkOperationResult", + "BulkUpdateEndpointStatusRequest", + "BulkUpdateEndpointStatusData", + "BulkUpdateEndpointStatusResponse", + "BulkTag", + "BulkAddTagRequest", + "BulkAddTagData", + "BulkAddTagResponse", + "BulkRemoveTagRequest", + "BulkRemoveTagData", + "BulkRemoveTagResponse", + "AccountTag", + "ListTagsData", + "ListTagsResponse", + "RenameTagRequest", + "RenameTagResponse", + "DeleteAccountTagData", + "DeleteAccountTagResponse", + "TagUsage", + "UsageByTagData", + "GetUsageByTagResponse", + "WebhooksApiClient", + "Webhook", + "ListWebhooksResponse", + "WebhookEnabledCountResponse", + "WebhookDestinationAttributes", + "TemplateArgs", + "WebhooksConfig", + "GetWebhooksParams", + "UpdateWebhookParams", + "EvmWalletFilterTemplate", + "EvmContractEventsTemplate", + "EvmAbiFilterTemplate", + "SolanaWalletFilterTemplate", + "BitcoinWalletFilterTemplate", + "XrplWalletFilterTemplate", + "HyperliquidWalletEventsFilterTemplate", + "StellarWalletTransactionsFilterTemplate", + "QuickNodeError", + "ConfigError", + "HttpError", + "TimeoutError", + "ConnectionError", + "ApiError", + "DecodeError", +] diff --git a/python/sdk/init_manual_override.pyi b/python/sdk/init_manual_override.pyi index 25c5254..01bbfeb 100644 --- a/python/sdk/init_manual_override.pyi +++ b/python/sdk/init_manual_override.pyi @@ -186,6 +186,13 @@ from sdk._core import ( XrplWalletFilterTemplate, HyperliquidWalletEventsFilterTemplate, StellarWalletTransactionsFilterTemplate, + QuickNodeError, + ConfigError, + HttpError, + TimeoutError, + ConnectionError, + ApiError, + DecodeError, ) __all__ = [ @@ -373,4 +380,11 @@ __all__ = [ "XrplWalletFilterTemplate", "HyperliquidWalletEventsFilterTemplate", "StellarWalletTransactionsFilterTemplate", + "QuickNodeError", + "ConfigError", + "HttpError", + "TimeoutError", + "ConnectionError", + "ApiError", + "DecodeError", ] diff --git a/ruby/examples/admin_e2e.rb b/ruby/examples/admin_e2e.rb index cf414c4..be9c8be 100644 --- a/ruby/examples/admin_e2e.rb +++ b/ruby/examples/admin_e2e.rb @@ -1,4 +1,5 @@ require "json" +require "securerandom" require_relative "../lib/quicknode_sdk" qn = QuickNodeSdk::SDK.from_env @@ -73,6 +74,8 @@ resp = JSON.parse(qn.admin.update_endpoint_status(id: endpoint_id, status: "active")) puts "update_endpoint_status active: #{resp["data"]}" +sleep 1 + # ── Tags ────────────────────────────────────────────────────────────────────── qn.admin.create_tag(id: endpoint_id, label: "example-tag") @@ -95,7 +98,9 @@ )) puts "get_endpoint_logs: #{resp["data"].length} entries" -resp = JSON.parse(qn.admin.get_endpoint_metrics(id: endpoint_id, period: "day", metric: "requests")) +sleep 1 + +resp = JSON.parse(qn.admin.get_endpoint_metrics(id: endpoint_id, period: "day", metric: "credits_over_time")) puts "get_endpoint_metrics: #{resp["data"].length} series" # ── Security options ────────────────────────────────────────────────────────── @@ -103,12 +108,17 @@ resp = JSON.parse(qn.admin.get_security_options(id: endpoint_id)) puts "get_security_options: #{resp["data"].length} options" +sleep 1 + resp = JSON.parse(qn.admin.get_endpoint_security(id: endpoint_id)) puts "get_endpoint_security: has_data=#{!resp["data"].nil?}" resp = JSON.parse(qn.admin.update_security_options(id: endpoint_id, tokens: "enabled")) puts "update_security_options: #{resp["data"].length} options" +sleep 0.5 + + # ── Token ───────────────────────────────────────────────────────────────────── qn.admin.create_token(id: endpoint_id) @@ -207,8 +217,8 @@ # ── Rate limits ─────────────────────────────────────────────────────────────── -qn.admin.update_rate_limits(id: endpoint_id, rps: 10) -puts "update_rate_limits: ok" +#qn.admin.update_rate_limits(id: endpoint_id, rps: 3) +#puts "update_rate_limits: ok" resp = JSON.parse(qn.admin.get_method_rate_limits(id: endpoint_id)) puts "get_method_rate_limits: #{resp["data"]}" @@ -247,12 +257,15 @@ # ── Account-level tags (bulk_add/remove + rename/delete) ───────────────────── -resp = JSON.parse(qn.admin.bulk_add_tag(ids: [endpoint_id], label: "sdk-bulk-tag")) +tag_suffix = SecureRandom.hex(4) +resp = JSON.parse(qn.admin.bulk_add_tag(ids: [endpoint_id], label: "sdk-bulk-#{tag_suffix}")) puts "bulk_add_tag: #{resp["data"]}" bulk_tag_id = resp.dig("data", "tag", "tag_id") +sleep 0.5 + if bulk_tag_id - resp = JSON.parse(qn.admin.rename_tag(id: bulk_tag_id, label: "sdk-bulk-tag-renamed")) + resp = JSON.parse(qn.admin.rename_tag(id: bulk_tag_id, label: "sdk-renamed-#{tag_suffix}")) puts "rename_tag: #{resp["data"]}" resp = JSON.parse(qn.admin.bulk_remove_tag(ids: [endpoint_id], tag_id: bulk_tag_id)) @@ -270,6 +283,7 @@ team_id = resp.dig("data", "id") puts "create_team: #{resp["data"]}" +sleep 0.5 if team_id resp = JSON.parse(qn.admin.get_team(id: team_id)) puts "get_team: #{resp.dig("data", "name")}" @@ -277,9 +291,13 @@ resp = JSON.parse(qn.admin.list_team_endpoints(id: team_id)) puts "list_team_endpoints: #{resp["data"].length} endpoints" + sleep 0.5 + resp = JSON.parse(qn.admin.update_team_endpoints(id: team_id, endpoint_ids: [endpoint_id])) puts "update_team_endpoints: #{resp["data"]}" + sleep 0.5 + begin resp = JSON.parse(qn.admin.invite_team_member(id: team_id, email: "placeholder@example.com")) puts "invite_team_member: #{resp["data"]}" @@ -299,3 +317,33 @@ qn.admin.archive_endpoint(id: endpoint_id) puts "archive_endpoint: ok" + +# ── Error handling ─────────────────────────────────────────────────────────── + +# 1) API error path — 404 on a bogus endpoint id. +begin + qn.admin.show_endpoint(id: "does-not-exist") + raise "expected 404" +rescue QuickNodeSdk::ApiError => e + raise "expected QuickNodeSdk::Error subclass" unless e.is_a?(QuickNodeSdk::Error) + raise "expected 404, got #{e.status}" unless e.status == 404 + puts "api error #{e.status}: #{e.body[0, 80]}" +end + +# 2) Timeout path — unreachable base URL + 1s timeout forces a timeout +prev_url = ENV["QN_SDK__ADMIN__BASE_URL"] +prev_timeout = ENV["QN_SDK__HTTP__TIMEOUT_SECS"] +ENV["QN_SDK__ADMIN__BASE_URL"] = "http://10.255.255.1/" +ENV["QN_SDK__HTTP__TIMEOUT_SECS"] = "1" +begin + blackhole = QuickNodeSdk::SDK.from_env + begin + blackhole.admin.get_endpoints(limit: 20) + raise "expected timeout" + rescue QuickNodeSdk::TimeoutError + puts "timed out as expected" + end +ensure + ENV["QN_SDK__ADMIN__BASE_URL"] = prev_url + ENV["QN_SDK__HTTP__TIMEOUT_SECS"] = prev_timeout +end