twfy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,10 @@
1
+ == 1.0.0 / 2006-11-08
2
+
3
+ * Initial release supporting all services from TWFY API 1.0.0
4
+ * Basic support for 'daisy-chained' service calls via DataElement subclasses, caching results
5
+ * MP (info, debates, comments)
6
+ * Constituency (mp, geometry)
7
+ * Coming up:
8
+ * Easy pagination of debates, comments, etc
9
+ * Data mining MPInfo information (ie expenses, accessible directly from MP instance)
10
+ * Other stuff :-)
data/Manifest.txt ADDED
@@ -0,0 +1,8 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/data_element.rb
6
+ lib/twfy.rb
7
+ test/test_twfy.rb
8
+ test/test_twfy_chain.rb
data/README.txt ADDED
@@ -0,0 +1,105 @@
1
+ twfy
2
+ by Bruce Williams (http://codefluency.com)
3
+ and Martin Owens
4
+
5
+ == DESCRIPTION:
6
+
7
+ Ruby library to interface with the TheyWorkForYou API.
8
+
9
+ From their website:
10
+
11
+ "TheyWorkForYou.com is a non-partisan, volunteer-run website which aims to make it easy
12
+ for people to keep tabs on their elected and unelected representatives in Parliament."
13
+
14
+ == FEATURES/PROBLEMS:
15
+
16
+ All services [currently] provided by the API are supported.
17
+
18
+ The Ruby API closely mirrors that of TWFY, with the exception that the client
19
+ methods are in lowercase and don't include the 'get' prefix.
20
+
21
+ Some examples:
22
+
23
+ getComments:: comments
24
+ getMPs:: mps
25
+ getMPInfo:: mp_info
26
+
27
+ Please submit bug reports to the RubyForge tracker via the project's homepage at
28
+ http://rubyforge.org/projects/twfy
29
+
30
+ == SYNOPSIS:
31
+
32
+ Use is very easy.
33
+
34
+ === Get a Client
35
+
36
+ require 'twfy'
37
+ client = Twfy::Client.new
38
+
39
+ === Call API methods directly on client
40
+
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{|l| l.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
+ === Daisy chaining
59
+
60
+ A few methods on the client return non-OpenStruct instances that support daisy chaining. Using these to access related data more naturally (with caching).
61
+
62
+ Here are some examples
63
+
64
+ # Get the MP for the last constituency (if you sort them alphabetically)
65
+ mp = client.constituencies.sort_by{|c| c.name }.last.mp
66
+ # get the geometry information for that constituency (coming from the MP)
67
+ geometry = mp.constituency.geometry
68
+
69
+ # An overkill example showing caching (no services are called here, since
70
+ # the results have already been cached from above)
71
+ mp = mp.constituency.mp.constituency.geometry.constituency.mp
72
+
73
+ # These return equivalent results (Note how much easier the first is)
74
+ info1 = mp.info # this is cached for subsequent calls
75
+ info2 = client.mp_info(:id=>mp.person_id)
76
+
77
+ # Get pages of debates mentioning 'medicine'
78
+ debates1 = mp.debates(:search=>'medicine')
79
+ debates2 = mp.debates(:search=>'medicine', :page=>2)
80
+
81
+ See http://www.theyworkforyou.com/api/docs for API documentation.
82
+
83
+ Please note that data pulled from the API is licensed separately;
84
+ see the LICENSE portion of this README for further details.
85
+
86
+ == REQUIREMENTS:
87
+
88
+ + json library (available as a gem)
89
+
90
+ == INSTALL:
91
+
92
+ No special instructions.
93
+
94
+ == LICENSE:
95
+
96
+ This library uses the MIT License
97
+ Copyright (c) 2006 Bruce Williams
98
+
99
+ Data is licensed separately:
100
+
101
+ The TheyWorkForYou license statement, from their website (http://www.theyworkforyou.com/api/), is:
102
+
103
+ To use parliamentary material yourself (that's data returned from getDebates, getWrans, and getWMS), you will need to get a Parliamentary Licence from the Office of Public Sector Information. Our own data - lists of MPs, Lords, constituencies and so on - is available under the Creative Commons Attribution-ShareAlike license version 2.5.
104
+
105
+ Non-commercial use is free, please contact us for commercial use.
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ # -*- ruby -*-
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 Owens"]
20
+ end
21
+
22
+ # vim: syntax=Ruby
@@ -0,0 +1,101 @@
1
+ require 'uri'
2
+ require 'date'
3
+
4
+ module Twfy
5
+
6
+ class DataElement
7
+ @@conversions = {}
8
+ class << self
9
+ def convert(*fields,&block)
10
+ fields.each do |field|
11
+ @@conversions[field] = block
12
+ end
13
+ end
14
+ def convert_to_date(*fields)
15
+ fields.each do |field|
16
+ convert field do |d|
17
+ Date.parse(d)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ attr_reader :client
24
+ def initialize(client, data={})
25
+ @client = client
26
+ data.each do |field,value|
27
+ instance_variable_set("@#{field}", convert(field, value))
28
+ unless self.respond_to?(field)
29
+ self.class.send(:define_method, field) do
30
+ instance_variable_get("@#{field}")
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ def convert(field, value)
37
+ if conversion = @@conversions[field.to_sym]
38
+ args = [value]
39
+ args.unshift self if conversion.arity == 2
40
+ conversion.call(*args)
41
+ else
42
+ value
43
+ end
44
+ end
45
+
46
+ end
47
+
48
+ class MP < DataElement
49
+
50
+ convert_to_date :entered_house, :left_house
51
+ convert :image do |value|
52
+ URI.parse("http://theyworkforyou.com#{value}")
53
+ end
54
+ convert :constituency do |source,value|
55
+ Constituency.new(source.client, :name => value, :mp => source)
56
+ end
57
+
58
+ def in_office?
59
+ @left_reason == 'still_in_office'
60
+ end
61
+
62
+ def info
63
+ @info ||= @client.mp_info(:id => @person_id)
64
+ end
65
+
66
+ def debates(params={})
67
+ @debates ||= {}
68
+ @debates[params] ||= @client.debates(params.merge(:person=>@person_id, :type=>'commons'))
69
+ end
70
+
71
+ def comments(params={})
72
+ @comments ||= {}
73
+ @comments[params] ||= @client.comments(params.merge(:pid=>@person_id))
74
+ end
75
+
76
+ end
77
+
78
+ class Constituency < DataElement
79
+
80
+ def initialize(*args)
81
+ super
82
+ end
83
+ def geometry
84
+ @geometry ||= @client.geometry(:name=>@name)
85
+ end
86
+
87
+ def mp
88
+ @mp ||= @client.mp(:constituency=>@name)
89
+ end
90
+
91
+ end
92
+
93
+ class Geometry < DataElement
94
+
95
+ convert :constituency do |source,value|
96
+ Constituency.new(source.client, :name => value, :geometry => source)
97
+ end
98
+
99
+ end
100
+
101
+ end
data/lib/twfy.rb ADDED
@@ -0,0 +1,173 @@
1
+ require 'open-uri'
2
+ require 'json'
3
+ require 'cgi'
4
+ require 'ostruct'
5
+ require 'logger'
6
+
7
+ require File.join(File.dirname(__FILE__), 'data_element')
8
+
9
+ module Twfy
10
+
11
+ VERSION = '1.0.0'
12
+ BASE = URI.parse('http://www.theyworkforyou.com/api/')
13
+
14
+ API_VERSION = '1.0.0'
15
+
16
+ class Client
17
+
18
+ class Error < ::StandardError; end
19
+ class ServiceArgumentError < ::ArgumentError; end
20
+ class APIError < ::StandardError; end
21
+
22
+ def initialize(log_to=$stderr)
23
+ @logger = Logger.new(log_to)
24
+ end
25
+
26
+ def log(message, level=:info)
27
+ @logger.send(level, message) if $DEBUG
28
+ end
29
+
30
+ def convert_url(params={})
31
+ service :convertURL, validate(params, :require => :url) do |value|
32
+ URI.parse(value['url'])
33
+ end
34
+ end
35
+
36
+ def constituency(params={})
37
+ service :getConstituency, validate(params, :require => :postcode), Constituency
38
+ end
39
+
40
+ def constituencies(params={})
41
+ service :getConstituencies, validate(params, :allow => [:date, :search, :longitude, :latitude, :distance]), Constituency
42
+ end
43
+
44
+ def mp(params={})
45
+ service :getMP, validate(params, :allow => [:postcode, :constituency, :id, :always_return]), MP
46
+ end
47
+
48
+ def mp_info(params={})
49
+ service :getMPInfo, validate(params, :require => :id)
50
+ end
51
+
52
+ def mps(params={})
53
+ service :getMPs, validate(params, :allow => [:date, :search]), MP
54
+ end
55
+
56
+ def lord(params={})
57
+ service :getLord, validate(params, :require => :id)
58
+ end
59
+
60
+ def lords(params={})
61
+ service :getLords, validate(params, :allow => [:date, :search])
62
+ end
63
+
64
+ def geometry(params={})
65
+ service :getGeometry, validate(params, :allow => :name), Geometry
66
+ end
67
+
68
+ def committee(params={})
69
+ service :getCommittee, validate(params, :require => :name, :allow => :date)
70
+ end
71
+
72
+ def debates(params={})
73
+ service :getDebates, validate(params, :require => :type, :require_one_of => [:date, :person, :search, :gid], :allow_dependencies => {
74
+ :search => [:order, :page, :num],
75
+ :person => [:order, :page, :num]
76
+ })
77
+ end
78
+
79
+ def wrans(params={})
80
+ service :getWrans, validate(params, :require_one_of => [:date, :person, :search, :gid], :allow_dependencies => {
81
+ :search => [:order, :page, :num],
82
+ :person => [:order, :page, :num]
83
+ } )
84
+ end
85
+
86
+ def wms(params={})
87
+ service :getWMS, validate(params, :require_one_of => [:date, :person, :search, :gid], :allow_dependencies => {
88
+ :search => [:order, :page, :num],
89
+ :person => [:order, :page, :num]
90
+ } )
91
+ end
92
+
93
+ def comments(params={})
94
+ service :getComments, validate(params, :allow => [:date, :search, :user_id, :pid, :page, :num])
95
+ end
96
+
97
+ def service(name, params={}, target=OpenStruct, &block)
98
+ log "Calling service #{name}"
99
+ url = BASE + name.to_s
100
+ url.query = build_query(params)
101
+ result = JSON.parse(url.read)
102
+ log result.inspect
103
+ unless result.kind_of?(Enumerable)
104
+ raise Error, "Could not handle result: #{result.inspect}"
105
+ end
106
+ if result.kind_of?(Hash) && result['error']
107
+ raise APIError, result['error'].to_s
108
+ else
109
+ structure result, block || target
110
+ end
111
+ end
112
+
113
+ #######
114
+ private
115
+ #######
116
+
117
+ def validate(params, against)
118
+ requirements = against[:require].kind_of?(Array) ? against[:require] : [against[:require]].compact
119
+ allowed = against[:allow].kind_of?(Array) ? against[:allow] : [against[:allow]].compact
120
+ require_one = against[:require_one_of].kind_of?(Array) ? against[:require_one_of] : [against[:require_one_of]].compact
121
+ allow_dependencies = against[:allow_dependencies] || {}
122
+ provided_keys = params.keys.map{|k| k.to_sym }
123
+
124
+ # Add allowed dependencies
125
+ allow_dependencies.each do |key,dependent_keys|
126
+ dependent_keys = dependent_keys.kind_of?(Array) ? dependent_keys : [dependent_keys].compact
127
+ allowed += dependent_keys if provided_keys.include?(key)
128
+ end
129
+
130
+ if (missing = requirements.select{|r| !provided_keys.include? r }).any?
131
+ raise ServiceArgumentError, "Missing required params #{missing.inspect}"
132
+ end
133
+
134
+ if require_one.any?
135
+ if (count = provided_keys.inject(0){|count,item| count + (require_one.include?(item) ? 1 : 0) }) < 1
136
+ raise ServiceArgumentError, "Need exactly one of #{require_one.inspect}"
137
+ elsif count > 1
138
+ raise ServiceArgumentError, "Only one of #{require_one.inspect} allowed"
139
+ end
140
+ end
141
+
142
+ unless (extra = provided_keys - (requirements + allowed + require_one)).empty?
143
+ raise ServiceArgumentError, "Unknown params #{extra.inspect}"
144
+ end
145
+
146
+ params
147
+ end
148
+
149
+ def structure(value, target)
150
+ case value
151
+ when Array
152
+ value.map{|r| structure(r, target) }
153
+ when Hash
154
+ if target.kind_of?(Proc)
155
+ target.call(value)
156
+ elsif target == Hash
157
+ value
158
+ else
159
+ target.ancestors.include?(DataElement) ? target.new(self,value) : target.new(value)
160
+ end
161
+ else
162
+ result
163
+ end
164
+ end
165
+
166
+ def build_query(params)
167
+ params.update(:version=>API_VERSION)
168
+ params.map{|set| set.map{|i| CGI.escape(i.to_s)}.join('=') }.join('&')
169
+ end
170
+
171
+ end
172
+
173
+ end
data/test/test_twfy.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'test/unit'
2
+ require File.join(File.dirname(__FILE__), '../lib/twfy')
3
+
4
+ class BasicReturnedDataTest < Test::Unit::TestCase
5
+
6
+ def setup
7
+ @client = Twfy::Client.new
8
+ end
9
+
10
+ def test_convert_url
11
+ uri = @client.convert_url(:url=>'http://www.publications.parliament.uk/pa/cm200506/cmhansrd/cm061018/debtext/61018-0002.htm#06101834000471')
12
+ assert_kind_of URI::HTTP, uri
13
+ assert_equal "www.theyworkforyou.com", uri.host
14
+ end
15
+
16
+ def test_mp_and_mp_info
17
+ mp = @client.mp(:postcode=>'IP6 9PN')
18
+ assert_kind_of Twfy::MP, mp
19
+ assert_kind_of OpenStruct, @client.mp_info(:id=>mp.person_id)
20
+ end
21
+
22
+ def test_mps
23
+ mps = @client.mps
24
+ assert_kind_of Array, mps
25
+ mps.each do |mp|
26
+ assert_kind_of Twfy::MP, mp
27
+ end
28
+ end
29
+
30
+ def test_constituency_and_geometry
31
+ c = @client.constituency(:postcode => 'IP6 9PN')
32
+ assert_kind_of Twfy::Constituency, c
33
+ assert_kind_of Twfy::Geometry, @client.geometry(:name=>c.name)
34
+ end
35
+
36
+ def test_constituencies
37
+ cs = @client.constituencies
38
+ assert_kind_of Array, cs
39
+ cs.each do |c|
40
+ assert_kind_of Twfy::Constituency, c
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,49 @@
1
+ require 'test/unit'
2
+ require File.join(File.dirname(__FILE__), '../lib/twfy')
3
+
4
+ class ChainDataTest < Test::Unit::TestCase
5
+
6
+ def setup
7
+ @client = Twfy::Client.new
8
+ @mp = @client.mp(:postcode=>'IP6 9PN')
9
+ end
10
+
11
+ def test_single_chain_class
12
+ assert_kind_of Twfy::Constituency, @mp.constituency
13
+ c = @mp.instance_eval{ @constituency }
14
+ assert c
15
+ assert_kind_of Twfy::Constituency, c
16
+ end
17
+
18
+ def test_reflexive_chain_class
19
+ c = @mp.constituency
20
+ assert_kind_of Twfy::Constituency, c
21
+ mp = @mp.constituency.mp
22
+ assert_kind_of Twfy::MP, mp
23
+ assert c.instance_eval{ @mp }
24
+ assert_equal mp, c.instance_eval{ @mp }
25
+ end
26
+
27
+ def test_round_trip_chain_class
28
+ c = @mp.constituency
29
+ assert_kind_of Twfy::Constituency, c
30
+ c2 = @mp.constituency.mp.constituency
31
+ assert_kind_of Twfy::Constituency, @mp.instance_eval{ @constituency }
32
+ assert_kind_of Twfy::MP, @mp.constituency.instance_eval{ @mp }
33
+ assert_kind_of Twfy::Constituency, @mp.constituency.mp.instance_eval{ @constituency }
34
+ assert_kind_of Twfy::Constituency, c2
35
+ assert_equal c.name, c2.name
36
+ end
37
+
38
+ def test_overkill_chain_class
39
+ mp = @mp.constituency.mp.constituency.geometry.constituency.mp
40
+ assert_kind_of Twfy::MP, mp
41
+ assert_equal @mp.full_name, mp.full_name
42
+ end
43
+
44
+ def test_simple_chain_and_direct_call_are_equivalent
45
+ assert_equal @client.mp_info(:id=>@mp.person_id), @mp.info
46
+ end
47
+
48
+ end
49
+
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: twfy
5
+ version: !ruby/object:Gem::Version
6
+ version: 1.0.0
7
+ date: 2006-11-20 00:00:00 -07:00
8
+ summary: Ruby library to interface with the TheyWorkForYou(.com) API; an information source on Parliament
9
+ require_paths:
10
+ - lib
11
+ - test
12
+ email: bruce@codefluency.com
13
+ homepage: http://twfy.rubyforge.org
14
+ rubyforge_project: twfy
15
+ description: Ruby library to interface with the TheyWorkForYou API. TheyWorkForYou.com is "a non-partisan, volunteer-run website which aims to make it easy for people to keep tabs on their elected and unelected representatives in Parliament."
16
+ autorequire:
17
+ default_executable:
18
+ bindir: bin
19
+ has_rdoc: true
20
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
21
+ requirements:
22
+ - - ">"
23
+ - !ruby/object:Gem::Version
24
+ version: 0.0.0
25
+ version:
26
+ platform: ruby
27
+ signing_key:
28
+ cert_chain:
29
+ authors:
30
+ - Bruce Williams
31
+ - Martin Owens
32
+ files:
33
+ - History.txt
34
+ - Manifest.txt
35
+ - README.txt
36
+ - Rakefile
37
+ - lib/data_element.rb
38
+ - lib/twfy.rb
39
+ - test/test_twfy.rb
40
+ - test/test_twfy_chain.rb
41
+ test_files: []
42
+
43
+ rdoc_options: []
44
+
45
+ extra_rdoc_files: []
46
+
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ requirements: []
52
+
53
+ dependencies:
54
+ - !ruby/object:Gem::Dependency
55
+ name: json
56
+ version_requirement:
57
+ version_requirements: !ruby/object:Gem::Version::Requirement
58
+ requirements:
59
+ - - ">"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.0.0
62
+ version:
63
+ - !ruby/object:Gem::Dependency
64
+ name: paginator
65
+ version_requirement:
66
+ version_requirements: !ruby/object:Gem::Version::Requirement
67
+ requirements:
68
+ - - ">"
69
+ - !ruby/object:Gem::Version
70
+ version: 0.0.0
71
+ version: