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 +7 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +104 -0
- data/lib/open_fec/client.rb +218 -0
- data/lib/open_fec/configuration.rb +76 -0
- data/lib/open_fec/error.rb +58 -0
- data/lib/open_fec/resources/base.rb +25 -0
- data/lib/open_fec/resources/candidate_totals.rb +34 -0
- data/lib/open_fec/resources/candidates.rb +49 -0
- data/lib/open_fec/resources/committees.rb +38 -0
- data/lib/open_fec/resources/contribution_aggregates.rb +87 -0
- data/lib/open_fec/resources/contributions.rb +55 -0
- data/lib/open_fec/resources/disbursements.rb +29 -0
- data/lib/open_fec/resources/elections.rb +41 -0
- data/lib/open_fec/resources/independent_expenditures.rb +50 -0
- data/lib/open_fec/response.rb +184 -0
- data/lib/open_fec/version.rb +5 -0
- data/lib/open_fec.rb +107 -0
- metadata +196 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenFec
|
|
4
|
+
module Resources
|
|
5
|
+
# Aggregated contribution data (by employer, occupation, size, state, zip).
|
|
6
|
+
# All endpoints use offset-based pagination.
|
|
7
|
+
#
|
|
8
|
+
# @example Top employer donors to a committee
|
|
9
|
+
# OpenFec.contribution_aggregates.by_employer(
|
|
10
|
+
# committee_id: 'C00213512', cycle: 2024, sort: '-total', per_page: 10
|
|
11
|
+
# )
|
|
12
|
+
#
|
|
13
|
+
# @example Donations by zip code
|
|
14
|
+
# OpenFec.contribution_aggregates.by_zip(
|
|
15
|
+
# committee_id: 'C00555888', cycle: 2024, sort: '-total', per_page: 10
|
|
16
|
+
# )
|
|
17
|
+
class ContributionAggregates < Base
|
|
18
|
+
# Contributions aggregated by employer.
|
|
19
|
+
# Returns: employer, total, count, cycle.
|
|
20
|
+
#
|
|
21
|
+
# @param committee_id [String] FEC committee ID
|
|
22
|
+
# @param cycle [Integer] election cycle year
|
|
23
|
+
# @param params [Hash] additional params (:sort, :per_page, etc.)
|
|
24
|
+
# @return [OpenFec::Response]
|
|
25
|
+
def by_employer(committee_id:, cycle:, **params)
|
|
26
|
+
get('schedules/schedule_a/by_employer/',
|
|
27
|
+
params.merge(committee_id: committee_id, cycle: cycle))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Contributions aggregated by occupation.
|
|
31
|
+
#
|
|
32
|
+
# @param committee_id [String] FEC committee ID
|
|
33
|
+
# @param cycle [Integer] election cycle year
|
|
34
|
+
# @param params [Hash] additional params
|
|
35
|
+
# @return [OpenFec::Response]
|
|
36
|
+
def by_occupation(committee_id:, cycle:, **params)
|
|
37
|
+
get('schedules/schedule_a/by_occupation/',
|
|
38
|
+
params.merge(committee_id: committee_id, cycle: cycle))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Contributions aggregated by size (small/medium/large dollar).
|
|
42
|
+
#
|
|
43
|
+
# @param candidate_id [String] FEC candidate ID
|
|
44
|
+
# @param cycle [Integer] election cycle year
|
|
45
|
+
# @param params [Hash] additional params
|
|
46
|
+
# @return [OpenFec::Response]
|
|
47
|
+
def by_size(candidate_id:, cycle:, **params)
|
|
48
|
+
get('schedules/schedule_a/by_size/by_candidate/',
|
|
49
|
+
params.merge(candidate_id: candidate_id, cycle: cycle))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Contributions aggregated by state.
|
|
53
|
+
#
|
|
54
|
+
# @param candidate_id [String] FEC candidate ID
|
|
55
|
+
# @param cycle [Integer] election cycle year
|
|
56
|
+
# @param params [Hash] additional params
|
|
57
|
+
# @return [OpenFec::Response]
|
|
58
|
+
def by_state(candidate_id:, cycle:, **params)
|
|
59
|
+
get('schedules/schedule_a/by_state/by_candidate/',
|
|
60
|
+
params.merge(candidate_id: candidate_id, cycle: cycle))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Contributions aggregated by zip code.
|
|
64
|
+
# Returns: zip, total, count, cycle.
|
|
65
|
+
#
|
|
66
|
+
# @param committee_id [String] FEC committee ID
|
|
67
|
+
# @param cycle [Integer] election cycle year
|
|
68
|
+
# @param params [Hash] additional params (:sort, :per_page, etc.)
|
|
69
|
+
# @return [OpenFec::Response]
|
|
70
|
+
def by_zip(committee_id:, cycle:, **params)
|
|
71
|
+
get('schedules/schedule_a/by_zip/',
|
|
72
|
+
params.merge(committee_id: committee_id, cycle: cycle))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Paginate through employer contribution aggregates.
|
|
76
|
+
#
|
|
77
|
+
# @param committee_id [String] FEC committee ID
|
|
78
|
+
# @param cycle [Integer] election cycle year
|
|
79
|
+
# @param params [Hash] additional params
|
|
80
|
+
# @yield [OpenFec::Response] each page of results
|
|
81
|
+
def each_employer_page(committee_id:, cycle:, **params, &)
|
|
82
|
+
client.paginate('schedules/schedule_a/by_employer/',
|
|
83
|
+
params.merge(committee_id: committee_id, cycle: cycle), &)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenFec
|
|
4
|
+
module Resources
|
|
5
|
+
# Itemized contribution records (Schedule A).
|
|
6
|
+
# Uses seek/cursor-based pagination (not offset-based).
|
|
7
|
+
#
|
|
8
|
+
# @example Get PAC contributions to a committee
|
|
9
|
+
# OpenFec.contributions.from_pacs(committee_id: 'C00213512', cycle: 2024)
|
|
10
|
+
#
|
|
11
|
+
# @example Paginate all contributions
|
|
12
|
+
# OpenFec.contributions.each_page(committee_id: 'C00213512') do |page|
|
|
13
|
+
# page.each { |c| puts c['contributor_name'] }
|
|
14
|
+
# end
|
|
15
|
+
class Contributions < Base
|
|
16
|
+
# List itemized contributions (Schedule A).
|
|
17
|
+
#
|
|
18
|
+
# @param params [Hash] filters (:committee_id, :contributor_name,
|
|
19
|
+
# :entity_type, :two_year_transaction_period, :sort, etc.)
|
|
20
|
+
# @return [OpenFec::Response]
|
|
21
|
+
def list(**params)
|
|
22
|
+
get('schedules/schedule_a/', params)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# PAC contributions to a committee (entity_type=COM).
|
|
26
|
+
#
|
|
27
|
+
# @param committee_id [String] FEC committee ID
|
|
28
|
+
# @param cycle [Integer, nil] election cycle year
|
|
29
|
+
# @param params [Hash] additional filters
|
|
30
|
+
# @return [OpenFec::Response]
|
|
31
|
+
def from_pacs(committee_id:, cycle: nil, **params)
|
|
32
|
+
merged = params.merge(committee_id: committee_id, entity_type: 'COM')
|
|
33
|
+
merged[:two_year_transaction_period] = cycle if cycle
|
|
34
|
+
get('schedules/schedule_a/', merged)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Paginate through itemized contributions (seek/cursor-based).
|
|
38
|
+
#
|
|
39
|
+
# @param params [Hash] filters
|
|
40
|
+
# @yield [OpenFec::Response] each page of results
|
|
41
|
+
def each_page(**params, &)
|
|
42
|
+
client.paginate_seek('schedules/schedule_a/', params, &)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Fetch all itemized contributions (seek-based). Use with caution on
|
|
46
|
+
# large result sets.
|
|
47
|
+
#
|
|
48
|
+
# @param params [Hash] filters
|
|
49
|
+
# @return [Array<Hash>] all contribution records
|
|
50
|
+
def all_contributions(**params)
|
|
51
|
+
client.all_seek('schedules/schedule_a/', params)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenFec
|
|
4
|
+
module Resources
|
|
5
|
+
# Itemized disbursement records (Schedule B).
|
|
6
|
+
# Uses seek/cursor-based pagination (not offset-based).
|
|
7
|
+
#
|
|
8
|
+
# @example List disbursements for a committee
|
|
9
|
+
# OpenFec.disbursements.list(committee_id: 'C00213512')
|
|
10
|
+
class Disbursements < Base
|
|
11
|
+
# List itemized disbursements (Schedule B).
|
|
12
|
+
#
|
|
13
|
+
# @param params [Hash] filters (:committee_id, :recipient_name,
|
|
14
|
+
# :two_year_transaction_period, :sort, etc.)
|
|
15
|
+
# @return [OpenFec::Response]
|
|
16
|
+
def list(**params)
|
|
17
|
+
get('schedules/schedule_b/', params)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Paginate through disbursements (seek/cursor-based).
|
|
21
|
+
#
|
|
22
|
+
# @param params [Hash] filters
|
|
23
|
+
# @yield [OpenFec::Response] each page of results
|
|
24
|
+
def each_page(**params, &)
|
|
25
|
+
client.paginate_seek('schedules/schedule_b/', params, &)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenFec
|
|
4
|
+
module Resources
|
|
5
|
+
# Election results and candidate comparisons for a specific race.
|
|
6
|
+
# Shows all candidates who ran in a district with financial totals side by side.
|
|
7
|
+
#
|
|
8
|
+
# @example Get all candidates who ran for VA-08 in 2024
|
|
9
|
+
# OpenFec.elections.list(state: 'VA', district: '08', cycle: 2024, office: 'house')
|
|
10
|
+
#
|
|
11
|
+
# @example Election summary for a race
|
|
12
|
+
# OpenFec.elections.summary(cycle: 2024, office: 'house', state: 'VA', district: '08')
|
|
13
|
+
class Elections < Base
|
|
14
|
+
# List all candidates in a specific election with financial totals.
|
|
15
|
+
# Returns candidate_name, party, total_receipts, total_disbursements,
|
|
16
|
+
# cash_on_hand_end_period, etc.
|
|
17
|
+
#
|
|
18
|
+
# @param params [Hash] filters (:state, :district, :cycle, :office — "house" or "senate")
|
|
19
|
+
# @return [OpenFec::Response]
|
|
20
|
+
def list(**params)
|
|
21
|
+
get('elections/', params)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Search for elections by state, office, and cycle.
|
|
25
|
+
#
|
|
26
|
+
# @param params [Hash] filters (:state, :office, :cycle, :district, :zip)
|
|
27
|
+
# @return [OpenFec::Response]
|
|
28
|
+
def search(**params)
|
|
29
|
+
get('elections/search/', params)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Election summary with aggregate spending data.
|
|
33
|
+
#
|
|
34
|
+
# @param params [Hash] filters (:cycle, :office, :state, :district, :election_full)
|
|
35
|
+
# @return [OpenFec::Response]
|
|
36
|
+
def summary(**params)
|
|
37
|
+
get('elections/summary/', params)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenFec
|
|
4
|
+
module Resources
|
|
5
|
+
# Independent expenditures (Schedule E) — Super PAC and outside group
|
|
6
|
+
# spending for or against candidates.
|
|
7
|
+
#
|
|
8
|
+
# @example Super PAC spending for/against a candidate
|
|
9
|
+
# OpenFec.independent_expenditures.by_candidate(candidate_id: 'H4VA08224', cycle: 2024)
|
|
10
|
+
#
|
|
11
|
+
# @example Totals by candidate
|
|
12
|
+
# OpenFec.independent_expenditures.totals_by_candidate(candidate_id: 'H4VA08224', cycle: 2024)
|
|
13
|
+
class IndependentExpenditures < Base
|
|
14
|
+
# List itemized independent expenditures (Schedule E).
|
|
15
|
+
# Uses seek/cursor-based pagination.
|
|
16
|
+
#
|
|
17
|
+
# @param params [Hash] filters (:committee_id, :candidate_id, :cycle,
|
|
18
|
+
# :support_oppose_indicator — "S" for support, "O" for oppose, etc.)
|
|
19
|
+
# @return [OpenFec::Response]
|
|
20
|
+
def list(**params)
|
|
21
|
+
get('schedules/schedule_e/', params)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Independent expenditures aggregated by candidate.
|
|
25
|
+
# Shows committee_name, support/oppose indicator, total amount.
|
|
26
|
+
#
|
|
27
|
+
# @param params [Hash] filters (:candidate_id, :cycle, :office, :state, :district)
|
|
28
|
+
# @return [OpenFec::Response]
|
|
29
|
+
def by_candidate(**params)
|
|
30
|
+
get('schedules/schedule_e/by_candidate/', params)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Totals of independent expenditures by candidate.
|
|
34
|
+
#
|
|
35
|
+
# @param params [Hash] filters (:candidate_id, :cycle, :election_full)
|
|
36
|
+
# @return [OpenFec::Response]
|
|
37
|
+
def totals_by_candidate(**params)
|
|
38
|
+
get('schedules/schedule_e/totals/by_candidate/', params)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Paginate through itemized independent expenditures (seek-based).
|
|
42
|
+
#
|
|
43
|
+
# @param params [Hash] filters
|
|
44
|
+
# @yield [OpenFec::Response] each page of results
|
|
45
|
+
def each_page(**params, &)
|
|
46
|
+
client.paginate_seek('schedules/schedule_e/', params, &)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenFec
|
|
4
|
+
# Wraps FEC API JSON responses with pagination metadata and Enumerable access.
|
|
5
|
+
#
|
|
6
|
+
# The FEC API uses two pagination models:
|
|
7
|
+
# - **Offset-based** (most endpoints): uses `page`/`pages` counters
|
|
8
|
+
# - **Seek/cursor-based** (itemized transactions): uses `last_indexes` cursors
|
|
9
|
+
#
|
|
10
|
+
# This class auto-detects which model is in use and exposes a unified
|
|
11
|
+
# {#next_page_params} method for both.
|
|
12
|
+
#
|
|
13
|
+
# @example Iterating results
|
|
14
|
+
# response = OpenFec.candidates.search(name: 'Pelosi')
|
|
15
|
+
# response.each { |candidate| puts candidate['name'] }
|
|
16
|
+
#
|
|
17
|
+
# @example Checking pagination
|
|
18
|
+
# response.next_page? # => true
|
|
19
|
+
# response.next_page_params # => { page: 2 } or { last_index: '456', ... }
|
|
20
|
+
class Response
|
|
21
|
+
include Enumerable
|
|
22
|
+
|
|
23
|
+
# @return [Hash, Array] raw parsed JSON body
|
|
24
|
+
attr_reader :raw
|
|
25
|
+
# @return [Integer] HTTP status code
|
|
26
|
+
attr_reader :status
|
|
27
|
+
|
|
28
|
+
# @param body [Hash, Array] parsed JSON response body
|
|
29
|
+
# @param status [Integer] HTTP status code
|
|
30
|
+
def initialize(body, status)
|
|
31
|
+
@raw = body
|
|
32
|
+
@status = status
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Yields each result record.
|
|
36
|
+
# @yield [Hash] individual result record
|
|
37
|
+
def each(&)
|
|
38
|
+
results.each(&)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns the results array from the response.
|
|
42
|
+
# @return [Array<Hash>]
|
|
43
|
+
def results
|
|
44
|
+
return raw if raw.is_a?(Array)
|
|
45
|
+
return [] unless raw.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
raw['results'] || []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Integer] number of results in this page
|
|
51
|
+
def size
|
|
52
|
+
results.size
|
|
53
|
+
end
|
|
54
|
+
alias length size
|
|
55
|
+
|
|
56
|
+
# @return [Boolean] true if no results in this page
|
|
57
|
+
def empty?
|
|
58
|
+
results.empty?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# --- Pagination metadata ---
|
|
62
|
+
|
|
63
|
+
# @return [Hash] raw pagination metadata from the API
|
|
64
|
+
def pagination
|
|
65
|
+
return {} unless raw.is_a?(Hash)
|
|
66
|
+
|
|
67
|
+
raw['pagination'] || {}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Integer, nil] total number of matching records across all pages
|
|
71
|
+
def total_count
|
|
72
|
+
pagination['count']
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Integer, nil] total number of pages (offset-based only)
|
|
76
|
+
def total_pages
|
|
77
|
+
pagination['pages']
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [Integer, nil] current page number (offset-based only)
|
|
81
|
+
def current_page
|
|
82
|
+
pagination['page']
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [Integer, nil] results per page
|
|
86
|
+
def per_page
|
|
87
|
+
pagination['per_page']
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Whether there are more pages of results to fetch.
|
|
91
|
+
# Works for both offset-based and seek-based pagination.
|
|
92
|
+
#
|
|
93
|
+
# @return [Boolean]
|
|
94
|
+
def next_page?
|
|
95
|
+
return seek_next_page? if seek_paginated?
|
|
96
|
+
|
|
97
|
+
page = current_page
|
|
98
|
+
pages = total_pages
|
|
99
|
+
return false if page.nil? || pages.nil?
|
|
100
|
+
|
|
101
|
+
page < pages
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @return [Integer, nil] the next page number (offset-based only)
|
|
105
|
+
def next_page_number
|
|
106
|
+
return nil unless next_page? && !seek_paginated?
|
|
107
|
+
|
|
108
|
+
current_page + 1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Whether this response uses seek/cursor-based pagination.
|
|
112
|
+
# True for itemized transaction endpoints (Schedule A, Schedule B).
|
|
113
|
+
#
|
|
114
|
+
# @return [Boolean]
|
|
115
|
+
def seek_paginated?
|
|
116
|
+
pagination.key?('last_indexes')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @return [Hash] cursor keys for seek-based pagination
|
|
120
|
+
def last_indexes
|
|
121
|
+
pagination['last_indexes'] || {}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Returns the query params to fetch the next page of results.
|
|
125
|
+
# For offset-based: `{ page: N }`.
|
|
126
|
+
# For seek-based: `{ last_index: "...", last_<sort_field>: "..." }`.
|
|
127
|
+
#
|
|
128
|
+
# @return [Hash, nil] params for next page, or nil if no more pages
|
|
129
|
+
def next_page_params
|
|
130
|
+
return nil unless next_page?
|
|
131
|
+
|
|
132
|
+
if seek_paginated?
|
|
133
|
+
last_indexes.transform_keys(&:to_sym)
|
|
134
|
+
else
|
|
135
|
+
{ page: next_page_number }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# --- Direct access ---
|
|
140
|
+
|
|
141
|
+
# Access a top-level key from the raw response.
|
|
142
|
+
# @param key [String]
|
|
143
|
+
# @return [Object, nil]
|
|
144
|
+
def [](key)
|
|
145
|
+
return nil unless raw.is_a?(Hash)
|
|
146
|
+
|
|
147
|
+
raw[key]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Dig into nested keys in the raw response.
|
|
151
|
+
# @param keys [Array<String>]
|
|
152
|
+
# @return [Object, nil]
|
|
153
|
+
def dig(*keys)
|
|
154
|
+
return nil unless raw.is_a?(Hash)
|
|
155
|
+
|
|
156
|
+
raw.dig(*keys)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# @return [Hash]
|
|
160
|
+
def to_h
|
|
161
|
+
raw.is_a?(Hash) ? raw : { 'results' => raw }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# @return [String] JSON representation
|
|
165
|
+
def to_json(*)
|
|
166
|
+
to_h.to_json
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# @return [Boolean] true if HTTP status is 2xx
|
|
170
|
+
def success?
|
|
171
|
+
status >= 200 && status < 300
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def inspect
|
|
175
|
+
"#<#{self.class} status=#{status} size=#{size} total_count=#{total_count.inspect} next_page?=#{next_page?}>"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def seek_next_page?
|
|
181
|
+
!last_indexes.empty? && !results.empty?
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
data/lib/open_fec.rb
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open_fec/version'
|
|
4
|
+
require 'open_fec/configuration'
|
|
5
|
+
require 'open_fec/error'
|
|
6
|
+
require 'open_fec/response'
|
|
7
|
+
require 'open_fec/client'
|
|
8
|
+
require 'open_fec/resources/base'
|
|
9
|
+
require 'open_fec/resources/candidates'
|
|
10
|
+
require 'open_fec/resources/candidate_totals'
|
|
11
|
+
require 'open_fec/resources/committees'
|
|
12
|
+
require 'open_fec/resources/contributions'
|
|
13
|
+
require 'open_fec/resources/contribution_aggregates'
|
|
14
|
+
require 'open_fec/resources/disbursements'
|
|
15
|
+
require 'open_fec/resources/elections'
|
|
16
|
+
require 'open_fec/resources/independent_expenditures'
|
|
17
|
+
|
|
18
|
+
# Ruby client for the Federal Election Commission (FEC) API.
|
|
19
|
+
# @see https://api.open.fec.gov/developers/
|
|
20
|
+
#
|
|
21
|
+
# @example Basic usage
|
|
22
|
+
# OpenFec.configure { |c| c.api_key = 'your-api-key' }
|
|
23
|
+
# results = OpenFec.candidates.search(name: 'Pelosi')
|
|
24
|
+
# totals = OpenFec.candidate_totals.for_cycle('H8CA05035', 2024)
|
|
25
|
+
module OpenFec
|
|
26
|
+
# Mutex for thread-safe initialization of configuration and client.
|
|
27
|
+
@mutex = Mutex.new
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# Returns the current configuration instance.
|
|
31
|
+
# Thread-safe via Mutex.
|
|
32
|
+
#
|
|
33
|
+
# @return [OpenFec::Configuration]
|
|
34
|
+
def configuration
|
|
35
|
+
@mutex.synchronize { @configuration ||= Configuration.new }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Yields the configuration instance for modification.
|
|
39
|
+
#
|
|
40
|
+
# @yieldparam config [OpenFec::Configuration]
|
|
41
|
+
# @return [void]
|
|
42
|
+
def configure
|
|
43
|
+
@mutex.synchronize { yield(@configuration ||= Configuration.new) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Resets configuration and client to defaults.
|
|
47
|
+
# Thread-safe via Mutex.
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
def reset!
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
@configuration = Configuration.new
|
|
53
|
+
@client = nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns the shared API client instance.
|
|
58
|
+
# Thread-safe via Mutex.
|
|
59
|
+
#
|
|
60
|
+
# @return [OpenFec::Client]
|
|
61
|
+
def client
|
|
62
|
+
@mutex.synchronize { @client ||= Client.new(@configuration || Configuration.new) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Resource shorthand accessors
|
|
66
|
+
|
|
67
|
+
# @return [OpenFec::Resources::Candidates]
|
|
68
|
+
def candidates
|
|
69
|
+
Resources::Candidates.new(client)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [OpenFec::Resources::CandidateTotals]
|
|
73
|
+
def candidate_totals
|
|
74
|
+
Resources::CandidateTotals.new(client)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @return [OpenFec::Resources::Committees]
|
|
78
|
+
def committees
|
|
79
|
+
Resources::Committees.new(client)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [OpenFec::Resources::Contributions]
|
|
83
|
+
def contributions
|
|
84
|
+
Resources::Contributions.new(client)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [OpenFec::Resources::ContributionAggregates]
|
|
88
|
+
def contribution_aggregates
|
|
89
|
+
Resources::ContributionAggregates.new(client)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [OpenFec::Resources::Disbursements]
|
|
93
|
+
def disbursements
|
|
94
|
+
Resources::Disbursements.new(client)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @return [OpenFec::Resources::Elections]
|
|
98
|
+
def elections
|
|
99
|
+
Resources::Elections.new(client)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [OpenFec::Resources::IndependentExpenditures]
|
|
103
|
+
def independent_expenditures
|
|
104
|
+
Resources::IndependentExpenditures.new(client)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|