tango-python 1.8.0__py3-none-any.whl → 1.10.0__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 +9 -1
- tango/client.py +262 -45
- tango/exceptions.py +10 -0
- tango/models.py +112 -0
- tango/shapes/explicit_schemas.py +93 -0
- tango/shapes/generated_overlay.py +8 -19
- {tango_python-1.8.0.dist-info → tango_python-1.10.0.dist-info}/METADATA +10 -1
- {tango_python-1.8.0.dist-info → tango_python-1.10.0.dist-info}/RECORD +11 -11
- {tango_python-1.8.0.dist-info → tango_python-1.10.0.dist-info}/WHEEL +0 -0
- {tango_python-1.8.0.dist-info → tango_python-1.10.0.dist-info}/entry_points.txt +0 -0
- {tango_python-1.8.0.dist-info → tango_python-1.10.0.dist-info}/licenses/LICENSE +0 -0
tango/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from .client import TangoClient
|
|
4
4
|
from .exceptions import (
|
|
5
5
|
TangoAPIError,
|
|
6
|
+
TangoAttachmentLinkError,
|
|
6
7
|
TangoAuthError,
|
|
7
8
|
TangoNotFoundError,
|
|
8
9
|
TangoRateLimitError,
|
|
@@ -14,6 +15,9 @@ from .models import (
|
|
|
14
15
|
DibbsAward,
|
|
15
16
|
DibbsRfp,
|
|
16
17
|
DibbsRfq,
|
|
18
|
+
EbuyAccess,
|
|
19
|
+
EbuyAttachment,
|
|
20
|
+
EbuyRequest,
|
|
17
21
|
Exclusion,
|
|
18
22
|
FederalRegisterDocument,
|
|
19
23
|
GsaElibraryContract,
|
|
@@ -55,13 +59,14 @@ from .webhooks import (
|
|
|
55
59
|
)
|
|
56
60
|
from .webhooks.receiver import Delivery, WebhookReceiver
|
|
57
61
|
|
|
58
|
-
__version__ = "1.
|
|
62
|
+
__version__ = "1.10.0"
|
|
59
63
|
__all__ = [
|
|
60
64
|
"TangoClient",
|
|
61
65
|
"TangoAPIError",
|
|
62
66
|
"TangoAuthError",
|
|
63
67
|
"TangoNotFoundError",
|
|
64
68
|
"TangoValidationError",
|
|
69
|
+
"TangoAttachmentLinkError",
|
|
65
70
|
"TangoRateLimitError",
|
|
66
71
|
"RateLimitInfo",
|
|
67
72
|
"ResolveCandidate",
|
|
@@ -69,6 +74,9 @@ __all__ = [
|
|
|
69
74
|
"BudgetAccount",
|
|
70
75
|
"ContractAppeal",
|
|
71
76
|
"FederalRegisterDocument",
|
|
77
|
+
"EbuyAccess",
|
|
78
|
+
"EbuyAttachment",
|
|
79
|
+
"EbuyRequest",
|
|
72
80
|
"DibbsAward",
|
|
73
81
|
"DibbsRfp",
|
|
74
82
|
"DibbsRfq",
|
tango/client.py
CHANGED
|
@@ -5,12 +5,13 @@ import warnings
|
|
|
5
5
|
from datetime import date, datetime
|
|
6
6
|
from decimal import Decimal
|
|
7
7
|
from typing import Any, Literal, cast
|
|
8
|
-
from urllib.parse import urljoin
|
|
8
|
+
from urllib.parse import quote, urljoin
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
11
11
|
|
|
12
12
|
from tango.exceptions import (
|
|
13
13
|
TangoAPIError,
|
|
14
|
+
TangoAttachmentLinkError,
|
|
14
15
|
TangoAuthError,
|
|
15
16
|
TangoNotFoundError,
|
|
16
17
|
TangoRateLimitError,
|
|
@@ -28,6 +29,8 @@ from tango.models import (
|
|
|
28
29
|
DibbsAward,
|
|
29
30
|
DibbsRfp,
|
|
30
31
|
DibbsRfq,
|
|
32
|
+
EbuyAccess,
|
|
33
|
+
EbuyRequest,
|
|
31
34
|
Entity,
|
|
32
35
|
Exclusion,
|
|
33
36
|
FederalRegisterDocument,
|
|
@@ -175,55 +178,57 @@ class TangoClient:
|
|
|
175
178
|
self._last_response_headers = response.headers
|
|
176
179
|
self._last_rate_limit_info = self._parse_rate_limit_headers(response.headers)
|
|
177
180
|
|
|
178
|
-
|
|
179
|
-
raise TangoAuthError(
|
|
180
|
-
"Invalid API key or authentication required", response.status_code
|
|
181
|
-
)
|
|
182
|
-
elif response.status_code == 404:
|
|
183
|
-
raise TangoNotFoundError("Resource not found", response.status_code)
|
|
184
|
-
elif response.status_code == 400:
|
|
185
|
-
error_data = response.json() if response.content else {}
|
|
186
|
-
error_msg = "Invalid request parameters"
|
|
187
|
-
if error_data:
|
|
188
|
-
# Try to extract a more specific error message
|
|
189
|
-
if isinstance(error_data, dict):
|
|
190
|
-
detail = (
|
|
191
|
-
error_data.get("detail")
|
|
192
|
-
or error_data.get("message")
|
|
193
|
-
or error_data.get("error")
|
|
194
|
-
)
|
|
195
|
-
if detail:
|
|
196
|
-
error_msg = f"Invalid request parameters: {detail}"
|
|
197
|
-
issues = error_data.get("issues")
|
|
198
|
-
if isinstance(issues, list):
|
|
199
|
-
rejected = [
|
|
200
|
-
f"{issue['path']} ({issue['reason']})"
|
|
201
|
-
if issue.get("reason")
|
|
202
|
-
else str(issue["path"])
|
|
203
|
-
for issue in issues
|
|
204
|
-
if isinstance(issue, dict) and issue.get("path")
|
|
205
|
-
]
|
|
206
|
-
if rejected:
|
|
207
|
-
error_msg = f"{error_msg}: {', '.join(rejected)}"
|
|
208
|
-
raise TangoValidationError(
|
|
209
|
-
error_msg,
|
|
210
|
-
response.status_code,
|
|
211
|
-
error_data,
|
|
212
|
-
)
|
|
213
|
-
elif response.status_code == 429:
|
|
214
|
-
error_data = response.json() if response.content else {}
|
|
215
|
-
detail = error_data.get("detail", "Rate limit exceeded")
|
|
216
|
-
raise TangoRateLimitError(detail, response.status_code, error_data)
|
|
217
|
-
elif not response.is_success:
|
|
218
|
-
raise TangoAPIError(
|
|
219
|
-
f"API request failed with status {response.status_code}", response.status_code
|
|
220
|
-
)
|
|
181
|
+
self._raise_for_status(response)
|
|
221
182
|
|
|
222
183
|
return response.json() if response.content else {}
|
|
223
184
|
|
|
224
185
|
except httpx.HTTPError as e:
|
|
225
186
|
raise TangoAPIError(f"Request failed: {str(e)}") from e
|
|
226
187
|
|
|
188
|
+
def _raise_for_status(self, response: httpx.Response) -> None:
|
|
189
|
+
"""Raise the SDK exception matching a non-success response; return quietly on success."""
|
|
190
|
+
if response.status_code == 401:
|
|
191
|
+
raise TangoAuthError("Invalid API key or authentication required", response.status_code)
|
|
192
|
+
elif response.status_code == 404:
|
|
193
|
+
raise TangoNotFoundError("Resource not found", response.status_code)
|
|
194
|
+
elif response.status_code == 400:
|
|
195
|
+
error_data = response.json() if response.content else {}
|
|
196
|
+
error_msg = "Invalid request parameters"
|
|
197
|
+
if error_data:
|
|
198
|
+
# Try to extract a more specific error message
|
|
199
|
+
if isinstance(error_data, dict):
|
|
200
|
+
detail = (
|
|
201
|
+
error_data.get("detail")
|
|
202
|
+
or error_data.get("message")
|
|
203
|
+
or error_data.get("error")
|
|
204
|
+
)
|
|
205
|
+
if detail:
|
|
206
|
+
error_msg = f"Invalid request parameters: {detail}"
|
|
207
|
+
issues = error_data.get("issues")
|
|
208
|
+
if isinstance(issues, list):
|
|
209
|
+
rejected = [
|
|
210
|
+
f"{issue['path']} ({issue['reason']})"
|
|
211
|
+
if issue.get("reason")
|
|
212
|
+
else str(issue["path"])
|
|
213
|
+
for issue in issues
|
|
214
|
+
if isinstance(issue, dict) and issue.get("path")
|
|
215
|
+
]
|
|
216
|
+
if rejected:
|
|
217
|
+
error_msg = f"{error_msg}: {', '.join(rejected)}"
|
|
218
|
+
raise TangoValidationError(
|
|
219
|
+
error_msg,
|
|
220
|
+
response.status_code,
|
|
221
|
+
error_data,
|
|
222
|
+
)
|
|
223
|
+
elif response.status_code == 429:
|
|
224
|
+
error_data = response.json() if response.content else {}
|
|
225
|
+
detail = error_data.get("detail", "Rate limit exceeded")
|
|
226
|
+
raise TangoRateLimitError(detail, response.status_code, error_data)
|
|
227
|
+
elif not response.is_success:
|
|
228
|
+
raise TangoAPIError(
|
|
229
|
+
f"API request failed with status {response.status_code}", response.status_code
|
|
230
|
+
)
|
|
231
|
+
|
|
227
232
|
def _get(self, endpoint: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
228
233
|
"""Make a GET request"""
|
|
229
234
|
return self._request("GET", endpoint, params=params)
|
|
@@ -2963,6 +2968,215 @@ class TangoClient:
|
|
|
2963
2968
|
data, shape, FederalRegisterDocument, flat, flat_lists
|
|
2964
2969
|
)
|
|
2965
2970
|
|
|
2971
|
+
# ============================================================================
|
|
2972
|
+
# GSA eBuy requests
|
|
2973
|
+
# ============================================================================
|
|
2974
|
+
|
|
2975
|
+
def list_ebuy_requests(
|
|
2976
|
+
self,
|
|
2977
|
+
page: int = 1,
|
|
2978
|
+
limit: int = 25,
|
|
2979
|
+
shape: str | None = None,
|
|
2980
|
+
flat: bool = False,
|
|
2981
|
+
flat_lists: bool = False,
|
|
2982
|
+
search: str | None = None,
|
|
2983
|
+
rfq_id: str | None = None,
|
|
2984
|
+
reference_number: str | None = None,
|
|
2985
|
+
request_type: str | None = None,
|
|
2986
|
+
status: str | None = None,
|
|
2987
|
+
sin: str | None = None,
|
|
2988
|
+
schedule: str | None = None,
|
|
2989
|
+
buyer_agency: str | None = None,
|
|
2990
|
+
agency: str | None = None,
|
|
2991
|
+
contract_number: str | None = None,
|
|
2992
|
+
issue_date_after: str | None = None,
|
|
2993
|
+
issue_date_before: str | None = None,
|
|
2994
|
+
close_date_after: str | None = None,
|
|
2995
|
+
close_date_before: str | None = None,
|
|
2996
|
+
ordering: str | None = None,
|
|
2997
|
+
) -> PaginatedResponse:
|
|
2998
|
+
"""
|
|
2999
|
+
List GSA eBuy requests (RFQs, RFPs and RFIs).
|
|
3000
|
+
|
|
3001
|
+
Requires the Pro tier or above; below it the API returns 403.
|
|
3002
|
+
Results are scoped to your account: you see only requests posted under the GSA schedule contracts linked to it.
|
|
3003
|
+
With no linked contract the page is empty rather than an error, so use ``get_ebuy_access()`` to tell "no access" from "no matches".
|
|
3004
|
+
|
|
3005
|
+
``status`` is frozen at the last state the request was seen in.
|
|
3006
|
+
Only currently-active requests are carried, so a request that closes stops appearing rather than getting a final row, and ``Open`` means "open the last time it was seen".
|
|
3007
|
+
Read ``last_seen`` for staleness.
|
|
3008
|
+
|
|
3009
|
+
Args:
|
|
3010
|
+
page: Page number
|
|
3011
|
+
limit: Results per page (max 100)
|
|
3012
|
+
shape: Response shape string (defaults to minimal shape)
|
|
3013
|
+
flat: If True, flatten nested objects in shaped response
|
|
3014
|
+
flat_lists: If True, flatten arrays using indexed keys
|
|
3015
|
+
search: Full-text search over the title, description, reference number, request id and attachment text. Results rank by relevance unless ``ordering`` is given
|
|
3016
|
+
rfq_id: Exact request id, e.g. ``RFQ1835158``. OR several with ``|``
|
|
3017
|
+
reference_number: The buyer's own solicitation number; dashes are ignored
|
|
3018
|
+
request_type: ``RFQ``, ``RFP`` or ``RFI``. OR several with ``|``
|
|
3019
|
+
status: ``Open`` or ``Cancelled``, as last seen. OR several with ``|``
|
|
3020
|
+
sin: Special Item Number, e.g. ``54151S``. OR several with ``|``
|
|
3021
|
+
schedule: GSA schedule. OR several with ``|``
|
|
3022
|
+
buyer_agency: The buyer agency as eBuy names it (free text). OR several with ``|``
|
|
3023
|
+
agency: A Tango agency name, abbreviation, code or organization key, e.g. ``GSA``. Matches the whole organization subtree, so a department includes its sub-agencies. OR several with ``|``. Requires Tango API 5.3.0
|
|
3024
|
+
contract_number: Narrow to requests posted under one of your own linked contracts. A contract not linked to your account returns an empty page, not an error
|
|
3025
|
+
issue_date_after: Issued on or after (YYYY-MM-DD)
|
|
3026
|
+
issue_date_before: Issued on or before (YYYY-MM-DD)
|
|
3027
|
+
close_date_after: Closes on or after (YYYY-MM-DD)
|
|
3028
|
+
close_date_before: Closes on or before (YYYY-MM-DD)
|
|
3029
|
+
ordering: Sort field — ``issue_date`` (the default, as ``-issue_date``), ``close_date``, ``last_seen`` or ``modified``. Prefix ``-`` for descending
|
|
3030
|
+
"""
|
|
3031
|
+
params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
|
|
3032
|
+
|
|
3033
|
+
if shape is None:
|
|
3034
|
+
shape = ShapeConfig.EBUY_REQUESTS_MINIMAL
|
|
3035
|
+
if shape:
|
|
3036
|
+
params["shape"] = shape
|
|
3037
|
+
if flat:
|
|
3038
|
+
params["flat"] = "true"
|
|
3039
|
+
if flat_lists:
|
|
3040
|
+
params["flat_lists"] = "true"
|
|
3041
|
+
|
|
3042
|
+
for key, val in (
|
|
3043
|
+
("search", search),
|
|
3044
|
+
("rfq_id", rfq_id),
|
|
3045
|
+
("reference_number", reference_number),
|
|
3046
|
+
("request_type", request_type),
|
|
3047
|
+
("status", status),
|
|
3048
|
+
("sin", sin),
|
|
3049
|
+
("schedule", schedule),
|
|
3050
|
+
("buyer_agency", buyer_agency),
|
|
3051
|
+
("agency", agency),
|
|
3052
|
+
("contract_number", contract_number),
|
|
3053
|
+
("issue_date_after", issue_date_after),
|
|
3054
|
+
("issue_date_before", issue_date_before),
|
|
3055
|
+
("close_date_after", close_date_after),
|
|
3056
|
+
("close_date_before", close_date_before),
|
|
3057
|
+
("ordering", ordering),
|
|
3058
|
+
):
|
|
3059
|
+
if val is not None:
|
|
3060
|
+
params[key] = val
|
|
3061
|
+
|
|
3062
|
+
data = self._get("/api/ebuy/requests/", params)
|
|
3063
|
+
|
|
3064
|
+
results = [
|
|
3065
|
+
self._parse_response_with_shape(item, shape, EbuyRequest, flat, flat_lists)
|
|
3066
|
+
for item in data["results"]
|
|
3067
|
+
]
|
|
3068
|
+
|
|
3069
|
+
return PaginatedResponse(
|
|
3070
|
+
count=data["count"],
|
|
3071
|
+
next=data.get("next"),
|
|
3072
|
+
previous=data.get("previous"),
|
|
3073
|
+
results=results,
|
|
3074
|
+
meta=data.get("meta"),
|
|
3075
|
+
)
|
|
3076
|
+
|
|
3077
|
+
def get_ebuy_request(
|
|
3078
|
+
self,
|
|
3079
|
+
rfq_id: str,
|
|
3080
|
+
shape: str | None = None,
|
|
3081
|
+
flat: bool = False,
|
|
3082
|
+
flat_lists: bool = False,
|
|
3083
|
+
) -> Any:
|
|
3084
|
+
"""
|
|
3085
|
+
Get a single GSA eBuy request by its request id.
|
|
3086
|
+
|
|
3087
|
+
A request outside your account's scope raises ``TangoNotFoundError``, the same as an id that does not exist.
|
|
3088
|
+
|
|
3089
|
+
Args:
|
|
3090
|
+
rfq_id: Request id, e.g. ``RFQ1835158``
|
|
3091
|
+
shape: Response shape string (defaults to the comprehensive shape: every field plus the ``organization`` and ``attachments`` expands)
|
|
3092
|
+
flat: If True, flatten nested objects in shaped response
|
|
3093
|
+
flat_lists: If True, flatten arrays using indexed keys
|
|
3094
|
+
"""
|
|
3095
|
+
params: dict[str, Any] = {}
|
|
3096
|
+
if shape is None:
|
|
3097
|
+
shape = ShapeConfig.EBUY_REQUESTS_COMPREHENSIVE
|
|
3098
|
+
if shape:
|
|
3099
|
+
params["shape"] = shape
|
|
3100
|
+
if flat:
|
|
3101
|
+
params["flat"] = "true"
|
|
3102
|
+
if flat_lists:
|
|
3103
|
+
params["flat_lists"] = "true"
|
|
3104
|
+
|
|
3105
|
+
data = self._get(f"/api/ebuy/requests/{quote(rfq_id, safe='')}/", params)
|
|
3106
|
+
return self._parse_response_with_shape(data, shape, EbuyRequest, flat, flat_lists)
|
|
3107
|
+
|
|
3108
|
+
def get_ebuy_attachment_url(self, rfq_id: str, doc_seq_num: int) -> str:
|
|
3109
|
+
"""
|
|
3110
|
+
Get a short-lived download URL for one attachment on a GSA eBuy request.
|
|
3111
|
+
|
|
3112
|
+
The API answers with a redirect to a signed URL that expires after about five minutes; this returns that URL without downloading the document.
|
|
3113
|
+
Fetch it promptly, and call this again rather than storing it.
|
|
3114
|
+
|
|
3115
|
+
Args:
|
|
3116
|
+
rfq_id: Request id, e.g. ``RFQ1835158``
|
|
3117
|
+
doc_seq_num: The attachment's ``doc_seq_num`` from ``get_ebuy_request()``
|
|
3118
|
+
|
|
3119
|
+
Raises:
|
|
3120
|
+
TangoAttachmentLinkError: The entry is an external link (``is_link``), not a stored document. The link is on ``.url`` and in the message. Subclasses ``TangoValidationError``.
|
|
3121
|
+
TangoNotFoundError: The request is unknown or outside your scope, the attachment does not exist, or its document has not been captured yet.
|
|
3122
|
+
"""
|
|
3123
|
+
endpoint = (
|
|
3124
|
+
f"/api/ebuy/requests/{quote(rfq_id, safe='')}/attachments/{int(doc_seq_num)}/download/"
|
|
3125
|
+
)
|
|
3126
|
+
url = urljoin(f"{self.base_url}/", endpoint.lstrip("/"))
|
|
3127
|
+
|
|
3128
|
+
try:
|
|
3129
|
+
response = self.client.request(method="GET", url=url, follow_redirects=False)
|
|
3130
|
+
except httpx.HTTPError as e:
|
|
3131
|
+
raise TangoAPIError(f"Request failed: {str(e)}") from e
|
|
3132
|
+
self._last_response_headers = response.headers
|
|
3133
|
+
self._last_rate_limit_info = self._parse_rate_limit_headers(response.headers)
|
|
3134
|
+
|
|
3135
|
+
location = response.headers.get("Location")
|
|
3136
|
+
if response.is_redirect and location:
|
|
3137
|
+
return str(location)
|
|
3138
|
+
|
|
3139
|
+
if response.status_code in (400, 404):
|
|
3140
|
+
try:
|
|
3141
|
+
error_data = response.json() if response.content else {}
|
|
3142
|
+
except ValueError:
|
|
3143
|
+
error_data = {}
|
|
3144
|
+
if not isinstance(error_data, dict):
|
|
3145
|
+
error_data = {}
|
|
3146
|
+
if response.status_code == 400 and error_data.get("url"):
|
|
3147
|
+
raise TangoAttachmentLinkError(
|
|
3148
|
+
f"Attachment {doc_seq_num} on {rfq_id} is an external link, not a stored document: {error_data['url']}",
|
|
3149
|
+
response.status_code,
|
|
3150
|
+
error_data,
|
|
3151
|
+
)
|
|
3152
|
+
if response.status_code == 404:
|
|
3153
|
+
raise TangoNotFoundError(
|
|
3154
|
+
error_data.get("detail") or "Resource not found",
|
|
3155
|
+
response.status_code,
|
|
3156
|
+
error_data,
|
|
3157
|
+
)
|
|
3158
|
+
|
|
3159
|
+
self._raise_for_status(response)
|
|
3160
|
+
raise TangoAPIError(
|
|
3161
|
+
f"Expected a redirect to the attachment, got status {response.status_code}",
|
|
3162
|
+
response.status_code,
|
|
3163
|
+
)
|
|
3164
|
+
|
|
3165
|
+
def get_ebuy_access(self) -> EbuyAccess:
|
|
3166
|
+
"""
|
|
3167
|
+
Check whether your account can read GSA eBuy requests.
|
|
3168
|
+
|
|
3169
|
+
``list_ebuy_requests()`` returns an empty page, not an error, when no contract is linked; this tells the two apart.
|
|
3170
|
+
``reason`` is ``"tier_required"`` below the Pro tier (it wins when both apply), ``"no_contract_grant"`` when no contract is linked, and ``None`` when ``enabled`` is true.
|
|
3171
|
+
``contracts`` lists your own linked contracts, sorted.
|
|
3172
|
+
"""
|
|
3173
|
+
data = self._get("/api/ebuy/access/")
|
|
3174
|
+
return EbuyAccess(
|
|
3175
|
+
enabled=bool(data.get("enabled")),
|
|
3176
|
+
reason=data.get("reason"),
|
|
3177
|
+
contracts=list(data.get("contracts") or []),
|
|
3178
|
+
)
|
|
3179
|
+
|
|
2966
3180
|
# ============================================================================
|
|
2967
3181
|
# DLA DIBBS (RFQs, RFPs, awards)
|
|
2968
3182
|
# ============================================================================
|
|
@@ -3519,6 +3733,7 @@ class TangoClient:
|
|
|
3519
3733
|
flat_lists: bool = False,
|
|
3520
3734
|
topic_number: str | None = None,
|
|
3521
3735
|
solicitation_number: str | None = None,
|
|
3736
|
+
cycle_name: str | None = None,
|
|
3522
3737
|
agency: str | None = None,
|
|
3523
3738
|
activity: str | None = None,
|
|
3524
3739
|
year: int | None = None,
|
|
@@ -3545,6 +3760,7 @@ class TangoClient:
|
|
|
3545
3760
|
flat_lists: If True, flatten arrays using indexed keys
|
|
3546
3761
|
topic_number: Filter by topic number
|
|
3547
3762
|
solicitation_number: Filter by solicitation number
|
|
3763
|
+
cycle_name: Filter by DSIP solicitation cycle, exact (e.g. "DOD_SBIR_2026_P1_CBZ"; requires Tango API 5.3.1)
|
|
3548
3764
|
agency: Filter by agency
|
|
3549
3765
|
activity: Filter by activity (component/branch)
|
|
3550
3766
|
year: Filter by solicitation year
|
|
@@ -3572,6 +3788,7 @@ class TangoClient:
|
|
|
3572
3788
|
for key, val in (
|
|
3573
3789
|
("topic_number", topic_number),
|
|
3574
3790
|
("solicitation_number", solicitation_number),
|
|
3791
|
+
("cycle_name", cycle_name),
|
|
3575
3792
|
("agency", agency),
|
|
3576
3793
|
("activity", activity),
|
|
3577
3794
|
("year", year),
|
|
@@ -5022,7 +5239,7 @@ class TangoClient:
|
|
|
5022
5239
|
query_type: One of ``opportunity``, ``contract``, ``idv``, ``ota``,
|
|
5023
5240
|
``otidv``, ``entity``, ``grant``, ``forecast``, ``exclusion``,
|
|
5024
5241
|
``dibbs_rfq``, ``dibbs_rfp``, ``sled_opportunity``,
|
|
5025
|
-
``contract_appeal`` or ``
|
|
5242
|
+
``contract_appeal``, ``federal_register`` or ``ebuy_request``. :meth:`list_webhook_event_types` is the
|
|
5026
5243
|
current authority — this list can only go stale.
|
|
5027
5244
|
filters: Dict of query parameters that the alert matches against
|
|
5028
5245
|
(e.g. ``{"naics": "541330", "set_aside": "SBA"}``).
|
tango/exceptions.py
CHANGED
|
@@ -53,6 +53,16 @@ class TangoValidationError(TangoAPIError):
|
|
|
53
53
|
return val if isinstance(val, dict) else None
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
class TangoAttachmentLinkError(TangoValidationError):
|
|
57
|
+
"""The requested attachment is an external link, not a stored document, so there is nothing to download."""
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def url(self) -> str | None:
|
|
61
|
+
"""The external link the entry points at."""
|
|
62
|
+
val = self.response_data.get("url")
|
|
63
|
+
return val if isinstance(val, str) else None
|
|
64
|
+
|
|
65
|
+
|
|
56
66
|
class TangoRateLimitError(TangoAPIError):
|
|
57
67
|
"""Rate limit exceeded error"""
|
|
58
68
|
|
tango/models.py
CHANGED
|
@@ -1000,6 +1000,7 @@ class SbirTopic:
|
|
|
1000
1000
|
activity: str | None = None
|
|
1001
1001
|
year: int | None = None
|
|
1002
1002
|
solicitation_number: str | None = None
|
|
1003
|
+
cycle_name: str | None = None
|
|
1003
1004
|
solicitation_status: str | None = None
|
|
1004
1005
|
release_date: str | None = None
|
|
1005
1006
|
open_date: str | None = None
|
|
@@ -1272,6 +1273,97 @@ class FederalRegisterDocument:
|
|
|
1272
1273
|
full_text: str | None = None
|
|
1273
1274
|
|
|
1274
1275
|
|
|
1276
|
+
class EbuyAttachment:
|
|
1277
|
+
"""Schema definition for one entry in an eBuy request's ``attachments`` (not used for instances).
|
|
1278
|
+
|
|
1279
|
+
``is_link=True`` means the entry is an outbound URL in ``doc_path`` with no stored document behind it, so ``get_ebuy_attachment_url()`` raises ``TangoAttachmentLinkError`` for it.
|
|
1280
|
+
"""
|
|
1281
|
+
|
|
1282
|
+
doc_seq_num: int | None = None
|
|
1283
|
+
doc_name: str | None = None
|
|
1284
|
+
doc_type: int | None = None
|
|
1285
|
+
doc_path: str | None = None
|
|
1286
|
+
is_link: bool | None = None
|
|
1287
|
+
doc_session_date: datetime | None = None
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
class EbuyRequest:
|
|
1291
|
+
"""Schema definition for EbuyRequest (not used for instances).
|
|
1292
|
+
|
|
1293
|
+
GSA eBuy requests (RFQs, RFPs, RFIs) at ``/api/ebuy/requests/``, keyed by ``rfq_id``.
|
|
1294
|
+
Shape-on-demand, so every field is optional.
|
|
1295
|
+
|
|
1296
|
+
The resource is scoped to your account: you see only requests posted under the GSA schedule contracts linked to it.
|
|
1297
|
+
|
|
1298
|
+
``status`` is frozen at the last state the request was seen in.
|
|
1299
|
+
Only currently-active requests are carried, so a request that closes stops appearing rather than getting a final row, and ``Open`` means "open the last time it was seen".
|
|
1300
|
+
Read ``last_seen`` for staleness.
|
|
1301
|
+
|
|
1302
|
+
``buyer_agency_code`` and some other buyer and contact fields are sparse on older requests.
|
|
1303
|
+
The contract number a request was posted under is never returned.
|
|
1304
|
+
"""
|
|
1305
|
+
|
|
1306
|
+
rfq_id: str | None = None
|
|
1307
|
+
request_type: str | None = None
|
|
1308
|
+
title: str | None = None
|
|
1309
|
+
description: str | None = None
|
|
1310
|
+
schedule: str | None = None
|
|
1311
|
+
sin: str | None = None
|
|
1312
|
+
status: str | None = None
|
|
1313
|
+
buyer_name: str | None = None
|
|
1314
|
+
buyer_agency: str | None = None
|
|
1315
|
+
buyer_agency_code: str | None = None
|
|
1316
|
+
buyer_email: str | None = None
|
|
1317
|
+
buyer_user_id: str | None = None
|
|
1318
|
+
reference_number: str | None = None
|
|
1319
|
+
award_method: str | None = None
|
|
1320
|
+
contract_type: str | None = None
|
|
1321
|
+
commercial_type: str | None = None
|
|
1322
|
+
follow_on: bool | None = None
|
|
1323
|
+
source_sought: bool | None = None
|
|
1324
|
+
issue_date: datetime | None = None
|
|
1325
|
+
close_date: datetime | None = None
|
|
1326
|
+
cancel_date: datetime | None = None
|
|
1327
|
+
last_mod_date: datetime | None = None
|
|
1328
|
+
pop_start_date: datetime | None = None
|
|
1329
|
+
pop_end_date: datetime | None = None
|
|
1330
|
+
oco_name: str | None = None
|
|
1331
|
+
oco_title: str | None = None
|
|
1332
|
+
oco_agency: str | None = None
|
|
1333
|
+
oco_phone: str | None = None
|
|
1334
|
+
oco_aac: str | None = None
|
|
1335
|
+
ocs_name: str | None = None
|
|
1336
|
+
ocs_title: str | None = None
|
|
1337
|
+
ocs_agency: str | None = None
|
|
1338
|
+
ocs_phone: str | None = None
|
|
1339
|
+
ocs_aac: str | None = None
|
|
1340
|
+
amendment_count: int | None = None
|
|
1341
|
+
mod_version: int | None = None
|
|
1342
|
+
qa_document_count: int | None = None
|
|
1343
|
+
attachment_count: int | None = None
|
|
1344
|
+
link_count: int | None = None
|
|
1345
|
+
amendments: list[dict[str, Any]] | None = None
|
|
1346
|
+
line_items: list[dict[str, Any]] | None = None
|
|
1347
|
+
addresses: list[dict[str, Any]] | None = None
|
|
1348
|
+
detail_fetched: bool | None = None
|
|
1349
|
+
first_seen: datetime | None = None
|
|
1350
|
+
last_seen: datetime | None = None
|
|
1351
|
+
organization: dict[str, Any] | None = None
|
|
1352
|
+
attachments: list[EbuyAttachment] | None = None
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
@dataclass
|
|
1356
|
+
class EbuyAccess:
|
|
1357
|
+
"""Result of ``GET /api/ebuy/access/``: whether your account can read eBuy requests.
|
|
1358
|
+
|
|
1359
|
+
``list_ebuy_requests()`` returns an empty page, not an error, when no contract is linked, so this is how to tell "no access" from "no matches".
|
|
1360
|
+
"""
|
|
1361
|
+
|
|
1362
|
+
enabled: bool
|
|
1363
|
+
reason: Literal["tier_required", "no_contract_grant"] | None
|
|
1364
|
+
contracts: list[str]
|
|
1365
|
+
|
|
1366
|
+
|
|
1275
1367
|
@dataclass
|
|
1276
1368
|
class PaginatedResponse[T]:
|
|
1277
1369
|
"""Paginated API response
|
|
@@ -1471,6 +1563,26 @@ class ShapeConfig:
|
|
|
1471
1563
|
"comment_url,regulations_dot_gov_url,raw_text_url,body_html_url"
|
|
1472
1564
|
)
|
|
1473
1565
|
|
|
1566
|
+
# Default for list_ebuy_requests(). Mirrors the API's own list default.
|
|
1567
|
+
EBUY_REQUESTS_MINIMAL: Final = (
|
|
1568
|
+
"rfq_id,request_type,title,schedule,sin,status,buyer_name,buyer_agency,"
|
|
1569
|
+
"buyer_agency_code,reference_number,issue_date,close_date,attachment_count,"
|
|
1570
|
+
"link_count,last_seen"
|
|
1571
|
+
)
|
|
1572
|
+
|
|
1573
|
+
# Default for get_ebuy_request(). Mirrors the API's own retrieve default; the attachment fields are named rather than `attachments(*)` so their dates parse.
|
|
1574
|
+
EBUY_REQUESTS_COMPREHENSIVE: Final = (
|
|
1575
|
+
"rfq_id,request_type,title,description,schedule,sin,status,buyer_name,"
|
|
1576
|
+
"buyer_agency,buyer_agency_code,buyer_email,buyer_user_id,reference_number,"
|
|
1577
|
+
"award_method,contract_type,commercial_type,follow_on,source_sought,issue_date,"
|
|
1578
|
+
"close_date,cancel_date,last_mod_date,pop_start_date,pop_end_date,oco_name,"
|
|
1579
|
+
"oco_title,oco_agency,oco_phone,oco_aac,ocs_name,ocs_title,ocs_agency,ocs_phone,"
|
|
1580
|
+
"ocs_aac,amendment_count,mod_version,qa_document_count,attachment_count,"
|
|
1581
|
+
"link_count,amendments,line_items,addresses,detail_fetched,first_seen,last_seen,"
|
|
1582
|
+
"organization(*),"
|
|
1583
|
+
"attachments(doc_seq_num,doc_name,doc_type,doc_path,is_link,doc_session_date)"
|
|
1584
|
+
)
|
|
1585
|
+
|
|
1474
1586
|
# Default for list_dibbs_rfqs()
|
|
1475
1587
|
DIBBS_RFQS_MINIMAL: Final = (
|
|
1476
1588
|
"uuid,solicitation,nsn,part_number,nomenclature,quantity,issue_date,return_by_date,is_open"
|
tango/shapes/explicit_schemas.py
CHANGED
|
@@ -877,6 +877,97 @@ FEDERAL_REGISTER_DOCUMENT_SCHEMA: dict[str, FieldSchema] = {
|
|
|
877
877
|
}
|
|
878
878
|
|
|
879
879
|
|
|
880
|
+
EBUY_ATTACHMENT_SCHEMA: dict[str, FieldSchema] = {
|
|
881
|
+
"doc_seq_num": FieldSchema(name="doc_seq_num", type=int, is_optional=True, is_list=False),
|
|
882
|
+
"doc_name": FieldSchema(name="doc_name", type=str, is_optional=True, is_list=False),
|
|
883
|
+
"doc_type": FieldSchema(name="doc_type", type=int, is_optional=True, is_list=False),
|
|
884
|
+
"doc_path": FieldSchema(name="doc_path", type=str, is_optional=True, is_list=False),
|
|
885
|
+
"is_link": FieldSchema(name="is_link", type=bool, is_optional=True, is_list=False),
|
|
886
|
+
"doc_session_date": FieldSchema(
|
|
887
|
+
name="doc_session_date", type=datetime, is_optional=True, is_list=False
|
|
888
|
+
),
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
EBUY_REQUEST_SCHEMA: dict[str, FieldSchema] = {
|
|
893
|
+
"rfq_id": FieldSchema(name="rfq_id", type=str, is_optional=True, is_list=False),
|
|
894
|
+
"request_type": FieldSchema(name="request_type", type=str, is_optional=True, is_list=False),
|
|
895
|
+
"title": FieldSchema(name="title", type=str, is_optional=True, is_list=False),
|
|
896
|
+
"description": FieldSchema(name="description", type=str, is_optional=True, is_list=False),
|
|
897
|
+
"schedule": FieldSchema(name="schedule", type=str, is_optional=True, is_list=False),
|
|
898
|
+
"sin": FieldSchema(name="sin", type=str, is_optional=True, is_list=False),
|
|
899
|
+
"status": FieldSchema(name="status", type=str, is_optional=True, is_list=False),
|
|
900
|
+
"buyer_name": FieldSchema(name="buyer_name", type=str, is_optional=True, is_list=False),
|
|
901
|
+
"buyer_agency": FieldSchema(name="buyer_agency", type=str, is_optional=True, is_list=False),
|
|
902
|
+
"buyer_agency_code": FieldSchema(
|
|
903
|
+
name="buyer_agency_code", type=str, is_optional=True, is_list=False
|
|
904
|
+
),
|
|
905
|
+
"buyer_email": FieldSchema(name="buyer_email", type=str, is_optional=True, is_list=False),
|
|
906
|
+
"buyer_user_id": FieldSchema(name="buyer_user_id", type=str, is_optional=True, is_list=False),
|
|
907
|
+
"reference_number": FieldSchema(
|
|
908
|
+
name="reference_number", type=str, is_optional=True, is_list=False
|
|
909
|
+
),
|
|
910
|
+
"award_method": FieldSchema(name="award_method", type=str, is_optional=True, is_list=False),
|
|
911
|
+
"contract_type": FieldSchema(name="contract_type", type=str, is_optional=True, is_list=False),
|
|
912
|
+
"commercial_type": FieldSchema(
|
|
913
|
+
name="commercial_type", type=str, is_optional=True, is_list=False
|
|
914
|
+
),
|
|
915
|
+
"follow_on": FieldSchema(name="follow_on", type=bool, is_optional=True, is_list=False),
|
|
916
|
+
"source_sought": FieldSchema(name="source_sought", type=bool, is_optional=True, is_list=False),
|
|
917
|
+
"issue_date": FieldSchema(name="issue_date", type=datetime, is_optional=True, is_list=False),
|
|
918
|
+
"close_date": FieldSchema(name="close_date", type=datetime, is_optional=True, is_list=False),
|
|
919
|
+
"cancel_date": FieldSchema(name="cancel_date", type=datetime, is_optional=True, is_list=False),
|
|
920
|
+
"last_mod_date": FieldSchema(
|
|
921
|
+
name="last_mod_date", type=datetime, is_optional=True, is_list=False
|
|
922
|
+
),
|
|
923
|
+
"pop_start_date": FieldSchema(
|
|
924
|
+
name="pop_start_date", type=datetime, is_optional=True, is_list=False
|
|
925
|
+
),
|
|
926
|
+
"pop_end_date": FieldSchema(
|
|
927
|
+
name="pop_end_date", type=datetime, is_optional=True, is_list=False
|
|
928
|
+
),
|
|
929
|
+
"oco_name": FieldSchema(name="oco_name", type=str, is_optional=True, is_list=False),
|
|
930
|
+
"oco_title": FieldSchema(name="oco_title", type=str, is_optional=True, is_list=False),
|
|
931
|
+
"oco_agency": FieldSchema(name="oco_agency", type=str, is_optional=True, is_list=False),
|
|
932
|
+
"oco_phone": FieldSchema(name="oco_phone", type=str, is_optional=True, is_list=False),
|
|
933
|
+
"oco_aac": FieldSchema(name="oco_aac", type=str, is_optional=True, is_list=False),
|
|
934
|
+
"ocs_name": FieldSchema(name="ocs_name", type=str, is_optional=True, is_list=False),
|
|
935
|
+
"ocs_title": FieldSchema(name="ocs_title", type=str, is_optional=True, is_list=False),
|
|
936
|
+
"ocs_agency": FieldSchema(name="ocs_agency", type=str, is_optional=True, is_list=False),
|
|
937
|
+
"ocs_phone": FieldSchema(name="ocs_phone", type=str, is_optional=True, is_list=False),
|
|
938
|
+
"ocs_aac": FieldSchema(name="ocs_aac", type=str, is_optional=True, is_list=False),
|
|
939
|
+
"amendment_count": FieldSchema(
|
|
940
|
+
name="amendment_count", type=int, is_optional=True, is_list=False
|
|
941
|
+
),
|
|
942
|
+
"mod_version": FieldSchema(name="mod_version", type=int, is_optional=True, is_list=False),
|
|
943
|
+
"qa_document_count": FieldSchema(
|
|
944
|
+
name="qa_document_count", type=int, is_optional=True, is_list=False
|
|
945
|
+
),
|
|
946
|
+
"attachment_count": FieldSchema(
|
|
947
|
+
name="attachment_count", type=int, is_optional=True, is_list=False
|
|
948
|
+
),
|
|
949
|
+
"link_count": FieldSchema(name="link_count", type=int, is_optional=True, is_list=False),
|
|
950
|
+
"amendments": FieldSchema(name="amendments", type=dict, is_optional=True, is_list=True),
|
|
951
|
+
"line_items": FieldSchema(name="line_items", type=dict, is_optional=True, is_list=True),
|
|
952
|
+
"addresses": FieldSchema(name="addresses", type=dict, is_optional=True, is_list=True),
|
|
953
|
+
"detail_fetched": FieldSchema(
|
|
954
|
+
name="detail_fetched", type=bool, is_optional=True, is_list=False
|
|
955
|
+
),
|
|
956
|
+
"first_seen": FieldSchema(name="first_seen", type=datetime, is_optional=True, is_list=False),
|
|
957
|
+
"last_seen": FieldSchema(name="last_seen", type=datetime, is_optional=True, is_list=False),
|
|
958
|
+
"organization": FieldSchema(
|
|
959
|
+
name="organization",
|
|
960
|
+
type=dict,
|
|
961
|
+
is_optional=True,
|
|
962
|
+
is_list=False,
|
|
963
|
+
nested_model="OrganizationOffice",
|
|
964
|
+
),
|
|
965
|
+
"attachments": FieldSchema(
|
|
966
|
+
name="attachments", type=dict, is_optional=True, is_list=True, nested_model="EbuyAttachment"
|
|
967
|
+
),
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
|
|
880
971
|
AGENCY_SCHEMA: dict[str, FieldSchema] = {
|
|
881
972
|
"abbreviation": FieldSchema(name="abbreviation", type=str, is_optional=True, is_list=False),
|
|
882
973
|
"code": FieldSchema(name="code", type=str, is_optional=False, is_list=False),
|
|
@@ -1623,6 +1714,8 @@ EXPLICIT_SCHEMAS: dict[str, dict[str, FieldSchema]] = {
|
|
|
1623
1714
|
"ProtestDocket": PROTEST_DOCKET_SCHEMA,
|
|
1624
1715
|
"ContractAppeal": CONTRACT_APPEAL_SCHEMA,
|
|
1625
1716
|
"FederalRegisterDocument": FEDERAL_REGISTER_DOCUMENT_SCHEMA,
|
|
1717
|
+
"EbuyRequest": EBUY_REQUEST_SCHEMA,
|
|
1718
|
+
"EbuyAttachment": EBUY_ATTACHMENT_SCHEMA,
|
|
1626
1719
|
"Agency": AGENCY_SCHEMA,
|
|
1627
1720
|
"Grant": GRANT_SCHEMA,
|
|
1628
1721
|
# Vehicles (Awards)
|
|
@@ -81,17 +81,6 @@ ARCHIVE_SCHEMA: dict[str, FieldSchema] = {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
ATTACHMENTS_SCHEMA: dict[str, FieldSchema] = {
|
|
84
|
-
"doc_name": FieldSchema(name="doc_name", type=str, is_optional=True, is_list=False),
|
|
85
|
-
"doc_path": FieldSchema(name="doc_path", type=str, is_optional=True, is_list=False),
|
|
86
|
-
"doc_seq_num": FieldSchema(name="doc_seq_num", type=str, is_optional=True, is_list=False),
|
|
87
|
-
"doc_session_date": FieldSchema(
|
|
88
|
-
name="doc_session_date", type=date, is_optional=True, is_list=False
|
|
89
|
-
),
|
|
90
|
-
"doc_type": FieldSchema(name="doc_type", type=str, is_optional=True, is_list=False),
|
|
91
|
-
"is_link": FieldSchema(name="is_link", type=bool, is_optional=True, is_list=False),
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
ATTACHMENTS2_SCHEMA: dict[str, FieldSchema] = {
|
|
95
84
|
"attachment_id": FieldSchema(name="attachment_id", type=str, is_optional=True, is_list=False),
|
|
96
85
|
"doc_role": FieldSchema(name="doc_role", type=str, is_optional=True, is_list=False),
|
|
97
86
|
"doc_role_alt": FieldSchema(name="doc_role_alt", type=str, is_optional=True, is_list=False),
|
|
@@ -105,7 +94,7 @@ ATTACHMENTS2_SCHEMA: dict[str, FieldSchema] = {
|
|
|
105
94
|
"url": FieldSchema(name="url", type=str, is_optional=True, is_list=False),
|
|
106
95
|
}
|
|
107
96
|
|
|
108
|
-
|
|
97
|
+
ATTACHMENTS2_SCHEMA: dict[str, FieldSchema] = {
|
|
109
98
|
"attachment_id": FieldSchema(name="attachment_id", type=str, is_optional=True, is_list=False),
|
|
110
99
|
"doc_role": FieldSchema(name="doc_role", type=str, is_optional=True, is_list=False),
|
|
111
100
|
"doc_role_alt": FieldSchema(name="doc_role_alt", type=str, is_optional=True, is_list=False),
|
|
@@ -119,7 +108,7 @@ ATTACHMENTS3_SCHEMA: dict[str, FieldSchema] = {
|
|
|
119
108
|
"url": FieldSchema(name="url", type=str, is_optional=True, is_list=False),
|
|
120
109
|
}
|
|
121
110
|
|
|
122
|
-
|
|
111
|
+
ATTACHMENTS3_SCHEMA: dict[str, FieldSchema] = {
|
|
123
112
|
"char_count": FieldSchema(name="char_count", type=int, is_optional=True, is_list=False),
|
|
124
113
|
"checksum": FieldSchema(name="checksum", type=str, is_optional=True, is_list=False),
|
|
125
114
|
"download_status": FieldSchema(
|
|
@@ -144,7 +133,7 @@ ATTACHMENTS4_SCHEMA: dict[str, FieldSchema] = {
|
|
|
144
133
|
"word_count": FieldSchema(name="word_count", type=int, is_optional=True, is_list=False),
|
|
145
134
|
}
|
|
146
135
|
|
|
147
|
-
|
|
136
|
+
ATTACHMENTS4_SCHEMA: dict[str, FieldSchema] = {
|
|
148
137
|
"attachment_id": FieldSchema(name="attachment_id", type=str, is_optional=True, is_list=False),
|
|
149
138
|
"file_size": FieldSchema(name="file_size", type=str, is_optional=True, is_list=False),
|
|
150
139
|
"mime_type": FieldSchema(name="mime_type", type=str, is_optional=True, is_list=False),
|
|
@@ -155,7 +144,7 @@ ATTACHMENTS5_SCHEMA: dict[str, FieldSchema] = {
|
|
|
155
144
|
"url": FieldSchema(name="url", type=str, is_optional=True, is_list=False),
|
|
156
145
|
}
|
|
157
146
|
|
|
158
|
-
|
|
147
|
+
ATTACHMENTS5_SCHEMA: dict[str, FieldSchema] = {
|
|
159
148
|
"attachment_id": FieldSchema(name="attachment_id", type=str, is_optional=True, is_list=False),
|
|
160
149
|
"doc_role": FieldSchema(name="doc_role", type=str, is_optional=True, is_list=False),
|
|
161
150
|
"doc_role_alt": FieldSchema(name="doc_role_alt", type=str, is_optional=True, is_list=False),
|
|
@@ -1119,7 +1108,6 @@ GENERATED_NESTED: dict[str, dict[str, FieldSchema]] = {
|
|
|
1119
1108
|
"Attachments3": ATTACHMENTS3_SCHEMA,
|
|
1120
1109
|
"Attachments4": ATTACHMENTS4_SCHEMA,
|
|
1121
1110
|
"Attachments5": ATTACHMENTS5_SCHEMA,
|
|
1122
|
-
"Attachments6": ATTACHMENTS6_SCHEMA,
|
|
1123
1111
|
"Awardee": AWARDEE_SCHEMA,
|
|
1124
1112
|
"AwardingOffice": AWARDING_OFFICE_SCHEMA,
|
|
1125
1113
|
"AwardingOffice2": AWARDING_OFFICE2_SCHEMA,
|
|
@@ -2238,7 +2226,7 @@ GENERATED_OVERLAY: dict[str, dict[str, FieldSchema]] = {
|
|
|
2238
2226
|
type=dict,
|
|
2239
2227
|
is_optional=True,
|
|
2240
2228
|
is_list=True,
|
|
2241
|
-
nested_model="
|
|
2229
|
+
nested_model="Attachments",
|
|
2242
2230
|
),
|
|
2243
2231
|
"meta": FieldSchema(
|
|
2244
2232
|
name="meta", type=dict, is_optional=True, is_list=False, nested_model="Meta"
|
|
@@ -2486,7 +2474,7 @@ GENERATED_OVERLAY: dict[str, dict[str, FieldSchema]] = {
|
|
|
2486
2474
|
type=dict,
|
|
2487
2475
|
is_optional=True,
|
|
2488
2476
|
is_list=False,
|
|
2489
|
-
nested_model="
|
|
2477
|
+
nested_model="Attachments5",
|
|
2490
2478
|
),
|
|
2491
2479
|
"department": FieldSchema(
|
|
2492
2480
|
name="department",
|
|
@@ -2743,6 +2731,7 @@ GENERATED_OVERLAY: dict[str, dict[str, FieldSchema]] = {
|
|
|
2743
2731
|
"activity": FieldSchema(name="activity", type=str, is_optional=True, is_list=False),
|
|
2744
2732
|
"agency": FieldSchema(name="agency", type=str, is_optional=True, is_list=False),
|
|
2745
2733
|
"close_date": FieldSchema(name="close_date", type=date, is_optional=True, is_list=False),
|
|
2734
|
+
"cycle_name": FieldSchema(name="cycle_name", type=str, is_optional=True, is_list=False),
|
|
2746
2735
|
"description": FieldSchema(name="description", type=str, is_optional=True, is_list=False),
|
|
2747
2736
|
"doc_source": FieldSchema(name="doc_source", type=str, is_optional=True, is_list=False),
|
|
2748
2737
|
"due_dates_text": FieldSchema(
|
|
@@ -2873,7 +2862,7 @@ GENERATED_OVERLAY: dict[str, dict[str, FieldSchema]] = {
|
|
|
2873
2862
|
type=dict,
|
|
2874
2863
|
is_optional=True,
|
|
2875
2864
|
is_list=True,
|
|
2876
|
-
nested_model="
|
|
2865
|
+
nested_model="Attachments3",
|
|
2877
2866
|
),
|
|
2878
2867
|
"bid_opening_date": FieldSchema(
|
|
2879
2868
|
name="bid_opening_date", type=datetime, is_optional=True, is_list=False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tango-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.0
|
|
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
|
|
@@ -308,6 +308,15 @@ protests = client.list_protests(source_system="gao", outcome="Sustained", limit=
|
|
|
308
308
|
protest = client.get_protest("CASE_UUID")
|
|
309
309
|
```
|
|
310
310
|
|
|
311
|
+
### GSA eBuy
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
access = client.get_ebuy_access() # scoped to the schedule contracts linked to your account
|
|
315
|
+
requests = client.list_ebuy_requests(status="Open", sin="54151S", limit=25)
|
|
316
|
+
request = client.get_ebuy_request("RFQ1835158")
|
|
317
|
+
url = client.get_ebuy_attachment_url("RFQ1835158", doc_seq_num=1) # short-lived signed URL
|
|
318
|
+
```
|
|
319
|
+
|
|
311
320
|
### GSA eLibrary Contracts
|
|
312
321
|
|
|
313
322
|
```python
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
tango/__init__.py,sha256=
|
|
2
|
-
tango/client.py,sha256=
|
|
3
|
-
tango/exceptions.py,sha256=
|
|
4
|
-
tango/models.py,sha256=
|
|
1
|
+
tango/__init__.py,sha256=rSPFjxFs-K1mVSICHc9nU94SvN5N2dhBEDVrD9y2BII,2535
|
|
2
|
+
tango/client.py,sha256=Srf-68uv9VvO5Dv0hi9SP-bHorOQl97zOysmX1owTeA,237960
|
|
3
|
+
tango/exceptions.py,sha256=oPiLCccBoWLK5SjHkzP-oI6mUD9twEeKSvEuLV5h01I,4224
|
|
4
|
+
tango/models.py,sha256=MwloM-gYq0ng9obnDn_lqYFG_cJ5aQZq6OvDqrW7S2s,66647
|
|
5
5
|
tango/shapes/__init__.py,sha256=7ea1WU74jp4znhNw-gXruag6m6eyPZtbVgbDFmFUWro,1072
|
|
6
|
-
tango/shapes/explicit_schemas.py,sha256=
|
|
6
|
+
tango/shapes/explicit_schemas.py,sha256=spYyMmWKOiMJygz9yViBQrjIxUPFfv7HpVkJcWy13x8,84021
|
|
7
7
|
tango/shapes/factory.py,sha256=uDPhnp6VVzrLBKodxteZy5AcU43juEv-qKVRWth51j0,31550
|
|
8
|
-
tango/shapes/generated_overlay.py,sha256=
|
|
8
|
+
tango/shapes/generated_overlay.py,sha256=udDQ1vQ9d4DpAOyhjRhsTD_8gx9QqhZfwk_PKXwZUJE,138465
|
|
9
9
|
tango/shapes/generator.py,sha256=xzakpwTwqTMUr89I06hp54-FIYZziviWHk2Glscg-LM,22775
|
|
10
10
|
tango/shapes/models.py,sha256=h3pIhOqrrdlN953Y6r0oney5HFbKPOD-frRndRWimJ0,3018
|
|
11
11
|
tango/shapes/parser.py,sha256=AecNx5oT_hV_pf2AWZnsR8Y2aLmCtjfgS9-znA2Pesc,27429
|
|
@@ -16,8 +16,8 @@ tango/webhooks/cli.py,sha256=f_vQbJ2AeSQjKnQo7MxHFyUB2SAKcgHYmGQULS0isqQ,13858
|
|
|
16
16
|
tango/webhooks/receiver.py,sha256=5yhsVhlLyoxmOCGvmbynWAIlDB2OaCPVf1H4GA1SxmU,9279
|
|
17
17
|
tango/webhooks/signing.py,sha256=92Ee-0B6PR7ZkvY3Np3gzl88-mtfKkh-I7lxqCe2lGw,2374
|
|
18
18
|
tango/webhooks/simulate.py,sha256=g2Osa0FYU5mJuon07T2aUCtmkUoTEzsY261tlp76fF0,3165
|
|
19
|
-
tango_python-1.
|
|
20
|
-
tango_python-1.
|
|
21
|
-
tango_python-1.
|
|
22
|
-
tango_python-1.
|
|
23
|
-
tango_python-1.
|
|
19
|
+
tango_python-1.10.0.dist-info/METADATA,sha256=GhxRXx0J567G_IXyAKx2JcDMXoyFFUbqezsYjNjvFig,22118
|
|
20
|
+
tango_python-1.10.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
21
|
+
tango_python-1.10.0.dist-info/entry_points.txt,sha256=kGLUbglUjuaAqEFvOZ1QuSW0vWb6VeSpCIFKaOFkKoQ,50
|
|
22
|
+
tango_python-1.10.0.dist-info/licenses/LICENSE,sha256=j2kYVHMwTkoGn3ZNScnrdIueG0k2XzB_LCPFoyBc2wk,1064
|
|
23
|
+
tango_python-1.10.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|