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 +72 -0
- data/bin/cycle_hire_history +18 -0
- data/bin/cycle_hire_status +17 -0
- data/lib/cycle_hire/journey.rb +26 -0
- data/lib/cycle_hire/journey_parser.rb +53 -0
- data/lib/cycle_hire/session.rb +64 -0
- data/lib/cycle_hire/station.rb +45 -0
- data/lib/cycle_hire/station_parser.rb +48 -0
- data/lib/cycle_hire.rb +20 -0
- metadata +77 -0
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: []
|