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,310 @@
|
|
|
1
|
+
"""Logging configuration for USASpending API client.
|
|
2
|
+
|
|
3
|
+
For applications using this library:
|
|
4
|
+
|
|
5
|
+
#### Configure Only USASpending Logs
|
|
6
|
+
|
|
7
|
+
To control only the USASpending library logs without affecting other libraries:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import logging
|
|
11
|
+
from usaspending import USASpendingClient
|
|
12
|
+
|
|
13
|
+
# Configure only the usaspending logger
|
|
14
|
+
usaspending_logger = logging.getLogger('usaspending')
|
|
15
|
+
usaspending_logger.setLevel(logging.DEBUG)
|
|
16
|
+
|
|
17
|
+
# Add a handler for usaspending logs
|
|
18
|
+
handler = logging.StreamHandler()
|
|
19
|
+
handler.setFormatter(logging.Formatter('%(name)s - %(levelname)s - %(message)s'))
|
|
20
|
+
usaspending_logger.addHandler(handler)
|
|
21
|
+
|
|
22
|
+
client = USASpendingClient()
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
#### Silence USASpending Logs
|
|
26
|
+
|
|
27
|
+
To suppress USASpending log messages even if your application has logging configured:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import logging
|
|
31
|
+
from usaspending import USASpendingClient
|
|
32
|
+
|
|
33
|
+
# Silence usaspending logs
|
|
34
|
+
logging.getLogger('usaspending').setLevel(logging.WARNING)
|
|
35
|
+
|
|
36
|
+
client = USASpendingClient()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### Log to File
|
|
40
|
+
|
|
41
|
+
To write USASpending logs to a file:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import logging
|
|
45
|
+
from usaspending import USASpendingClient
|
|
46
|
+
|
|
47
|
+
# Configure file logging for usaspending
|
|
48
|
+
usaspending_logger = logging.getLogger('usaspending')
|
|
49
|
+
usaspending_logger.setLevel(logging.INFO)
|
|
50
|
+
|
|
51
|
+
file_handler = logging.FileHandler('usaspending.log')
|
|
52
|
+
file_handler.setFormatter(
|
|
53
|
+
logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
54
|
+
)
|
|
55
|
+
usaspending_logger.addHandler(file_handler)
|
|
56
|
+
|
|
57
|
+
client = USASpendingClient()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
from __future__ import annotations
|
|
63
|
+
|
|
64
|
+
import logging
|
|
65
|
+
from collections import Counter
|
|
66
|
+
from typing import Dict, Any, Optional, TYPE_CHECKING
|
|
67
|
+
|
|
68
|
+
if TYPE_CHECKING:
|
|
69
|
+
from .queries.filters import BaseFilter
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class USASpendingLogger:
|
|
73
|
+
"""Library logging manager.
|
|
74
|
+
|
|
75
|
+
Example usage within the library:
|
|
76
|
+
from usaspending.logging_config import USASpendingLogger
|
|
77
|
+
|
|
78
|
+
logger = USASpendingLogger.get_logger(__name__)
|
|
79
|
+
logger.info("Library message")
|
|
80
|
+
|
|
81
|
+
Example usage by applications:
|
|
82
|
+
import logging
|
|
83
|
+
from usaspending import USASpendingClient
|
|
84
|
+
|
|
85
|
+
# Configure logging
|
|
86
|
+
logging.basicConfig(
|
|
87
|
+
level=logging.INFO,
|
|
88
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
client = USASpendingClient()
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
_loggers: Dict[str, logging.Logger] = {}
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def get_logger(cls, name: str) -> logging.Logger:
|
|
98
|
+
"""Get a logger instance with proper library configuration.
|
|
99
|
+
|
|
100
|
+
This method ensures that library loggers:
|
|
101
|
+
- Have a NullHandler to prevent "No handlers found" warnings
|
|
102
|
+
- Have propagation enabled so applications can control them
|
|
103
|
+
- Are cached to avoid repeated configuration
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
name: Logger name (typically __name__ from the calling module)
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Configured logger instance suitable for library use
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
logger = USASpendingLogger.get_logger(__name__)
|
|
113
|
+
logger.info("Library message")
|
|
114
|
+
"""
|
|
115
|
+
if name not in cls._loggers:
|
|
116
|
+
logger = logging.getLogger(name)
|
|
117
|
+
|
|
118
|
+
# Only add NullHandler if this is a library logger and has no handlers
|
|
119
|
+
if name.startswith("usaspending") and not logger.handlers:
|
|
120
|
+
# NullHandler prevents "No handler found" warnings while allowing
|
|
121
|
+
# the application to control actual logging behavior
|
|
122
|
+
logger.addHandler(logging.NullHandler())
|
|
123
|
+
|
|
124
|
+
# Ensure propagation is enabled so parent loggers (configured by
|
|
125
|
+
# the application) can handle our log records
|
|
126
|
+
logger.propagate = True
|
|
127
|
+
|
|
128
|
+
cls._loggers[name] = logger
|
|
129
|
+
|
|
130
|
+
return cls._loggers[name]
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def is_debug_enabled(cls) -> bool:
|
|
134
|
+
"""Check if debug logging is enabled for the library.
|
|
135
|
+
|
|
136
|
+
This checks the effective level of the 'usaspending' logger to determine
|
|
137
|
+
if debug messages would be processed. Useful for expensive debug operations.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if debug logging is enabled, False otherwise
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
if USASpendingLogger.is_debug_enabled():
|
|
144
|
+
logger.debug(f"Expensive debug info: {expensive_operation()}")
|
|
145
|
+
"""
|
|
146
|
+
usaspending_logger = logging.getLogger("usaspending")
|
|
147
|
+
return usaspending_logger.isEnabledFor(logging.DEBUG)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_logger(name: str) -> logging.Logger:
|
|
151
|
+
"""Convenience function to get a library logger.
|
|
152
|
+
|
|
153
|
+
This is a shortcut for USASpendingLogger.get_logger() that's commonly used
|
|
154
|
+
throughout the library codebase.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
name: Logger name (typically __name__)
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Configured logger instance
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
from usaspending.logging_config import get_logger
|
|
164
|
+
|
|
165
|
+
logger = get_logger(__name__)
|
|
166
|
+
logger.info("Library message")
|
|
167
|
+
"""
|
|
168
|
+
return USASpendingLogger.get_logger(name)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def log_api_request(
|
|
172
|
+
logger: logging.Logger,
|
|
173
|
+
method: str,
|
|
174
|
+
url: str,
|
|
175
|
+
params: Optional[Dict[str, Any]] = None,
|
|
176
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Log an API request with appropriate detail level.
|
|
179
|
+
|
|
180
|
+
This helper function provides consistent formatting for API request logging
|
|
181
|
+
throughout the library. The level of detail depends on the logger's effective level.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
logger: Logger instance to use for output
|
|
185
|
+
method: HTTP method (GET, POST, etc.)
|
|
186
|
+
url: Request URL
|
|
187
|
+
params: Optional query parameters dict
|
|
188
|
+
json_data: Optional JSON payload dict
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
log_api_request(logger, "POST", "https://api.example.com/search",
|
|
192
|
+
json_data={"filters": {...}})
|
|
193
|
+
"""
|
|
194
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
195
|
+
logger.debug(f"API Request: {method} {url}")
|
|
196
|
+
if params:
|
|
197
|
+
logger.debug(f"Query params: {params}")
|
|
198
|
+
if json_data:
|
|
199
|
+
logger.debug(f"JSON payload: {json_data}")
|
|
200
|
+
else:
|
|
201
|
+
logger.info(f"API Request: {method} {url}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def log_api_response(
|
|
205
|
+
logger: logging.Logger,
|
|
206
|
+
status_code: int,
|
|
207
|
+
response_size: Optional[int] = None,
|
|
208
|
+
duration: Optional[float] = None,
|
|
209
|
+
error: Optional[str] = None,
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Log an API response with appropriate detail level.
|
|
212
|
+
|
|
213
|
+
This helper function provides consistent formatting for API response logging
|
|
214
|
+
throughout the library. The message format and level depend on the response status.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
logger: Logger instance to use for output
|
|
218
|
+
status_code: HTTP status code
|
|
219
|
+
response_size: Optional response size in bytes
|
|
220
|
+
duration: Optional request duration in seconds
|
|
221
|
+
error: Optional error message for failed requests
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
log_api_response(logger, 200, response_size=1024, duration=0.543)
|
|
225
|
+
log_api_response(logger, 500, error="Internal server error")
|
|
226
|
+
"""
|
|
227
|
+
if error:
|
|
228
|
+
logger.error(f"API Response: {status_code} - Error: {error}")
|
|
229
|
+
elif status_code >= 400:
|
|
230
|
+
logger.warning(f"API Response: {status_code}")
|
|
231
|
+
else:
|
|
232
|
+
msg_parts = [f"API Response: {status_code}"]
|
|
233
|
+
if duration is not None:
|
|
234
|
+
msg_parts.append(f"({duration:.3f}s)")
|
|
235
|
+
if response_size is not None and logger.isEnabledFor(logging.DEBUG):
|
|
236
|
+
msg_parts.append(f"- {response_size} bytes")
|
|
237
|
+
|
|
238
|
+
if status_code >= 300:
|
|
239
|
+
logger.warning(" ".join(msg_parts))
|
|
240
|
+
else:
|
|
241
|
+
logger.debug(" ".join(msg_parts))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def log_query_execution(
|
|
245
|
+
logger: logging.Logger,
|
|
246
|
+
query_type: str,
|
|
247
|
+
filter_objects: list["BaseFilter"],
|
|
248
|
+
endpoint: str,
|
|
249
|
+
page: int = 1,
|
|
250
|
+
) -> None:
|
|
251
|
+
"""Log query execution details.
|
|
252
|
+
|
|
253
|
+
This helper function provides consistent formatting for query execution logging
|
|
254
|
+
throughout the library. Debug level includes additional endpoint, pagination info,
|
|
255
|
+
and detailed filter breakdown.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
logger: Logger instance to use for output
|
|
259
|
+
query_type: Type of query being executed (e.g., "AwardsSearch")
|
|
260
|
+
filter_objects: List of filter objects applied to the query
|
|
261
|
+
endpoint: API endpoint being called
|
|
262
|
+
page: Page number for paginated requests (default: 1)
|
|
263
|
+
|
|
264
|
+
Example:
|
|
265
|
+
log_query_execution(logger, "AwardsSearch", [KeywordsFilter(...), TimePeriodFilter(...)], "/api/v2/search/spending_by_award/", 2)
|
|
266
|
+
"""
|
|
267
|
+
filters_count = len(filter_objects)
|
|
268
|
+
|
|
269
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
270
|
+
# Create filter breakdown by type
|
|
271
|
+
filter_types = [type(f).__name__ for f in filter_objects]
|
|
272
|
+
filter_counts = Counter(filter_types)
|
|
273
|
+
|
|
274
|
+
if filter_counts:
|
|
275
|
+
filter_breakdown = ", ".join(
|
|
276
|
+
[
|
|
277
|
+
f"{count} {filter_type}"
|
|
278
|
+
for filter_type, count in filter_counts.items()
|
|
279
|
+
]
|
|
280
|
+
)
|
|
281
|
+
filter_summary = f"{filters_count} filters ({filter_breakdown})"
|
|
282
|
+
else:
|
|
283
|
+
filter_summary = "0 filters"
|
|
284
|
+
|
|
285
|
+
logger.debug(
|
|
286
|
+
f"Executing {query_type} query - {filter_summary}, "
|
|
287
|
+
f"endpoint: {endpoint}, page: {page}"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Log individual filter details in debug mode
|
|
291
|
+
for i, filter_obj in enumerate(filter_objects):
|
|
292
|
+
filter_dict = filter_obj.to_dict()
|
|
293
|
+
logger.debug(f"Filter {i + 1}: {type(filter_obj).__name__} = {filter_dict}")
|
|
294
|
+
|
|
295
|
+
else:
|
|
296
|
+
# Extract filter keys for INFO level logging
|
|
297
|
+
if filters_count == 0:
|
|
298
|
+
logger.info(f"Executing {query_type} query - no filters")
|
|
299
|
+
else:
|
|
300
|
+
# Get the keys from each filter's dictionary representation
|
|
301
|
+
filter_keys = []
|
|
302
|
+
for filter_obj in filter_objects:
|
|
303
|
+
filter_dict = filter_obj.to_dict()
|
|
304
|
+
# Each filter's to_dict() returns a dict with one key
|
|
305
|
+
filter_keys.extend(filter_dict.keys())
|
|
306
|
+
|
|
307
|
+
# Join unique keys (in case of duplicates)
|
|
308
|
+
unique_keys = list(dict.fromkeys(filter_keys)) # Preserves order
|
|
309
|
+
keys_str = ", ".join(unique_keys)
|
|
310
|
+
logger.info(f"Executing {query_type} query - filters: {keys_str}")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Model classes for USASpending data structures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# Base classes
|
|
6
|
+
from .base_model import BaseModel, ClientAwareModel
|
|
7
|
+
from .lazy_record import LazyRecord
|
|
8
|
+
|
|
9
|
+
# Core models
|
|
10
|
+
from .award import Award
|
|
11
|
+
from .contract import Contract
|
|
12
|
+
from .grant import Grant
|
|
13
|
+
from .idv import IDV
|
|
14
|
+
from .loan import Loan
|
|
15
|
+
from .recipient import Recipient
|
|
16
|
+
from .location import Location
|
|
17
|
+
from .transaction import Transaction
|
|
18
|
+
from .funding import Funding
|
|
19
|
+
from .period_of_performance import PeriodOfPerformance
|
|
20
|
+
from .award_factory import create_award
|
|
21
|
+
from .agency import Agency
|
|
22
|
+
from .subtier_agency import SubTierAgency
|
|
23
|
+
from .subaward import SubAward
|
|
24
|
+
|
|
25
|
+
# Spending models
|
|
26
|
+
from .spending import Spending
|
|
27
|
+
from .recipient_spending import RecipientSpending
|
|
28
|
+
from .district_spending import DistrictSpending
|
|
29
|
+
from .state_spending import StateSpending
|
|
30
|
+
|
|
31
|
+
# Award type constants
|
|
32
|
+
from .award_types import (
|
|
33
|
+
AWARD_TYPE_GROUPS,
|
|
34
|
+
AWARD_TYPE_DESCRIPTIONS,
|
|
35
|
+
CONTRACT_CODES,
|
|
36
|
+
IDV_CODES,
|
|
37
|
+
LOAN_CODES,
|
|
38
|
+
GRANT_CODES,
|
|
39
|
+
DIRECT_PAYMENT_CODES,
|
|
40
|
+
OTHER_CODES,
|
|
41
|
+
ALL_AWARD_CODES,
|
|
42
|
+
get_category_for_code,
|
|
43
|
+
is_valid_award_type,
|
|
44
|
+
get_description,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
# Base classes
|
|
49
|
+
"BaseModel",
|
|
50
|
+
"ClientAwareModel",
|
|
51
|
+
"LazyRecord",
|
|
52
|
+
# Core models
|
|
53
|
+
"Award",
|
|
54
|
+
"Contract",
|
|
55
|
+
"Grant",
|
|
56
|
+
"IDV",
|
|
57
|
+
"Loan",
|
|
58
|
+
"Recipient",
|
|
59
|
+
"Location",
|
|
60
|
+
"Transaction",
|
|
61
|
+
"Funding",
|
|
62
|
+
"PeriodOfPerformance",
|
|
63
|
+
"Agency",
|
|
64
|
+
"SubTierAgency",
|
|
65
|
+
"SubAward",
|
|
66
|
+
# Spending models
|
|
67
|
+
"Spending",
|
|
68
|
+
"RecipientSpending",
|
|
69
|
+
"DistrictSpending",
|
|
70
|
+
"StateSpending",
|
|
71
|
+
# Factory
|
|
72
|
+
"create_award",
|
|
73
|
+
# Award type constants
|
|
74
|
+
"AWARD_TYPE_GROUPS",
|
|
75
|
+
"AWARD_TYPE_DESCRIPTIONS",
|
|
76
|
+
"CONTRACT_CODES",
|
|
77
|
+
"IDV_CODES",
|
|
78
|
+
"LOAN_CODES",
|
|
79
|
+
"GRANT_CODES",
|
|
80
|
+
"DIRECT_PAYMENT_CODES",
|
|
81
|
+
"OTHER_CODES",
|
|
82
|
+
"ALL_AWARD_CODES",
|
|
83
|
+
"get_category_for_code",
|
|
84
|
+
"is_valid_award_type",
|
|
85
|
+
"get_description",
|
|
86
|
+
]
|