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

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 

8 

9logger = USASpendingLogger.get_logger(__name__) 

10 

11 

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 """ 

17 

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 

28 

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) 

34 

35 # Logging configuration 

36 self.logging_level: str = "DEBUG" 

37 self.debug_mode: bool = True 

38 self.log_file: Optional[str] = None 

39 

40 # Apply the initial default settings when the object is created 

41 self._apply_cachier_settings() 

42 

43 def configure(self, **kwargs): 

44 """ 

45 Updates configuration settings and applies them across the library. 

46 

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. 

49 

50 Args: 

51 **kwargs: Configuration keys and their new values. 

52 

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 ) 

66 

67 self.validate() 

68 self._apply_cachier_settings() 

69 self._apply_logging_settings() 

70 

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() 

86 

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 ) 

95 

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") 

104 

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 ) 

110 

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}") 

114 

115 

116# Global configuration object 

117# This is the single instance that should be used throughout the library 

118config = _Config() 

119 

120 

121# In src/usaspendingapi/config.py 

122 

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} 

157 

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} 

164 

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()) 

172 

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) 

258 

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())