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
« 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
12class SubAward(ClientAwareModel):
13 """Model representing a subaward from USASpending data.
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.
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
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.
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)
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 ]
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 ]
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
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 )
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
122 return recipient
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
134 @property
135 def id(self) -> Optional[str]:
136 """Internal subaward identifier."""
137 return self.raw.get("internal_id")
139 @property
140 def sub_award_id(self) -> Optional[str]:
141 """Subaward identifier."""
142 return self.raw.get("Sub-Award ID")
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")
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
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"))
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"))
165 @property
166 def awarding_agency(self) -> Optional[str]:
167 """Name of the awarding agency."""
168 return self.raw.get("Awarding Agency")
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")
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")
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
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")
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
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")
202 @property
203 def sub_recipient_uei(self) -> Optional[str]:
204 """Sub-recipient Unique Entity Identifier."""
205 return self.raw.get("Sub-Recipient UEI")
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")
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")
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
223 @property
224 def naics(self) -> Optional[str]:
225 """NAICS code for contract subawards."""
226 return self.raw.get("NAICS")
228 @property
229 def psc(self) -> Optional[str]:
230 """Product Service Code for contract subawards."""
231 return self.raw.get("PSC")
233 @property
234 def assistance_listing(self) -> Optional[str]:
235 """Assistance listing for grant subawards."""
236 return self.raw.get("Assistance Listing")
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}>"
242 @property
243 def name(self) -> Optional[str]:
244 """Alias for sub_awardee_name."""
245 return self.sub_awardee_name
247 @property
248 def amount(self) -> Optional[float]:
249 """Alias for sub_award_amount."""
250 return self.sub_award_amount
252 @property
253 def description(self) -> Optional[str]:
254 """Alias for sub_award_description."""
255 return self.sub_award_description
257 @property
258 def award_date(self) -> Optional[datetime]:
259 """Alias for sub_award_date."""
260 return self.sub_award_date