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 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-daywalke
10
+ sudo gem install technicalpickles-daywalker
11
11
 
12
12
  == Get an API key
13
13
 
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :minor: 1
3
- :patch: 2
2
+ :minor: 2
3
+ :patch: 0
4
4
  :major: 0
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 BadApiKey < StandardError
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
@@ -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 BadApiKey
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
- raise "Don't know how to handle #{body.inspect}"
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
@@ -11,23 +11,26 @@ module Daywalker
11
11
  element 'number', Integer
12
12
  element 'state', String
13
13
 
14
- # Find districts by latitude and longitude.
15
- def self.find_by_latlng(lat, lng)
16
- # TODO use ArgumentError
17
- raise(ArgumentError, 'missing required parameter latitude') if lat.nil?
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 => lat,
21
- :longitude => lng,
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) # TODO should only ever return one?
27
+ handle_response(response).first
26
28
  end
27
29
 
28
- # Find districts by zip code
29
- def self.find_by_zip(zip)
30
- # TODO use ArgumentError
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 /^find_(all_by|by)_([_a-zA-Z]\w*)$/
8
- @finder = case $1
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
@@ -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
- # * district_number
6
- # * title (ether :senator or :representative)
7
- # * eventful_id (on http://eventful.com)
8
- # * in_office (true or false)
9
- # * state (two-letter abbreviation)
10
- # * votesmart_id (on http://www.votesmart.org)
11
- # * party (:democrat, :republican, or :independent)
12
- # * crp_id (on http://opensecrets.org)
13
- # * website_url
14
- # * fax_number
15
- # * govtrack_id (on http://www.govtrack.us)
16
- # * first_name
17
- # * middle_name
18
- # * last_name
19
- # * congress_office (address in Washington, DC)
20
- # * bioguide_id (on http://bioguide.congress.gov)
21
- # * webform_url
22
- # * youtube_url
23
- # * nickname
24
- # * phone
25
- # * fec_id (on http://fec.gov)
26
- # * gender (:male or :female)
27
- # * name_suffix
28
- # * twitter_id (on http://twitter.com)
29
- # * congresspedia_url
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 'district_number', Integer, :tag => 'district'
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 = [:district_number, :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]
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 legislators in a particular zip code
66
- def self.find_all_by_zip(zip)
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 one or many legislators, based on a set of conditions. See
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.find(:one, :state => 'NY', :district => 4)
134
+ # Daywalker::Legislator.unique(:state => 'NY', :district => 4)
85
135
  #
86
- # Otherwise, use :all.
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
- # Additionally, dynamic finders based on these attributes are available:
138
+ # Daywalker::Legislator.unique_by_state_and_district('NY', 4)
91
139
  #
92
- # Daywalker::Legislator.find_by_state_and_district('NY', 4)
93
- # Daywalker::Legislator.find_all_by_state_and_senator('NY', :senator)
94
- def self.find(sym, conditions)
95
- url = case sym
96
- when :one then '/legislators.get.xml'
97
- when :all then '/legislators.getList.xml'
98
- else raise ArgumentError, "invalid argument #{sym.inspect}, only :one and :all are allowed"
99
- end
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
- case sym
106
- when :one then handle_response(response).first
107
- when :all then handle_response(response)
108
- end
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.find_all_by_district_number_and_state(*args)
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| # [:district_number, :state].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
- find(#{finder.inspect}, conditions) # find(:all, conditions)
244
+ #{finder}(conditions) # all(conditions)
148
245
  end # end
149
246
  }
150
247
  end