civic_aide 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +454 -0
- data/Rakefile +7 -0
- data/civic_aide.gemspec +29 -0
- data/lib/civic_aide.rb +34 -0
- data/lib/civic_aide/client.rb +71 -0
- data/lib/civic_aide/elections.rb +22 -0
- data/lib/civic_aide/errors.rb +56 -0
- data/lib/civic_aide/hash.rb +36 -0
- data/lib/civic_aide/representatives.rb +16 -0
- data/lib/civic_aide/string.rb +9 -0
- data/lib/civic_aide/version.rb +3 -0
- data/spec/civic_aide/client_spec.rb +62 -0
- data/spec/civic_aide/elections_spec.rb +130 -0
- data/spec/civic_aide/errors_spec.rb +21 -0
- data/spec/civic_aide/hash_spec.rb +61 -0
- data/spec/civic_aide/representatives_spec.rb +137 -0
- data/spec/civic_aide/string_spec.rb +16 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/vcr/elections/all.yml +63 -0
- data/spec/vcr/elections/single.yml +83 -0
- data/spec/vcr/representatives/single.yml +106 -0
- metadata +178 -0
data/Rakefile
ADDED
data/civic_aide.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'civic_aide/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "civic_aide"
|
8
|
+
spec.version = CivicAide::VERSION
|
9
|
+
spec.authors = ["Tyler Pearson"]
|
10
|
+
spec.email = ["ty.pearson@gmail.com"]
|
11
|
+
spec.summary = %q{A Ruby wrapper for the Google Civic Information API}
|
12
|
+
spec.description = spec.summary
|
13
|
+
spec.homepage = "https://github.com/tylerpearson/civic_aide"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.1.1"
|
23
|
+
spec.add_development_dependency "rspec", "~> 2.14.1"
|
24
|
+
spec.add_development_dependency "vcr", '~> 2.5.0'
|
25
|
+
spec.add_development_dependency "webmock", '1.13.0'
|
26
|
+
|
27
|
+
spec.add_runtime_dependency "httparty"
|
28
|
+
spec.add_runtime_dependency "hashie"
|
29
|
+
end
|
data/lib/civic_aide.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'hashie'
|
3
|
+
require 'civic_aide/version'
|
4
|
+
require 'civic_aide/client'
|
5
|
+
require 'civic_aide/hash'
|
6
|
+
require 'civic_aide/string'
|
7
|
+
require 'civic_aide/elections'
|
8
|
+
require 'civic_aide/representatives'
|
9
|
+
require 'civic_aide/errors'
|
10
|
+
|
11
|
+
module CivicAide
|
12
|
+
class << self
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
def api_key
|
16
|
+
raise APIKeyNotSet if @api_key.nil?
|
17
|
+
@api_key
|
18
|
+
end
|
19
|
+
|
20
|
+
def api_key=(api_key)
|
21
|
+
@api_key = api_key
|
22
|
+
end
|
23
|
+
|
24
|
+
delegate [
|
25
|
+
:elections,
|
26
|
+
:representatives
|
27
|
+
] => :client
|
28
|
+
|
29
|
+
def client
|
30
|
+
@client = CivicAide::Client.new(@api_key)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module CivicAide
|
5
|
+
class Client
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
include HTTParty
|
9
|
+
|
10
|
+
API_ENDPOINT = 'https://www.googleapis.com/civicinfo/'
|
11
|
+
API_VERSION = 'us_v1'
|
12
|
+
|
13
|
+
base_uri "#{API_ENDPOINT}#{API_VERSION}"
|
14
|
+
headers "Content-Type" => "application/json"
|
15
|
+
headers "User-Agent" => "CivicAide Ruby gem v#{CivicAide::VERSION}".freeze
|
16
|
+
|
17
|
+
attr_reader :api_key
|
18
|
+
|
19
|
+
def initialize(api_key=nil)
|
20
|
+
@api_key = api_key
|
21
|
+
@api_key ||= CivicAide.api_key
|
22
|
+
end
|
23
|
+
|
24
|
+
def get(url, query={})
|
25
|
+
response = self.class.get(url, :query => query.merge(self.default_query))
|
26
|
+
format_response(response.body)
|
27
|
+
end
|
28
|
+
|
29
|
+
def post(url, query={}, body={})
|
30
|
+
response = self.class.post(url,
|
31
|
+
:query => query.merge(self.default_query),
|
32
|
+
:body => body.to_json
|
33
|
+
)
|
34
|
+
check_response_status(response['status'])
|
35
|
+
format_response(response.body)
|
36
|
+
end
|
37
|
+
|
38
|
+
def elections(election_id=nil)
|
39
|
+
CivicAide::Elections.new(self, election_id)
|
40
|
+
end
|
41
|
+
|
42
|
+
def representatives
|
43
|
+
CivicAide::Representatives.new(self)
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def default_query
|
49
|
+
{:key => @api_key, :prettyPrint => false}
|
50
|
+
end
|
51
|
+
|
52
|
+
def format_response(body)
|
53
|
+
body = JSON.parse(body)
|
54
|
+
body.change_zip! # to prevent Array#zip clashing when rubifying the keys
|
55
|
+
body.rubyify_keys!
|
56
|
+
Hashie::Mash.new(body)
|
57
|
+
end
|
58
|
+
|
59
|
+
def check_response_status(code)
|
60
|
+
unless code.downcase == "success"
|
61
|
+
error_type = classify_error(code)
|
62
|
+
raise error_type
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def classify_error(code)
|
67
|
+
code.slice(0,1).capitalize + code.slice(1..-1)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module CivicAide
|
2
|
+
class Elections
|
3
|
+
attr_reader :client, :election_id
|
4
|
+
|
5
|
+
def initialize(client, election_id=nil)
|
6
|
+
@client = client
|
7
|
+
@election_id = election_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def all
|
11
|
+
response = client.get('/elections')
|
12
|
+
response.except!(:kind)
|
13
|
+
end
|
14
|
+
|
15
|
+
def at(address)
|
16
|
+
raise ElectionIdMissing, "Missing a required election id" if @election_id.nil?
|
17
|
+
response = client.post("/voterinfo/#{election_id}/lookup", {officialOnly: false}, {:address => address})
|
18
|
+
response.except!(:kind, :status)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
class APIError < StandardError; end
|
2
|
+
|
3
|
+
class APIKeyNotSet < StandardError
|
4
|
+
def initialize(msg = "Missing a required Google API key.")
|
5
|
+
super
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class ElectionIdMissing < StandardError
|
10
|
+
def initialize(msg = "Missing a required election id.")
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class NoStreetSegmentFound < APIError
|
16
|
+
def initialize(msg = "The API currently has no information about what electoral precinct and/or district this address belongs to. It may be that we are still sourcing/processing new data, or that there are no voters who have registered to vote at this address.")
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class AddressUnparseable < APIError
|
22
|
+
def initialize(msg = "The requested address is not formatted correctly or cannot be geocoded (i.e. the Google Maps API does not know anything about this address).")
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class NoAddressParameter < APIError
|
28
|
+
def initialize(msg = "No address was provided.")
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class MultipleStreetSegmentsFound < APIError
|
34
|
+
def initialize(msg = "The API cannot find information for the specified address, but it has information about nearby addresses. The user should contact their election official for more information.")
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class ElectionOver < APIError
|
40
|
+
def initialize(msg = "The requested election is over. API results for the election are no longer available. Make an electionQuery to find an id for an upcoming election.")
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class ElectionUnknown < APIError
|
46
|
+
def initialize(msg = "The requested election id is invalid. Make an electionQuery to find a valid id.")
|
47
|
+
super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class InternalLookupFailure < APIError
|
52
|
+
def initialize(msg = "An unspecified error occurred processing the request.")
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class Hash
|
2
|
+
|
3
|
+
def rubyify_keys!
|
4
|
+
keys.each do |k|
|
5
|
+
val = self[k]
|
6
|
+
# ignore Open Civic Data identifiers
|
7
|
+
unless k[0..3] == "ocd-"
|
8
|
+
delete(k)
|
9
|
+
new_key = k.to_s.underscore
|
10
|
+
self[new_key] = val
|
11
|
+
end
|
12
|
+
val.rubyify_keys! if val.is_a?(Hash)
|
13
|
+
val.each{|p| p.rubyify_keys! if p.is_a?(Hash)} if val.is_a?(Array)
|
14
|
+
end
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def except(*keys)
|
19
|
+
dup.except!(*keys)
|
20
|
+
end
|
21
|
+
|
22
|
+
def except!(*keys)
|
23
|
+
keys.each { |key| delete(key) }
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def change_zip!
|
28
|
+
keys.each do |k|
|
29
|
+
self["zipCode"] = self.delete "zip" if k == "zip"
|
30
|
+
self[k].change_zip! if self[k].is_a? Hash
|
31
|
+
self[k].each{|p| p.change_zip! if p.is_a?(Hash)} if self[k].is_a?(Array)
|
32
|
+
end
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module CivicAide
|
2
|
+
class Representatives
|
3
|
+
attr_reader :client, :include_offices
|
4
|
+
|
5
|
+
def initialize(client)
|
6
|
+
@client = client
|
7
|
+
@include_offices = true
|
8
|
+
end
|
9
|
+
|
10
|
+
def at(address)
|
11
|
+
response = client.post("/representatives/lookup", {includeOffices: @include_offices}, {:address => address})
|
12
|
+
response.except!(:kind, :status)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CivicAide::Client do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@client = CivicAide::Client.new("AIzaSyDWJSisG_4Azd6nVJTU5gdKPiKKTCovupY")
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '.new' do
|
10
|
+
it 'raises an error with no API key' do
|
11
|
+
expect{ CivicAide::Client.new }.to raise_error(APIKeyNotSet)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "doesn't raise an error with class API key set" do
|
15
|
+
CivicAide.api_key = "AIzaSyDWJSisG_4Azd6nVJTU5gdKPiKKTCovupY"
|
16
|
+
expect{ CivicAide::Client.new }.to_not raise_error
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#new" do
|
21
|
+
it "takes one parameter and returns a Client object" do
|
22
|
+
@client.should be_an_instance_of CivicAide::Client
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'configuration' do
|
27
|
+
it 'should have correct API endpoint' do
|
28
|
+
expect(CivicAide::Client::API_ENDPOINT).to eq('https://www.googleapis.com/civicinfo/')
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should have the correct API version' do
|
32
|
+
expect(CivicAide::Client::API_VERSION).to eq('us_v1')
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should include HTTParty' do
|
36
|
+
@client.extend(HTTParty)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should have the correct base_uri' do
|
40
|
+
expect(CivicAide::Client.base_uri).to eq('https://www.googleapis.com/civicinfo/us_v1')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#default_query' do
|
45
|
+
it 'should have the api key' do
|
46
|
+
@client.send(:default_query).should == {:key => @client.api_key, :prettyPrint => false}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#elections" do
|
51
|
+
it "should be the right class" do
|
52
|
+
expect(@client.elections).to be_an_instance_of CivicAide::Elections
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#representatives" do
|
57
|
+
it "should be the right class" do
|
58
|
+
expect(@client.representatives).to be_an_instance_of CivicAide::Representatives
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CivicAide::Elections do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@client = CivicAide::Client.new("AIzaSyDWJSisG_4Azd6nVJTU5gdKPiKKTCovupY")
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "get all elections" do
|
10
|
+
|
11
|
+
let(:info) { @client.elections.all }
|
12
|
+
|
13
|
+
before do
|
14
|
+
VCR.insert_cassette 'elections/all', :record => :new_episodes
|
15
|
+
@election = info.elections.first
|
16
|
+
end
|
17
|
+
|
18
|
+
after do
|
19
|
+
VCR.eject_cassette
|
20
|
+
end
|
21
|
+
|
22
|
+
it "must parse the response from JSON to Hash" do
|
23
|
+
expect(info).to be_a Hash
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should have elections" do
|
27
|
+
expect(info).to respond_to(:elections)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should have multiple elections" do
|
31
|
+
info.elections.should_not be_empty
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should have an id" do
|
35
|
+
expect(@election.id).to eq("2000")
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should have a name" do
|
39
|
+
expect(@election.name).to eq("VIP Test Election")
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should have an election day" do
|
43
|
+
expect(@election.election_day).to eq("2015-06-06")
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "get info for specific election" do
|
49
|
+
|
50
|
+
let(:info) { @client.elections(4015).at('4910 Willet Drive, Annandale, VA 22003') }
|
51
|
+
|
52
|
+
before do
|
53
|
+
VCR.insert_cassette 'elections/single', :record => :new_episodes
|
54
|
+
end
|
55
|
+
|
56
|
+
after do
|
57
|
+
VCR.eject_cassette
|
58
|
+
end
|
59
|
+
|
60
|
+
it "must parse the response from JSON to Hash" do
|
61
|
+
expect(info).to be_a Hash
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should have an election" do
|
65
|
+
expect(info.election).to be_a Hash
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should have an election id" do
|
69
|
+
expect(info.election.id).to eq("4015")
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should have an election name" do
|
73
|
+
expect(info.election.name).to eq("VA State Election")
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should have an election day" do
|
77
|
+
expect(info.election.election_day).to eq("2013-11-05")
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should have normalized input" do
|
81
|
+
expect(info).to respond_to(:normalized_input)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should have normalized input hash" do
|
85
|
+
expect(info.normalized_input).to be_a Hash
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should have normalized input line 1" do
|
89
|
+
expect(info.normalized_input.line1).to eq("4910 willet dr")
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should have normalized input city" do
|
93
|
+
expect(info.normalized_input.city).to eq("annandale")
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should have normalized input state" do
|
97
|
+
expect(info.normalized_input.state).to eq("VA")
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should have normalized input zip" do
|
101
|
+
expect(info.normalized_input.zip_code).to eq("22003")
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should have contests" do
|
105
|
+
expect(info).to respond_to(:contests)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should have contests as array" do
|
109
|
+
expect(info.contests).to be_a Array
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should have candidates" do
|
113
|
+
expect(info.contests[0]).to respond_to(:candidates)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should have candidates array" do
|
117
|
+
expect(info.contests[0].candidates).to be_a Array
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should have sources" do
|
121
|
+
expect(info.contests[0]).to respond_to(:sources)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should have sources array" do
|
125
|
+
expect(info.contests[0].sources).to be_a Array
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|