Coverage for src/usaspending/models/subaward.py: 99%

112 statements  

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

1from __future__ import annotations 

2from typing import Dict, Any, Optional 

3from datetime import datetime 

4from functools import cached_property 

5from ..utils.formatter import contracts_titlecase, smart_sentence_case, to_float, to_date 

6from .base_model import ClientAwareModel 

7from .recipient import Recipient 

8from .location import Location 

9from .award import Award 

10from ..client import USASpending 

11 

12class SubAward(ClientAwareModel): 

13 """Model representing a subaward from USASpending data. 

14  

15 Subawards are secondary awards issued by prime recipients of federal contracts 

16 or grants to other entities (subrecipients) to carry out part of the federal 

17 program or project. They represent the flow of federal funds from prime  

18 recipients to subrecipients. 

19  

20 Key characteristics: 

21 - Issued by prime recipients, not directly by federal agencies 

22 - Subject to federal regulations and oversight requirements 

23 - Must be reported when exceeding $30,000 (per FFATA requirements) 

24 - Include both contract subawards (subcontracts) and grant subawards 

25  

26 Subawards enable prime recipients to delegate portions of work while  

27 maintaining overall responsibility for the federal award's success. 

28 They extend the reach of federal funding through multiple tiers of 

29 organizations. 

30  

31 Example: 

32 >>> # Find subawards for a specific prime award 

33 >>> subawards = client.subawards.search() 

34 ... .for_prime_award_piid("80NSSC21C0123") 

35 ... .limit(10) 

36 >>> for subaward in subawards: 

37 ... print(f"{subaward.sub_awardee_name}: ${subaward.sub_award_amount:,.2f}") 

38 """ 

39 def __init__(self, data: Dict[str, Any], client: Optional[USASpending] = None): 

40 super().__init__(data, client) 

41 

42 # Contract Subaward fields 

43 CONTRACT_SUBAWARD_FIELDS = [ 

44 "Awarding Agency", 

45 "Awarding Sub Agency", 

46 "NAICS", 

47 "Prime Award ID", 

48 "prime_award_recipient_id", 

49 "Prime Award Recipient UEI", 

50 "Prime Recipient Name", 

51 "PSC", 

52 "Sub-Award Amount", 

53 "Sub-Award Date", 

54 "Sub-Award Description", 

55 "Sub-Award ID", 

56 "Sub-Award Primary Place of Performance", 

57 "sub_award_recipient_id", 

58 "Sub-Award Type", 

59 "Sub-Awardee Name", 

60 "Sub-Recipient Location", 

61 "Sub-Recipient UEI", 

62 "prime_award_generated_internal_id", 

63 "prime_award_internal_id", 

64 "internal_id", 

65 "subaward_description_sorted" 

66 ] 

67 

68 # Grant Subaward fields 

69 GRANT_SUBAWARD_FIELDS = [ 

70 "Assistance Listing", 

71 "Awarding Agency", 

72 "Awarding Sub Agency", 

73 "Prime Award ID", 

74 "prime_award_recipient_id", 

75 "Prime Award Recipient UEI", 

76 "Prime Recipient Name", 

77 "Sub-Award Amount", 

78 "Sub-Award Date", 

79 "Sub-Award Description", 

80 "Sub-Award ID", 

81 "Sub-Award Primary Place of Performance", 

82 "sub_award_recipient_id", 

83 "Sub-Award Type", 

84 "Sub-Awardee Name", 

85 "Sub-Recipient Location", 

86 "Sub-Recipient UEI", 

87 "prime_award_generated_internal_id", 

88 "prime_award_internal_id", 

89 "internal_id", 

90 "subaward_description_sorted" 

91 ] 

92 

93 @cached_property 

94 def place_of_performance(self) -> Optional[Location]: 

95 """Place of performance details for the subaward.""" 

96 pop_data = self.raw.get("Sub-Award Primary Place of Performance") 

97 if pop_data: 

98 return Location(pop_data) 

99 else: 

100 return None 

101 

102 @cached_property 

103 def recipient(self) -> Optional[Recipient]: 

104 """Sub-award recipient and location.""" 

105 recipient = Recipient( 

106 { 

107 "recipient_name": self.get_value(["Sub-Awardee Name"]), 

108 "recipient_unique_id": self.get_value(["sub_award_recipient_id"]), 

109 "recipient_uei": self.get_value(["Sub-Recipient UEI"]), 

110 }, 

111 client=self._client, 

112 ) 

113 

114 # Add location if available to avoid separate API call 

115 if isinstance(self.get_value(["Sub-Recipient Location"]), dict): 

116 location_data = self._data.get("Sub-Recipient Location") 

117 recipient_location = ( 

118 Location(location_data) if location_data else None 

119 ) 

120 recipient.location = recipient_location 

121 

122 return recipient 

123 

124 @cached_property 

125 def parent_award(self) -> Optional[Award]: 

126 if self.prime_award_generated_internal_id: 

