tesla 0.0.1 → 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.
- checksums.yaml +7 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +31 -0
- data/CONTRIBUTING.md +45 -0
- data/{LICENSE.txt → LICENSE.md} +1 -1
- data/README.md +366 -0
- data/SECURITY.md +38 -0
- data/lib/tesla/api/alert_endpoints.rb +35 -0
- data/lib/tesla/api/charging_endpoints.rb +115 -0
- data/lib/tesla/api/climate_endpoints.rb +180 -0
- data/lib/tesla/api/closure_endpoints.rb +142 -0
- data/lib/tesla/api/command_endpoints.rb +63 -0
- data/lib/tesla/api/media_endpoints.rb +114 -0
- data/lib/tesla/api/navigation_endpoints.rb +51 -0
- data/lib/tesla/api/oauth_endpoints.rb +118 -0
- data/lib/tesla/api/partner_endpoints.rb +62 -0
- data/lib/tesla/api/security_endpoints.rb +123 -0
- data/lib/tesla/api/software_endpoints.rb +38 -0
- data/lib/tesla/api/user_endpoints.rb +40 -0
- data/lib/tesla/api/vehicle_endpoints.rb +174 -0
- data/lib/tesla/api.rb +39 -0
- data/lib/tesla/authenticator.rb +33 -0
- data/lib/tesla/bearer_token_authenticator.rb +38 -0
- data/lib/tesla/certificate_options.rb +51 -0
- data/lib/tesla/charge_state.rb +111 -0
- data/lib/tesla/charging_site.rb +67 -0
- data/lib/tesla/client.rb +336 -0
- data/lib/tesla/client_credentials.rb +230 -0
- data/lib/tesla/climate_state.rb +116 -0
- data/lib/tesla/configuration.rb +483 -0
- data/lib/tesla/connection.rb +361 -0
- data/lib/tesla/connection_pool.rb +186 -0
- data/lib/tesla/deep_copy.rb +41 -0
- data/lib/tesla/drive_state.rb +80 -0
- data/lib/tesla/errors/bad_gateway.rb +9 -0
- data/lib/tesla/errors/bad_request.rb +9 -0
- data/lib/tesla/errors/client_error.rb +9 -0
- data/lib/tesla/errors/command_failed.rb +42 -0
- data/lib/tesla/errors/device_unexpected_response.rb +12 -0
- data/lib/tesla/errors/error.rb +7 -0
- data/lib/tesla/errors/forbidden.rb +14 -0
- data/lib/tesla/errors/gateway_timeout.rb +9 -0
- data/lib/tesla/errors/http_error.rb +89 -0
- data/lib/tesla/errors/internal_server_error.rb +9 -0
- data/lib/tesla/errors/invalid_response.rb +33 -0
- data/lib/tesla/errors/locked.rb +12 -0
- data/lib/tesla/errors/method_not_allowed.rb +9 -0
- data/lib/tesla/errors/misdirected_request.rb +13 -0
- data/lib/tesla/errors/network_error.rb +30 -0
- data/lib/tesla/errors/not_acceptable.rb +9 -0
- data/lib/tesla/errors/not_found.rb +9 -0
- data/lib/tesla/errors/oauth_error.rb +53 -0
- data/lib/tesla/errors/payment_required.rb +12 -0
- data/lib/tesla/errors/precondition_failed.rb +13 -0
- data/lib/tesla/errors/request_timeout.rb +13 -0
- data/lib/tesla/errors/server_error.rb +9 -0
- data/lib/tesla/errors/service_unavailable.rb +9 -0
- data/lib/tesla/errors/too_many_redirects.rb +9 -0
- data/lib/tesla/errors/too_many_requests.rb +9 -0
- data/lib/tesla/errors/unauthorized.rb +9 -0
- data/lib/tesla/errors/unavailable_for_legal_reasons.rb +12 -0
- data/lib/tesla/errors/unprocessable_content.rb +12 -0
- data/lib/tesla/errors/vehicle_asleep.rb +32 -0
- data/lib/tesla/gui_settings.rb +60 -0
- data/lib/tesla/idempotence.rb +30 -0
- data/lib/tesla/identifiers.rb +27 -0
- data/lib/tesla/json_parsing.rb +38 -0
- data/lib/tesla/nearby_charging_sites.rb +35 -0
- data/lib/tesla/path_escaping.rb +30 -0
- data/lib/tesla/redacted_output.rb +161 -0
- data/lib/tesla/redirect_handler.rb +196 -0
- data/lib/tesla/region.rb +27 -0
- data/lib/tesla/request_builder.rb +138 -0
- data/lib/tesla/resource.rb +403 -0
- data/lib/tesla/response_parser.rb +92 -0
- data/lib/tesla/retry_after.rb +34 -0
- data/lib/tesla/retry_handler.rb +252 -0
- data/lib/tesla/settings.rb +115 -0
- data/lib/tesla/url_validation.rb +131 -0
- data/lib/tesla/user.rb +36 -0
- data/lib/tesla/vehicle.rb +90 -0
- data/lib/tesla/vehicle_config.rb +95 -0
- data/lib/tesla/vehicle_data.rb +67 -0
- data/lib/tesla/vehicle_state.rb +125 -0
- data/lib/tesla/version.rb +6 -0
- data/lib/tesla.rb +139 -0
- data/sig/manifest.yaml +10 -0
- data/sig/tesla.rbs +989 -0
- metadata +134 -118
- data/.document +0 -5
- data/.rvmrc +0 -1
- data/Gemfile +0 -13
- data/Gemfile.lock +0 -20
- data/README.rdoc +0 -19
- data/Rakefile +0 -47
- data/test/helper.rb +0 -19
- data/test/test_tesla.rb +0 -7
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require_relative "errors/network_error"
|
|
5
|
+
require_relative "retry_after"
|
|
6
|
+
require_relative "settings"
|
|
7
|
+
|
|
8
|
+
module Tesla
|
|
9
|
+
# Sends a request again when the server, or the network, turns it away
|
|
10
|
+
#
|
|
11
|
+
# A 429 Too Many Requests says a rate limiter turned the request away before it reached the endpoint, so sending it
|
|
12
|
+
# again is safe for any request, even one that is not idempotent, such as a command: the vehicle never saw the
|
|
13
|
+
# attempt before it.
|
|
14
|
+
#
|
|
15
|
+
# A 502 Bad Gateway, a 503 Service Unavailable, a 504 Gateway Timeout, and a request lost to the network say less.
|
|
16
|
+
# A 502 or 504 comes from a gateway that could not read an answer from the origin behind it, a 503 can come from a
|
|
17
|
+
# service that gave up waiting on a vehicle still working on the request, and a request lost to the network may
|
|
18
|
+
# have arrived and been acted on before its answer went missing. The vehicle may have acted on the request in
|
|
19
|
+
# each case, so a request that asks it to do something, such as honking the horn or opening the trunk, is not sent
|
|
20
|
+
# again for one: a trunk asked to open a second time after it opened closes again on the vehicles that can close
|
|
21
|
+
# it.
|
|
22
|
+
#
|
|
23
|
+
# Whether a request is one of those is the caller's to say, with the `retry_unanswered` argument of {#handle}:
|
|
24
|
+
# {Client} says whether the method of the request is safe, since a command cannot be sent a second time to find
|
|
25
|
+
# out whether the vehicle acted on the first. A 429 is sent again whatever the caller says, since no request
|
|
26
|
+
# reached the endpoint.
|
|
27
|
+
#
|
|
28
|
+
# A request is sent again twice by default, which is enough for the moment of rate limiting or the lost
|
|
29
|
+
# connection that a retry is for, and {#max_retries} of zero turns retrying off, so that a request that was
|
|
30
|
+
# turned away raises {TooManyRequests}, {BadGateway}, {ServiceUnavailable}, {GatewayTimeout}, or {NetworkError}
|
|
31
|
+
# rather than pausing the caller's thread. A response asking to wait longer than {#max_retry_delay} raises
|
|
32
|
+
# whatever {#max_retries} is, so the wait a caller can be held for is theirs to cap.
|
|
33
|
+
#
|
|
34
|
+
# @api private
|
|
35
|
+
class RetryHandler
|
|
36
|
+
include RetryAfter
|
|
37
|
+
include Settings
|
|
38
|
+
|
|
39
|
+
# Default number of times a request is sent again
|
|
40
|
+
DEFAULT_MAX_RETRIES = 2
|
|
41
|
+
# Default longest a request waits before it is sent again, in seconds
|
|
42
|
+
DEFAULT_MAX_RETRY_DELAY = 60 # seconds
|
|
43
|
+
# The status that says a rate limiter turned the request away before it reached the endpoint
|
|
44
|
+
THROTTLED_STATUS = 429
|
|
45
|
+
# The statuses that say a gateway read no answer from the origin, which may have acted on the request
|
|
46
|
+
UNANSWERED_STATUSES = [502, 503, 504].freeze
|
|
47
|
+
private_constant :THROTTLED_STATUS, :UNANSWERED_STATUSES
|
|
48
|
+
|
|
49
|
+
# @!method max_retries
|
|
50
|
+
# The number of times a request is sent again
|
|
51
|
+
# @api private
|
|
52
|
+
# @return [Integer] the number of times a request is sent again
|
|
53
|
+
# @example Get the maximum retries
|
|
54
|
+
# handler.max_retries
|
|
55
|
+
# @!method max_retries=(max_retries)
|
|
56
|
+
# Set the number of times a request is sent again
|
|
57
|
+
# @api private
|
|
58
|
+
# @param max_retries [Integer] the number of times a request is sent again
|
|
59
|
+
# @return [void]
|
|
60
|
+
# @raise [ArgumentError] if it is not a whole number of times, in which case the maximum is left as it was
|
|
61
|
+
# @example Set the maximum retries
|
|
62
|
+
# handler.max_retries = 3
|
|
63
|
+
count_setting :max_retries
|
|
64
|
+
|
|
65
|
+
# @!method max_retry_delay
|
|
66
|
+
# The longest a request waits before it is sent again, in seconds
|
|
67
|
+
# @api private
|
|
68
|
+
# @return [Numeric] the longest a request waits before it is sent again, in seconds
|
|
69
|
+
# @example Get the maximum retry delay
|
|
70
|
+
# handler.max_retry_delay
|
|
71
|
+
# @!method max_retry_delay=(max_retry_delay)
|
|
72
|
+
# Set the longest a request waits before it is sent again, in seconds
|
|
73
|
+
# @api private
|
|
74
|
+
# @param max_retry_delay [Numeric] the longest a request waits before it is sent again, in seconds
|
|
75
|
+
# @return [void]
|
|
76
|
+
# @raise [ArgumentError] if it is not a number of seconds, in which case the maximum is left as it was
|
|
77
|
+
# @example Set the maximum retry delay
|
|
78
|
+
# handler.max_retry_delay = 30
|
|
79
|
+
seconds_setting :max_retry_delay
|
|
80
|
+
|
|
81
|
+
# The source of the randomness the backoff is jittered with
|
|
82
|
+
# @api private
|
|
83
|
+
# @return [Random, Class<Random>] the source of randomness
|
|
84
|
+
# @example Get the source of randomness
|
|
85
|
+
# handler.random
|
|
86
|
+
attr_reader :random
|
|
87
|
+
|
|
88
|
+
# Initialize a new RetryHandler
|
|
89
|
+
#
|
|
90
|
+
# @api private
|
|
91
|
+
# @param max_retries [Integer] the number of times a request is sent again
|
|
92
|
+
# @param max_retry_delay [Numeric] the longest a request waits before it is sent again, in seconds
|
|
93
|
+
# @param random [Random, Class<Random>] the source of the randomness the backoff is jittered with, which
|
|
94
|
+
# answers `rand` with a Float between zero and one
|
|
95
|
+
# @return [RetryHandler] a new instance
|
|
96
|
+
# @example Create a retry handler
|
|
97
|
+
# handler = Tesla::RetryHandler.new(max_retries: 3)
|
|
98
|
+
def initialize(max_retries: DEFAULT_MAX_RETRIES, max_retry_delay: DEFAULT_MAX_RETRY_DELAY, random: Random)
|
|
99
|
+
self.max_retries = max_retries
|
|
100
|
+
self.max_retry_delay = max_retry_delay
|
|
101
|
+
@random = random
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Send a request until the server and the network stop turning it away
|
|
105
|
+
#
|
|
106
|
+
# The block is called again for each retry, so a request that was redirected is followed again from the start.
|
|
107
|
+
# The wait between attempts is the one the `Retry-After` header of the response asks for, and doubles from one
|
|
108
|
+
# second otherwise, up to {#max_retry_delay}, jittered as {#backoff} describes; that is the wait after a
|
|
109
|
+
# {NetworkError} too, since a request that never reached the server has no response to read a wait from. A
|
|
110
|
+
# response that asks to wait longer than {#max_retry_delay} is returned rather than waited for, so that the
|
|
111
|
+
# caller is told what happened instead of pausing for as long as the server likes, and a {NetworkError} that
|
|
112
|
+
# has no retry left is raised as it was.
|
|
113
|
+
#
|
|
114
|
+
# A request a rate limiter turned away is told apart from one the origin may have acted on, since they are not
|
|
115
|
+
# equally safe to send again: a 429 says the request never reached the endpoint, where a 502, 503, or 504, or a
|
|
116
|
+
# request lost to the network, may have been acted on before the answer went missing. The caller says whether
|
|
117
|
+
# its request is safe to send again for those. A 429 is sent again for every request.
|
|
118
|
+
#
|
|
119
|
+
# The request is not named, since the block builds one of its own each time it is called, so that a body read
|
|
120
|
+
# as a stream is sent from the start (see {Client#perform}).
|
|
121
|
+
#
|
|
122
|
+
# @api private
|
|
123
|
+
# @param retry_unanswered [Boolean] whether a request the origin may have acted on is sent again
|
|
124
|
+
# @yield the response, each time the request is sent
|
|
125
|
+
# @return [Net::HTTPResponse] the last response
|
|
126
|
+
# @raise [NetworkError] if the request is lost to the network and is not sent again
|
|
127
|
+
# @example Send a safe request, which is safe to send again whatever happened to it
|
|
128
|
+
# handler.handle(retry_unanswered: true) { connection.perform(request:) }
|
|
129
|
+
# @example Send a request that is safe to send again only when a rate limiter turned it away
|
|
130
|
+
# handler.handle(retry_unanswered: false) { connection.perform(request:) }
|
|
131
|
+
def handle(retry_unanswered:)
|
|
132
|
+
retries = 0
|
|
133
|
+
loop do
|
|
134
|
+
response = attempt(retries:, retry_unanswered:) { yield }
|
|
135
|
+
return response if response
|
|
136
|
+
|
|
137
|
+
retries += 1
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
# Send a request once, waiting afterwards when it is to be sent again
|
|
144
|
+
#
|
|
145
|
+
# @api private
|
|
146
|
+
# @param retries [Integer] the number of times the request has been sent again
|
|
147
|
+
# @param retry_unanswered [Boolean] whether a request the origin may have acted on is sent again
|
|
148
|
+
# @yield the response to the request
|
|
149
|
+
# @return [Net::HTTPResponse, nil] the response, or nil once the wait before sending the request again is over
|
|
150
|
+
# @raise [NetworkError] if the request is lost to the network and is not sent again
|
|
151
|
+
def attempt(retries:, retry_unanswered:)
|
|
152
|
+
response = yield
|
|
153
|
+
delay = retry_delay(response:, retries:, unanswered: retry_unanswered)
|
|
154
|
+
delay ? wait(delay) : response
|
|
155
|
+
rescue NetworkError
|
|
156
|
+
delay = network_retry_delay(retries:, allowed: retry_unanswered)
|
|
157
|
+
raise unless delay
|
|
158
|
+
|
|
159
|
+
wait(delay)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Wait before a request is sent again
|
|
163
|
+
#
|
|
164
|
+
# Kernel#sleep takes any Numeric, but its signature asks for the narrower interface Integer and Float happen to
|
|
165
|
+
# answer, so the seconds are passed to it unchecked rather than narrowing what {#max_retry_delay} accepts.
|
|
166
|
+
#
|
|
167
|
+
# @api private
|
|
168
|
+
# @param delay [Numeric] the seconds to wait
|
|
169
|
+
# @return [nil] nothing, so that the caller sends the request again
|
|
170
|
+
def wait(delay)
|
|
171
|
+
sleep(delay) # steep:ignore UnresolvedOverloading
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# The seconds to wait before sending a request again
|
|
176
|
+
#
|
|
177
|
+
# A wait the response asked for is given up on when it is longer than {#max_retry_delay}, where the wait the
|
|
178
|
+
# handler chose for itself is shortened to it, since a backoff that has grown past the cap is the handler
|
|
179
|
+
# asking to wait longer than the caller allowed rather than the server doing so.
|
|
180
|
+
#
|
|
181
|
+
# @api private
|
|
182
|
+
# @param response [Net::HTTPResponse] the response to the request
|
|
183
|
+
# @param retries [Integer] the number of times the request has been sent again
|
|
184
|
+
# @param unanswered [Boolean] whether a request the origin may have acted on is sent again
|
|
185
|
+
# @return [Numeric, nil] the seconds to wait, or nil when the request is not sent again
|
|
186
|
+
def retry_delay(response:, retries:, unanswered:)
|
|
187
|
+
return unless retryable?(retry?(response, unanswered), retries)
|
|
188
|
+
|
|
189
|
+
retry_after = retry_after_of(response)
|
|
190
|
+
retry_after ? capped(retry_after) : backoff(retries)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# The seconds to wait before sending a request again after a network error
|
|
194
|
+
# @api private
|
|
195
|
+
# @param retries [Integer] the number of times the request has been sent again
|
|
196
|
+
# @param allowed [Boolean] whether a request lost to the network is sent again
|
|
197
|
+
# @return [Numeric, nil] the seconds to wait, or nil when the request is not sent again
|
|
198
|
+
def network_retry_delay(retries:, allowed:)
|
|
199
|
+
return unless retryable?(allowed, retries)
|
|
200
|
+
|
|
201
|
+
backoff(retries)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Whether a request has a retry left and is one the caller allows to be sent again
|
|
205
|
+
# @api private
|
|
206
|
+
# @param allowed [Boolean] whether the request is sent again for what happened to it
|
|
207
|
+
# @param retries [Integer] the number of times the request has been sent again
|
|
208
|
+
# @return [Boolean] whether the request is sent again
|
|
209
|
+
def retryable?(allowed, retries)
|
|
210
|
+
retries < max_retries && allowed
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# The seconds a response asked for, unless longer than the caller allowed
|
|
214
|
+
# @api private
|
|
215
|
+
# @param delay [Integer] the seconds the response asked to wait
|
|
216
|
+
# @return [Integer, nil] the seconds to wait, or nil when they are longer than {#max_retry_delay}
|
|
217
|
+
def capped(delay)
|
|
218
|
+
delay unless delay > max_retry_delay
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Whether a response is one the request is sent again for
|
|
222
|
+
#
|
|
223
|
+
# A 429 says a rate limiter turned the request away before it reached the endpoint, and is sent again whatever
|
|
224
|
+
# the request; a 502, 503, or 504 says the origin may have acted on it, and is sent again only by the caller
|
|
225
|
+
# that says its request can be answered twice.
|
|
226
|
+
#
|
|
227
|
+
# @api private
|
|
228
|
+
# @param response [Net::HTTPResponse] the response to the request
|
|
229
|
+
# @param unanswered [Boolean] whether a request the origin may have acted on is sent again
|
|
230
|
+
# @return [Boolean] whether the request is sent again
|
|
231
|
+
def retry?(response, unanswered)
|
|
232
|
+
status = Integer(response.code)
|
|
233
|
+
status.eql?(THROTTLED_STATUS) || (unanswered && UNANSWERED_STATUSES.include?(status))
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# The seconds to wait when the response does not ask for a wait of its own
|
|
237
|
+
#
|
|
238
|
+
# The wait doubles with each retry up to {#max_retry_delay}, and is then jittered down by up to half of itself,
|
|
239
|
+
# so that the clients a server turned away at the same moment do not all send their requests again at the same
|
|
240
|
+
# instant and turn the retry into a second wave of the load the server was shedding. A wait the response asked
|
|
241
|
+
# for is not jittered: the server named the moment it is ready for the request, and jittering that wait down
|
|
242
|
+
# would send the request again before then.
|
|
243
|
+
#
|
|
244
|
+
# @api private
|
|
245
|
+
# @param retries [Integer] the number of times the request has been sent again
|
|
246
|
+
# @return [Numeric] the seconds to wait, between half of the doubling wait and all of it
|
|
247
|
+
def backoff(retries)
|
|
248
|
+
delay = [2**retries, max_retry_delay].min
|
|
249
|
+
delay - (random.rand * delay / 2) #: Numeric
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tesla
|
|
4
|
+
# Declares the settings that are a number of seconds, a number of times, or a credential the environment can
|
|
5
|
+
# carry, and checks what is assigned to them, mixed into the configuration, the connection, and the retry handler
|
|
6
|
+
#
|
|
7
|
+
# A number is checked where it is assigned, as a host, a proxy URL, and the path of a certificate are (see
|
|
8
|
+
# {URLValidation} and {CertificateOptions}), so that the error names the assignment that was wrong rather than
|
|
9
|
+
# the request that later waits for a negative number of seconds, or compares the times it has been sent again
|
|
10
|
+
# with a String. The value that is rejected is left as it was.
|
|
11
|
+
#
|
|
12
|
+
# The settings are declared rather than written out, as the readers of a {Resource} are, since the reader and the
|
|
13
|
+
# writer of each differ only in the name they carry and the check the writer makes; each is documented where it
|
|
14
|
+
# is declared.
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
module Settings
|
|
18
|
+
# The macros that declare the settings, extended into whatever includes {Settings}
|
|
19
|
+
# @api private
|
|
20
|
+
module Declaration
|
|
21
|
+
# Declare a setting that is a number of seconds
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
# @param name [Symbol] the name of the setting
|
|
25
|
+
# @return [Symbol] the name of the writer
|
|
26
|
+
# @example Declare a timeout
|
|
27
|
+
# seconds_setting :open_timeout
|
|
28
|
+
def seconds_setting(name)
|
|
29
|
+
attr_reader(name)
|
|
30
|
+
|
|
31
|
+
define_method(:"#{name}=") do |seconds|
|
|
32
|
+
# @type self: Settings
|
|
33
|
+
instance_variable_set(:"@#{name}", validate_number(name, seconds))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Declare a setting that is a number of times
|
|
38
|
+
#
|
|
39
|
+
# @api private
|
|
40
|
+
# @param name [Symbol] the name of the setting
|
|
41
|
+
# @return [Symbol] the name of the writer
|
|
42
|
+
# @example Declare a maximum
|
|
43
|
+
# count_setting :max_retries
|
|
44
|
+
def count_setting(name)
|
|
45
|
+
attr_reader(name)
|
|
46
|
+
|
|
47
|
+
define_method(:"#{name}=") do |count|
|
|
48
|
+
# @type self: Settings
|
|
49
|
+
instance_variable_set(:"@#{name}", validate_count(name, count))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Declare a setting that falls back to an environment variable
|
|
54
|
+
#
|
|
55
|
+
# The environment is read each time the setting is, rather than once as the library is loaded, so that a
|
|
56
|
+
# variable set afterwards, such as one a dotenv file is read for, is the one a client is built with. A setting
|
|
57
|
+
# that has been assigned, even nil, is read as it was assigned.
|
|
58
|
+
#
|
|
59
|
+
# @api private
|
|
60
|
+
# @param name [Symbol] the name of the setting
|
|
61
|
+
# @param variable [String] the name of the environment variable the setting falls back to
|
|
62
|
+
# @return [Array<Symbol>] the name of the writer
|
|
63
|
+
# @example Declare a credential
|
|
64
|
+
# environment_setting :client_id, "TESLA_CLIENT_ID"
|
|
65
|
+
def environment_setting(name, variable)
|
|
66
|
+
define_method(name) do
|
|
67
|
+
# @type self: Settings
|
|
68
|
+
instance_variable_defined?(:"@#{name}") ? instance_variable_get(:"@#{name}") : ENV.fetch(variable, nil)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
attr_writer(name)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Extend whatever includes the mixin with the macros that declare the settings
|
|
76
|
+
#
|
|
77
|
+
# @api private
|
|
78
|
+
# @param base [Module] the module or class including {Settings}
|
|
79
|
+
# @return [void]
|
|
80
|
+
def self.included(base)
|
|
81
|
+
base.extend(Declaration)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Check that a setting is a number of zero or more
|
|
87
|
+
#
|
|
88
|
+
# Any Numeric is one, since a timeout shorter than a second is a Float, and zero is one: it turns off the wait
|
|
89
|
+
# or the retrying the setting names rather than leaving it unset.
|
|
90
|
+
#
|
|
91
|
+
# @api private
|
|
92
|
+
# @param name [Symbol] the name of the setting, which the message names
|
|
93
|
+
# @param number [Object] the value assigned
|
|
94
|
+
# @return [Numeric] the number
|
|
95
|
+
# @raise [ArgumentError] if the value is not a number of zero or more
|
|
96
|
+
def validate_number(name, number)
|
|
97
|
+
raise ArgumentError, "Invalid #{name}: #{number.inspect}" unless number.is_a?(Numeric) && number >= 0
|
|
98
|
+
|
|
99
|
+
number
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Check that a setting is a whole number of times
|
|
103
|
+
#
|
|
104
|
+
# @api private
|
|
105
|
+
# @param name [Symbol] the name of the setting, which the message names
|
|
106
|
+
# @param count [Object] the value assigned
|
|
107
|
+
# @return [Integer] the count
|
|
108
|
+
# @raise [ArgumentError] if the value is not a whole number of zero or more times
|
|
109
|
+
def validate_count(name, count)
|
|
110
|
+
raise ArgumentError, "Invalid #{name}: #{count.inspect}" unless validate_number(name, count).integer?
|
|
111
|
+
|
|
112
|
+
count
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Tesla
|
|
6
|
+
# Checks and compares the URLs a host, a proxy, and a redirect are given as, mixed into the configuration, the
|
|
7
|
+
# client, the connection, and the redirect handler
|
|
8
|
+
#
|
|
9
|
+
# A URL is checked where it is assigned rather than when a request is made with it, so that the error names the
|
|
10
|
+
# assignment that was wrong instead of the call that happened to send the next request. The value that is rejected
|
|
11
|
+
# is left as it was.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
module URLValidation
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# Check that a host is a URL requests can be sent to
|
|
18
|
+
#
|
|
19
|
+
# A host that carries a user and password, such as "https://user:pass@tesla.example.com", is refused: Net::HTTP
|
|
20
|
+
# sends neither, and the host is shown by {Client#inspect} and in error messages, which would show the
|
|
21
|
+
# password. The message the host is refused with leaves them out.
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
# @param host [String] the host, including scheme
|
|
25
|
+
# @return [String] the host
|
|
26
|
+
# @raise [ArgumentError] if the host is not an HTTP or HTTPS URL, or carries a user and password
|
|
27
|
+
def validate_host(host)
|
|
28
|
+
raise ArgumentError, "Invalid host: #{redact(host.to_s)}" unless http_url?(host)
|
|
29
|
+
if URI(host).userinfo
|
|
30
|
+
raise ArgumentError, "Invalid host: #{redact(host)} carries a user and password, which are not sent"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
host
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Parse and validate a proxy URL
|
|
37
|
+
#
|
|
38
|
+
# @api private
|
|
39
|
+
# @param proxy_url [String] the proxy URL
|
|
40
|
+
# @return [URI::HTTP] the proxy URI
|
|
41
|
+
# @raise [ArgumentError] if the proxy URL is not a valid HTTP or HTTPS URL; the message leaves out its user and
|
|
42
|
+
# password
|
|
43
|
+
def parse_proxy_uri(proxy_url)
|
|
44
|
+
proxy_uri = URI(proxy_url)
|
|
45
|
+
raise ArgumentError, "Invalid proxy URL: #{redact(proxy_url)}" unless proxy_uri.is_a?(URI::HTTP)
|
|
46
|
+
|
|
47
|
+
proxy_uri
|
|
48
|
+
rescue URI::InvalidURIError
|
|
49
|
+
raise ArgumentError, "Invalid proxy URL: #{redact(proxy_url)}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Whether a host is an HTTP or HTTPS URL with a host
|
|
53
|
+
# @api private
|
|
54
|
+
# @param host [Object] the host
|
|
55
|
+
# @return [Boolean] whether the host is a URL requests can be sent to
|
|
56
|
+
def http_url?(host)
|
|
57
|
+
uri = URI(host)
|
|
58
|
+
uri.is_a?(URI::HTTP) && !uri.host.to_s.empty?
|
|
59
|
+
rescue ArgumentError, URI::InvalidURIError
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Join a host and a request path, keeping any path prefix on the host
|
|
64
|
+
#
|
|
65
|
+
# A path that is a URL of its own is refused rather than followed to the host it names, since the access token
|
|
66
|
+
# of a request is the one issued for the host it was meant for, and a path that moved the request to another
|
|
67
|
+
# host would take the token with it.
|
|
68
|
+
#
|
|
69
|
+
# A path that climbs out of the prefix the host carries, such as "../.." for a host of
|
|
70
|
+
# "https://tesla.example.com/fleet", is refused for the same reason: the client was pointed at that prefix, and
|
|
71
|
+
# a path that leaves it asks the rest of the host with the access token of the client. A host without a prefix
|
|
72
|
+
# names the whole of its origin, so there is nothing to climb out of.
|
|
73
|
+
#
|
|
74
|
+
# @api private
|
|
75
|
+
# @param host [String] the host, optionally carrying a path prefix
|
|
76
|
+
# @param path [String] the request path
|
|
77
|
+
# @return [URI] the request URI
|
|
78
|
+
# @raise [ArgumentError] if the path is a URL of an origin other than the host, or climbs out of its prefix
|
|
79
|
+
def build_uri(host, path)
|
|
80
|
+
uri = URI.join("#{host.chomp("/")}/", path.delete_prefix("/"))
|
|
81
|
+
raise ArgumentError, "Path is not on #{host}: #{path}" unless on_host?(uri, host)
|
|
82
|
+
|
|
83
|
+
uri
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Whether two URLs share a scheme, host, and port
|
|
87
|
+
#
|
|
88
|
+
# @api private
|
|
89
|
+
# @param url [String, URI::Generic] one URL
|
|
90
|
+
# @param other [String, URI::Generic] the other URL
|
|
91
|
+
# @return [Boolean] whether the URLs share an origin
|
|
92
|
+
def same_origin?(url, other)
|
|
93
|
+
origin(url).eql?(origin(other))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Whether a URL is the host's own, at or under the path the host names
|
|
97
|
+
#
|
|
98
|
+
# The path is compared as well as the origin, so that a URL climbing out of the prefix a host carries, such as
|
|
99
|
+
# "../.." for "https://tesla.example.com/fleet", is told from one the host was pointed at, as a URL of
|
|
100
|
+
# another scheme, host, or port is. A host that carries no prefix names the whole of its origin, so every URL
|
|
101
|
+
# of that origin is on it.
|
|
102
|
+
#
|
|
103
|
+
# The paths are read once the origins are known to match, so a URL that has none, such as a "mailto:" URL, is
|
|
104
|
+
# answered by the comparison of the origins rather than by reading the path it does not have.
|
|
105
|
+
#
|
|
106
|
+
# @api private
|
|
107
|
+
# @param uri [URI::Generic] the URL, as the URI it was read as
|
|
108
|
+
# @param host [String, URI::Generic] the host, optionally carrying a path prefix
|
|
109
|
+
# @return [Boolean] whether the URL is the host's own, at or under the path it names
|
|
110
|
+
def on_host?(uri, host)
|
|
111
|
+
same_origin?(uri, host) && uri.path.start_with?("#{URI(host).path.chomp("/")}/") # steep:ignore NoMethod
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The origin of a URL, with the scheme and host in lowercase
|
|
115
|
+
# @api private
|
|
116
|
+
# @param url [String, URI::Generic] the URL
|
|
117
|
+
# @return [Array] the scheme, host, and port
|
|
118
|
+
def origin(url)
|
|
119
|
+
uri = URI(url).normalize
|
|
120
|
+
[uri.scheme, uri.host, uri.port]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Remove the user and password from a URL
|
|
124
|
+
# @api private
|
|
125
|
+
# @param url [String, nil] the URL
|
|
126
|
+
# @return [String, nil] the URL without its userinfo, or nil for nil
|
|
127
|
+
def redact(url)
|
|
128
|
+
url&.sub(%r{(?<=//)[^/@]*@}, "")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/tesla/user.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "resource"
|
|
4
|
+
|
|
5
|
+
module Tesla
|
|
6
|
+
# The user of an account
|
|
7
|
+
# @api public
|
|
8
|
+
class User < Resource
|
|
9
|
+
inspect_with :email
|
|
10
|
+
identified_by :email
|
|
11
|
+
|
|
12
|
+
# @!method email
|
|
13
|
+
# The email address of the user
|
|
14
|
+
# @api public
|
|
15
|
+
# @return [String, nil] the email address of the user
|
|
16
|
+
# @example
|
|
17
|
+
# user.email
|
|
18
|
+
attribute :email
|
|
19
|
+
|
|
20
|
+
# @!method full_name
|
|
21
|
+
# The full name of the user
|
|
22
|
+
# @api public
|
|
23
|
+
# @return [String, nil] the full name of the user
|
|
24
|
+
# @example
|
|
25
|
+
# user.full_name
|
|
26
|
+
attribute :full_name
|
|
27
|
+
|
|
28
|
+
# @!method profile_image_url
|
|
29
|
+
# The URL of the profile image of the user
|
|
30
|
+
# @api public
|
|
31
|
+
# @return [String, nil] the URL of the profile image of the user
|
|
32
|
+
# @example
|
|
33
|
+
# user.profile_image_url
|
|
34
|
+
attribute :profile_image_url
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "resource"
|
|
4
|
+
|
|
5
|
+
module Tesla
|
|
6
|
+
# A vehicle of the account
|
|
7
|
+
# @api public
|
|
8
|
+
class Vehicle < Resource
|
|
9
|
+
inspect_with :vin, :display_name, :state
|
|
10
|
+
identified_by :vin
|
|
11
|
+
|
|
12
|
+
# @!method id
|
|
13
|
+
# The ID the Fleet API knows the vehicle by
|
|
14
|
+
# @api public
|
|
15
|
+
# @return [Integer, nil] the ID the Fleet API knows the vehicle by
|
|
16
|
+
# @example
|
|
17
|
+
# vehicle.id
|
|
18
|
+
attribute :id
|
|
19
|
+
|
|
20
|
+
# @!method vehicle_id
|
|
21
|
+
# The ID the streaming and Autopark APIs know the vehicle by
|
|
22
|
+
# @api public
|
|
23
|
+
# @return [Integer, nil] the ID the streaming and Autopark APIs know the vehicle by
|
|
24
|
+
# @example
|
|
25
|
+
# vehicle.vehicle_id
|
|
26
|
+
attribute :vehicle_id
|
|
27
|
+
|
|
28
|
+
# @!method vin
|
|
29
|
+
# The vehicle identification number
|
|
30
|
+
# @api public
|
|
31
|
+
# @return [String, nil] the vehicle identification number
|
|
32
|
+
# @example
|
|
33
|
+
# vehicle.vin
|
|
34
|
+
attribute :vin
|
|
35
|
+
|
|
36
|
+
# @!method display_name
|
|
37
|
+
# The name the owner gave the vehicle
|
|
38
|
+
# @api public
|
|
39
|
+
# @return [String, nil] the name the owner gave the vehicle
|
|
40
|
+
# @example
|
|
41
|
+
# vehicle.display_name
|
|
42
|
+
attribute :display_name
|
|
43
|
+
|
|
44
|
+
# @!method state
|
|
45
|
+
# The state of the vehicle, such as "online", "asleep", or "offline"
|
|
46
|
+
# @api public
|
|
47
|
+
# @return [String, nil] the state of the vehicle
|
|
48
|
+
# @example
|
|
49
|
+
# vehicle.state
|
|
50
|
+
attribute :state
|
|
51
|
+
|
|
52
|
+
# @!method access_type
|
|
53
|
+
# The access the account has to the vehicle, such as "OWNER" or "DRIVER"
|
|
54
|
+
# @api public
|
|
55
|
+
# @return [String, nil] the access the account has to the vehicle
|
|
56
|
+
# @example
|
|
57
|
+
# vehicle.access_type
|
|
58
|
+
attribute :access_type
|
|
59
|
+
|
|
60
|
+
# @!method api_version
|
|
61
|
+
# The version of the API the vehicle speaks
|
|
62
|
+
# @api public
|
|
63
|
+
# @return [Integer, nil] the version of the API the vehicle speaks
|
|
64
|
+
# @example
|
|
65
|
+
# vehicle.api_version
|
|
66
|
+
attribute :api_version
|
|
67
|
+
|
|
68
|
+
# @!method in_service?
|
|
69
|
+
# Whether the vehicle is being serviced
|
|
70
|
+
# @api public
|
|
71
|
+
# @return [Boolean] whether the vehicle is being serviced
|
|
72
|
+
# @example
|
|
73
|
+
# vehicle.in_service?
|
|
74
|
+
predicate :in_service
|
|
75
|
+
|
|
76
|
+
# Whether the vehicle is online
|
|
77
|
+
#
|
|
78
|
+
# A vehicle takes commands and answers with its data only while it is online.
|
|
79
|
+
#
|
|
80
|
+
# A vehicle that is asleep is woken with {API::VehicleEndpoints#wake_up}.
|
|
81
|
+
#
|
|
82
|
+
# @api public
|
|
83
|
+
# @return [Boolean] whether the state of the vehicle is "online"
|
|
84
|
+
# @example
|
|
85
|
+
# vehicle.online?
|
|
86
|
+
def online?
|
|
87
|
+
state.eql?("online")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|