usaspending-orm 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- usaspending/__init__.py +144 -0
- usaspending/cli/__init__.py +2 -0
- usaspending/cli/download_award.py +113 -0
- usaspending/client.py +550 -0
- usaspending/config.py +119 -0
- usaspending/download/__init__.py +8 -0
- usaspending/download/job.py +219 -0
- usaspending/download/manager.py +140 -0
- usaspending/exceptions.py +71 -0
- usaspending/logging_config.py +310 -0
- usaspending/models/__init__.py +86 -0
- usaspending/models/agency.py +631 -0
- usaspending/models/award.py +933 -0
- usaspending/models/award_factory.py +72 -0
- usaspending/models/award_types.py +114 -0
- usaspending/models/base_model.py +214 -0
- usaspending/models/contract.py +147 -0
- usaspending/models/district_spending.py +70 -0
- usaspending/models/download.py +74 -0
- usaspending/models/funding.py +182 -0
- usaspending/models/grant.py +147 -0
- usaspending/models/idv.py +165 -0
- usaspending/models/lazy_record.py +82 -0
- usaspending/models/loan.py +85 -0
- usaspending/models/location.py +125 -0
- usaspending/models/period_of_performance.py +43 -0
- usaspending/models/recipient.py +204 -0
- usaspending/models/recipient_business_categories.py +364 -0
- usaspending/models/recipient_spending.py +58 -0
- usaspending/models/spending.py +70 -0
- usaspending/models/state_spending.py +43 -0
- usaspending/models/subaward.py +270 -0
- usaspending/models/subtier_agency.py +98 -0
- usaspending/models/transaction.py +89 -0
- usaspending/queries/__init__.py +87 -0
- usaspending/queries/agencies_search.py +196 -0
- usaspending/queries/agency_award_summary.py +134 -0
- usaspending/queries/agency_query.py +102 -0
- usaspending/queries/award_query.py +40 -0
- usaspending/queries/awarding_agencies_search.py +16 -0
- usaspending/queries/awards_search.py +602 -0
- usaspending/queries/filters.py +437 -0
- usaspending/queries/funding_agencies_search.py +20 -0
- usaspending/queries/funding_search.py +180 -0
- usaspending/queries/query_builder.py +1430 -0
- usaspending/queries/recipient_query.py +66 -0
- usaspending/queries/recipients_search.py +167 -0
- usaspending/queries/single_resource_base.py +83 -0
- usaspending/queries/spending_search.py +261 -0
- usaspending/queries/sub_agency_query.py +177 -0
- usaspending/queries/subawards_search.py +196 -0
- usaspending/queries/transactions_search.py +213 -0
- usaspending/resources/__init__.py +17 -0
- usaspending/resources/agency_resource.py +105 -0
- usaspending/resources/award_resource.py +119 -0
- usaspending/resources/base_resource.py +31 -0
- usaspending/resources/download_resource.py +102 -0
- usaspending/resources/funding_resource.py +41 -0
- usaspending/resources/recipients_resource.py +104 -0
- usaspending/resources/spending_resource.py +45 -0
- usaspending/resources/subawards_resource.py +68 -0
- usaspending/resources/transactions_resource.py +39 -0
- usaspending/utils/__init__.py +6 -0
- usaspending/utils/formatter.py +550 -0
- usaspending/utils/rate_limit.py +135 -0
- usaspending/utils/retry.py +257 -0
- usaspending/utils/special_cases.yaml +155 -0
- usaspending_orm-0.6.0.dist-info/LICENSE +18 -0
- usaspending_orm-0.6.0.dist-info/METADATA +324 -0
- usaspending_orm-0.6.0.dist-info/RECORD +71 -0
- usaspending_orm-0.6.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
"""Agency model for USASpending data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Dict, Any, Optional, List, TYPE_CHECKING
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from ..utils.formatter import to_decimal, to_int, to_date
|
|
8
|
+
from datetime import date
|
|
9
|
+
from functools import cached_property
|
|
10
|
+
from .lazy_record import LazyRecord
|
|
11
|
+
from ..logging_config import USASpendingLogger
|
|
12
|
+
from .award_types import (
|
|
13
|
+
CONTRACT_CODES,
|
|
14
|
+
GRANT_CODES,
|
|
15
|
+
IDV_CODES,
|
|
16
|
+
LOAN_CODES,
|
|
17
|
+
DIRECT_PAYMENT_CODES,
|
|
18
|
+
OTHER_CODES,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from ..client import USASpendingClient
|
|
23
|
+
from ..queries.awards_search import AwardsSearch
|
|
24
|
+
from .subtier_agency import SubTierAgency
|
|
25
|
+
|
|
26
|
+
logger = USASpendingLogger.get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Create data class for def_codes
|
|
30
|
+
@dataclass
|
|
31
|
+
class DefCode:
|
|
32
|
+
"""Disaster Emergency Fund Code (DEFC) data structure.
|
|
33
|
+
|
|
34
|
+
Represents a disaster emergency fund code associated with an agency,
|
|
35
|
+
containing legislative and reference information.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
code: The DEFC code identifier.
|
|
39
|
+
public_law: Associated public law reference.
|
|
40
|
+
title: Optional descriptive title of the fund.
|
|
41
|
+
urls: Optional list of related URLs for additional information.
|
|
42
|
+
disaster: Optional disaster category or description.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
code: str
|
|
46
|
+
public_law: str
|
|
47
|
+
title: Optional[str] = None
|
|
48
|
+
urls: Optional[List[str]] = None
|
|
49
|
+
disaster: Optional[str] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Agency(LazyRecord):
|
|
53
|
+
"""Rich wrapper around a USAspending toptier agency record.
|
|
54
|
+
|
|
55
|
+
This model represents a toptier agency with its essential properties.
|
|
56
|
+
For subtier agency information, use the SubTierAgency model separately.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
data: Dict[str, Any],
|
|
62
|
+
client: USASpendingClient,
|
|
63
|
+
subtier_data: Optional[Dict[str, Any]] = None,
|
|
64
|
+
):
|
|
65
|
+
"""Initialize Agency instance.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
data: Toptier agency data merged with top-level agency fields
|
|
69
|
+
client: USASpendingClient client instance
|
|
70
|
+
subtier_data: Optional subtier agency data for subtier_agency property
|
|
71
|
+
"""
|
|
72
|
+
# Use the base validation method (dict-only)
|
|
73
|
+
raw = self.validate_init_data(data, "Agency", allow_string_id=False)
|
|
74
|
+
super().__init__(raw, client)
|
|
75
|
+
|
|
76
|
+
# Store subtier data separately
|
|
77
|
+
self._subtier_data = subtier_data
|
|
78
|
+
|
|
79
|
+
def _fetch_details(self) -> Optional[Dict[str, Any]]:
|
|
80
|
+
"""Fetch full agency details if we have a toptier_code and client.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Optional[Dict[str, Any]]: Full agency data from the API, or None
|
|
84
|
+
if unable to fetch due to missing toptier_code or API error.
|
|
85
|
+
"""
|
|
86
|
+
# Try to get toptier_code from existing data
|
|
87
|
+
toptier_code = None
|
|
88
|
+
if "toptier_code" in self._data:
|
|
89
|
+
toptier_code = self._data["toptier_code"]
|
|
90
|
+
elif "code" in self._data:
|
|
91
|
+
toptier_code = self._data["code"]
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
# Fetch full agency details using the toptier_code
|
|
95
|
+
from ..queries.agency_query import AgencyQuery
|
|
96
|
+
|
|
97
|
+
query = AgencyQuery(self._client)
|
|
98
|
+
|
|
99
|
+
# Get fiscal_year if available in current data
|
|
100
|
+
fiscal_year = self._data.get("fiscal_year")
|
|
101
|
+
full_agency = query._get_resource_with_params(toptier_code, fiscal_year)
|
|
102
|
+
|
|
103
|
+
return full_agency
|
|
104
|
+
except Exception as e:
|
|
105
|
+
# Log but don't raise - lazy loading should fail gracefully
|
|
106
|
+
logger.debug(f"Could not fetch agency details for {toptier_code}: {e}")
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def _get_award_summary(
|
|
110
|
+
self,
|
|
111
|
+
award_type_codes: Optional[List[str]] = None,
|
|
112
|
+
fiscal_year: Optional[int] = None,
|
|
113
|
+
agency_type: str = "awarding",
|
|
114
|
+
) -> Optional[Dict[str, Any]]:
|
|
115
|
+
"""Fetch award summary data for a given agency code.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
award_type_codes (Optional[List[str]]): Optional list of award type codes
|
|
119
|
+
to filter by. If None, includes all award types.
|
|
120
|
+
fiscal_year (Optional[int]): Fiscal year to filter by. If None,
|
|
121
|
+
defaults to the current fiscal year.
|
|
122
|
+
agency_type (str): Type of agency relationship to filter by.
|
|
123
|
+
Must be "awarding" or "funding". Defaults to "awarding".
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Optional[Dict[str, Any]]: Award summary data dictionary containing
|
|
127
|
+
obligations, transaction counts, and other summary metrics,
|
|
128
|
+
or None if unable to fetch due to missing agency code or API error.
|
|
129
|
+
"""
|
|
130
|
+
# Get toptier code
|
|
131
|
+
toptier_code = self.code
|
|
132
|
+
if not toptier_code:
|
|
133
|
+
logger.error("Cannot fetch agency award summaries without agency code.")
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
from ..queries.agency_award_summary import AgencyAwardSummary
|
|
138
|
+
|
|
139
|
+
query = AgencyAwardSummary(self._client)
|
|
140
|
+
|
|
141
|
+
return query.get_awards_summary(
|
|
142
|
+
toptier_code=toptier_code,
|
|
143
|
+
fiscal_year=fiscal_year,
|
|
144
|
+
agency_type=agency_type,
|
|
145
|
+
award_type_codes=award_type_codes,
|
|
146
|
+
)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error(f"Could not fetch award summary for {toptier_code}: {e}")
|
|
149
|
+
return {}
|
|
150
|
+
|
|
151
|
+
# Properties from full agency API endpoint
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def fiscal_year(self) -> Optional[int]:
|
|
155
|
+
"""Fiscal year for the agency data.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Optional[int]: The fiscal year for this agency's data,
|
|
159
|
+
or None if not available.
|
|
160
|
+
"""
|
|
161
|
+
fiscal_year = self._lazy_get("fiscal_year")
|
|
162
|
+
return to_int(fiscal_year)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def toptier_code(self) -> Optional[str]:
|
|
166
|
+
"""Agency toptier code (3-4 digit string).
|
|
167
|
+
|
|
168
|
+
This is the Treasury Account Fund Symbol (TAFS) used to identify
|
|
169
|
+
the agency at the top-tier level in the government hierarchy.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Optional[str]: The toptier agency code, or None if not available.
|
|
173
|
+
"""
|
|
174
|
+
return self._lazy_get("toptier_code", "code")
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def code(self) -> Optional[str]:
|
|
178
|
+
"""Alias for toptier_code property.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Optional[str]: The toptier agency code, or None if not available.
|
|
182
|
+
"""
|
|
183
|
+
return self.toptier_code
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def name(self) -> Optional[str]:
|
|
187
|
+
"""Primary agency name.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Optional[str]: The official name of the agency,
|
|
191
|
+
or None if not available.
|
|
192
|
+
"""
|
|
193
|
+
# Agency now contains toptier data directly
|
|
194
|
+
return self._lazy_get("name")
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def abbreviation(self) -> Optional[str]:
|
|
198
|
+
"""Primary agency abbreviation.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Optional[str]: The official abbreviation of the agency,
|
|
202
|
+
or None if not available.
|
|
203
|
+
"""
|
|
204
|
+
# Agency now contains toptier data directly
|
|
205
|
+
return self._lazy_get("abbreviation")
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def id(self):
|
|
209
|
+
"""Internal identifier from USASpending.gov.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Optional[int]: The internal agency identifier,
|
|
213
|
+
or None if not available.
|
|
214
|
+
"""
|
|
215
|
+
return self.agency_id
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def agency_id(self) -> Optional[int]:
|
|
219
|
+
"""Internal identifier from USASpending.gov.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Optional[int]: The internal agency identifier used by the API,
|
|
223
|
+
or None if not available.
|
|
224
|
+
"""
|
|
225
|
+
agency_id = self._lazy_get("agency_id", "id")
|
|
226
|
+
return to_int(agency_id)
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def icon_filename(self) -> Optional[str]:
|
|
230
|
+
"""Filename of the agency's icon/logo.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Optional[str]: The filename of the agency's icon or logo image,
|
|
234
|
+
or None if not available.
|
|
235
|
+
"""
|
|
236
|
+
return self._lazy_get("icon_filename")
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def mission(self) -> Optional[str]:
|
|
240
|
+
"""Agency mission statement.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Optional[str]: The official mission statement of the agency,
|
|
244
|
+
or None if not available.
|
|
245
|
+
"""
|
|
246
|
+
return self._lazy_get("mission")
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def website(self) -> Optional[str]:
|
|
250
|
+
"""Agency website URL.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Optional[str]: The official website URL of the agency,
|
|
254
|
+
or None if not available.
|
|
255
|
+
"""
|
|
256
|
+
return self._lazy_get("website")
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def congressional_justification_url(self) -> Optional[str]:
|
|
260
|
+
"""URL to the agency's congressional justification.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Optional[str]: The URL to the agency's congressional justification document,
|
|
264
|
+
or None if not available.
|
|
265
|
+
"""
|
|
266
|
+
return self._lazy_get("congressional_justification_url")
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def about_agency_data(self) -> Optional[str]:
|
|
270
|
+
"""Additional information about the agency's data.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Optional[str]: Descriptive text about the agency's data quality,
|
|
274
|
+
coverage, or other relevant information, or None if not available.
|
|
275
|
+
"""
|
|
276
|
+
return self._lazy_get("about_agency_data")
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def subtier_agency_count(self) -> Optional[int]:
|
|
280
|
+
"""Number of subtier agencies under this agency.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Optional[int]: The total count of subtier agencies that report
|
|
284
|
+
to this toptier agency, or None if not available.
|
|
285
|
+
"""
|
|
286
|
+
count = self._lazy_get("subtier_agency_count")
|
|
287
|
+
return to_int(count)
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def messages(self) -> List[str]:
|
|
291
|
+
"""API messages related to this agency data.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
List[str]: List of informational messages from the API
|
|
295
|
+
related to this agency's data. Empty list if no messages.
|
|
296
|
+
"""
|
|
297
|
+
messages = self._lazy_get("messages", default=[])
|
|
298
|
+
if not isinstance(messages, list):
|
|
299
|
+
return []
|
|
300
|
+
return messages
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def def_codes(self) -> List[DefCode]:
|
|
304
|
+
"""List of Disaster Emergency Fund Codes (DEFC) for this agency.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
List[DefCode]: List of DefCode dataclass instances containing
|
|
308
|
+
disaster emergency fund codes associated with this agency.
|
|
309
|
+
Empty list if no DEFCs are available.
|
|
310
|
+
"""
|
|
311
|
+
def_codes_data = self._lazy_get("def_codes", default=[])
|
|
312
|
+
if not isinstance(def_codes_data, list):
|
|
313
|
+
return []
|
|
314
|
+
|
|
315
|
+
result = []
|
|
316
|
+
for code_data in def_codes_data:
|
|
317
|
+
if isinstance(code_data, dict):
|
|
318
|
+
# Handle the case where urls might be a string or list
|
|
319
|
+
urls = code_data.get("urls")
|
|
320
|
+
if isinstance(urls, str):
|
|
321
|
+
urls = [urls] if urls else None
|
|
322
|
+
elif urls and not isinstance(urls, list):
|
|
323
|
+
urls = None
|
|
324
|
+
|
|
325
|
+
def_code = DefCode(
|
|
326
|
+
code=code_data.get("code", ""),
|
|
327
|
+
public_law=code_data.get("public_law", ""),
|
|
328
|
+
title=code_data.get("title"),
|
|
329
|
+
urls=urls,
|
|
330
|
+
disaster=code_data.get("disaster"),
|
|
331
|
+
)
|
|
332
|
+
result.append(def_code)
|
|
333
|
+
|
|
334
|
+
return result
|
|
335
|
+
|
|
336
|
+
# Properties derived or related to the agency record
|
|
337
|
+
# These properties are not included in the agency detail API endpoint
|
|
338
|
+
# (generally, they come from a related agency properties in an award)
|
|
339
|
+
# so they cannot be lazy-loaded.
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def has_agency_page(self) -> bool:
|
|
343
|
+
"""Whether this agency has a dedicated page on USASpending.gov.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
bool: True if the agency has a dedicated profile page on USASpending.gov,
|
|
347
|
+
False otherwise.
|
|
348
|
+
"""
|
|
349
|
+
return bool(self.get_value(["has_agency_page"], default=False))
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def office_agency_name(self) -> Optional[str]:
|
|
353
|
+
"""Name of the specific office within the agency.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Optional[str]: The name of a specific office or division within
|
|
357
|
+
the agency, or None if not available.
|
|
358
|
+
"""
|
|
359
|
+
return self.get_value("office_agency_name")
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def slug(self) -> Optional[str]:
|
|
363
|
+
"""URL slug for this agency.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Optional[str]: The URL-friendly slug identifier used in
|
|
367
|
+
USASpending.gov URLs for this agency, or None if not available.
|
|
368
|
+
"""
|
|
369
|
+
return self.get_value("slug")
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def obligations(self) -> Optional[Decimal]:
|
|
373
|
+
"""Alias for total_obligations property.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Optional[Decimal]: The current fiscal year's total obligations
|
|
377
|
+
for this agency, or None if not available.
|
|
378
|
+
"""
|
|
379
|
+
return self.total_obligations
|
|
380
|
+
|
|
381
|
+
# Related and derived resources.
|
|
382
|
+
# Some of these properties are provided by search query
|
|
383
|
+
# results, others are helper methods that provide quick access
|
|
384
|
+
# to related award and transaction data.
|
|
385
|
+
|
|
386
|
+
@cached_property
|
|
387
|
+
def total_obligations(self) -> Optional[Decimal]:
|
|
388
|
+
"""Current fiscal year's total obligations made by this agency.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Optional[Decimal]: The total dollar amount of obligations
|
|
392
|
+
for the current fiscal year, or None if not available.
|
|
393
|
+
"""
|
|
394
|
+
obligations = self.get_value(["total_obligations", "obligations"])
|
|
395
|
+
if not obligations:
|
|
396
|
+
# If not present, fetch from award summary
|
|
397
|
+
obligations = self.get_obligations()
|
|
398
|
+
return obligations
|
|
399
|
+
|
|
400
|
+
@cached_property
|
|
401
|
+
def latest_action_date(self) -> Optional[date]:
|
|
402
|
+
"""Date of the most recent action for this agency's awards.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Optional[date]: The date of the most recent award action
|
|
406
|
+
associated with this agency, or None if not available.
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
# Check if value is present already (often provided in search results)
|
|
410
|
+
latest_action_date_string = self.get_value("latest_action_date")
|
|
411
|
+
|
|
412
|
+
# If not, fetch from agency award summary endpoint
|
|
413
|
+
if not latest_action_date_string:
|
|
414
|
+
summary = self._get_award_summary()
|
|
415
|
+
latest_action_date_string = summary.get("latest_action_date")
|
|
416
|
+
|
|
417
|
+
return to_date(latest_action_date_string)
|
|
418
|
+
|
|
419
|
+
@cached_property
|
|
420
|
+
def transaction_count(self) -> Optional[int]:
|
|
421
|
+
"""Total transaction count for this agency across all awards.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Optional[int]: The total number of transactions associated
|
|
425
|
+
with all awards for this agency, or None if not available.
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
# Check if value is present already (often provided in search results)
|
|
429
|
+
transaction_count = self.get_value("transaction_count")
|
|
430
|
+
|
|
431
|
+
# If not, fetch from agency award summary endpoint
|
|
432
|
+
if not transaction_count:
|
|
433
|
+
transaction_count = self.get_transaction_count()
|
|
434
|
+
|
|
435
|
+
return to_int(transaction_count)
|
|
436
|
+
|
|
437
|
+
@property
|
|
438
|
+
def awards(self) -> "AwardsSearch":
|
|
439
|
+
"""Get an AwardsSearch instance pre-filtered to this agency as awarding agency.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
AwardsSearch: A query builder instance pre-filtered to show awards
|
|
443
|
+
where this agency is the top-tier awarding agency. Use this to
|
|
444
|
+
search and filter awards associated with this agency.
|
|
445
|
+
"""
|
|
446
|
+
return self._client.awards.search().agency(self.name, "awarding", "toptier")
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def subagencies(self) -> List["SubTierAgency"]:
|
|
450
|
+
"""Get list of subtier agencies under this toptier agency.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
List[SubTierAgency]: List of SubTierAgency model instances
|
|
454
|
+
representing all subtier agencies that report to this
|
|
455
|
+
toptier agency. Returns empty list if no subagencies
|
|
456
|
+
are found or if there's an API error.
|
|
457
|
+
"""
|
|
458
|
+
# Get toptier code
|
|
459
|
+
toptier_code = self.code
|
|
460
|
+
if not toptier_code:
|
|
461
|
+
logger.error("Cannot fetch sub-agencies without agency code.")
|
|
462
|
+
return []
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
from ..queries.sub_agency_query import SubAgencyQuery
|
|
466
|
+
from .subtier_agency import SubTierAgency
|
|
467
|
+
|
|
468
|
+
query = SubAgencyQuery(self._client)
|
|
469
|
+
|
|
470
|
+
# Use fiscal year from this agency if available
|
|
471
|
+
fiscal_year = self.fiscal_year
|
|
472
|
+
|
|
473
|
+
response = query.get_subagencies(
|
|
474
|
+
toptier_code=toptier_code,
|
|
475
|
+
fiscal_year=fiscal_year,
|
|
476
|
+
limit=100, # Default to maximum
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# Transform results into SubTierAgency objects
|
|
480
|
+
subagencies = []
|
|
481
|
+
results = response.get("results", [])
|
|
482
|
+
for result in results:
|
|
483
|
+
if isinstance(result, dict):
|
|
484
|
+
subagency = SubTierAgency(result, self._client)
|
|
485
|
+
subagencies.append(subagency)
|
|
486
|
+
|
|
487
|
+
return subagencies
|
|
488
|
+
|
|
489
|
+
except Exception as e:
|
|
490
|
+
logger.debug(f"Could not fetch sub-agencies for {toptier_code}: {e}")
|
|
491
|
+
return []
|
|
492
|
+
|
|
493
|
+
def get_obligations(
|
|
494
|
+
self,
|
|
495
|
+
fiscal_year: Optional[int] = None,
|
|
496
|
+
agency_type: str = "awarding",
|
|
497
|
+
award_type_codes: Optional[List[str]] = None,
|
|
498
|
+
) -> Optional[float]:
|
|
499
|
+
"""Get obligations for this agency, optionally filtered.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
fiscal_year (Optional[int]): Fiscal year to filter by.
|
|
503
|
+
If None, defaults to the current fiscal year.
|
|
504
|
+
agency_type (str): Type of agency relationship to filter by.
|
|
505
|
+
Must be "awarding" or "funding". Defaults to "awarding".
|
|
506
|
+
award_type_codes (Optional[List[str]]): Optional list of award type codes
|
|
507
|
+
to filter by. If None, includes all award types.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
Optional[float]: Total obligations amount as a float,
|
|
511
|
+
or None if unavailable due to missing data or API error.
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
# Fetch from award summary API
|
|
515
|
+
summary = self._get_award_summary(
|
|
516
|
+
award_type_codes=award_type_codes,
|
|
517
|
+
fiscal_year=fiscal_year,
|
|
518
|
+
agency_type=agency_type,
|
|
519
|
+
)
|
|
520
|
+
return to_decimal(summary.get("obligations")) if summary else None
|
|
521
|
+
|
|
522
|
+
@cached_property
|
|
523
|
+
def contract_obligations(self) -> Optional[Decimal]:
|
|
524
|
+
"""Contract obligations for this agency in the current fiscal year.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
Optional[Decimal]: The total dollar amount of contract obligations
|
|
528
|
+
for this agency, or None if unavailable.
|
|
529
|
+
"""
|
|
530
|
+
summary = self._get_award_summary(award_type_codes=list(CONTRACT_CODES))
|
|
531
|
+
return to_decimal(summary.get("obligations")) if summary else None
|
|
532
|
+
|
|
533
|
+
@cached_property
|
|
534
|
+
def grant_obligations(self) -> Optional[Decimal]:
|
|
535
|
+
"""Grant obligations for this agency in the current fiscal year.
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Optional[Decimal]: The total dollar amount of grant obligations
|
|
539
|
+
for this agency, or None if unavailable.
|
|
540
|
+
"""
|
|
541
|
+
summary = self._get_award_summary(award_type_codes=list(GRANT_CODES))
|
|
542
|
+
return to_decimal(summary.get("obligations")) if summary else None
|
|
543
|
+
|
|
544
|
+
@cached_property
|
|
545
|
+
def idv_obligations(self) -> Optional[Decimal]:
|
|
546
|
+
"""Indefinite Delivery Vehicle (IDV) obligations for this agency.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
Optional[Decimal]: The total dollar amount of IDV obligations
|
|
550
|
+
for this agency in the current fiscal year, or None if unavailable.
|
|
551
|
+
"""
|
|
552
|
+
summary = self._get_award_summary(award_type_codes=list(IDV_CODES))
|
|
553
|
+
return to_decimal(summary.get("obligations")) if summary else None
|
|
554
|
+
|
|
555
|
+
@cached_property
|
|
556
|
+
def loan_obligations(self) -> Optional[Decimal]:
|
|
557
|
+
"""Loan obligations for this agency in the current fiscal year.
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
Optional[Decimal]: The total dollar amount of loan obligations
|
|
561
|
+
for this agency, or None if unavailable.
|
|
562
|
+
"""
|
|
563
|
+
summary = self._get_award_summary(award_type_codes=list(LOAN_CODES))
|
|
564
|
+
return to_decimal(summary.get("obligations")) if summary else None
|
|
565
|
+
|
|
566
|
+
@cached_property
|
|
567
|
+
def direct_payment_obligations(self) -> Optional[Decimal]:
|
|
568
|
+
"""Direct payment obligations for this agency in the current fiscal year.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Optional[Decimal]: The total dollar amount of direct payment obligations
|
|
572
|
+
for this agency, or None if unavailable.
|
|
573
|
+
"""
|
|
574
|
+
summary = self._get_award_summary(award_type_codes=list(DIRECT_PAYMENT_CODES))
|
|
575
|
+
return to_decimal(summary.get("obligations")) if summary else None
|
|
576
|
+
|
|
577
|
+
@cached_property
|
|
578
|
+
def other_obligations(self) -> Optional[Decimal]:
|
|
579
|
+
"""Other assistance obligations for this agency in the current fiscal year.
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
Optional[Decimal]: The total dollar amount of other assistance obligations
|
|
583
|
+
for this agency, or None if unavailable.
|
|
584
|
+
"""
|
|
585
|
+
summary = self._get_award_summary(award_type_codes=list(OTHER_CODES))
|
|
586
|
+
return to_decimal(summary.get("obligations")) if summary else None
|
|
587
|
+
|
|
588
|
+
def get_transaction_count(
|
|
589
|
+
self,
|
|
590
|
+
fiscal_year: Optional[int] = None,
|
|
591
|
+
agency_type: str = "awarding",
|
|
592
|
+
award_type_codes: Optional[List[str]] = None,
|
|
593
|
+
) -> Optional[int]:
|
|
594
|
+
"""Get transaction count for this agency, optionally filtered.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
fiscal_year (Optional[int]): Fiscal year to filter by.
|
|
598
|
+
If None, uses the agency's fiscal year or current fiscal year.
|
|
599
|
+
agency_type (str): Type of agency relationship to filter by.
|
|
600
|
+
Must be "awarding" or "funding". Defaults to "awarding".
|
|
601
|
+
award_type_codes (Optional[List[str]]): Optional list of award type codes
|
|
602
|
+
to filter by. If None, includes all award types.
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
Optional[int]: Total transaction count as an integer,
|
|
606
|
+
or None if unavailable due to missing data or API error.
|
|
607
|
+
"""
|
|
608
|
+
# If no filters and we have existing data, return it
|
|
609
|
+
if not any([fiscal_year, award_type_codes]) and agency_type == "awarding":
|
|
610
|
+
existing = self._lazy_get("transaction_count")
|
|
611
|
+
if existing is not None:
|
|
612
|
+
return to_int(existing)
|
|
613
|
+
|
|
614
|
+
# Fetch from award summary API
|
|
615
|
+
summary = self._get_award_summary(
|
|
616
|
+
award_type_codes=award_type_codes,
|
|
617
|
+
fiscal_year=fiscal_year,
|
|
618
|
+
agency_type=agency_type,
|
|
619
|
+
)
|
|
620
|
+
return to_int(summary.get("transaction_count")) if summary else None
|
|
621
|
+
|
|
622
|
+
def __repr__(self) -> str:
|
|
623
|
+
"""String representation of Agency.
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
str: A string representation showing the agency code and name
|
|
627
|
+
in the format "<Agency CODE: NAME>".
|
|
628
|
+
"""
|
|
629
|
+
name = self.name or "?"
|
|
630
|
+
code = self.code or "?"
|
|
631
|
+
return f"<Agency {code}: {name}>"
|