twfy 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2e4a7122b6b66c6ef20ed5fd0291dfa1f76599fc
4
+ data.tar.gz: c199d960584b65f80b353e9ba5da4b60a8895abc
5
+ SHA512:
6
+ metadata.gz: c81d7d14a1ce73b2c4f86f501a69afa9299513b1e3d62cd5e82263ed003c0d1c2fff7bfdd1c6313a283a4a0242d6cc37b6ee48fc9a3d5ba18b597d41c4994435
7
+ data.tar.gz: 1d1b0d34b3be41dfaccefd13e3b856d11fb5f34a6eeaee30797f26ebe332dffdcb529a7c540f40dbe8e7f3c557787cc30a600036ba3366cafdd749860548bfcc
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
data/History.txt CHANGED
@@ -1,3 +1,9 @@
1
+ == 1.1.0 / 2013-11-12
2
+
3
+ * Modernize source
4
+ * Support Ruby 1.9, 2.0
5
+ * Requires >= Ruby 1.9
6
+
1
7
  == 1.0.1 / 2008-08-02
2
8
 
3
9
  * In the near future the TWFY API will require an API Key for all calls. In preparation for this, this binding now requires an API Key. They are available at http://www.theyworkforyou.com/api/key
data/LICENSE.txt ADDED
@@ -0,0 +1,35 @@
1
+ Copyright (c) 2006 - 2013 Bruce Williams, Martin Owen & Tom Hipkin
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ Data is licensed separately:
25
+
26
+ The TheyWorkForYou license statement, from their website
27
+ (http://www.theyworkforyou.com/api/), is:
28
+
29
+ To use parliamentary material yourself (that's data returned from getDebates,
30
+ getWrans, and getWMS), you will need to get a Parliamentary Licence from
31
+ the Office of Public Sector Information. Our own data - lists of MPs, Lords,
32
+ constituencies and so on - is available under the Creative Commons
33
+ Attribution-ShareAlike license version 2.5.
34
+
35
+ Non-commercial use is free, please contact us for commercial use.
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # They Work For You
2
+
3
+ A Ruby library to interface with the
4
+ [TheyWorkForYou](http://www.theyworkforyou.com) API.
5
+
6
+ [![Build Status](https://travis-ci.org/bruce/twfy.png?branch=master)](https://travis-ci.org/bruce/twfy)
7
+ [![Code Climate](https://codeclimate.com/github/bruce/twfy.png)](https://codeclimate.com/github/bruce/twfy)
8
+
9
+ ## Features
10
+
11
+ The Ruby API closely mirrors that of TWFY, with the exception that the client
12
+ methods are in lowercase and don't include the `get` prefix.
13
+
14
+ Some examples:
15
+
16
+ * `getComments` -> `comments`
17
+ * `getMPs` -> `mps`
18
+ * `getMPInfo` -> `mp_info`
19
+
20
+ ## Requirements
21
+
22
+ Ruby 1.9+
23
+
24
+ * multi_json
25
+ * paginator
26
+
27
+ ## Examples
28
+
29
+ ### Get a Client
30
+
31
+ ```ruby
32
+ require 'twfy'
33
+ client = Twfy::Client.new("YOUR-API-KEY")
34
+ ```
35
+
36
+ You can get an API key from http://www.theyworkforyou.com/api/key
37
+
38
+ ### Call API methods directly on client
39
+
40
+ ```ruby
41
+ puts client.constituency(postcode: 'IP6 9PN').name
42
+ # => Central Suffolk & North Ipswich
43
+
44
+ mp = client.mp(postcode: 'IP6 9PN')
45
+ puts mp.full_name
46
+ # => Michael Lord
47
+
48
+ # Get a *lot* of info about this MP
49
+ info = client.mp_info(id: mp.person_id)
50
+
51
+ # Get a sorted list of all the parties in the House of Lords
52
+ client.lords.map(&:party).uniq.sort
53
+ # => ["Bishop", "Conservative", "Crossbench", "DUP", "Green", "Labour", "Liberal Democrat", "Other"]
54
+
55
+ # Get number of debates in the House of Commons mentioning 'Iraq'
56
+ number = client.debates(type: 'commons', search: 'Iraq').info['total_results']
57
+ ```
58
+
59
+ ### Daisy Chaining
60
+
61
+ A few methods on the client return non-OpenStruct instances that
62
+ support daisy chaining. Using these to access related data more
63
+ naturally (with caching).
64
+
65
+ Here are some examples
66
+
67
+ ```ruby
68
+ # Get the MP for the last constituency (if you sort them alphabetically)
69
+ mp = client.constituencies.sort_by(&:name).last.mp
70
+ # get the geometry information for that constituency (coming from the MP)
71
+ geometry = mp.constituency.geometry
72
+
73
+ # An overkill example showing caching (no services are called here, since
74
+ # the results have already been cached from above)
75
+ mp = mp.constituency.mp.constituency.geometry.constituency.mp
76
+
77
+ # These return equivalent results (Note how much easier the first is)
78
+ info1 = mp.info # this is cached for subsequent calls
79
+ info2 = client.mp_info(id: mp.person_id)
80
+
81
+ # Get pages of debates mentioning 'medicine'
82
+ debates1 = mp.debates(search: 'medicine')
83
+ debates2 = mp.debates(search: 'medicine', page: 2)
84
+ ```
85
+
86
+ See http://www.theyworkforyou.com/api/docs for API documentation.
87
+
88
+ ## Support
89
+
90
+ Please submit issues to https://github.com/bruce/twfy/issues
91
+
92
+ Pull requests gratefully accepted.
93
+
94
+ ## License
95
+
96
+ See LICENSE.txt.
97
+
98
+ Please note that data pulled from the API is licensed separately from
99
+ this library.
data/Rakefile CHANGED
@@ -1,22 +1,10 @@
1
- # -*- ruby -*-
1
+ require "bundler/gem_tasks"
2
2
 
3
- require 'rubygems'
4
- require 'hoe'
5
- require 'lib/twfy.rb'
6
-
7
- Hoe.new('twfy', Twfy::VERSION) do |p|
8
- p.rubyforge_name = 'twfy'
9
- p.summary = 'Ruby library to interface with the TheyWorkForYou(.com) API; an information source on Parliament'
10
- p.description =<<EOD
11
- Ruby library to interface with the TheyWorkForYou API. TheyWorkForYou.com is
12
- "a non-partisan, volunteer-run website which aims to make it easy for people to keep
13
- tabs on their elected and unelected representatives in Parliament."
14
- EOD
15
- p.url = "http://twfy.rubyforge.org"
16
- p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
17
- p.extra_deps = ['json', 'paginator']
18
- p.email = %q{bruce@codefluency.com}
19
- p.author = ["Bruce Williams", "Martin Owen"]
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ t.verbose = true
20
8
  end
21
9
 
22
- # vim: syntax=Ruby
10
+ task default: :test
data/lib/twfy/api.rb ADDED
@@ -0,0 +1,47 @@
1
+ module Twfy
2
+
3
+ module API
4
+
5
+ VERSION = '1.0.0'
6
+
7
+ VALIDATIONS = {
8
+ convertURL: {require: :url},
9
+ getConstituency: {require: :postcode},
10
+ getConstituencies: {allow: [:date, :search, :longitude, :latitude, :distance]},
11
+ getMP: {allow: [:postcode, :constituency, :id, :always_return]},
12
+ getMPInfo: {require: :id},
13
+ getMPs: {allow: [:date, :party, :search]},
14
+ getLord: {require: :id},
15
+ getLords: {allow: [:date, :party, :search]},
16
+ getMLAs: {allow: [:date, :party, :search]},
17
+ getMSPs: {allow: [:date, :party, :search]},
18
+ getGeometry: {allow: :name},
19
+ getCommittee: {require: :name, allow: :date},
20
+ getDebates: {
21
+ require: :type,
22
+ require_one_of: [:date, :person, :search, :gid],
23
+ allow_dependencies: {
24
+ search: [:order, :page, :num],
25
+ person: [:order, :page, :num]
26
+ }
27
+ },
28
+ getWrans: {
29
+ require_one_of: [:date, :person, :search, :gid],
30
+ allow_dependencies: {
31
+ search: [:order, :page, :num],
32
+ person: [:order, :page, :num]
33
+ }
34
+ },
35
+ getWMS: {
36
+ require_one_of: [:date, :person, :search, :gid],
37
+ allow_dependencies: {
38
+ search: [:order, :page, :num],
39
+ person: [:order, :page, :num]
40
+ }
41
+ },
42
+ getComments: {allow: [:date, :search, :user_id, :pid, :page, :num]}
43
+ }
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,84 @@
1
+ require 'cgi'
2
+ require 'logger'
3
+ require 'multi_json'
4
+ require 'open-uri'
5
+ require 'ostruct'
6
+
7
+ module Twfy
8
+
9
+ class Client
10
+ include API
11
+ include Commands
12
+
13
+ class Error < ::StandardError; end
14
+ class APIError < ::StandardError; end
15
+
16
+ def initialize(api_key, log_to=$stderr)
17
+ @api_key = api_key
18
+ @logger = Logger.new(log_to)
19
+ end
20
+
21
+ def log(message, level=:info)
22
+ @logger.send(level, message) if $DEBUG
23
+ end
24
+
25
+ private
26
+
27
+ def service(name, params = {}, target = OpenStruct, &block)
28
+ log "Calling service #{name}"
29
+ url = service_url(name, params)
30
+ result = MultiJson.load(url.read)
31
+ log result.inspect
32
+ unless result.kind_of?(Enumerable)
33
+ raise Error, "Could not handle result: #{result.inspect}"
34
+ end
35
+ if result.kind_of?(Hash) && result['error']
36
+ raise APIError, result['error'].to_s
37
+ else
38
+ structure result, block || target
39
+ end
40
+ end
41
+
42
+ def service_url(name, params = {})
43
+ url = BASE + name.to_s
44
+ url.query = build_query(validate(params, API::VALIDATIONS[name]))
45
+ url
46
+ end
47
+
48
+ def validate(params, against)
49
+ Validation.run(params, against)
50
+ params
51
+ end
52
+
53
+ def structure(value, target)
54
+ case value
55
+ when Array
56
+ value.map{|r| structure(r, target) }
57
+ when Hash
58
+ if target.kind_of?(Proc)
59
+ target.call(value)
60
+ elsif target == Hash
61
+ value
62
+ else
63
+ target.ancestors.include?(DataElement) ? target.new(self,value) : target.new(value)
64
+ end
65
+ else
66
+ result
67
+ end
68
+ end
69
+
70
+ def build_query(params)
71
+ params.merge(api_params).map { |set|
72
+ set.map { |i|
73
+ CGI.escape(i.to_s)
74
+ }.join('=')
75
+ }.join('&')
76
+ end
77
+
78
+ def api_params
79
+ @api_params ||= {key: @api_key, version: API::VERSION}
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -0,0 +1,73 @@
1
+ module Twfy
2
+ module Commands
3
+
4
+ def convert_url(params = {})
5
+ service :convertURL, params do |value|
6
+ URI.parse(value['url'])
7
+ end
8
+ end
9
+
10
+ def constituency(params = {})
11
+ service :getConstituency, params, Constituency
12
+ end
13
+
14
+ def constituencies(params = {})
15
+ service :getConstituencies, params, Constituency
16
+ end
17
+
18
+ def mp(params = {})
19
+ service :getMP, params, MP
20
+ end
21
+
22
+ def mp_info(params = {})
23
+ service :getMPInfo, params
24
+ end
25
+
26
+ def mps(params = {})
27
+ service :getMPs, params, MP
28
+ end
29
+
30
+ def lord(params = {})
31
+ service :getLord, params
32
+ end
33
+
34
+ def lords(params = {})
35
+ service :getLords, params
36
+ end
37
+
38
+ # Members of Legislative Assembly
39
+ def mlas(params = {})
40
+ service :getMLAs, params
41
+ end
42
+
43
+ # Member of Scottish parliament
44
+ def msps(params = {})
45
+ service :getMSPs, params
46
+ end
47
+
48
+ def geometry(params = {})
49
+ service :getGeometry, params, Geometry
50
+ end
51
+
52
+ def committee(params = {})
53
+ service :getCommittee, params
54
+ end
55
+
56
+ def debates(params = {})
57
+ service :getDebates, params
58
+ end
59
+
60
+ def wrans(params = {})
61
+ service :getWrans, params
62
+ end
63
+
64
+ def wms(params = {})
65
+ service :getWMS, params
66
+ end
67
+
68
+ def comments(params = {})
69
+ service :getComments, params
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,15 @@
1
+ module Twfy
2
+
3
+ class Constituency < DataElement
4
+
5
+ def geometry
6
+ @geometry ||= @client.geometry(name: @name).with(constituency: self)
7
+ end
8
+
9
+ def mp
10
+ @mp ||= @client.mp(constituency: @name).with(constituency: self)
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,62 @@
1
+ require 'uri'
2
+ require 'date'
3
+
4
+ module Twfy
5
+
6
+ class DataElement
7
+
8
+ @@conversions = {}
9
+
10
+ class << self
11
+
12
+ def convert(*fields,&block)
13
+ fields.each do |field|
14
+ @@conversions[field] = block
15
+ end
16
+ end
17
+
18
+ def convert_to_date(*fields)
19
+ fields.each do |field|
20
+ convert field do |d|
21
+ Date.parse(d)
22
+ end
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ attr_reader :client
29
+ def initialize(client, data={})
30
+ @client = client
31
+ update_attributes(data)
32
+ end
33
+
34
+ def update_attributes(data = {})
35
+ data.each do |field,value|
36
+ instance_variable_set("@#{field}", convert(field, value))
37
+ unless self.respond_to?(field)
38
+ self.class.send(:define_method, field) do
39
+ instance_variable_get("@#{field}")
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def with(data = {})
46
+ update_attributes(data)
47
+ self
48
+ end
49
+
50
+ def convert(field, value)
51
+ if conversion = @@conversions[field.to_sym]
52
+ args = [value]
53
+ args.unshift self if conversion.arity == 2
54
+ conversion.call(*args)
55
+ else
56
+ value
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,17 @@
1
+ module Twfy
2
+
3
+ class Geometry < DataElement
4
+
5
+ convert :constituency do |source, value|
6
+ if value.is_a?(Constituency)
7
+ value
8
+ else
9
+ Constituency.new(source.client,
10
+ name: value,
11
+ geometry: source)
12
+ end
13
+ end
14
+
15
+ end
16
+
17
+ end
data/lib/twfy/mp.rb ADDED
@@ -0,0 +1,39 @@
1
+ module Twfy
2
+
3
+ class MP < DataElement
4
+
5
+ convert_to_date :entered_house, :left_house
6
+
7
+ convert :image do |value|
8
+ URI.parse("http://theyworkforyou.com#{value}")
9
+ end
10
+
11
+ convert :constituency do |source, value|
12
+ if value.is_a?(Constituency)
13
+ value
14
+ else
15
+ Constituency.new(source.client, name: value, mp: source)
16
+ end
17
+ end
18
+
19
+ def in_office?
20
+ @left_reason == 'still_in_office'
21
+ end
22
+
23
+ def info
24
+ @info ||= @client.mp_info(id: @person_id)
25
+ end
26
+
27
+ def debates(params={})
28
+ @debates ||= {}
29
+ @debates[params] ||= @client.debates(params.merge(person: @person_id, type: 'commons'))
30
+ end
31
+
32
+ def comments(params={})
33
+ @comments ||= {}
34
+ @comments[params] ||= @client.comments(params.merge(pid: @person_id))
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,88 @@
1
+ module Twfy
2
+
3
+ class Validation
4
+
5
+ class ServiceArgumentError < ::ArgumentError; end
6
+
7
+ def self.run(params = {}, against = {})
8
+ new(params, against).check!
9
+ end
10
+
11
+ def initialize(params = {}, against = {})
12
+ @params = params
13
+ @against = against
14
+ end
15
+
16
+ def check!
17
+ unless missing.empty?
18
+ raise ServiceArgumentError, "Missing required params #{missing.inspect}"
19
+ end
20
+ unless extra.empty?
21
+ raise ServiceArgumentError, "Unknown params #{extra.inspect}"
22
+ end
23
+ check_one_required!
24
+ end
25
+
26
+ private
27
+
28
+ def required
29
+ @required ||= list(@against[:require])
30
+ end
31
+
32
+ def allowed
33
+ @allowed ||= allowed_with_dependencies
34
+ end
35
+
36
+ def one_required
37
+ @one_required ||= list(@against[:require_one_of])
38
+ end
39
+
40
+ def dependencies_allowed
41
+ @dependencies_allowed ||= @against[:allow_dependencies] || {}
42
+ end
43
+
44
+ def allowed_with_dependencies
45
+ base = list(@against[:allow])
46
+ dependencies_allowed.each_with_object(base) do |(key, dependent_keys), memo|
47
+ if provided.include?(key)
48
+ memo.push(*list(dependent_keys))
49
+ end
50
+ end
51
+ end
52
+
53
+ def provided
54
+ @provided ||= @params.keys.map(&:to_sym)
55
+ end
56
+
57
+ def list(candidate)
58
+ Array(candidate).dup.compact
59
+ end
60
+
61
+ def missing
62
+ @missing ||= required - provided
63
+ end
64
+
65
+ def extra
66
+ @extra ||= provided - (required + allowed + one_required)
67
+ end
68
+
69
+ def check_one_required!
70
+ count = one_required_count
71
+ if count
72
+ if count < 1
73
+ raise ServiceArgumentError, "Need exactly one of #{one_required.inspect}"
74
+ elsif count > 1
75
+ raise ServiceArgumentError, "Only one of #{one_required.inspect} allowed"
76
+ end
77
+ end
78
+ end
79
+
80
+ def one_required_count
81
+ if one_required.any?
82
+ provided.select { |item| one_required.include?(item) }.size
83
+ end
84
+ end
85
+
86
+ end
87
+
88
+ end
@@ -0,0 +1,3 @@
1
+ module Twfy
2
+ VERSION = '1.1.0'
3
+ end