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.
Files changed (71) hide show
  1. usaspending/__init__.py +144 -0
  2. usaspending/cli/__init__.py +2 -0
  3. usaspending/cli/download_award.py +113 -0
  4. usaspending/client.py +550 -0
  5. usaspending/config.py +119 -0
  6. usaspending/download/__init__.py +8 -0
  7. usaspending/download/job.py +219 -0
  8. usaspending/download/manager.py +140 -0
  9. usaspending/exceptions.py +71 -0
  10. usaspending/logging_config.py +310 -0
  11. usaspending/models/__init__.py +86 -0
  12. usaspending/models/agency.py +631 -0
  13. usaspending/models/award.py +933 -0
  14. usaspending/models/award_factory.py +72 -0
  15. usaspending/models/award_types.py +114 -0
  16. usaspending/models/base_model.py +214 -0
  17. usaspending/models/contract.py +147 -0
  18. usaspending/models/district_spending.py +70 -0
  19. usaspending/models/download.py +74 -0
  20. usaspending/models/funding.py +182 -0
  21. usaspending/models/grant.py +147 -0
  22. usaspending/models/idv.py +165 -0
  23. usaspending/models/lazy_record.py +82 -0
  24. usaspending/models/loan.py +85 -0
  25. usaspending/models/location.py +125 -0
  26. usaspending/models/period_of_performance.py +43 -0
  27. usaspending/models/recipient.py +204 -0
  28. usaspending/models/recipient_business_categories.py +364 -0
  29. usaspending/models/recipient_spending.py +58 -0
  30. usaspending/models/spending.py +70 -0
  31. usaspending/models/state_spending.py +43 -0
  32. usaspending/models/subaward.py +270 -0
  33. usaspending/models/subtier_agency.py +98 -0
  34. usaspending/models/transaction.py +89 -0
  35. usaspending/queries/__init__.py +87 -0
  36. usaspending/queries/agencies_search.py +196 -0
  37. usaspending/queries/agency_award_summary.py +134 -0
  38. usaspending/queries/agency_query.py +102 -0
  39. usaspending/queries/award_query.py +40 -0
  40. usaspending/queries/awarding_agencies_search.py +16 -0
  41. usaspending/queries/awards_search.py +602 -0
  42. usaspending/queries/filters.py +437 -0
  43. usaspending/queries/funding_agencies_search.py +20 -0
  44. usaspending/queries/funding_search.py +180 -0
  45. usaspending/queries/query_builder.py +1430 -0
  46. usaspending/queries/recipient_query.py +66 -0
  47. usaspending/queries/recipients_search.py +167 -0
  48. usaspending/queries/single_resource_base.py +83 -0
  49. usaspending/queries/spending_search.py +261 -0
  50. usaspending/queries/sub_agency_query.py +177 -0
  51. usaspending/queries/subawards_search.py +196 -0
  52. usaspending/queries/transactions_search.py +213 -0
  53. usaspending/resources/__init__.py +17 -0
  54. usaspending/resources/agency_resource.py +105 -0
  55. usaspending/resources/award_resource.py +119 -0
  56. usaspending/resources/base_resource.py +31 -0
  57. usaspending/resources/download_resource.py +102 -0
  58. usaspending/resources/funding_resource.py +41 -0
  59. usaspending/resources/recipients_resource.py +104 -0
  60. usaspending/resources/spending_resource.py +45 -0
  61. usaspending/resources/subawards_resource.py +68 -0
  62. usaspending/resources/transactions_resource.py +39 -0
  63. usaspending/utils/__init__.py +6 -0
  64. usaspending/utils/formatter.py +550 -0
  65. usaspending/utils/rate_limit.py +135 -0
  66. usaspending/utils/retry.py +257 -0
  67. usaspending/utils/special_cases.yaml +155 -0
  68. usaspending_orm-0.6.0.dist-info/LICENSE +18 -0
  69. usaspending_orm-0.6.0.dist-info/METADATA +324 -0
  70. usaspending_orm-0.6.0.dist-info/RECORD +71 -0
  71. 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")