Skip to content

Commit a14ba50

Browse files
makegov-mark[bot]vdavezclaude
authored
v1.2.0: expose full range-filter surface on list_budget_accounts (#44)
The REST endpoint at /api/budget/accounts/ has always supported __gte / __lte lookups on every numeric metric (the 26 fields in the backend's RANGE_NUMERIC_FIELDS), but the SDK only forwarded identity / taxonomy filters. Callers were forced to discover accounts by exact symbol lookup; pipeline-style queries weren't expressible without dropping to raw HTTP. This change adds 78 explicit named parameters (each of the 26 range fields × {exact, _gte, _lte}) so the discovery query "FY24 accounts where contract share ≥ 60%, unobligated balance ≥ $200M, and next-year growth ≥ 15%, sorted by largest headroom first" is a single SDK call. Per the SDK's filter-surface non-negotiable, every new filter is an explicit kwarg with a type hint — no **kwargs passthrough. Mapping body is table-driven to keep the method readable. Co-authored-by: V. David Zvenyach <dave@zvenyach.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 76766df commit a14ba50

6 files changed

Lines changed: 351 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.2.0] - 2026-06-05
11+
12+
### Added
13+
- `list_budget_accounts()` now exposes the full range-filter surface that the
14+
REST endpoint has always supported. Every numeric metric on
15+
`/api/budget/accounts/` — the 26 fields in the backend's
16+
`RANGE_NUMERIC_FIELDS` list (`enacted_ba`, `apportioned`, `obligated_total`,
17+
`unobligated_balance`, `contract_obligated`,
18+
`contract_share_of_obligated_capped`, `ba_growth_next_year_pct`,
19+
`actual_vs_requested_contract`, all the ratio fields and their `_capped`
20+
variants, etc.) — is now accepted in three forms: exact match (`field=`),
21+
greater-or-equal (`field_gte=`), and less-or-equal (`field_lte=`).
22+
Previously only the identity / taxonomy filters were exposed, which forced
23+
callers to discover accounts by exact symbol lookup; the new surface makes
24+
pipeline-style queries (e.g. "all FY24 accounts where contract share ≥ 60%,
25+
unobligated balance ≥ $200M, and next-year growth ≥ 15%, sorted by largest
26+
headroom first") a single SDK call. The new params map to the API's
27+
`field__gte` / `field__lte` form; `ordering=` already accepted any of these
28+
fields and continues to.
29+
1030
## [1.1.3] - 2026-06-04
1131

1232
### Added

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "tango-python"
7-
version = "1.1.3"
7+
version = "1.2.0"
88
description = "Python SDK for the Tango API"
99
readme = "README.md"
1010
requires-python = ">=3.12"

tango/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
)
4545
from .webhooks.receiver import Delivery, WebhookReceiver
4646

47-
__version__ = "1.1.3"
47+
__version__ = "1.2.0"
4848
__all__ = [
4949
"TangoClient",
5050
"TangoAPIError",

tango/client.py

Lines changed: 260 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2649,6 +2649,85 @@ def list_budget_accounts(
26492649
bea_category: str | None = None,
26502650
on_off_budget: str | None = None,
26512651
subfunction_code: str | None = None,
2652+
# Range-numeric filters: each field also accepts ``__gte`` / ``__lte``.
2653+
requested_ba: float | None = None,
2654+
requested_ba_gte: float | None = None,
2655+
requested_ba_lte: float | None = None,
2656+
enacted_ba: float | None = None,
2657+
enacted_ba_gte: float | None = None,
2658+
enacted_ba_lte: float | None = None,
2659+
apportioned: float | None = None,
2660+
apportioned_gte: float | None = None,
2661+
apportioned_lte: float | None = None,
2662+
obligated_total: float | None = None,
2663+
obligated_total_gte: float | None = None,
2664+
obligated_total_lte: float | None = None,
2665+
outlayed_total: float | None = None,
2666+
outlayed_total_gte: float | None = None,
2667+
outlayed_total_lte: float | None = None,
2668+
unobligated_balance: float | None = None,
2669+
unobligated_balance_gte: float | None = None,
2670+
unobligated_balance_lte: float | None = None,
2671+
contract_obligated: float | None = None,
2672+
contract_obligated_gte: float | None = None,
2673+
contract_obligated_lte: float | None = None,
2674+
contract_outlayed: float | None = None,
2675+
contract_outlayed_gte: float | None = None,
2676+
contract_outlayed_lte: float | None = None,
2677+
assistance_obligated: float | None = None,
2678+
assistance_obligated_gte: float | None = None,
2679+
assistance_obligated_lte: float | None = None,
2680+
assistance_outlayed: float | None = None,
2681+
assistance_outlayed_gte: float | None = None,
2682+
assistance_outlayed_lte: float | None = None,
2683+
contract_share_of_obligated_capped: float | None = None,
2684+
contract_share_of_obligated_capped_gte: float | None = None,
2685+
contract_share_of_obligated_capped_lte: float | None = None,
2686+
obligated_to_apportioned_pct: float | None = None,
2687+
obligated_to_apportioned_pct_gte: float | None = None,
2688+
obligated_to_apportioned_pct_lte: float | None = None,
2689+
obligated_to_apportioned_pct_capped: float | None = None,
2690+
obligated_to_apportioned_pct_capped_gte: float | None = None,
2691+
obligated_to_apportioned_pct_capped_lte: float | None = None,
2692+
apportioned_to_enacted_pct: float | None = None,
2693+
apportioned_to_enacted_pct_gte: float | None = None,
2694+
apportioned_to_enacted_pct_lte: float | None = None,
2695+
apportioned_to_enacted_pct_capped: float | None = None,
2696+
apportioned_to_enacted_pct_capped_gte: float | None = None,
2697+
apportioned_to_enacted_pct_capped_lte: float | None = None,
2698+
obligated_to_enacted_pct: float | None = None,
2699+
obligated_to_enacted_pct_gte: float | None = None,
2700+
obligated_to_enacted_pct_lte: float | None = None,
2701+
obligated_to_enacted_pct_capped: float | None = None,
2702+
obligated_to_enacted_pct_capped_gte: float | None = None,
2703+
obligated_to_enacted_pct_capped_lte: float | None = None,
2704+
outlayed_to_obligated_pct: float | None = None,
2705+
outlayed_to_obligated_pct_gte: float | None = None,
2706+
outlayed_to_obligated_pct_lte: float | None = None,
2707+
outlayed_to_obligated_pct_capped: float | None = None,
2708+
outlayed_to_obligated_pct_capped_gte: float | None = None,
2709+
outlayed_to_obligated_pct_capped_lte: float | None = None,
2710+
unobligated_pct: float | None = None,
2711+
unobligated_pct_gte: float | None = None,
2712+
unobligated_pct_lte: float | None = None,
2713+
enacted_ba_yoy_pct: float | None = None,
2714+
enacted_ba_yoy_pct_gte: float | None = None,
2715+
enacted_ba_yoy_pct_lte: float | None = None,
2716+
obligated_yoy_pct: float | None = None,
2717+
obligated_yoy_pct_gte: float | None = None,
2718+
obligated_yoy_pct_lte: float | None = None,
2719+
enacted_ba_5yr_cagr: float | None = None,
2720+
enacted_ba_5yr_cagr_gte: float | None = None,
2721+
enacted_ba_5yr_cagr_lte: float | None = None,
2722+
ba_growth_next_year_pct: float | None = None,
2723+
ba_growth_next_year_pct_gte: float | None = None,
2724+
ba_growth_next_year_pct_lte: float | None = None,
2725+
actual_vs_requested_contract: float | None = None,
2726+
actual_vs_requested_contract_gte: float | None = None,
2727+
actual_vs_requested_contract_lte: float | None = None,
2728+
actual_vs_requested_contract_capped: float | None = None,
2729+
actual_vs_requested_contract_capped_gte: float | None = None,
2730+
actual_vs_requested_contract_capped_lte: float | None = None,
26522731
search: str | None = None,
26532732
ordering: str | None = None,
26542733
) -> PaginatedResponse:
@@ -2674,8 +2753,49 @@ def list_budget_accounts(
26742753
bea_category: BEA category (exact).
26752754
on_off_budget: On/off budget flag (exact).
26762755
subfunction_code: Subfunction code (exact).
2756+
requested_ba: President's-budget requested BA (exact). Also
2757+
``requested_ba_gte`` / ``requested_ba_lte`` for range queries.
2758+
enacted_ba: Enacted budget authority (exact / gte / lte).
2759+
apportioned: Apportioned amount (exact / gte / lte).
2760+
obligated_total: Total obligated (exact / gte / lte).
2761+
outlayed_total: Total outlayed (exact / gte / lte).
2762+
unobligated_balance: Apportioned minus obligated, in dollars
2763+
(exact / gte / lte). Use ``__gte`` to surface accounts with
2764+
appropriated headroom that hasn't yet hit contract.
2765+
contract_obligated: Contract-only obligated (exact / gte / lte).
2766+
contract_outlayed: Contract-only outlayed (exact / gte / lte).
2767+
assistance_obligated: Assistance-only obligated (exact / gte / lte).
2768+
assistance_outlayed: Assistance-only outlayed (exact / gte / lte).
2769+
contract_share_of_obligated_capped: Contracts as share of
2770+
obligated, capped at 1.0 (exact / gte / lte). Use ``__gte`` to
2771+
filter to contract-heavy accounts.
2772+
obligated_to_apportioned_pct: Burn ratio
2773+
(obligated / apportioned). Also ``_capped`` variant capped at
2774+
1.0. Both expose exact / gte / lte.
2775+
apportioned_to_enacted_pct: Apportionment ratio
2776+
(apportioned / enacted). Also ``_capped`` variant. Exact / gte / lte.
2777+
obligated_to_enacted_pct: Obligated-to-enacted ratio. Also
2778+
``_capped`` variant. Exact / gte / lte.
2779+
outlayed_to_obligated_pct: Spendout ratio (outlayed / obligated).
2780+
Also ``_capped`` variant. Exact / gte / lte.
2781+
unobligated_pct: Unobligated share of apportioned (exact / gte / lte).
2782+
enacted_ba_yoy_pct: Year-over-year enacted BA growth
2783+
(exact / gte / lte).
2784+
obligated_yoy_pct: Year-over-year obligated growth
2785+
(exact / gte / lte).
2786+
enacted_ba_5yr_cagr: 5-year compound annual growth of enacted BA
2787+
(exact / gte / lte).
2788+
ba_growth_next_year_pct: Next-year requested BA growth
2789+
(exact / gte / lte). Use ``__gte`` for forward-looking
2790+
pipeline discovery.
2791+
actual_vs_requested_contract: Realization ratio of contract
2792+
obligated against the prior-year request (exact / gte / lte).
2793+
Also ``_capped`` variant.
26772794
search: Full-text search over account_title/agency_name/bureau_name.
2678-
ordering: Sort field (prefix with '-' for descending).
2795+
ordering: Sort field (prefix with '-' for descending). Any of the
2796+
numeric fields above is a valid ordering target — e.g.
2797+
``ordering="-unobligated_balance"`` to rank by largest
2798+
headroom first.
26792799
"""
26802800
params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
26812801
if shape is None:
@@ -2686,7 +2806,7 @@ def list_budget_accounts(
26862806
params["flat"] = "true"
26872807
if flat_lists:
26882808
params["flat_lists"] = "true"
2689-
for key, val in (
2809+
scalar_filters: tuple[tuple[str, Any], ...] = (
26902810
("federal_account_symbol", federal_account_symbol),
26912811
("fiscal_year", fiscal_year),
26922812
("fiscal_year__gte", fiscal_year_gte),
@@ -2699,9 +2819,146 @@ def list_budget_accounts(
26992819
("subfunction_code", subfunction_code),
27002820
("search", search),
27012821
("ordering", ordering),
2702-
):
2822+
)
2823+
for key, val in scalar_filters:
27032824
if val is not None:
27042825
params[key] = val
2826+
# Range-numeric filters: each field has exact / __gte / __lte forms.
2827+
range_filters: tuple[tuple[str, float | None, float | None, float | None], ...] = (
2828+
("requested_ba", requested_ba, requested_ba_gte, requested_ba_lte),
2829+
("enacted_ba", enacted_ba, enacted_ba_gte, enacted_ba_lte),
2830+
("apportioned", apportioned, apportioned_gte, apportioned_lte),
2831+
("obligated_total", obligated_total, obligated_total_gte, obligated_total_lte),
2832+
("outlayed_total", outlayed_total, outlayed_total_gte, outlayed_total_lte),
2833+
(
2834+
"unobligated_balance",
2835+
unobligated_balance,
2836+
unobligated_balance_gte,
2837+
unobligated_balance_lte,
2838+
),
2839+
(
2840+
"contract_obligated",
2841+
contract_obligated,
2842+
contract_obligated_gte,
2843+
contract_obligated_lte,
2844+
),
2845+
(
2846+
"contract_outlayed",
2847+
contract_outlayed,
2848+
contract_outlayed_gte,
2849+
contract_outlayed_lte,
2850+
),
2851+
(
2852+
"assistance_obligated",
2853+
assistance_obligated,
2854+
assistance_obligated_gte,
2855+
assistance_obligated_lte,
2856+
),
2857+
(
2858+
"assistance_outlayed",
2859+
assistance_outlayed,
2860+
assistance_outlayed_gte,
2861+
assistance_outlayed_lte,
2862+
),
2863+
(
2864+
"contract_share_of_obligated_capped",
2865+
contract_share_of_obligated_capped,
2866+
contract_share_of_obligated_capped_gte,
2867+
contract_share_of_obligated_capped_lte,
2868+
),
2869+
(
2870+
"obligated_to_apportioned_pct",
2871+
obligated_to_apportioned_pct,
2872+
obligated_to_apportioned_pct_gte,
2873+
obligated_to_apportioned_pct_lte,
2874+
),
2875+
(
2876+
"obligated_to_apportioned_pct_capped",
2877+
obligated_to_apportioned_pct_capped,
2878+
obligated_to_apportioned_pct_capped_gte,
2879+
obligated_to_apportioned_pct_capped_lte,
2880+
),
2881+
(
2882+
"apportioned_to_enacted_pct",
2883+
apportioned_to_enacted_pct,
2884+
apportioned_to_enacted_pct_gte,
2885+
apportioned_to_enacted_pct_lte,
2886+
),
2887+
(
2888+
"apportioned_to_enacted_pct_capped",
2889+
apportioned_to_enacted_pct_capped,
2890+
apportioned_to_enacted_pct_capped_gte,
2891+
apportioned_to_enacted_pct_capped_lte,
2892+
),
2893+
(
2894+
"obligated_to_enacted_pct",
2895+
obligated_to_enacted_pct,
2896+
obligated_to_enacted_pct_gte,
2897+
obligated_to_enacted_pct_lte,
2898+
),
2899+
(
2900+
"obligated_to_enacted_pct_capped",
2901+
obligated_to_enacted_pct_capped,
2902+
obligated_to_enacted_pct_capped_gte,
2903+
obligated_to_enacted_pct_capped_lte,
2904+
),
2905+
(
2906+
"outlayed_to_obligated_pct",
2907+
outlayed_to_obligated_pct,
2908+
outlayed_to_obligated_pct_gte,
2909+
outlayed_to_obligated_pct_lte,
2910+
),
2911+
(
2912+
"outlayed_to_obligated_pct_capped",
2913+
outlayed_to_obligated_pct_capped,
2914+
outlayed_to_obligated_pct_capped_gte,
2915+
outlayed_to_obligated_pct_capped_lte,
2916+
),
2917+
("unobligated_pct", unobligated_pct, unobligated_pct_gte, unobligated_pct_lte),
2918+
(
2919+
"enacted_ba_yoy_pct",
2920+
enacted_ba_yoy_pct,
2921+
enacted_ba_yoy_pct_gte,
2922+
enacted_ba_yoy_pct_lte,
2923+
),
2924+
(
2925+
"obligated_yoy_pct",
2926+
obligated_yoy_pct,
2927+
obligated_yoy_pct_gte,
2928+
obligated_yoy_pct_lte,
2929+
),
2930+
(
2931+
"enacted_ba_5yr_cagr",
2932+
enacted_ba_5yr_cagr,
2933+
enacted_ba_5yr_cagr_gte,
2934+
enacted_ba_5yr_cagr_lte,
2935+
),
2936+
(
2937+
"ba_growth_next_year_pct",
2938+
ba_growth_next_year_pct,
2939+
ba_growth_next_year_pct_gte,
2940+
ba_growth_next_year_pct_lte,
2941+
),
2942+
(
2943+
"actual_vs_requested_contract",
2944+
actual_vs_requested_contract,
2945+
actual_vs_requested_contract_gte,
2946+
actual_vs_requested_contract_lte,
2947+
),
2948+
(
2949+
"actual_vs_requested_contract_capped",
2950+
actual_vs_requested_contract_capped,
2951+
actual_vs_requested_contract_capped_gte,
2952+
actual_vs_requested_contract_capped_lte,
2953+
),
2954+
)
2955+
for field, exact, gte, lte in range_filters:
2956+
if exact is not None:
2957+
params[field] = exact
2958+
if gte is not None:
2959+
params[f"{field}__gte"] = gte
2960+
if lte is not None:
2961+
params[f"{field}__lte"] = lte
27052962
data = self._get("/api/budget/accounts/", params)
27062963
results = [
27072964
self._parse_response_with_shape(obj, shape, BudgetAccount, flat, flat_lists)

0 commit comments

Comments
 (0)