Coverage for src/usaspending/resources/award_resource.py: 95%
40 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-03 17:15 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-03 17:15 -0700
1"""Award resource implementation."""
3from __future__ import annotations
4from typing import TYPE_CHECKING, Optional
6from .base_resource import BaseResource
7from ..logging_config import USASpendingLogger
9if TYPE_CHECKING:
10 from ..queries.awards_search import AwardsSearch
11 from ..models.award import Award
13logger = USASpendingLogger.get_logger(__name__)
16class AwardResource(BaseResource):
17 """Resource for award-related operations.
19 Provides access to award search and retrieval endpoints.
20 """
22 def find_by_generated_id(self, generated_award_id: str) -> "Award":
23 """Retrieve a single award by the system's internally generated
24 award entry ID (e.g. "CONT_AWD_80GSFC18C0008_8000_-NONE-_-NONE-")
26 Args:
27 generated_award_id: Unique award identifier
29 Returns:
30 Award model instance
32 Raises:
33 : If generated_award_id is invalid
34 APIError: If award not found
35 """
36 logger.debug(f"Retrieving award by ID: {generated_award_id}")
37 from ..queries.award_query import AwardQuery
39 return AwardQuery(self._client).find_by_generated_id(generated_award_id)
41 def find_by_award_id(self, award_id: str) -> Optional["Award"]:
42 """Find an award by its PIID or FAIN unique identifier.
43 Args:
44 award_id: Unique identifier for the award (PIID or FAIN)
46 Returns:
47 Award model instance if found, otherwise None
48 """
49 logger.debug(f"Finding award by ID: {award_id}")
50 from ..queries.awards_search import AwardsSearch
52 # Get counts by award type
53 search_result = AwardsSearch(self._client).with_award_ids(award_id).count_awards_by_type()
55 # Find which type has exactly one result
56 matching_types = [(award_type, count) for award_type, count in search_result.items() if count == 1]
58 if len(matching_types) != 1:
59 total_awards = sum(count for count in search_result.values() if count > 0)
60 if total_awards == 0:
61 logger.info(f"No awards found for ID {award_id}")
62 else:
63 logger.warning(f"Expected exactly one award for ID {award_id}, found {total_awards} awards across {len(matching_types)} types")
64 logger.debug(f"Search result: {search_result}")
65 return None
67 award_type, _ = matching_types[0]
68 logger.info(f"Found 1 award of type {award_type} for ID {award_id}")
70 # Map API response keys to method names
71 method_mapping = {
72 "contracts": "contracts",
73 "grants": "grants",
74 "idvs": "idvs",
75 "loans": "loans",
76 "direct_payments": "direct_payments",
77 "other": "other"
78 }
80 method_name = method_mapping.get(award_type)
81 if not method_name:
82 logger.error(f"Unknown award type from API: {award_type}")
83 return None
85 # Create search and apply the appropriate filter
86 awards_search = AwardsSearch(self._client)
87 if hasattr(awards_search, method_name):
88 logger.debug(f"Calling .{method_name}() method on AwardsSearch")
89 awards_search = getattr(awards_search, method_name)()
90 return awards_search.with_award_ids(award_id).first()
91 else:
92 logger.error(f"Method {method_name} not found on AwardsSearch")
93 return None
98 def search(self) -> AwardsSearch:
99 """Create a new award search query builder.
101 Returns:
102 AwardSearch query builder for chaining filters
104 Example:
105 >>> awards = client.awards.search()
106 ... .for_agency("NASA")
107 ... .in_state("TX")
108 ... .fiscal_years(2023, 2024)
109 ... .limit(10)
110 """
111 logger.debug("Creating new AwardsSearch query builder")
112 from ..queries.awards_search import AwardsSearch
114 return AwardsSearch(self._client)