Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion tango/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
)
from .webhooks.receiver import Delivery, WebhookReceiver

__version__ = "1.18.2"
__version__ = "1.19.0"
__all__ = [
"TangoClient",
"TangoAPIError",
Expand Down
75 changes: 68 additions & 7 deletions tango/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down Expand Up @@ -5615,18 +5619,48 @@ 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:
code: PSC code.
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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions tests/test_api_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
Expand Down Expand Up @@ -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": []}
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading