open_fec 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8dd8d770d0957b0f17a29d0e4197501679ed4a7dea95790483bcdf4dbaa6a156
4
+ data.tar.gz: 0560f6316507f7cecfb1e307275010299f3a548810b989b53d628dfb789ef971
5
+ SHA512:
6
+ metadata.gz: 806ebc183c698c3db5b814b7cfd537afa003dfae26ab853a3b4826638d31734dad89fac780a265cacd860aeadbbb8683eb90b937ce7e7bc8eaf563dfab4d71f4
7
+ data.tar.gz: 10df7ef81f0c814c29aff29fe9e574b3797d6fd9e99229de061abc5330aa6db8bafd78c2c0452b9a7c2ae65c56ad00e44d8baf5348abb36903222c6e5370b61f
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-03-25
4
+
5
+ ### Added
6
+ - Initial release
7
+ - Candidates resource: search, find, committees
8
+ - CandidateTotals resource: financial summaries per cycle
9
+ - Committees resource: search, find
10
+ - Contributions resource: itemized Schedule A (seek-paginated)
11
+ - ContributionAggregates resource: by_employer, by_occupation, by_size, by_state
12
+ - Disbursements resource: itemized Schedule B (seek-paginated)
13
+ - Dual pagination support (offset-based and seek/cursor-based)
14
+ - Thread-safe configuration with Mutex
15
+ - Faraday 2.0 with retry middleware (429, 5xx)
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,104 @@
1
+ # OpenFec
2
+
3
+ Ruby client for the [Federal Election Commission (FEC) API](https://api.open.fec.gov/developers/).
4
+
5
+ Part of the [govapi-rb](https://github.com/govapi-rb) collection of Ruby clients for US government APIs.
6
+
7
+ ## Installation
8
+
9
+ ```ruby
10
+ gem 'open_fec'
11
+ ```
12
+
13
+ ## Configuration
14
+
15
+ Get a free API key at [api.data.gov/signup](https://api.data.gov/signup/).
16
+
17
+ ```ruby
18
+ # Via environment variable (recommended)
19
+ ENV['OPEN_FEC_API_KEY'] = 'your-api-key'
20
+
21
+ # Or via configuration block
22
+ OpenFec.configure do |c|
23
+ c.api_key = 'your-api-key'
24
+ end
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Candidates
30
+
31
+ ```ruby
32
+ # Search for candidates
33
+ results = OpenFec.candidates.search(name: 'Pelosi', state: 'CA')
34
+ results.each { |c| puts "#{c['name']} (#{c['party']})" }
35
+
36
+ # Find a specific candidate
37
+ candidate = OpenFec.candidates.find('H8CA05035')
38
+ candidate.results.first['name'] # => "PELOSI, NANCY"
39
+
40
+ # Get a candidate's committees
41
+ committees = OpenFec.candidates.committees('H8CA05035')
42
+ ```
43
+
44
+ ### Campaign Finance Totals
45
+
46
+ ```ruby
47
+ # Financial totals for a candidate
48
+ totals = OpenFec.candidate_totals.list('H8CA05035')
49
+ totals.each do |t|
50
+ puts "Cycle #{t['cycle']}: raised #{t['receipts']}, spent #{t['disbursements']}"
51
+ end
52
+
53
+ # For a specific cycle
54
+ totals = OpenFec.candidate_totals.for_cycle('H8CA05035', 2024)
55
+ ```
56
+
57
+ ### Contributions by Employer
58
+
59
+ ```ruby
60
+ # Top employer donors to a committee
61
+ employers = OpenFec.contribution_aggregates.by_employer(
62
+ committee_id: 'C00213512',
63
+ cycle: 2024,
64
+ sort: '-total',
65
+ per_page: 10
66
+ )
67
+ employers.each { |e| puts "#{e['employer']}: $#{e['total']}" }
68
+ ```
69
+
70
+ ### PAC Contributions
71
+
72
+ ```ruby
73
+ # PAC contributions to a committee
74
+ pacs = OpenFec.contributions.from_pacs(committee_id: 'C00213512', cycle: 2024)
75
+ pacs.each { |c| puts "#{c['contributor_name']}: $#{c['contribution_receipt_amount']}" }
76
+ ```
77
+
78
+ ### Pagination
79
+
80
+ Most endpoints use offset-based pagination:
81
+
82
+ ```ruby
83
+ OpenFec.candidates.each_page(state: 'VA') do |page|
84
+ page.results.each { |c| puts c['name'] }
85
+ end
86
+ ```
87
+
88
+ Itemized transaction endpoints (Schedule A, Schedule B) use seek/cursor pagination:
89
+
90
+ ```ruby
91
+ OpenFec.contributions.each_page(committee_id: 'C00213512') do |page|
92
+ page.results.each { |c| puts c['contributor_name'] }
93
+ end
94
+ ```
95
+
96
+ ## Rate Limits
97
+
98
+ - `DEMO_KEY`: 30 requests/hour
99
+ - Registered key: 1,000 requests/hour
100
+ - Contact `APIinfo@fec.gov` for higher limits
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday/retry'
5
+
6
+ module OpenFec
7
+ # HTTP client for the FEC API. Handles authentication, retries, and pagination.
8
+ #
9
+ # @example Direct usage
10
+ # client = OpenFec.client
11
+ # response = client.get('candidates/search/', name: 'Pelosi')
12
+ class Client
13
+ # @return [OpenFec::Configuration]
14
+ attr_reader :config
15
+
16
+ # @param config [OpenFec::Configuration]
17
+ # @raise [ConfigurationError] if api_key is missing
18
+ def initialize(config = OpenFec.configuration)
19
+ @config = config
20
+ config.validate!
21
+ end
22
+
23
+ # @return [Faraday::Connection]
24
+ def connection
25
+ @connection ||= build_connection
26
+ end
27
+
28
+ # Make a GET request to the FEC API.
29
+ #
30
+ # @param path [String] API endpoint path (e.g. "candidates/search/")
31
+ # @param params [Hash] query parameters
32
+ # @return [OpenFec::Response]
33
+ # @raise [ClientError, ServerError, ConnectionError]
34
+ def get(path, params = {})
35
+ response = connection.get(path, with_api_key(compact_params(params)))
36
+ handle_response(response)
37
+ rescue Faraday::TimeoutError => e
38
+ raise ConnectionError, "Request timed out: #{e.message}"
39
+ rescue Faraday::ConnectionFailed => e
40
+ raise ConnectionError, "Connection failed: #{e.message}"
41
+ end
42
+
43
+ # Iterate through offset-paginated results, yielding each page.
44
+ # Used by most FEC API endpoints.
45
+ #
46
+ # @param path [String] API endpoint path
47
+ # @param params [Hash] query parameters
48
+ # @yield [OpenFec::Response] each page of results
49
+ def paginate(path, params = {})
50
+ page = 1
51
+ iterations = 0
52
+ loop do
53
+ iterations += 1
54
+ response = get(path, params.merge(page: page, per_page: config.per_page))
55
+ yield response
56
+ break if response.empty?
57
+ break unless response.next_page?
58
+
59
+ page = response.next_page_number
60
+ break if page.nil?
61
+ break if iterations >= config.max_pages
62
+ end
63
+ end
64
+
65
+ # Iterate through seek/cursor-paginated results, yielding each page.
66
+ # Used by itemized transaction endpoints (Schedule A, Schedule B).
67
+ #
68
+ # @param path [String] API endpoint path
69
+ # @param params [Hash] query parameters
70
+ # @yield [OpenFec::Response] each page of results
71
+ def paginate_seek(path, params = {})
72
+ iterations = 0
73
+ cursor_params = {}
74
+ loop do
75
+ iterations += 1
76
+ response = get(path, params.merge(per_page: config.per_page, **cursor_params))
77
+ yield response
78
+ break if response.empty?
79
+ break unless response.next_page?
80
+
81
+ cursor_params = response.next_page_params || {}
82
+ break if cursor_params.empty?
83
+ break if iterations >= config.max_pages
84
+ end
85
+ end
86
+
87
+ # Fetch all results from an offset-paginated endpoint.
88
+ #
89
+ # @param path [String] API endpoint path
90
+ # @param params [Hash] query parameters
91
+ # @return [Array<Hash>] all result records
92
+ def all(path, params = {})
93
+ records = []
94
+ paginate(path, params) { |response| records.concat(response.results) }
95
+ records
96
+ end
97
+
98
+ # Fetch all results from a seek-paginated endpoint.
99
+ #
100
+ # @param path [String] API endpoint path
101
+ # @param params [Hash] query parameters
102
+ # @return [Array<Hash>] all result records
103
+ def all_seek(path, params = {})
104
+ records = []
105
+ paginate_seek(path, params) { |response| records.concat(response.results) }
106
+ records
107
+ end
108
+
109
+ # Resource accessors
110
+
111
+ # @return [OpenFec::Resources::Candidates]
112
+ def candidates
113
+ @candidates ||= Resources::Candidates.new(self)
114
+ end
115
+
116
+ # @return [OpenFec::Resources::CandidateTotals]
117
+ def candidate_totals
118
+ @candidate_totals ||= Resources::CandidateTotals.new(self)
119
+ end
120
+
121
+ # @return [OpenFec::Resources::Committees]
122
+ def committees
123
+ @committees ||= Resources::Committees.new(self)
124
+ end
125
+
126
+ # @return [OpenFec::Resources::Contributions]
127
+ def contributions
128
+ @contributions ||= Resources::Contributions.new(self)
129
+ end
130
+
131
+ # @return [OpenFec::Resources::ContributionAggregates]
132
+ def contribution_aggregates
133
+ @contribution_aggregates ||= Resources::ContributionAggregates.new(self)
134
+ end
135
+
136
+ # @return [OpenFec::Resources::Disbursements]
137
+ def disbursements
138
+ @disbursements ||= Resources::Disbursements.new(self)
139
+ end
140
+
141
+ # @return [OpenFec::Resources::Elections]
142
+ def elections
143
+ @elections ||= Resources::Elections.new(self)
144
+ end
145
+
146
+ # @return [OpenFec::Resources::IndependentExpenditures]
147
+ def independent_expenditures
148
+ @independent_expenditures ||= Resources::IndependentExpenditures.new(self)
149
+ end
150
+
151
+ # @return [String]
152
+ def inspect
153
+ "#<#{self.class} base_url=#{config.base_url.inspect} timeout=#{config.timeout}>"
154
+ end
155
+
156
+ private
157
+
158
+ def build_connection
159
+ Faraday.new(url: config.base_url) do |f|
160
+ f.options.timeout = config.timeout
161
+ f.options.open_timeout = 10
162
+
163
+ f.headers['User-Agent'] = "open_fec-rb/#{OpenFec::VERSION}"
164
+
165
+ f.request :json
166
+ f.response :json, content_type: /\bjson$/
167
+
168
+ f.request :retry,
169
+ max: config.retries,
170
+ interval: 0.5,
171
+ interval_randomness: 0.5,
172
+ backoff_factor: 2,
173
+ retry_statuses: [429, 500, 502, 503, 504],
174
+ exceptions: [
175
+ Faraday::TimeoutError,
176
+ Faraday::ConnectionFailed,
177
+ Faraday::RetriableResponse
178
+ ]
179
+
180
+ f.response :logger, config.logger if config.logger
181
+ f.adapter config.adapter
182
+ end
183
+ end
184
+
185
+ def with_api_key(params)
186
+ params.merge('api_key' => config.api_key)
187
+ end
188
+
189
+ def compact_params(params)
190
+ params.compact.transform_keys(&:to_s)
191
+ end
192
+
193
+ def handle_response(response) # rubocop:disable Metrics/MethodLength
194
+ case response.status
195
+ when 200..299
196
+ Response.new(response.body, response.status)
197
+ when 400
198
+ raise BadRequestError.new(status: response.status, body: response.body)
199
+ when 401
200
+ raise UnauthorizedError.new(status: response.status, body: response.body)
201
+ when 403
202
+ raise ForbiddenError.new(status: response.status, body: response.body)
203
+ when 404
204
+ raise NotFoundError.new(status: response.status, body: response.body)
205
+ when 422
206
+ raise UnprocessableEntityError.new(status: response.status, body: response.body)
207
+ when 429
208
+ raise RateLimitError.new(status: 429, body: response.body)
209
+ when 400..499
210
+ raise ClientError.new(status: response.status, body: response.body)
211
+ when 500..599
212
+ raise ServerError.new(status: response.status, body: response.body)
213
+ else
214
+ raise Error, "Unexpected HTTP status: #{response.status}"
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFec
4
+ # Configuration for the OpenFEC API client.
5
+ #
6
+ # @example
7
+ # OpenFec.configure do |c|
8
+ # c.api_key = 'your-api-key'
9
+ # c.per_page = 50
10
+ # c.timeout = 60
11
+ # end
12
+ class Configuration
13
+ DEFAULT_BASE_URL = 'https://api.open.fec.gov/v1/'
14
+ DEFAULT_TIMEOUT = 30
15
+ DEFAULT_RETRIES = 3
16
+ DEFAULT_PER_PAGE = 20
17
+ DEFAULT_MAX_PAGES = 100
18
+
19
+ # @return [String] base URL for the FEC API
20
+ attr_accessor :base_url
21
+ # @return [Integer] HTTP timeout in seconds
22
+ attr_accessor :timeout
23
+ # @return [Integer] number of retries on 429/5xx
24
+ attr_accessor :retries
25
+ # @return [Logger, nil] optional logger for HTTP requests
26
+ attr_accessor :logger
27
+ # @return [Symbol] Faraday adapter (default: system default)
28
+ attr_accessor :adapter
29
+ # @return [String, nil] FEC API key (get one free at https://api.data.gov/signup)
30
+ attr_accessor :api_key
31
+ # @return [Integer] default results per page
32
+ attr_accessor :per_page
33
+ # @return [Integer] max pages to fetch during pagination
34
+ attr_accessor :max_pages
35
+
36
+ def initialize
37
+ @base_url = DEFAULT_BASE_URL
38
+ @timeout = DEFAULT_TIMEOUT
39
+ @retries = DEFAULT_RETRIES
40
+ @per_page = DEFAULT_PER_PAGE
41
+ @max_pages = DEFAULT_MAX_PAGES
42
+ @logger = nil
43
+ @adapter = Faraday.default_adapter
44
+ @api_key = ENV.fetch('OPEN_FEC_API_KEY', nil)
45
+ end
46
+
47
+ # Validates that required configuration is present.
48
+ #
49
+ # @raise [ConfigurationError] if api_key is missing
50
+ # @return [void]
51
+ def validate!
52
+ return unless api_key.nil? || api_key.empty?
53
+
54
+ raise ConfigurationError,
55
+ 'api_key is required (get one free at https://api.data.gov/signup)'
56
+ end
57
+
58
+ # @return [Hash] all configuration values
59
+ def to_h
60
+ {
61
+ base_url: base_url,
62
+ timeout: timeout,
63
+ retries: retries,
64
+ per_page: per_page,
65
+ max_pages: max_pages,
66
+ logger: logger,
67
+ adapter: adapter,
68
+ api_key: api_key
69
+ }
70
+ end
71
+
72
+ def inspect
73
+ "#<#{self.class} base_url=#{base_url.inspect} timeout=#{timeout} retries=#{retries}>"
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFec
4
+ # Base error class for all OpenFec errors.
5
+ class Error < StandardError; end
6
+
7
+ # Raised for 4xx HTTP responses.
8
+ class ClientError < Error
9
+ # @return [Integer, nil] HTTP status code
10
+ attr_reader :status
11
+ # @return [Hash, String, nil] response body
12
+ attr_reader :body
13
+
14
+ # @param message [String, nil]
15
+ # @param status [Integer, nil]
16
+ # @param body [Hash, String, nil]
17
+ def initialize(message = nil, status: nil, body: nil)
18
+ @status = status
19
+ @body = body
20
+ super(message || "HTTP #{status}: #{body}")
21
+ end
22
+ end
23
+
24
+ # Raised on 400 Bad Request.
25
+ class BadRequestError < ClientError; end
26
+ # Raised on 401 Unauthorized (invalid or missing API key).
27
+ class UnauthorizedError < ClientError; end
28
+ # Raised on 403 Forbidden.
29
+ class ForbiddenError < ClientError; end
30
+ # Raised on 404 Not Found.
31
+ class NotFoundError < ClientError; end
32
+ # Raised on 429 Too Many Requests (rate limit exceeded).
33
+ class RateLimitError < ClientError; end
34
+ # Raised on 422 Unprocessable Entity.
35
+ class UnprocessableEntityError < ClientError; end
36
+
37
+ # Raised for 5xx HTTP responses.
38
+ class ServerError < Error
39
+ # @return [Integer, nil] HTTP status code
40
+ attr_reader :status
41
+ # @return [Hash, String, nil] response body
42
+ attr_reader :body
43
+
44
+ # @param message [String, nil]
45
+ # @param status [Integer, nil]
46
+ # @param body [Hash, String, nil]
47
+ def initialize(message = nil, status: nil, body: nil)
48
+ @status = status
49
+ @body = body
50
+ super(message || "Server error HTTP #{status}")
51
+ end
52
+ end
53
+
54
+ # Raised on network-level failures (timeout, connection refused).
55
+ class ConnectionError < Error; end
56
+ # Raised when configuration is invalid (e.g. missing API key).
57
+ class ConfigurationError < Error; end
58
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFec
4
+ # Resource classes for interacting with FEC API endpoints.
5
+ module Resources
6
+ # Base class for all FEC API resource classes.
7
+ class Base
8
+ # @param client [OpenFec::Client]
9
+ def initialize(client = OpenFec.client)
10
+ @client = client
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :client
16
+
17
+ # @param path [String]
18
+ # @param params [Hash]
19
+ # @return [OpenFec::Response]
20
+ def get(path, params = {})
21
+ client.get(path, params)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFec
4
+ module Resources
5
+ # Campaign finance totals for candidates across election cycles.
6
+ #
7
+ # @example Get all cycles for a candidate
8
+ # OpenFec.candidate_totals.list('H8CA05035')
9
+ #
10
+ # @example Get totals for a specific cycle
11
+ # OpenFec.candidate_totals.for_cycle('H8CA05035', 2024)
12
+ class CandidateTotals < Base
13
+ # Financial totals for a candidate across election cycles.
14
+ # Returns receipts, disbursements, cash_on_hand, individual_contributions,
15
+ # other_political_committee_contributions (PAC money), etc.
16
+ #
17
+ # @param candidate_id [String] FEC candidate ID (e.g. "H8CA05035")
18
+ # @param params [Hash] optional filters (:cycle, :election_full, etc.)
19
+ # @return [OpenFec::Response]
20
+ def list(candidate_id, **params)
21
+ get("candidate/#{candidate_id}/totals/", params)
22
+ end
23
+
24
+ # Fetch totals for a specific election cycle.
25
+ #
26
+ # @param candidate_id [String] FEC candidate ID
27
+ # @param cycle [Integer] election cycle year (e.g. 2024)
28
+ # @return [OpenFec::Response]
29
+ def for_cycle(candidate_id, cycle)
30
+ list(candidate_id, cycle: cycle)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFec
4
+ module Resources
5
+ # Search and retrieve FEC candidate records.
6
+ #
7
+ # @example Search by name
8
+ # OpenFec.candidates.search(name: 'Pelosi', state: 'CA')
9
+ #
10
+ # @example Get a candidate with their principal committees
11
+ # candidate = OpenFec.candidates.find('H8CA05035')
12
+ # committee_id = candidate.results.first.dig('principal_committees', 0, 'committee_id')
13
+ class Candidates < Base
14
+ # Search for candidates by name, state, district, party, etc.
15
+ #
16
+ # @param params [Hash] search filters (:name, :state, :district, :party, :office, etc.)
17
+ # @return [OpenFec::Response]
18
+ def search(**params)
19
+ get('candidates/search/', params)
20
+ end
21
+
22
+ # Fetch a single candidate by FEC candidate ID.
23
+ # Includes principal_committees in the response.
24
+ #
25
+ # @param candidate_id [String] FEC candidate ID (e.g. "H8CA05035")
26
+ # @return [OpenFec::Response]
27
+ def find(candidate_id)
28
+ get("candidate/#{candidate_id}/")
29
+ end
30
+
31
+ # List committees associated with a candidate.
32
+ #
33
+ # @param candidate_id [String] FEC candidate ID
34
+ # @param params [Hash] optional filters (:designation, :organization_type, etc.)
35
+ # @return [OpenFec::Response]
36
+ def committees(candidate_id, **params)
37
+ get("candidate/#{candidate_id}/committees/", params)
38
+ end
39
+
40
+ # Paginate through candidate search results (offset-based).
41
+ #
42
+ # @param params [Hash] search filters
43
+ # @yield [OpenFec::Response] each page of results
44
+ def each_page(**params, &)
45
+ client.paginate('candidates/search/', params, &)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenFec
4
+ module Resources
5
+ # Search and retrieve FEC committee records (PACs, campaign committees, etc.).
6
+ #
7
+ # @example Find committees for a candidate
8
+ # OpenFec.committees.search(candidate_id: 'H8CA05035')
9
+ #
10
+ # @example Get a specific committee
11
+ # OpenFec.committees.find('C00213512')
12
+ class Committees < Base
13
+ # Search committees by name, candidate_id, state, committee_type, etc.
14
+ #
15
+ # @param params [Hash] search filters
16
+ # @return [OpenFec::Response]
17
+ def search(**params)
18
+ get('committees/', params)
19
+ end
20
+
21
+ # Fetch a single committee by FEC committee ID.
22
+ #
23
+ # @param committee_id [String] FEC committee ID (e.g. "C00213512")
24
+ # @return [OpenFec::Response]
25
+ def find(committee_id)
26
+ get("committee/#{committee_id}/")
27
+ end
28
+
29
+ # Paginate through committee search results (offset-based).
30
+ #
31
+ # @param params [Hash] search filters
32
+ # @yield [OpenFec::Response] each page of results
33
+ def each_page(**params, &)
34
+ client.paginate('committees/', params, &)
35
+ end
36
+ end
37
+ end
38
+ end