Coverage for src/usaspending/queries/awards_search.py: 99%

221 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-03 17:15 -0700

1from __future__ import annotations 

2 

3import datetime 

4from typing import Any, Optional, Union 

5 

6from usaspending.client import USASpending 

7from usaspending.exceptions import ValidationError 

8from usaspending.models.award_factory import create_award 

9from usaspending.models import Award 

10from usaspending.models.contract import Contract 

11from usaspending.models.grant import Grant 

12from usaspending.models.idv import IDV 

13from usaspending.models.loan import Loan 

14from usaspending.queries.query_builder import QueryBuilder 

15from usaspending.logging_config import USASpendingLogger 

16from usaspending.queries.filters import ( 

17 AgencyFilter, 

18 AgencyTier, 

19 AgencyType, 

20 AwardAmount, 

21 AwardAmountFilter, 

22 AwardDateType, 

23 KeywordsFilter, 

24 LocationSpec, 

25 LocationFilter, 

26 LocationScope, 

27 LocationScopeFilter, 

28 SimpleListFilter, 

29 TieredCodeFilter, 

30 TimePeriodFilter, 

31 TreasuryAccountComponentsFilter, 

32) 

33 

34# Import award type codes from config 

35# These are defined by USASpending.gov and represent different categories of awards 

36from ..config import ( 

37 CONTRACT_CODES, 

38 IDV_CODES, 

39 LOAN_CODES, 

40 GRANT_CODES, 

41 DIRECT_PAYMENT_CODES, 

42 OTHER_CODES, 

43 AWARD_TYPE_GROUPS, 

44) 

45 

46logger = USASpendingLogger.get_logger(__name__) 

47 

48 

49class AwardsSearch(QueryBuilder["Award"]): 

50 """ 

51 Builds and executes a spending_by_award search query, allowing for complex 

52 filtering on award data. This class follows a fluent interface pattern. 

53 """ 

54 

55 def __init__(self, client: USASpending): 

56 """ 

57 Initializes the AwardsSearch query builder. 

58 

59 Args: 

60 client: The USASpending client instance. 

61 """ 

62 super().__init__(client) 

63 

64 @property 

65 def _endpoint(self) -> str: 

66 """The API endpoint for this query.""" 

67 return "/search/spending_by_award/" 

68 

69 def _clone(self) -> AwardsSearch: 

70 """Creates an immutable copy of the query builder.""" 

71 clone = super()._clone() 

72 clone._filter_objects = self._filter_objects.copy() 

73 return clone 

74 

75 def _build_payload(self, page: int) -> dict[str, Any]: 

76 """Constructs the final API request payload from the filter objects.""" 

77 

78 final_filters = self._aggregate_filters() 

79 

80 # The 'award_type_codes' filter is required by the API. 

81 if "award_type_codes" not in final_filters: 

82 raise ValidationError( 

83 "A filter for 'award_type_codes' is required. " 

84 "Use the .with_award_types() method." 

85 ) 

86 

87 payload = { 

88 "filters": final_filters, 

89 "fields": self._get_fields(), 

90 "limit": self._get_effective_page_size(), 

91 "page": page, 

92 } 

93 return payload 

94 

95 def _transform_result(self, result: dict[str, Any]) -> Award: 

96 """Transforms a single API result item into an Award model.""" 

97 # Get award type codes from current filters 

98 award_type_codes = self._get_award_type_codes() 

99 

100 # If we're filtering for a single award type category, add it to the result 

101 # This ensures the correct Award subclass is created even when the API 

102 # response doesn't include explicit type information 

103 if award_type_codes: 

104 if award_type_codes.issubset(CONTRACT_CODES): 

105 result["category"] = "contract" 

106 elif award_type_codes.issubset(IDV_CODES): 

107 result["category"] = "idv" 

108 elif award_type_codes.issubset(GRANT_CODES): 

109 result["category"] = "grant" 

110 elif award_type_codes.issubset(LOAN_CODES): 

111 result["category"] = "loan" 

112 

113 return create_award(result, self._client) 

114 

115 def _get_award_type_codes(self) -> set[str]: 

116 """Extract award type codes from current filters.""" 

117 for filter_obj in self._filter_objects: 

118 filter_dict = filter_obj.to_dict() 

119 if "award_type_codes" in filter_dict: 

120 return set(filter_dict["award_type_codes"]) 

121 return set() 

122 

123 def _validate_single_award_type_category(self, new_codes: set[str]) -> None: 

124 """ 

125 Validate that only one category of award types is present. 

126 

127 Args: 

128 new_codes: New award type codes being added 

129 

130 Raises: 

131 ValidationError: If mixing award type categories 

132 """ 

133 existing_codes = self._get_award_type_codes() 

134 all_codes = existing_codes | new_codes 

135 

136 if not all_codes: 

137 return 

138 

139 # Check how many categories are represented using the config mapping 

140 categories_present = 0 

141 category_names = [] 

142 

143 for category_name, codes in AWARD_TYPE_GROUPS.items(): 

144 if all_codes & frozenset(codes.keys()): 

145 categories_present += 1 

146 category_names.append(category_name) 

147 

148 if categories_present > 1: 

149 raise ValidationError( 

150 f"Cannot mix different award type categories: {', '.join(category_names)}. " 

151 "Use separate queries for each award type category." 

152 ) 

153 

154 def count(self) -> int: 

155 """ 

156 Get the total count of results without fetching all items. 

157 

158 Returns: 

159 The total number of matching awards for the selected award type category. 

160 """ 

161 logger.debug(f"{self.__class__.__name__}.count() called") 

162 

163 # Aggregate filters to prepare for the count request 

164 final_filters = self._aggregate_filters() 

165 

166 # The 'award_type_codes' filter is required by the API. 

167 if "award_type_codes" not in final_filters: 

168 raise ValidationError( 

169 "A filter for 'award_type_codes' is required. " 

170 "Use the .with_award_types() method." 

171 ) 

172 

173 # Make the API call to count awards by type 

174 results = self.count_awards_by_type() 

175 

176 # Get the award type codes to determine which category to count 

177 award_type_codes = self._get_award_type_codes() 

178 

179 # Determine the category based on award type codes 

180 category = self._get_award_type_category(award_type_codes) 

181 

182 # Extract the count for the specific category 

183 total = results.get(category, 0) 

184 

185 logger.info(f"{self.__class__.__name__}.count() = {total} ({category})") 

186 return total 

187 

188 def count_awards_by_type(self) -> dict[str, int]: 

189 """ Shared logic that calls the awards count endpoint. 

190  

191 Returns: 

192 A dictionary mapping award type categories to their result counts 

193 for the matching filter set. 

194 """ 

195 endpoint = "/search/spending_by_award_count/" 

196 final_filters = self._aggregate_filters() 

197 

198 payload = { 

199 "filters": final_filters, 

200 } 

201 

202 from ..logging_config import log_query_execution 

203 

204 log_query_execution( 

205 logger, "AwardsSearch._count_awards_by_type", len(self._filter_objects), endpoint 

206 ) 

207 

208 # Send the request to the count endpoint 

209 response = self._client._make_request("POST", endpoint, json=payload) 

210 results = response.get("results", {}) 

211 

212 return results 

213 

214 def _get_award_type_category(self, award_type_codes: set[str]) -> str: 

215 """ 

216 Determine the award type category based on the award type codes. 

217 

218 Args: 

219 award_type_codes: Set of award type codes 

220 

221 Returns: 

222 The category name as used in the count endpoint response 

223 """ 

224 # Map config category names to API response names 

225 category_mapping = { 

226 "contracts": "contracts", 

227 "idvs": "idvs", 

228 "loans": "loans", 

229 "grants": "grants", 

230 "direct_payments": "direct_payments", 

231 "other_assistance": "other", 

232 } 

233 

234 for category_name, codes in AWARD_TYPE_GROUPS.items(): 

235 if award_type_codes & frozenset(codes.keys()): 

236 return category_mapping[category_name] 

237 

238 # Fail hard if no valid award type category is found 

239 raise ValidationError("No valid award type category found. ") 

240 

241 def _get_fields(self) -> list[str]: 

242 """ 

243 Determines the list of fields to request based on award type filters. 

244 

245 Returns different field sets depending on the award type codes: 

246 - Contracts (A, B, C, D): Include contract-specific fields 

247 - IDV (IDV_A, IDV_B, etc.): Include IDV-specific fields 

248 - Loans (07, 08): Include loan-specific fields 

249 - Grants/Assistance (02, 03, 04, 05, 06, 09, 10, 11, -1): Include assistance fields 

250 """ 

251 # Start with base fields from Award model 

252 base_fields = Award.SEARCH_FIELDS.copy() 

253 

254 # Get award type codes from filters 

255 award_types = self._get_award_type_codes() 

256 additional_fields = [] 

257 

258 # Check each category and add appropriate fields based on model 

259 for category_name, codes in AWARD_TYPE_GROUPS.items(): 

260 if award_types & frozenset(codes.keys()): 

261 if category_name == "contracts": 

262 # Use Contract.SEARCH_FIELDS but exclude base fields 

263 additional_fields.extend( 

264 [f for f in Contract.SEARCH_FIELDS if f not in base_fields] 

265 ) 

266 elif category_name == "idvs": 

267 # Use IDV.SEARCH_FIELDS but exclude base fields 

268 additional_fields.extend( 

269 [f for f in IDV.SEARCH_FIELDS if f not in base_fields] 

270 ) 

271 elif category_name == "loans": 

272 # Use Loan.SEARCH_FIELDS but exclude base fields 

273 additional_fields.extend( 

274 [f for f in Loan.SEARCH_FIELDS if f not in base_fields] 

275 ) 

276 elif category_name in ["grants", "direct_payments", "other_assistance"]: 

277 # Use Grant.SEARCH_FIELDS but exclude base fields 

278 additional_fields.extend( 

279 [f for f in Grant.SEARCH_FIELDS if f not in base_fields] 

280 ) 

281 

282 # Combine base fields with additional fields, removing duplicates 

283 all_fields = base_fields + additional_fields 

284 return list( 

285 dict.fromkeys(all_fields) 

286 ) # Remove duplicates while preserving order 

287 

288 # ========================================================================== 

289 # Filter Methods 

290 # ========================================================================== 

291 

292 def with_keywords(self, *keywords: str) -> AwardsSearch: 

293 """ 

294 Filter by a list of keywords. 

295 

296 Args: 

297 *keywords: One or more keywords to search for. 

298 

299 Returns: 

300 A new `AwardsSearch` instance with the filter applied. 

301 """ 

302 clone = self._clone() 

303 clone._filter_objects.append(KeywordsFilter(values=list(keywords))) 

304 return clone 

305 

306 def in_time_period( 

307 self, 

308 start_date: Union[datetime.date, str], 

309 end_date: Union[datetime.date, str], 

310 new_awards_only: bool = False, 

311 date_type: Optional[AwardDateType] = None, 

312 ) -> AwardsSearch: 

313 """ 

314 Filter by a specific date range. 

315 

316 Args: 

317 start_date: The start date of the period (datetime.date or string in "YYYY-MM-DD" format). 

318 end_date: The end date of the period (datetime.date or string in "YYYY-MM-DD" format). 

319 new_awards_only: If True, filters by awards with a start date within the given range. 

320 date_type: The type of date to filter on (e.g., action_date). 

321 

322 Returns: 

323 A new `AwardsSearch` instance with the filter applied. 

324 

325 Raises: 

326 ValidationError: If string dates are not in valid "YYYY-MM-DD" format. 

327 """ 

