From 3796e0fd3cccb6416396bffc53df5a55f121ea35 Mon Sep 17 00:00:00 2001 From: "V. David Zvenyach" Date: Wed, 21 Oct 2026 16:56:43 -0500 Subject: [PATCH] feat(metrics): pass group_by, fiscal_year and rolling to the metrics endpoints (v1.19.0) get_entity_metrics, get_naics_metrics and get_psc_metrics built only the path, so callers could not split obligations by funding agency or department or bucket them by fiscal year, though the API has always accepted those query parameters. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 ++++ docs/API_REFERENCE.md | 16 +++++++++ pyproject.toml | 2 +- tango/__init__.py | 2 +- tango/client.py | 75 ++++++++++++++++++++++++++++++++++++---- tests/test_api_parity.py | 26 ++++++++++++++ uv.lock | 2 +- 7 files changed, 119 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 263a0ab..d14cf13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.19.0] - 2026-10-21 + +### Added + +- **`group_by`, `fiscal_year` and `rolling` on `get_entity_metrics()`, `get_naics_metrics()` and `get_psc_metrics()`.** The metrics endpoints have always accepted these query parameters, but the SDK built only the path, so callers could not split obligations by funding `"agency"` or `"department"` or bucket them by federal fiscal year. Each is optional; existing calls send the same request as before. + ## [1.18.2] - 2026-10-19 ### Fixed diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 2cb7d6f..2ddeffa 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -2071,6 +2071,16 @@ psc = client.get_psc("D302") metrics = client.get_psc_metrics(code="D302", months=12, period_grouping="month") ``` +**Parameters:** +- `code` (str): PSC code. Required. +- `months` (int): Trailing window in months. +- `period_grouping` (str): `"month"`, `"quarter"` or `"year"`. +- `group_by` (`"agency"` | `"department"` | list | None): Split each period by funding organization. `agency` is an L2 short code such as `"2100"`; `department` is the CGAC code as an int, e.g. `97` for DoD. +- `fiscal_year` (bool): Bucket by federal fiscal year instead of calendar periods. Default `False`. +- `rolling` (bool): Add rolling averages. The API skips them, with a `warning`, when `group_by` is set. Default `False`. + +`get_naics_metrics()` and `get_entity_metrics()` take the same options. + --- ## MAS SINs @@ -2203,8 +2213,14 @@ lcats = client.list_entity_lcats("ABCDEF123456", limit=25) ```python metrics = client.get_entity_metrics("ABCDEF123456", months=12, period_grouping="month") +# five fiscal years of obligations, split by funding department +by_dept = client.get_entity_metrics( + "ABCDEF123456", months=60, period_grouping="year", group_by="department", fiscal_year=True +) ``` +Takes the same `group_by`, `fiscal_year` and `rolling` options as [`get_psc_metrics()`](#get_psc_metrics). + ### get_entity_budget_flows() Get budget flows for an entity (`/api/entities/{uei}/budget-flows/`) — the federal accounts that funded contracts and assistance awarded to this entity. diff --git a/pyproject.toml b/pyproject.toml index 93c95b7..a696593 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "tango-python" -version = "1.18.2" +version = "1.19.0" description = "Python SDK for the Tango API" readme = "README.md" requires-python = ">=3.12" diff --git a/tango/__init__.py b/tango/__init__.py index 5f43718..07f9069 100644 --- a/tango/__init__.py +++ b/tango/__init__.py @@ -62,7 +62,7 @@ ) from .webhooks.receiver import Delivery, WebhookReceiver -__version__ = "1.18.2" +__version__ = "1.19.0" __all__ = [ "TangoClient", "TangoAPIError", diff --git a/tango/client.py b/tango/client.py index 1fbf405..32c96b8 100644 --- a/tango/client.py +++ b/tango/client.py @@ -100,6 +100,10 @@ def _error_reason(error_data: dict[str, Any]) -> str | None: return None +# Funding-organization keys the metrics endpoints accept in `group_by`. +MetricsGroupBy = Literal["agency", "department"] + + class TangoClient: """Tango API Client""" @@ -5615,7 +5619,15 @@ def get_psc( params["flat_lists"] = "true" return self._get(f"/api/psc/{code}/", params) - def get_psc_metrics(self, code: str, months: int, period_grouping: str) -> dict[str, Any]: + def get_psc_metrics( + self, + code: str, + months: int, + period_grouping: str, + group_by: MetricsGroupBy | list[MetricsGroupBy] | None = None, + fiscal_year: bool = False, + rolling: bool = False, + ) -> dict[str, Any]: """Get rolling PSC metrics (`/api/psc/{code}/metrics/{months}/{period_grouping}/`). Args: @@ -5623,10 +5635,32 @@ def get_psc_metrics(self, code: str, months: int, period_grouping: str) -> dict[ months: Window size in months (e.g. 6, 12, 24, 36). period_grouping: ``"month"``, ``"quarter"``, or ``"year"`` (the values the API accepts on the path segment). + group_by: Split each period by the funding ``"agency"`` (an L2 short code such as ``"2100"``) or ``"department"`` (the CGAC code as an int, e.g. ``97`` for DoD), or both. + fiscal_year: Bucket by federal fiscal year (October-September) instead of calendar periods. + rolling: Add rolling averages. The API skips them, with a ``warning``, when ``group_by`` is set. """ if not code: raise TangoValidationError("PSC code is required") - return self._get(f"/api/psc/{code}/metrics/{months}/{period_grouping}/") + return self._get( + f"/api/psc/{code}/metrics/{months}/{period_grouping}/", + self._metrics_params(group_by, fiscal_year, rolling), + ) + + @staticmethod + def _metrics_params( + group_by: MetricsGroupBy | list[MetricsGroupBy] | None, + fiscal_year: bool, + rolling: bool, + ) -> dict[str, Any]: + """Query params shared by the entity, NAICS and PSC metrics endpoints.""" + params: dict[str, Any] = {} + if group_by: + params["group_by"] = group_by if isinstance(group_by, str) else ",".join(group_by) + if fiscal_year: + params["fiscal_year"] = "true" + if rolling: + params["rolling"] = "true" + return params def get_naics( self, @@ -5647,11 +5681,25 @@ def get_naics( params["flat_lists"] = "true" return self._get(f"/api/naics/{code}/", params) - def get_naics_metrics(self, code: str, months: int, period_grouping: str) -> dict[str, Any]: - """Get rolling NAICS metrics (`/api/naics/{code}/metrics/{months}/{period_grouping}/`).""" + def get_naics_metrics( + self, + code: str, + months: int, + period_grouping: str, + group_by: MetricsGroupBy | list[MetricsGroupBy] | None = None, + fiscal_year: bool = False, + rolling: bool = False, + ) -> dict[str, Any]: + """Get rolling NAICS metrics (`/api/naics/{code}/metrics/{months}/{period_grouping}/`). + + Takes the same ``group_by``, ``fiscal_year`` and ``rolling`` options as :meth:`get_psc_metrics`. + """ if not code: raise TangoValidationError("NAICS code is required") - return self._get(f"/api/naics/{code}/metrics/{months}/{period_grouping}/") + return self._get( + f"/api/naics/{code}/metrics/{months}/{period_grouping}/", + self._metrics_params(group_by, fiscal_year, rolling), + ) def get_business_type( self, @@ -6026,13 +6074,26 @@ def list_entity_lcats( meta=data.get("meta"), ) - def get_entity_metrics(self, uei: str, months: int, period_grouping: str) -> dict[str, Any]: + def get_entity_metrics( + self, + uei: str, + months: int, + period_grouping: str, + group_by: MetricsGroupBy | list[MetricsGroupBy] | None = None, + fiscal_year: bool = False, + rolling: bool = False, + ) -> dict[str, Any]: """Get rolling metrics for an entity (`/api/entities/{uei}/metrics/{months}/{period_grouping}/`). + + Takes the same ``group_by``, ``fiscal_year`` and ``rolling`` options as :meth:`get_psc_metrics`. """ if not uei: raise TangoValidationError("UEI is required") - return self._get(f"/api/entities/{uei}/metrics/{months}/{period_grouping}/") + return self._get( + f"/api/entities/{uei}/metrics/{months}/{period_grouping}/", + self._metrics_params(group_by, fiscal_year, rolling), + ) # ============================================================================ # IDV sub-resources diff --git a/tests/test_api_parity.py b/tests/test_api_parity.py index 1987eb3..5d64093 100644 --- a/tests/test_api_parity.py +++ b/tests/test_api_parity.py @@ -372,6 +372,23 @@ def test_get_psc_metrics(self, mock_request: Mock) -> None: client = TangoClient(api_key="x", base_url="https://t.example") client.get_psc_metrics("R425", 12, "month") assert mock_request.call_args[1]["url"].endswith("/api/psc/R425/metrics/12/month/") + assert not mock_request.call_args[1]["params"] + + def test_metrics_query_params(self, mock_request: Mock) -> None: + mock_request.return_value = _mock_response({"results": []}) + client = TangoClient(api_key="x", base_url="https://t.example") + client.get_psc_metrics("R425", 12, "year", group_by="department", fiscal_year=True) + assert mock_request.call_args[1]["params"] == { + "group_by": "department", + "fiscal_year": "true", + } + client.get_naics_metrics( + "541511", 12, "year", group_by=["agency", "department"], rolling=True + ) + assert mock_request.call_args[1]["params"] == { + "group_by": "agency,department", + "rolling": "true", + } def test_get_naics(self, mock_request: Mock) -> None: mock_request.return_value = _mock_response({"code": "541511"}) @@ -409,6 +426,15 @@ def test_get_entity_metrics(self, mock_request: Mock) -> None: client.get_entity_metrics("UEI1", 24, "quarter") assert mock_request.call_args[1]["url"].endswith("/api/entities/UEI1/metrics/24/quarter/") + def test_get_entity_metrics_by_department(self, mock_request: Mock) -> None: + mock_request.return_value = _mock_response({"results": []}) + client = TangoClient(api_key="x", base_url="https://t.example") + client.get_entity_metrics("UEI1", 60, "year", group_by="department", fiscal_year=True) + assert mock_request.call_args[1]["params"] == { + "group_by": "department", + "fiscal_year": "true", + } + def test_list_entity_lcats(self, mock_request: Mock) -> None: mock_request.return_value = _mock_response( {"count": 0, "next": None, "previous": None, "results": []} diff --git a/uv.lock b/uv.lock index 66928ee..395068d 100644 --- a/uv.lock +++ b/uv.lock @@ -515,7 +515,7 @@ wheels = [ [[package]] name = "tango-python" -version = "1.18.2" +version = "1.19.0" source = { editable = "." } dependencies = [ { name = "httpx" },