nytimes-congress 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ #
2
+ # Copyright (c) 2010 Patrick Ewing (<patrick.henry.ewing@gmail.com>) and Derek Willis (<dwillis@gmail.com>).
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ # this software and associated documentation files (the "Software"), to deal in the
6
+ # Software without restriction, including without limitation the rights to use,
7
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8
+ # Software, and to permit persons to whom the Software is furnished to do so,
9
+ # subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in all
12
+ # copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
18
+ # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
19
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,108 @@
1
+ NY Times Congress
2
+ ===============
3
+
4
+ A Ruby wrapper for the New York Times Congress API.
5
+ ---------------------
6
+ The NY Times has been quietly scraping the web for data related to the United States Congress, and making the data freely available through a public [API](http://open.blogs.nytimes.com/2009/01/08/introducing-the-congress-api/). [nytimes-congress](http://github.com/dwillis/nytimes-congress/) aims to make it even easier to write Ruby applications using this data, and also provides a command shell for interacting with the API directly.
7
+
8
+ Introducing Congresh
9
+ ---------------------
10
+ The Congress Shell (congresh for short/cute) is a simple interactive prompt that wraps IRB. It includes your API key (see setup below) and provides a few conveniences for test-driving the API.
11
+
12
+ Congress.new
13
+ ---------------------
14
+ You get a Congress object either by calling Congress.new with a number and chamber with an optional session number, or by using the Senate and House constants which return the current session of each:
15
+
16
+ current_senate = Senate
17
+ current_house = House
18
+ 2007_senate = Congress.new(109, 'senate')
19
+ 2009_house_2nd_session = Congress.new(111, 'house', 2)
20
+
21
+ Through a Congress object, you can get a hash of Legislators, keyed by congressional Bio ID.
22
+
23
+ senators = Senate.members
24
+
25
+ Legislator
26
+ ---------------------
27
+ You could crawl the Hash values to find the bio ID in question, as this is a useful key to use across other open government APIs (notably Sunlight).
28
+
29
+ $ hrc = senators.values.find {|legislator| legislator.name == "Hillary Clinton"}
30
+ $ bio_id = hrc.bio_id
31
+
32
+ Legislators come down the wire with a lot of interesting info, such as how often they vote along party lines, what roles they serve in Congress and more. Check out the full set of attributes in the Legislator class.
33
+
34
+ You can also grab a Legislator directly by their Bio ID. This call includes full details on the congressperson's roles, biographical info and more:
35
+
36
+ $ Legislator.find('C001041')
37
+
38
+ When accessing a Legislator from a collection of congress members, they include only a limited set of attributes. However, the library will make a second API call and lazy-load full attributes if you ask for them specifically. So even though a Legislator object returned through Congress.members don't have the #gender attribute, if you call for a specific Legislators gender, that data will be fetched and populated just in time.
39
+
40
+ Current Member
41
+ ---------------------
42
+ If you need to check the current representative for a House district or senators from a state, each chamber exposes a method for that. Note that the senate call does not need a district and returns a list of two senators.
43
+
44
+ $ pa19 = House.current_member_for_state_district("PA", 19)
45
+ $ pa19.name
46
+ "Todd R. Platts"
47
+
48
+
49
+ Roll Call Votes
50
+ ---------------------
51
+
52
+ To find a Vote in any congress, you need to know the session (usually 1 or 2, although rarely congress will go into a 3rd session) and then the ID of the vote. Recent votes can be found easily through [Thomas](http://thomas.loc.gov/home/rollcallvotes.html). Curious about the details of the "American Recovery and Reinvestment Act"? That's vote number 61:
53
+
54
+ $ vote = Senate.roll_call_vote(1, 61)
55
+
56
+ This returns a Vote object which also has an array of Positions, showing which legislators voted for and against this bill. Thus it would be simple to collect everyone who voted for and against this bill:
57
+
58
+ $ vote.positions.find_all {|position| position.for?}
59
+ $ vote.positions.find_all {|position| position.against?}
60
+
61
+ The vote also includes an array of vacant seats, if present.
62
+
63
+ You can also find recent votes by any given senator:
64
+
65
+ $ hillary = Legislator.find('C001041')
66
+ $ hillary.votes
67
+
68
+
69
+ Setup
70
+ ---------------------
71
+
72
+ Add Github to your rubygems sources (if you haven't already) and then install the gem:
73
+ $ gem sources -a http://gems.github.com
74
+ $ sudo gem install nytimes-congress
75
+
76
+ You'll also need to [get an API key]:http://developer.nytimes.com/apps/register from the [NY Times Developer Network]: http://developer.nytimes.com/. If you set the key returned to an environment variable as shown below, Congresh will pick it up and include it in all your requests. You can keep this in your bash profile, but I also recommend putting all your developer keys in a separate .api_keys file in your path
77
+
78
+ export NY_TIMES_CONGRESS_API_KEY="123456789ETC"
79
+
80
+ Then, just call 'congresh' at the command line, which will open up the Congresh shell.
81
+
82
+ Within an app, you can use the library like so:
83
+
84
+ require 'ny-times-congress'
85
+ include NYTimes::Congress
86
+ Base.api_key = '123456789ETC'
87
+
88
+
89
+ Acknowledgements
90
+ ---------------------
91
+ All information made available through this software is generously gathered and hosted by the New York Times (read Terms of Use below). Inspiration and code was borrowed from the excellent nytimes-movies gem written by Jacob Harris and the sunlight gem by Luigi Montanez. Thanks to Marcel Molina, Jr. for pairing with me on the lazy-loading idea.
92
+
93
+
94
+ Terms of Use
95
+ ---------------------
96
+ All information made available through this software is generously organized and hosted by the New York Times and is subject to copyright. By obtaining an API key throught their Developer program and accessing this data you are agreeing to abide by certain rules and restrictions. These are available at the URLs below and you should read them before proceeding:
97
+
98
+ http://developer.nytimes.com/attribution
99
+ http://developer.nytimes.com/Api_terms_of_use
100
+
101
+
102
+ License
103
+ ---------------------
104
+ Copyright (c) 2010 Patrick Ewing (<patrick.henry.ewing@gmail.com>) and Derek Willis (<dwillis@gmail.com>).
105
+
106
+
107
+ Made available under the MIT License (read LICENSE for details).
108
+
@@ -0,0 +1,56 @@
1
+ require 'rake'
2
+
3
+ begin
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |s|
6
+ s.name = "nytimes-congress"
7
+ s.executables = "congresh"
8
+ s.summary = "Ruby wrapper and command shell for the New York Times Congress API"
9
+ s.authors = ["Patrick Ewing", "Derek Willis"]
10
+ s.files = FileList["[A-Z]*", "{bin,lib,spec}/**/*"]
11
+ s.add_dependency("json", [">= 1.1.3"])
12
+ s.email = "dwillis@gmail.com"
13
+ s.homepage = "http://github.com/dwillis/nytimes-congress"
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
18
+
19
+ require 'rake/rdoctask'
20
+ Rake::RDocTask.new do |rdoc|
21
+ rdoc.rdoc_dir = 'rdoc'
22
+ rdoc.title = 'ny-times-congress'
23
+ rdoc.options << '--line-numbers' << '--inline-source'
24
+ rdoc.rdoc_files.include('README*')
25
+ rdoc.rdoc_files.include('lib/**/*.rb')
26
+ end
27
+
28
+ begin
29
+ require 'spec/rake/spectask'
30
+ desc "Run all examples"
31
+ Spec::Rake::SpecTask.new('spec') do |t|
32
+ t.spec_files = FileList['spec/**/*_spec.rb']
33
+ end
34
+ rescue LoadError
35
+ puts "RSpec is not available. You'll need it to test ny-times-congress."
36
+ end
37
+
38
+ begin
39
+ require 'rcov/rcovtask'
40
+ Rcov::RcovTask.new do |t|
41
+ t.libs << 'test'
42
+ t.test_files = FileList['test/**/*_test.rb']
43
+ t.verbose = true
44
+ end
45
+ rescue LoadError
46
+ puts "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
47
+ end
48
+
49
+ begin
50
+ require 'cucumber/rake/task'
51
+ Cucumber::Rake::Task.new(:features)
52
+ rescue LoadError
53
+ puts "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
54
+ end
55
+
56
+ task :default => :spec
data/TODO ADDED
@@ -0,0 +1,20 @@
1
+ Currently Supported:
2
+ * Roll-Call Votes
3
+ * Members of Congress
4
+ * Member Bio and Roles
5
+ * Member Vote Positions
6
+ * Member Vote Comparison
7
+ * New Members
8
+ * Current member for state/district
9
+
10
+ Not yet supported:
11
+ Missed Votes and Party Votes
12
+ Nomination Votes
13
+ Committees and Committee Members
14
+ Bills Cosponsored by a Member
15
+ Member Floor Appearances
16
+ Recent Bills
17
+ Bills by Member
18
+ Bill Details
19
+ Bill Subjects, Amendments and Related Bills
20
+ Bill Cosponsors
@@ -0,0 +1,5 @@
1
+ ---
2
+ :build:
3
+ :patch: 0
4
+ :major: 1
5
+ :minor: 4
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ congress_lib = File.dirname(__FILE__) + '/../lib/ny-times-congress'
3
+ setup = File.dirname(__FILE__) + '/setup'
4
+ irb_name = RUBY_PLATFORM =~ /mswin32/ ? 'irb.bat' : 'irb'
5
+
6
+ exec "#{irb_name} -r #{congress_lib} -r #{setup} --simple-prompt"
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ include NYTimes::Congress
3
+
4
+ if ENV['NY_TIMES_CONGRESS_API_KEY']
5
+ NYTimes::Congress::Base.api_key = ENV['NY_TIMES_CONGRESS_API_KEY']
6
+
7
+ Senate = Congress.new(111, :senate, 2)
8
+ House = Congress.new(111, :house, 2)
9
+
10
+ end
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.push(File.join(File.dirname(__FILE__), "../lib/ny-times"))
2
+
3
+ require 'rubygems'
4
+ require 'open-uri'
5
+ require 'JSON'
6
+
7
+ require 'congress/base'
8
+ require 'congress/attribute_transformation'
9
+ require 'congress/congress'
10
+ require 'congress/legislator'
11
+ require 'congress/legislator_vote_comparison'
12
+ require 'congress/role'
13
+ require 'congress/roll_call_vote'
14
+ require 'congress/position'
15
+ require 'congress/nomination'
16
+ require 'congress/floor'
17
+ require 'congress/current_member'
@@ -0,0 +1,75 @@
1
+ module NYTimes
2
+ module Congress
3
+ module AttributeTransformation
4
+
5
+ def transform(args, attributes_map)
6
+ raise "Can only transform a hash" unless args.kind_of?(Hash)
7
+ new_values = {}
8
+ attributes_map.each_pair do |transformation, attributes|
9
+ attributes.each do |attribute_name|
10
+ value = args[attribute_name.to_s]
11
+ new_values[attribute_name] = do_transformation(transformation, value)
12
+ end
13
+ end
14
+ new_values
15
+ end
16
+
17
+ def do_transformation(transformation, value)
18
+ send(transformation, value) unless empty?(value)
19
+ end
20
+
21
+ def date_for(string)
22
+ Date.parse(string)
23
+ end
24
+
25
+ def string_for(value)
26
+ value.to_s
27
+ end
28
+
29
+ def float_for(value)
30
+ value.to_f
31
+ end
32
+
33
+ def integer_for(string)
34
+ if string.respond_to? :to_i
35
+ string.to_i
36
+ else
37
+ string
38
+ end
39
+ end
40
+
41
+ def symbol_for(string)
42
+ string.to_sym
43
+ end
44
+
45
+ def roles_for(roles_array)
46
+ roles_array.collect {|e| Role.new(e)}
47
+ end
48
+
49
+ def appearances_for(floor_array)
50
+ floor_array.collect do |floor_hash|
51
+ Floor.new(floor_hash['date'], floor_hash['title'], floor_hash['url'], floor_hash['start_time'], floor_hash['end_time'])
52
+ end
53
+ end
54
+
55
+ def votes_for(votes_array)
56
+ votes_array.collect {|vote_hash| RollCallVote.new(vote_hash['vote'])}
57
+ end
58
+
59
+ def positions_for(votes_array)
60
+ votes_array.collect do |vote_hash|
61
+ Position.new(vote_hash['member_id'], vote_hash['vote_position'])
62
+ end
63
+ end
64
+
65
+ def vacant_seats_for(seats_array)
66
+ seats_array.collect {|s| Role.new(s)}
67
+ end
68
+
69
+ def empty?(value)
70
+ value.nil? || value == "N/A" || value == ""
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,70 @@
1
+ module NYTimes
2
+ module Congress
3
+ class Base
4
+ API_NAME = 'congress'
5
+ API_SERVER = 'api.nytimes.com'
6
+ API_VERSION = 3
7
+ API_BASE = "/svc/politics/v#{API_VERSION}/us/legislative/#{API_NAME}"
8
+
9
+ @@api_key = nil
10
+ @@copyright = nil
11
+
12
+ class << self
13
+
14
+ # The copyright footer to be placed at the bottom of any data from the New York Times. Note this is only set after an API call.
15
+ def copyright
16
+ @@copyright
17
+ end
18
+
19
+ # Set the API key used for operations. This needs to be called before any requests against the API. To obtain an API key, go to http://developer.nytimes.com/
20
+ def api_key=(key)
21
+ @@api_key = key
22
+ end
23
+
24
+ def api_key
25
+ @@api_key
26
+ end
27
+
28
+ # Builds a request URI to call the API server
29
+ def build_request_url(path, params)
30
+ URI::HTTP.build :host => API_SERVER,
31
+ :path => "#{API_BASE}/#{path}",
32
+ :query => params.map {|k,v| "#{k}=#{v}"}.join('&')
33
+ end
34
+
35
+ def invoke(path, params={})
36
+ begin
37
+ if @@api_key.nil?
38
+ raise "You must initialize the API key before you run any API queries"
39
+ end
40
+
41
+ full_params = params.merge 'api-key' => @@api_key
42
+ uri = build_request_url(path, full_params)
43
+
44
+ reply = uri.read
45
+ reply = JSON.parse(reply)
46
+
47
+ raise "Empty reply returned from API" if reply.nil?
48
+
49
+ @@copyright = reply['copyright']
50
+
51
+ reply
52
+ rescue OpenURI::HTTPError => e
53
+ return nil if e.message =~ /^404/
54
+ raise "Error connecting to URL #{uri} #{e}"
55
+ end
56
+ end
57
+
58
+ def define_lazy_reader_for_attribute_named(attribute)
59
+ class_eval(<<-EVAL, __FILE__, __LINE__)
60
+ def #{attribute}
61
+ load_fully if attributes[:#{attribute}].nil? && !fully_loaded
62
+ attributes[:#{attribute}]
63
+ end
64
+ EVAL
65
+ end
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,82 @@
1
+ module NYTimes
2
+ module Congress
3
+ class Congress < Base
4
+ attr_reader :number, :session, :chamber
5
+
6
+ include AttributeTransformation
7
+
8
+ def initialize(number, chamber, session = nil)
9
+ @number, @session = integer_for(number), integer_for(session)
10
+ @chamber = symbol_for(chamber)
11
+ raise AttributeError unless number && chamber
12
+ end
13
+
14
+ def members(params = {})
15
+ @members ||= fetch_members(Base.invoke("#{api_path}/members.json")['results'].first)
16
+ end
17
+
18
+ def self.new_members(params = {})
19
+ Congress.fetch_new_members
20
+ end
21
+
22
+ def current_member_for_state_district(state, district=nil)
23
+ if district
24
+ api_path = "members/house/#{state}/#{district}/current.json"
25
+ else
26
+ api_path = "members/senate/#{state}/current.json"
27
+ end
28
+ response = Base.invoke(api_path)['results']
29
+ fetch_current_members(response)
30
+ end
31
+
32
+ def roll_call_vote(session_number, roll_call_number, params = {})
33
+ results = Base.invoke("#{api_path}/sessions/#{session_number}/votes/#{roll_call_number}.json")['results']['votes']
34
+ RollCallVote.new(results)
35
+ end
36
+
37
+ def to_s
38
+ "#{number} #{chamber.upcase}"
39
+ end
40
+
41
+ def compare(legislator_1, legislator_2)
42
+ response = Base.invoke("members/#{legislator_1}/compare/#{legislator_2}/#{number}/#{chamber}.json")
43
+ if response
44
+ LegislatorVoteComparison.new(response['results'].first)
45
+ end
46
+ end
47
+
48
+ protected
49
+
50
+ def fetch_members(results)
51
+ results.inject({}) do |hash, member|
52
+ hash[member['id']] = Legislator.new(member)
53
+ hash.delete_if {|k,v| k.nil? }
54
+ hash
55
+ end
56
+ end
57
+
58
+ def fetch_current_members(results)
59
+ if results.length > 1
60
+ results.collect do |member|
61
+ CurrentMember.new(member)
62
+ end
63
+ else
64
+ CurrentMember.new(results.first)
65
+ end
66
+ end
67
+
68
+ def self.fetch_new_members
69
+ results = Base.invoke("/members/new.json")['results'].first
70
+ results['members'].inject({}) do |hash, member|
71
+ hash[member['id']] = Legislator.new(member)
72
+ hash
73
+ end
74
+ end
75
+
76
+ def api_path
77
+ "#{number}/#{chamber}"
78
+ end
79
+ end
80
+
81
+ end
82
+ end