328 

329 # Parse string dates if needed 

330 if isinstance(start_date, str): 

331 try: 

332 start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d").date() 

333 except ValueError: 

334 raise ValidationError( 

335 f"Invalid start_date format: '{start_date}'. Expected 'YYYY-MM-DD'." 

336 ) 

337 

338 if isinstance(end_date, str): 

339 try: 

340 end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d").date() 

341 except ValueError: 

342 raise ValidationError( 

343 f"Invalid end_date format: '{end_date}'. Expected 'YYYY-MM-DD'." 

344 ) 

345 

346 # If convenience flag is set, use NEW_AWARDS_ONLY date type 

347 # and override any provided date_type 

348 if new_awards_only: 

349 date_type = AwardDateType.NEW_AWARDS_ONLY 

350 clone = self._clone() 

351 clone._filter_objects.append( 

352 TimePeriodFilter( 

353 start_date=start_date, end_date=end_date, date_type=date_type 

354 ) 

355 ) 

356 return clone 

357 

358 def for_fiscal_year( 

359 self, 

360 year: int, 

361 new_awards_only: bool = False, 

362 date_type: Optional[AwardDateType] = None, 

363 ) -> AwardsSearch: 

364 """ 

365 Adds a time period filter for a single US government fiscal year 

366 (October 1 to September 30). 

367 

368 Args: 

369 year: The fiscal year to filter by. 

370 new_awards_only: If True, filters by awards with a start date within the FY 

371 date_type: The type of date to filter on (e.g., action_date). 

372 

373 Returns: 

374 A new `AwardsSearch` instance with the fiscal year filter applied. 

375 """ 

376 start_date = datetime.date(year - 1, 10, 1) 

377 end_date = datetime.date(year, 9, 30) 

378 return self.in_time_period( 

379 start_date=start_date, 

380 end_date=end_date, 

381 new_awards_only=new_awards_only, 

382 date_type=date_type, 

383 ) 

384 

385 def with_place_of_performance_scope(self, scope: LocationScope) -> AwardsSearch: 

386 """ 

387 Filter awards by domestic or foreign place of performance. 

388 

389 Args: 

390 scope: The scope, either DOMESTIC or FOREIGN. 

391 

392 Returns: 

393 A new `AwardsSearch` instance with the filter applied. 

394 """ 

395 clone = self._clone() 

396 clone._filter_objects.append( 

397 LocationScopeFilter(key="place_of_performance_scope", scope=scope) 

398 ) 

399 return clone 

400 

401 def with_place_of_performance_locations(self, *locations: LocationSpec) -> AwardsSearch: 

402 """ 

403 Filter by one or more specific geographic places of performance. 

404 

405 Args: 

406 *locations: One or more `LocationSpec` objects. 

407 

408 Returns: 

409 A new `AwardsSearch` instance with the filter applied. 

410 """ 

411 clone = self._clone() 

412 clone._filter_objects.append( 

413 LocationFilter( 

414 key="place_of_performance_locations", locations=list(locations) 

415 ) 

416 ) 

417 return clone 

418 

419 def for_agency( 

420 self, 

421 name: str, 

422 agency_type: AgencyType = AgencyType.AWARDING, 

423 tier: AgencyTier = AgencyTier.TOPTIER, 

424 ) -> AwardsSearch: 

425 """ 

426 Filter by a specific awarding or funding agency. 

427 

428 Args: 

429 name: The name of the agency. 

430 agency_type: The type of agency (AWARDING or FUNDING). 

431 tier: The agency tier (TOPTIER or SUBTIER). 

432 

433 Returns: 

434 A new `AwardsSearch` instance with the filter applied. 

435 """ 

436 clone = self._clone() 

