Coverage for src/usaspending/config.py: 89%
71 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-03 17:15 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-03 17:15 -0700
1from __future__ import annotations
2from datetime import timedelta
3from typing import Optional
4from usaspending.logging_config import USASpendingLogger
5from usaspending.exceptions import ConfigurationError
6import os
7import cachier
9logger = USASpendingLogger.get_logger(__name__)
12class _Config:
13 """
14 A container for all library configuration settings.
15 Do not instantiate this class directly. Instead, import and use the global `config` object.
16 """
18 def __init__(self):
19 # Default settings are defined here as instance attributes
20 self.base_url: str = "https://api.usaspending.gov/api/v2/"
21 self.user_agent: str = "usaspendingapi-python/0.1.0"
22 self.timeout: int = 30
23 self.max_retries: int = 3
24 self.retry_delay: float = 1.0
25 self.retry_backoff: float = 2.0
26 self.rate_limit_calls: int = 30
27 self.rate_limit_period: int = 1
29 # Caching via cachier
30 self.cache_enabled: bool = True
31 self.cache_backend: str = "pickle" # Default file-based backend for cachier
32 self.cache_dir: str = os.path.join(os.environ.get('XDG_CACHE_HOME', os.path.expanduser('~/.cache')), 'usaspending')
33 self.cache_ttl: timedelta = timedelta(weeks=1)
35 # Logging configuration
36 self.logging_level: str = "DEBUG"
37 self.debug_mode: bool = True
38 self.log_file: Optional[str] = None
40 # Apply the initial default settings when the object is created
41 self._apply_cachier_settings()
43 def configure(self, **kwargs):
44 """
45 Updates configuration settings and applies them across the library.
47 This is the primary method for users to modify the library's behavior.
48 Any keyword argument passed will overwrite the existing configuration value.
50 Args:
51 **kwargs: Configuration keys and their new values.
53 Raises:
54 ConfigurationError: If any provided configuration value is invalid.
55 """
56 for key, value in kwargs.items():
57 if hasattr(self, key):
58 if key == "cache_ttl" and isinstance(value, (int, float)):
59 self.cache_ttl = timedelta(seconds=value)
60 else:
61 setattr(self, key, value)
62 else:
63 logger.warning(
64 f"Warning: Unknown configuration key '{key}' was ignored."
65 )
67 self.validate()
68 self._apply_cachier_settings()
69 self._apply_logging_settings()
71 def _apply_cachier_settings(self):
72 """Applies the current caching settings to the cachier library."""
73 if self.cache_enabled:
74 if self.cache_backend == "file":
75 cache_backend = "pickle" # cachier uses 'pickle' for file caching
76 else:
77 cache_backend = self.cache_backend
78 cachier.set_global_params(
79 stale_after=self.cache_ttl,
80 cache_dir=self.cache_dir,
81 backend=cache_backend,
82 )
83 cachier.enable_caching()
84 else:
85 cachier.disable_caching()
87 def _apply_logging_settings(self):
88 """Applies the current logging settings to the logger."""
89 # This is the logic moved from your client file
90 USASpendingLogger.configure(
91 level=self.logging_level,
92 debug_mode=self.debug_mode,
93 log_file=self.log_file,
94 )
96 def validate(self) -> None:
97 """Validate the current configuration values."""
98 if self.timeout <= 0:
99 raise ConfigurationError("timeout must be positive")
100 if self.max_retries < 0:
101 raise ConfigurationError("max_retries must be non-negative")
102 if self.rate_limit_calls <= 0:
103 raise ConfigurationError("rate_limit_calls must be positive")
105 valid_log_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
106 if self.logging_level.upper() not in valid_log_levels:
107 raise ConfigurationError(
108 f"logging_level must be one of: {valid_log_levels}"
109 )
111 valid_backends = {"file", "memory"}
112 if self.cache_enabled and (self.cache_backend not in valid_backends):
113 raise ConfigurationError(f"cache_backend must be one of: {valid_backends}")
116# Global configuration object
117# This is the single instance that should be used throughout the library
118config = _Config()
121# In src/usaspendingapi/config.py
123AWARD_TYPE_GROUPS = {
124 "contracts": {
125 "A": "BPA Call",
126 "B": "Purchase Order",
127 "C": "Delivery Order",
128 "D": "Definitive Contract",
129 },
130 "loans": {"07": "Direct Loan", "08": "Guaranteed/Insured Loan"},
131 "idvs": {
132 "IDV_A": "GWAC Government Wide Acquisition Contract",
133 "IDV_B": "IDC Multi-Agency Contract, Other Indefinite Delivery Contract",
134 "IDV_B_A": "IDC Indefinite Delivery Contract / Requirements",
135 "IDV_B_B": "IDC Indefinite Delivery Contract / Indefinite Quantity",
136 "IDV_B_C": "IDC Indefinite Delivery Contract / Definite Quantity",
137 "IDV_C": "FSS Federal Supply Schedule",
138 "IDV_D": "BOA Basic Ordering Agreement",
139 "IDV_E": "BPA Blanket Purchase Agreement",
140 },
141 "grants": {
142 "02": "Block Grant",
143 "03": "Formula Grant",
144 "04": "Project Grant",
145 "05": "Cooperative Agreement",
146 },
147 "direct_payments": {
148 "06": "Direct Payment for Specified Use",
149 "10": "Direct Payment with Unrestricted Use",
150 },
151 "other_assistance": {
152 "09": "Insurance",
153 "11": "Other Financial Assistance",
154 "-1": "Not Specified",
155 },
156}
158# Create a flattened map for easy description lookups
159AWARD_TYPE_DESCRIPTIONS = {
160 code: description
161 for group in AWARD_TYPE_GROUPS.values()
162 for code, description in group.items()
163}
165# Regenerate frozensets from this single source of truth
166CONTRACT_CODES = frozenset(AWARD_TYPE_GROUPS["contracts"].keys())
167IDV_CODES = frozenset(AWARD_TYPE_GROUPS["idvs"].keys())
168LOAN_CODES = frozenset(AWARD_TYPE_GROUPS["loans"].keys())
169GRANT_CODES = frozenset(AWARD_TYPE_GROUPS["grants"].keys())
170DIRECT_PAYMENT_CODES = frozenset(AWARD_TYPE_GROUPS["direct_payments"].keys())
171OTHER_CODES = frozenset(AWARD_TYPE_GROUPS["other_assistance"].keys())
173# Dictionary of Business Categories that pair them with their human readable name
174# Taken directly from USASpending API source
175BUSINESS_CATEGORIES = frozenset(
176 {
177 # Category Business
178 "category_business": "Category Business",
179 "small_business": "Small Business",
180 "other_than_small_business": "Not Designated a Small Business",
181 "corporate_entity_tax_exempt": "Corporate Entity Tax Exempt",
182 "corporate_entity_not_tax_exempt": "Corporate Entity Not Tax Exempt",
183 "partnership_or_limited_liability_partnership": "Partnership or Limited Liability Partnership",
184 "sole_proprietorship": "Sole Proprietorship",
185 "manufacturer_of_goods": "Manufacturer of Goods",
186 "subchapter_s_corporation": "Subchapter S Corporation",
187 "limited_liability_corporation": "Limited Liability Corporation",
188 # Minority Owned Business
189 "minority_owned_business": "Minority Owned Business",
190 "alaskan_native_corporation_owned_firm": "Alaskan Native Corporation Owned Firm",
191 "american_indian_owned_business": "American Indian Owned Business",
192 "asian_pacific_american_owned_business": "Asian Pacific American Owned Business",
193 "black_american_owned_business": "Black American Owned Business",
194 "hispanic_american_owned_business": "Hispanic American Owned Business",
195 "native_american_owned_business": "Native American Owned Business",
196 "native_hawaiian_organization_owned_firm": "Native Hawaiian Organization Owned Firm",
197 "subcontinent_asian_indian_american_owned_business": "Indian (Subcontinent) American Owned Business",
198 "tribally_owned_firm": "Tribally Owned Firm",
199 "other_minority_owned_business": "Other Minority Owned Business",
200 # Women Owned Business
201 "woman_owned_business": "Woman Owned Business",
202 "women_owned_small_business": "Women Owned Small Business",
203 "economically_disadvantaged_women_owned_small_business": "Economically Disadvantaged Women Owned Small Business",
204 "joint_venture_women_owned_small_business": "Joint Venture Women Owned Small Business",
205 "joint_venture_economically_disadvantaged_women_owned_small_business": "Joint Venture Economically Disadvantaged Women Owned Small Business",
206 # Veteran Owned Business
207 "veteran_owned_business": "Veteran Owned Business",
208 "service_disabled_veteran_owned_business": "Service Disabled Veteran Owned Business",
209 # Special Designations
210 "special_designations": "Special Designations",
211 "8a_program_participant": "8(a) Program Participant",
212 "ability_one_program": "AbilityOne Program Participant",
213 "dot_certified_disadvantaged_business_enterprise": "DoT Certified Disadvantaged Business Enterprise",
214 "emerging_small_business": "Emerging Small Business",
215 "federally_funded_research_and_development_corp": "Federally Funded Research and Development Corp",
216 "historically_underutilized_business_firm": "HUBZone Firm",
217 "labor_surplus_area_firm": "Labor Surplus Area Firm",
218 "sba_certified_8a_joint_venture": "SBA Certified 8 a Joint Venture",
219 "self_certified_small_disadvanted_business": "Self-Certified Small Disadvantaged Business",
220 "small_agricultural_cooperative": "Small Agricultural Cooperative",
221 "small_disadvantaged_business": "Small Disadvantaged Business",
222 "community_developed_corporation_owned_firm": "Community Developed Corporation Owned Firm",
223 "us_owned_business": "U.S.-Owned Business",
224 "foreign_owned_and_us_located_business": "Foreign-Owned and U.S.-Incorporated Business",
225 "foreign_owned": "Foreign Owned",
226 "foreign_government": "Foreign Government",
227 "international_organization": "International Organization",
228 "domestic_shelter": "Domestic Shelter",
229 "hospital": "Hospital",
230 "veterinary_hospital": "Veterinary Hospital",
231 # Nonprofit
232 "nonprofit": "Nonprofit Organization",
233 "foundation": "Foundation",
234 "community_development_corporations": "Community Development Corporation",
235 # Higher education
236 "higher_education": "Higher Education",
237 "public_institution_of_higher_education": "Higher Education (Public)",
238 "private_institution_of_higher_education": "Higher Education (Private)",
239 "minority_serving_institution_of_higher_education": "Higher Education (Minority Serving)",
240 "educational_institution": "Educational Institution",
241 "school_of_forestry": "School of Forestry",
242 "veterinary_college": "Veterinary College",
243 # Government
244 "government": "Government",
245 "national_government": "U.S. National Government",
246 "regional_and_state_government": "U.S. Regional/State Government",
247 "regional_organization": "U.S. Regional Government Organization",
248 "interstate_entity": "U.S. Interstate Government Entity",
249 "us_territory_or_possession": "U.S. Territory Government",
250 "local_government": "U.S. Local Government",
251 "indian_native_american_tribal_government": "Native American Tribal Government",
252 "authorities_and_commissions": "U.S. Government Authorities",
253 "council_of_governments": "Council of Governments",
254 # Individuals
255 "individuals": "Individuals",
256 }
257)
259# List of CFO CGACS (Common Government-wide Accounting Classification)
260# for all U.S. government agencies.
261CFO_CGACS_MAPPING = {
262 "012": "Department of Agriculture",
263 "013": "Department of Commerce",
264 "097": "Department of Defense",
265 "091": "Department of Education",
266 "089": "Department of Energy",
267 "075": "Department of Health and Human Services",
268 "070": "Department of Homeland Security",
269 "086": "Department of Housing and Urban Development",
270 "015": "Department of Justice",
271 "1601": "Department of Labor",
272 "019": "Department of State",
273 "014": "Department of the Interior",
274 "020": "Department of the Treasury",
275 "069": "Department of Transportation",
276 "036": "Department of Veterans Affairs",
277 "068": "Environmental Protection Agency",
278 "047": "General Services Administration",
279 "080": "National Aeronautics and Space Administration",
280 "049": "National Science Foundation",
281 "031": "Nuclear Regulatory Commission",
282 "024": "Office of Personnel Management",
283 "073": "Small Business Administration",
284 "028": "Social Security Administration",
285 "072": "Agency for International Development",
286}
287CFO_CGACS = list(CFO_CGACS_MAPPING.keys())