govkit-h 0.7.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.
Files changed (72) hide show
  1. data/.document +5 -0
  2. data/.rspec +3 -0
  3. data/Gemfile +13 -0
  4. data/LICENSE +20 -0
  5. data/README.md +70 -0
  6. data/Rakefile +82 -0
  7. data/USAGE +1 -0
  8. data/VERSION +1 -0
  9. data/generators/govkit/govkit_generator.rb +24 -0
  10. data/generators/govkit/templates/govkit.rb +24 -0
  11. data/govkit.gemspec +130 -0
  12. data/init.rb +4 -0
  13. data/lib/generators/govkit/govkit_generator.rb +21 -0
  14. data/lib/generators/govkit/templates/create_mentions.rb +21 -0
  15. data/lib/generators/govkit/templates/govkit.rb +24 -0
  16. data/lib/generators/govkit/templates/mention.rb +15 -0
  17. data/lib/gov_kit.rb +45 -0
  18. data/lib/gov_kit/acts_as_noteworthy.rb +63 -0
  19. data/lib/gov_kit/configuration.rb +58 -0
  20. data/lib/gov_kit/follow_the_money.rb +176 -0
  21. data/lib/gov_kit/open_congress.rb +125 -0
  22. data/lib/gov_kit/open_congress/bill.rb +171 -0
  23. data/lib/gov_kit/open_congress/blog_post.rb +15 -0
  24. data/lib/gov_kit/open_congress/news_post.rb +15 -0
  25. data/lib/gov_kit/open_congress/person.rb +141 -0
  26. data/lib/gov_kit/open_congress/person_stat.rb +13 -0
  27. data/lib/gov_kit/open_congress/roll_call.rb +14 -0
  28. data/lib/gov_kit/open_congress/roll_call_comparison.rb +28 -0
  29. data/lib/gov_kit/open_congress/voting_comparison.rb +44 -0
  30. data/lib/gov_kit/open_states.rb +132 -0
  31. data/lib/gov_kit/railtie.rb +24 -0
  32. data/lib/gov_kit/resource.rb +190 -0
  33. data/lib/gov_kit/search_engines.rb +7 -0
  34. data/lib/gov_kit/search_engines/bing.rb +38 -0
  35. data/lib/gov_kit/search_engines/google_blog.rb +32 -0
  36. data/lib/gov_kit/search_engines/google_news.rb +47 -0
  37. data/lib/gov_kit/search_engines/technorati.rb +35 -0
  38. data/lib/gov_kit/search_engines/wikipedia.rb +27 -0
  39. data/lib/gov_kit/transparency_data.rb +144 -0
  40. data/lib/gov_kit/vote_smart.rb +126 -0
  41. data/lib/govkit.rb +1 -0
  42. data/spec/fixtures/bing/news_search.response +1 -0
  43. data/spec/fixtures/bing/no_results.response +1 -0
  44. data/spec/fixtures/follow_the_money/business-page0.response +28 -0
  45. data/spec/fixtures/follow_the_money/business-page1.response +12 -0
  46. data/spec/fixtures/follow_the_money/contribution.response +12 -0
  47. data/spec/fixtures/follow_the_money/unauthorized.response +8 -0
  48. data/spec/fixtures/open_congress/person.response +8 -0
  49. data/spec/fixtures/open_states/401.response +10 -0
  50. data/spec/fixtures/open_states/404.response +9 -0
  51. data/spec/fixtures/open_states/410.response +6 -0
  52. data/spec/fixtures/open_states/bill.response +240 -0
  53. data/spec/fixtures/open_states/bill_query.response +1990 -0
  54. data/spec/fixtures/open_states/committee_find.response +53 -0
  55. data/spec/fixtures/open_states/committee_query.response +190 -0
  56. data/spec/fixtures/open_states/legislator.response +34 -0
  57. data/spec/fixtures/open_states/legislator_query.response +144 -0
  58. data/spec/fixtures/open_states/state.response +60 -0
  59. data/spec/fixtures/search_engines/google_news.response +8 -0
  60. data/spec/fixtures/transparency_data/contributions.response +18 -0
  61. data/spec/fixtures/transparency_data/entities_search.response +7 -0
  62. data/spec/fixtures/transparency_data/entities_search_limit_0.response +7 -0
  63. data/spec/fixtures/transparency_data/entities_search_limit_1.response +7 -0
  64. data/spec/fixtures/transparency_data/grants_find_all.response +7 -0
  65. data/spec/fixtures/transparency_data/lobbyists_find_all.response +7 -0
  66. data/spec/follow_the_money_spec.rb +61 -0
  67. data/spec/open_congress_spec.rb +108 -0
  68. data/spec/open_states_spec.rb +213 -0
  69. data/spec/search_engines_spec.rb +44 -0
  70. data/spec/spec_helper.rb +36 -0
  71. data/spec/transparency_data_spec.rb +106 -0
  72. metadata +258 -0