437 clone._filter_objects.append( 

438 AgencyFilter(agency_type=agency_type, tier=tier, name=name) 

439 ) 

440 return clone 

441 

442 def with_recipient_search_text(self, *search_terms: str) -> AwardsSearch: 

443 """ 

444 Filter by recipient name, UEI, or DUNS. 

445 

446 Args: 

447 *search_terms: Text to search for across recipient identifiers. 

448 

449 Returns: 

450 A new `AwardsSearch` instance with the filter applied. 

451 """ 

452 clone = self._clone() 

453 clone._filter_objects.append( 

454 SimpleListFilter(key="recipient_search_text", values=list(search_terms)) 

455 ) 

456 return clone 

457 

458 def with_recipient_scope(self, scope: LocationScope) -> AwardsSearch: 

459 """ 

460 Filter recipients by domestic or foreign scope. 

461 

462 Args: 

463 scope: The scope, either DOMESTIC or FOREIGN. 

464 

465 Returns: 

466 A new `AwardsSearch` instance with the filter applied. 

467 """ 

468 clone = self._clone() 

469 clone._filter_objects.append( 

470 LocationScopeFilter(key="recipient_scope", scope=scope) 

471 ) 

472 return clone 

473 

474 def with_recipient_locations(self, *locations: LocationSpec) -> AwardsSearch: 

475 """ 

476 Filter by one or more specific recipient locations. 

477 

478 Args: 

479 *locations: One or more `LocationSpec` objects. 

480 

481 Returns: 

482 A new `AwardsSearch` instance with the filter applied. 

483 """ 

484 clone = self._clone() 

485 clone._filter_objects.append( 

486 LocationFilter(key="recipient_locations", locations=list(locations)) 

487 ) 

488 return clone 

489 

490 def with_recipient_types(self, *type_names: str) -> AwardsSearch: 

491 """ 

492 Filter by one or more recipient or business types. 

493 

494 Args: 

495 *type_names: The names of the recipient types (e.g., "small_business"). 

496 

497 Returns: 

498 A new `AwardsSearch` instance with the filter applied. 

499 """ 

500 clone = self._clone() 

501 clone._filter_objects.append( 

502 SimpleListFilter(key="recipient_type_names", values=list(type_names)) 

503 ) 

504 return clone 

505 

506 def with_award_types(self, *award_codes: str) -> AwardsSearch: 

507 """ 

508 Filter by one or more award type codes. This filter is **required**. 

509 

510 Args: 

511 *award_codes: A sequence of award type codes (e.g., "A", "B", "02"). 

512 

513 Returns: 

514 A new `AwardsSearch` instance with the filter applied. 

515 

516 Raises: 

517 ValidationError: If mixing different award type categories. 

518 """ 

519 new_codes = set(award_codes) 

520 self._validate_single_award_type_category(new_codes) 

521 

522 clone = self._clone() 

523 clone._filter_objects.append( 

524 SimpleListFilter(key="award_type_codes", values=list(award_codes)) 

525 ) 

526 return clone 

527 

528 def contracts(self) -> AwardsSearch: 

529 """ 

530 Filter to search for contract awards only (types A, B, C, D). 

531 

532 Returns: 

533 A new `AwardsSearch` instance configured for contract awards. 

534 """ 

535 return self.with_award_types(*CONTRACT_CODES) 

536 

537 def idvs(self) -> AwardsSearch: 

538 """ 

539 Filter to search for IDV awards only (types IDV_A, IDV_B, etc.). 

540 

541 Returns: 

542 A new `AwardsSearch` instance configured for IDV awards. 

543 """ 

544 return self.with_award_types(*IDV_CODES) 

545 

546 def loans(self) -> AwardsSearch: 

547 """ 

548 Filter to search for loan awards only (types 07, 08). 

549 

550 Returns: 

551 A new `AwardsSearch` instance configured for loan awards. 

552 """ 

