tfl_api_client 0.1.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.
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