usaspending-orm 0.6.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.
- usaspending/__init__.py +144 -0
- usaspending/cli/__init__.py +2 -0
- usaspending/cli/download_award.py +113 -0
- usaspending/client.py +550 -0
- usaspending/config.py +119 -0
- usaspending/download/__init__.py +8 -0
- usaspending/download/job.py +219 -0
- usaspending/download/manager.py +140 -0
- usaspending/exceptions.py +71 -0
- usaspending/logging_config.py +310 -0
- usaspending/models/__init__.py +86 -0
- usaspending/models/agency.py +631 -0
- usaspending/models/award.py +933 -0
- usaspending/models/award_factory.py +72 -0
- usaspending/models/award_types.py +114 -0
- usaspending/models/base_model.py +214 -0
- usaspending/models/contract.py +147 -0
- usaspending/models/district_spending.py +70 -0
- usaspending/models/download.py +74 -0
- usaspending/models/funding.py +182 -0
- usaspending/models/grant.py +147 -0
- usaspending/models/idv.py +165 -0
- usaspending/models/lazy_record.py +82 -0
- usaspending/models/loan.py +85 -0
- usaspending/models/location.py +125 -0
- usaspending/models/period_of_performance.py +43 -0
- usaspending/models/recipient.py +204 -0
- usaspending/models/recipient_business_categories.py +364 -0
- usaspending/models/recipient_spending.py +58 -0
- usaspending/models/spending.py +70 -0
- usaspending/models/state_spending.py +43 -0
- usaspending/models/subaward.py +270 -0
- usaspending/models/subtier_agency.py +98 -0
- usaspending/models/transaction.py +89 -0
- usaspending/queries/__init__.py +87 -0
- usaspending/queries/agencies_search.py +196 -0
- usaspending/queries/agency_award_summary.py +134 -0
- usaspending/queries/agency_query.py +102 -0
- usaspending/queries/award_query.py +40 -0
- usaspending/queries/awarding_agencies_search.py +16 -0
- usaspending/queries/awards_search.py +602 -0
- usaspending/queries/filters.py +437 -0
- usaspending/queries/funding_agencies_search.py +20 -0
- usaspending/queries/funding_search.py +180 -0
- usaspending/queries/query_builder.py +1430 -0
- usaspending/queries/recipient_query.py +66 -0
- usaspending/queries/recipients_search.py +167 -0
- usaspending/queries/single_resource_base.py +83 -0
- usaspending/queries/spending_search.py +261 -0
- usaspending/queries/sub_agency_query.py +177 -0
- usaspending/queries/subawards_search.py +196 -0
- usaspending/queries/transactions_search.py +213 -0
- usaspending/resources/__init__.py +17 -0
- usaspending/resources/agency_resource.py +105 -0
- usaspending/resources/award_resource.py +119 -0
- usaspending/resources/base_resource.py +31 -0
- usaspending/resources/download_resource.py +102 -0
- usaspending/resources/funding_resource.py +41 -0
- usaspending/resources/recipients_resource.py +104 -0
- usaspending/resources/spending_resource.py +45 -0
- usaspending/resources/subawards_resource.py +68 -0
- usaspending/resources/transactions_resource.py +39 -0
- usaspending/utils/__init__.py +6 -0
- usaspending/utils/formatter.py +550 -0
- usaspending/utils/rate_limit.py +135 -0
- usaspending/utils/retry.py +257 -0
- usaspending/utils/special_cases.yaml +155 -0
- usaspending_orm-0.6.0.dist-info/LICENSE +18 -0
- usaspending_orm-0.6.0.dist-info/METADATA +324 -0
- usaspending_orm-0.6.0.dist-info/RECORD +71 -0
- usaspending_orm-0.6.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
"""Award model for USASpending data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Dict, Any, Optional, List, TYPE_CHECKING
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from datetime import date
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
|
|
9
|
+
from .lazy_record import LazyRecord
|
|
10
|
+
from .recipient import Recipient
|
|
11
|
+
from .location import Location
|
|
12
|
+
from .period_of_performance import PeriodOfPerformance
|
|
13
|
+
from .agency import Agency
|
|
14
|
+
from .subtier_agency import SubTierAgency
|
|
15
|
+
from .download import AwardType, FileFormat
|
|
16
|
+
|
|
17
|
+
from ..exceptions import ValidationError
|
|
18
|
+
from ..logging_config import USASpendingLogger
|
|
19
|
+
from ..utils.formatter import smart_sentence_case, to_decimal, to_date
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from ..client import USASpendingClient
|
|
23
|
+
from ..queries.transactions_search import TransactionsSearch
|
|
24
|
+
from ..queries.funding_search import FundingSearch
|
|
25
|
+
from ..queries.subawards_search import SubAwardsSearch
|
|
26
|
+
from ..download.job import DownloadJob
|
|
27
|
+
|
|
28
|
+
logger = USASpendingLogger.get_logger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Award(LazyRecord):
|
|
32
|
+
"""Rich wrapper around a USAspending award record."""
|
|
33
|
+
|
|
34
|
+
# Base fields common to all award types
|
|
35
|
+
SEARCH_FIELDS = [
|
|
36
|
+
"Award ID",
|
|
37
|
+
"recipient_id",
|
|
38
|
+
"Recipient Name",
|
|
39
|
+
"Recipient DUNS Number",
|
|
40
|
+
"Recipient UEI",
|
|
41
|
+
"Recipient Location",
|
|
42
|
+
"Awarding Agency",
|
|
43
|
+
"Awarding Agency Code",
|
|
44
|
+
"Awarding Sub Agency",
|
|
45
|
+
"Awarding Sub Agency Code",
|
|
46
|
+
"Funding Agency",
|
|
47
|
+
"Funding Agency Code",
|
|
48
|
+
"Funding Sub Agency",
|
|
49
|
+
"Funding Sub Agency Code",
|
|
50
|
+
"Place of Performance City Code",
|
|
51
|
+
"Place of Performance State Code",
|
|
52
|
+
"Place of Performance Country Code",
|
|
53
|
+
"Place of Performance Zip5",
|
|
54
|
+
"Description",
|
|
55
|
+
"Last Modified Date",
|
|
56
|
+
"Base Obligation Date",
|
|
57
|
+
"prime_award_recipient_id",
|
|
58
|
+
"generated_internal_id",
|
|
59
|
+
"def_codes",
|
|
60
|
+
"COVID-19 Obligations",
|
|
61
|
+
"COVID-19 Outlays",
|
|
62
|
+
"Infrastructure Obligations",
|
|
63
|
+
"Infrastructure Outlays",
|
|
64
|
+
"Primary Place of Performance",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
def __init__(self, data_or_id: Dict[str, Any] | str, client: USASpendingClient):
|
|
68
|
+
"""
|
|
69
|
+
Initialize Award instance.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
data_or_id (Dict[str, Any] | str): Either a dictionary containing award data (must include 'generated_unique_award_id'), or a string representing the unique award identifier. If a dictionary is provided with additional properties, those will be used to populate the instance.
|
|
73
|
+
client (USASpendingClient): USASpendingClient client instance.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValidationError: If data_or_id is not a dict or string, or if required keys are missing.
|
|
77
|
+
"""
|
|
78
|
+
# Use the base validation method
|
|
79
|
+
raw = self.validate_init_data(
|
|
80
|
+
data_or_id,
|
|
81
|
+
"Award",
|
|
82
|
+
id_field="generated_unique_award_id",
|
|
83
|
+
allow_string_id=True,
|
|
84
|
+
)
|
|
85
|
+
super().__init__(raw, client)
|
|
86
|
+
|
|
87
|
+
def _fetch_details(self) -> Optional[Dict[str, Any]]:
|
|
88
|
+
"""Fetch full award details from the awards resource.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Optional[Dict[str, Any]]: Award data dictionary or None if fetch fails.
|
|
92
|
+
"""
|
|
93
|
+
award_id = self.generated_unique_award_id
|
|
94
|
+
if not award_id:
|
|
95
|
+
raise ValidationError(
|
|
96
|
+
"Cannot lazy-load Award data. Property `generated_unique_award_id` is required to fetch details."
|
|
97
|
+
)
|
|
98
|
+
try:
|
|
99
|
+
# Use the awards resource to get full award data
|
|
100
|
+
full_award = self._client.awards.find_by_generated_id(award_id)
|
|
101
|
+
full_data = full_award.raw
|
|
102
|
+
|
|
103
|
+
# If we're a base Award class and now have type information,
|
|
104
|
+
# convert to appropriate subclass
|
|
105
|
+
if full_data and self.__class__ == Award:
|
|
106
|
+
from .award_factory import create_award
|
|
107
|
+
|
|
108
|
+
new_instance = create_award(full_data, self._client)
|
|
109
|
+
if new_instance.__class__ != Award:
|
|
110
|
+
# Copy state from new instance to self
|
|
111
|
+
self.__class__ = new_instance.__class__
|
|
112
|
+
# Merge the data
|
|
113
|
+
self._data.update(full_data)
|
|
114
|
+
return full_data
|
|
115
|
+
|
|
116
|
+
return full_data
|
|
117
|
+
except Exception:
|
|
118
|
+
logger.error(
|
|
119
|
+
f"Failed to fetch full details for Award ID {award_id}. "
|
|
120
|
+
"Check if the ID is valid and the client is configured correctly."
|
|
121
|
+
)
|
|
122
|
+
raise
|
|
123
|
+
|
|
124
|
+
# Core Award properties
|
|
125
|
+
@property
|
|
126
|
+
def id(self) -> Optional[int]:
|
|
127
|
+
"""Internal USASpending database ID for this award.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Optional[int]: Internal USASpending database ID.
|
|
131
|
+
"""
|
|
132
|
+
return self._lazy_get("id", "internal_id")
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def generated_unique_award_id(self) -> Optional[str]:
|
|
136
|
+
"""The award identifier used across USASpending and its Broker systems.
|
|
137
|
+
|
|
138
|
+
The code is created by combining various identifiers of award type, awarding
|
|
139
|
+
agency codes, and other standard identifiers:
|
|
140
|
+
|
|
141
|
+
Procurement Award ID Formats (FPDS):
|
|
142
|
+
`upper('CONT_AWD' + '_' + Coalesce(<piid>,'-NONE-') + '_' + Coalesce(<agency_id>,'-NONE-') + '_' + Coalesce(<parent_award_id>,'-NONE-') + '_' + Coalesce(<referenced_idv_agency_iden>,'-NONE-'))`
|
|
143
|
+
`upper('CONT_IDV' + '_' + Coalesce(<piid>,'-NONE-') + '_' + Coalesce(<agency_id>,'-NONE-'))`
|
|
144
|
+
|
|
145
|
+
Assistance Award ID Formats (FABS)
|
|
146
|
+
`upper('ASST_AGG' + '_' + Coalesce(<uri>,'-NONE-') + '_' + Coalesce(<awarding_sub_tier_agency_c>,'-NONE-'))`
|
|
147
|
+
`upper('ASST_NON' + '_' + Coalesce(<fain>,'-NONE-') + '_' + Coalesce(<awarding_sub_tier_agency_c>,'-NONE-'))`
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Optional[str]: The generated unique award identifier.
|
|
151
|
+
"""
|
|
152
|
+
# This cannot be lazy-loaded since it's required to fetch details
|
|
153
|
+
return self.get_value(["generated_unique_award_id", "generated_internal_id"])
|
|
154
|
+
|
|
155
|
+
def _derived_award_identifier(self) -> Optional[str]:
|
|
156
|
+
"""
|
|
157
|
+
Extract the award identifier (PIID, FAIN, or URI) from generated_unique_award_id.
|
|
158
|
+
|
|
159
|
+
Parses the generated ID format to extract the original identifier:
|
|
160
|
+
- CONT_AWD_<piid>_<agency>_<parent>_<ref> -> returns piid
|
|
161
|
+
- CONT_IDV_<piid>_<agency> -> returns piid
|
|
162
|
+
- ASST_NON_<fain>_<agency> -> returns fain
|
|
163
|
+
- ASST_AGG_<uri>_<agency> -> returns uri
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The extracted identifier or None if not found or is "-NONE-"
|
|
167
|
+
"""
|
|
168
|
+
gen_id = self.generated_unique_award_id
|
|
169
|
+
if not gen_id:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
parts = gen_id.split("_")
|
|
174
|
+
|
|
175
|
+
# Validate minimum parts based on format
|
|
176
|
+
if len(parts) < 3:
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
prefix = "_".join(parts[:2]) # e.g., "CONT_AWD" or "ASST_NON"
|
|
180
|
+
|
|
181
|
+
# Validate expected number of parts for each format
|
|
182
|
+
if prefix == "CONT_AWD" and len(parts) != 6:
|
|
183
|
+
return None
|
|
184
|
+
elif prefix == "CONT_IDV" and len(parts) != 4:
|
|
185
|
+
return None
|
|
186
|
+
elif prefix in ("ASST_NON", "ASST_AGG") and len(parts) != 4:
|
|
187
|
+
return None
|
|
188
|
+
elif prefix not in ("CONT_AWD", "CONT_IDV", "ASST_NON", "ASST_AGG"):
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
identifier = parts[2] # The actual ID is always the 3rd segment
|
|
192
|
+
|
|
193
|
+
# Don't return placeholder values
|
|
194
|
+
if identifier == "-NONE-" or not identifier:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
return identifier
|
|
198
|
+
except (IndexError, AttributeError):
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def award_identifier(self) -> str:
|
|
203
|
+
"""General-purpose award identifier, type-agnostic.
|
|
204
|
+
|
|
205
|
+
Specific property getters for PIID, FAIN, and URI are implemented in subclasses such as Contract, Grant, and IDV.
|
|
206
|
+
Use this property for a unified identifier, and refer to subclass documentation for type-specific identifiers.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
str: The award identifier (PIID, FAIN, or URI).
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
contract = Contract(...)
|
|
213
|
+
print(contract.award_identifier) # Returns PIID for contracts
|
|
214
|
+
"""
|
|
215
|
+
# Derive from generated_unique_award_id
|
|
216
|
+
derived_award_id = self._derived_award_identifier()
|
|
217
|
+
return derived_award_id if derived_award_id else ""
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def category(self) -> str:
|
|
221
|
+
"""Plain English description of the award type.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
str: One of "contract", "grant", "idv", "loan", or "other" if unknown.
|
|
225
|
+
"""
|
|
226
|
+
return self._lazy_get("category", default="")
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def type(self) -> Optional[str]:
|
|
230
|
+
"""Award's subtype code.
|
|
231
|
+
|
|
232
|
+
See `award_types.py` for all valid codes.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Optional[str]: The award subtype code.
|
|
236
|
+
"""
|
|
237
|
+
return self._lazy_get("type", default="")
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def award_type_code(self) -> Optional[str]:
|
|
241
|
+
"""More expressive property name for `type` to avoid confusion with Python built-in.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Optional[str]: The award subtype code.
|
|
245
|
+
"""
|
|
246
|
+
return self.type
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def type_description(self) -> Optional[str]:
|
|
250
|
+
"""Plain text description of the award type.
|
|
251
|
+
|
|
252
|
+
Retrieves a description for the award type using a lazy getter,
|
|
253
|
+
prioritizing "Contract Award Type" and "Award Type" keys.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Optional[str]: The description of the award type, or empty string if not available.
|
|
257
|
+
"""
|
|
258
|
+
return self._lazy_get(
|
|
259
|
+
"type_description", "Contract Award Type", "Award Type", default=""
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def description(self) -> str:
|
|
264
|
+
"""Brief, plain English summary of the award.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
str: The award description in sentence case, or empty string if not available.
|
|
268
|
+
"""
|
|
269
|
+
desc = self._lazy_get("description", "Description")
|
|
270
|
+
if isinstance(desc, str):
|
|
271
|
+
return smart_sentence_case(desc)
|
|
272
|
+
return ""
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def total_obligation(self) -> Decimal:
|
|
276
|
+
"""The amount of money the government is obligated to pay for the award.
|
|
277
|
+
|
|
278
|
+
This is a system generated element providing the sum of all the amounts
|
|
279
|
+
entered in the "Action Obligation" field for a particular PIID and Agency.
|
|
280
|
+
|
|
281
|
+
Example: Contract has 9 Modifications under "Transaction Number" as '1'
|
|
282
|
+
and 9 modifications with the same PIID under "Transaction Number" as '2'.
|
|
283
|
+
The base contracts and all the modifications have "Action Obligation" as $10
|
|
284
|
+
each. The value for the field "Total Obligated Amount" when the either of
|
|
285
|
+
the bases or the modification is retrieved through atom feeds will be $200
|
|
286
|
+
($100 under Transaction Number 1 + $100 under Transaction Number 2).
|
|
287
|
+
"Total Obligated Amount" is generated irrespective of the "Transaction Number"
|
|
288
|
+
on the Awards.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Decimal: The total obligated amount for the award or 0.00.
|
|
292
|
+
|
|
293
|
+
"""
|
|
294
|
+
return to_decimal(
|
|
295
|
+
self._lazy_get("total_obligation", "Award Amount")
|
|
296
|
+
) or Decimal("0.00")
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def subaward_count(self) -> int:
|
|
300
|
+
"""Number of subawards associated with this award.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
int: The count of subawards.
|
|
304
|
+
"""
|
|
305
|
+
return int(self._lazy_get("subaward_count", default=0))
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def total_subaward_amount(self) -> Optional[Decimal]:
|
|
309
|
+
"""Total amount of subawards for this award.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Optional[Decimal]: The total subaward amount, or None if not available.
|
|
313
|
+
"""
|
|
314
|
+
return to_decimal(self._lazy_get("total_subaward_amount", default=None))
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def date_signed(self) -> Optional[date]:
|
|
318
|
+
"""Date the award was signed by the Government or a binding agreement was reached.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Optional[date]: The date the award was signed, or None if not available.
|
|
322
|
+
"""
|
|
323
|
+
return to_date(
|
|
324
|
+
self._lazy_get("date_signed", "Base Obligation Date", default=None)
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def base_obligation_date(self) -> Optional[date]:
|
|
329
|
+
"""Base obligation date for the award (alias for date_signed).
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Optional[date]: The base obligation date, or None if not available.
|
|
333
|
+
"""
|
|
334
|
+
return self.date_signed
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def total_account_outlay(self) -> Optional[Decimal]:
|
|
338
|
+
"""Total amount of money paid out for the award from associated federal accounts.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Optional[Decimal]: The total account outlay amount, or None if not available.
|
|
342
|
+
"""
|
|
343
|
+
return to_decimal(self._lazy_get("total_account_outlay", default=None))
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def total_account_obligation(self) -> Optional[Decimal]:
|
|
347
|
+
"""Total amount obligated for this award.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Optional[Decimal]: The total account obligation amount, or None if not available.
|
|
351
|
+
"""
|
|
352
|
+
return to_decimal(self._lazy_get("total_account_obligation", default=None))
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def total_outlay(self) -> Optional[Decimal]:
|
|
356
|
+
"""Total outlay amount for the award.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Optional[Decimal]: The total outlay amount, or None if not available.
|
|
360
|
+
"""
|
|
361
|
+
return to_decimal(self._lazy_get("total_outlay", "Total Outlays", default=None))
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def account_outlays_by_defc(self) -> List[Dict[str, Any]]:
|
|
365
|
+
"""Outlays broken down by Disaster Emergency Fund Code (DEFC).
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
List[Dict[str, Any]]: List of outlay records by DEFC code.
|
|
369
|
+
"""
|
|
370
|
+
return self._lazy_get("account_outlays_by_defc", default=[])
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def account_obligations_by_defc(self) -> List[Dict[str, Any]]:
|
|
374
|
+
"""Obligations broken down by Disaster Emergency Fund Code (DEFC).
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
List[Dict[str, Any]]: List of obligation records by DEFC code.
|
|
378
|
+
"""
|
|
379
|
+
return self._lazy_get("account_obligations_by_defc", default=[])
|
|
380
|
+
|
|
381
|
+
@cached_property
|
|
382
|
+
def parent_award(self) -> Optional[Award]:
|
|
383
|
+
"""Reference to parent award for child awards.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Optional[Award]: The parent award object, or None if this is a parent award.
|
|
387
|
+
"""
|
|
388
|
+
data = self._lazy_get("parent_award")
|
|
389
|
+
from .award_factory import create_award
|
|
390
|
+
|
|
391
|
+
return create_award(data, self._client) if data else None
|
|
392
|
+
|
|
393
|
+
@cached_property
|
|
394
|
+
def executive_details(self) -> Optional[Dict[str, Any]]:
|
|
395
|
+
"""Executive compensation details for the award recipient.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Optional[Dict[str, Any]]: Executive compensation data, or None if not available.
|
|
399
|
+
"""
|
|
400
|
+
return self._lazy_get("executive_details")
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def recipient_uei(self) -> Optional[str]:
|
|
404
|
+
"""Recipient Unique Entity Identifier (UEI).
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Optional[str]: The recipient's UEI, or None if not available.
|
|
408
|
+
"""
|
|
409
|
+
# Try nested recipient object if available
|
|
410
|
+
if self.recipient and self.recipient.uei:
|
|
411
|
+
uei = self.recipient.uei
|
|
412
|
+
else:
|
|
413
|
+
uei = self._lazy_get("recipient_uei", "Recipient UEI")
|
|
414
|
+
|
|
415
|
+
return uei
|
|
416
|
+
|
|
417
|
+
@property
|
|
418
|
+
def covid19_obligations(self) -> Decimal:
|
|
419
|
+
"""COVID-19 related obligations amount.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Decimal: The COVID-19 obligations amount, or 0.00 if not available.
|
|
423
|
+
"""
|
|
424
|
+
return to_decimal(
|
|
425
|
+
self._lazy_get("covid19_obligations", "COVID-19 Obligations", default=0)
|
|
426
|
+
) or Decimal("0.00")
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def covid19_outlays(self) -> Decimal:
|
|
430
|
+
"""COVID-19 related outlays amount.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
Decimal: The COVID-19 outlays amount, or 0.00 if not available.
|
|
434
|
+
"""
|
|
435
|
+
return to_decimal(
|
|
436
|
+
self._lazy_get("covid19_outlays", "COVID-19 Outlays", default=0)
|
|
437
|
+
) or Decimal("0.00")
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
def infrastructure_obligations(self) -> Decimal:
|
|
441
|
+
"""Infrastructure related obligations amount.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
Decimal: The infrastructure obligations amount, or 0.00 if not available.
|
|
445
|
+
"""
|
|
446
|
+
return to_decimal(
|
|
447
|
+
self._lazy_get(
|
|
448
|
+
"infrastructure_obligations", "Infrastructure Obligations", default=0
|
|
449
|
+
)
|
|
450
|
+
) or Decimal("0.00")
|
|
451
|
+
|
|
452
|
+
@property
|
|
453
|
+
def infrastructure_outlays(self) -> Decimal:
|
|
454
|
+
"""Infrastructure related outlays amount.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Decimal: The infrastructure outlays amount, or 0.00 if not available.
|
|
458
|
+
"""
|
|
459
|
+
return to_decimal(
|
|
460
|
+
self._lazy_get(
|
|
461
|
+
"infrastructure_outlays", "Infrastructure Outlays", default=0
|
|
462
|
+
)
|
|
463
|
+
) or Decimal("0.00")
|
|
464
|
+
|
|
465
|
+
# Helper properties properties. These often map to field names returned by
|
|
466
|
+
# the spending_by_award/Award Search results, or provide general access methods
|
|
467
|
+
# that are common across award types.
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def award_amount(self) -> Decimal:
|
|
471
|
+
"""General helper for total obligated or loaned amount.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
Decimal: The total award amount, or 0.00 if not available.
|
|
475
|
+
"""
|
|
476
|
+
return to_decimal(
|
|
477
|
+
self._lazy_get(
|
|
478
|
+
"Award Amount", "Loan Amount", "total_obligation", "total_funding"
|
|
479
|
+
)
|
|
480
|
+
) or Decimal("0.00")
|
|
481
|
+
|
|
482
|
+
@property
|
|
483
|
+
def start_date(self) -> Optional[date]:
|
|
484
|
+
"""Award start date from period of performance or obligation data.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
Optional[date]: The award start date, or None if not available.
|
|
488
|
+
"""
|
|
489
|
+
start_date = self.get_value(
|
|
490
|
+
["Start Date", "Base Obligation Date", "Period of Performance Start Date"]
|
|
491
|
+
)
|
|
492
|
+
if not start_date:
|
|
493
|
+
if self.period_of_performance and self.period_of_performance.start_date:
|
|
494
|
+
start_date = self.period_of_performance.start_date
|
|
495
|
+
return to_date(start_date)
|
|
496
|
+
|
|
497
|
+
@property
|
|
498
|
+
def end_date(self) -> Optional[date]:
|
|
499
|
+
"""Award end date from period of performance data.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Optional[date]: The award end date, or None if not available.
|
|
503
|
+
"""
|
|
504
|
+
end_date = self.get_value(["End Date", "Period of Performance End Date"])
|
|
505
|
+
if not end_date:
|
|
506
|
+
if self.period_of_performance and self.period_of_performance.end_date:
|
|
507
|
+
end_date = self.period_of_performance.end_date
|
|
508
|
+
return to_date(end_date)
|
|
509
|
+
|
|
510
|
+
@property
|
|
511
|
+
def usa_spending_url(self) -> str:
|
|
512
|
+
"""USASpending.gov public URL for this award.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
str: The public URL for this award, or empty string if award ID unavailable.
|
|
516
|
+
"""
|
|
517
|
+
award_id = self.generated_unique_award_id
|
|
518
|
+
if award_id and isinstance(award_id, str):
|
|
519
|
+
return f"https://www.usaspending.gov/award/{award_id}/"
|
|
520
|
+
else:
|
|
521
|
+
return ""
|
|
522
|
+
|
|
523
|
+
# Properties that return complex objects and related award data
|
|
524
|
+
#
|
|
525
|
+
# Currently implemented are:
|
|
526
|
+
#
|
|
527
|
+
# Belongs To (one-to-one relationships):
|
|
528
|
+
# - parent_award (Award object: parent award if this is a child award)
|
|
529
|
+
# - recipient (Recipient object: details about the award recipient)
|
|
530
|
+
# - funding_agency (Agency object: details about the funding agency)
|
|
531
|
+
# - awarding_agency (Agency object: details about the awarding agency)
|
|
532
|
+
# - funding_subtier_agency (SubTierAgency object: details about the funding subtier agency)
|
|
533
|
+
# - awarding_subtier_agency (SubTierAgency object: details about the awarding subtier agency)
|
|
534
|
+
#
|
|
535
|
+
# Has One (one-to-one relationships):
|
|
536
|
+
# - period_of_performance (PlaceOfPerformance object: Start and End dates for award)
|
|
537
|
+
# - place_of_performance (Location object: location where the work is performed)
|
|
538
|
+
#
|
|
539
|
+
# Has Many (one-to-many relationships):
|
|
540
|
+
# - transactions (TransactionsSearch object: query builder for transactions associated with the award)
|
|
541
|
+
# - funding (FundingSearch object: query builder for treasury funding records (outlay and obligation) associated with the award)
|
|
542
|
+
# - subawards (SubAwardsSearch object: query builder for subawards associated with the award)
|
|
543
|
+
|
|
544
|
+
@cached_property
|
|
545
|
+
def period_of_performance(self) -> Optional[PeriodOfPerformance]:
|
|
546
|
+
"""Award period of performance dates.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
Optional[PeriodOfPerformance]: Period of performance object with start/end dates, or None.
|
|
550
|
+
"""
|
|
551
|
+
if "period_of_performance" in self.raw and isinstance(
|
|
552
|
+
self.raw.get("period_of_performance"), dict
|
|
553
|
+
):
|
|
554
|
+
return PeriodOfPerformance(self.raw.get("period_of_performance"))
|
|
555
|
+
|
|
556
|
+
# Award search results return Period of Performance information in a flat structure
|
|
557
|
+
# We need to assign these values to a PeriodOfPerformance object
|
|
558
|
+
# to maintain consistency.
|
|
559
|
+
date_keys = ["Start Date", "End Date", "Last Modified Date"]
|
|
560
|
+
if any(k in self._data for k in date_keys):
|
|
561
|
+
return PeriodOfPerformance(
|
|
562
|
+
{
|
|
563
|
+
"start_date": self.get_value(
|
|
564
|
+
[
|
|
565
|
+
"Start Date",
|
|
566
|
+
"Base Obligation Date",
|
|
567
|
+
"Period of Performance Start Date",
|
|
568
|
+
]
|
|
569
|
+
),
|
|
570
|
+
"end_date": self.get_value(
|
|
571
|
+
["End Date", "Period of Performance Current End Date"]
|
|
572
|
+
),
|
|
573
|
+
"last_modified_date": self.get_value("Last Modified Date"),
|
|
574
|
+
}
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
# If no data, trigger fetch
|
|
578
|
+
self._ensure_details()
|
|
579
|
+
return PeriodOfPerformance(self.get_value("period_of_performance"))
|
|
580
|
+
|
|
581
|
+
@cached_property
|
|
582
|
+
def place_of_performance(self) -> Optional[Location]:
|
|
583
|
+
"""Award place of performance location.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
Optional[Location]: Location object for where work is performed, or None.
|
|
587
|
+
"""
|
|
588
|
+
data = self._lazy_get(
|
|
589
|
+
"place_of_performance", "Primary Place of Performance", default=None
|
|
590
|
+
)
|
|
591
|
+
if not isinstance(data, dict) or not data:
|
|
592
|
+
return None
|
|
593
|
+
|
|
594
|
+
# Check if all values in the dict are None/null (common for IDV awards)
|
|
595
|
+
if all(v is None for v in data.values()):
|
|
596
|
+
return None
|
|
597
|
+
|
|
598
|
+
return Location(data, self._client)
|
|
599
|
+
|
|
600
|
+
@cached_property
|
|
601
|
+
def recipient(self) -> Optional[Recipient]:
|
|
602
|
+
"""Award recipient with lazy loading.
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
Optional[Recipient]: Recipient object with award recipient details, or None.
|
|
606
|
+
"""
|
|
607
|
+
# First check if we already have a nested recipient object
|
|
608
|
+
if "recipient" in self._data and isinstance(self._data["recipient"], dict):
|
|
609
|
+
return Recipient(self._data["recipient"], self._client)
|
|
610
|
+
|
|
611
|
+
# Then, check for flat recipient fields from search results
|
|
612
|
+
recipient_keys = ["Recipient Name", "recipient_id", "Recipient Location"]
|
|
613
|
+
if any(key in self._data for key in recipient_keys):
|
|
614
|
+
recipient_data = {
|
|
615
|
+
"recipient_name": self._data.get("Recipient Name"),
|
|
616
|
+
"recipient_unique_id": self._data.get("Recipient DUNS Number"),
|
|
617
|
+
"recipient_id": self._data.get("recipient_id"),
|
|
618
|
+
"recipient_hash": self._data.get("recipient_hash"),
|
|
619
|
+
"recipient_uei": self._data.get("Recipient UEI"),
|
|
620
|
+
}
|
|
621
|
+
recipient = Recipient(recipient_data, self._client)
|
|
622
|
+
if "Recipient Location" in self._data and isinstance(
|
|
623
|
+
self._data["Recipient Location"], dict
|
|
624
|
+
):
|
|
625
|
+
recipient.location = Location(
|
|
626
|
+
self._data["Recipient Location"], self._client
|
|
627
|
+
)
|
|
628
|
+
return recipient
|
|
629
|
+
|
|
630
|
+
# If no recipient data is available locally, trigger a fetch
|
|
631
|
+
self._ensure_details()
|
|
632
|
+
if "recipient" in self._data and isinstance(self._data["recipient"], dict):
|
|
633
|
+
return Recipient(self._data["recipient"], self._client)
|
|
634
|
+
|
|
635
|
+
return None
|
|
636
|
+
|
|
637
|
+
def _load_agency_data(self, agency_type: str) -> Optional[Dict[str, Any]]:
|
|
638
|
+
"""Load agency data from either nested or flat structure.
|
|
639
|
+
|
|
640
|
+
Args:
|
|
641
|
+
agency_type (str): Either "funding" or "awarding".
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
Optional[Dict[str, Any]]: Processed agency data dict or None if not available.
|
|
645
|
+
"""
|
|
646
|
+
if agency_type not in ["funding", "awarding"]:
|
|
647
|
+
raise ValueError(f"Invalid agency_type: {agency_type}")
|
|
648
|
+
|
|
649
|
+
# Define field mappings based on agency type
|
|
650
|
+
if agency_type == "funding":
|
|
651
|
+
nested_key = "funding_agency"
|
|
652
|
+
flat_keys = [
|
|
653
|
+
"Funding Agency",
|
|
654
|
+
"Funding Agency Code",
|
|
655
|
+
"Funding Sub Agency",
|
|
656
|
+
"Funding Sub Agency Code",
|
|
657
|
+
]
|
|
658
|
+
name_key = "Funding Agency"
|
|
659
|
+
code_key = "Funding Agency Code"
|
|
660
|
+
sub_name_key = "Funding Sub Agency"
|
|
661
|
+
sub_code_key = "Funding Sub Agency Code"
|
|
662
|
+
# No funding_agency_id available in search results
|
|
663
|
+
id_key = None
|
|
664
|
+
else: # awarding
|
|
665
|
+
nested_key = "awarding_agency"
|
|
666
|
+
flat_keys = [
|
|
667
|
+
"Awarding Agency",
|
|
668
|
+
"Awarding Agency Code",
|
|
669
|
+
"Awarding Sub Agency",
|
|
670
|
+
"Awarding Sub Agency Code",
|
|
671
|
+
]
|
|
672
|
+
name_key = "Awarding Agency"
|
|
673
|
+
code_key = "Awarding Agency Code"
|
|
674
|
+
sub_name_key = "Awarding Sub Agency"
|
|
675
|
+
sub_code_key = "Awarding Sub Agency Code"
|
|
676
|
+
id_key = "awarding_agency_id"
|
|
677
|
+
|
|
678
|
+
# First check if we have nested agency data (from full award details)
|
|
679
|
+
if self.raw.get(nested_key):
|
|
680
|
+
return self.raw.get(nested_key)
|
|
681
|
+
|
|
682
|
+
# Then check for flat agency fields (from search results)
|
|
683
|
+
if any(key in self.raw for key in flat_keys):
|
|
684
|
+
data = {
|
|
685
|
+
"toptier_agency": {
|
|
686
|
+
"name": self.raw.get(name_key),
|
|
687
|
+
"code": self.raw.get(code_key), # Agency code
|
|
688
|
+
"abbreviation": self.raw.get(code_key),
|
|
689
|
+
},
|
|
690
|
+
"subtier_agency": {
|
|
691
|
+
"name": self.raw.get(sub_name_key),
|
|
692
|
+
"code": self.raw.get(sub_code_key), # Subtier code
|
|
693
|
+
"abbreviation": self.raw.get(sub_code_key),
|
|
694
|
+
},
|
|
695
|
+
"id": self.raw.get(id_key) if id_key else None,
|
|
696
|
+
"has_agency_page": False, # Not available in search results
|
|
697
|
+
"office_agency_name": None, # Not available in search results
|
|
698
|
+
}
|
|
699
|
+
return data
|
|
700
|
+
|
|
701
|
+
# Finally try lazy loading
|
|
702
|
+
return self._lazy_get(nested_key)
|
|
703
|
+
|
|
704
|
+
@cached_property
|
|
705
|
+
def funding_agency(self) -> Optional[Agency]:
|
|
706
|
+
"""Funding agency information.
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
Optional[Agency]: Agency object for the funding agency, or None.
|
|
710
|
+
"""
|
|
711
|
+
data = self._load_agency_data("funding")
|
|
712
|
+
|
|
713
|
+
if not data:
|
|
714
|
+
return None
|
|
715
|
+
|
|
716
|
+
# Extract toptier data and merge with top-level agency fields
|
|
717
|
+
toptier_data = data.get("toptier_agency", {})
|
|
718
|
+
agency_data = {
|
|
719
|
+
"agency_id": data.get("id"),
|
|
720
|
+
"has_agency_page": data.get("has_agency_page"),
|
|
721
|
+
"office_agency_name": data.get("office_agency_name"),
|
|
722
|
+
**toptier_data, # Merge toptier fields (name, code, abbreviation, slug)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
subtier_data = data.get("subtier_agency")
|
|
726
|
+
return Agency(agency_data, self._client, subtier_data)
|
|
727
|
+
|
|
728
|
+
@cached_property
|
|
729
|
+
def awarding_agency(self) -> Optional[Agency]:
|
|
730
|
+
"""Awarding agency information.
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
Optional[Agency]: Agency object for the awarding agency, or None.
|
|
734
|
+
"""
|
|
735
|
+
data = self._load_agency_data("awarding")
|
|
736
|
+
|
|
737
|
+
if not data:
|
|
738
|
+
return None
|
|
739
|
+
|
|
740
|
+
# Extract toptier data and merge with top-level agency fields
|
|
741
|
+
toptier_data = data.get("toptier_agency", {})
|
|
742
|
+
agency_data = {
|
|
743
|
+
"agency_id": data.get("id"),
|
|
744
|
+
"has_agency_page": data.get("has_agency_page"),
|
|
745
|
+
"office_agency_name": data.get("office_agency_name"),
|
|
746
|
+
**toptier_data, # Merge toptier fields (name, code, abbreviation, slug)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
subtier_data = data.get("subtier_agency")
|
|
750
|
+
return Agency(agency_data, self._client, subtier_data)
|
|
751
|
+
|
|
752
|
+
@cached_property
|
|
753
|
+
def funding_subtier_agency(self) -> Optional["SubTierAgency"]:
|
|
754
|
+
"""Funding subtier agency information.
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
Optional[SubTierAgency]: SubTierAgency object for the funding subtier, or None.
|
|
758
|
+
"""
|
|
759
|
+
data = self._load_agency_data("funding")
|
|
760
|
+
|
|
761
|
+
if not data:
|
|
762
|
+
return None
|
|
763
|
+
|
|
764
|
+
subtier_data = data.get("subtier_agency")
|
|
765
|
+
if not subtier_data:
|
|
766
|
+
return None
|
|
767
|
+
|
|
768
|
+
# Create a copy and add office_agency_name if available
|
|
769
|
+
enhanced_subtier_data = subtier_data.copy()
|
|
770
|
+
office_name = data.get("office_agency_name")
|
|
771
|
+
if office_name:
|
|
772
|
+
enhanced_subtier_data["office_agency_name"] = office_name
|
|
773
|
+
|
|
774
|
+
from .subtier_agency import SubTierAgency
|
|
775
|
+
|
|
776
|
+
return SubTierAgency(enhanced_subtier_data, self._client)
|
|
777
|
+
|
|
778
|
+
@cached_property
|
|
779
|
+
def awarding_subtier_agency(self) -> Optional["SubTierAgency"]:
|
|
780
|
+
"""Awarding subtier agency information.
|
|
781
|
+
|
|
782
|
+
Returns:
|
|
783
|
+
Optional[SubTierAgency]: SubTierAgency object for the awarding subtier, or None.
|
|
784
|
+
"""
|
|
785
|
+
data = self._load_agency_data("awarding")
|
|
786
|
+
|
|
787
|
+
if not data:
|
|
788
|
+
return None
|
|
789
|
+
|
|
790
|
+
subtier_data = data.get("subtier_agency")
|
|
791
|
+
if not subtier_data:
|
|
792
|
+
return None
|
|
793
|
+
|
|
794
|
+
# Create a copy and add office_agency_name if available
|
|
795
|
+
enhanced_subtier_data = subtier_data.copy()
|
|
796
|
+
office_name = data.get("office_agency_name")
|
|
797
|
+
if office_name:
|
|
798
|
+
enhanced_subtier_data["office_agency_name"] = office_name
|
|
799
|
+
|
|
800
|
+
from .subtier_agency import SubTierAgency
|
|
801
|
+
|
|
802
|
+
return SubTierAgency(enhanced_subtier_data, self._client)
|
|
803
|
+
|
|
804
|
+
@property
|
|
805
|
+
def transactions(self) -> "TransactionsSearch":
|
|
806
|
+
"""Get transactions query builder for this award.
|
|
807
|
+
|
|
808
|
+
Returns a TransactionsSearch object that can be further filtered and chained.
|
|
809
|
+
|
|
810
|
+
Examples:
|
|
811
|
+
>>> award.transactions.count() # Get count without loading all data
|
|
812
|
+
>>> award.transactions.limit(10).all() # Get first 10 transactions
|
|
813
|
+
>>> list(award.transactions) # Iterate through all transactions
|
|
814
|
+
"""
|
|
815
|
+
return self._client.transactions.award_id(self.generated_unique_award_id)
|
|
816
|
+
|
|
817
|
+
@property
|
|
818
|
+
def funding(self) -> "FundingSearch":
|
|
819
|
+
"""Get funding query builder for this award.
|
|
820
|
+
|
|
821
|
+
Returns a FundingSearch object that can be further filtered and chained.
|
|
822
|
+
|
|
823
|
+
Examples:
|
|
824
|
+
>>> award.funding.count() # Get count without loading all data
|
|
825
|
+
>>> award.funding.order_by("fiscal_date", "asc").all() # Get all funding records sorted by date
|
|
826
|
+
>>> list(award.funding.limit(10)) # Iterate through first 10 funding records
|
|
827
|
+
"""
|
|
828
|
+
return self._client.funding.award_id(self.generated_unique_award_id)
|
|
829
|
+
|
|
830
|
+
@property
|
|
831
|
+
def subawards(self) -> "SubAwardsSearch":
|
|
832
|
+
"""Get subawards query builder for this award.
|
|
833
|
+
|
|
834
|
+
Returns:
|
|
835
|
+
SubAwardsSearch: Query builder object for subawards (implemented in subclasses).
|
|
836
|
+
|
|
837
|
+
Note:
|
|
838
|
+
Implemented in subclasses.
|
|
839
|
+
"""
|
|
840
|
+
raise NotImplementedError()
|
|
841
|
+
|
|
842
|
+
# Downloading detailed award data
|
|
843
|
+
@property
|
|
844
|
+
def _download_type(self) -> Optional[AwardType]:
|
|
845
|
+
"""Type required by the download API.
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
Optional[AwardType]: Download type ('contract', 'assistance', or 'idv').
|
|
849
|
+
|
|
850
|
+
Note:
|
|
851
|
+
Implementation varies by model subclass.
|
|
852
|
+
"""
|
|
853
|
+
from .contract import Contract
|
|
854
|
+
from .grant import Grant
|
|
855
|
+
from .idv import IDV
|
|
856
|
+
|
|
857
|
+
if isinstance(self, Contract):
|
|
858
|
+
return "contract"
|
|
859
|
+
elif isinstance(self, Grant):
|
|
860
|
+
return "assistance"
|
|
861
|
+
elif isinstance(self, IDV):
|
|
862
|
+
return "idv"
|
|
863
|
+
else:
|
|
864
|
+
raise (NotImplementedError)
|
|
865
|
+
|
|
866
|
+
def download(
|
|
867
|
+
self, file_format: FileFormat = "csv", destination_dir: Optional[str] = None
|
|
868
|
+
) -> "DownloadJob":
|
|
869
|
+
"""
|
|
870
|
+
Queue a download job for this award's detailed data.
|
|
871
|
+
|
|
872
|
+
This utilizes the USASpending bulk download API, which queues the request
|
|
873
|
+
and processes it asynchronously.
|
|
874
|
+
|
|
875
|
+
Args:
|
|
876
|
+
file_format: The format of the file(s) in the zip file containing the data
|
|
877
|
+
destination_dir: Directory where the file will be saved (defaults to CWD).
|
|
878
|
+
|
|
879
|
+
Returns:
|
|
880
|
+
A DownloadJob object. Use job.wait_for_completion() to block until finished.
|
|
881
|
+
|
|
882
|
+
Raises:
|
|
883
|
+
ConfigurationError: If the Award instance lacks a client reference.
|
|
884
|
+
ValidationError: If the award ID or download type is missing/invalid.
|
|
885
|
+
|
|
886
|
+
Example:
|
|
887
|
+
>>> contract = client.awards.find_by_generated_id("CONT_AWD_123...")
|
|
888
|
+
>>> job = contract.download(destination_dir="./data")
|
|
889
|
+
>>> print(f"Job queued: {job.file_name}. Waiting...")
|
|
890
|
+
>>> extracted_files = job.wait_for_completion(timeout=600)
|
|
891
|
+
>>> print(f"Download complete. Files: {extracted_files}")
|
|
892
|
+
"""
|
|
893
|
+
|
|
894
|
+
award_id = self.generated_unique_award_id
|
|
895
|
+
|
|
896
|
+
if not award_id:
|
|
897
|
+
# If we don't have an award ID, we cannot proceed
|
|
898
|
+
raise ValidationError(
|
|
899
|
+
"Cannot download award data without a 'generated_unique_award_id'. Ensure the award object is fully loaded."
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
download_type = self._download_type
|
|
903
|
+
|
|
904
|
+
if not download_type:
|
|
905
|
+
# Safety check in case a subclass doesn't implement _download_type or the implementation returns None
|
|
906
|
+
raise ValidationError(
|
|
907
|
+
f"Download is not supported or implemented for award type: {self.__class__.__name__}."
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
# Access the DownloadManager via the client's download resource.
|
|
911
|
+
# We route the call through the appropriate method on the resource.
|
|
912
|
+
if download_type == "contract":
|
|
913
|
+
return self._client.downloads.contract(
|
|
914
|
+
award_id, file_format, destination_dir
|
|
915
|
+
)
|
|
916
|
+
elif download_type == "assistance":
|
|
917
|
+
return self._client.downloads.assistance(
|
|
918
|
+
award_id, file_format, destination_dir
|
|
919
|
+
)
|
|
920
|
+
elif download_type == "idv":
|
|
921
|
+
return self._client.downloads.idv(award_id, file_format, destination_dir)
|
|
922
|
+
else:
|
|
923
|
+
raise NotImplementedError
|
|
924
|
+
|
|
925
|
+
def __repr__(self) -> str:
|
|
926
|
+
"""String representation of Award.
|
|
927
|
+
|
|
928
|
+
Returns:
|
|
929
|
+
str: Formatted string showing award ID and recipient name.
|
|
930
|
+
"""
|
|
931
|
+
recipient_name = self.recipient.name if self.recipient else "?"
|
|
932
|
+
award_id = self.award_identifier or self.generated_unique_award_id or "?"
|
|
933
|
+
return f"<Award {award_id} → {recipient_name}>"
|