From 332402f0e8be2a03664f8964c79eb8f8bb43da94 Mon Sep 17 00:00:00 2001 From: "V. David Zvenyach" Date: Thu, 4 Jun 2026 15:22:55 -0500 Subject: [PATCH] feat(client): add shape/flat/flat_lists to reference-data endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tango API has always supported the shape system on NAICS, PSC, Assistance Listings, Business Types, and MAS SINs viewsets (all use `ShapeOnDemandMixin`), but the SDK's list/get methods for these resources didn't expose `shape` / `flat` / `flat_lists`. Adds the three params to: list_naics/get_naics, list_psc/get_psc, list_assistance_listings/ get_assistance_listing, list_business_types/get_business_type, and list_mas_sins/get_mas_sin. When `shape` is omitted, the API applies its own per-resource default — existing callers see no behavior change. `list_business_types` returns raw dicts when a shape is supplied so the caller gets exactly the shape requested (otherwise still yields `BusinessType` instances). Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 13 ++++ tango/client.py | 169 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 165 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0d2461..564d0ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Reference-data list/get methods now accept `shape` (and the associated + `flat` / `flat_lists`) parameters, matching the underlying API which has + always supported the shape system via `ShapeOnDemandMixin`. Affected: + `list_naics` / `get_naics`, `list_psc` / `get_psc`, + `list_assistance_listings` / `get_assistance_listing`, + `list_business_types` / `get_business_type`, + `list_mas_sins` / `get_mas_sin`. When `shape` is omitted, behavior is + unchanged — the API applies its own per-resource default. + `list_business_types` returns raw dicts (instead of `BusinessType` + instances) when a `shape` is supplied so the caller gets exactly the + shape requested. + ## [1.1.2] - 2026-06-04 ### Changed diff --git a/tango/client.py b/tango/client.py index 6b32731..919359a 100644 --- a/tango/client.py +++ b/tango/client.py @@ -1923,15 +1923,37 @@ def list_vehicle_orders( ) # Business Types endpoints - def list_business_types(self, page: int = 1, limit: int = 25) -> PaginatedResponse: - """List business types""" - params = {"page": page, "limit": min(limit, 100)} + def list_business_types( + self, + page: int = 1, + limit: int = 25, + shape: str | None = None, + flat: bool = False, + flat_lists: bool = False, + ) -> PaginatedResponse: + """List business types. + + When ``shape`` is omitted the API applies its own default + (``name,code``) and results are returned as :class:`BusinessType` + instances. When ``shape`` is provided, raw dicts are returned so the + caller can rely on the exact shape requested. + """ + params: dict[str, Any] = {"page": page, "limit": min(limit, 100)} + if shape: + params["shape"] = shape + if flat: + params["flat"] = "true" + if flat_lists: + params["flat_lists"] = "true" data = self._get("/api/business_types/", params) + results: list[Any] = ( + list(data["results"]) if shape else [BusinessType(**btype) for btype in data["results"]] + ) return PaginatedResponse( count=data["count"], next=data.get("next"), previous=data.get("previous"), - results=[BusinessType(**btype) for btype in data["results"]], + results=results, ) def list_naics( @@ -1945,8 +1967,17 @@ def list_naics( revenue_limit_gte: int | None = None, revenue_limit_lte: int | None = None, search: str | None = None, + shape: str | None = None, + flat: bool = False, + flat_lists: bool = False, ) -> PaginatedResponse: - """List NAICS codes (`/api/naics/`).""" + """List NAICS codes (`/api/naics/`). + + When ``shape`` is omitted the API applies its own default. Passing any + of the ``revenue_limit*`` / ``employee_limit*`` filters causes the API + to widen the default shape to include ``size_standards`` and + ``federal_obligations``. + """ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)} if employee_limit is not None: params["employee_limit"] = employee_limit @@ -1962,6 +1993,12 @@ def list_naics( params["revenue_limit_lte"] = revenue_limit_lte if search is not None: params["search"] = search + if shape: + params["shape"] = shape + if flat: + params["flat"] = "true" + if flat_lists: + params["flat_lists"] = "true" data = self._get("/api/naics/", params) return PaginatedResponse( count=data.get("count", 0), @@ -3344,9 +3381,22 @@ def get_department(self, code: str) -> dict[str, Any]: raise TangoValidationError("Department code is required") return self._get(f"/api/departments/{code}/") - def list_psc(self, page: int = 1, limit: int = 25) -> PaginatedResponse[dict[str, Any]]: + def list_psc( + self, + page: int = 1, + limit: int = 25, + shape: str | None = None, + flat: bool = False, + flat_lists: bool = False, + ) -> PaginatedResponse[dict[str, Any]]: """List Product Service Codes (`/api/psc/`).""" params: dict[str, Any] = {"page": page, "limit": min(limit, 100)} + if shape: + params["shape"] = shape + if flat: + params["flat"] = "true" + if flat_lists: + params["flat_lists"] = "true" data = self._get("/api/psc/", params) return PaginatedResponse( count=int(data.get("count", 0)), @@ -3355,11 +3405,24 @@ def list_psc(self, page: int = 1, limit: int = 25) -> PaginatedResponse[dict[str results=list(data.get("results") or []), ) - def get_psc(self, code: str) -> dict[str, Any]: + def get_psc( + self, + code: str, + shape: str | None = None, + flat: bool = False, + flat_lists: bool = False, + ) -> dict[str, Any]: """Get a Product Service Code by code (`/api/psc/{code}/`).""" if not code: raise TangoValidationError("PSC code is required") - return self._get(f"/api/psc/{code}/") + params: dict[str, Any] = {} + if shape: + params["shape"] = shape + if flat: + params["flat"] = "true" + if flat_lists: + 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]: """Get rolling PSC metrics (`/api/psc/{code}/metrics/{months}/{period_grouping}/`). @@ -3374,11 +3437,24 @@ def get_psc_metrics(self, code: str, months: int, period_grouping: str) -> dict[ raise TangoValidationError("PSC code is required") return self._get(f"/api/psc/{code}/metrics/{months}/{period_grouping}/") - def get_naics(self, code: str) -> dict[str, Any]: + def get_naics( + self, + code: str, + shape: str | None = None, + flat: bool = False, + flat_lists: bool = False, + ) -> dict[str, Any]: """Get a NAICS code by code (`/api/naics/{code}/`).""" if not code: raise TangoValidationError("NAICS code is required") - return self._get(f"/api/naics/{code}/") + params: dict[str, Any] = {} + if shape: + params["shape"] = shape + if flat: + params["flat"] = "true" + if flat_lists: + 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}/`).""" @@ -3386,17 +3462,41 @@ def get_naics_metrics(self, code: str, months: int, period_grouping: str) -> dic raise TangoValidationError("NAICS code is required") return self._get(f"/api/naics/{code}/metrics/{months}/{period_grouping}/") - def get_business_type(self, code: str) -> dict[str, Any]: + def get_business_type( + self, + code: str, + shape: str | None = None, + flat: bool = False, + flat_lists: bool = False, + ) -> dict[str, Any]: """Get a business type by code (`/api/business_types/{code}/`).""" if not code: raise TangoValidationError("Business type code is required") - return self._get(f"/api/business_types/{code}/") + params: dict[str, Any] = {} + if shape: + params["shape"] = shape + if flat: + params["flat"] = "true" + if flat_lists: + params["flat_lists"] = "true" + return self._get(f"/api/business_types/{code}/", params) def list_assistance_listings( - self, page: int = 1, limit: int = 25 + self, + page: int = 1, + limit: int = 25, + shape: str | None = None, + flat: bool = False, + flat_lists: bool = False, ) -> PaginatedResponse[dict[str, Any]]: """List Assistance Listings (CFDA programs) (`/api/assistance_listings/`).""" params: dict[str, Any] = {"page": page, "limit": min(limit, 100)} + if shape: + params["shape"] = shape + if flat: + params["flat"] = "true" + if flat_lists: + params["flat_lists"] = "true" data = self._get("/api/assistance_listings/", params) return PaginatedResponse( count=int(data.get("count", 0)), @@ -3405,22 +3505,44 @@ def list_assistance_listings( results=list(data.get("results") or []), ) - def get_assistance_listing(self, number: str) -> dict[str, Any]: + def get_assistance_listing( + self, + number: str, + shape: str | None = None, + flat: bool = False, + flat_lists: bool = False, + ) -> dict[str, Any]: """Get an Assistance Listing by CFDA number (`/api/assistance_listings/{number}/`).""" if not number: raise TangoValidationError("Assistance listing number is required") - return self._get(f"/api/assistance_listings/{number}/") + params: dict[str, Any] = {} + if shape: + params["shape"] = shape + if flat: + params["flat"] = "true" + if flat_lists: + params["flat_lists"] = "true" + return self._get(f"/api/assistance_listings/{number}/", params) def list_mas_sins( self, page: int = 1, limit: int = 25, search: str | None = None, + shape: str | None = None, + flat: bool = False, + flat_lists: bool = False, ) -> PaginatedResponse[dict[str, Any]]: """List GSA MAS SINs (`/api/mas_sins/`).""" params: dict[str, Any] = {"page": page, "limit": min(limit, 100)} if search is not None: params["search"] = search + if shape: + params["shape"] = shape + if flat: + params["flat"] = "true" + if flat_lists: + params["flat_lists"] = "true" data = self._get("/api/mas_sins/", params) return PaginatedResponse( count=int(data.get("count", 0)), @@ -3429,11 +3551,24 @@ def list_mas_sins( results=list(data.get("results") or []), ) - def get_mas_sin(self, sin: str) -> dict[str, Any]: + def get_mas_sin( + self, + sin: str, + shape: str | None = None, + flat: bool = False, + flat_lists: bool = False, + ) -> dict[str, Any]: """Get a MAS SIN by code (`/api/mas_sins/{sin}/`).""" if not sin: raise TangoValidationError("MAS SIN is required") - return self._get(f"/api/mas_sins/{sin}/") + params: dict[str, Any] = {} + if shape: + params["shape"] = shape + if flat: + params["flat"] = "true" + if flat_lists: + params["flat_lists"] = "true" + return self._get(f"/api/mas_sins/{sin}/", params) # ============================================================================ # Entity sub-resources