cycle_hire 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # Cycle Hire
2
+
3
+ Cycle Hire is a Ruby API for the Barclays Cycle Hire scheme in London. It primarily works by scraping the TfL website.
4
+
5
+ It provides access to the following data:
6
+
7
+ * User journey history
8
+ * Docking station status
9
+
10
+ ## Requirements
11
+
12
+ Ruby 1.9 and Bundler. (It'll probably work on Ruby 1.8 but is untested)
13
+
14
+ Bundler installs:
15
+
16
+ * Nokogiri
17
+ * HTTParty
18
+
19
+ ## Installation
20
+
21
+ gem install cycle-hire
22
+
23
+ Or, add this to your Gemfile:
24
+
25
+ gem 'cycle-hire'
26
+
27
+ ## Usage (binaries)
28
+
29
+ ### Fetch journey history
30
+
31
+ ./bin/cycle_hire_history <email> <password>
32
+
33
+ Where the access credentials are your login to the cycle hire section of the TfL website. Outputs the data in YAML format.
34
+
35
+ ### Fetch docking station status
36
+
37
+ ./bin/cycle_hire_status <station>
38
+
39
+ Where station is a regex of stations you want to match. Outputs the data in YAML format.
40
+
41
+ ## Usage (in your code)
42
+
43
+ ### Fetch journey history
44
+
45
+ session = CycleHire.authenticate email, password
46
+ # raises CycleHire::Session::AuthenticationError if
47
+ # the email or password and incorrect
48
+
49
+ journeys = session.journeys
50
+ # array of the users journeys
51
+
52
+ ### Fetch docking station status
53
+
54
+ stations = CycleHire.stations
55
+ # array of stations, see CycleHire::Station
56
+ # for more details
57
+
58
+ ## Todo
59
+
60
+ * Tests
61
+ * Access to more user account data (e.g. balance, membership expiry)
62
+
63
+ ## Contributing
64
+
65
+ * Fork the project.
66
+ * Make your feature addition or bug fix.
67
+ * Send me a pull request. Bonus points for topic branches.
68
+ * Add tests for what you are building, and make sure you don't break any existing tests.
69
+
70
+ ## License
71
+
72
+ <a rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/"><img alt="Creative Commons License" style="border-width:0" src="http://i.creativecommons.org/l/by-sa/3.0/88x31.png" /></a><br /><span xmlns:dct="http://purl.org/dc/terms/" href="http://purl.org/dc/dcmitype/InteractiveResource" property="dct:title" rel="dct:type">cycle-hire</span> by <a xmlns:cc="http://creativecommons.org/ns#" href="https://github.com/lucaspiller/cycle-hire" property="cc:attributionName" rel="cc:attributionURL">Luca Spiller</a> is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/">Creative Commons Attribution-ShareAlike 3.0 Unported License</a>.<br />Based on a work at <a xmlns:dct="http://purl.org/dc/terms/" href="https://github.com/lucaspiller/cycle-hire" rel="dct:source">github.com</a>.<br />Permissions beyond the scope of this license may be available at <a xmlns:cc="http://creativecommons.org/ns#" href="https://github.com/lucaspiller/cycle-hire" rel="cc:morePermissions">https://github.com/lucaspiller/cycle-hire</a>.
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.expand_path(File.join(File.dirname(__FILE__), '../lib'))
4
+
5
+ require 'cycle_hire'
6
+
7
+ unless ARGV.size == 2
8
+ abort "Usage: #{$0} username password"
9
+ end
10
+
11
+ begin
12
+ session = CycleHire.authenticate ARGV[0], ARGV[1]
13
+ session.journeys.each do |journey|
14
+ puts journey.to_h.to_yaml
15
+ end
16
+ rescue CycleHire::Session::AuthenticationError
17
+ abort "Invalid username and password"
18
+ end
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.expand_path(File.join(File.dirname(__FILE__), '../lib'))
4
+
5
+ require 'cycle_hire'
6
+ require 'yaml'
7
+
8
+ unless ARGV.size == 1
9
+ abort "Usage: #{$0} station_regex"
10
+ end
11
+
12
+ stations = CycleHire.stations
13
+ stations.each do |station|
14
+ if station.name =~ /#{ARGV[0]}/i
15
+ puts station.to_h.to_yaml
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ class CycleHire::Journey
2
+ attr_reader :start_time, :start_station, :end_time, :end_station, :cost
3
+
4
+ def initialize(start_time, start_station, end_time, end_station, cost)
5
+ @start_time = start_time
6
+ @start_station = start_station
7
+ @end_time = end_time
8
+ @end_station = end_station
9
+ @cost = cost
10
+ end
11
+
12
+ def duration
13
+ ((@end_time - @start_time) / 60).to_i
14
+ end
15
+
16
+ def to_h
17
+ {
18
+ :start_time => @start_time,
19
+ :start_station => @start_station.to_s,
20
+ :end_time => @end_time,
21
+ :end_station => @end_station.to_s,
22
+ :cost => @cost,
23
+ :duration => duration
24
+ }
25
+ end
26
+ end
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require 'nokogiri'
4
+
5
+ class CycleHire::JourneyParser
6
+ def initialize(data)
7
+ @data = data
8
+ end
9
+
10
+ def parse
11
+ rows = rows_from_data(@data)
12
+ rows.collect do |row|
13
+ columns = column_data_for_row(row)
14
+
15
+ # ignore the header and other column types
16
+ if columns[4] == 'Hire'
17
+ start_time = parse_time(columns[0])
18
+ end_time = parse_time(columns[1])
19
+ cost = parse_cost(columns[5])
20
+ CycleHire::Journey.new(start_time, columns[2], end_time, columns[3], cost)
21
+ else
22
+ nil
23
+ end
24
+ end.compact
25
+ end
26
+
27
+ protected
28
+
29
+ def rows_from_data(data)
30
+ doc = Nokogiri::HTML(data)
31
+ rows = doc.xpath("//table[@summary='My Account - Activity log']/tbody/tr")
32
+ end
33
+
34
+ def column_data_for_row(row)
35
+ row.xpath('td').collect do |column|
36
+ column.inner_text.strip
37
+ end
38
+ end
39
+
40
+ # the data in the tables is seperated by a br, which the inner_text
41
+ # method strips, as such dates come in as "11 Oct 201109:15"
42
+ def parse_time(string)
43
+ if string =~ /(.*)([0-9]{2}:[0-9]{2})/
44
+ Time.parse($1 + " " + $2 + " London")
45
+ end
46
+ end
47
+
48
+ def parse_cost(string)
49
+ if string =~ /£([0-9]+\.[0-9]{2})/
50
+ $1.to_f
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,64 @@
1
+ require 'httparty'
2
+ require 'nokogiri'
3
+
4
+ class CycleHire::Session
5
+ include HTTParty
6
+
7
+ base_uri 'https://web.barclayscyclehire.tfl.gov.uk/'
8
+
9
+ def initialize(email, password)
10
+ @email = email
11
+ @password = password
12
+ end
13
+
14
+ def authenticate!
15
+ # initial request to get csrf
16
+ csrf_response = make_request(:get, '/')
17
+ doc = Nokogiri::HTML(csrf_response.body)
18
+ csrf = doc.xpath("//input[@id='login__csrf_token']").first.attribute('value').value
19
+
20
+ # authentication request
21
+ options = {
22
+ :query => {
23
+ 'login[Email]' => @email,
24
+ 'login[Password]' => @password,
25
+ 'login[_csrf_token]' => csrf
26
+ },
27
+ :headers => {
28
+ 'Content-Type' => 'application/x-www-form-urlencoded',
29
+ }
30
+ }
31
+ authentication_response = make_request(:post, '/', options)
32
+ raise AuthenticationError unless authentication_response.body =~ /Account Summary/
33
+ self
34
+ end
35
+
36
+ def journeys
37
+ response = make_request(:get, '/account/activity')
38
+ parser = CycleHire::JourneyParser.new(response.body)
39
+ parser.parse
40
+ end
41
+
42
+ class AuthenticationError < Exception
43
+ end
44
+
45
+ protected
46
+
47
+ def make_request(method, path, options = {})
48
+ options[:headers] ||= {}
49
+ options[:headers]['Cookie'] = @cookies if @cookies
50
+ response = self.class.send(method, path, options)
51
+ set_cookies_from_response(response)
52
+ response
53
+ end
54
+
55
+ def set_cookies_from_response(httparty_response)
56
+ # TODO there must be a better way to do this
57
+ headers = httparty_response.response.to_hash
58
+ if headers['set-cookie']
59
+ @cookies = headers['set-cookie'].map do |cookie|
60
+ cookie.split(';').first
61
+ end.join('; ')
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,45 @@
1
+ class CycleHire::Station
2
+ attr_reader :name, :area, :id, :latitude, :longitude, :bikes, :empty_docks, :installed, :locked, :temporary, :timestamp
3
+
4
+ def initialize(name)
5
+ parse_name!(name)
6
+ end
7
+
8
+ def initialize(name, id, latitude, longitude, bikes, empty_docks, installed, locked, temporary, timestamp)
9
+ parse_name!(name)
10
+ @id = id
11
+ @latitude = latitude
12
+ @longitude = longitude
13
+
14
+ @bikes = bikes
15
+ @empty_docks = empty_docks
16
+
17
+ @installed = installed
18
+ @locked = locked
19
+ @temporary = temporary
20
+ @timestamp = timestamp
21
+ end
22
+
23
+ def to_h
24
+ {
25
+ :id => @id,
26
+ :name => @name,
27
+ :area => @area,
28
+ :latitude => @latitude,
29
+ :longitude => @longitude,
30
+ :bikes => @bikes,
31
+ :empty_docks => @empty_docks,
32
+ :installed => @installed,
33
+ :locked => @locked,
34
+ :temporary => @temporary,
35
+ :timestamp => @timestamp
36
+ }
37
+ end
38
+
39
+ protected
40
+
41
+ def parse_name!(name)
42
+ @name = (name.split(',').first || '').strip
43
+ @area = (name.split(',').last || '').strip
44
+ end
45
+ end
@@ -0,0 +1,48 @@
1
+ require 'open-uri'
2
+
3
+ class CycleHire::StationParser
4
+ STATION_REGEX = /\{id:"(\d+)".+?name:"(.+?)".+?lat:"(.+?)".+?long:"(.+?)".+?nbBikes:"(\d+)".+?nbEmptyDocks:"(\d+)".+?installed:"(.+?)".+?locked:"(.+?)".+?temporary:"(.+?)"\}/
5
+ TIME_REGEX = /var hour='(\d\d:\d\d)'/
6
+ ENDPOINT = "https://web.barclayscyclehire.tfl.gov.uk/maps"
7
+
8
+ def self.get_stations
9
+ data = open(ENDPOINT).read
10
+ parser = self.new data
11
+ parser.parse
12
+ end
13
+
14
+ def initialize(data)
15
+ @data = data
16
+ end
17
+
18
+ def parse
19
+ @data.scan(STATION_REGEX).map do |station|
20
+ parse_station(station)
21
+ end
22
+ end
23
+
24
+ def timestamp
25
+ @timestamp ||= Time.parse(TIME_REGEX.match(@data).to_s)
26
+ end
27
+
28
+ protected
29
+
30
+ def parse_station(st)
31
+ installed = (st[6] == 'true')
32
+ locked = (st[7] == 'true')
33
+ temporary = (st[8] == 'true')
34
+
35
+ CycleHire::Station.new(
36
+ st[1],
37
+ st[0],
38
+ st[2].to_f,
39
+ st[3].to_f,
40
+ st[4].to_i,
41
+ st[5].to_i,
42
+ installed,
43
+ locked,
44
+ temporary,
45
+ timestamp
46
+ )
47
+ end
48
+ end
data/lib/cycle_hire.rb ADDED
@@ -0,0 +1,20 @@
1
+ module CycleHire
2
+ ROOT = File.expand_path(File.dirname(__FILE__))
3
+ VERSION = '1.0.0'
4
+
5
+ autoload :Session, "#{ROOT}/cycle_hire/session"
6
+ autoload :JourneyParser, "#{ROOT}/cycle_hire/journey_parser"
7
+ autoload :Journey, "#{ROOT}/cycle_hire/journey"
8
+ autoload :StationParser, "#{ROOT}/cycle_hire/station_parser"
9
+ autoload :Station, "#{ROOT}/cycle_hire/station"
10
+
11
+ # raises CycleHire::Session::AuthenticationError
12
+ def self.authenticate(email, password)
13
+ session = Session.new email, password
14
+ session.authenticate!
15
+ end
16
+
17
+ def self.stations
18
+ StationParser.get_stations
19
+ end
20
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cycle_hire
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Luca Spiller
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-11-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: httparty
16
+ requirement: &70304140311480 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0.8'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70304140311480
25
+ - !ruby/object:Gem::Dependency
26
+ name: nokogiri
27
+ requirement: &70304140310540 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '1.5'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70304140310540
36
+ description: cycle_hire gem
37
+ email: luca@stackednotion.com
38
+ executables:
39
+ - cycle_hire_history
40
+ - cycle_hire_status
41
+ extensions: []
42
+ extra_rdoc_files: []
43
+ files:
44
+ - README.md
45
+ - lib/cycle_hire/journey.rb
46
+ - lib/cycle_hire/journey_parser.rb
47
+ - lib/cycle_hire/session.rb
48
+ - lib/cycle_hire/station.rb
49
+ - lib/cycle_hire/station_parser.rb
50
+ - lib/cycle_hire.rb
51
+ - bin/cycle_hire_history
52
+ - bin/cycle_hire_status
53
+ homepage: http://github.com/lucaspiller/cycle-hire
54
+ licenses: []
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubyforge_project:
73
+ rubygems_version: 1.8.10
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: A Ruby API for the Barclays Cycle Hire scheme in London
77
+ test_files: []