Coverage for src/usaspending/models/grant.py: 97%

60 statements  

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

1"""Grant award model for USASpending data.""" 

2 

3from __future__ import annotations 

4from typing import Dict, Any, Optional, List, TYPE_CHECKING 

5from functools import cached_property 

6 

7from .award import Award 

8from ..utils.formatter import to_float 

9 

10if TYPE_CHECKING: 

11 from ..queries.subawards_search import SubAwardsSearch 

12 

13class Grant(Award): 

14 """Grant and assistance award types.""" 

15 

16 TYPE_FIELDS = [ 

17 "fain", 

18 "uri", 

19 "record_type", 

20 "cfda_info", 

21 "cfda_number", 

22 "primary_cfda_info", 

23 "sai_number", 

24 "funding_opportunity", 

25 "non_federal_funding", 

26 "total_funding", 

27 "transaction_obligated_amount", 

28 ] 

29 

30 SEARCH_FIELDS = Award.SEARCH_FIELDS + [ 

31 "Start Date", 

32 "End Date", 

33 "Award Amount", 

34 "Total Outlays", 

35 "Award Type", 

36 "SAI Number", 

37 "CFDA Number", 

38 "Assistance Listings", 

39 "primary_assistance_listing", 

40 ] 

41 

42 @property 

43 def fain(self) -> Optional[str]: 

44 """ 

45 An identification code assigned to each financial assistance award tracking 

46 purposes. The FAIN is tied to that award (and all future modifications to that 

47 award) throughout the award's life. Each FAIN is assigned by an agency. Within 

48 an agency, FAIN are unique: each new award must be issued a new FAIN. FAIN 

49 stands for Federal Award Identification Number, though the digits are letters, 

50 not numbers. 

51 """ 

52 return self._lazy_get("fain") 

53 

54 @property 

55 def uri(self) -> Optional[str]: 

56 """The uri of the award""" 

57 return self._lazy_get("uri") 

58 

59 @property 

60 def record_type(self) -> Optional[int]: 

61 """Grant record type identifier.""" 

62 return self._lazy_get("record_type") 

63 

64 @property 

65 def cfda_info(self) -> List[Dict[str, Any]]: 

66 """Catalog of Federal Domestic Assistance information for grants.""" 

67 return self._lazy_get("cfda_info", "Assistance Listings", default=[]) 

68 

69 @property 

70 def cfda_number(self) -> Optional[str]: 

71 """Primary CFDA number for grants.""" 

72 primary_info = self._lazy_get("primary_cfda_info", "primary_assistance_listing") 

73 if primary_info: 

74 return primary_info.get("cfda_number") 

75 

76 cfda_list = self._lazy_get("cfda_info", "Assistance Listings") 

77 if cfda_list: 

78 return cfda_list[0].get("cfda_number") 

79 

80 return self._lazy_get("cfda_number", "CFDA Number") 

81 

82 @property 

83 def primary_cfda_info(self) -> Optional[Dict[str, Any]]: 

84 """Primary CFDA program information.""" 

85 return self._lazy_get("primary_cfda_info", "primary_assistance_listing") 

86 

87 @property 

88 def sai_number(self) -> Optional[str]: 

89 """System for Award Identification (SAI) number for grants.""" 

90 return self._lazy_get("sai_number", "SAI Number") 

91 

92 @cached_property 

93 def funding_opportunity(self) -> Optional[Dict[str, Any]]: 

94 """Funding opportunity details for grants.""" 

95 return self._lazy_get("funding_opportunity") 

96 

97 @property 

98 def non_federal_funding(self) -> Optional[float]: 

99 """A summation of this award's transactions' non-federal funding amount""" 

100 return to_float(self._lazy_get("non_federal_funding", default=None)) 

101 

102 @property 

103 def total_funding(self) -> Optional[float]: 

104 """The sum of the federal action obligations and the Non-Federal funding amount.""" 

105 return to_float(self._lazy_get("total_funding", default=None)) 

106 

107 @property 

108 def transaction_obligated_amount(self) -> Optional[float]: 

109 """Transaction-level obligated amount.""" 

110 return to_float(self._lazy_get("transaction_obligated_amount", default=None)) 

111 

112 @property 

113 def total_subsidy_cost(self) -> Optional[float]: 

114 """Total subsidy cost for this award.""" 

115 return to_float(self._lazy_get("total_subsidy_cost", default=None)) 

116 

117 @property 

118 def base_exercised_options(self) -> Optional[float]: 

119 """The total amount obligated for the base and exercised options of this award.""" 

120 return to_float(self._lazy_get("base_exercised_options", default=None)) 

121 

122 @property 

123 def base_and_all_options(self) -> Optional[float]: 

124 """The total amount obligated for the base and all options of this award.""" 

125 return to_float(self._lazy_get("base_and_all_options", default=None)) 

126 @property 

127 def subawards(self) -> "SubAwardsSearch": 

128 """Get subawards query builder for this grant award with appropriate award type filters. 

129 

130 Returns a SubAwardsSearch object that can be further filtered and chained. 

131 Automatically applies grant award type filters. 

132 

133 Examples: 

134 >>> grant.subawards.count() # Get count without loading all data 

135 >>> grant.subawards.limit(10).all() # Get first 10 subawards 

136 >>> list(grant.subawards) # Iterate through all subawards 

137 """ 

138 from ..config import GRANT_CODES 

139 

140 # Grant subawards use grant award types only 

141 # Note: Due to validation in AwardsSearch, we cannot mix grant/direct_payment/other categories 

142 return (self._client.subawards 

143 .for_award(self.generated_unique_award_id) 

144 .with_award_types(*GRANT_CODES))