553 return self.with_award_types(*LOAN_CODES) 

554 

555 def grants(self) -> AwardsSearch: 

556 """ 

557 Filter to search for grant and assistance awards only (types 02, 03, 04, 05). 

558 

559 Returns: 

560 A new `AwardsSearch` instance configured for grant/assistance awards. 

561 """ 

562 return self.with_award_types(*GRANT_CODES) 

563 

564 def direct_payments(self) -> AwardsSearch: 

565 """ 

566 Filter to search for direct payment awards only (types 06, 10). 

567 

568 Returns: 

569 A new `AwardsSearch` instance configured for direct payment awards. 

570 """ 

571 return self.with_award_types(*DIRECT_PAYMENT_CODES) 

572 

573 def other(self) -> AwardsSearch: 

574 """ 

575 Filter to search for other assistance awards only (types 09, 11, -1). 

576 

577 Returns: 

578 A new `AwardsSearch` instance configured for other assistance awards. 

579 """ 

580 return self.with_award_types(*OTHER_CODES) 

581 

582 def with_award_ids(self, *award_ids: str) -> AwardsSearch: 

583 """ 

584 Filter by specific award IDs (FAIN, PIID, URI). 

585 

586 Args: 

587 *award_ids: The exact award IDs to search for. 

588 

589 Returns: 

590 A new `AwardsSearch` instance with the filter applied. 

591 """ 

592 clone = self._clone() 

593 clone._filter_objects.append( 

594 SimpleListFilter(key="award_ids", values=list(award_ids)) 

595 ) 

596 return clone 

597 

598 def with_award_amounts(self, *amounts: AwardAmount) -> AwardsSearch: 

599 """ 

600 Filter by one or more award amount ranges. 

601 

602 Args: 

603 *amounts: One or more `AwardAmount` objects defining the ranges. 

604 

605 Returns: 

606 A new `AwardsSearch` instance with the filter applied. 

607 """ 

608 clone = self._clone() 

609 clone._filter_objects.append(AwardAmountFilter(amounts=list(amounts))) 

610 return clone 

611 

612 def with_cfda_numbers(self, *program_numbers: str) -> AwardsSearch: 

613 """ 

614 Filter by one or more CFDA program numbers. 

615 

616 Args: 

617 *program_numbers: The CFDA numbers to filter by. 

618 

619 Returns: 

620 A new `AwardsSearch` instance with the filter applied. 

621 """ 

622 clone = self._clone() 

623 clone._filter_objects.append( 

624 SimpleListFilter(key="program_numbers", values=list(program_numbers)) 

625 ) 

626 return clone 

627 

628 def with_naics_codes( 

629 self, 

630 require: Optional[list[str]] = None, 

631 exclude: Optional[list[str]] = None, 

632 ) -> AwardsSearch: 

633 """ 

634 Filter by NAICS codes, including or excluding specific codes. 

635 

636 Args: 

637 require: A list of NAICS codes to require. 

638 exclude: A list of NAICS codes to exclude. 

639 

640 Returns: 

641 A new `AwardsSearch` instance with the filter applied. 

642 """ 

643 clone = self._clone() 

644 # The API expects a list of lists, but for NAICS, each list contains one element. 

645 require_list = [[code] for code in require] if require else [] 

646 exclude_list = [[code] for code in exclude] if exclude else [] 

647 clone._filter_objects.append( 

648 TieredCodeFilter( 

649 key="naics_codes", require=require_list, exclude=exclude_list 

650 ) 

651 ) 

652 return clone 

653 

654 def with_psc_codes( 

655 self, 

656 require: Optional[list[list[str]]] = None, 

657 exclude: Optional[list[list[str]]] = None, 

658 ) -> AwardsSearch: 

659 """ 

660 Filter by Product and Service Codes (PSC), including or excluding codes. 

661 

662 Args: 

663 require: A list of PSC code paths to require. 

664 exclude: A list of PSC code paths to exclude. 

665 

666 Returns: 

667 A new `AwardsSearch` instance with the filter applied. 

668 """ 