@@ -0,0 +1,132 @@
1
+ module GovKit
2
+
3
+ # Parent class for OpenStates resources
4
+ # See http://openstates.sunlightlabs.com/api/
5
+ class OpenStatesResource < Resource
6
+
7
+ # Uses default_params from the HTTParty gem.
8
+ # See HTTParty::ClassMethods:
9
+ # http://rubydoc.info/gems/httparty/0.7.4/HTTParty/ClassMethods#default_params-instance_method
10
+ default_params :output => 'json', :apikey => GovKit::configuration.sunlight_apikey
11
+ base_uri GovKit::configuration.openstates_base_url
12
+
13
+ # Do a GET query, with optional parameters.
14
+ #
15
+ # OpenStates returns a 404 error when a query
16
+ # returns nothing.
17
+ #
18
+ # So, if a query result is a resource not found error,
19
+ # we return an empty set.
20
+ def self.get_uri(uri, options={})
21
+ begin
22
+ response = get(uri, options)
23
+ result = parse(response)
24
+ rescue ResourceNotFound
25
+ return []
26
+ end
27
+ result
28
+ end
29
+
30
+ end
31
+
32
+ # Ruby module for interacting with the Open States Project API
33
+ # See http://openstates.sunlightlabs.com/api/
34
+ # Most +find+ and +search+ methods:
35
+ # * call HTTParty::ClassMethods#get
36
+ # * which returns an HTTParty::Response object
37
+ # * which is passed to GovKit::Resource#parse
38
+ # * which uses the response to populate a Resource
39
+ #
40
+ module OpenStates
41
+ ROLE_MEMBER = "member"
42
+ ROLE_COMMITTEE_MEMBER = "committee member"
43
+ CHAMBER_UPPER = "upper"
44
+ CHAMBER_LOWER = "lower"
45
+
46
+ # The State class represents the state data returned from Open States.
47
+ #
48
+ # For details about fields returned, see the Open States documentation, at
49
+ # http://openstates.sunlightlabs.com/api/metadata/,
50
+ #
51
+ class State < OpenStatesResource
52
+ def self.find_by_abbreviation(abbreviation)
53
+ get_uri("/metadata/#{abbreviation}/")
54
+ end
55
+ end
56
+
57
+ # The Bill class represents the bill data returned from Open States.
58
+ #
59
+ # For details about fields returned, see the Open States documentation, at
60
+ # http://openstates.sunlightlabs.com/api/bills/,
61
+ #
62
+ class Bill < OpenStatesResource
63
+ # http://openstates.sunlightlabs.com/api/v1/bills/ca/20092010/AB 667/
64
+ def self.find(state_abbrev, session, bill_id, chamber = '')
65
+ escaped_bill_id = bill_id.gsub(/ /, '%20')
66
+ escaped_session = session.gsub(/ /, '%20')
67
+
68
+ get_uri("/bills/#{state_abbrev.downcase}/#{escaped_session}/#{chamber.blank? ? '' : chamber + '/'}#{escaped_bill_id}/")
69
+ end
70
+
71
+ def self.search(query, options = {})
72
+ result = get_uri('/bills/', :query => {:q => query}.merge(options))
73
+ return Array(result)
74
+ end
75
+
76
+ def self.latest(updated_since, ops = {})
77
+ response = get('/bills/', :query => {:updated_since => updated_since.to_s}.merge(ops))
78
+ parse(response)
79
+ end
80
+ end
81
+
82
+ # The Legislator class represents the legislator data returned from Open States.
83
+ #
84
+ # For details about fields returned, see the Open States documentation, at
85
+ # http://openstates.sunlightlabs.com/api/legislators/,
86
+ #
87
+ class Legislator < OpenStatesResource
88
+ def self.find(legislator_id)
89
+ get_uri("/legislators/#{legislator_id}/")
90
+ end
91
+
92
+ def self.search(options = {})
93
+ result = get_uri('/legislators/', :query => options)
94
+ return Array(result)
95
+ end
96
+ end
97
+
98
+ # The Committee class represents the committee data returned from Open States.
99
+ #
100
+ # For details about fields returned, see the Open States documentation, at
101
+ # http://openstates.sunlightlabs.com/api/committees/,
102
+ #
103
+ class Committee < OpenStatesResource
104
+ def self.find(committee_id)
105
+ get_uri("/committees/#{committee_id}/")
106
+ end
107
+
108
+ def self.search(options = {})
109
+ get_uri('/committees/', :query => options)
110
+ end
111
+ end
112
+
113
+ class Role < OpenStatesResource; end
114
+
115
+ class Sponsor < OpenStatesResource; end
116
+
117
+ class Version < OpenStatesResource; end
118
+
119
+ class Source < OpenStatesResource; end
120
+
121
+ class Address < OpenStatesResource; end
122
+
123
+ class Action < OpenStatesResource; end
124
+
125
+ class Vote < OpenStatesResource
126
+ def self.find(vote_id)
127
+ response = get("/votes/#{vote_id}/")
128
+ parse(response)
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,24 @@
1
+ require 'govkit'
2
+
3
+ module GovKit
4
+ if defined? Rails::Railtie
5
+ require 'rails'
6
+ class Railtie < Rails::Railtie
7
+ initializer 'govkit.insert_into_active_record' do
8
+ ActiveSupport.on_load :active_record do
9
+ GovKit::Railtie.insert
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ # This class exists in order to run its insert method while
16
+ # Rails is loading.
17
+ # This then adds GovKit::ActsAsNoteworthy to ActiveRecord::Base.
18
+ # See http://api.rubyonrails.org/classes/Rails/Railtie.html
19
+ class Railtie
20
+ def self.insert
21
+ ActiveRecord::Base.send(:include, GovKit::ActsAsNoteworthy)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,190 @@
1
+ module GovKit
2
+
3
+
4
+ # This is the parent class for the classes that wrap
5
+ # the data returned to govkit.
6
+ #
7
+ # Subclasses are responsible for fetching the data from
8
+ # different web services; Resource will then parse the returned data,
9
+ # converting returned fields to instance methods.
10
+ #
11
+ # Initialize a Resource with a hash of attributes, or an array of hashes.
12
+ # For each attribute, add a getter and setter to this instance.
13
+ # @example
14
+ # res = Resource.new { "aaa" => "111", "bbb" => "222", "ccc" => "333" }
15
+ # res.aaa == "111"
16
+ # res.bbb == "222"
17
+ # res.ccc == "333"
18
+ #
19
+ # Includes {http://rdoc.info/github/jnunemaker/httparty/master/HTTParty/ClassMethods HTTParty}, which provides convenience methods like get().
20
+ class Resource
21
+ include HTTParty
22
+ format :json
23
+
24
+ # The attributes data returned by the service.
25
+ attr_reader :attributes
26
+
27
+ # The response returned by the service.
28
+ attr_reader :raw_response
29
+
30
+ def initialize(attributes = {})
31
+ @attributes = {}
32
+ @raw_response = attributes
33
+
34
+ unload(attributes)
35
+ end
36
+
37
+ # @return [Hash] the response object, potentially useful for comparison on sync
38
+ #
39
+ def to_md5
40
+ Digest::MD5.hexdigest(@raw_response.body)
41
+ end
42
+
43
+ # Handles the basic responses we might get back from a web service.
44
+ #
45
+ # On failure, throws an error.
46
+ #
47
+ # If a service returns something other than a 404 when an object is not found,
48
+ # you'll need to handle that in the subclass.
49
+ #
50
+ # @param [Object] response the object.
51
+ # @return [Resource] a new Resource created from the response.
52
+ #
53
+ def self.parse(response)
54
+
55
+ if response.class == HTTParty::Response
56
+ case response.response
57
+ when Net::HTTPNotFound
58
+ raise ResourceNotFound, "404 Not Found"
59
+ when Net::HTTPGone
60
+ raise ResourceNotFound, "404 Not Found"
61
+ when Net::HTTPUnauthorized
62
+ raise NotAuthorized, "401 Not Authorized; have you set up your API key?"
63
+ when Net::HTTPServerError
64
+ raise ServerError, '5xx server error'
65
+ when Net::HTTPClientError
66
+ raise ClientError, '4xx client error'
67
+ end
68
+ end
69
+
70
+ return [] unless !response.blank?
71
+
72
+ instantiate(response)
73
+ end
74
+
75
+ # Instantiate new GovKit::Resources.
76
+ #
77
+ # @param [Hash] record a hash of values returned by a service, or an array of hashes.
78
+ # @return [Resource]
79
+ #
80
+ # If +record+ is a hash, return a single GovKit::Resource.
81
+ # If it is an array, return an array of GovKit::Resources.
82
+ #
83
+ def self.instantiate(record)
84
+ if record.is_a?(Array)
85
+ instantiate_collection(record)
86
+ else
87
+ new(record)
88
+ end
89
+ end
90
+
91
+ # Instantiate a set of records.
92
+ #
93
+ # @return [Array] Array of records
94
+ # @param [Array] collection An array of records
95
+ def self.instantiate_collection(collection)
96
+ collection.collect! { |record| new(record) }
97
+ end
98
+
99
+ # Given a hash of attributes, assign it to the @attributes member.
100
+ # Then for each attribute, create or set a pair of member accessors with the name
101
+ # of the attribute's key.
102
+ #
103
+ # If the value of the attribute is itself an array or a hash,
104
+ # then create a new class with the (singularized) key as a name, and with a parent class of Resource,
105
+ # and initialize it with the hash.
106
+ #
107
+ # @param [Hash] attributes the attributes returned by the web service.
108
+ #
109
+ def unload(attributes)
110
+ raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
111
+
112
+ attributes.each do |key, value|
113
+ @attributes[key.to_s] =
114
+ case value
115
+ when Array
116
+ resource = resource_for_collection(key)
117
+ value.map do |attrs|
118
+ if attrs.is_a?(String) || attrs.is_a?(Numeric)
119
+ attrs.duplicable? ? attrs.dup : attrs
120
+ else
121
+ resource.new(attrs)
122
+ end
123
+ end
124
+ when Hash
125
+ resource = find_or_create_resource_for(key)
126
+ resource.new(value)
127
+ else
128
+ value.dup rescue value
129
+ end
130
+ end
131
+ self
132
+ end
133
+
134
+ private
135
+
136
+ # Finds a member of the GovKit module with the given name.
137
+ # If the resource doesn't exist, creates it.
138
+ #
139
+ def resource_for_collection(name)
140
+ find_or_create_resource_for(name.to_s.singularize)
141
+ end
142
+
143
+ # Searches each module in +ancestors+ for members named +resource_name+
144
+ # Returns the named resource
145
+ # Throws a NameError if none of the resources in the list contains +resource_name+
146
+ #
147
+ def find_resource_in_modules(resource_name, ancestors)
148
+ if namespace = ancestors.detect { |a| a.constants.include?(resource_name.to_sym) }
149
+ return namespace.const_get(resource_name)
150
+ else
151
+ raise NameError, "Namespace for #{namespace} not found"
152
+ end
153
+ end
154
+
155
+ # Searches the GovKit module for a resource with the name +name+, cleaned and camelized
156
+ # Returns that resource.
157
+ # If the resource isn't found, it's created.
158
+ #
159
+ def find_or_create_resource_for(name)
160
+ resource_name = name.to_s.gsub(/^[_\-+]/,'').gsub(/^(\-?\d)/, "n#{$1}").gsub(/(\s|-)/, '').camelize
161
+ if self.class.parents.size > 1
162
+ find_resource_in_modules(resource_name, self.class.parents)
163
+ else
164
+ self.class.const_get(resource_name)
165
+ end
166
+ rescue NameError
167
+ if self.class.const_defined?(resource_name)
168
+ resource = self.class.const_get(resource_name)
169
+ else
170
+ resource = self.class.const_set(resource_name, Class.new(GovKit::Resource))
171
+ end
172
+ resource
173
+ end
174
+
175
+ def method_missing(method_symbol, * arguments) #:nodoc:
176
+ method_name = method_symbol.to_s
177
+
178
+ case method_name.last
179
+ when "="
180
+ attributes[method_name.first(-1)] = arguments.first
181
+ when "?"
182
+ !attributes[method_name.first(-1)].blank?
183
+ when "]"
184
+ attributes[arguments.first.to_s]
185
+ else
186
+ attributes.has_key?(method_name) ? attributes[method_name] : super
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,7 @@
1
+ module GovKit::SearchEngines
2
+ autoload :GoogleNews, 'gov_kit/search_engines/google_news'
3
+ autoload :GoogleBlog, 'gov_kit/search_engines/google_blog'
4
+ autoload :Technorati, 'gov_kit/search_engines/technorati'
5
+ autoload :Bing, 'gov_kit/search_engines/bing'
6
+ autoload :Wikipedia, 'gov_kit/search_engines/wikipedia'
7
+ end
@@ -0,0 +1,38 @@
1
+ module GovKit
2
+ module SearchEngines
3
+ class Bing
4
+ def self.search(query=[], options={})
5
+ host = GovKit::configuration.bing_base_url
6
+ query = [query, options[:geo]].compact.join('+')
7
+
8
+ options['Sources'] ||= 'news'
9
+
10
+ path = "/json.aspx?Query=#{URI::encode(query)}&AppId=#{GovKit::configuration.bing_appid}&Sources=#{options['Sources']}"
11
+
12
+ doc = JSON.parse(make_request(host, path))
13
+
14
+ mentions = []
15
+
16
+ if news_items = doc['SearchResponse']['News']
17
+ puts "#{news_items['Results'].size} from Bing"
18
+ news_items['Results'].each do |i|
19
+ mention = GovKit::Mention.new
20
+ mention.title = i['Title']
21
+ mention.search_source = 'Bing'
22
+ mention.date = DateTime.parse(i['Date'])
23
+ mention.excerpt = i['Snippet']
24
+ mention.source = i['Source']
25
+ mention.url = i['Url']
26
+
27
+ mentions << mention
28
+ end
29
+ end
30
+ mentions
31
+ end
32
+
33
+ def self.make_request(host, path)
34
+ Net::HTTP.get(host, path)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,32 @@
1
+ module GovKit
2
+ module SearchEngines
3
+ class GoogleBlog
4
+ def self.search(query=[], options = {})
5
+ query = [query, options[:geo]].compact.join('+')
6
+ host = GovKit::configuration.google_blog_base_url
7
+ path = "/blogsearch_feeds?q=#{URI::encode(query)}&hl=en&output=rss&num=50"
8
+
9
+ doc = Nokogiri::XML(make_request(host, path))
10
+
11
+ mentions = []
12
+
13
+ doc.xpath('//item').each do |i|
14
+ mention = GovKit::Mention.new
15
+ mention.title = i.xpath('title').inner_text
16
+ mention.search_source = 'Google Blogs'
17
+ mention.date = i.xpath('dc:date').inner_text
18
+ mention.excerpt = i.xpath('description').inner_text
19
+ mention.source = i.xpath('dc:publisher').inner_text
20
+ mention.url = i.xpath('link').inner_text
21
+
22
+ mentions << mention
23
+ end
24
+ mentions
25
+ end
26
+
27
+ def self.make_request(host, path)
28
+ response = Net::HTTP.get(host, path)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,47 @@
1
+ module GovKit
2
+ module SearchEngines
3
+
4
+ # Class to wrap access to Google News.
5
+ class GoogleNews
6
+
7
+ # Fetches stories about a topic from google news.
8
+ # Returns an array of GovKit::Mention objects.
9
+ #
10
+ # query: The query wanted For example:
11
+ # mentions = GoogleNews.search("Nancy Pelosi")
12
+ #
13
+ # options: Any additional parameters to the search. eg.:
14
+ # :geo => 'Texas' will add &geo=Texas to the URL.
15
+ # :num => 100 will show 100 results per page.
16
+ #
17
+ def self.search(query=[], options={})
18
+ query = Array(query).join('+')
19
+ host = GovKit::configuration.google_news_base_url
20
+ options[:num] ||= 50
21
+
22
+ path = "/news?q=#{URI::encode(query)}&output=rss" + '&' + options.map { |k, v| URI::encode(k.to_s) + '=' + URI::encode(v.to_s) }.join('&')
23
+
24
+ doc = Nokogiri::XML(make_request(host, path))
25
+
26
+ mentions = []
27
+
28
+ doc.xpath('//item').each do |i|
29
+ mention = GovKit::Mention.new
30
+ mention.title = i.xpath('title').inner_text.split(" - ").first
31
+ mention.date = i.xpath('pubDate').inner_text
32
+ mention.excerpt = i.xpath('description').inner_text
33
+ mention.source = i.xpath('title').inner_text.split(" - ").last
34
+ mention.url = i.xpath('link').inner_text
35
+
36
+ mentions << mention
37
+ end
38
+
39
+ mentions
40
+ end
41
+
42
+ def self.make_request(host, path)
43
+ response = Net::HTTP.get(host, path)
44
+ end
45
+ end
46
+ end
47
+ end