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