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.
Files changed (71) hide show
  1. usaspending/__init__.py +144 -0
  2. usaspending/cli/__init__.py +2 -0
  3. usaspending/cli/download_award.py +113 -0
  4. usaspending/client.py +550 -0
  5. usaspending/config.py +119 -0
  6. usaspending/download/__init__.py +8 -0
  7. usaspending/download/job.py +219 -0
  8. usaspending/download/manager.py +140 -0
  9. usaspending/exceptions.py +71 -0
  10. usaspending/logging_config.py +310 -0
  11. usaspending/models/__init__.py +86 -0
  12. usaspending/models/agency.py +631 -0
  13. usaspending/models/award.py +933 -0
  14. usaspending/models/award_factory.py +72 -0
  15. usaspending/models/award_types.py +114 -0
  16. usaspending/models/base_model.py +214 -0
  17. usaspending/models/contract.py +147 -0
  18. usaspending/models/district_spending.py +70 -0
  19. usaspending/models/download.py +74 -0
  20. usaspending/models/funding.py +182 -0
  21. usaspending/models/grant.py +147 -0
  22. usaspending/models/idv.py +165 -0
  23. usaspending/models/lazy_record.py +82 -0
  24. usaspending/models/loan.py +85 -0
  25. usaspending/models/location.py +125 -0
  26. usaspending/models/period_of_performance.py +43 -0
  27. usaspending/models/recipient.py +204 -0
  28. usaspending/models/recipient_business_categories.py +364 -0
  29. usaspending/models/recipient_spending.py +58 -0
  30. usaspending/models/spending.py +70 -0
  31. usaspending/models/state_spending.py +43 -0
  32. usaspending/models/subaward.py +270 -0
  33. usaspending/models/subtier_agency.py +98 -0
  34. usaspending/models/transaction.py +89 -0
  35. usaspending/queries/__init__.py +87 -0
  36. usaspending/queries/agencies_search.py +196 -0
  37. usaspending/queries/agency_award_summary.py +134 -0
  38. usaspending/queries/agency_query.py +102 -0
  39. usaspending/queries/award_query.py +40 -0
  40. usaspending/queries/awarding_agencies_search.py +16 -0
  41. usaspending/queries/awards_search.py +602 -0
  42. usaspending/queries/filters.py +437 -0
  43. usaspending/queries/funding_agencies_search.py +20 -0
  44. usaspending/queries/funding_search.py +180 -0
  45. usaspending/queries/query_builder.py +1430 -0
  46. usaspending/queries/recipient_query.py +66 -0
  47. usaspending/queries/recipients_search.py +167 -0
  48. usaspending/queries/single_resource_base.py +83 -0
  49. usaspending/queries/spending_search.py +261 -0
  50. usaspending/queries/sub_agency_query.py +177 -0
  51. usaspending/queries/subawards_search.py +196 -0
  52. usaspending/queries/transactions_search.py +213 -0
  53. usaspending/resources/__init__.py +17 -0
  54. usaspending/resources/agency_resource.py +105 -0
  55. usaspending/resources/award_resource.py +119 -0
  56. usaspending/resources/base_resource.py +31 -0
  57. usaspending/resources/download_resource.py +102 -0
  58. usaspending/resources/funding_resource.py +41 -0
  59. usaspending/resources/recipients_resource.py +104 -0
  60. usaspending/resources/spending_resource.py +45 -0
  61. usaspending/resources/subawards_resource.py +68 -0
  62. usaspending/resources/transactions_resource.py +39 -0
  63. usaspending/utils/__init__.py +6 -0
  64. usaspending/utils/formatter.py +550 -0
  65. usaspending/utils/rate_limit.py +135 -0
  66. usaspending/utils/retry.py +257 -0
  67. usaspending/utils/special_cases.yaml +155 -0
  68. usaspending_orm-0.6.0.dist-info/LICENSE +18 -0
  69. usaspending_orm-0.6.0.dist-info/METADATA +324 -0
  70. usaspending_orm-0.6.0.dist-info/RECORD +71 -0
  71. 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
+