tango-python 0.4.1__py3-none-any.whl → 0.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tango/__init__.py CHANGED
@@ -11,6 +11,7 @@ from .exceptions import (
11
11
  from .models import (
12
12
  GsaElibraryContract,
13
13
  PaginatedResponse,
14
+ RateLimitInfo,
14
15
  SearchFilters,
15
16
  ShapeConfig,
16
17
  WebhookEndpoint,
@@ -27,7 +28,7 @@ from .shapes import (
27
28
  TypeGenerator,
28
29
  )
29
30
 
30
- __version__ = "0.4.1"
31
+ __version__ = "0.4.3"
31
32
  __all__ = [
32
33
  "TangoClient",
33
34
  "TangoAPIError",
@@ -35,6 +36,7 @@ __all__ = [
35
36
  "TangoNotFoundError",
36
37
  "TangoValidationError",
37
38
  "TangoRateLimitError",
39
+ "RateLimitInfo",
38
40
  "GsaElibraryContract",
39
41
  "PaginatedResponse",
40
42
  "SearchFilters",
tango/client.py CHANGED
@@ -31,6 +31,8 @@ from tango.models import (
31
31
  Opportunity,
32
32
  Organization,
33
33
  PaginatedResponse,
34
+ Protest,
35
+ RateLimitInfo,
34
36
  SearchFilters,
35
37
  ShapeConfig,
36
38
  Subaward,
@@ -76,6 +78,7 @@ class TangoClient:
76
78
  headers["X-API-KEY"] = self.api_key
77
79
 
78
80
  self.client = httpx.Client(headers=headers, timeout=30.0)
81
+ self._last_rate_limit_info: RateLimitInfo | None = None
79
82
 
80
83
  # Use hardcoded sensible defaults
81
84
  cache_size = 100
@@ -97,6 +100,34 @@ class TangoClient:
97
100
  # Core HTTP Request Utilities
98
101
  # ============================================================================
99
102
 
103
+ @property
104
+ def rate_limit_info(self) -> RateLimitInfo | None:
105
+ """Rate limit info from the most recent API response."""
106
+ return self._last_rate_limit_info
107
+
108
+ @staticmethod
109
+ def _parse_rate_limit_headers(headers: httpx.Headers) -> RateLimitInfo:
110
+ """Extract rate limit info from response headers."""
111
+ def _int_or_none(val: str | None) -> int | None:
112
+ if val is None:
113
+ return None
114
+ try:
115
+ return int(val)
116
+ except (ValueError, TypeError):
117
+ return None
118
+
119
+ return RateLimitInfo(
120
+ limit=_int_or_none(headers.get("X-RateLimit-Limit")),
121
+ remaining=_int_or_none(headers.get("X-RateLimit-Remaining")),
122
+ reset=_int_or_none(headers.get("X-RateLimit-Reset")),
123
+ daily_limit=_int_or_none(headers.get("X-RateLimit-Daily-Limit")),
124
+ daily_remaining=_int_or_none(headers.get("X-RateLimit-Daily-Remaining")),
125
+ daily_reset=_int_or_none(headers.get("X-RateLimit-Daily-Reset")),
126
+ burst_limit=_int_or_none(headers.get("X-RateLimit-Burst-Limit")),
127
+ burst_remaining=_int_or_none(headers.get("X-RateLimit-Burst-Remaining")),
128
+ burst_reset=_int_or_none(headers.get("X-RateLimit-Burst-Reset")),
129
+ )
130
+
100
131
  def _request(
101
132
  self,
102
133
  method: str,
@@ -109,6 +140,7 @@ class TangoClient:
109
140
 
110
141
  try:
111
142
  response = self.client.request(method=method, url=url, params=params, json=json_data)
143
+ self._last_rate_limit_info = self._parse_rate_limit_headers(response.headers)
112
144
 
113
145
  if response.status_code == 401:
114
146
  raise TangoAuthError(
@@ -135,7 +167,9 @@ class TangoClient:
135
167
  error_data,
136
168
  )
137
169
  elif response.status_code == 429:
138
- raise TangoRateLimitError("Rate limit exceeded", response.status_code)
170
+ error_data = response.json() if response.content else {}
171
+ detail = error_data.get("detail", "Rate limit exceeded")
172
+ raise TangoRateLimitError(detail, response.status_code, error_data)
139
173
  elif not response.is_success:
140
174
  raise TangoAPIError(
141
175
  f"API request failed with status {response.status_code}", response.status_code
@@ -943,32 +977,6 @@ class TangoClient:
943
977
  page_metadata=data.get("page_metadata"),
944
978
  )
945
979
 
946
- def get_idv_summary(self, identifier: str) -> dict[str, Any]:
947
- """Get a summary for an IDV solicitation identifier (`/api/idvs/{identifier}/summary/`)."""
948
- return self._get(f"/api/idvs/{identifier}/summary/")
949
-
950
- def list_idv_summary_awards(
951
- self,
952
- identifier: str,
953
- limit: int = 25,
954
- cursor: str | None = None,
955
- ordering: str | None = None,
956
- ) -> PaginatedResponse:
957
- """List awards under an IDV summary (`/api/idvs/{identifier}/summary/awards/`)."""
958
- params: dict[str, Any] = {"limit": min(limit, 100)}
959
- if cursor:
960
- params["cursor"] = cursor
961
- if ordering:
962
- params["ordering"] = ordering
963
- data = self._get(f"/api/idvs/{identifier}/summary/awards/", params)
964
- return PaginatedResponse(
965
- count=int(data.get("count") or len(data.get("results") or [])),
966
- next=data.get("next"),
967
- previous=data.get("previous"),
968
- results=data.get("results") or [],
969
- page_metadata=data.get("page_metadata"),
970
- )
971
-
972
980
  def list_otas(
973
981
  self,
974
982
  limit: int = 25,
@@ -1859,6 +1867,129 @@ class TangoClient:
1859
1867
  results=results,
1860
1868
  )
1861
1869
 
1870
+ # Protest endpoints
1871
+ # See https://tango.makegov.com/docs/api-reference/protests.md
1872
+ # Note: Protests API does not support ordering (returns 400 if provided).
1873
+ # Use shape=...,dockets(...) to request nested dockets.
1874
+ def list_protests(
1875
+ self,
1876
+ page: int = 1,
1877
+ limit: int = 25,
1878
+ shape: str | None = None,
1879
+ flat: bool = False,
1880
+ flat_lists: bool = False,
1881
+ source_system: str | None = None,
1882
+ outcome: str | None = None,
1883
+ case_type: str | None = None,
1884
+ agency: str | None = None,
1885
+ case_number: str | None = None,
1886
+ solicitation_number: str | None = None,
1887
+ protester: str | None = None,
1888
+ filed_date_after: str | None = None,
1889
+ filed_date_before: str | None = None,
1890
+ decision_date_after: str | None = None,
1891
+ decision_date_before: str | None = None,
1892
+ search: str | None = None,
1893
+ ) -> PaginatedResponse:
1894
+ """
1895
+ List bid protests.
1896
+
1897
+ Returns case-level protest records. Use shape=...,dockets(...) to include
1898
+ nested dockets. API reference: https://tango.makegov.com/docs/api-reference/protests.md
1899
+
1900
+ Args:
1901
+ page: Page number
1902
+ limit: Results per page (max 100)
1903
+ shape: Response shape string (defaults to minimal shape)
1904
+ flat: If True, flatten nested objects in shaped response
1905
+ flat_lists: If True, flatten arrays using indexed keys
1906
+ source_system: Filter by source system (e.g. gao)
1907
+ outcome: Filter by outcome (e.g. Denied, Dismissed, Withdrawn, Sustained)
1908
+ case_type: Filter by case type
1909
+ agency: Filter by protested agency text
1910
+ case_number: Filter by case number (e.g. b-423274)
1911
+ solicitation_number: Filter by exact solicitation number
1912
+ protester: Filter by protester name text
1913
+ filed_date_after: Filed date on or after
1914
+ filed_date_before: Filed date on or before
1915
+ decision_date_after: Decision date on or after
1916
+ decision_date_before: Decision date on or before
1917
+ search: Full-text search over protest searchable fields
1918
+ """
1919
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1920
+
1921
+ if shape is None:
1922
+ shape = ShapeConfig.PROTESTS_MINIMAL
1923
+ if shape:
1924
+ params["shape"] = shape
1925
+ if flat:
1926
+ params["flat"] = "true"
1927
+ if flat_lists:
1928
+ params["flat_lists"] = "true"
1929
+
1930
+ for key, val in (
1931
+ ("source_system", source_system),
1932
+ ("outcome", outcome),
1933
+ ("case_type", case_type),
1934
+ ("agency", agency),
1935
+ ("case_number", case_number),
1936
+ ("solicitation_number", solicitation_number),
1937
+ ("protester", protester),
1938
+ ("filed_date_after", filed_date_after),
1939
+ ("filed_date_before", filed_date_before),
1940
+ ("decision_date_after", decision_date_after),
1941
+ ("decision_date_before", decision_date_before),
1942
+ ("search", search),
1943
+ ):
1944
+ if val is not None:
1945
+ params[key] = val
1946
+
1947
+ data = self._get("/api/protests/", params)
1948
+
1949
+ results = [
1950
+ self._parse_response_with_shape(item, shape, Protest, flat, flat_lists)
1951
+ for item in data["results"]
1952
+ ]
1953
+
1954
+ return PaginatedResponse(
1955
+ count=data["count"],
1956
+ next=data.get("next"),
1957
+ previous=data.get("previous"),
1958
+ results=results,
1959
+ )
1960
+
1961
+ def get_protest(
1962
+ self,
1963
+ case_id: str,
1964
+ shape: str | None = None,
1965
+ flat: bool = False,
1966
+ flat_lists: bool = False,
1967
+ ) -> Any:
1968
+ """
1969
+ Get a single protest by case_id (RFC 4122 UUID).
1970
+
1971
+ Use shape=...,dockets(...) to include nested dockets.
1972
+ API reference: https://tango.makegov.com/docs/api-reference/protests.md
1973
+
1974
+ Args:
1975
+ case_id: Deterministic case UUID (from source_system + base_case_number)
1976
+ shape: Response shape string (defaults to minimal shape)
1977
+ flat: If True, flatten nested objects in shaped response
1978
+ flat_lists: If True, flatten arrays using indexed keys
1979
+ """
1980
+ params: dict[str, Any] = {}
1981
+ if shape is None:
1982
+ shape = ShapeConfig.PROTESTS_MINIMAL
1983
+ if shape:
1984
+ params["shape"] = shape
1985
+ if flat:
1986
+ params["flat"] = "true"
1987
+ if flat_lists:
1988
+ params["flat_lists"] = "true"
1989
+
1990
+ data = self._get(f"/api/protests/{case_id}/", params)
1991
+ return self._parse_response_with_shape(data, shape, Protest, flat, flat_lists)
1992
+
1862
1993
  # Grant endpoints
1863
1994
  def list_grants(
1864
1995
  self,
@@ -1945,47 +2076,6 @@ class TangoClient:
1945
2076
  results=results,
1946
2077
  )
1947
2078
 
1948
- def list_assistance(
1949
- self,
1950
- limit: int = 25,
1951
- cursor: str | None = None,
1952
- assistance_type: str | None = None,
1953
- award_key: str | None = None,
1954
- fiscal_year: int | None = None,
1955
- fiscal_year_gte: int | None = None,
1956
- fiscal_year_lte: int | None = None,
1957
- highly_compensated_officers: str | None = None,
1958
- recipient: str | None = None,
1959
- recipient_address: str | None = None,
1960
- search: str | None = None,
1961
- ) -> PaginatedResponse:
1962
- """List assistance (financial assistance) transactions (`/api/assistance/`). Keyset pagination."""
1963
- params: dict[str, Any] = {"limit": min(limit, 100)}
1964
- if cursor:
1965
- params["cursor"] = cursor
1966
- for key, val in (
1967
- ("assistance_type", assistance_type),
1968
- ("award_key", award_key),
1969
- ("fiscal_year", fiscal_year),
1970
- ("fiscal_year_gte", fiscal_year_gte),
1971
- ("fiscal_year_lte", fiscal_year_lte),
1972
- ("highly_compensated_officers", highly_compensated_officers),
1973
- ("recipient", recipient),
1974
- ("recipient_address", recipient_address),
1975
- ("search", search),
1976
- ):
1977
- if val is not None:
1978
- params[key] = val
1979
- data = self._get("/api/assistance/", params)
1980
- return PaginatedResponse(
1981
- count=int(data.get("count") or len(data.get("results") or [])),
1982
- next=data.get("next"),
1983
- previous=data.get("previous"),
1984
- results=data.get("results", []),
1985
- cursor=data.get("cursor"),
1986
- page_metadata=data.get("page_metadata"),
1987
- )
1988
-
1989
2079
  # ============================================================================
1990
2080
  # Webhooks (v2)
1991
2081
  # ============================================================================
tango/exceptions.py CHANGED
@@ -39,7 +39,34 @@ class TangoValidationError(TangoAPIError):
39
39
  class TangoRateLimitError(TangoAPIError):
40
40
  """Rate limit exceeded error"""
41
41
 
42
- pass
42
+ @property
43
+ def wait_in_seconds(self) -> int | None:
44
+ """Seconds to wait before retrying, from API response."""
45
+ val = self.response_data.get("wait_in_seconds")
46
+ if val is not None:
47
+ try:
48
+ return int(val)
49
+ except (ValueError, TypeError):
50
+ return None
51
+ return None
52
+
53
+ @property
54
+ def detail(self) -> str | None:
55
+ """Human-readable detail from API response."""
56
+ return self.response_data.get("detail")
57
+
58
+ @property
59
+ def limit_type(self) -> str | None:
60
+ """Which limit was hit: 'burst' or 'daily', parsed from detail."""
61
+ d = self.detail
62
+ if not d:
63
+ return None
64
+ lower = d.lower()
65
+ if "burst" in lower or "minute" in lower:
66
+ return "burst"
67
+ if "daily" in lower or "day" in lower:
68
+ return "daily"
69
+ return None
43
70
 
44
71
 
45
72
  class ShapeError(TangoAPIError):
tango/models.py CHANGED
@@ -23,6 +23,21 @@ T = TypeVar("T")
23
23
  # ============================================================================
24
24
 
25
25
 
26
+ @dataclass
27
+ class RateLimitInfo:
28
+ """Rate limit information from API response headers."""
29
+
30
+ limit: int | None = None
31
+ remaining: int | None = None
32
+ reset: int | None = None
33
+ daily_limit: int | None = None
34
+ daily_remaining: int | None = None
35
+ daily_reset: int | None = None
36
+ burst_limit: int | None = None
37
+ burst_remaining: int | None = None
38
+ burst_reset: int | None = None
39
+
40
+
26
41
  @dataclass
27
42
  class SearchFilters:
28
43
  """Search filter parameters for contract search
@@ -433,6 +448,33 @@ class Notice:
433
448
  naics_code: str | None = None
434
449
 
435
450
 
451
+ @dataclass
452
+ class Protest:
453
+ """Schema definition for Protest (not used for instances)
454
+
455
+ Bid protest records at /api/protests/. Case-level object identified by case_id (UUID).
456
+ See https://tango.makegov.com/docs/api-reference/protests.md.
457
+ """
458
+
459
+ case_id: str
460
+ case_number: str | None = None
461
+ title: str | None = None
462
+ source_system: str | None = None
463
+ outcome: str | None = None
464
+ agency: str | None = None
465
+ protester: str | None = None
466
+ solicitation_number: str | None = None
467
+ case_type: str | None = None
468
+ filed_date: datetime | None = None
469
+ posted_date: datetime | None = None
470
+ decision_date: datetime | None = None
471
+ due_date: datetime | None = None
472
+ docket_url: str | None = None
473
+ decision_url: str | None = None
474
+ digest: str | None = None
475
+ dockets: list[dict[str, Any]] | None = None
476
+
477
+
436
478
  @dataclass
437
479
  class AssistanceListing:
438
480
  """Schema definition for Assistance Listing (not used for instances)"""
@@ -575,7 +617,7 @@ class ShapeConfig:
575
617
  "business_types,primary_naics,naics_codes,psc_codes,"
576
618
  "email_address,entity_url,description,capabilities,keywords,"
577
619
  "physical_address,mailing_address,"
578
- "federal_obligations,congressional_district"
620
+ "federal_obligations(*),congressional_district"
579
621
  )
580
622
 
581
623
  # Default for list_forecasts()
@@ -589,6 +631,9 @@ class ShapeConfig:
589
631
  # Default for list_notices()
590
632
  NOTICES_MINIMAL: Final = "notice_id,title,solicitation_number,posted_date"
591
633
 
634
+ # Default for list_protests()
635
+ PROTESTS_MINIMAL: Final = "case_id,case_number,title,source_system,outcome,filed_date"
636
+
592
637
  # Default for list_grants()
593
638
  GRANTS_MINIMAL: Final = "grant_id,opportunity_number,title,status(*),agency_code"
594
639
 
@@ -637,6 +637,57 @@ NOTICE_SCHEMA: dict[str, FieldSchema] = {
637
637
  }
638
638
 
639
639
 
640
+ # Docket-level fields for Protest dockets expansion: dockets(docket_number, filed_date, ...)
641
+ PROTEST_DOCKET_SCHEMA: dict[str, FieldSchema] = {
642
+ "source_system": FieldSchema(name="source_system", type=str, is_optional=True, is_list=False),
643
+ "case_number": FieldSchema(name="case_number", type=str, is_optional=True, is_list=False),
644
+ "docket_number": FieldSchema(name="docket_number", type=str, is_optional=True, is_list=False),
645
+ "title": FieldSchema(name="title", type=str, is_optional=True, is_list=False),
646
+ "protester": FieldSchema(name="protester", type=str, is_optional=True, is_list=False),
647
+ "agency": FieldSchema(name="agency", type=str, is_optional=True, is_list=False),
648
+ "solicitation_number": FieldSchema(
649
+ name="solicitation_number", type=str, is_optional=True, is_list=False
650
+ ),
651
+ "case_type": FieldSchema(name="case_type", type=str, is_optional=True, is_list=False),
652
+ "outcome": FieldSchema(name="outcome", type=str, is_optional=True, is_list=False),
653
+ "filed_date": FieldSchema(name="filed_date", type=datetime, is_optional=True, is_list=False),
654
+ "posted_date": FieldSchema(name="posted_date", type=datetime, is_optional=True, is_list=False),
655
+ "decision_date": FieldSchema(
656
+ name="decision_date", type=datetime, is_optional=True, is_list=False
657
+ ),
658
+ "due_date": FieldSchema(name="due_date", type=datetime, is_optional=True, is_list=False),
659
+ "docket_url": FieldSchema(name="docket_url", type=str, is_optional=True, is_list=False),
660
+ "decision_url": FieldSchema(name="decision_url", type=str, is_optional=True, is_list=False),
661
+ "digest": FieldSchema(name="digest", type=str, is_optional=True, is_list=False),
662
+ }
663
+
664
+ PROTEST_SCHEMA: dict[str, FieldSchema] = {
665
+ "case_id": FieldSchema(name="case_id", type=str, is_optional=False, is_list=False),
666
+ "case_number": FieldSchema(name="case_number", type=str, is_optional=True, is_list=False),
667
+ "title": FieldSchema(name="title", type=str, is_optional=True, is_list=False),
668
+ "source_system": FieldSchema(name="source_system", type=str, is_optional=True, is_list=False),
669
+ "outcome": FieldSchema(name="outcome", type=str, is_optional=True, is_list=False),
670
+ "agency": FieldSchema(name="agency", type=str, is_optional=True, is_list=False),
671
+ "protester": FieldSchema(name="protester", type=str, is_optional=True, is_list=False),
672
+ "solicitation_number": FieldSchema(
673
+ name="solicitation_number", type=str, is_optional=True, is_list=False
674
+ ),
675
+ "case_type": FieldSchema(name="case_type", type=str, is_optional=True, is_list=False),
676
+ "filed_date": FieldSchema(name="filed_date", type=datetime, is_optional=True, is_list=False),
677
+ "posted_date": FieldSchema(name="posted_date", type=datetime, is_optional=True, is_list=False),
678
+ "decision_date": FieldSchema(
679
+ name="decision_date", type=datetime, is_optional=True, is_list=False
680
+ ),
681
+ "due_date": FieldSchema(name="due_date", type=datetime, is_optional=True, is_list=False),
682
+ "docket_url": FieldSchema(name="docket_url", type=str, is_optional=True, is_list=False),
683
+ "decision_url": FieldSchema(name="decision_url", type=str, is_optional=True, is_list=False),
684
+ "digest": FieldSchema(name="digest", type=str, is_optional=True, is_list=False),
685
+ "dockets": FieldSchema(
686
+ name="dockets", type=dict, is_optional=True, is_list=True, nested_model="ProtestDocket"
687
+ ),
688
+ }
689
+
690
+
640
691
  AGENCY_SCHEMA: dict[str, FieldSchema] = {
641
692
  "abbreviation": FieldSchema(name="abbreviation", type=str, is_optional=True, is_list=False),
642
693
  "code": FieldSchema(name="code", type=str, is_optional=False, is_list=False),
@@ -1105,6 +1156,8 @@ EXPLICIT_SCHEMAS: dict[str, dict[str, FieldSchema]] = {
1105
1156
  "Forecast": FORECAST_SCHEMA,
1106
1157
  "Opportunity": OPPORTUNITY_SCHEMA,
1107
1158
  "Notice": NOTICE_SCHEMA,
1159
+ "Protest": PROTEST_SCHEMA,
1160
+ "ProtestDocket": PROTEST_DOCKET_SCHEMA,
1108
1161
  "Agency": AGENCY_SCHEMA,
1109
1162
  "Grant": GRANT_SCHEMA,
1110
1163
  # Vehicles (Awards)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tango-python
3
- Version: 0.4.1
3
+ Version: 0.4.3
4
4
  Summary: Python SDK for the Tango API
5
5
  Project-URL: Homepage, https://github.com/makegov/tango-python
6
6
  Project-URL: Documentation, https://docs.makegov.com/tango-python
@@ -61,7 +61,7 @@ A modern Python SDK for the [Tango API](https://tango.makegov.com) by MakeGov, f
61
61
 
62
62
  - **Dynamic Response Shaping** - Request only the fields you need, reducing payload sizes by 60-80%
63
63
  - **Full Type Safety** - Runtime-generated TypedDict types with accurate type hints for IDE autocomplete
64
- - **Comprehensive API Coverage** - All major Tango API endpoints (contracts, entities, forecasts, opportunities, notices, grants, webhooks) [Note: the current version does NOT implement all endpoints, we will be adding them incrementally]
64
+ - **Comprehensive API Coverage** - All major Tango API endpoints (contracts, IDVs, OTAs, entities, forecasts, opportunities, notices, grants, protests, webhooks, and more)
65
65
  - **Flexible Data Access** - Dictionary-based response objects with validation
66
66
  - **Modern Python** - Built for Python 3.12+ using modern async-ready patterns
67
67
  - **Production-Ready** - Comprehensive test suite with VCR.py-based integration tests
@@ -207,14 +207,33 @@ contracts = client.list_contracts(
207
207
  **Response Options:**
208
208
  - `shape`, `flat`, `flat_lists` - Response shaping options
209
209
 
210
+ ### IDVs, OTAs, OTIDVs
211
+
212
+ ```python
213
+ # List IDVs (keyset pagination)
214
+ idvs = client.list_idvs(limit=25, awarding_agency="4700")
215
+
216
+ # Get single IDV with shaping
217
+ idv = client.get_idv("IDV_KEY", shape=ShapeConfig.IDVS_COMPREHENSIVE)
218
+
219
+ # OTAs and OTIDVs follow the same pattern
220
+ otas = client.list_otas(limit=25)
221
+ otidvs = client.list_otidvs(limit=25)
222
+ ```
223
+
224
+ ### Vehicles
225
+
226
+ ```python
227
+ vehicles = client.list_vehicles(search="GSA schedule", shape=ShapeConfig.VEHICLES_MINIMAL)
228
+ vehicle = client.get_vehicle("UUID", shape=ShapeConfig.VEHICLES_COMPREHENSIVE)
229
+ awardees = client.list_vehicle_awardees("UUID")
230
+ ```
231
+
210
232
  ### Entities (Vendors/Recipients)
211
233
 
212
234
  ```python
213
- # List entities
214
- entities = client.list_entities(
215
- page=1,
216
- limit=25
217
- )
235
+ # List entities with filters
236
+ entities = client.list_entities(search="Booz Allen", state="VA", limit=25)
218
237
 
219
238
  # Get specific entity by UEI or CAGE code
220
239
  entity = client.get_entity("ZQGGHJH74DW7")
@@ -223,47 +242,49 @@ entity = client.get_entity("ZQGGHJH74DW7")
223
242
  ### Forecasts
224
243
 
225
244
  ```python
226
- # List contract forecasts
227
- forecasts = client.list_forecasts(
228
- agency="GSA",
229
- limit=25
230
- )
245
+ forecasts = client.list_forecasts(agency="GSA", fiscal_year=2025, limit=25)
231
246
  ```
232
247
 
233
248
  ### Opportunities
234
249
 
235
250
  ```python
236
- # List opportunities/solicitations
237
- opportunities = client.list_opportunities(
238
- agency="DOD",
239
- limit=25
240
- )
251
+ opportunities = client.list_opportunities(agency="DOD", active=True, limit=25)
241
252
  ```
242
253
 
243
254
  ### Notices
244
255
 
245
256
  ```python
246
- # List contract notices
247
- notices = client.list_notices(
248
- agency="DOD",
249
- limit=25
250
- )
257
+ notices = client.list_notices(agency="DOD", notice_type="award", limit=25)
251
258
  ```
252
259
 
253
260
  ### Grants
254
261
 
255
262
  ```python
256
- # List grant opportunities
257
- grants = client.list_grants(
258
- agency_code="HHS",
259
- limit=25
260
- )
263
+ grants = client.list_grants(agency="HHS", status="forecasted", limit=25)
264
+ ```
265
+
266
+ ### Protests
267
+
268
+ ```python
269
+ protests = client.list_protests(source_system="gao", outcome="Sustained", limit=25)
270
+ protest = client.get_protest("CASE_UUID")
271
+ ```
272
+
273
+ ### GSA eLibrary Contracts
274
+
275
+ ```python
276
+ contracts = client.list_gsa_elibrary_contracts(schedule="MAS", limit=25)
277
+ contract = client.get_gsa_elibrary_contract("UUID")
261
278
  ```
262
279
 
263
- ### Business Types
280
+ ### Reference Data
264
281
 
265
282
  ```python
266
- # List business types
283
+ # Offices, organizations, NAICS, subawards, business types
284
+ offices = client.list_offices(search="acquisitions")
285
+ organizations = client.list_organizations(level=1)
286
+ naics = client.list_naics(search="software")
287
+ subawards = client.list_subawards(prime_uei="UEI123")
267
288
  business_types = client.list_business_types()
268
289
  ```
269
290
 
@@ -476,9 +497,16 @@ tango-python/
476
497
  │ ├── test_entities_integration.py
477
498
  │ ├── test_forecasts_integration.py
478
499
  │ ├── test_grants_integration.py
500
+ │ ├── test_naics_integration.py
479
501
  │ ├── test_notices_integration.py
502
+ │ ├── test_offices_integration.py
480
503
  │ ├── test_opportunities_integration.py
504
+ │ ├── test_organizations_integration.py
505
+ │ ├── test_otas_otidvs_integration.py
506
+ │ ├── test_protests_integration.py
481
507
  │ ├── test_reference_data_integration.py
508
+ │ ├── test_subawards_integration.py
509
+ │ ├── test_vehicles_idvs_integration.py
482
510
  │ └── test_edge_cases_integration.py
483
511
  ├── docs/ # Documentation
484
512
  │ ├── API_REFERENCE.md # Complete API reference
@@ -0,0 +1,16 @@
1
+ tango/__init__.py,sha256=MkZ4VcgIB5WFfCaxK6XXrK25icagQ2o9ZNta64xsqgw,1142
2
+ tango/client.py,sha256=kzUVPPOur45QobKBrRzPXGOOYZHXg-cBEWlSIB8vvAo,87725
3
+ tango/exceptions.py,sha256=aRvDm0dUCEtNDfRVYCX7SEDdd1WlIVVY6sN78Tzo-a0,3114
4
+ tango/models.py,sha256=EDKsZ4fsxkAbDhX5roOfiKYyPjZaTV-5QKrPaQCsb0Y,20959
5
+ tango/shapes/__init__.py,sha256=7ea1WU74jp4znhNw-gXruag6m6eyPZtbVgbDFmFUWro,1072
6
+ tango/shapes/explicit_schemas.py,sha256=H4pYs0LCTSV5msRCxftmgiM_-3sc4LsqpDPgj36DkPY,55202
7
+ tango/shapes/factory.py,sha256=ytpMi5Uw72XZ8MimhuSsLDVXF3zO_Zt3_tAL6NF7LnU,34318
8
+ tango/shapes/generator.py,sha256=61V1T3lm8Ps_KSMJAezQJLQVFbNKt1jtoLyhiqNtFTs,23380
9
+ tango/shapes/models.py,sha256=h3pIhOqrrdlN953Y6r0oney5HFbKPOD-frRndRWimJ0,3018
10
+ tango/shapes/parser.py,sha256=k6OsI2w3GH6-IBbc-XTLgL1mWH7bMf7A_dA6pr1xKfw,24619
11
+ tango/shapes/schema.py,sha256=VRPOB1sBdjFyimNchrZKIpTHn83CyX4RfU9077aQtIU,14136
12
+ tango/shapes/types.py,sha256=27jrAE0VIdrKaLjR_FK71hfIIGX2Tg3ex7REEBV1TFE,1301
13
+ tango_python-0.4.3.dist-info/METADATA,sha256=OhV1f_Hifu1hUd2c0zfGc91rIIe07ddJ3Nw3MDnqFl8,17595
14
+ tango_python-0.4.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
15
+ tango_python-0.4.3.dist-info/licenses/LICENSE,sha256=j2kYVHMwTkoGn3ZNScnrdIueG0k2XzB_LCPFoyBc2wk,1064
16
+ tango_python-0.4.3.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- tango/__init__.py,sha256=7EdEUgCcK0lDNJWWDgdSWxx8xxryO-fHL-iml9PaYLw,1102
2
- tango/client.py,sha256=ykeTSh4rI4P3LZFALLfGkl2Yr6jM7NWrq_E4V5vY0nc,84112
3
- tango/exceptions.py,sha256=JmtbOY0ofBnX24pUErh2XFlTj9dim2ngyboserEGRFw,2226
4
- tango/models.py,sha256=E5jbcQ1CHevTJBD1KOREAqql9bTkft91WaNsL8qbnuA,19575
5
- tango/shapes/__init__.py,sha256=7ea1WU74jp4znhNw-gXruag6m6eyPZtbVgbDFmFUWro,1072
6
- tango/shapes/explicit_schemas.py,sha256=iKvmpmeFU8pr4PpTenlpXDKxNbLkuyfUdDc8IV7p4zk,51734
7
- tango/shapes/factory.py,sha256=ytpMi5Uw72XZ8MimhuSsLDVXF3zO_Zt3_tAL6NF7LnU,34318
8
- tango/shapes/generator.py,sha256=61V1T3lm8Ps_KSMJAezQJLQVFbNKt1jtoLyhiqNtFTs,23380
9
- tango/shapes/models.py,sha256=h3pIhOqrrdlN953Y6r0oney5HFbKPOD-frRndRWimJ0,3018
10
- tango/shapes/parser.py,sha256=k6OsI2w3GH6-IBbc-XTLgL1mWH7bMf7A_dA6pr1xKfw,24619
11
- tango/shapes/schema.py,sha256=VRPOB1sBdjFyimNchrZKIpTHn83CyX4RfU9077aQtIU,14136
12
- tango/shapes/types.py,sha256=27jrAE0VIdrKaLjR_FK71hfIIGX2Tg3ex7REEBV1TFE,1301
13
- tango_python-0.4.1.dist-info/METADATA,sha256=QTa_gFDGAc9jj23jv35_0Zy9Js3bXlzK6FV-9OnjLFg,16210
14
- tango_python-0.4.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
15
- tango_python-0.4.1.dist-info/licenses/LICENSE,sha256=j2kYVHMwTkoGn3ZNScnrdIueG0k2XzB_LCPFoyBc2wk,1064
16
- tango_python-0.4.1.dist-info/RECORD,,