tfl_api_client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +37 -0
  3. data/.travis.yml +29 -0
  4. data/CHANGELOG.md +9 -0
  5. data/CONTRIBUTING.md +62 -0
  6. data/GETTING_STARTED.md +161 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE +22 -0
  9. data/README.md +62 -0
  10. data/Rakefile +30 -0
  11. data/lib/tfl_api_client/bike_point.rb +105 -0
  12. data/lib/tfl_api_client/client.rb +235 -0
  13. data/lib/tfl_api_client/exceptions.rb +91 -0
  14. data/lib/tfl_api_client/version.rb +28 -0
  15. data/lib/tfl_api_client.rb +33 -0
  16. data/spec/cassettes/bike_point/authorised_client_location.yml +83 -0
  17. data/spec/cassettes/bike_point/authorised_client_locations.yml +8179 -0
  18. data/spec/cassettes/bike_point/authorised_client_locations_within_bounding_box.yml +402 -0
  19. data/spec/cassettes/bike_point/authorised_client_locations_within_locus.yml +106 -0
  20. data/spec/cassettes/bike_point/authorised_client_search.yml +80 -0
  21. data/spec/cassettes/bike_point/unauthorised_client_location.yml +50 -0
  22. data/spec/cassettes/bike_point/unauthorised_client_locations.yml +50 -0
  23. data/spec/cassettes/bike_point/unauthorised_client_locations_within_bounding_box.yml +50 -0
  24. data/spec/cassettes/bike_point/unauthorised_client_locations_within_locus.yml +50 -0
  25. data/spec/cassettes/bike_point/unauthorised_client_search.yml +50 -0
  26. data/spec/integration/bike_point_spec.rb +158 -0
  27. data/spec/spec_helper.rb +114 -0
  28. data/spec/support/coverage.rb +36 -0
  29. data/spec/support/helpers.rb +81 -0
  30. data/spec/support/vcr.rb +43 -0
  31. data/spec/unit/bike_point_spec.rb +87 -0
  32. data/spec/unit/client_spec.rb +199 -0
  33. data/tfl_api_client.gemspec +36 -0
  34. metadata +222 -0
@@ -0,0 +1,235 @@
1
+ #
2
+ # Copyright (c) 2015 Luke Hackett
3
+ #
4
+ # MIT License
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #
25
+
26
+ require 'json'
27
+ require 'logger'
28
+ require 'openssl'
29
+ require 'net/http'
30
+ require 'tfl_api_client/exceptions'
31
+
32
+ module TflApi
33
+ # This is the client class that allows direct access to the subclasses and to
34
+ # the TFL API. The class contains methods that perform GET and POST requests
35
+ # to the API.
36
+ #
37
+ class Client
38
+
39
+ # Parameters that are permitted as options while initializing the client
40
+ VALID_PARAMS = %w( app_id app_key host logger log_level log_location ).freeze
41
+
42
+ # HTTP verbs supported by the Client
43
+ VERB_MAP = {
44
+ get: Net::HTTP::Get
45
+ }
46
+
47
+ # Client accessors
48
+ attr_reader :app_id, :app_key, :host, :logger, :log_level, :log_location
49
+
50
+ # Initialize a Client object with TFL API credentials
51
+ #
52
+ # @param args [Hash] Arguments to connect to TFL API
53
+ #
54
+ # @option args [String] :app_id the application id generated by registering an app with TFL
55
+ # @option args [String] :app_key the application key generated by registering an app with TFL
56
+ # @option args [String] :host the API's host url - defaults to api.tfl.gov.uk
57
+ #
58
+ # @return [TflApi::Client] a client object to the TFL API
59
+ #
60
+ # @raise [ArgumentError] when required options are not provided.
61
+ #
62
+ def initialize(args)
63
+ args.each do |key, value|
64
+ if value && VALID_PARAMS.include?(key.to_s)
65
+ instance_variable_set("@#{key.to_sym}", value)
66
+ end
67
+ end if args.is_a? Hash
68
+
69
+ # Ensure the Application ID and Key is given
70
+ raise ArgumentError, "Application ID (app_id) is required to interact with TFL's APIs" unless app_id
71
+ raise ArgumentError, "Application Key (app_key) is required to interact with TFL's APIs" unless app_key
72
+
73
+ # Set client defaults
74
+ @host ||= 'https://api.tfl.gov.uk'
75
+ @host = URI.parse(@host)
76
+
77
+ # Create a global Net:HTTP instance
78
+ @http = Net::HTTP.new(@host.host, @host.port)
79
+ @http.use_ssl = true
80
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
81
+
82
+ # Logging
83
+ if @logger
84
+ raise ArgumentError, 'logger parameter must be a Logger object' unless @logger.is_a?(Logger)
85
+ raise ArgumentError, 'log_level cannot be set if using custom logger' if @log_level
86
+ raise ArgumentError, 'log_location cannot be set if using custom logger' if @log_location
87
+ else
88
+ @log_level = Logger::INFO unless @log_level
89
+ @log_location = STDOUT unless @log_location
90
+ @logger = Logger.new(@log_location)
91
+ @logger.level = @log_level
92
+ @logger.datetime_format = '%F T%T%z'
93
+ @logger.formatter = proc do |severity, datetime, _progname, msg|
94
+ "[%s] %-6s %s \r\n" % [datetime, severity, msg]
95
+ end
96
+ end
97
+ end
98
+
99
+ # Creates an instance to the BikePoint class by passing a reference to self
100
+ #
101
+ # @return [TflApi::Client::BikePoint] An object to BikePoint subclass
102
+ #
103
+ def bike_point
104
+ TflApi::Client::BikePoint.new(self)
105
+ end
106
+
107
+ # Performs a HTTP GET request to the api, based upon the given URI resource
108
+ # and any additional HTTP query parameters. This method will automatically
109
+ # inject the mandatory application id and application key HTTP query
110
+ # parameters.
111
+ #
112
+ # @return [hash] HTTP response as a hash
113
+ #
114
+ def get(path, query={})
115
+ request_json :get, path, query
116
+ end
117
+
118
+ # Overrides the inspect method to prevent the TFL Application ID and Key
119
+ # credentials being shown when the `inspect` method is called. The method
120
+ # will only print the important variables.
121
+ #
122
+ # @return [String] String representation of the current object
123
+ #
124
+ def inspect
125
+ "#<#{self.class.name}:0x#{(self.__id__ * 2).to_s(16)} " +
126
+ "@host=#{host.to_s}, " +
127
+ "@log_level=#{log_level}, " +
128
+ "@log_location=#{log_location.inspect}>"
129
+ end
130
+
131
+ private
132
+
133
+ # This method requests the given path via the given resource with the additional url
134
+ # params. All successful responses will yield a hash of the response body, whilst
135
+ # all other response types will raise a child of TflApi::Exceptions::ApiException.
136
+ # For example a 404 response would raise a TflApi::Exceptions::NotFound exception.
137
+ #
138
+ # @param method [Symbol] The type of HTTP request to make, e.g. :get
139
+ # @param path [String] the path of the resource (not including the base url) to request
140
+ # @param params [Hash]
141
+ #
142
+ # @return [HTTPResponse] HTTP response object
143
+ #
144
+ # @raise [TflApi::Exceptions::ApiException] when an error has occurred with TFL's API
145
+ #
146
+ def request_json(method, path, params)
147
+ response = request(method, path, params)
148
+
149
+ if response.kind_of? Net::HTTPSuccess
150
+ parse_response_json(response)
151
+ else
152
+ raise_exception(response)
153
+ end
154
+ end
155
+
156
+ # Creates and performs HTTP request by the request medium to the given path
157
+ # with any additional uri parameters. The method will return the HTTP
158
+ # response object upon completion.
159
+ #
160
+ # @param method [Symbol] The type of HTTP request to make, e.g. :get
161
+ # @param path [String] the path of the resource (not including the base url) to request
162
+ # @param params [Hash] Additional url params to be added to the request
163
+ #
164
+ # @return [HTTPResponse] HTTP response object
165
+ #
166
+ def request(method, path, params)
167
+ full_path = format_request_uri(path, params)
168
+ request = VERB_MAP[method.to_sym].new(full_path)
169
+ # TODO - Enable when supporting other HTTP Verbs
170
+ # request.set_form_data(params) unless method == :get
171
+
172
+ @logger.debug "#{method.to_s.upcase} #{path}"
173
+ @http.request(request)
174
+ end
175
+
176
+ # Returns a full, well-formatted HTTP request URI that can be used to directly
177
+ # interact with the TFL API.
178
+ #
179
+ # @param path [String] the path of the resource (not including the base url) to request
180
+ # @param params [Hash] Additional url params to be added to the request
181
+ #
182
+ # @return [String] Full HTTP request URI
183
+ #
184
+ def format_request_uri(path, params)
185
+ params.merge!({app_id: app_id, app_key: app_key})
186
+ params_string = URI.encode_www_form(params)
187
+ URI::HTTPS.build(host: host.host, path: path, query: params_string)
188
+ end
189
+
190
+ # Parses the given response body as JSON, and returns a hash representation of the
191
+ # the response. Failure to successfully parse the response will result in an
192
+ # TflApi::Exceptions::ApiException being raised.
193
+ #
194
+ # @param response [HTTPResponse] the HTTP response object
195
+ #
196
+ # @return [HTTPResponse] HTTP response object
197
+ #
198
+ # @raise [TflApi::Exceptions::ApiException] when trying to parse a malformed JSON response
199
+ #
200
+ def parse_response_json(response)
201
+ begin
202
+ JSON.parse(response.body)
203
+ rescue JSON::ParserError
204
+ raise TflApi::Exceptions::ApiException, logger, "Invalid JSON returned from #{host.host}"
205
+ end
206
+ end
207
+
208
+ # Raises a child of TflApi::Exceptions::ApiException based upon the response code being
209
+ # classified as non-successful, i.e. a non 2xx response code. All non-successful
210
+ # responses will raise an TflApi::Exceptions::ApiException by default. Popular
211
+ # non-successful response codes are mapped to internal exceptions, for example a 404
212
+ # response code would raise TflApi::Exceptions::NotFound.
213
+ #
214
+ # @param response [HTTPResponse] the HTTP response object
215
+ #
216
+ # @raise [TflApi::Exceptions::ApiException] when an error has occurred with TFL's API
217
+ #
218
+ def raise_exception(response)
219
+ case response.code.to_i
220
+ when 401
221
+ raise TflApi::Exceptions::Unauthorized, logger
222
+ when 403
223
+ raise TflApi::Exceptions::Forbidden, logger
224
+ when 404
225
+ raise TflApi::Exceptions::NotFound, logger
226
+ when 500
227
+ raise TflApi::Exceptions::InternalServerError, logger
228
+ when 503
229
+ raise TflApi::Exceptions::ServiceUnavailable, logger
230
+ else
231
+ raise TflApi::Exceptions::ApiException, logger, "non-successful response (#{response.code}) was returned"
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,91 @@
1
+ #
2
+ # Copyright (c) 2015 Luke Hackett
3
+ #
4
+ # MIT License
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #
25
+
26
+ module TflApi
27
+ # This module contains classes that define exceptions for various categories.
28
+ #
29
+ module Exceptions
30
+ # This is the base class for Exceptions that is inherited from
31
+ # RuntimeError.
32
+ #
33
+ class ApiException < RuntimeError
34
+ def initialize(logger, message = '', log_level = Logger::ERROR)
35
+ logger.add(log_level) { "#{self.class}: #{message}" }
36
+ super(message)
37
+ end
38
+ end
39
+
40
+ # This exception class handles cases where invalid credentials are provided
41
+ # to connect to the TFL API.
42
+ #
43
+ class Unauthorized < ApiException
44
+ def initialize(logger, message = '')
45
+ message = 'Access denied. Please ensure you have valid TFL credentials.' if message.nil? || message.empty?
46
+ super(logger, message)
47
+ end
48
+ end
49
+
50
+ # This exception class handles cases where valid credentials are provided
51
+ # to connect to the TFL API, but those credentials do not have the access
52
+ # level to perform the requested task.
53
+ #
54
+ class Forbidden < ApiException
55
+ def initialize(logger, message = '')
56
+ message = 'Access denied. Your credentials do not permit this request.' if message.nil? || message.empty?
57
+ super(logger, message, Logger::FATAL)
58
+ end
59
+ end
60
+
61
+ # This exception class handles cases where a requested resource is not found
62
+ # on the remote TFL API.
63
+ #
64
+ class NotFound < ApiException
65
+ def initialize(logger, message = '')
66
+ message = 'Requested resource was not found on the TFL API.' if message.nil? || message.empty?
67
+ super(logger, message)
68
+ end
69
+ end
70
+
71
+ # This exception class handles cases where the TFL API returns with a
72
+ # 500 Internal Server Error.
73
+ #
74
+ class InternalServerError < ApiException
75
+ def initialize(logger, message = '')
76
+ message = 'TFL API threw an Internal Server Error. Please try again.' if message.nil? || message.empty?
77
+ super(logger, message)
78
+ end
79
+ end
80
+
81
+ # This exception class handles cases where the Jenkins is getting restarted
82
+ # or reloaded where the response code returned is 503
83
+ #
84
+ class ServiceUnavailable < ApiException
85
+ def initialize(logger, message = '')
86
+ message = 'TFL API is currently unavailable. Please try again.' if message.nil? || message.empty?
87
+ super(logger, message)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,28 @@
1
+ #
2
+ # Copyright (c) 2015 Luke Hackett
3
+ #
4
+ # MIT License
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #
25
+
26
+ module TflApi
27
+ VERSION = '0.1.0'
28
+ end
@@ -0,0 +1,33 @@
1
+ #
2
+ # Copyright (c) 2015 Luke Hackett
3
+ #
4
+ # MIT License
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #
25
+
26
+ require 'tfl_api_client/version'
27
+ require 'tfl_api_client/client'
28
+ require 'tfl_api_client/bike_point'
29
+ require 'tfl_api_client/exceptions'
30
+
31
+ module TflApi
32
+ # Your code goes here...
33
+ end
@@ -0,0 +1,83 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: https://api.tfl.gov.uk/BikePoint/BikePoints_10?app_id=TFL_APP_ID&app_key=TFL_APP_KEY
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ Accept-Encoding:
11
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
12
+ Accept:
13
+ - "*/*"
14
+ User-Agent:
15
+ - Ruby
16
+ Host:
17
+ - api.tfl.gov.uk
18
+ response:
19
+ status:
20
+ code: 200
21
+ message: OK
22
+ headers:
23
+ Access-Control-Allow-Headers:
24
+ - Content-Type
25
+ Access-Control-Allow-Methods:
26
+ - GET,POST,PUT,DELETE,OPTIONS
27
+ Access-Control-Allow-Origin:
28
+ - "*"
29
+ Age:
30
+ - '0'
31
+ Api-Entity-Payload:
32
+ - Place
33
+ Cache-Control:
34
+ - public, must-revalidate, max-age=150, s-maxage=300
35
+ Content-Type:
36
+ - application/json; charset=utf-8
37
+ Date:
38
+ - Tue, 11 Aug 2015 16:52:37 GMT
39
+ Server:
40
+ - Microsoft-IIS/8.5
41
+ Via:
42
+ - 1.1 varnish
43
+ X-Aspnet-Version:
44
+ - 4.0.30319
45
+ X-Backend:
46
+ - api
47
+ X-Backend-Url:
48
+ - "/BikePoint/BikePoints_10"
49
+ X-Banning:
50
+ - ''
51
+ X-Cache:
52
+ - MISS
53
+ X-Cacheable:
54
+ - Yes. Cacheable
55
+ X-Hash-Url:
56
+ - "/bikepoint/bikepoints_10"
57
+ X-Ttl:
58
+ - '300.000'
59
+ X-Ttl-Rule:
60
+ - '0'
61
+ X-Varnish:
62
+ - 10.75.2.208
63
+ - '478472446'
64
+ Content-Length:
65
+ - '406'
66
+ Connection:
67
+ - keep-alive
68
+ body:
69
+ encoding: ASCII-8BIT
70
+ string: '{"$type":"Tfl.Api.Presentation.Entities.Place, Tfl.Api.Presentation.Entities","id":"BikePoints_10","url":"https://api-prod5.tfl.gov.uk/Place/BikePoints_10","commonName":"Park
71
+ Street, Bankside","placeType":"BikePoint","additionalProperties":[{"$type":"Tfl.Api.Presentation.Entities.AdditionalProperties,
72
+ Tfl.Api.Presentation.Entities","category":"Description","key":"TerminalName","sourceSystemKey":"BikePoints","value":"001024","modified":"2015-08-11T16:47:52.26"},{"$type":"Tfl.Api.Presentation.Entities.AdditionalProperties,
73
+ Tfl.Api.Presentation.Entities","category":"Description","key":"Installed","sourceSystemKey":"BikePoints","value":"true","modified":"2015-08-11T16:47:52.26"},{"$type":"Tfl.Api.Presentation.Entities.AdditionalProperties,
74
+ Tfl.Api.Presentation.Entities","category":"Description","key":"Locked","sourceSystemKey":"BikePoints","value":"false","modified":"2015-08-11T16:47:52.26"},{"$type":"Tfl.Api.Presentation.Entities.AdditionalProperties,
75
+ Tfl.Api.Presentation.Entities","category":"Description","key":"InstallDate","sourceSystemKey":"BikePoints","value":"1278242460000","modified":"2015-08-11T16:47:52.26"},{"$type":"Tfl.Api.Presentation.Entities.AdditionalProperties,
76
+ Tfl.Api.Presentation.Entities","category":"Description","key":"RemovalDate","sourceSystemKey":"BikePoints","value":"","modified":"2015-08-11T16:47:52.26"},{"$type":"Tfl.Api.Presentation.Entities.AdditionalProperties,
77
+ Tfl.Api.Presentation.Entities","category":"Description","key":"Temporary","sourceSystemKey":"BikePoints","value":"false","modified":"2015-08-11T16:47:52.26"},{"$type":"Tfl.Api.Presentation.Entities.AdditionalProperties,
78
+ Tfl.Api.Presentation.Entities","category":"Description","key":"NbBikes","sourceSystemKey":"BikePoints","value":"11","modified":"2015-08-11T16:47:52.26"},{"$type":"Tfl.Api.Presentation.Entities.AdditionalProperties,
79
+ Tfl.Api.Presentation.Entities","category":"Description","key":"NbEmptyDocks","sourceSystemKey":"BikePoints","value":"7","modified":"2015-08-11T16:47:52.26"},{"$type":"Tfl.Api.Presentation.Entities.AdditionalProperties,
80
+ Tfl.Api.Presentation.Entities","category":"Description","key":"NbDocks","sourceSystemKey":"BikePoints","value":"18","modified":"2015-08-11T16:47:52.26"}],"children":[],"lat":51.505974,"lon":-0.092754}'
81
+ http_version:
82
+ recorded_at: Tue, 11 Aug 2015 16:52:38 GMT
83
+ recorded_with: VCR 2.9.3