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,602 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AwardsSearch Query Builder for USAspending.gov API.
|
|
3
|
+
|
|
4
|
+
This module provides a fluent interface for querying award data from the USAspending.gov API.
|
|
5
|
+
It wraps the following API endpoints:
|
|
6
|
+
|
|
7
|
+
1. **Primary Search Endpoint**: `/api/v2/search/spending_by_award/`
|
|
8
|
+
- Returns detailed award information based on complex filter criteria
|
|
9
|
+
- Supports pagination, sorting, and field selection
|
|
10
|
+
- Full documentation: https://github.com/fedspendingtransparency/usaspending-api/blob/102960f58c87e0a7b6490dc0317055cbfcaa9b7b/usaspending_api/api_contracts/contracts/v2/search/spending_by_award.md
|
|
11
|
+
|
|
12
|
+
2. **Count Endpoint**: `/api/v2/search/spending_by_award_count/`
|
|
13
|
+
- Returns counts of awards grouped by type
|
|
14
|
+
- Used internally by the count() method
|
|
15
|
+
- Full documentation: https://github.com/fedspendingtransparency/usaspending-api/blob/102960f58c87e0a7b6490dc0317055cbfcaa9b7b/usaspending_api/api_contracts/contracts/v2/search/spending_by_award_count.md
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
The AwardsSearch class provides a chainable query builder pattern for constructing complex
|
|
20
|
+
award searches using the USAspending API. All filter methods return a new instance,
|
|
21
|
+
allowing for immutable query construction.
|
|
22
|
+
|
|
23
|
+
## Award Types
|
|
24
|
+
|
|
25
|
+
USAspending.gov categorizes awards into several types, each with specific codes:
|
|
26
|
+
|
|
27
|
+
Due to limitations in the API, you cannot mix different award type categories. Any query
|
|
28
|
+
must include one of the following award types:
|
|
29
|
+
|
|
30
|
+
- **Contracts**: Types A, B, C, D - Procurement contracts
|
|
31
|
+
- **IDVs**: Types IDV_A through IDV_E - Indefinite Delivery Vehicles
|
|
32
|
+
- **Grants**: Types 02, 03, 04, 05 - Grant awards
|
|
33
|
+
- **Loans**: Types 07, 08 - Loan awards
|
|
34
|
+
- **Direct Payments**: Types 06, 10 - Direct payment awards
|
|
35
|
+
- **Other**: Types 09, 11, -1 - Other assistance awards
|
|
36
|
+
|
|
37
|
+
## Basic Usage Examples
|
|
38
|
+
|
|
39
|
+
### Example 1: Search for Recent Contracts
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from usaspending import USASpendingClient
|
|
43
|
+
|
|
44
|
+
client = USASpendingClient()
|
|
45
|
+
|
|
46
|
+
# Find contracts from FY2024 for small businesses
|
|
47
|
+
contracts = (
|
|
48
|
+
client.awards.search()
|
|
49
|
+
.contracts() # Filter to contract types only
|
|
50
|
+
.fiscal_year(2024)
|
|
51
|
+
.recipient_type_names("small_business")
|
|
52
|
+
.order_by("Last Modified Date", "desc")
|
|
53
|
+
.limit(100)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
for contract in contracts:
|
|
57
|
+
print(f"{contract.recipient.name}: ${contract.award_amount:,.2f} ${contract.period_of_performance.last_modified_date}")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Example 2: Search Grants by Agency
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# Find all grants from the Department of Education in 2023
|
|
64
|
+
grants = (
|
|
65
|
+
client.awards.search()
|
|
66
|
+
.grants() # Filter to grant types
|
|
67
|
+
.agency("Department of Education")
|
|
68
|
+
.fiscal_year(2023)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
print(f"Total grants: {grants.count()}")
|
|
72
|
+
|
|
73
|
+
# Iterate through results (automatically handles pagination)
|
|
74
|
+
for grant in grants:
|
|
75
|
+
print(f"{grant.award_id}: {grant.description}")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Example 3: Complex Multi-Filter Search
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# Search for NASA-related contracts in California since 2020
|
|
82
|
+
from datetime import date
|
|
83
|
+
|
|
84
|
+
results = (
|
|
85
|
+
client.awards.search()
|
|
86
|
+
.contracts()
|
|
87
|
+
.agency("National Aeronautics and Space Administration")
|
|
88
|
+
.time_period("2020-03-01", date.today().isoformat())
|
|
89
|
+
.place_of_performance_locations(
|
|
90
|
+
{"state_code": "CA", "country_code": "USA"}
|
|
91
|
+
)
|
|
92
|
+
.award_amounts(
|
|
93
|
+
{"lower_bound": 100000, "upper_bound": 10000000}
|
|
94
|
+
)
|
|
95
|
+
.order_by("Last Modified Date", "desc")
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
for award in results.page(1):
|
|
99
|
+
print(f"{award.award_id}: {award.recipient.name}")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
## Important Notes
|
|
104
|
+
|
|
105
|
+
The same filtering limitations and requirements that apply to the USAspending.gov API also apply here:
|
|
106
|
+
|
|
107
|
+
1. **Required Award Type Filter**: Every query must include a filter for `award_type_codes`.
|
|
108
|
+
Use the `.award_type_codes()` method or convenience methods like `.contracts()`, `.grants()`, `.loans()`, `.idvs()`, `.direct_payments()`, or `.other_assistance()`
|
|
109
|
+
to set the award type category.
|
|
110
|
+
|
|
111
|
+
2. **Single Category Restriction**: You cannot mix different award type categories
|
|
112
|
+
(e.g., contracts and grants) in a single query. Use separate queries for each.
|
|
113
|
+
|
|
114
|
+
3. **Automatic Pagination**: Iterating over results automatically handles pagination.
|
|
115
|
+
Use `limit()` to control returned result size.
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
from __future__ import annotations
|
|
120
|
+
|
|
121
|
+
from typing import Any
|
|
122
|
+
|
|
123
|
+
from ..client import USASpendingClient
|
|
124
|
+
from usaspending.exceptions import ValidationError
|
|
125
|
+
from usaspending.models.award_factory import create_award
|
|
126
|
+
from usaspending.models import Award
|
|
127
|
+
from usaspending.models.contract import Contract
|
|
128
|
+
from usaspending.models.grant import Grant
|
|
129
|
+
from usaspending.models.idv import IDV
|
|
130
|
+
from usaspending.models.loan import Loan
|
|
131
|
+
from usaspending.queries.query_builder import SearchQueryBuilder
|
|
132
|
+
from usaspending.logging_config import USASpendingLogger
|
|
133
|
+
from usaspending.queries.filters import (
|
|
134
|
+
SimpleListFilter,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Import award type codes from models
|
|
138
|
+
# These are defined by USASpending.gov and represent different categories of awards
|
|
139
|
+
from ..models.award_types import (
|
|
140
|
+
CONTRACT_CODES,
|
|
141
|
+
IDV_CODES,
|
|
142
|
+
LOAN_CODES,
|
|
143
|
+
GRANT_CODES,
|
|
144
|
+
AWARD_TYPE_GROUPS,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
logger = USASpendingLogger.get_logger(__name__)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class AwardsSearch(SearchQueryBuilder["Award"]):
|
|
151
|
+
"""
|
|
152
|
+
Builds and executes spending_by_award search queries with complex filtering.
|
|
153
|
+
|
|
154
|
+
This class provides a fluent interface for constructing queries against the
|
|
155
|
+
USAspending.gov API's spending_by_award endpoint. All methods return new
|
|
156
|
+
instances, ensuring immutability.
|
|
157
|
+
|
|
158
|
+
Attributes:
|
|
159
|
+
_client: The USASpending client instance for API communication.
|
|
160
|
+
_filter_objects: List of filter objects to apply to the query.
|
|
161
|
+
_order_by: Field to sort results by.
|
|
162
|
+
_order_direction: Sort direction ('asc' or 'desc').
|
|
163
|
+
|
|
164
|
+
See module docstring for detailed usage examples.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(self, client: USASpendingClient):
|
|
168
|
+
"""
|
|
169
|
+
Initialize the AwardsSearch query builder.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
client: The USASpending client instance for API communication.
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
>>> client = USASpendingClient()
|
|
176
|
+
>>> search = AwardsSearch(client)
|
|
177
|
+
"""
|
|
178
|
+
super().__init__(client)
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def _endpoint(self) -> str:
|
|
182
|
+
"""
|
|
183
|
+
Return the API endpoint for award searches.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
str: The endpoint path '/search/spending_by_award/'.
|
|
187
|
+
"""
|
|
188
|
+
return "/search/spending_by_award/"
|
|
189
|
+
|
|
190
|
+
def _clone(self) -> AwardsSearch:
|
|
191
|
+
"""
|
|
192
|
+
Create an immutable copy of the query builder.
|
|
193
|
+
|
|
194
|
+
This method ensures that all filter operations return new instances,
|
|
195
|
+
maintaining immutability of the query builder.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
AwardsSearch: A new instance with copied filter objects.
|
|
199
|
+
"""
|
|
200
|
+
clone = super()._clone()
|
|
201
|
+
clone._filter_objects = self._filter_objects.copy()
|
|
202
|
+
return clone
|
|
203
|
+
|
|
204
|
+
def _build_payload(self, page: int) -> dict[str, Any]:
|
|
205
|
+
"""
|
|
206
|
+
Construct the API request payload from filter objects.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
page: The page number to retrieve (1-indexed).
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
dict[str, Any]: The complete payload for the API request.
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
ValidationError: If required 'award_type_codes' filter is missing.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
final_filters = self._aggregate_filters()
|
|
219
|
+
|
|
220
|
+
# The 'award_type_codes' filter is required by the API.
|
|
221
|
+
if "award_type_codes" not in final_filters:
|
|
222
|
+
raise ValidationError(
|
|
223
|
+
"A filter for 'award_type_codes' is required. "
|
|
224
|
+
"Use the .award_type_codes() method or a convenience method like .contracts()."
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
payload = {
|
|
228
|
+
"filters": final_filters,
|
|
229
|
+
"fields": self._get_fields(),
|
|
230
|
+
"limit": self._get_effective_page_size(),
|
|
231
|
+
"page": page,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# Add sorting parameters if specified
|
|
235
|
+
if self._order_by:
|
|
236
|
+
payload["sort"] = self._order_by
|
|
237
|
+
payload["order"] = self._order_direction
|
|
238
|
+
|
|
239
|
+
return payload
|
|
240
|
+
|
|
241
|
+
def _transform_result(self, result: dict[str, Any]) -> Award:
|
|
242
|
+
"""
|
|
243
|
+
Transform a single API result into an Award model instance.
|
|
244
|
+
|
|
245
|
+
This method determines the appropriate Award subclass based on the
|
|
246
|
+
award type codes in the current filters.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
result: A single result dictionary from the API response.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Award: An appropriate Award subclass instance (Contract, Grant, etc.).
|
|
253
|
+
"""
|
|
254
|
+
# Get award type codes from current filters
|
|
255
|
+
award_type_codes = self._get_award_type_codes()
|
|
256
|
+
|
|
257
|
+
# If we're filtering for a single award type category, add it to the result
|
|
258
|
+
# This ensures the correct Award subclass is created even when the API
|
|
259
|
+
# response doesn't include explicit type information
|
|
260
|
+
if award_type_codes:
|
|
261
|
+
if award_type_codes.issubset(CONTRACT_CODES):
|
|
262
|
+
result["category"] = "contract"
|
|
263
|
+
elif award_type_codes.issubset(IDV_CODES):
|
|
264
|
+
result["category"] = "idv"
|
|
265
|
+
elif award_type_codes.issubset(GRANT_CODES):
|
|
266
|
+
result["category"] = "grant"
|
|
267
|
+
elif award_type_codes.issubset(LOAN_CODES):
|
|
268
|
+
result["category"] = "loan"
|
|
269
|
+
|
|
270
|
+
return create_award(result, self._client)
|
|
271
|
+
|
|
272
|
+
def _get_award_type_codes(self) -> set[str]:
|
|
273
|
+
"""
|
|
274
|
+
Extract award type codes from current filters.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
set[str]: Set of award type codes from filters, or empty set if none.
|
|
278
|
+
"""
|
|
279
|
+
for filter_obj in self._filter_objects:
|
|
280
|
+
filter_dict = filter_obj.to_dict()
|
|
281
|
+
if "award_type_codes" in filter_dict:
|
|
282
|
+
return set(filter_dict["award_type_codes"])
|
|
283
|
+
return set()
|
|
284
|
+
|
|
285
|
+
def _validate_single_award_type_category(self, new_codes: set[str]) -> None:
|
|
286
|
+
"""
|
|
287
|
+
Validate that only one category of award types is present.
|
|
288
|
+
|
|
289
|
+
USAspending API does not support mixing different award type categories
|
|
290
|
+
(e.g., contracts and grants) in a single query.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
new_codes: New award type codes being added to the query.
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
ValidationError: If the new codes would mix different award type
|
|
297
|
+
categories with existing codes.
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
>>> # This would raise ValidationError:
|
|
301
|
+
>>> search.award_type_codes("A", "02") # Contract + Grant
|
|
302
|
+
"""
|
|
303
|
+
existing_codes = self._get_award_type_codes()
|
|
304
|
+
all_codes = existing_codes | new_codes
|
|
305
|
+
|
|
306
|
+
if not all_codes:
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
# Check how many categories are represented using the config mapping
|
|
310
|
+
categories_present = 0
|
|
311
|
+
category_names = []
|
|
312
|
+
|
|
313
|
+
for category_name, codes in AWARD_TYPE_GROUPS.items():
|
|
314
|
+
if all_codes & frozenset(codes.keys()):
|
|
315
|
+
categories_present += 1
|
|
316
|
+
category_names.append(category_name)
|
|
317
|
+
|
|
318
|
+
if categories_present > 1:
|
|
319
|
+
raise ValidationError(
|
|
320
|
+
f"Cannot mix different award type categories: {', '.join(category_names)}. "
|
|
321
|
+
"Use separate queries for each award type category."
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def count(self) -> int:
|
|
325
|
+
"""
|
|
326
|
+
Get the total count of results without fetching all items.
|
|
327
|
+
|
|
328
|
+
This method uses the /search/spending_by_award_count/ endpoint to
|
|
329
|
+
efficiently retrieve counts without downloading full result data.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
int: The total number of matching awards for the selected award type category.
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
ValidationError: If award_type_codes filter is not set.
|
|
336
|
+
|
|
337
|
+
Example:
|
|
338
|
+
>>> contracts = client.awards.search().contracts().fiscal_year(2024)
|
|
339
|
+
>>> total = contracts.count()
|
|
340
|
+
>>> print(f"Found {total} contracts in FY2024")
|
|
341
|
+
"""
|
|
342
|
+
logger.debug(f"{self.__class__.__name__}.count() called")
|
|
343
|
+
|
|
344
|
+
# Aggregate filters to prepare for the count request
|
|
345
|
+
final_filters = self._aggregate_filters()
|
|
346
|
+
|
|
347
|
+
# The 'award_type_codes' filter is required by the API.
|
|
348
|
+
if "award_type_codes" not in final_filters:
|
|
349
|
+
raise ValidationError(
|
|
350
|
+
"A filter for 'award_type_codes' is required. "
|
|
351
|
+
"Use the .award_type_codes() method or a convenience method like .contracts()."
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Make the API call to count awards by type
|
|
355
|
+
results = self.count_awards_by_type()
|
|
356
|
+
|
|
357
|
+
# Get the award type codes to determine which category to count
|
|
358
|
+
award_type_codes = self._get_award_type_codes()
|
|
359
|
+
|
|
360
|
+
# Determine the category based on award type codes
|
|
361
|
+
category = self._get_award_type_category(award_type_codes)
|
|
362
|
+
|
|
363
|
+
# Extract the count for the specific category
|
|
364
|
+
total = results.get(category, 0)
|
|
365
|
+
|
|
366
|
+
logger.info(f"{self.__class__.__name__}.count() = {total} ({category})")
|
|
367
|
+
return total
|
|
368
|
+
|
|
369
|
+
def count_awards_by_type(self) -> dict[str, int]:
|
|
370
|
+
"""
|
|
371
|
+
Get counts of awards grouped by type category.
|
|
372
|
+
|
|
373
|
+
This method calls the /search/spending_by_award_count/ endpoint to get
|
|
374
|
+
counts for all award type categories matching the current filters.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
dict[str, int]: Dictionary mapping award type categories
|
|
378
|
+
('contracts', 'grants', 'loans', etc.) to their counts.
|
|
379
|
+
|
|
380
|
+
Example:
|
|
381
|
+
>>> search = client.awards.search().fiscal_year(2024)
|
|
382
|
+
>>> counts = search.count_awards_by_type()
|
|
383
|
+
>>> print(counts) # {'contracts': 1234, 'grants': 567, ...}
|
|
384
|
+
"""
|
|
385
|
+
endpoint = "/search/spending_by_award_count/"
|
|
386
|
+
final_filters = self._aggregate_filters()
|
|
387
|
+
|
|
388
|
+
payload = {
|
|
389
|
+
"filters": final_filters,
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
from ..logging_config import log_query_execution
|
|
393
|
+
|
|
394
|
+
log_query_execution(
|
|
395
|
+
logger,
|
|
396
|
+
"AwardsSearch._count_awards_by_type",
|
|
397
|
+
self._filter_objects,
|
|
398
|
+
endpoint,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Send the request to the count endpoint
|
|
402
|
+
response = self._client._make_request("POST", endpoint, json=payload)
|
|
403
|
+
results = response.get("results", {})
|
|
404
|
+
|
|
405
|
+
return results
|
|
406
|
+
|
|
407
|
+
def _get_award_type_category(self, award_type_codes: set[str]) -> str:
|
|
408
|
+
"""
|
|
409
|
+
Determine the award type category from award type codes.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
award_type_codes: Set of award type codes (e.g., {'A', 'B', 'C'}).
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
str: The category name as used in the count endpoint response
|
|
416
|
+
(e.g., 'contracts', 'grants', 'loans').
|
|
417
|
+
|
|
418
|
+
Raises:
|
|
419
|
+
ValidationError: If no valid award type category is found.
|
|
420
|
+
"""
|
|
421
|
+
# Map config category names to API response names
|
|
422
|
+
category_mapping = {
|
|
423
|
+
"contracts": "contracts",
|
|
424
|
+
"idvs": "idvs",
|
|
425
|
+
"loans": "loans",
|
|
426
|
+
"grants": "grants",
|
|
427
|
+
"direct_payments": "direct_payments",
|
|
428
|
+
"other_assistance": "other",
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
for category_name, codes in AWARD_TYPE_GROUPS.items():
|
|
432
|
+
if award_type_codes & frozenset(codes.keys()):
|
|
433
|
+
return category_mapping[category_name]
|
|
434
|
+
|
|
435
|
+
# Fail hard if no valid award type category is found
|
|
436
|
+
raise ValidationError("No valid award type category found. ")
|
|
437
|
+
|
|
438
|
+
def _get_fields(self) -> list[str]:
|
|
439
|
+
"""
|
|
440
|
+
Determine the list of fields to request based on award type filters.
|
|
441
|
+
|
|
442
|
+
The API returns different fields depending on the award type:
|
|
443
|
+
- Contracts (A, B, C, D): Include contract-specific fields like PSC, NAICS
|
|
444
|
+
- IDVs (IDV_*): Include IDV-specific fields like Last Date to Order
|
|
445
|
+
- Loans (07, 08): Include loan-specific fields like Loan Value, Subsidy Cost
|
|
446
|
+
- Grants/Assistance: Include assistance fields like CFDA Number, SAI Number
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
list[str]: List of field names to request from the API, combining
|
|
450
|
+
base Award fields with type-specific fields.
|
|
451
|
+
"""
|
|
452
|
+
# Start with base fields from Award model
|
|
453
|
+
base_fields = Award.SEARCH_FIELDS.copy()
|
|
454
|
+
|
|
455
|
+
# Get award type codes from filters
|
|
456
|
+
award_types = self._get_award_type_codes()
|
|
457
|
+
additional_fields = []
|
|
458
|
+
|
|
459
|
+
# Check each category and add appropriate fields based on model
|
|
460
|
+
for category_name, codes in AWARD_TYPE_GROUPS.items():
|
|
461
|
+
if award_types & frozenset(codes.keys()):
|
|
462
|
+
if category_name == "contracts":
|
|
463
|
+
# Use Contract.SEARCH_FIELDS but exclude base fields
|
|
464
|
+
additional_fields.extend(
|
|
465
|
+
[f for f in Contract.SEARCH_FIELDS if f not in base_fields]
|
|
466
|
+
)
|
|
467
|
+
elif category_name == "idvs":
|
|
468
|
+
# Use IDV.SEARCH_FIELDS but exclude base fields
|
|
469
|
+
additional_fields.extend(
|
|
470
|
+
[f for f in IDV.SEARCH_FIELDS if f not in base_fields]
|
|
471
|
+
)
|
|
472
|
+
elif category_name == "loans":
|
|
473
|
+
# Use Loan.SEARCH_FIELDS but exclude base fields
|
|
474
|
+
additional_fields.extend(
|
|
475
|
+
[f for f in Loan.SEARCH_FIELDS if f not in base_fields]
|
|
476
|
+
)
|
|
477
|
+
elif category_name in ["grants", "direct_payments", "other_assistance"]:
|
|
478
|
+
# Use Grant.SEARCH_FIELDS but exclude base fields
|
|
479
|
+
additional_fields.extend(
|
|
480
|
+
[f for f in Grant.SEARCH_FIELDS if f not in base_fields]
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# Combine base fields with additional fields, removing duplicates
|
|
484
|
+
all_fields = base_fields + additional_fields
|
|
485
|
+
return list(
|
|
486
|
+
dict.fromkeys(all_fields)
|
|
487
|
+
) # Remove duplicates while preserving order
|
|
488
|
+
|
|
489
|
+
def order_by(self, field: str, direction: str = "desc") -> AwardsSearch:
|
|
490
|
+
"""
|
|
491
|
+
Set the sort field and direction for query results.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
field: The field name to sort by. Must be a valid field from the
|
|
495
|
+
current award type's available fields.
|
|
496
|
+
direction: The sort direction, either "asc" or "desc".
|
|
497
|
+
Defaults to "desc".
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
AwardsSearch: A new instance with the ordering applied.
|
|
501
|
+
|
|
502
|
+
Raises:
|
|
503
|
+
ValidationError: If the field is not valid for the current
|
|
504
|
+
award type configuration.
|
|
505
|
+
|
|
506
|
+
Example:
|
|
507
|
+
>>> # Sort contracts by award amount, highest first
|
|
508
|
+
>>> results = (
|
|
509
|
+
... client.awards.search()
|
|
510
|
+
... .contracts()
|
|
511
|
+
... .order_by("Award Amount", "desc")
|
|
512
|
+
... )
|
|
513
|
+
|
|
514
|
+
>>> # Sort grants by date, newest first
|
|
515
|
+
>>> grants = (
|
|
516
|
+
... client.awards.search()
|
|
517
|
+
... .grants()
|
|
518
|
+
... .order_by("Start Date", "desc")
|
|
519
|
+
... )
|
|
520
|
+
"""
|
|
521
|
+
# Get the valid fields for the current award type configuration
|
|
522
|
+
valid_fields = self._get_fields()
|
|
523
|
+
|
|
524
|
+
# Validate that the field is in the list of valid fields
|
|
525
|
+
if field not in valid_fields:
|
|
526
|
+
# Build a helpful error message
|
|
527
|
+
award_types = self._get_award_type_codes()
|
|
528
|
+
if award_types:
|
|
529
|
+
# Determine which category we're searching
|
|
530
|
+
category_names = []
|
|
531
|
+
for category_name, codes in AWARD_TYPE_GROUPS.items():
|
|
532
|
+
if award_types & frozenset(codes.keys()):
|
|
533
|
+
category_names.append(category_name)
|
|
534
|
+
category_str = (
|
|
535
|
+
", ".join(category_names)
|
|
536
|
+
if category_names
|
|
537
|
+
else "selected award types"
|
|
538
|
+
)
|
|
539
|
+
else:
|
|
540
|
+
category_str = "all award types (no type filter applied)"
|
|
541
|
+
|
|
542
|
+
raise ValidationError(
|
|
543
|
+
f"Invalid sort field '{field}' for {category_str}. "
|
|
544
|
+
f"Valid fields are: {', '.join(sorted(valid_fields))}"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Call the parent class order_by method
|
|
548
|
+
return super().order_by(field, direction)
|
|
549
|
+
|
|
550
|
+
# ==========================================================================
|
|
551
|
+
# Filter Methods
|
|
552
|
+
# ==========================================================================
|
|
553
|
+
|
|
554
|
+
def award_type_codes(self, *award_codes: str) -> AwardsSearch:
|
|
555
|
+
"""
|
|
556
|
+
Filter by one or more award type codes.
|
|
557
|
+
|
|
558
|
+
**This filter is required** - the API requires at least one award type code.
|
|
559
|
+
|
|
560
|
+
Award type codes include:
|
|
561
|
+
- Contracts: A, B, C, D
|
|
562
|
+
- IDVs: IDV_A, IDV_B, IDV_B_A, IDV_B_B, IDV_B_C, IDV_C, IDV_D, IDV_E
|
|
563
|
+
- Grants: 02, 03, 04, 05
|
|
564
|
+
- Loans: 07, 08
|
|
565
|
+
- Direct Payments: 06, 10
|
|
566
|
+
- Other: 09, 11, -1
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
*award_codes: A sequence of award type codes.
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
AwardsSearch: A new instance with the award type filter applied.
|
|
573
|
+
|
|
574
|
+
Raises:
|
|
575
|
+
ValidationError: If mixing different award type categories
|
|
576
|
+
(e.g., contracts and grants in the same query).
|
|
577
|
+
|
|
578
|
+
Example:
|
|
579
|
+
>>> # Search for specific contract types
|
|
580
|
+
>>> contracts = (
|
|
581
|
+
... client.awards.search()
|
|
582
|
+
... .award_type_codes("A", "B", "C", "D")
|
|
583
|
+
... )
|
|
584
|
+
|
|
585
|
+
>>> # Search for grants only
|
|
586
|
+
>>> grants = (
|
|
587
|
+
... client.awards.search()
|
|
588
|
+
... .award_type_codes("02", "03", "04", "05")
|
|
589
|
+
... )
|
|
590
|
+
|
|
591
|
+
>>> # This will raise ValidationError (mixing categories):
|
|
592
|
+
>>> # client.awards.search().award_type_codes("A", "02") # Contract + Grant
|
|
593
|
+
"""
|
|
594
|
+
new_codes = set(award_codes)
|
|
595
|
+
self._validate_single_award_type_category(new_codes)
|
|
596
|
+
|
|
597
|
+
clone = self._clone()
|
|
598
|
+
clone._filter_objects.append(
|
|
599
|
+
SimpleListFilter(key="award_type_codes", values=list(award_codes))
|
|
600
|
+
)
|
|
601
|
+
return clone
|
|
602
|
+
|