twfy 1.0.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.
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: