twfy 1.0.1 → 1.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.
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