127 return Award( 

128 {"generated_unique_award_id": self.prime_award_generated_internal_id}, 

129 client=self._client, 

130 ) 

131 else: 

132 return None 

133 

134 @property 

135 def id(self) -> Optional[str]: 

136 """Internal subaward identifier.""" 

137 return self.raw.get("internal_id") 

138 

139 @property 

140 def sub_award_id(self) -> Optional[str]: 

141 """Subaward identifier.""" 

142 return self.raw.get("Sub-Award ID") 

143 

144 @property 

145 def sub_award_type(self) -> Optional[str]: 

146 """Type of subaward (e.g., sub-contract, sub-grant).""" 

147 return self.raw.get("Sub-Award Type") 

148 

149 @property 

150 def sub_awardee_name(self) -> Optional[str]: 

151 """Name of the subaward recipient.""" 

152 name = self.raw.get("Sub-Awardee Name") 

153 return contracts_titlecase(name) if name else None 

154 

155 @property 

156 def sub_award_date(self) -> Optional[datetime]: 

157 """Date the subaward was issued.""" 

158 return to_date(self.raw.get("Sub-Award Date")) 

159 

160 @property 

161 def sub_award_amount(self) -> Optional[float]: 

162 """Amount of the subaward.""" 

163 return to_float(self.raw.get("Sub-Award Amount")) 

164 

165 @property 

166 def awarding_agency(self) -> Optional[str]: 

167 """Name of the awarding agency.""" 

168 return self.raw.get("Awarding Agency") 

169 

170 @property 

171 def awarding_sub_agency(self) -> Optional[str]: 

172 """Name of the awarding sub-agency.""" 

173 return self.raw.get("Awarding Sub Agency") 

174 

175 @property 

176 def prime_award_id(self) -> Optional[str]: 

177 """Prime award identifier (PIID/FAIN/URI).""" 

178 return self.raw.get("Prime Award ID") 

179 

180 @property 

181 def prime_recipient_name(self) -> Optional[str]: 

182 """Name of the prime award recipient.""" 

183 name = self.raw.get("Prime Recipient Name") 

184 return contracts_titlecase(name) if name else None 

185 

186 @property 

187 def prime_award_recipient_id(self) -> Optional[str]: 

188 """Prime award recipient identifier.""" 

189 return self.raw.get("prime_award_recipient_id") 

190 

191 @property 

192 def sub_award_description(self) -> Optional[str]: 

193 """Description of the subaward.""" 

194 desc = self.raw.get("Sub-Award Description") 

195 return smart_sentence_case(desc) if desc else None 

196 

197 @property 

198 def subaward_description_sorted(self) -> Optional[str]: 

199 """Sorted version of the subaward description for API internal use.""" 

200 return self.raw.get("subaward_description_sorted") 

201 

202 @property 

203 def sub_recipient_uei(self) -> Optional[str]: 

204 """Sub-recipient Unique Entity Identifier.""" 

205 return self.raw.get("Sub-Recipient UEI") 

206 

207 @property 

208 def prime_award_recipient_uei(self) -> Optional[str]: 

209 """Prime award recipient Unique Entity Identifier.""" 

210 return self.raw.get("Prime Award Recipient UEI") 

211 

212 @property 

213 def prime_award_generated_internal_id(self) -> Optional[str]: 

214 """USASpending-generated unique identifier for the prime award.""" 

215 return self.raw.get("prime_award_generated_internal_id") 

216 

217 @property 

218 def prime_award_internal_id(self) -> Optional[int]: 

219 """Internal database ID for the prime award.""" 

220 val = self.raw.get("prime_award_internal_id") 

221 return int(val) if val is not None else None 

222 

223 @property 

224 def naics(self) -> Optional[str]: 

225 """NAICS code for contract subawards.""" 

226 return self.raw.get("NAICS") 

227 

228 @property 

229 def psc(self) -> Optional[str]: 

230 """Product Service Code for contract subawards.""" 

231 return self.raw.get("PSC") 

232 

233 @property 

234 def assistance_listing(self) -> Optional[str]: 

235 """Assistance listing for grant subawards.""" 

236 return self.raw.get("Assistance Listing") 

237 

238 def __repr__(self) -> str: 

239 """String representation of SubAward.""" 

240 return f"<SubAward {self.sub_award_id or '?'} {self.sub_awardee_name or '?'} ${self.sub_award_amount or 0:,.2f}>" 

241 

242 @property 

243 def name(self) -> Optional[str]: 

244 """Alias for sub_awardee_name.""" 

245 return self.sub_awardee_name 

246 

247 @property 

248 def amount(self) -> Optional[float]: 

249 """Alias for sub_award_amount.""" 

250 return self.sub_award_amount 

251 

252 @property 

253 def description(self) -> Optional[str]: 

254 """Alias for sub_award_description.""" 

255 return self.sub_award_description 

256 

257 @property 

258 def award_date(self) -> Optional[datetime]: 

259 """Alias for sub_award_date.""" 

260 return self.sub_award_date