From fd40fa706fc0c4177d0bffcbd829e2d7e3cc9173 Mon Sep 17 00:00:00 2001 From: Andres Lopez <190146319+AndresL230@users.noreply.github.com> Date: Wed, 13 May 2026 03:07:17 -0400 Subject: [PATCH 1/7] feat(flask): rename ReCost to RecostExtension; deprecate old name The Flask extension class is renamed to RecostExtension to match the naming convention of RecostMiddleware (FastAPI) and the rest of the public surface. The old ReCost class remains importable as a thin subclass that emits a DeprecationWarning on construction. Refs #5 --- recost/frameworks/flask.py | 55 ++++++++++++++++++++++++++++++++------ tests/test_flask.py | 44 +++++++++++++++++++++++++----- 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/recost/frameworks/flask.py b/recost/frameworks/flask.py index 16b7259..4403763 100644 --- a/recost/frameworks/flask.py +++ b/recost/frameworks/flask.py @@ -3,14 +3,15 @@ Usage: from flask import Flask - from recost.frameworks.flask import ReCost + from recost.frameworks.flask import RecostExtension app = Flask(__name__) - eco = ReCost(app, api_key="...", project_id="...") + ext = RecostExtension(app, api_key="...", project_id="...") """ from __future__ import annotations +import warnings from typing import Any, Optional from .._init import init @@ -19,25 +20,63 @@ try: from flask import Flask - class ReCost: - """Flask extension that initializes ReCost telemetry.""" + class RecostExtension: + """Flask extension that initializes Recost telemetry.""" - def __init__(self, app: Optional[Flask] = None, config: Optional[RecostConfig] = None, **kwargs: Any) -> None: + def __init__( + self, + app: Optional[Flask] = None, + config: Optional[RecostConfig] = None, + **kwargs: Any, + ) -> None: self._handle = None if app is not None: self.init_app(app, config, **kwargs) - def init_app(self, app: Flask, config: Optional[RecostConfig] = None, **kwargs: Any) -> None: + def init_app( + self, + app: Flask, + config: Optional[RecostConfig] = None, + **kwargs: Any, + ) -> None: if config is None: config = RecostConfig(**kwargs) self._handle = init(config) + class ReCost(RecostExtension): + """Deprecated alias for :class:`RecostExtension`. + + Will be removed in a future release. Switch to + ``from recost.frameworks.flask import RecostExtension``. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "recost.frameworks.flask.ReCost is deprecated and will be " + "removed in a future release; use RecostExtension instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) + except ImportError: - class ReCost: # type: ignore[no-redef] + class RecostExtension: # type: ignore[no-redef] """Stub — install 'flask' to use: pip install recost[flask]""" def __init__(self, *args: Any, **kwargs: Any) -> None: raise ImportError( - "flask is required for ReCost extension. " + "flask is required for the Recost extension. " "Install it with: pip install recost[flask]" ) + + class ReCost(RecostExtension): # type: ignore[no-redef] + """Stub deprecated alias — install 'flask' to use.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "recost.frameworks.flask.ReCost is deprecated; use " + "RecostExtension instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) diff --git a/tests/test_flask.py b/tests/test_flask.py index 689e8ac..2700253 100644 --- a/tests/test_flask.py +++ b/tests/test_flask.py @@ -2,20 +2,22 @@ Tests for recost/frameworks/flask.py """ +import warnings + import pytest from recost._interceptor import is_installed, uninstall from recost._types import RecostConfig -class TestFlaskExtension: +class TestRecostExtension: def test_extension_initializes_interceptor(self): try: from flask import Flask - from recost.frameworks.flask import ReCost + from recost.frameworks.flask import RecostExtension app = Flask(__name__) - eco = ReCost(app, config=RecostConfig(enabled=True)) + RecostExtension(app, config=RecostConfig(enabled=True)) assert is_installed() finally: uninstall() @@ -23,22 +25,50 @@ def test_extension_initializes_interceptor(self): def test_extension_init_app_pattern(self): try: from flask import Flask - from recost.frameworks.flask import ReCost + from recost.frameworks.flask import RecostExtension - eco = ReCost() + ext = RecostExtension() app = Flask(__name__) - eco.init_app(app, config=RecostConfig(enabled=True)) + ext.init_app(app, config=RecostConfig(enabled=True)) assert is_installed() finally: uninstall() def test_extension_accepts_kwargs(self): + try: + from flask import Flask + from recost.frameworks.flask import RecostExtension + + app = Flask(__name__) + RecostExtension(app, enabled=True, debug=False) + assert is_installed() + finally: + uninstall() + + +class TestReCostDeprecationAlias: + """The old `ReCost` name must keep working for one release but emit a + DeprecationWarning so users migrate to `RecostExtension`.""" + + def test_old_name_still_constructs(self): try: from flask import Flask from recost.frameworks.flask import ReCost app = Flask(__name__) - eco = ReCost(app, enabled=True, debug=False) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + ReCost(app, config=RecostConfig(enabled=True)) assert is_installed() + deprecations = [w for w in caught if issubclass(w.category, DeprecationWarning)] + assert deprecations, "expected a DeprecationWarning on ReCost(...)" + assert "RecostExtension" in str(deprecations[0].message) finally: uninstall() + + def test_old_name_is_subclass_or_alias_of_new(self): + """Importing the old name must yield the same class as the new name + so isinstance checks and existing type annotations keep working.""" + from recost.frameworks.flask import ReCost, RecostExtension + + assert ReCost is RecostExtension or issubclass(ReCost, RecostExtension) From 6581ec0bc8ecdf29c0f19b6e3241d05133e19766 Mon Sep 17 00:00:00 2001 From: Andres Lopez <190146319+AndresL230@users.noreply.github.com> Date: Wed, 13 May 2026 03:10:42 -0400 Subject: [PATCH 2/7] fix(test): remove unused pytest import from test_flask Refs #5 --- tests/test_flask.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_flask.py b/tests/test_flask.py index 2700253..339d53d 100644 --- a/tests/test_flask.py +++ b/tests/test_flask.py @@ -4,8 +4,6 @@ import warnings -import pytest - from recost._interceptor import is_installed, uninstall from recost._types import RecostConfig From 10848e395e61034f48143792c467ae540fb45d75 Mon Sep 17 00:00:00 2001 From: Andres Lopez <190146319+AndresL230@users.noreply.github.com> Date: Wed, 13 May 2026 03:11:43 -0400 Subject: [PATCH 3/7] docs(__init__): standardize on 'Recost' spelling in module docstring Refs #5 --- recost/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recost/__init__.py b/recost/__init__.py index 6351c6e..782f201 100644 --- a/recost/__init__.py +++ b/recost/__init__.py @@ -1,8 +1,8 @@ """ -recost — Python SDK for ReCost. +recost — Python SDK for Recost. Tracks outbound HTTP API calls and reports cost, latency, and usage patterns -to the ReCost dashboard or your local VS Code extension. +to the Recost dashboard or your local VS Code extension. """ from ._types import ( From 35bf3c06d0f1fe9228ee0093814257863773df0f Mon Sep 17 00:00:00 2001 From: Andres Lopez <190146319+AndresL230@users.noreply.github.com> Date: Wed, 13 May 2026 03:14:25 -0400 Subject: [PATCH 4/7] test(provider-registry): pin BUILTIN_PROVIDERS count to 34 Adds a regression assertion so adding/removing built-in rules forces a corresponding docs update. Also corrects the stale '21 built-in provider rules' docstring. Refs #5 --- tests/test_provider_registry.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/test_provider_registry.py b/tests/test_provider_registry.py index bb75bf7..b9077f5 100644 --- a/tests/test_provider_registry.py +++ b/tests/test_provider_registry.py @@ -11,7 +11,7 @@ class TestBuiltinProviders: - """Tests for all 21 built-in provider rules.""" + """Tests for all 34 built-in provider rules (14 unique providers).""" def setup_method(self): self.registry = ProviderRegistry() @@ -370,3 +370,18 @@ def test_pinecone_specific_before_catch_all(self): # Need to import ProviderDef for custom provider tests from recost import ProviderDef # noqa: E402 + + +# --------------------------------------------------------------------------- +# Built-in provider count — pins the published claim +# --------------------------------------------------------------------------- + +def test_builtin_providers_count_is_pinned(): + """If you add or remove a built-in provider rule, update this assertion + AND update the provider-count claims in README.md and CLAUDE.md.""" + from recost._provider_registry import BUILTIN_PROVIDERS + + assert len(BUILTIN_PROVIDERS) == 34, ( + f"BUILTIN_PROVIDERS has {len(BUILTIN_PROVIDERS)} rules; " + f"docs claim 34. Update docs and this assertion together." + ) From 59209c0f11676b536a88eb52f8a27ec7d624927b Mon Sep 17 00:00:00 2001 From: Andres Lopez <190146319+AndresL230@users.noreply.github.com> Date: Wed, 13 May 2026 03:16:32 -0400 Subject: [PATCH 5/7] docs(CLAUDE): strip nonexistent EcoAPI* names; fix provider count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md referenced EcoAPIHandle, EcoAPIConfig, EcoAPIMiddleware, and EcoAPI — none of which exist in the codebase. Replace with the actual class names (RecostHandle, RecostConfig, RecostMiddleware, RecostExtension). Correct the provider-rule count from '21+' to '34'. Refs #5 --- CLAUDE.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d066beb..a46e58e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,21 +16,21 @@ Python SDK that automatically tracks outbound HTTP API calls, matches them again ``` recost/ __init__.py # Public API surface (re-exports only) - _init.py # Main entry point — wires interceptor, registry, aggregator, transport; returns EcoAPIHandle - _types.py # All types: RawEvent, MetricEntry, WindowSummary, ProviderDef, EcoAPIConfig, TransportMode - _provider_registry.py # ProviderRegistry — 21+ built-in rules, wildcard host matching, custom provider priority + _init.py # Main entry point — wires interceptor, registry, aggregator, transport; returns RecostHandle + _types.py # All types: RawEvent, MetricEntry, WindowSummary, ProviderDef, RecostConfig, FlushStatus, TransportMode + _provider_registry.py # ProviderRegistry — 34 built-in rules (14 providers), wildcard host matching, custom provider priority _interceptor.py # Patches urllib3.HTTPConnectionPool.urlopen, httpx.Client.send, httpx.AsyncClient.send, aiohttp.ClientSession._request _aggregator.py # Time-windowed bucketing by provider+endpoint+method, p50/p95 percentiles, cost aggregation _transport.py # Cloud mode (HTTPS POST with retry) + local mode (WebSocket with reconnect on background thread) frameworks/ __init__.py - fastapi.py # EcoAPIMiddleware — ASGI middleware for FastAPI/Starlette - flask.py # EcoAPI — Flask extension with init_app() pattern + fastapi.py # RecostMiddleware — ASGI middleware for FastAPI/Starlette + flask.py # RecostExtension — Flask extension with init_app() pattern (ReCost is a deprecated alias) tests/ conftest.py # Fixtures — cleanup interceptor after each test test_scaffold.py # Smoke tests for public API exports test_types.py # MetricEntry & WindowSummary serialization (camelCase conversion) - test_provider_registry.py # All 21 built-in providers, wildcards, Twilio refinement, custom priority + test_provider_registry.py # All 34 built-in provider rules, wildcards, Twilio refinement, custom priority test_aggregator.py # Flush, grouping, percentiles, error counting, byte sums, cost, null handling test_interceptor.py # urllib3/requests, httpx sync+async, aiohttp, lifecycle, double-count prevention test_transport.py # Mode detection, HTTP server mocking, retry logic @@ -63,7 +63,7 @@ mypy recost/ # Type check (strict mode) ## Provider Registry -21+ built-in rules covering: +34 built-in rules across 14 providers: - **AI**: OpenAI (6 endpoint rules), Anthropic (2 rules) - **Payments**: Stripe (5 rules) - **Communication**: Twilio (1 rule with dynamic SMS/voice refinement), SendGrid (2 rules) From 5c0cc51cbd61479e0ebbbd857f4e9ecbf8b5ea1d Mon Sep 17 00:00:00 2001 From: Andres Lopez <190146319+AndresL230@users.noreply.github.com> Date: Wed, 13 May 2026 03:18:35 -0400 Subject: [PATCH 6/7] docs(README): use RecostExtension; document real config fields - Flask example switches to RecostExtension (with a note about the deprecated ReCost alias). - Config table swaps the stale 'flush_interval' (seconds, deprecated) for the canonical 'flush_interval_ms' (milliseconds), with a separate row marking the old form deprecated. - Adds previously-undocumented options: max_buckets, shutdown_flush_timeout_ms. Refs #5 --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e4df87c..ca05818 100644 --- a/README.md +++ b/README.md @@ -81,19 +81,23 @@ app.add_middleware(RecostMiddleware, api_key="...", project_id="...") ```python from flask import Flask -from recost.frameworks.flask import ReCost +from recost.frameworks.flask import RecostExtension app = Flask(__name__) -ReCost(app, api_key="...", project_id="...") +RecostExtension(app, api_key="...", project_id="...") ``` Or using the `init_app` pattern: ```python -recost = ReCost() -recost.init_app(app, api_key="...", project_id="...") +ext = RecostExtension() +ext.init_app(app, api_key="...", project_id="...") ``` +> **Note:** the old class name `ReCost` is still importable as a deprecated +> alias and will continue to work for one release with a `DeprecationWarning`. +> Migrate to `RecostExtension`. + ## Configuration All fields are optional. Pass them as keyword arguments or via a `RecostConfig` instance. @@ -103,8 +107,10 @@ All fields are optional. Pass them as keyword arguments or via a `RecostConfig` | `api_key` | `str` | — | Recost API key (`rc-...`). If omitted, runs in local mode. | | `project_id` | `str` | — | Recost project ID. Required in cloud mode. | | `environment` | `str` | `"development"` | Environment tag attached to all telemetry. | -| `flush_interval` | `float` | `30.0` | Seconds between automatic flushes. | +| `flush_interval_ms` | `int` | `30000` | Milliseconds between automatic aggregator flushes. | +| `flush_interval` | `float` | — | **Deprecated.** Legacy seconds-based flush interval. If set, takes precedence over `flush_interval_ms` and emits a `DeprecationWarning`. Will be removed in a future release. | | `max_batch_size` | `int` | `100` | Early-flush threshold (number of events). | +| `max_buckets` | `int` | `2000` | Maximum unique (provider, endpoint, method) triplets per window. Crossing this triggers an early flush. | | `local_port` | `int` | `9847` | WebSocket port for the VS Code extension. | | `debug` | `bool` | `False` | Log telemetry activity to stderr. | | `enabled` | `bool` | `True` | Master kill switch — set `False` to disable entirely. | @@ -112,7 +118,8 @@ All fields are optional. Pass them as keyword arguments or via a `RecostConfig` | `exclude_patterns` | `list[str]` | `[]` | URL substrings — matching requests are silently dropped. | | `base_url` | `str` | `"https://api.recost.dev"` | Override for self-hosted deployments. | | `max_retries` | `int` | `3` | Retry attempts for failed cloud flushes. | -| `on_error` | `Callable` | — | Called on internal SDK errors. | +| `shutdown_flush_timeout_ms` | `int` | `3000` | How long `dispose()` waits for the final flush to complete before closing the transport. | +| `on_error` | `Callable[[Exception], None]` | — | Called on internal SDK errors. | ### Custom providers From af57b108bb4fab039ff5089f8746d422ed4e755b Mon Sep 17 00:00:00 2001 From: Andres Lopez <190146319+AndresL230@users.noreply.github.com> Date: Wed, 13 May 2026 03:26:28 -0400 Subject: [PATCH 7/7] docs: sweep remaining 'ReCost' brand-spelling stragglers Final review found four docstring/comment references to the old 'ReCost' spelling that the planned task list missed. They are all non-API surface (docstrings + module-level comments), but the PR's stated goal is a single brand spelling, so clean them up here. Touched: - recost/_init.py:83 (init() docstring) - recost/_transport.py:4 (module docstring) - recost/frameworks/fastapi.py:25 (RecostMiddleware docstring) - CLAUDE.md:3 (one-line project description) Refs #5 --- CLAUDE.md | 2 +- recost/_init.py | 2 +- recost/_transport.py | 2 +- recost/frameworks/fastapi.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a46e58e..04cf43f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # recost — Python Middleware -Python SDK that automatically tracks outbound HTTP API calls, matches them against a built-in provider registry, aggregates events into time-windowed summaries, and ships telemetry to the ReCost cloud API or VS Code extension. +Python SDK that automatically tracks outbound HTTP API calls, matches them against a built-in provider registry, aggregates events into time-windowed summaries, and ships telemetry to the Recost cloud API or VS Code extension. ## Tech Stack diff --git a/recost/_init.py b/recost/_init.py index 5578adf..4567250 100644 --- a/recost/_init.py +++ b/recost/_init.py @@ -80,7 +80,7 @@ def dispose(self) -> None: def init(config: Optional[RecostConfig] = None) -> RecostHandle: """ - Initialize the ReCost SDK. + Initialize the Recost SDK. - Patches urllib3, httpx, and aiohttp. - Starts a flush interval that sends aggregated telemetry. diff --git a/recost/_transport.py b/recost/_transport.py index f35c4be..2a02e50 100644 --- a/recost/_transport.py +++ b/recost/_transport.py @@ -1,7 +1,7 @@ """ Transport — delivers WindowSummary payloads to either: - api.recost.dev (cloud mode) via HTTPS POST with exponential-backoff retry, or - - the ReCost VS Code extension (local mode) via WebSocket on localhost. + - the Recost VS Code extension (local mode) via WebSocket on localhost. Uses urllib.request (stdlib) for cloud transport to avoid self-instrumentation (the interceptor patches urllib3, not urllib.request). diff --git a/recost/frameworks/fastapi.py b/recost/frameworks/fastapi.py index ac72549..2ad092a 100644 --- a/recost/frameworks/fastapi.py +++ b/recost/frameworks/fastapi.py @@ -22,7 +22,7 @@ from starlette.responses import Response class RecostMiddleware(BaseHTTPMiddleware): - """ASGI middleware that initializes ReCost telemetry.""" + """ASGI middleware that initializes Recost telemetry.""" def __init__(self, app: Any, config: Optional[RecostConfig] = None, **kwargs: Any) -> None: super().__init__(app)