usaspending-rb 0.1.2

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 (33) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +49 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +354 -0
  5. data/lib/usaspending/client.rb +153 -0
  6. data/lib/usaspending/configuration.rb +58 -0
  7. data/lib/usaspending/error.rb +66 -0
  8. data/lib/usaspending/resources/agency.rb +274 -0
  9. data/lib/usaspending/resources/autocomplete.rb +302 -0
  10. data/lib/usaspending/resources/award_spending.rb +24 -0
  11. data/lib/usaspending/resources/awards.rb +240 -0
  12. data/lib/usaspending/resources/base.rb +31 -0
  13. data/lib/usaspending/resources/budget_functions.rb +28 -0
  14. data/lib/usaspending/resources/bulk_download.rb +79 -0
  15. data/lib/usaspending/resources/disaster.rb +294 -0
  16. data/lib/usaspending/resources/download.rb +129 -0
  17. data/lib/usaspending/resources/federal_accounts.rb +111 -0
  18. data/lib/usaspending/resources/federal_obligations.rb +24 -0
  19. data/lib/usaspending/resources/financial_balances.rb +22 -0
  20. data/lib/usaspending/resources/financial_spending.rb +44 -0
  21. data/lib/usaspending/resources/idv.rb +93 -0
  22. data/lib/usaspending/resources/recipient.rb +130 -0
  23. data/lib/usaspending/resources/references.rb +236 -0
  24. data/lib/usaspending/resources/reporting.rb +117 -0
  25. data/lib/usaspending/resources/search.rb +228 -0
  26. data/lib/usaspending/resources/spending.rb +111 -0
  27. data/lib/usaspending/resources/spending_explorer.rb +31 -0
  28. data/lib/usaspending/resources/subawards.rb +24 -0
  29. data/lib/usaspending/resources/transactions.rb +24 -0
  30. data/lib/usaspending/response.rb +164 -0
  31. data/lib/usaspending/version.rb +5 -0
  32. data/lib/usaspending.rb +212 -0
  33. metadata +209 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f51a07faf6f074e31046ff7938b346bef6b19733eef7036861f157d388b1ca1f
4
+ data.tar.gz: 1547247f8b6dbdad18bc27b484e65e49905c67ea6d55f89244423485fe5e1c2a
5
+ SHA512:
6
+ metadata.gz: 3f62a38fcc8302ea54b37f1de49eeebab30d43843351bec16b2be1029ff79450593b7f76e33d54ce630d678f1aaf31f0411d4f8888bd1f04bc56a0bd68dfa35e
7
+ data.tar.gz: 6ffcb3b9f7c27046c530b90419f5875d95110c664b308e66d82e1229739f2c12804a3f8e67c173d1543a98d96fc8abb701771b4cfd605ed334f74d6f5f107cad
data/CHANGELOG.md ADDED
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.1.2] - 2026-03-10
6
+
7
+ ### Fixed
8
+ - Fix geo_layer values: `congressional_district` → `district` to match actual API
9
+ - Fix NAICS autocomplete response key in examples
10
+ - Fix basic_search example referencing unavailable `total_count`
11
+ - Update YARD docs for correct geo_layer values across Search, Spending, Disaster
12
+
13
+ ### Changed
14
+ - Author and license updated to Jack Killilea
15
+
16
+ ### Added
17
+ - `bin/console` for interactive IRB sessions
18
+
19
+ ## [0.1.0] - 2026-03-10
20
+
21
+ ### Added
22
+ - **21 resource classes** covering ~120 USAspending.gov v2 API endpoints
23
+ - **Awards**: search, detail, accounts, funding, counts, and cursor-paginated `search_all`
24
+ - **Agency**: overview, budgets, sub-agencies, federal accounts, object classes, program activities, and TAS breakdowns (20 endpoints)
25
+ - **Search**: spending by award, geography, category (15 category methods via metaprogramming), transaction, time series, and counts
26
+ - **Spending**: validated geographic and categorical breakdowns with `for_district` convenience method
27
+ - **Recipient**: search, profiles, child entities, state-level data
28
+ - **Autocomplete**: agencies, recipients, locations, NAICS, PSC, CFDA, glossary, city, and TAS components (19 endpoints)
29
+ - **References**: CFDA/assistance listings, NAICS, PSC trees, TAS trees, DEF codes, glossary, data dictionary, submission periods
30
+ - **Disaster**: COVID/IIJA emergency fund spending — overview, agency/recipient/CFDA/federal account breakdowns, loans, geography
31
+ - **Federal Accounts**: list, detail, snapshots, program activities, object classes
32
+ - **Downloads**: award/transaction/account CSV generation, status polling, bulk downloads, monthly file listings
33
+ - **IDV**: indefinite delivery vehicle awards, activity, funding, amounts
34
+ - **Subawards & Transactions**: paginated listings for prime awards
35
+ - **Spending Explorer**: drill-down visualization data
36
+ - **Reporting**: agency submission history, discrepancies, unlinked awards
37
+ - **Financial**: balances by agency, spending by object class, federal obligations, budget functions
38
+ - **Award Spending**: recipient breakdowns by fiscal year and agency
39
+ - Configurable timeouts, retries, and exponential backoff on 429/5xx
40
+ - Response wrapper with `Enumerable`, pagination helpers, `inspect`, `to_json`, `success?`
41
+ - Error hierarchy: `HttpError` base with `BadRequestError`, `NotFoundError`, `UnprocessableEntityError`, `RateLimitError`, `ServerError`, `ConnectionError`
42
+ - Per-instance client support (`USAspending::Client.new(config)`) with memoized resource accessors
43
+ - User-Agent header (`usaspending-rb/VERSION`)
44
+ - YARD documentation with `@param`, `@return`, `@raise`, `@example` tags
45
+ - RSpec test suite (181 specs, 91%+ coverage)
46
+ - SimpleCov with 85% minimum coverage threshold
47
+ - RuboCop + rubocop-rspec (0 offenses)
48
+ - VCR + WebMock for HTTP fixture recording
49
+ - Rakefile with `spec`, `rubocop`, and `yard` tasks
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Jack Killilea
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,354 @@
1
+ # USAspending
2
+
3
+ A Ruby client for the [USAspending.gov](https://api.usaspending.gov) REST API v2. Access federal award data, agency spending, geographic breakdowns, recipient profiles, and more.
4
+
5
+ No API key required. The USAspending.gov API is fully public.
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem 'usaspending'
13
+ ```
14
+
15
+ Then `bundle install`, or install directly:
16
+
17
+ ```bash
18
+ gem install usaspending
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```ruby
24
+ require 'usaspending'
25
+
26
+ # Search for contracts
27
+ response = USAspending.awards.search(
28
+ filters: { award_type_codes: %w[A B C D] },
29
+ limit: 10
30
+ )
31
+ response.each { |award| puts award['Recipient Name'] }
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### Award Search
37
+
38
+ ```ruby
39
+ response = USAspending.awards.search(
40
+ filters: {
41
+ award_type_codes: %w[A B C D],
42
+ place_of_performance_locations: [
43
+ { country: 'USA', state: 'VA', congressional_district: '08' }
44
+ ]
45
+ },
46
+ fields: ['Award ID', 'Recipient Name', 'Award Amount'],
47
+ sort: 'Award Amount',
48
+ limit: 25
49
+ )
50
+
51
+ response.results # => [{ "Award ID" => "...", ... }, ...]
52
+ response.size # => 25
53
+ response.total_count # => 1234
54
+ response.next_page? # => true
55
+ ```
56
+
57
+ ### Paginate All Results
58
+
59
+ Uses cursor pagination for reliability with large result sets:
60
+
61
+ ```ruby
62
+ USAspending.awards.search_all(
63
+ filters: { award_type_codes: %w[A B C D] },
64
+ limit: 100
65
+ ) do |page|
66
+ page.each { |award| save_to_db(award) }
67
+ end
68
+ ```
69
+
70
+ ### Award Detail
71
+
72
+ ```ruby
73
+ award = USAspending.awards.find('CONT_AWD_N0018923C0001_9700_-NONE-_-NONE-')
74
+ award['description']
75
+ award['total_obligation']
76
+
77
+ # Related data
78
+ USAspending.awards.accounts(award_id: 'CONT_AWD_...')
79
+ USAspending.awards.funding(award_id: 'CONT_AWD_...')
80
+ USAspending.awards.transaction_count('CONT_AWD_...')
81
+ ```
82
+
83
+ ### Geographic Spending
84
+
85
+ ```ruby
86
+ # State-level breakdown
87
+ response = USAspending.spending.by_geography(
88
+ scope: 'place_of_performance',
89
+ geo_layer: 'state',
90
+ filters: { fiscal_years: [2025] }
91
+ )
92
+
93
+ # Congressional district convenience method
94
+ response = USAspending.spending.for_district(
95
+ state_abbr: 'VA',
96
+ district: '08',
97
+ fiscal_years: [2025]
98
+ )
99
+ ```
100
+
101
+ ### Category Breakdown
102
+
103
+ ```ruby
104
+ # By NAICS industry code
105
+ USAspending.spending.by_category(
106
+ category: 'naics',
107
+ filters: { fiscal_years: [2025] },
108
+ limit: 25
109
+ )
110
+
111
+ # Or use Search directly for any of the 15 categories
112
+ USAspending.search.spending_by_awarding_agency(filters: { fiscal_years: [2025] })
113
+ USAspending.search.spending_by_recipient(filters: { fiscal_years: [2025] })
114
+ USAspending.search.spending_by_cfda(filters: { fiscal_years: [2025] })
115
+ USAspending.search.spending_by_naics(filters: { fiscal_years: [2025] })
116
+ USAspending.search.spending_by_psc(filters: { fiscal_years: [2025] })
117
+ # ... and 10 more category methods
118
+ ```
119
+
120
+ ### Agency Data
121
+
122
+ ```ruby
123
+ # List all top-tier agencies
124
+ agencies = USAspending.agency.list
125
+
126
+ # Agency overview (e.g. Treasury = "020")
127
+ treasury = USAspending.agency.overview('020', fiscal_year: 2025)
128
+
129
+ # Drill down
130
+ USAspending.agency.sub_agencies('020', fiscal_year: 2025)
131
+ USAspending.agency.federal_accounts('020', fiscal_year: 2025)
132
+ USAspending.agency.budget_function('020', fiscal_year: 2025)
133
+ USAspending.agency.obligations_by_award_category('020', fiscal_year: 2025)
134
+ USAspending.agency.budgetary_resources('020')
135
+ ```
136
+
137
+ ### Recipient Data
138
+
139
+ ```ruby
140
+ # Search recipients
141
+ results = USAspending.recipient.search('Lockheed Martin')
142
+
143
+ # Recipient profile
144
+ profile = USAspending.recipient.find('abc123-hash-value')
145
+
146
+ # State-level data
147
+ states = USAspending.recipient.states
148
+ virginia = USAspending.recipient.state('51') # FIPS code
149
+ ```
150
+
151
+ ### Autocomplete
152
+
153
+ ```ruby
154
+ USAspending.autocomplete.location('22201', geo_layer: 'zip_code')
155
+ USAspending.autocomplete.naics('541')
156
+ USAspending.autocomplete.recipient('Lockheed')
157
+ USAspending.autocomplete.awarding_agency('Treasury')
158
+ USAspending.autocomplete.city('Arlington', state_code: 'VA')
159
+ USAspending.autocomplete.psc('R425')
160
+ ```
161
+
162
+ ### Reference Data
163
+
164
+ ```ruby
165
+ USAspending.references.toptier_agencies
166
+ USAspending.references.assistance_listings # CFDA programs
167
+ USAspending.references.def_codes # Disaster emergency fund codes
168
+ USAspending.references.naics(depth: 2) # NAICS industry codes
169
+ USAspending.references.psc_tree # Product service codes
170
+ USAspending.references.data_dictionary
171
+ USAspending.references.glossary
172
+ USAspending.references.submission_periods
173
+ ```
174
+
175
+ ### Disaster / Emergency Fund Spending
176
+
177
+ ```ruby
178
+ # COVID spending overview
179
+ USAspending.disaster.overview(def_codes: %w[L M N])
180
+
181
+ # Drill down by entity
182
+ USAspending.disaster.agency_spending(def_codes: %w[L M])
183
+ USAspending.disaster.recipient_spending(def_codes: %w[L])
184
+ USAspending.disaster.cfda_spending(def_codes: %w[L])
185
+
186
+ # Geographic breakdown
187
+ USAspending.disaster.spending_by_geography(def_codes: %w[L], geo_layer: 'state')
188
+ ```
189
+
190
+ ### Spending Explorer
191
+
192
+ ```ruby
193
+ USAspending.spending_explorer.explore(
194
+ type: 'budget_function',
195
+ filters: { fy: '2025', quarter: '1' }
196
+ )
197
+ ```
198
+
199
+ ### Downloads
200
+
201
+ ```ruby
202
+ # Generate a CSV download
203
+ response = USAspending.download.awards(
204
+ filters: { award_type_codes: %w[A], fiscal_years: [2025] }
205
+ )
206
+ file_name = response['file_name']
207
+
208
+ # Check status
209
+ USAspending.download.status(file_name)
210
+
211
+ # Bulk downloads
212
+ USAspending.bulk_download.awards(filters: { fiscal_years: [2025] })
213
+ ```
214
+
215
+ ### Federal Accounts
216
+
217
+ ```ruby
218
+ USAspending.federal_accounts.list
219
+ USAspending.federal_accounts.find('020-0001') # by account code
220
+ USAspending.federal_accounts.fiscal_year_snapshot(1234) # by numeric ID
221
+ ```
222
+
223
+ ### Additional Resources
224
+
225
+ ```ruby
226
+ # Transactions for an award
227
+ USAspending.transactions.list(award_id: 'CONT_AWD_...')
228
+
229
+ # Subawards
230
+ USAspending.subawards.list(award_id: 'CONT_AWD_...')
231
+
232
+ # IDV (Indefinite Delivery Vehicles)
233
+ USAspending.idv.amounts('IDV_AWD_...')
234
+ USAspending.idv.awards(award_id: 'IDV_AWD_...')
235
+
236
+ # Reporting
237
+ USAspending.reporting.agencies_overview(fiscal_year: 2025, fiscal_period: 6)
238
+
239
+ # Financial data
240
+ USAspending.financial_balances.agencies(funding_agency_identifier: '020', fiscal_year: 2025)
241
+ USAspending.budget_functions.list
242
+ ```
243
+
244
+ ## Configuration
245
+
246
+ ```ruby
247
+ USAspending.configure do |config|
248
+ config.timeout = 60 # Request timeout in seconds (default: 30)
249
+ config.retries = 5 # Max retries on 429/5xx (default: 3)
250
+ config.logger = Logger.new($stdout) # Optional request logging
251
+ end
252
+ ```
253
+
254
+ ### Rails Initializer
255
+
256
+ ```ruby
257
+ # config/initializers/usaspending.rb
258
+ USAspending.configure do |config|
259
+ config.timeout = 30
260
+ config.retries = 3
261
+ config.logger = Rails.logger if Rails.env.development?
262
+ end
263
+ ```
264
+
265
+ ### Per-Instance Client
266
+
267
+ For different configurations in the same process:
268
+
269
+ ```ruby
270
+ client = USAspending::Client.new(custom_config)
271
+ client.awards.search(filters: { ... })
272
+ client.agency.overview('020')
273
+ ```
274
+
275
+ ## Response Object
276
+
277
+ All API calls return a `USAspending::Response`:
278
+
279
+ ```ruby
280
+ response = USAspending.awards.search(filters: { ... })
281
+
282
+ response.results # Array of result hashes
283
+ response.each { |r| ... } # Enumerable — supports map, select, first, etc.
284
+ response.size # Results on this page
285
+ response.empty? # No results?
286
+ response.total_count # Total matching records (not just this page)
287
+ response.next_page? # More pages available?
288
+ response.last_record_unique_id # Cursor for next page
289
+ response.success? # HTTP 2xx?
290
+
291
+ response['key'] # Direct hash access
292
+ response.dig('nested', 'key')
293
+ response.raw # Raw parsed JSON body
294
+ response.to_h # Always returns a Hash
295
+ response.to_json # JSON string
296
+ ```
297
+
298
+ ## Error Handling
299
+
300
+ ```ruby
301
+ begin
302
+ USAspending.awards.find('INVALID')
303
+ rescue USAspending::BadRequestError => e
304
+ puts "Bad request: #{e.status} — #{e.body}"
305
+ rescue USAspending::NotFoundError => e
306
+ puts "Not found: #{e.status}"
307
+ rescue USAspending::UnprocessableEntityError => e
308
+ puts "Invalid filters: #{e.body}"
309
+ rescue USAspending::RateLimitError
310
+ puts 'Rate limited — retries exhausted'
311
+ rescue USAspending::ServerError => e
312
+ puts "Server error: #{e.status}"
313
+ rescue USAspending::ConnectionError => e
314
+ puts "Network issue: #{e.message}"
315
+ end
316
+ ```
317
+
318
+ Error hierarchy:
319
+
320
+ ```
321
+ USAspending::Error
322
+ ├── HttpError
323
+ │ ├── ClientError
324
+ │ │ ├── BadRequestError (400)
325
+ │ │ ├── NotFoundError (404)
326
+ │ │ ├── UnprocessableEntityError (422)
327
+ │ │ └── RateLimitError (429)
328
+ │ └── ServerError (500+)
329
+ └── ConnectionError (timeouts, DNS, refused)
330
+ ```
331
+
332
+ All `HttpError` subclasses expose `.status` and `.body`.
333
+
334
+ ## Development
335
+
336
+ ```bash
337
+ git clone https://github.com/thepublictab/usaspending-rb.git
338
+ cd usaspending-rb
339
+ bundle install
340
+
341
+ bundle exec rspec # Run tests
342
+ bundle exec rubocop # Lint
343
+ bundle exec yard # Generate docs
344
+ bundle exec rake # Default: runs rspec
345
+ ```
346
+
347
+ ## Requirements
348
+
349
+ - Ruby >= 3.1
350
+ - Faraday ~> 2.0
351
+
352
+ ## License
353
+
354
+ MIT License. See [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday/retry'
5
+ require 'json'
6
+
7
+ module USAspending
8
+ # HTTP client that wraps Faraday with retry logic and error handling.
9
+ # Supports both module-level and per-instance usage patterns.
10
+ #
11
+ # @example Module-level (shared client)
12
+ # USAspending.awards.search(filters: { ... })
13
+ #
14
+ # @example Per-instance (custom config)
15
+ # client = USAspending::Client.new(config)
16
+ # client.awards.search(filters: { ... })
17
+ class Client
18
+ # @return [Configuration] the configuration used by this client
19
+ attr_reader :config
20
+
21
+ # @param config [Configuration] configuration object (defaults to global config)
22
+ def initialize(config = USAspending.configuration)
23
+ @config = config
24
+ end
25
+
26
+ # Returns the Faraday connection, lazily built.
27
+ # @return [Faraday::Connection]
28
+ def connection
29
+ @connection ||= Faraday.new(url: config.base_url) do |f|
30
+ f.options.timeout = config.timeout
31
+ f.options.open_timeout = 10
32
+
33
+ f.headers['User-Agent'] = "usaspending-rb/#{USAspending::VERSION}"
34
+
35
+ f.request :json
36
+ f.response :json, content_type: /\bjson$/
37
+
38
+ f.request :retry,
39
+ max: config.retries,
40
+ interval: 0.5,
41
+ interval_randomness: 0.5,
42
+ backoff_factor: 2,
43
+ retry_statuses: [429, 500, 502, 503, 504],
44
+ exceptions: [
45
+ Faraday::TimeoutError,
46
+ Faraday::ConnectionFailed,
47
+ Faraday::RetriableResponse
48
+ ]
49
+
50
+ f.response :logger, config.logger if config.logger
51
+ f.adapter config.adapter
52
+ end
53
+ end
54
+
55
+ # Performs a GET request.
56
+ #
57
+ # @param path [String] API endpoint path relative to base URL
58
+ # @param params [Hash] query parameters
59
+ # @return [Response]
60
+ # @raise [HttpError] on any non-2xx response
61
+ # @raise [ConnectionError] on network failures or timeouts
62
+ def get(path, params = {})
63
+ response = connection.get(path, params)
64
+ handle_response(response)
65
+ rescue Faraday::TimeoutError => e
66
+ raise ConnectionError, "Request timed out: #{e.message}"
67
+ rescue Faraday::ConnectionFailed => e
68
+ raise ConnectionError, "Connection failed: #{e.message}"
69
+ end
70
+
71
+ # Performs a POST request with a JSON body.
72
+ #
73
+ # @param path [String] API endpoint path relative to base URL
74
+ # @param body [Hash] request body (serialized to JSON)
75
+ # @return [Response]
76
+ # @raise [HttpError] on any non-2xx response
77
+ # @raise [ConnectionError] on network failures or timeouts
78
+ def post(path, body = {})
79
+ response = connection.post(path, body)
80
+ handle_response(response)
81
+ rescue Faraday::TimeoutError => e
82
+ raise ConnectionError, "Request timed out: #{e.message}"
83
+ rescue Faraday::ConnectionFailed => e
84
+ raise ConnectionError, "Connection failed: #{e.message}"
85
+ end
86
+
87
+ # @!group Resource Accessors
88
+
89
+ # @!macro [attach] resource_accessor
90
+ # @return [Resources::$1]
91
+ RESOURCE_ACCESSORS = {
92
+ awards: 'Awards',
93
+ agency: 'Agency',
94
+ spending: 'Spending',
95
+ recipient: 'Recipient',
96
+ autocomplete: 'Autocomplete',
97
+ references: 'References',
98
+ disaster: 'Disaster',
99
+ search: 'Search',
100
+ federal_accounts: 'FederalAccounts',
101
+ download: 'Download',
102
+ bulk_download: 'BulkDownload',
103
+ idv: 'Idv',
104
+ subawards: 'Subawards',
105
+ transactions: 'Transactions',
106
+ spending_explorer: 'SpendingExplorer',
107
+ reporting: 'Reporting',
108
+ federal_obligations: 'FederalObligations',
109
+ financial_balances: 'FinancialBalances',
110
+ financial_spending: 'FinancialSpending',
111
+ budget_functions: 'BudgetFunctions',
112
+ award_spending: 'AwardSpending'
113
+ }.freeze
114
+
115
+ RESOURCE_ACCESSORS.each do |method_name, class_name|
116
+ define_method(method_name) do
117
+ ivar = "@#{method_name}"
118
+ instance_variable_get(ivar) || instance_variable_set(ivar, Resources.const_get(class_name).new(self))
119
+ end
120
+ end
121
+
122
+ # @!endgroup
123
+
124
+ # Concise representation for IRB/Pry debugging.
125
+ # @return [String]
126
+ def inspect
127
+ "#<#{self.class} base_url=#{config.base_url.inspect} timeout=#{config.timeout}>"
128
+ end
129
+
130
+ private
131
+
132
+ def handle_response(response)
133
+ case response.status
134
+ when 200..299
135
+ Response.new(response.body, response.status)
136
+ when 400
137
+ raise BadRequestError.new(status: response.status, body: response.body)
138
+ when 404
139
+ raise NotFoundError.new(status: response.status, body: response.body)
140
+ when 422
141
+ raise UnprocessableEntityError.new(status: response.status, body: response.body)
142
+ when 429
143
+ raise RateLimitError.new(status: 429, body: response.body)
144
+ when 401..499
145
+ raise ClientError.new(status: response.status, body: response.body)
146
+ when 500..599
147
+ raise ServerError.new(status: response.status, body: response.body)
148
+ else
149
+ raise Error, "Unexpected HTTP status: #{response.status}"
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module USAspending
4
+ # Holds configuration options for the USAspending client.
5
+ #
6
+ # @example
7
+ # USAspending.configure do |config|
8
+ # config.base_url = "https://api.usaspending.gov/api/v2/"
9
+ # config.timeout = 60
10
+ # config.retries = 5
11
+ # config.logger = Logger.new($stdout)
12
+ # end
13
+ class Configuration
14
+ DEFAULT_BASE_URL = 'https://api.usaspending.gov/api/v2/'
15
+ DEFAULT_TIMEOUT = 30
16
+ DEFAULT_RETRIES = 3
17
+
18
+ # @return [String] base URL for the USAspending API
19
+ attr_accessor :base_url
20
+
21
+ # @return [Integer] request timeout in seconds
22
+ attr_accessor :timeout
23
+
24
+ # @return [Integer] maximum number of retries on 429/5xx responses
25
+ attr_accessor :retries
26
+
27
+ # @return [Logger, nil] optional logger for request/response logging
28
+ attr_accessor :logger
29
+
30
+ # @return [Symbol] Faraday adapter to use (default: Faraday.default_adapter)
31
+ attr_accessor :adapter
32
+
33
+ def initialize
34
+ @base_url = DEFAULT_BASE_URL
35
+ @timeout = DEFAULT_TIMEOUT
36
+ @retries = DEFAULT_RETRIES
37
+ @logger = nil
38
+ @adapter = Faraday.default_adapter
39
+ end
40
+
41
+ # @return [Hash] configuration as a plain hash
42
+ def to_h
43
+ {
44
+ base_url: base_url,
45
+ timeout: timeout,
46
+ retries: retries,
47
+ logger: logger,
48
+ adapter: adapter
49
+ }
50
+ end
51
+
52
+ # Concise representation for IRB/Pry debugging.
53
+ # @return [String]
54
+ def inspect
55
+ "#<#{self.class} base_url=#{base_url.inspect} timeout=#{timeout} retries=#{retries}>"
56
+ end
57
+ end
58
+ end