technicalpickles-daywalker 0.1.2 → 0.2.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.
- 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
|