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,72 @@
|
|
|
1
|
+
"""Factory for creating appropriate Award subclass instances."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Dict, Any, Optional, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from .award_types import get_category_for_code
|
|
7
|
+
from ..exceptions import ValidationError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .award import Award
|
|
11
|
+
from ..client import USASpendingClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_award(
|
|
15
|
+
data_or_id: Dict[str, Any] | str, client: Optional[USASpendingClient] = None
|
|
16
|
+
) -> Award:
|
|
17
|
+
"""
|
|
18
|
+
Factory function to create the appropriate Award subclass based on the award data.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
data_or_id: Award data dictionary or unique award ID string
|
|
22
|
+
client: Optional USASpendingClient instance
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Appropriate Award subclass instance (Contract, Grant, IDV, Loan, or base Award)
|
|
26
|
+
"""
|
|
27
|
+
# Import here to avoid circular imports
|
|
28
|
+
from .award import Award
|
|
29
|
+
from .contract import Contract
|
|
30
|
+
from .grant import Grant
|
|
31
|
+
from .idv import IDV
|
|
32
|
+
from .loan import Loan
|
|
33
|
+
|
|
34
|
+
# If it's just an ID, create base Award and let lazy loading determine type
|
|
35
|
+
if isinstance(data_or_id, str):
|
|
36
|
+
return Award(data_or_id, client)
|
|
37
|
+
|
|
38
|
+
if not isinstance(data_or_id, dict):
|
|
39
|
+
raise ValidationError("Award factory expects a dict or an award_id string")
|
|
40
|
+
|
|
41
|
+
# Determine award type from data
|
|
42
|
+
category = data_or_id.get("category", "").lower()
|
|
43
|
+
award_type = data_or_id.get("type", "")
|
|
44
|
+
|
|
45
|
+
award_class_map = {
|
|
46
|
+
"contract": Contract,
|
|
47
|
+
"idv": IDV,
|
|
48
|
+
"grant": Grant,
|
|
49
|
+
"loan": Loan,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Determine class from category first (most reliable)
|
|
53
|
+
award_class = award_class_map.get(category)
|
|
54
|
+
|
|
55
|
+
# Fallback to type codes if category doesn't match
|
|
56
|
+
if not award_class and award_type:
|
|
57
|
+
code_category = get_category_for_code(award_type)
|
|
58
|
+
if code_category:
|
|
59
|
+
# Map config category names to award class names
|
|
60
|
+
category_map = {
|
|
61
|
+
"contracts": "contract",
|
|
62
|
+
"idvs": "idv",
|
|
63
|
+
"grants": "grant",
|
|
64
|
+
"loans": "loan",
|
|
65
|
+
}
|
|
66
|
+
mapped_category = category_map.get(code_category)
|
|
67
|
+
if mapped_category:
|
|
68
|
+
award_class = award_class_map.get(mapped_category)
|
|
69
|
+
|
|
70
|
+
# Use the determined class or default to the base Award class
|
|
71
|
+
cls = award_class or Award
|
|
72
|
+
return cls(data_or_id, client)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Award type constants and categorizations for USASpending data.
|
|
2
|
+
|
|
3
|
+
This module contains all award type codes, groupings, and descriptions
|
|
4
|
+
as defined by the USASpending.gov API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
# Dictionary of award type groups with their codes and descriptions
|
|
10
|
+
# This is the single source of truth for award type categorization
|
|
11
|
+
AWARD_TYPE_GROUPS = {
|
|
12
|
+
"contracts": {
|
|
13
|
+
"A": "BPA Call",
|
|
14
|
+
"B": "Purchase Order",
|
|
15
|
+
"C": "Delivery Order",
|
|
16
|
+
"D": "Definitive Contract",
|
|
17
|
+
},
|
|
18
|
+
"loans": {"07": "Direct Loan", "08": "Guaranteed/Insured Loan"},
|
|
19
|
+
"idvs": {
|
|
20
|
+
"IDV_A": "GWAC Government Wide Acquisition Contract",
|
|
21
|
+
"IDV_B": "IDC Multi-Agency Contract, Other Indefinite Delivery Contract",
|
|
22
|
+
"IDV_B_A": "IDC Indefinite Delivery Contract / Requirements",
|
|
23
|
+
"IDV_B_B": "IDC Indefinite Delivery Contract / Indefinite Quantity",
|
|
24
|
+
"IDV_B_C": "IDC Indefinite Delivery Contract / Definite Quantity",
|
|
25
|
+
"IDV_C": "FSS Federal Supply Schedule",
|
|
26
|
+
"IDV_D": "BOA Basic Ordering Agreement",
|
|
27
|
+
"IDV_E": "BPA Blanket Purchase Agreement",
|
|
28
|
+
},
|
|
29
|
+
"grants": {
|
|
30
|
+
"02": "Block Grant",
|
|
31
|
+
"03": "Formula Grant",
|
|
32
|
+
"04": "Project Grant",
|
|
33
|
+
"05": "Cooperative Agreement",
|
|
34
|
+
},
|
|
35
|
+
"direct_payments": {
|
|
36
|
+
"06": "Direct Payment for Specified Use",
|
|
37
|
+
"10": "Direct Payment with Unrestricted Use",
|
|
38
|
+
},
|
|
39
|
+
"other_assistance": {
|
|
40
|
+
"09": "Insurance",
|
|
41
|
+
"11": "Other Financial Assistance",
|
|
42
|
+
"-1": "Not Specified",
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Create a flattened map for easy description lookups
|
|
47
|
+
AWARD_TYPE_DESCRIPTIONS = {
|
|
48
|
+
code: description
|
|
49
|
+
for group in AWARD_TYPE_GROUPS.values()
|
|
50
|
+
for code, description in group.items()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Regenerate frozensets from this single source of truth
|
|
54
|
+
CONTRACT_CODES = frozenset(AWARD_TYPE_GROUPS["contracts"].keys())
|
|
55
|
+
IDV_CODES = frozenset(AWARD_TYPE_GROUPS["idvs"].keys())
|
|
56
|
+
LOAN_CODES = frozenset(AWARD_TYPE_GROUPS["loans"].keys())
|
|
57
|
+
GRANT_CODES = frozenset(AWARD_TYPE_GROUPS["grants"].keys())
|
|
58
|
+
DIRECT_PAYMENT_CODES = frozenset(AWARD_TYPE_GROUPS["direct_payments"].keys())
|
|
59
|
+
OTHER_CODES = frozenset(AWARD_TYPE_GROUPS["other_assistance"].keys())
|
|
60
|
+
|
|
61
|
+
# All valid award type codes
|
|
62
|
+
ALL_AWARD_CODES = (
|
|
63
|
+
CONTRACT_CODES
|
|
64
|
+
| IDV_CODES
|
|
65
|
+
| LOAN_CODES
|
|
66
|
+
| GRANT_CODES
|
|
67
|
+
| DIRECT_PAYMENT_CODES
|
|
68
|
+
| OTHER_CODES
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_category_for_code(code: str) -> str:
|
|
73
|
+
"""Get the category name for a given award type code.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
code: Award type code (e.g., "A", "02", "IDV_A")
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Category name (e.g., "contracts", "grants", "idvs") or empty string if not found
|
|
80
|
+
"""
|
|
81
|
+
if not isinstance(code, str):
|
|
82
|
+
return ""
|
|
83
|
+
for category_name, codes in AWARD_TYPE_GROUPS.items():
|
|
84
|
+
if code in codes:
|
|
85
|
+
return category_name
|
|
86
|
+
return ""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def is_valid_award_type(code: str) -> bool:
|
|
90
|
+
"""Check if a code is a valid award type.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
code: Award type code to validate
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if the code is valid, False otherwise
|
|
97
|
+
"""
|
|
98
|
+
if not isinstance(code, str):
|
|
99
|
+
return False
|
|
100
|
+
return code in ALL_AWARD_CODES
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_description(code: str) -> str:
|
|
104
|
+
"""Get the description for a given award type code.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
code: Award type code
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Description string or empty string if not found
|
|
111
|
+
"""
|
|
112
|
+
if not isinstance(code, str):
|
|
113
|
+
return ""
|
|
114
|
+
return AWARD_TYPE_DESCRIPTIONS.get(code, "")
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# usaspending/models/base_model.py
|
|
2
|
+
from typing import Optional, Dict, Any, List, Set, TYPE_CHECKING
|
|
3
|
+
from weakref import ref
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ..client import USASpendingClient
|
|
7
|
+
|
|
8
|
+
from ..exceptions import ValidationError, DetachedInstanceError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseModel:
|
|
12
|
+
"""Base class for all models with fundamental behaviors."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, data: Dict[str, Any]):
|
|
15
|
+
self._data = data or {}
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def validate_init_data(
|
|
19
|
+
data_or_id: Any,
|
|
20
|
+
model_name: str,
|
|
21
|
+
id_field: Optional[str] = None,
|
|
22
|
+
allow_string_id: bool = False,
|
|
23
|
+
) -> Dict[str, Any]:
|
|
24
|
+
"""
|
|
25
|
+
Validate and normalize initialization data for models.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
data_or_id: Input data (dict, string, or other type)
|
|
29
|
+
model_name: Name of the model for error messages
|
|
30
|
+
id_field: Field name for ID when string is provided
|
|
31
|
+
allow_string_id: Whether to accept string as ID
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Normalized dictionary of data
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValidationError: If data is invalid
|
|
38
|
+
"""
|
|
39
|
+
if data_or_id is None:
|
|
40
|
+
raise ValidationError(
|
|
41
|
+
f"{model_name} data cannot be None. This may indicate an API or caching issue."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if isinstance(data_or_id, dict):
|
|
45
|
+
# Return a copy to avoid modifying the original
|
|
46
|
+
return data_or_id.copy()
|
|
47
|
+
|
|
48
|
+
if isinstance(data_or_id, str):
|
|
49
|
+
if not allow_string_id:
|
|
50
|
+
raise ValidationError(f"{model_name} expects dict, got str")
|
|
51
|
+
|
|
52
|
+
if not id_field:
|
|
53
|
+
raise ValidationError(f"{model_name} expects dict, got str")
|
|
54
|
+
|
|
55
|
+
if not data_or_id.strip():
|
|
56
|
+
raise ValidationError(f"{model_name} ID cannot be empty or whitespace")
|
|
57
|
+
|
|
58
|
+
return {id_field: data_or_id}
|
|
59
|
+
|
|
60
|
+
# Invalid type
|
|
61
|
+
type_name = type(data_or_id).__name__
|
|
62
|
+
if allow_string_id:
|
|
63
|
+
raise ValidationError(
|
|
64
|
+
f"{model_name} expects dict or string, got {type_name}"
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
raise ValidationError(f"{model_name} expects dict, got {type_name}")
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def raw(self) -> Dict[str, Any]:
|
|
71
|
+
"""Get raw data dictionary."""
|
|
72
|
+
return self._data
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
75
|
+
"""Convert to dictionary."""
|
|
76
|
+
return self._data
|
|
77
|
+
|
|
78
|
+
def get_value(self, keys: List[str], default: Any = None) -> Any:
|
|
79
|
+
"""Return the first non-None value from the given keys."""
|
|
80
|
+
if not isinstance(keys, list):
|
|
81
|
+
keys = [keys]
|
|
82
|
+
|
|
83
|
+
if not isinstance(self._data, dict):
|
|
84
|
+
raise TypeError("Empty object data")
|
|
85
|
+
for key in keys:
|
|
86
|
+
if key in self._data:
|
|
87
|
+
value = self._data[key]
|
|
88
|
+
if value is not None: # Check for non-None instead of truthiness
|
|
89
|
+
return value
|
|
90
|
+
return default
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ClientAwareModel(BaseModel):
|
|
94
|
+
"""Base class for all models that need API client access."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, data: Dict[str, Any], client: "USASpendingClient"):
|
|
97
|
+
super().__init__(data)
|
|
98
|
+
self._client_ref = ref(client) # Weak reference prevents circular refs
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def _client(self) -> "USASpendingClient":
|
|
102
|
+
"""Get client instance if still alive and usable.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
USASpendingClient: The client instance.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
DetachedInstanceError: If the client has been closed or garbage collected.
|
|
109
|
+
"""
|
|
110
|
+
client = self._client_ref() if self._client_ref else None
|
|
111
|
+
|
|
112
|
+
if client is None:
|
|
113
|
+
raise DetachedInstanceError(
|
|
114
|
+
f"Cannot access {self.__class__.__name__} properties: "
|
|
115
|
+
"the USASpendingClient has been garbage collected. "
|
|
116
|
+
"Ensure the client remains in scope, or access all needed data "
|
|
117
|
+
"within the 'with USASpendingClient()' context block."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if hasattr(client, "_closed") and client._closed:
|
|
121
|
+
raise DetachedInstanceError(
|
|
122
|
+
f"Cannot access {self.__class__.__name__} properties: "
|
|
123
|
+
"the USASpendingClient session is closed. "
|
|
124
|
+
"Access all lazy-loaded properties within the 'with' block, "
|
|
125
|
+
"or use explicit cleanup (client.close()) only after you're done with the models."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return client
|
|
129
|
+
|
|
130
|
+
def reattach(
|
|
131
|
+
self,
|
|
132
|
+
client: "USASpendingClient",
|
|
133
|
+
recursive: bool = False,
|
|
134
|
+
_visited: Optional[Set[int]] = None,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Reattach this model to a new client session.
|
|
137
|
+
|
|
138
|
+
This method updates the model's client reference to point to a new
|
|
139
|
+
USASpendingClient, allowing you to use objects created in one session
|
|
140
|
+
within a different session context.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
client: The new USASpendingClient to attach to.
|
|
144
|
+
recursive: If True, recursively reattach nested LazyRecord objects
|
|
145
|
+
(e.g., award.recipient, award.awarding_agency). Default False.
|
|
146
|
+
_visited: Internal parameter for cycle detection. Do not pass manually.
|
|
147
|
+
|
|
148
|
+
Example:
|
|
149
|
+
>>> # Create objects in one session
|
|
150
|
+
>>> with USASpendingClient() as client:
|
|
151
|
+
... award = client.awards.find_by_award_id("123")
|
|
152
|
+
... # Session closes here
|
|
153
|
+
...
|
|
154
|
+
>>> # Later, reattach to a new session
|
|
155
|
+
>>> with USASpendingClient() as new_client:
|
|
156
|
+
... award.reattach(new_client)
|
|
157
|
+
... print(award.subaward_count) # Now works!
|
|
158
|
+
...
|
|
159
|
+
>>> # Recursive reattach for nested objects
|
|
160
|
+
>>> with USASpendingClient() as new_client:
|
|
161
|
+
... award.reattach(new_client, recursive=True)
|
|
162
|
+
... # Now award.recipient and award.awarding_agency also reattached
|
|
163
|
+
... print(award.recipient.name)
|
|
164
|
+
|
|
165
|
+
Note:
|
|
166
|
+
QueryBuilder properties (like award.transactions) are not affected
|
|
167
|
+
by reattach. They create new query builders when accessed, which
|
|
168
|
+
automatically use the reattached client.
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
DetachedInstanceError: If the provided client is closed.
|
|
172
|
+
"""
|
|
173
|
+
# Validate that the new client is usable
|
|
174
|
+
if hasattr(client, "_closed") and client._closed:
|
|
175
|
+
raise DetachedInstanceError(
|
|
176
|
+
f"Cannot reattach {self.__class__.__name__} to a closed client. "
|
|
177
|
+
"Ensure the client is active (within a 'with' block or before close())."
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Update this model's client reference
|
|
181
|
+
self._client_ref = ref(client)
|
|
182
|
+
|
|
183
|
+
# Handle recursive reattachment if requested
|
|
184
|
+
if recursive:
|
|
185
|
+
# Import here to avoid circular dependency
|
|
186
|
+
from .lazy_record import LazyRecord
|
|
187
|
+
|
|
188
|
+
# Initialize visited set for cycle detection
|
|
189
|
+
if _visited is None:
|
|
190
|
+
_visited = set()
|
|
191
|
+
|
|
192
|
+
# Prevent infinite recursion on circular references
|
|
193
|
+
obj_id = id(self)
|
|
194
|
+
if obj_id in _visited:
|
|
195
|
+
return
|
|
196
|
+
_visited.add(obj_id)
|
|
197
|
+
|
|
198
|
+
# Find and reattach all LazyRecord properties
|
|
199
|
+
for attr_name in dir(self):
|
|
200
|
+
# Skip private attributes and methods
|
|
201
|
+
if attr_name.startswith("_"):
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
attr = getattr(self, attr_name)
|
|
206
|
+
|
|
207
|
+
# Recursively reattach LazyRecord instances
|
|
208
|
+
if isinstance(attr, LazyRecord):
|
|
209
|
+
attr.reattach(client, recursive=True, _visited=_visited)
|
|
210
|
+
|
|
211
|
+
except (AttributeError, Exception):
|
|
212
|
+
# Skip properties that raise errors during access
|
|
213
|
+
# (e.g., properties that require API calls, methods, etc.)
|
|
214
|
+
pass
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Contract award model for USASpending data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Dict, Any, Optional, 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 Contract(Award):
|
|
16
|
+
"""Contract award type including definitive contracts and purchase orders."""
|
|
17
|
+
|
|
18
|
+
TYPE_FIELDS = [
|
|
19
|
+
"piid",
|
|
20
|
+
"base_exercised_options",
|
|
21
|
+
"base_and_all_options",
|
|
22
|
+
"contract_award_type",
|
|
23
|
+
"naics_code",
|
|
24
|
+
"naics_description",
|
|
25
|
+
"naics_hierarchy",
|
|
26
|
+
"psc_code",
|
|
27
|
+
"psc_description",
|
|
28
|
+
"psc_hierarchy",
|
|
29
|
+
"latest_transaction_contract_data",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
SEARCH_FIELDS = Award.SEARCH_FIELDS + [
|
|
33
|
+
"Start Date",
|
|
34
|
+
"End Date",
|
|
35
|
+
"Award Amount",
|
|
36
|
+
"Total Outlays",
|
|
37
|
+
"Contract Award Type",
|
|
38
|
+
"NAICS",
|
|
39
|
+
"PSC",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def piid(self) -> Optional[str]:
|
|
44
|
+
"""
|
|
45
|
+
Procurement Instrument Identifier - A unique identifier assigned to a federal
|
|
46
|
+
contract, purchase order, basic ordering agreement, basic agreement, and
|
|
47
|
+
blanket purchase agreement. It is used to track the contract, and any
|
|
48
|
+
modifications or transactions related to it. After October 2017, it is
|
|
49
|
+
between 13 and 17 digits, both letters and numbers.
|
|
50
|
+
"""
|
|
51
|
+
return self._lazy_get("piid")
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def base_exercised_options(self) -> Optional[Decimal]:
|
|
55
|
+
"""The sum of the base_exercised_options_val from associated transactions"""
|
|
56
|
+
return to_decimal(self._lazy_get("base_exercised_options", default=None))
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def base_and_all_options(self) -> Optional[Decimal]:
|
|
60
|
+
"""The sum of the base_and_all_options_value from associated transactions"""
|
|
61
|
+
return to_decimal(self._lazy_get("base_and_all_options", default=None))
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def contract_award_type(self) -> Optional[str]:
|
|
65
|
+
"""Contract award type description."""
|
|
66
|
+
return self.type_description
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def naics_code(self) -> Optional[str]:
|
|
70
|
+
"""NAICS industry classification code."""
|
|
71
|
+
naics_data = self._lazy_get("naics", "NAICS")
|
|
72
|
+
if isinstance(naics_data, dict):
|
|
73
|
+
return naics_data.get("code")
|
|
74
|
+
if self.naics_hierarchy and isinstance(
|
|
75
|
+
self.naics_hierarchy.get("base_code"), dict
|
|
76
|
+
):
|
|
77
|
+
return self.naics_hierarchy["base_code"].get("code")
|
|
78
|
+
if self.latest_transaction_contract_data:
|
|
79
|
+
return self.latest_transaction_contract_data.get("naics")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def naics_description(self) -> Optional[str]:
|
|
84
|
+
"""NAICS industry classification description."""
|
|
85
|
+
naics_data = self._lazy_get("naics", "NAICS")
|
|
86
|
+
if isinstance(naics_data, dict):
|
|
87
|
+
return naics_data.get("description")
|
|
88
|
+
if self.naics_hierarchy and isinstance(
|
|
89
|
+
self.naics_hierarchy.get("base_code"), dict
|
|
90
|
+
):
|
|
91
|
+
return self.naics_hierarchy["base_code"].get("description")
|
|
92
|
+
if self.latest_transaction_contract_data:
|
|
93
|
+
return self.latest_transaction_contract_data.get("naics_description")
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def psc_code(self) -> Optional[str]:
|
|
98
|
+
"""Product/Service Code (PSC) for contracts."""
|
|
99
|
+
psc_data = self._lazy_get("psc", "PSC")
|
|
100
|
+
if isinstance(psc_data, dict):
|
|
101
|
+
return psc_data.get("code")
|
|
102
|
+
if self.psc_hierarchy and isinstance(self.psc_hierarchy.get("base_code"), dict):
|
|
103
|
+
return self.psc_hierarchy["base_code"].get("code")
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def psc_description(self) -> Optional[str]:
|
|
108
|
+
"""Product/Service Code (PSC) description."""
|
|
109
|
+
psc_data = self._lazy_get("psc", "PSC")
|
|
110
|
+
if isinstance(psc_data, dict):
|
|
111
|
+
return psc_data.get("description")
|
|
112
|
+
if self.psc_hierarchy and isinstance(self.psc_hierarchy.get("base_code"), dict):
|
|
113
|
+
return self.psc_hierarchy["base_code"].get("description")
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
@cached_property
|
|
117
|
+
def psc_hierarchy(self) -> Optional[Dict[str, Any]]:
|
|
118
|
+
"""Product/Service Code (PSC) hierarchy information."""
|
|
119
|
+
return self._lazy_get("psc_hierarchy")
|
|
120
|
+
|
|
121
|
+
@cached_property
|
|
122
|
+
def naics_hierarchy(self) -> Optional[Dict[str, Any]]:
|
|
123
|
+
"""North American Industry Classification System (NAICS) hierarchy."""
|
|
124
|
+
return self._lazy_get("naics_hierarchy")
|
|
125
|
+
|
|
126
|
+
@cached_property
|
|
127
|
+
def latest_transaction_contract_data(self) -> Optional[Dict[str, Any]]:
|
|
128
|
+
"""Latest contract transaction data with procurement-specific details."""
|
|
129
|
+
return self._lazy_get("latest_transaction_contract_data")
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def subawards(self) -> "SubAwardsSearch":
|
|
133
|
+
"""Get subawards query builder for this contract award with appropriate award type filters.
|
|
134
|
+
|
|
135
|
+
Returns a SubAwardsSearch object that can be further filtered and chained.
|
|
136
|
+
Automatically applies contract award type filters.
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
>>> contract.subawards.count() # Get count without loading all data
|
|
140
|
+
>>> contract.subawards.limit(10).all() # Get first 10 subawards
|
|
141
|
+
>>> list(contract.subawards) # Iterate through all subawards
|
|
142
|
+
"""
|
|
143
|
+
from .award_types import CONTRACT_CODES
|
|
144
|
+
|
|
145
|
+
return self._client.subawards.award_id(
|
|
146
|
+
self.generated_unique_award_id
|
|
147
|
+
).award_type_codes(*CONTRACT_CODES)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""District spending model for USASpending spending by district data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .spending import Spending
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ..client import USASpendingClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DistrictSpending(Spending):
|
|
14
|
+
"""Model for spending by congressional district data.
|
|
15
|
+
|
|
16
|
+
Represents spending data grouped by congressional district with
|
|
17
|
+
district-specific parsing and display logic.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, data: dict, client: Optional["USASpendingClient"] = None):
|
|
21
|
+
"""Initialize DistrictSpending model.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
data: Raw district spending data from API
|
|
25
|
+
client: USASpendingClient client instance
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(data, client)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def district_code(self) -> Optional[str]:
|
|
31
|
+
"""Congressional district code."""
|
|
32
|
+
return self.code
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def state_code(self) -> Optional[str]:
|
|
36
|
+
"""Extract state code from district name if available.
|
|
37
|
+
|
|
38
|
+
District names typically follow format like 'TX-12' or 'MS-MULTIPLE DISTRICTS'.
|
|
39
|
+
"""
|
|
40
|
+
if self.name:
|
|
41
|
+
# Names like "TX-12" or "MS-MULTIPLE DISTRICTS"
|
|
42
|
+
parts = self.name.split("-", 1)
|
|
43
|
+
if len(parts) >= 2:
|
|
44
|
+
return parts[0]
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def district_number(self) -> Optional[str]:
|
|
49
|
+
"""Extract district number from district name if available.
|
|
50
|
+
|
|
51
|
+
Returns the numeric part or special designation like 'MULTIPLE DISTRICTS'.
|
|
52
|
+
"""
|
|
53
|
+
if self.name:
|
|
54
|
+
# Names like "TX-12" or "MS-MULTIPLE DISTRICTS"
|
|
55
|
+
parts = self.name.split("-", 1)
|
|
56
|
+
if len(parts) >= 2:
|
|
57
|
+
return parts[1]
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_multiple_districts(self) -> bool:
|
|
62
|
+
"""Check if this represents multiple districts in a state."""
|
|
63
|
+
district_num = self.district_number
|
|
64
|
+
return district_num is not None and "MULTIPLE" in district_num.upper()
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
"""String representation of DistrictSpending."""
|
|
68
|
+
name = self.name or "Unknown District"
|
|
69
|
+
amount = self.amount or 0
|
|
70
|
+
return f"<DistrictSpending {name}: ${amount:,.2f}>"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# src/usaspending/models/download.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Optional, Literal
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from ..utils.formatter import to_float, to_int
|
|
8
|
+
from .base_model import BaseModel
|
|
9
|
+
|
|
10
|
+
AwardType = Literal["contract", "assistance", "idv"]
|
|
11
|
+
FileFormat = Literal["csv", "tsv", "pstxt"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DownloadState(Enum):
|
|
15
|
+
"""Enumeration for download job states."""
|
|
16
|
+
|
|
17
|
+
PENDING = "pending" # Custom state before first API check
|
|
18
|
+
READY = "ready"
|
|
19
|
+
RUNNING = "running"
|
|
20
|
+
FINISHED = "finished"
|
|
21
|
+
FAILED = "failed"
|
|
22
|
+
UNKNOWN = "unknown" # Custom state if API returns unexpected value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DownloadStatus(BaseModel):
|
|
26
|
+
"""Represents the status details of a download job returned by the API."""
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def file_name(self) -> Optional[str]:
|
|
30
|
+
return self.get_value("file_name")
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def message(self) -> Optional[str]:
|
|
34
|
+
"""A human readable error message if the status is failed, otherwise null."""
|
|
35
|
+
return self.get_value("message")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def seconds_elapsed(self) -> Optional[float]:
|
|
39
|
+
"""Time taken to generate the file or time taken so far."""
|
|
40
|
+
return to_float(self.get_value("seconds_elapsed"))
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def api_status(self) -> DownloadState:
|
|
44
|
+
"""Current state of the request from the API."""
|
|
45
|
+
status_str = self.get_value("status")
|
|
46
|
+
if status_str:
|
|
47
|
+
try:
|
|
48
|
+
return DownloadState(status_str)
|
|
49
|
+
except ValueError:
|
|
50
|
+
return DownloadState.UNKNOWN
|
|
51
|
+
return DownloadState.UNKNOWN
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def total_columns(self) -> Optional[int]:
|
|
55
|
+
return to_int(self.get_value("total_columns"))
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def total_rows(self) -> Optional[int]:
|
|
59
|
+
return to_int(self.get_value("total_rows"))
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def total_size_kb(self) -> Optional[float]:
|
|
63
|
+
"""Estimated file size in kilobytes."""
|
|
64
|
+
return to_float(self.get_value("total_size"))
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def file_url(self) -> Optional[str]:
|
|
68
|
+
"""The URL for the file (relative path)."""
|
|
69
|
+
return self.get_value("file_url")
|
|
70
|
+
|
|
71
|
+
def __repr__(self) -> str:
|
|
72
|
+
return (
|
|
73
|
+
f"<DownloadStatus status='{self.api_status.value}' file='{self.file_name}'>"
|
|
74
|
+
)
|