669 clone = self._clone() 

670 clone._filter_objects.append( 

671 TieredCodeFilter( 

672 key="psc_codes", 

673 require=require or [], 

674 exclude=exclude or [], 

675 ) 

676 ) 

677 return clone 

678 

679 def with_contract_pricing_types(self, *type_codes: str) -> AwardsSearch: 

680 """ 

681 Filter by one or more contract pricing type codes. 

682 

683 Args: 

684 *type_codes: The contract pricing type codes. 

685 

686 Returns: 

687 A new `AwardsSearch` instance with the filter applied. 

688 """ 

689 clone = self._clone() 

690 clone._filter_objects.append( 

691 SimpleListFilter(key="contract_pricing_type_codes", values=list(type_codes)) 

692 ) 

693 return clone 

694 

695 def with_set_aside_types(self, *type_codes: str) -> AwardsSearch: 

696 """ 

697 Filter by one or more set-aside type codes. 

698 

699 Args: 

700 *type_codes: The set-aside type codes. 

701 

702 Returns: 

703 A new `AwardsSearch` instance with the filter applied. 

704 """ 

705 clone = self._clone() 

706 clone._filter_objects.append( 

707 SimpleListFilter(key="set_aside_type_codes", values=list(type_codes)) 

708 ) 

709 return clone 

710 

711 def with_extent_competed_types(self, *type_codes: str) -> AwardsSearch: 

712 """ 

713 Filter by one or more extent competed type codes. 

714 

715 Args: 

716 *type_codes: The extent competed type codes. 

717 

718 Returns: 

719 A new `AwardsSearch` instance with the filter applied. 

720 """ 

721 clone = self._clone() 

722 clone._filter_objects.append( 

723 SimpleListFilter(key="extent_competed_type_codes", values=list(type_codes)) 

724 ) 

725 return clone 

726 

727 def with_tas_codes( 

728 self, 

729 require: Optional[list[list[str]]] = None, 

730 exclude: Optional[list[list[str]]] = None, 

731 ) -> AwardsSearch: 

732 """ 

733 Filter by Treasury Account Symbols (TAS), including or excluding codes. 

734 

735 Args: 

736 require: A list of TAS code paths to require. 

737 exclude: A list of TAS code paths to exclude. 

738 

739 Returns: 

740 A new `AwardsSearch` instance with the filter applied. 

741 """ 

742 clone = self._clone() 

743 clone._filter_objects.append( 

744 TieredCodeFilter( 

745 key="tas_codes", 

746 require=require or [], 

747 exclude=exclude or [], 

748 ) 

749 ) 

750 return clone 

751 

752 def with_treasury_account_components( 

753 self, *components: dict[str, str] 

754 ) -> AwardsSearch: 

755 """ 

756 Filter by specific components of a Treasury Account. 

757 

758 Args: 

759 *components: Dictionaries representing TAS components (aid, main, etc.). 

760 

761 Returns: 

762 A new `AwardsSearch` instance with the filter applied. 

763 """ 

764 clone = self._clone() 

765 clone._filter_objects.append( 

766 TreasuryAccountComponentsFilter(components=list(components)) 

767 ) 

768 return clone 

769 

770 def with_def_codes(self, *def_codes: str) -> AwardsSearch: 

771 """ 

772 Filter by one or more Disaster Emergency Fund (DEF) codes. 

773 

774 Args: 

775 *def_codes: The DEF codes (e.g., "L", "M", "N"). 

776 

777 Returns: 

778 A new `AwardsSearch` instance with the filter applied. 

779 """ 

780 clone = self._clone() 

781 clone._filter_objects.append( 

782 SimpleListFilter(key="def_codes", values=list(def_codes)) 

783 ) 

784 return clone