cycle_hire 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/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: []