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,182 @@
|
|
|
1
|
+
"""Funding model for USASpending data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Any, Optional, TYPE_CHECKING
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
|
|
8
|
+
from .base_model import BaseModel
|
|
9
|
+
from ..utils.formatter import to_decimal, round_to_millions
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .agency import Agency
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Funding(BaseModel):
|
|
16
|
+
"""Represents federal account funding data for an award."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, data: Dict[str, Any]):
|
|
19
|
+
"""Initialize Funding instance.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
data: Raw funding data from API response
|
|
23
|
+
"""
|
|
24
|
+
super().__init__(data)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def transaction_obligated_amount(self) -> Optional[Decimal]:
|
|
28
|
+
"""Amount obligated for this funding record."""
|
|
29
|
+
return to_decimal(self.get_value("transaction_obligated_amount", default=0.0))
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def gross_outlay_amount(self) -> Optional[Decimal]:
|
|
33
|
+
"""Gross outlay amount for this funding record."""
|
|
34
|
+
return to_decimal(self.get_value("gross_outlay_amount", default=0.0))
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def disaster_emergency_fund_code(self) -> Optional[str]:
|
|
38
|
+
"""Code indicating whether funding is associated with a disaster."""
|
|
39
|
+
return self.get_value("disaster_emergency_fund_code")
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def federal_account(self) -> Optional[str]:
|
|
43
|
+
"""Identifier of the federal account."""
|
|
44
|
+
return self.get_value("federal_account")
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def account_title(self) -> Optional[str]:
|
|
48
|
+
"""Federal account title."""
|
|
49
|
+
return self.get_value("account_title")
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def funding_agency_name(self) -> Optional[str]:
|
|
53
|
+
"""Name of the funding agency."""
|
|
54
|
+
return self.get_value("funding_agency_name")
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def funding_agency_id(self) -> Optional[int]:
|
|
58
|
+
"""Internal surrogate identifier of the funding agency."""
|
|
59
|
+
value = self.get_value("funding_agency_id")
|
|
60
|
+
return int(value) if value is not None else None
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def funding_toptier_agency_id(self) -> Optional[int]:
|
|
64
|
+
"""Top-tier funding agency identifier."""
|
|
65
|
+
value = self.get_value("funding_toptier_agency_id")
|
|
66
|
+
return int(value) if value is not None else None
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def funding_agency_slug(self) -> Optional[str]:
|
|
70
|
+
"""URL-friendly funding agency identifier."""
|
|
71
|
+
return self.get_value("funding_agency_slug")
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def funding_agency(self) -> Optional["Agency"]:
|
|
75
|
+
"""Retrieve the full Agency object for the funding agency."""
|
|
76
|
+
name = self.funding_agency_name
|
|
77
|
+
if name:
|
|
78
|
+
return self._client.agencies.find_all_funding_agencies_by_name(
|
|
79
|
+
name
|
|
80
|
+
).toptier()[0]
|
|
81
|
+
else:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def awarding_agency_name(self) -> Optional[str]:
|
|
86
|
+
"""Name of the awarding agency."""
|
|
87
|
+
return self.get_value("awarding_agency_name")
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def awarding_agency_id(self) -> Optional[int]:
|
|
91
|
+
"""Internal surrogate identifier of the awarding agency."""
|
|
92
|
+
value = self.get_value("awarding_agency_id")
|
|
93
|
+
return int(value) if value is not None else None
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def awarding_toptier_agency_id(self) -> Optional[int]:
|
|
97
|
+
"""Top-tier awarding agency identifier."""
|
|
98
|
+
value = self.get_value("awarding_toptier_agency_id")
|
|
99
|
+
return int(value) if value is not None else None
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def awarding_agency_slug(self) -> Optional[str]:
|
|
103
|
+
"""URL-friendly awarding agency identifier."""
|
|
104
|
+
return self.get_value("awarding_agency_slug")
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def awarding_agency(self) -> Optional["Agency"]:
|
|
108
|
+
"""Retrieve the full Agency object for the funding agency."""
|
|
109
|
+
name = self.awarding_agency_name
|
|
110
|
+
if name:
|
|
111
|
+
return self._client.agencies.find_all_awarding_agencies_by_name(
|
|
112
|
+
name
|
|
113
|
+
).toptier()[0]
|
|
114
|
+
else:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def object_class(self) -> Optional[str]:
|
|
119
|
+
"""Object class code."""
|
|
120
|
+
return self.get_value("object_class")
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def object_class_name(self) -> Optional[str]:
|
|
124
|
+
"""Object class name/description."""
|
|
125
|
+
return self.get_value("object_class_name")
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def program_activity_code(self) -> Optional[str]:
|
|
129
|
+
"""Program activity code."""
|
|
130
|
+
return self.get_value("program_activity_code")
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def program_activity_name(self) -> Optional[str]:
|
|
134
|
+
"""Program activity name."""
|
|
135
|
+
return self.get_value("program_activity_name")
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def reporting_fiscal_year(self) -> Optional[int]:
|
|
139
|
+
"""Fiscal year of the submission date."""
|
|
140
|
+
value = self.get_value("reporting_fiscal_year")
|
|
141
|
+
return int(value) if value is not None else None
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def reporting_fiscal_quarter(self) -> Optional[int]:
|
|
145
|
+
"""Fiscal quarter of the submission date."""
|
|
146
|
+
value = self.get_value("reporting_fiscal_quarter")
|
|
147
|
+
return int(value) if value is not None else None
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def reporting_fiscal_month(self) -> Optional[int]:
|
|
151
|
+
"""Fiscal month of the submission date."""
|
|
152
|
+
value = self.get_value("reporting_fiscal_month")
|
|
153
|
+
return int(value) if value is not None else None
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def is_quarterly_submission(self) -> Optional[bool]:
|
|
157
|
+
"""True if submission is quarterly, False if monthly."""
|
|
158
|
+
return self.get_value("is_quarterly_submission")
|
|
159
|
+
|
|
160
|
+
def __repr__(self) -> str:
|
|
161
|
+
"""String representation of Funding."""
|
|
162
|
+
year = self.reporting_fiscal_year or "?"
|
|
163
|
+
month = self.reporting_fiscal_month
|
|
164
|
+
|
|
165
|
+
# Format month with zero padding if it's a number
|
|
166
|
+
if month is not None:
|
|
167
|
+
month_str = f"{month:02d}"
|
|
168
|
+
else:
|
|
169
|
+
month_str = "?"
|
|
170
|
+
|
|
171
|
+
if self.transaction_obligated_amount:
|
|
172
|
+
amount = self.transaction_obligated_amount
|
|
173
|
+
amount_str = f"OBL: {round_to_millions(amount)}"
|
|
174
|
+
elif self.gross_outlay_amount:
|
|
175
|
+
amount = self.gross_outlay_amount
|
|
176
|
+
amount_str = f"OUTLAY: {round_to_millions(amount)}"
|
|
177
|
+
else:
|
|
178
|
+
amount_str = "?"
|
|
179
|
+
|
|
180
|
+
agency = self.funding_agency_name or "Unknown Agency"
|
|
181
|
+
|
|
182
|
+
return f"<Funding {year}-{month_str} {agency} {amount_str}>"
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Grant award model for USASpending data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Dict, Any, Optional, List, TYPE_CHECKING
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
|
|
8
|
+
from .award import Award
|
|
9
|
+
from ..utils.formatter import to_decimal
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..queries.subawards_search import SubAwardsSearch
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Grant(Award):
|
|
16
|
+
"""Grant and assistance award types."""
|
|
17
|
+
|
|
18
|
+
TYPE_FIELDS = [
|
|
19
|
+
"fain",
|
|
20
|
+
"uri",
|
|
21
|
+
"record_type",
|
|
22
|
+
"cfda_info",
|
|
23
|
+
"cfda_number",
|
|
24
|
+
"primary_cfda_info",
|
|
25
|
+
"sai_number",
|
|
26
|
+
"funding_opportunity",
|
|
27
|
+
"non_federal_funding",
|
|
28
|
+
"total_funding",
|
|
29
|
+
"transaction_obligated_amount",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
SEARCH_FIELDS = Award.SEARCH_FIELDS + [
|
|
33
|
+
"Start Date",
|
|
34
|
+
"End Date",
|
|
35
|
+
"Award Amount",
|
|
36
|
+
"Total Outlays",
|
|
37
|
+
"Award Type",
|
|
38
|
+
"SAI Number",
|
|
39
|
+
"CFDA Number",
|
|
40
|
+
"Assistance Listings",
|
|
41
|
+
"primary_assistance_listing",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def fain(self) -> Optional[str]:
|
|
46
|
+
"""
|
|
47
|
+
An identification code assigned to each financial assistance award tracking
|
|
48
|
+
purposes. The FAIN is tied to that award (and all future modifications to that
|
|
49
|
+
award) throughout the award's life. Each FAIN is assigned by an agency. Within
|
|
50
|
+
an agency, FAIN are unique: each new award must be issued a new FAIN. FAIN
|
|
51
|
+
stands for Federal Award Identification Number, though the digits are letters,
|
|
52
|
+
not numbers.
|
|
53
|
+
"""
|
|
54
|
+
return self._lazy_get("fain")
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def uri(self) -> Optional[str]:
|
|
58
|
+
"""The uri of the award"""
|
|
59
|
+
return self._lazy_get("uri")
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def record_type(self) -> Optional[int]:
|
|
63
|
+
"""Grant record type identifier."""
|
|
64
|
+
return self._lazy_get("record_type")
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def cfda_info(self) -> List[Dict[str, Any]]:
|
|
68
|
+
"""Catalog of Federal Domestic Assistance information for grants."""
|
|
69
|
+
return self._lazy_get("cfda_info", "Assistance Listings", default=[])
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def cfda_number(self) -> Optional[str]:
|
|
73
|
+
"""Primary CFDA number for grants."""
|
|
74
|
+
primary_info = self._lazy_get("primary_cfda_info", "primary_assistance_listing")
|
|
75
|
+
if primary_info:
|
|
76
|
+
return primary_info.get("cfda_number")
|
|
77
|
+
|
|
78
|
+
cfda_list = self._lazy_get("cfda_info", "Assistance Listings")
|
|
79
|
+
if cfda_list:
|
|
80
|
+
return cfda_list[0].get("cfda_number")
|
|
81
|
+
|
|
82
|
+
return self._lazy_get("cfda_number", "CFDA Number")
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def primary_cfda_info(self) -> Optional[Dict[str, Any]]:
|
|
86
|
+
"""Primary CFDA program information."""
|
|
87
|
+
return self._lazy_get("primary_cfda_info", "primary_assistance_listing")
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def sai_number(self) -> Optional[str]:
|
|
91
|
+
"""System for Award Identification (SAI) number for grants."""
|
|
92
|
+
return self._lazy_get("sai_number", "SAI Number")
|
|
93
|
+
|
|
94
|
+
@cached_property
|
|
95
|
+
def funding_opportunity(self) -> Optional[Dict[str, Any]]:
|
|
96
|
+
"""Funding opportunity details for grants."""
|
|
97
|
+
return self._lazy_get("funding_opportunity")
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def non_federal_funding(self) -> Optional[Decimal]:
|
|
101
|
+
"""A summation of this award's transactions' non-federal funding amount"""
|
|
102
|
+
return to_decimal(self._lazy_get("non_federal_funding", default=None))
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def total_funding(self) -> Optional[Decimal]:
|
|
106
|
+
"""The sum of the federal action obligations and the Non-Federal funding amount."""
|
|
107
|
+
return to_decimal(self._lazy_get("total_funding", default=None))
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def transaction_obligated_amount(self) -> Optional[Decimal]:
|
|
111
|
+
"""Transaction-level obligated amount."""
|
|
112
|
+
return to_decimal(self._lazy_get("transaction_obligated_amount", default=None))
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def total_subsidy_cost(self) -> Optional[Decimal]:
|
|
116
|
+
"""Total subsidy cost for this award."""
|
|
117
|
+
return to_decimal(self._lazy_get("total_subsidy_cost", default=None))
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def base_exercised_options(self) -> Optional[Decimal]:
|
|
121
|
+
"""The total amount obligated for the base and exercised options of this award."""
|
|
122
|
+
return to_decimal(self._lazy_get("base_exercised_options", default=None))
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def base_and_all_options(self) -> Optional[Decimal]:
|
|
126
|
+
"""The total amount obligated for the base and all options of this award."""
|
|
127
|
+
return to_decimal(self._lazy_get("base_and_all_options", default=None))
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def subawards(self) -> "SubAwardsSearch":
|
|
131
|
+
"""Get subawards query builder for this grant award with appropriate award type filters.
|
|
132
|
+
|
|
133
|
+
Returns a SubAwardsSearch object that can be further filtered and chained.
|
|
134
|
+
Automatically applies grant award type filters.
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
>>> grant.subawards.count() # Get count without loading all data
|
|
138
|
+
>>> grant.subawards.limit(10).all() # Get first 10 subawards
|
|
139
|
+
>>> list(grant.subawards) # Iterate through all subawards
|
|
140
|
+
"""
|
|
141
|
+
from .award_types import GRANT_CODES
|
|
142
|
+
|
|
143
|
+
# Grant subawards use grant award types only
|
|
144
|
+
# Note: Due to validation in AwardsSearch, we cannot mix grant/direct_payment/other categories
|
|
145
|
+
return self._client.subawards.award_id(
|
|
146
|
+
self.generated_unique_award_id
|
|
147
|
+
).award_type_codes(*GRANT_CODES)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""IDV (Indefinite Delivery Vehicle) award model for USASpending data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Dict, Any, Optional
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
|
|
8
|
+
from .award import Award
|
|
9
|
+
from .location import Location
|
|
10
|
+
from ..utils.formatter import to_decimal
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IDV(Award):
|
|
14
|
+
"""Indefinite Delivery Vehicle (IDV) award type.
|
|
15
|
+
|
|
16
|
+
IDVs are contract vehicles that provide for an indefinite quantity of supplies
|
|
17
|
+
or services during a fixed period of time. They establish broad parameters and
|
|
18
|
+
terms for ordering supplies/services, with specific orders placed against them
|
|
19
|
+
via delivery orders or task orders.
|
|
20
|
+
|
|
21
|
+
Common IDV types include:
|
|
22
|
+
- GWAC (Government-Wide Acquisition Contract)
|
|
23
|
+
- IDC (Indefinite Delivery Contract)
|
|
24
|
+
- FSS (Federal Supply Schedule)
|
|
25
|
+
- BOA (Basic Ordering Agreement)
|
|
26
|
+
- BPA (Blanket Purchase Agreement)
|
|
27
|
+
|
|
28
|
+
IDVs serve as parent contracts that streamline procurement by pre-negotiating
|
|
29
|
+
terms, conditions, and pricing for future orders. They reduce administrative
|
|
30
|
+
costs and enable faster acquisition of recurring needs.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> # Find all IDVs for an agency
|
|
34
|
+
>>> idvs = client.awards.search().idvs().agency("NASA").all()
|
|
35
|
+
>>> for idv in idvs:
|
|
36
|
+
... print(f"{idv.piid}: {idv.recipient_name} - ${idv.total_obligation:,.2f}")
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
TYPE_FIELDS = [
|
|
40
|
+
"piid",
|
|
41
|
+
"base_and_all_options",
|
|
42
|
+
"contract_award_type",
|
|
43
|
+
"naics_code",
|
|
44
|
+
"naics_description",
|
|
45
|
+
"naics_hierarchy",
|
|
46
|
+
"psc_code",
|
|
47
|
+
"psc_description",
|
|
48
|
+
"psc_hierarchy",
|
|
49
|
+
"latest_transaction_contract_data",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
SEARCH_FIELDS = Award.SEARCH_FIELDS + [
|
|
53
|
+
"Start Date",
|
|
54
|
+
"Award Amount",
|
|
55
|
+
"Total Outlays",
|
|
56
|
+
"Contract Award Type",
|
|
57
|
+
"Last Date to Order",
|
|
58
|
+
"NAICS",
|
|
59
|
+
"PSC",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def piid(self) -> Optional[str]:
|
|
64
|
+
"""
|
|
65
|
+
Procurement Instrument Identifier - A unique identifier assigned to a federal
|
|
66
|
+
contract, purchase order, basic ordering agreement, basic agreement, and
|
|
67
|
+
blanket purchase agreement. It is used to track the contract, and any
|
|
68
|
+
modifications or transactions related to it. After October 2017, it is
|
|
69
|
+
between 13 and 17 digits, both letters and numbers.
|
|
70
|
+
"""
|
|
71
|
+
return self._lazy_get("piid")
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def base_and_all_options(self) -> Optional[Decimal]:
|
|
75
|
+
"""
|
|
76
|
+
For IDVs, this is the mutually agreed upon total contract value including all options (if any)
|
|
77
|
+
AND the estimated value of all potential orders. For modifications enter the CHANGE, positive
|
|
78
|
+
or negative of these values, if any.
|
|
79
|
+
"""
|
|
80
|
+
return to_decimal(self._lazy_get("base_and_all_options", default=None))
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def base_exercised_options(self) -> Optional[Decimal]:
|
|
84
|
+
"""
|
|
85
|
+
The contract value for the base contract and any options that have been exercised.
|
|
86
|
+
"""
|
|
87
|
+
return to_decimal(self._lazy_get("base_exercised_options", default=None))
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def contract_award_type(self) -> Optional[str]:
|
|
91
|
+
"""Contract award type description."""
|
|
92
|
+
return self._lazy_get(
|
|
93
|
+
"contract_award_type", "Contract Award Type", "type_description"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def naics_code(self) -> Optional[str]:
|
|
98
|
+
"""NAICS industry classification code."""
|
|
99
|
+
naics_data = self._lazy_get("naics", "NAICS")
|
|
100
|
+
if isinstance(naics_data, dict):
|
|
101
|
+
return naics_data.get("code")
|
|
102
|
+
if self.naics_hierarchy and isinstance(
|
|
103
|
+
self.naics_hierarchy.get("base_code"), dict
|
|
104
|
+
):
|
|
105
|
+
return self.naics_hierarchy["base_code"].get("code")
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def naics_description(self) -> Optional[str]:
|
|
110
|
+
"""NAICS industry classification description."""
|
|
111
|
+
naics_data = self._lazy_get("naics", "NAICS")
|
|
112
|
+
if isinstance(naics_data, dict):
|
|
113
|
+
return naics_data.get("description")
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def psc_code(self) -> Optional[str]:
|
|
118
|
+
"""Product/Service Code (PSC) for contracts."""
|
|
119
|
+
psc_data = self._lazy_get("psc", "PSC")
|
|
120
|
+
if isinstance(psc_data, dict):
|
|
121
|
+
return psc_data.get("code")
|
|
122
|
+
if self.psc_hierarchy and isinstance(self.psc_hierarchy.get("base_code"), dict):
|
|
123
|
+
return self.psc_hierarchy["base_code"].get("code")
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def psc_description(self) -> Optional[str]:
|
|
128
|
+
"""Product/Service Code (PSC) description."""
|
|
129
|
+
psc_data = self._lazy_get("psc", "PSC")
|
|
130
|
+
if isinstance(psc_data, dict):
|
|
131
|
+
return psc_data.get("description")
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
@cached_property
|
|
135
|
+
def psc_hierarchy(self) -> Optional[Dict[str, Any]]:
|
|
136
|
+
"""Product/Service Code (PSC) hierarchy information."""
|
|
137
|
+
return self._lazy_get("psc_hierarchy")
|
|
138
|
+
|
|
139
|
+
@cached_property
|
|
140
|
+
def naics_hierarchy(self) -> Optional[Dict[str, Any]]:
|
|
141
|
+
"""North American Industry Classification System (NAICS) hierarchy."""
|
|
142
|
+
return self._lazy_get("naics_hierarchy")
|
|
143
|
+
|
|
144
|
+
@cached_property
|
|
145
|
+
def latest_transaction_contract_data(self) -> Optional[Dict[str, Any]]:
|
|
146
|
+
"""Latest contract transaction data with procurement-specific details."""
|
|
147
|
+
return self._lazy_get("latest_transaction_contract_data")
|
|
148
|
+
|
|
149
|
+
@cached_property
|
|
150
|
+
def place_of_performance(self) -> Optional[Location]:
|
|
151
|
+
"""Award place of performance location.
|
|
152
|
+
|
|
153
|
+
Note: IDVs typically have null or empty place_of_performance data.
|
|
154
|
+
"""
|
|
155
|
+
data = self._lazy_get(
|
|
156
|
+
"place_of_performance", "Primary Place of Performance", default=None
|
|
157
|
+
)
|
|
158
|
+
if not isinstance(data, dict) or not data:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
# Check if all values in the dict are None/null (common for IDV awards)
|
|
162
|
+
if all(v is None for v in data.values()):
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
return Location(data, self._client)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# usaspending/models/lazy_record.py
|
|
2
|
+
from .base_model import ClientAwareModel
|
|
3
|
+
from typing import Dict, Any, Optional, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ..client import USASpendingClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LazyRecord(ClientAwareModel):
|
|
10
|
+
"""Enhanced LazyRecord that maintains client reference."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, data: Dict[str, Any], client: "USASpendingClient"):
|
|
13
|
+
super().__init__(data, client)
|
|
14
|
+
self._details_fetched = False
|
|
15
|
+
|
|
16
|
+
def _ensure_details(self) -> None:
|
|
17
|
+
"""Fetch full details using the client."""
|
|
18
|
+
if self._details_fetched:
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
new_data = self._fetch_details()
|
|
22
|
+
if new_data:
|
|
23
|
+
self._data.update(new_data)
|
|
24
|
+
self._details_fetched = True
|
|
25
|
+
|
|
26
|
+
def fetch_all_details(self) -> None:
|
|
27
|
+
"""Eagerly fetch all lazy-loadable data for this model.
|
|
28
|
+
|
|
29
|
+
This method is useful when you need to access model properties outside
|
|
30
|
+
of the client context manager. Call this method before the client session
|
|
31
|
+
closes to ensure all data is fetched.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
with USASpendingClient() as client:
|
|
35
|
+
awards = client.awards.search().limit(10).all()
|
|
36
|
+
for award in awards:
|
|
37
|
+
award.fetch_all_details() # Fetch all lazy data
|
|
38
|
+
# Now safe to use awards outside the context
|
|
39
|
+
print(awards[0].subaward_count)
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
DetachedInstanceError: If the client session is already closed.
|
|
43
|
+
"""
|
|
44
|
+
self._ensure_details()
|
|
45
|
+
|
|
46
|
+
def _fetch_details(self) -> Optional[Dict[str, Any]]:
|
|
47
|
+
"""Override in subclasses."""
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
def _lazy_get(self, *keys: str, default: Any = None) -> Any:
|
|
51
|
+
"""Get value, triggering lazy load if needed."""
|
|
52
|
+
|
|
53
|
+
keys = list(keys)
|
|
54
|
+
|
|
55
|
+
# If we've already lazy-loaded details, return whatever
|
|
56
|
+
# values are present in the data
|
|
57
|
+
if self._details_fetched:
|
|
58
|
+
return self.get_value(keys, default=default)
|
|
59
|
+
|
|
60
|
+
# Check if any of the keys exist in the current data
|
|
61
|
+
key_found = False
|
|
62
|
+
value = None
|
|
63
|
+
|
|
64
|
+
for key in keys:
|
|
65
|
+
if key in self._data:
|
|
66
|
+
value = self._data[key]
|
|
67
|
+
key_found = True
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
# If no key was found, or if the value is None (indicating missing data),
|
|
71
|
+
# trigger lazy loading
|
|
72
|
+
if not key_found or value is None:
|
|
73
|
+
# Load full resource data from source
|
|
74
|
+
self._ensure_details()
|
|
75
|
+
|
|
76
|
+
# Set flag
|
|
77
|
+
self._details_fetched = True
|
|
78
|
+
|
|
79
|
+
# Attempt to return the value again
|
|
80
|
+
value = self.get_value(keys, default=default)
|
|
81
|
+
|
|
82
|
+
return value
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Loan award model for USASpending data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Dict, Any, Optional, List
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
|
|
7
|
+
from .grant import Grant
|
|
8
|
+
from .award import Award
|
|
9
|
+
from ..utils.formatter import to_decimal
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Loan(Grant):
|
|
13
|
+
"""Loan award type."""
|
|
14
|
+
|
|
15
|
+
TYPE_FIELDS = [
|
|
16
|
+
"fain",
|
|
17
|
+
"uri",
|
|
18
|
+
"total_subsidy_cost",
|
|
19
|
+
"total_loan_value",
|
|
20
|
+
"cfda_info",
|
|
21
|
+
"cfda_number",
|
|
22
|
+
"primary_cfda_info",
|
|
23
|
+
"sai_number",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
SEARCH_FIELDS = Award.SEARCH_FIELDS + [
|
|
27
|
+
"Issued Date",
|
|
28
|
+
"Loan Value",
|
|
29
|
+
"Subsidy Cost",
|
|
30
|
+
"SAI Number",
|
|
31
|
+
"CFDA Number",
|
|
32
|
+
"Assistance Listings",
|
|
33
|
+
"primary_assistance_listing",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def fain(self) -> Optional[str]:
|
|
38
|
+
"""
|
|
39
|
+
An identification code assigned to each financial assistance award tracking
|
|
40
|
+
purposes. The FAIN is tied to that award (and all future modifications to that
|
|
41
|
+
award) throughout the award's life. Each FAIN is assigned by an agency. Within
|
|
42
|
+
an agency, FAIN are unique: each new award must be issued a new FAIN. FAIN
|
|
43
|
+
stands for Federal Award Identification Number, though the digits are letters,
|
|
44
|
+
not numbers.
|
|
45
|
+
"""
|
|
46
|
+
return self._lazy_get("fain")
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def uri(self) -> Optional[str]:
|
|
50
|
+
"""The uri of the award"""
|
|
51
|
+
return self._lazy_get("uri")
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def total_subsidy_cost(self) -> Optional[Decimal]:
|
|
55
|
+
"""The total of the original_loan_subsidy_cost from associated transactions"""
|
|
56
|
+
return to_decimal(
|
|
57
|
+
self._lazy_get("Subsidy Cost", "total_subsidy_cost", default=None)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def total_loan_value(self) -> Optional[Decimal]:
|
|
62
|
+
"""The total of the face_value_loan_guarantee from associated transactions"""
|
|
63
|
+
return to_decimal(
|
|
64
|
+
self._lazy_get("Loan Value", "total_loan_value", default=None)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def cfda_info(self) -> List[Dict[str, Any]]:
|
|
69
|
+
"""Catalog of Federal Domestic Assistance information for loans."""
|
|
70
|
+
return self._lazy_get("cfda_info", "Assistance Listings", default=[])
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def cfda_number(self) -> Optional[str]:
|
|
74
|
+
"""Primary CFDA number for loans."""
|
|
75
|
+
return self._lazy_get("cfda_number", "CFDA Number")
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def primary_cfda_info(self) -> Optional[Dict[str, Any]]:
|
|
79
|
+
"""Primary CFDA program information."""
|
|
80
|
+
return self._lazy_get("primary_cfda_info", "primary_assistance_listing")
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def sai_number(self) -> Optional[str]:
|
|
84
|
+
"""System for Award Identification (SAI) number for loans."""
|
|
85
|
+
return self._lazy_get("sai_number", "SAI Number")
|