technicalpickles-daywalker 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +1 -1
- data/VERSION.yml +2 -2
- data/lib/daywalker.rb +40 -2
- data/lib/daywalker/base.rb +5 -2
- data/lib/daywalker/district.rb +24 -10
- data/lib/daywalker/dynamic_finder_match.rb +2 -6
- data/lib/daywalker/geocoder.rb +18 -0
- data/lib/daywalker/legislator.rb +154 -57
- data/lib/daywalker/type_converter.rb +25 -4
- data/spec/daywalker/district_spec.rb +67 -45
- data/spec/daywalker/dynamic_finder_match_spec.rb +9 -9
- data/spec/daywalker/geocoder_spec.rb +46 -0
- data/spec/daywalker/legislator_spec.rb +258 -71
- data/spec/daywalker/type_converter_spec.rb +45 -9
- data/spec/fixtures/get_nonexistent_legislator.xml +9 -0
- data/spec/fixtures/rpi_location.yml +5 -0
- data/spec/spec_helper.rb +40 -6
- metadata +37 -4
data/README.rdoc
CHANGED
@@ -7,7 +7,7 @@ A Ruby wrapper for the Sunlight Labs API: http://wiki.sunlightlabs.com/Sunlight_
|
|
7
7
|
# Run the following if you haven't already:
|
8
8
|
gem sources -a http://gems.github.com
|
9
9
|
# Install the gem(s):
|
10
|
-
sudo gem install technicalpickles-
|
10
|
+
sudo gem install technicalpickles-daywalker
|
11
11
|
|
12
12
|
== Get an API key
|
13
13
|
|
data/VERSION.yml
CHANGED
data/lib/daywalker.rb
CHANGED
@@ -1,14 +1,34 @@
|
|
1
1
|
require 'happymapper'
|
2
2
|
require 'httparty'
|
3
|
+
require 'graticule'
|
3
4
|
|
4
5
|
require 'daywalker/base'
|
5
6
|
require 'daywalker/type_converter'
|
6
7
|
require 'daywalker/dynamic_finder_match'
|
7
8
|
require 'daywalker/district'
|
8
9
|
require 'daywalker/legislator'
|
10
|
+
require 'daywalker/geocoder'
|
9
11
|
|
12
|
+
# Daywalker is a Ruby-wrapper around the Sunlight API (http://wiki.sunlightlabs.com/Sunlight_API_Documentation). It implements all the functionality of the API related to Districts and Legislators (the Lobbyist API is considered experimental).
|
13
|
+
#
|
14
|
+
# Before using the API, you must register for an API key: http://services.sunlightlabs.com/api/register/
|
15
|
+
#
|
16
|
+
# After registering, you'll receive an email with a link to activate the key.
|
17
|
+
#
|
18
|
+
# To begin with, here's a small example script to print out all the districts and legislators for a zipcode:
|
19
|
+
#
|
20
|
+
# require 'rubygems'
|
21
|
+
# require 'pp'
|
22
|
+
# require 'daywalker'
|
23
|
+
#
|
24
|
+
# Daywalker.api_key = 'the api key you received'
|
25
|
+
#
|
26
|
+
# pp Daywalker::Legislator.all_by_zip(02114)
|
27
|
+
# pp Daywalker::District.all_by_zip(02114)
|
28
|
+
#
|
29
|
+
# See Daywalker::District and Daywalker::Legislator for more details on usage.
|
10
30
|
module Daywalker
|
11
|
-
# Set the API to be used
|
31
|
+
# Set the API to be used. This must be set when using Daywalker, BadApiKeyErrors will be occur.
|
12
32
|
def self.api_key=(api_key)
|
13
33
|
@api_key = api_key
|
14
34
|
end
|
@@ -18,7 +38,25 @@ module Daywalker
|
|
18
38
|
@api_key
|
19
39
|
end
|
20
40
|
|
41
|
+
def self.geocoder=(geocoder) # :nodoc:
|
42
|
+
@geocoder = geocoder
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.geocoder # :nodoc:
|
46
|
+
@geocoder
|
47
|
+
end
|
48
|
+
|
49
|
+
self.geocoder = Daywalker::Geocoder.new
|
50
|
+
|
21
51
|
# Error for when you use the API with a bad API key
|
22
|
-
class
|
52
|
+
class BadApiKeyError < StandardError
|
53
|
+
end
|
54
|
+
|
55
|
+
# Error for when an address can't be geocoded
|
56
|
+
class AddressError < StandardError
|
57
|
+
end
|
58
|
+
|
59
|
+
# Error for when an object was specifically looked for, but does not exist
|
60
|
+
class NotFoundError < StandardError
|
23
61
|
end
|
24
62
|
end
|
data/lib/daywalker/base.rb
CHANGED
@@ -7,7 +7,7 @@ module Daywalker
|
|
7
7
|
|
8
8
|
def self.handle_response(response)
|
9
9
|
case response.code.to_i
|
10
|
-
when 403 then raise
|
10
|
+
when 403 then raise BadApiKeyError
|
11
11
|
when 200
|
12
12
|
begin
|
13
13
|
parse(response.body)
|
@@ -21,7 +21,10 @@ module Daywalker
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def self.handle_bad_request(body)
|
24
|
-
|
24
|
+
case body
|
25
|
+
when "No Such Object Exists" then raise NotFoundError
|
26
|
+
else raise "Don't know how to handle #{body.inspect}"
|
27
|
+
end
|
25
28
|
end
|
26
29
|
end
|
27
30
|
end
|
data/lib/daywalker/district.rb
CHANGED
@@ -11,23 +11,26 @@ module Daywalker
|
|
11
11
|
element 'number', Integer
|
12
12
|
element 'state', String
|
13
13
|
|
14
|
-
# Find
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
# Find the district for a specific latitude and longitude.
|
15
|
+
#
|
16
|
+
# Returns a District. Raises ArgumentError if you omit latitude or longitude.
|
17
|
+
def self.unique_by_latitude_and_longitude(latitude, longitude)
|
18
|
+
raise(ArgumentError, 'missing required parameter latitude') if latitude.nil?
|
19
|
+
raise(ArgumentError, 'missing required parameter longitude') if longitude.nil?
|
18
20
|
|
19
21
|
query = {
|
20
|
-
:latitude =>
|
21
|
-
:longitude =>
|
22
|
+
:latitude => latitude,
|
23
|
+
:longitude => longitude,
|
22
24
|
:apikey => Daywalker.api_key
|
23
25
|
}
|
24
26
|
response = get('/districts.getDistrictFromLatLong.xml', :query => query)
|
25
|
-
handle_response(response)
|
27
|
+
handle_response(response).first
|
26
28
|
end
|
27
29
|
|
28
|
-
#
|
29
|
-
|
30
|
-
|
30
|
+
# Finds all districts for a specific zip code.
|
31
|
+
#
|
32
|
+
# Returns an Array of Districts. Raises ArgumentError if you omit the zip.
|
33
|
+
def self.all_by_zipcode(zip)
|
31
34
|
raise(ArgumentError, 'missing required parameter zip') if zip.nil?
|
32
35
|
|
33
36
|
query = {
|
@@ -39,5 +42,16 @@ module Daywalker
|
|
39
42
|
handle_response(response)
|
40
43
|
end
|
41
44
|
|
45
|
+
# Find the district for a specific address.
|
46
|
+
#
|
47
|
+
# Returns a District.
|
48
|
+
#
|
49
|
+
# Raises Daywalker::AddressError if the address can't be geocoded.
|
50
|
+
def self.unique_by_address(address)
|
51
|
+
raise(ArgumentError, 'missing required parameter address') if address.nil?
|
52
|
+
location = Daywalker.geocoder.locate(address)
|
53
|
+
|
54
|
+
unique_by_latitude_and_longitude(location[:latitude], location[:longitude])
|
55
|
+
end
|
42
56
|
end
|
43
57
|
end
|
@@ -4,11 +4,8 @@ module Daywalker
|
|
4
4
|
attr_accessor :finder, :attribute_names
|
5
5
|
def initialize(method)
|
6
6
|
case method.to_s
|
7
|
-
when /^
|
8
|
-
@finder =
|
9
|
-
when 'all_by' then :all
|
10
|
-
when 'by' then :one
|
11
|
-
end
|
7
|
+
when /^(unique|all)_by_([_a-zA-Z]\w*)$/
|
8
|
+
@finder = $1.to_sym
|
12
9
|
@attribute_names = $2.split('_and_').map {|each| each.to_sym}
|
13
10
|
end
|
14
11
|
end
|
@@ -18,6 +15,5 @@ module Daywalker
|
|
18
15
|
Daywalker::Legislator::VALID_ATTRIBUTES.include? each
|
19
16
|
end
|
20
17
|
end
|
21
|
-
|
22
18
|
end
|
23
19
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Daywalker
|
2
|
+
class Geocoder # :nodoc:
|
3
|
+
|
4
|
+
def locate(address)
|
5
|
+
location = geocoder.locate(address)
|
6
|
+
{ :longitude => location.longitude, :latitude => location.latitude }
|
7
|
+
rescue Graticule::AddressError => e
|
8
|
+
raise Daywalker::AddressError, e.message
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def geocoder
|
14
|
+
Graticule.service(:geocoder_us).new
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
data/lib/daywalker/legislator.rb
CHANGED
@@ -2,36 +2,90 @@ module Daywalker
|
|
2
2
|
# Represents a legislator, either a Senator or Representative.
|
3
3
|
#
|
4
4
|
# They have the following attributes:
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
5
|
+
#
|
6
|
+
# +title+::
|
7
|
+
# The title held by a Legislator, as a Symbol, ether <tt>:senator</tt> or <tt>:representative</tt>
|
8
|
+
#
|
9
|
+
# +first_name+::
|
10
|
+
# Legislator's first name
|
11
|
+
#
|
12
|
+
# +middle_name+::
|
13
|
+
# Legislator's middle name
|
14
|
+
#
|
15
|
+
# +last_name+::
|
16
|
+
# Legislator's last name
|
17
|
+
#
|
18
|
+
# +name_suffix+::
|
19
|
+
# Legislator's suffix (Jr., III, etc)
|
20
|
+
#
|
21
|
+
# +nickname+::
|
22
|
+
# Preferred nickname of Legislator
|
23
|
+
#
|
24
|
+
# +party+::
|
25
|
+
# Legislator's party as a +Sybmol+, <tt>:democrat</tt>, <tt>:republican</tt>, or <tt>:independent</tt>.
|
26
|
+
#
|
27
|
+
# +state+::
|
28
|
+
# two-letter +String+ abbreviation of the Legislator's state.
|
29
|
+
#
|
30
|
+
# +district+::
|
31
|
+
# The district a Legislator represents. For Representatives, this is a +Fixnum+. For Senators, this is a +Symbol+, either <tt>:junior_seat</tt> or <tt>:senior_seat</tt>.
|
32
|
+
#
|
33
|
+
# +in_office+::
|
34
|
+
# +true+ if the Legislator is currently server, or false if the Legislator is no longer due to defeat/resignation/death/etc.
|
35
|
+
#
|
36
|
+
# +gender+::
|
37
|
+
# Legislator's gender as a +Symbol+, :male or :female
|
38
|
+
#
|
39
|
+
# +phone+::
|
40
|
+
# Legislator's Congressional office phone number
|
41
|
+
# # FIXME normalize this to phone_number
|
42
|
+
#
|
43
|
+
# +fax_number+::
|
44
|
+
# Legislator's Congressional office fax number
|
45
|
+
#
|
46
|
+
# +website_url+::
|
47
|
+
# URL of the Legislator's Congressional wesbite as a +String+
|
48
|
+
#
|
49
|
+
# +webform_url+::
|
50
|
+
# URL of the Legislator's web contact form as a +String+
|
51
|
+
#
|
52
|
+
# +email+::
|
53
|
+
# Legislator's email address
|
54
|
+
#
|
55
|
+
# +congress_office+::
|
56
|
+
# Legislator's Washington, DC Office Address
|
57
|
+
#
|
58
|
+
# +bioguide_id+::
|
59
|
+
# Legislator's ID assigned by the COngressional Biographical DIrectory (http://bioguide.congress.gov) and also used by the Washington Post and NY Times.
|
60
|
+
#
|
61
|
+
# +votesmart_id+::
|
62
|
+
# Legislator ID assigned by Project Vote Smart (http://www.votesmart.org).
|
63
|
+
#
|
64
|
+
# +fec_id+::
|
65
|
+
# Legislator's ID provided by the Federal Election Commission (http://fec.gov)
|
66
|
+
#
|
67
|
+
# +govtrack_id+::
|
68
|
+
# Legislator's ID provided by Govtrack.us (http://www.govtrack.us)
|
69
|
+
#
|
70
|
+
# +crp_id+::
|
71
|
+
# Legislator's ID provided by Center for Responsive Politics (http://opensecrets.org)
|
72
|
+
#
|
73
|
+
# +eventful_id+::
|
74
|
+
# Legislator's 'Performer ID' on http://eventful.com
|
75
|
+
#
|
76
|
+
# +congresspedia_url+::
|
77
|
+
# URL of the Legislator's Congresspedia entry (http://congresspedia.org)
|
78
|
+
#
|
79
|
+
# +twitter_id+::
|
80
|
+
# Legislator's ID on Twitter (http://twitter.com)
|
81
|
+
#
|
82
|
+
# +youtube_url+::
|
83
|
+
# URL of the Legislator's YouTube account (http://youtube.com) as a +String+
|
30
84
|
class Legislator < Base
|
31
85
|
include HappyMapper
|
32
86
|
|
33
87
|
tag 'legislator'
|
34
|
-
element '
|
88
|
+
element 'district', TypeConverter, :tag => 'district', :parser => :district_to_sym_or_i
|
35
89
|
element 'title', TypeConverter, :parser => :title_abbr_to_sym
|
36
90
|
element 'eventful_id', String
|
37
91
|
element 'in_office', Boolean
|
@@ -60,11 +114,12 @@ module Daywalker
|
|
60
114
|
element 'sunlight_old_id', String
|
61
115
|
element 'congresspedia_url', String
|
62
116
|
|
63
|
-
VALID_ATTRIBUTES = [:
|
117
|
+
VALID_ATTRIBUTES = [:district, :title, :eventful_id, :in_office, :state, :votesmart_id, :official_rss_url, :party, :email, :crp_id, :website_url, :fax_number, :govtrack_id, :first_name, :middle_name, :last_name, :congress_office, :bioguide_id, :webform_url, :youtube_url, :nickname, :phone, :fec_id, :gender, :name_suffix, :twitter_id, :sunlight_old_id, :congresspedia_url]
|
64
118
|
|
65
|
-
# Find all
|
66
|
-
def self.
|
119
|
+
# Find all Legislators who serve a particular zip code. This would include the Junior and Senior Senators, as well as the Representatives of any Districts in the zip code.
|
120
|
+
def self.all_by_zip(zip)
|
67
121
|
raise ArgumentError, 'missing required parameter zip' if zip.nil?
|
122
|
+
|
68
123
|
query = {
|
69
124
|
:zip => zip,
|
70
125
|
:apikey => Daywalker.api_key
|
@@ -74,38 +129,79 @@ module Daywalker
|
|
74
129
|
handle_response(response)
|
75
130
|
end
|
76
131
|
|
77
|
-
# Find
|
78
|
-
# VALID_ATTRIBUTES for possible attributes you can search for.
|
79
|
-
#
|
80
|
-
# If you want one legislators, and you expect there is exactly one
|
81
|
-
# legislator, use :one. An error will be raised if there are more than
|
82
|
-
# one result. An ArgumentErrror will be raised if multiple results come back.
|
132
|
+
# Find a unique legislator matching a Hash of attribute name/values. See VALID_ATTRIBUTES for possible attributes.
|
83
133
|
#
|
84
|
-
# Daywalker::Legislator.
|
134
|
+
# Daywalker::Legislator.unique(:state => 'NY', :district => 4)
|
85
135
|
#
|
86
|
-
#
|
87
|
-
#
|
88
|
-
# Daywalker::Legislator.find(:all, :state => 'NY', :title => :senator)
|
136
|
+
# Dynamic finders based on the attribute names are also possible. This query can be rewritten as:
|
89
137
|
#
|
90
|
-
#
|
138
|
+
# Daywalker::Legislator.unique_by_state_and_district('NY', 4)
|
91
139
|
#
|
92
|
-
#
|
93
|
-
#
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
140
|
+
# Returns a Legislator. ArgumentError is raised if more than one result is found.
|
141
|
+
#
|
142
|
+
# Gotchas:
|
143
|
+
# * Results are case insensative (Richard and richard are equivilant)
|
144
|
+
# * Results are exact (Richard vs Rich are not the same)
|
145
|
+
def self.unique(conditions)
|
146
|
+
conditions = TypeConverter.normalize_conditions(conditions)
|
147
|
+
query = conditions.merge(:apikey => Daywalker.api_key)
|
148
|
+
|
149
|
+
response = get('/legislators.get.xml', :query => query)
|
150
|
+
|
151
|
+
handle_response(response).first
|
152
|
+
end
|
100
153
|
|
154
|
+
# Find all legislators matching a Hash of attribute name/values. See VALID_ATTRIBUTES for possible attributes.
|
155
|
+
#
|
156
|
+
# Daywalker::Legislator.all(:state => 'NY', :title => :senator)
|
157
|
+
#
|
158
|
+
# Dynamic finders based on the attribute names are also possible. This query can be rewritten as:
|
159
|
+
#
|
160
|
+
# Daywalker::Legislator.all_by_state_and_title('NY', :senator)
|
161
|
+
#
|
162
|
+
# Returns an Array of Legislators.
|
163
|
+
#
|
164
|
+
# Gotchas:
|
165
|
+
# * Results are case insensative (Richard and richard are equivilant)
|
166
|
+
# * Results are exact (Richard vs Rich)
|
167
|
+
# * nil attributes will match anything, not legislators without a value for the attribute
|
168
|
+
# * Passing an Array of values to match, ie <tt>:state => ['NH', 'MA']</tt> is not supported at this time
|
169
|
+
def self.all(conditions)
|
101
170
|
conditions = TypeConverter.normalize_conditions(conditions)
|
102
171
|
query = conditions.merge(:apikey => Daywalker.api_key)
|
103
|
-
response = get(url, :query => query)
|
104
172
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
173
|
+
response = get('/legislators.getList.xml', :query => query)
|
174
|
+
|
175
|
+
handle_response(response)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Find all the legislators serving a specific latitude and longitude. This will include the district's Represenative, the Senior Senator, and the Junior Senator.
|
179
|
+
#
|
180
|
+
# Returns a Hash containing keys :representative, :junior_senator, and :senior_senator, with values corresponding to the appropriate Legislator.
|
181
|
+
#
|
182
|
+
def self.all_by_latitude_and_longitude(latitude, longitude)
|
183
|
+
district = District.unique_by_latitude_and_longitude(latitude, longitude)
|
184
|
+
|
185
|
+
representative = unique_by_state_and_district(district.state, district.number)
|
186
|
+
junior_senator = unique_by_state_and_district(district.state, :junior_seat)
|
187
|
+
senior_senator = unique_by_state_and_district(district.state, :senior_seat)
|
188
|
+
|
189
|
+
{
|
190
|
+
:representative => representative,
|
191
|
+
:junior_senator => junior_senator,
|
192
|
+
:senior_senator => senior_senator
|
193
|
+
}
|
194
|
+
end
|
195
|
+
|
196
|
+
# Find all the legislators serving a specific address. This will include the district's Represenative, the Senior Senator, and the Junior Senator.
|
197
|
+
#
|
198
|
+
# Returns a Hash containing keys :representative, :junior_senator, and :senior_senator, with values corresponding to the appropriate Legislator.
|
199
|
+
#
|
200
|
+
# Raises Daywalker::AddressError if the address can't be geocoded.
|
201
|
+
def self.all_by_address(address)
|
202
|
+
location = Daywalker.geocoder.locate(address)
|
203
|
+
|
204
|
+
all_by_latitude_and_longitude(location[:latitude], location[:longitude])
|
109
205
|
end
|
110
206
|
|
111
207
|
def self.method_missing(method_id, *args, &block) # :nodoc:
|
@@ -118,7 +214,7 @@ module Daywalker
|
|
118
214
|
end
|
119
215
|
end
|
120
216
|
|
121
|
-
def self.respond_to?(method_id) # :nodoc:
|
217
|
+
def self.respond_to?(method_id, include_private = false) # :nodoc:
|
122
218
|
match = DynamicFinderMatch.new(method_id)
|
123
219
|
if match.match?
|
124
220
|
true
|
@@ -132,6 +228,7 @@ module Daywalker
|
|
132
228
|
def self.handle_bad_request(body) # :nodoc:
|
133
229
|
case body
|
134
230
|
when "Multiple Legislators Returned" then raise(ArgumentError, "The conditions provided returned multiple results, by only one is expected")
|
231
|
+
# FIXME need to catcfh when legislator (or any object, which would mean it goes in super) is not found
|
135
232
|
else super
|
136
233
|
end
|
137
234
|
end
|
@@ -139,12 +236,12 @@ module Daywalker
|
|
139
236
|
|
140
237
|
def self.create_finder_method(method, finder, attribute_names) # :nodoc:
|
141
238
|
class_eval %{
|
142
|
-
def self.#{method}(*args) # def self.
|
239
|
+
def self.#{method}(*args) # def self.all_by_district_and_state(*args)
|
143
240
|
conditions = args.last.kind_of?(Hash) ? args.pop : {} # conditions = args.last.kind_of?(Hash) ? args.pop : {}
|
144
|
-
[:#{attribute_names.join(', :')}].each_with_index do |key, index| # [:
|
241
|
+
[:#{attribute_names.join(', :')}].each_with_index do |key, index| # [:district, :state].each_with_index do |key, index|
|
145
242
|
conditions[key] = args[index] # conditions[key] = args[index]
|
146
243
|
end # end
|
147
|
-
|
244
|
+
#{finder}(conditions) # all(conditions)
|
148
245
|
end # end
|
149
246
|
}
|
150
247
|
end
|