congress_gov 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 +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +156 -0
- data/lib/congress_gov/client.rb +125 -0
- data/lib/congress_gov/configuration.rb +48 -0
- data/lib/congress_gov/error.rb +46 -0
- data/lib/congress_gov/resources/amendment.rb +107 -0
- data/lib/congress_gov/resources/base.rb +74 -0
- data/lib/congress_gov/resources/bill.rb +199 -0
- data/lib/congress_gov/resources/bound_congressional_record.rb +48 -0
- data/lib/congress_gov/resources/clerk_vote.rb +125 -0
- data/lib/congress_gov/resources/committee.rb +136 -0
- data/lib/congress_gov/resources/committee_meeting.rb +37 -0
- data/lib/congress_gov/resources/committee_print.rb +50 -0
- data/lib/congress_gov/resources/committee_report.rb +65 -0
- data/lib/congress_gov/resources/congress_info.rb +32 -0
- data/lib/congress_gov/resources/crs_report.rb +17 -0
- data/lib/congress_gov/resources/daily_congressional_record.rb +48 -0
- data/lib/congress_gov/resources/hearing.rb +37 -0
- data/lib/congress_gov/resources/house_communication.rb +51 -0
- data/lib/congress_gov/resources/house_requirement.rb +36 -0
- data/lib/congress_gov/resources/house_vote.rb +127 -0
- data/lib/congress_gov/resources/law.rb +53 -0
- data/lib/congress_gov/resources/member.rb +151 -0
- data/lib/congress_gov/resources/nomination.rb +74 -0
- data/lib/congress_gov/resources/senate_communication.rb +51 -0
- data/lib/congress_gov/resources/summary.rb +27 -0
- data/lib/congress_gov/resources/treaty.rb +75 -0
- data/lib/congress_gov/response.rb +109 -0
- data/lib/congress_gov/version.rb +6 -0
- data/lib/congress_gov.rb +180 -0
- metadata +237 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8e7b32276ee034d37d47c0fbe21485df9fd922f51fc56623cccd567e1d3884ad
|
|
4
|
+
data.tar.gz: 5f4986720a61399f43c8c7bfe6ed14c9e64db870f6f281e766193a215f8014a2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8d1bc89489d8626316023c668ea5b131531405ae1ba0e13a2ac142d6186470e5e333c32da42cbb4b217c7cacdb02ab33b7dd0912dad8434b4fa44c43fa0281e6
|
|
7
|
+
data.tar.gz: d7ae6017db8987dd0b224434aee920e46a3c0e14bc000efa420ba2e3c5f6f78c2cf4b6206800aa00dad6be9766f052ce783cff7b9f035f05f41a9f583da8792f
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2025-09-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Initial release
|
|
7
|
+
- Member lookup by district, bioguide ID, sponsored/cosponsored legislation
|
|
8
|
+
- Bill detail, actions, subjects, summaries, cosponsors
|
|
9
|
+
- House roll call vote listing, detail, and per-member vote positions
|
|
10
|
+
- Clerk of the House XML fallback parser for vote records
|
|
11
|
+
- Committee listing and membership
|
|
12
|
+
- Full error hierarchy with retry support for rate limits
|
|
13
|
+
- VCR-based test suite with real Clerk XML fixture
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ThePublicTab
|
|
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,156 @@
|
|
|
1
|
+
# congress_gov
|
|
2
|
+
|
|
3
|
+
Ruby client for the [Congress.gov REST API v3](https://api.congress.gov/). Access member data, roll call votes, bill details, committee information, and more. Includes a fallback parser for Clerk of the House XML vote records.
|
|
4
|
+
|
|
5
|
+
The first Ruby client targeting the current v3 API.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "congress_gov"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then `bundle install`, or install directly:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
gem install congress_gov
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
Get an API key at https://api.congress.gov/sign-up/
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
CongressGov.configure do |config|
|
|
25
|
+
config.api_key = ENV["CONGRESS_GOV_API_KEY"]
|
|
26
|
+
config.timeout = 30 # optional, default 30s
|
|
27
|
+
config.retries = 3 # optional, default 3 (handles 429s with exponential backoff)
|
|
28
|
+
config.logger = Logger.new($stdout) # optional
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### Member Lookup
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# Get the current rep for a congressional district
|
|
38
|
+
rep = CongressGov.member.current_for_district(state: "VA", district: 8)
|
|
39
|
+
rep["bioguideId"] #=> "B001292"
|
|
40
|
+
|
|
41
|
+
# Full member profile
|
|
42
|
+
profile = CongressGov.member.get("B001292")
|
|
43
|
+
profile["member"]["directOrderName"] #=> "Donald S. Beyer"
|
|
44
|
+
|
|
45
|
+
# Sponsored legislation
|
|
46
|
+
CongressGov.member.sponsored_legislation("B001292", limit: 50)
|
|
47
|
+
|
|
48
|
+
# List all current members (paginated, max 250 per page)
|
|
49
|
+
CongressGov.member.list(current: true, limit: 250)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Roll Call Votes
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
# How did a member vote on a specific roll call?
|
|
56
|
+
position = CongressGov.house_vote.position_for_member(
|
|
57
|
+
congress: 119, session: 1, roll_call: 281, bioguide_id: "B001292"
|
|
58
|
+
)
|
|
59
|
+
position #=> "Nay"
|
|
60
|
+
|
|
61
|
+
# All member votes as a hash keyed by bioguide ID
|
|
62
|
+
votes = CongressGov.house_vote.member_votes_by_bioguide(
|
|
63
|
+
congress: 119, session: 1, roll_call: 281
|
|
64
|
+
)
|
|
65
|
+
# { "B001292" => "Nay", "A000055" => "Aye", ... }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Bills
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# Bill detail
|
|
72
|
+
bill = CongressGov.bill.get(119, "hr", 5371)
|
|
73
|
+
|
|
74
|
+
# Find House roll call votes associated with a bill
|
|
75
|
+
refs = CongressGov.bill.house_vote_references(119, "hr", 5371)
|
|
76
|
+
# [{ "rollNumber" => 281, "chamber" => "House", "url" => "https://clerk.house.gov/..." }]
|
|
77
|
+
|
|
78
|
+
# Actions, subjects, summaries, cosponsors
|
|
79
|
+
CongressGov.bill.actions(119, "hr", 5371)
|
|
80
|
+
CongressGov.bill.subjects(119, "hr", 5371)
|
|
81
|
+
CongressGov.bill.summaries(119, "hr", 5371)
|
|
82
|
+
CongressGov.bill.cosponsors(119, "hr", 5371)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Clerk of the House XML (Fallback)
|
|
86
|
+
|
|
87
|
+
Parse vote records directly from Clerk XML — no API key needed:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
vote = CongressGov.clerk_vote.fetch(year: 2025, roll_call: 281)
|
|
91
|
+
vote[:result] #=> "Passed"
|
|
92
|
+
vote[:totals][:yea] #=> 217
|
|
93
|
+
vote[:members]["B001292"] #=> { name: "Beyer", party: "D", state: "VA", vote: "Nay" }
|
|
94
|
+
|
|
95
|
+
# Or from a URL returned by bill actions
|
|
96
|
+
vote = CongressGov.clerk_vote.fetch_by_url("https://clerk.house.gov/evs/2025/roll281.xml")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Committees
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
CongressGov.committee.list(chamber: "house")
|
|
103
|
+
CongressGov.committee.get("senate", "ssap00")
|
|
104
|
+
CongressGov.committee.for_member("B001292")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Error Handling
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
begin
|
|
111
|
+
CongressGov.member.get("Z999999")
|
|
112
|
+
rescue CongressGov::NotFoundError
|
|
113
|
+
puts "Member not found"
|
|
114
|
+
rescue CongressGov::AuthenticationError
|
|
115
|
+
puts "Check your API key"
|
|
116
|
+
rescue CongressGov::RateLimitError
|
|
117
|
+
puts "Rate limited (5,000 req/hour)"
|
|
118
|
+
rescue CongressGov::ParseError => e
|
|
119
|
+
puts "Clerk XML parse failed: #{e.message}"
|
|
120
|
+
rescue CongressGov::ConnectionError => e
|
|
121
|
+
puts "Network issue: #{e.message}"
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## The Data Chain
|
|
126
|
+
|
|
127
|
+
This gem enables crossreferencing congressional votes with federal spending:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
District
|
|
131
|
+
-> CongressGov.member.current_for_district(state:, district:)
|
|
132
|
+
-> bioguide_id
|
|
133
|
+
-> CongressGov.bill.house_vote_references(congress, type, number)
|
|
134
|
+
-> roll_call numbers
|
|
135
|
+
-> CongressGov.house_vote.position_for_member(congress:, session:, roll_call:, bioguide_id:)
|
|
136
|
+
-> "Yea" or "Nay"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
bundle install
|
|
143
|
+
bundle exec rspec
|
|
144
|
+
bundle exec rubocop
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
To record VCR cassettes against the live API:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
export CONGRESS_GOV_API_KEY="your_real_key"
|
|
151
|
+
VCR_RECORD=new_episodes bundle exec rspec
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT License. See [LICENSE.txt](LICENSE.txt).
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'faraday/retry'
|
|
5
|
+
require 'faraday/http_cache'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
module CongressGov
|
|
9
|
+
# Low-level HTTP client that wraps Faraday for Congress.gov API requests.
|
|
10
|
+
# Handles authentication, retries, timeout, and error mapping.
|
|
11
|
+
class Client
|
|
12
|
+
# @return [CongressGov::Configuration]
|
|
13
|
+
attr_reader :config
|
|
14
|
+
|
|
15
|
+
# Creates a new client and validates the configuration.
|
|
16
|
+
#
|
|
17
|
+
# @param config [CongressGov::Configuration] configuration to use.
|
|
18
|
+
def initialize(config = CongressGov.configuration)
|
|
19
|
+
@config = config
|
|
20
|
+
config.validate!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns a memoized Faraday connection configured for the Congress.gov API.
|
|
24
|
+
#
|
|
25
|
+
# @return [Faraday::Connection]
|
|
26
|
+
def connection
|
|
27
|
+
@connection ||= Faraday.new(url: config.base_url) do |f|
|
|
28
|
+
f.options.timeout = config.timeout
|
|
29
|
+
f.options.open_timeout = 10
|
|
30
|
+
|
|
31
|
+
# Inject API key into every request as a query param
|
|
32
|
+
f.request :url_encoded
|
|
33
|
+
f.response :json, content_type: /\bjson$/
|
|
34
|
+
|
|
35
|
+
if config.cache_store
|
|
36
|
+
f.use :http_cache,
|
|
37
|
+
store: config.cache_store,
|
|
38
|
+
shared_cache: false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
f.request :retry,
|
|
42
|
+
max: config.retries,
|
|
43
|
+
interval: 1.0,
|
|
44
|
+
interval_randomness: 0.5,
|
|
45
|
+
backoff_factor: 2,
|
|
46
|
+
retry_statuses: [429, 500, 502, 503, 504],
|
|
47
|
+
exceptions: [
|
|
48
|
+
Faraday::TimeoutError,
|
|
49
|
+
Faraday::ConnectionFailed,
|
|
50
|
+
Faraday::RetriableResponse
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
f.response :logger, config.logger if config.logger
|
|
54
|
+
f.adapter config.adapter
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Plain HTTP client for fetching Clerk XML — no auth, no JSON parsing.
|
|
59
|
+
def clerk_connection
|
|
60
|
+
@clerk_connection ||= Faraday.new(url: config.clerk_base_url) do |f|
|
|
61
|
+
f.options.timeout = config.timeout
|
|
62
|
+
f.options.open_timeout = 10
|
|
63
|
+
f.adapter config.adapter
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# GET request — automatically appends api_key to params.
|
|
68
|
+
#
|
|
69
|
+
# @param path [String]
|
|
70
|
+
# @param params [Hash]
|
|
71
|
+
# @return [CongressGov::Response]
|
|
72
|
+
def get(path, params = {})
|
|
73
|
+
response = connection.get(path, params.merge(api_key: config.api_key, format: 'json'))
|
|
74
|
+
handle_response(response)
|
|
75
|
+
rescue Faraday::TimeoutError => e
|
|
76
|
+
raise ConnectionError, "Request timed out: #{e.message}"
|
|
77
|
+
rescue Faraday::ConnectionFailed => e
|
|
78
|
+
raise ConnectionError, "Connection failed: #{e.message}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Fetch raw XML from clerk.house.gov. Returns the raw body string.
|
|
82
|
+
# Does not raise on HTTP errors — callers handle nil/blank responses.
|
|
83
|
+
#
|
|
84
|
+
# @param path [String] e.g. "2025/roll281.xml"
|
|
85
|
+
# @return [String, nil]
|
|
86
|
+
def get_clerk_xml(path)
|
|
87
|
+
response = clerk_connection.get(path)
|
|
88
|
+
return nil unless response.status == 200
|
|
89
|
+
|
|
90
|
+
response.body
|
|
91
|
+
rescue Faraday::Error
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# Maps an HTTP response to a {Response} or raises an appropriate error.
|
|
98
|
+
#
|
|
99
|
+
# @param response [Faraday::Response]
|
|
100
|
+
# @return [CongressGov::Response]
|
|
101
|
+
# @raise [CongressGov::Error] on non-2xx status codes.
|
|
102
|
+
def handle_response(response)
|
|
103
|
+
case response.status
|
|
104
|
+
when 200..299
|
|
105
|
+
Response.new(response.body, response.status)
|
|
106
|
+
when 403
|
|
107
|
+
raise AuthenticationError.new(
|
|
108
|
+
'Invalid or missing API key',
|
|
109
|
+
status: 403,
|
|
110
|
+
body: response.body
|
|
111
|
+
)
|
|
112
|
+
when 404
|
|
113
|
+
raise NotFoundError.new(status: 404, body: response.body)
|
|
114
|
+
when 429
|
|
115
|
+
raise RateLimitError.new('Rate limit exceeded', status: 429, body: response.body)
|
|
116
|
+
when 400..499
|
|
117
|
+
raise ClientError.new(status: response.status, body: response.body)
|
|
118
|
+
when 500..599
|
|
119
|
+
raise ServerError.new(status: response.status, body: response.body)
|
|
120
|
+
else
|
|
121
|
+
raise Error, "Unexpected HTTP status: #{response.status}"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CongressGov
|
|
4
|
+
# Stores runtime settings for the CongressGov client.
|
|
5
|
+
# Use {CongressGov.configure} to modify these values.
|
|
6
|
+
class Configuration
|
|
7
|
+
# Default base URL for the Congress.gov API.
|
|
8
|
+
DEFAULT_BASE_URL = 'https://api.congress.gov/v3/'
|
|
9
|
+
# Default HTTP timeout in seconds.
|
|
10
|
+
DEFAULT_TIMEOUT = 30
|
|
11
|
+
# Default number of automatic retries on transient failures.
|
|
12
|
+
DEFAULT_RETRIES = 3
|
|
13
|
+
# Base URL for the House Clerk electronic voting system.
|
|
14
|
+
CLERK_BASE_URL = 'https://clerk.house.gov/evs/'
|
|
15
|
+
|
|
16
|
+
# @return [String, nil] API key used to authenticate requests.
|
|
17
|
+
# @return [String] base URL for the Congress.gov API.
|
|
18
|
+
# @return [String] base URL for the House Clerk voting site.
|
|
19
|
+
# @return [Integer] HTTP timeout in seconds.
|
|
20
|
+
# @return [Integer] number of automatic retries on transient failures.
|
|
21
|
+
# @return [Logger, nil] optional logger for request/response debugging.
|
|
22
|
+
# @return [Symbol] Faraday adapter to use for HTTP requests.
|
|
23
|
+
attr_accessor :api_key, :base_url, :clerk_base_url,
|
|
24
|
+
:timeout, :retries, :logger, :adapter,
|
|
25
|
+
:cache_store
|
|
26
|
+
|
|
27
|
+
# Initializes a new Configuration with sensible defaults.
|
|
28
|
+
# Reads +CONGRESS_GOV_API_KEY+ from the environment when available.
|
|
29
|
+
def initialize
|
|
30
|
+
@api_key = ENV.fetch('CONGRESS_GOV_API_KEY', nil)
|
|
31
|
+
@base_url = DEFAULT_BASE_URL
|
|
32
|
+
@clerk_base_url = CLERK_BASE_URL
|
|
33
|
+
@timeout = DEFAULT_TIMEOUT
|
|
34
|
+
@retries = DEFAULT_RETRIES
|
|
35
|
+
@logger = nil
|
|
36
|
+
@adapter = Faraday.default_adapter
|
|
37
|
+
@cache_store = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Raises {ConfigurationError} unless a valid API key is present.
|
|
41
|
+
#
|
|
42
|
+
# @return [void]
|
|
43
|
+
# @raise [ConfigurationError] if api_key is nil or empty.
|
|
44
|
+
def validate!
|
|
45
|
+
raise ConfigurationError, 'api_key is required' if api_key.nil? || api_key.empty?
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CongressGov
|
|
4
|
+
# Base error class. All gem errors inherit from this.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when api_key is missing or blank.
|
|
8
|
+
class ConfigurationError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised on 4xx responses (excluding 429).
|
|
11
|
+
class ClientError < Error
|
|
12
|
+
attr_reader :status, :body
|
|
13
|
+
|
|
14
|
+
def initialize(message = nil, status: nil, body: nil)
|
|
15
|
+
@status = status
|
|
16
|
+
@body = body
|
|
17
|
+
super(message || "HTTP #{status}: #{body}")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Raised on 403 — usually means invalid or missing API key.
|
|
22
|
+
class AuthenticationError < ClientError; end
|
|
23
|
+
|
|
24
|
+
# Raised on 404.
|
|
25
|
+
class NotFoundError < ClientError; end
|
|
26
|
+
|
|
27
|
+
# Raised on 429 after retries are exhausted.
|
|
28
|
+
class RateLimitError < ClientError; end
|
|
29
|
+
|
|
30
|
+
# Raised on 5xx responses.
|
|
31
|
+
class ServerError < Error
|
|
32
|
+
attr_reader :status, :body
|
|
33
|
+
|
|
34
|
+
def initialize(message = nil, status: nil, body: nil)
|
|
35
|
+
@status = status
|
|
36
|
+
@body = body
|
|
37
|
+
super(message || "Server error HTTP #{status}")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Raised on network-level failures (timeout, connection refused).
|
|
42
|
+
class ConnectionError < Error; end
|
|
43
|
+
|
|
44
|
+
# Raised when Clerk XML cannot be parsed or is malformed.
|
|
45
|
+
class ParseError < Error; end
|
|
46
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CongressGov
|
|
4
|
+
module Resources
|
|
5
|
+
# Access amendment data from the Congress.gov API.
|
|
6
|
+
class Amendment < Base
|
|
7
|
+
# Valid amendment type codes: samdt (Senate), hamdt (House), suamdt (Senate unprinted).
|
|
8
|
+
AMENDMENT_TYPES = %w[samdt hamdt suamdt].freeze
|
|
9
|
+
|
|
10
|
+
# List amendments with optional congress and type filters.
|
|
11
|
+
#
|
|
12
|
+
# @param congress [Integer, nil] e.g. 119
|
|
13
|
+
# @param amendment_type [String, nil] one of AMENDMENT_TYPES
|
|
14
|
+
# @param limit [Integer]
|
|
15
|
+
# @param offset [Integer]
|
|
16
|
+
# @return [CongressGov::Response]
|
|
17
|
+
def list(congress: nil, amendment_type: nil, limit: 20, offset: 0)
|
|
18
|
+
params = { limit: limit, offset: offset }
|
|
19
|
+
|
|
20
|
+
if congress && amendment_type
|
|
21
|
+
validate_type!(amendment_type)
|
|
22
|
+
client.get("amendment/#{congress}/#{amendment_type.downcase}", params)
|
|
23
|
+
elsif congress
|
|
24
|
+
client.get("amendment/#{congress}", params)
|
|
25
|
+
else
|
|
26
|
+
client.get('amendment', params)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Fetch a single amendment record.
|
|
31
|
+
#
|
|
32
|
+
# @param congress [Integer]
|
|
33
|
+
# @param type [String] one of AMENDMENT_TYPES
|
|
34
|
+
# @param number [Integer]
|
|
35
|
+
# @return [CongressGov::Response]
|
|
36
|
+
def get(congress, type, number)
|
|
37
|
+
validate_type!(type)
|
|
38
|
+
client.get("amendment/#{congress}/#{type.downcase}/#{number}")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Actions taken on an amendment.
|
|
42
|
+
#
|
|
43
|
+
# @param congress [Integer]
|
|
44
|
+
# @param type [String]
|
|
45
|
+
# @param number [Integer]
|
|
46
|
+
# @param limit [Integer]
|
|
47
|
+
# @param offset [Integer]
|
|
48
|
+
# @return [CongressGov::Response]
|
|
49
|
+
def actions(congress, type, number, limit: 20, offset: 0)
|
|
50
|
+
validate_type!(type)
|
|
51
|
+
client.get("amendment/#{congress}/#{type.downcase}/#{number}/actions",
|
|
52
|
+
{ limit: limit, offset: offset })
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Sub-amendments to an amendment.
|
|
56
|
+
#
|
|
57
|
+
# @param congress [Integer]
|
|
58
|
+
# @param type [String]
|
|
59
|
+
# @param number [Integer]
|
|
60
|
+
# @param limit [Integer]
|
|
61
|
+
# @param offset [Integer]
|
|
62
|
+
# @return [CongressGov::Response]
|
|
63
|
+
def amendments(congress, type, number, limit: 20, offset: 0)
|
|
64
|
+
validate_type!(type)
|
|
65
|
+
client.get("amendment/#{congress}/#{type.downcase}/#{number}/amendments",
|
|
66
|
+
{ limit: limit, offset: offset })
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Cosponsors of an amendment.
|
|
70
|
+
#
|
|
71
|
+
# @param congress [Integer]
|
|
72
|
+
# @param type [String]
|
|
73
|
+
# @param number [Integer]
|
|
74
|
+
# @param limit [Integer]
|
|
75
|
+
# @param offset [Integer]
|
|
76
|
+
# @return [CongressGov::Response]
|
|
77
|
+
def cosponsors(congress, type, number, limit: 20, offset: 0)
|
|
78
|
+
validate_type!(type)
|
|
79
|
+
client.get("amendment/#{congress}/#{type.downcase}/#{number}/cosponsors",
|
|
80
|
+
{ limit: limit, offset: offset })
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Text versions of an amendment.
|
|
84
|
+
#
|
|
85
|
+
# @param congress [Integer]
|
|
86
|
+
# @param type [String]
|
|
87
|
+
# @param number [Integer]
|
|
88
|
+
# @param limit [Integer]
|
|
89
|
+
# @param offset [Integer]
|
|
90
|
+
# @return [CongressGov::Response]
|
|
91
|
+
def text(congress, type, number, limit: 20, offset: 0)
|
|
92
|
+
validate_type!(type)
|
|
93
|
+
client.get("amendment/#{congress}/#{type.downcase}/#{number}/text",
|
|
94
|
+
{ limit: limit, offset: offset })
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def validate_type!(type)
|
|
100
|
+
return if AMENDMENT_TYPES.include?(type.to_s.downcase)
|
|
101
|
+
|
|
102
|
+
raise ArgumentError,
|
|
103
|
+
"amendment type must be one of: #{AMENDMENT_TYPES.join(', ')}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CongressGov
|
|
4
|
+
# Namespace for all Congress.gov API resource classes.
|
|
5
|
+
module Resources
|
|
6
|
+
# Abstract base class for all Congress.gov resource endpoints.
|
|
7
|
+
# Provides a shared client reference, a convenience +get+ wrapper,
|
|
8
|
+
# and generic auto-pagination via {#each_page} and {#paginate}.
|
|
9
|
+
class Base
|
|
10
|
+
# Creates a new resource bound to the given client.
|
|
11
|
+
#
|
|
12
|
+
# @param client [CongressGov::Client] the HTTP client to use.
|
|
13
|
+
def initialize(client = CongressGov.client)
|
|
14
|
+
@client = client
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Iterate through every page of a paginated endpoint.
|
|
18
|
+
# Yields each {CongressGov::Response} page.
|
|
19
|
+
#
|
|
20
|
+
# @param path [String] API path.
|
|
21
|
+
# @param params [Hash] query parameters (should include :limit).
|
|
22
|
+
# @yield [CongressGov::Response] each page of results.
|
|
23
|
+
# @return [Enumerator] if no block given.
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# CongressGov.bill.each_page('bill/119', limit: 250) do |page|
|
|
27
|
+
# page.results.each { |bill| process(bill) }
|
|
28
|
+
# end
|
|
29
|
+
def each_page(path, params = {}, &block)
|
|
30
|
+
return enum_for(:each_page, path, params) unless block
|
|
31
|
+
|
|
32
|
+
limit = params.fetch(:limit, 20)
|
|
33
|
+
offset = params.fetch(:offset, 0)
|
|
34
|
+
|
|
35
|
+
loop do
|
|
36
|
+
response = client.get(path, params.merge(limit: limit, offset: offset))
|
|
37
|
+
yield response
|
|
38
|
+
break unless response.has_next_page?
|
|
39
|
+
|
|
40
|
+
offset += limit
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Collect all results across all pages of a paginated endpoint.
|
|
45
|
+
# Returns a flat Array of all result hashes.
|
|
46
|
+
#
|
|
47
|
+
# @param path [String] API path.
|
|
48
|
+
# @param params [Hash] query parameters.
|
|
49
|
+
# @return [Array<Hash>] all results across all pages.
|
|
50
|
+
#
|
|
51
|
+
# @example
|
|
52
|
+
# all_bills = CongressGov.bill.paginate('bill/119/hr', limit: 250)
|
|
53
|
+
def paginate(path, params = {})
|
|
54
|
+
results = []
|
|
55
|
+
each_page(path, params) { |page| results.concat(page.results) }
|
|
56
|
+
results
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# @return [CongressGov::Client]
|
|
62
|
+
attr_reader :client
|
|
63
|
+
|
|
64
|
+
# Shorthand for {Client#get}.
|
|
65
|
+
#
|
|
66
|
+
# @param path [String] API path relative to the base URL.
|
|
67
|
+
# @param params [Hash] optional query parameters.
|
|
68
|
+
# @return [CongressGov::Response]
|
|
69
|
+
def get(path, params = {})
|
|
70
|
+
client.get(path, params)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|