typeform_data 1.0.0 → 2.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/README.md +7 -0
- data/Rakefile +1 -2
- data/lib/typeform_data.rb +1 -0
- data/lib/typeform_data/client.rb +3 -3
- data/lib/typeform_data/comparable_by_id_and_config.rb +2 -2
- data/lib/typeform_data/config.rb +1 -1
- data/lib/typeform_data/errors.rb +18 -3
- data/lib/typeform_data/requestor.rb +61 -26
- data/lib/typeform_data/typeform.rb +4 -4
- data/lib/typeform_data/utils.rb +24 -0
- data/lib/typeform_data/value_class.rb +2 -1
- data/lib/typeform_data/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d70cdb5034873b422938f7cf426b0b1341a992ab
|
4
|
+
data.tar.gz: a36e89135238d96c2ebe7ea0466c56fbf2f2b23f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ec3a03e56573d5b75fed0aff79d2f1a54dfeb176433c7afc1621e6532a76e7fb31a0ca9ca8477ca41c78ba04b9ef8b05ea4b0b5c9c88df2a7e870ea3ea47ed1
|
7
|
+
data.tar.gz: 38532360add588301c0e3c3621088e72dba3ccc7758cecd1a84f43ae4eb27a4824010a32db4ae0d874088573f5a6b3fd1ebf14ec517ed3b869f989124394858a
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -62,6 +62,13 @@ deserialized = client.load(serialized)
|
|
62
62
|
=> true
|
63
63
|
```
|
64
64
|
|
65
|
+
### Error-handling
|
66
|
+
|
67
|
+
Unless you've encountered a bug, all exceptions raised by this gem should extend from `TypeformData::Error`. For the full exception hierarchy, see `lib/typeform_data/errors`.
|
68
|
+
|
69
|
+
If a HTTP request to Typeform fails with an error that we expect to be transient (e.g. a 503) we retry the HTTP request up to 3 times, after waiting 1, 2 and 4 seconds. If you'd prefer a fail-fast approach, send us a PR!
|
70
|
+
|
71
|
+
|
65
72
|
## Notes on the API
|
66
73
|
|
67
74
|
So far, we've found Typeform's current Data API to be confusing. In particular, there are a couple design decisions that have been a source of friction for us:
|
data/Rakefile
CHANGED
data/lib/typeform_data.rb
CHANGED
data/lib/typeform_data/client.rb
CHANGED
@@ -11,7 +11,7 @@ module TypeformData
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def self.new_from_config(config)
|
14
|
-
raise ArgumentError, 'Missing config' unless config
|
14
|
+
raise TypeformData::ArgumentError, 'Missing config' unless config
|
15
15
|
new(api_key: config.api_key)
|
16
16
|
end
|
17
17
|
|
@@ -27,12 +27,12 @@ module TypeformData
|
|
27
27
|
|
28
28
|
def all_typeforms
|
29
29
|
get('forms').parsed_json.map do |form_hash|
|
30
|
-
|
30
|
+
TypeformData::Typeform.new(@config, form_hash)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
34
|
def typeform(id)
|
35
|
-
|
35
|
+
TypeformData::Typeform.new(@config, id: id)
|
36
36
|
end
|
37
37
|
|
38
38
|
def dump(object)
|
@@ -10,14 +10,14 @@ module TypeformData
|
|
10
10
|
|
11
11
|
def ==(other)
|
12
12
|
unless other.respond_to?(:sort_key, true) && other.respond_to?(:config, true)
|
13
|
-
raise ArgumentError, "#{other.inspect} does not specify a sort key and config"
|
13
|
+
raise TypeformData::ArgumentError, "#{other.inspect} does not specify a sort key and config"
|
14
14
|
end
|
15
15
|
other.sort_key == sort_key && other.config == config
|
16
16
|
end
|
17
17
|
|
18
18
|
def <=>(other)
|
19
19
|
unless other.respond_to?(:sort_key)
|
20
|
-
raise ArgumentError, "#{other.inspect} does not specify a sort key"
|
20
|
+
raise TypeformData::ArgumentError, "#{other.inspect} does not specify a sort key"
|
21
21
|
end
|
22
22
|
other.sort_key <=> sort_key
|
23
23
|
end
|
data/lib/typeform_data/config.rb
CHANGED
@@ -5,7 +5,7 @@ module TypeformData
|
|
5
5
|
|
6
6
|
def initialize(api_key:)
|
7
7
|
unless api_key.is_a?(String) && api_key.length.positive?
|
8
|
-
raise ArgumentError, 'An API key (as a nonempty String) is required'
|
8
|
+
raise TypeformData::ArgumentError, 'An API key (as a nonempty String) is required'
|
9
9
|
end
|
10
10
|
@api_key = api_key
|
11
11
|
end
|
data/lib/typeform_data/errors.rb
CHANGED
@@ -1,13 +1,28 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module TypeformData
|
3
|
+
|
3
4
|
class Error < StandardError; end
|
4
|
-
|
5
|
-
class ConnectionRefused < Error; end
|
6
|
-
class BadRequest < Error; end
|
5
|
+
|
7
6
|
class ArgumentError < Error; end
|
7
|
+
|
8
|
+
class InvalidApiKey < Error; end
|
9
|
+
|
8
10
|
class UnexpectedError < Error; end
|
9
11
|
|
12
|
+
class UnexpectedHttpError < UnexpectedError; end
|
13
|
+
class UnexpectedHttpResponse < UnexpectedHttpError; end
|
14
|
+
class BadRequest < UnexpectedHttpResponse; end
|
15
|
+
class TransientResponseError < UnexpectedHttpResponse; end
|
16
|
+
|
10
17
|
# When using the 'token' field in requests, the API may return a 404 even if the endpoint path
|
11
18
|
# is correct.
|
12
19
|
class InvalidEndpointOrMissingResource < Error; end
|
20
|
+
|
21
|
+
module Errors
|
22
|
+
def self.stringify_error(error)
|
23
|
+
"#{error.backtrace.first}: #{error.message} (#{error.class})\n" +
|
24
|
+
error.backtrace.drop(1).map { |line| "\t#{line}\n" }.join
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
13
28
|
end
|
@@ -1,25 +1,74 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require 'typeform_data/utils'
|
2
3
|
|
3
4
|
module TypeformData
|
4
5
|
module Requestor
|
5
6
|
|
7
|
+
RETRY_EXCEPTIONS = [
|
8
|
+
# We wouldn't ordinarily retry in this case, but Typeform appears to have transient issues
|
9
|
+
# with their SSL.
|
10
|
+
OpenSSL::SSL::SSLError,
|
11
|
+
|
12
|
+
Errno::ECONNREFUSED,
|
13
|
+
TypeformData::TransientResponseError,
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
RETRY_RESPONSE_CLASSES = [
|
17
|
+
Net::HTTPServiceUnavailable,
|
18
|
+
Net::HTTPTooManyRequests,
|
19
|
+
Net::HTTPBadGateway,
|
20
|
+
].freeze
|
21
|
+
|
6
22
|
def self.get(config, endpoint, params = nil)
|
7
23
|
request(config, Net::HTTP::Get, request_path(config, endpoint), params)
|
8
24
|
end
|
9
25
|
|
10
|
-
def self.request_path(config, endpoint)
|
26
|
+
private_class_method def self.request_path(config, endpoint)
|
11
27
|
"/v#{config.api_version}/#{endpoint}"
|
12
28
|
end
|
13
29
|
|
14
|
-
|
15
|
-
|
16
|
-
# rubocop:disable Metrics/MethodLength
|
30
|
+
# @raise TypeformData::Error
|
17
31
|
# @return TypeformData::ApiResponse
|
18
|
-
def self.request(config, method_class, path, input_params = {})
|
32
|
+
private_class_method def self.request(config, method_class, path, input_params = {})
|
19
33
|
params = input_params.dup
|
20
34
|
params[:key] = config.api_key
|
21
35
|
|
22
|
-
|
36
|
+
begin
|
37
|
+
Utils.retry_with_exponential_backoff(RETRY_EXCEPTIONS, max_retries: 3) do
|
38
|
+
request_and_validate_response(config, method_class, path, params)
|
39
|
+
end
|
40
|
+
rescue *RETRY_EXCEPTIONS => error
|
41
|
+
raise UnexpectedHttpError, 'Unexpected HTTP error (retried 3 times): ' +
|
42
|
+
TypeformData::Errors.stringify_error(error)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return TypeformData::ApiResponse
|
47
|
+
private_class_method def self.request_and_validate_response(config, method_class, path, params)
|
48
|
+
response = request_response(config, method_class, path, params)
|
49
|
+
|
50
|
+
case response
|
51
|
+
when Net::HTTPSuccess
|
52
|
+
return TypeformData::ApiResponse.new(response)
|
53
|
+
|
54
|
+
when Net::HTTPNotFound
|
55
|
+
raise TypeformData::InvalidEndpointOrMissingResource, path
|
56
|
+
when Net::HTTPForbidden
|
57
|
+
raise TypeformData::InvalidApiKey, "Invalid api key: #{config.api_key}"
|
58
|
+
when Net::HTTPBadRequest
|
59
|
+
raise TypeformData::BadRequest, 'Response was a Net::HTTPBadRequest with body: '\
|
60
|
+
"#{response.body}. Your request with params: #{params} could not be processed."
|
61
|
+
when *RETRY_RESPONSE_CLASSES
|
62
|
+
raise TypeformData::TransientResponseError, "Response was a #{response.class} "\
|
63
|
+
"(code #{response.code}) with message #{response.message}"
|
64
|
+
else
|
65
|
+
raise TypeformData::UnexpectedHttpResponse, 'Unexpected HTTP response with code: '\
|
66
|
+
"#{response.code} and message: #{response.message}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private_class_method def self.request_response(config, method_class, path, params)
|
71
|
+
Net::HTTP.new(config.host, config.port).tap { |http|
|
23
72
|
http.use_ssl = true
|
24
73
|
|
25
74
|
# Uncomment this line for debugging:
|
@@ -30,28 +79,14 @@ module TypeformData
|
|
30
79
|
'Content-Type' => 'application/json'
|
31
80
|
)
|
32
81
|
)
|
82
|
+
rescue *RETRY_EXCEPTIONS
|
83
|
+
raise # So retry_with_exponential_backoff can catch the exception and retry.
|
33
84
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
raise TypeformData::InvalidApiKey, "Invalid api key: #{config.api_key}"
|
39
|
-
when Net::HTTPBadRequest then
|
40
|
-
raise TypeformData::BadRequest, 'There was an error processing your request: '\
|
41
|
-
"#{response.body}, with params: #{params}"
|
42
|
-
when Net::HTTPSuccess
|
43
|
-
return TypeformData::ApiResponse.new(response)
|
44
|
-
else
|
45
|
-
raise TypeformData::UnexpectedError, "A #{response.code} error has occurred: "\
|
46
|
-
"'#{response.message}'"
|
47
|
-
end
|
48
|
-
|
49
|
-
rescue Errno::ECONNREFUSED
|
50
|
-
raise TypeformData::ConnectionRefused, 'The connection was refused'
|
85
|
+
# Why are we rescuing StandardError? See http://stackoverflow.com/a/11802674/1067145
|
86
|
+
rescue StandardError => error
|
87
|
+
raise UnexpectedHttpError, 'Unexpected HTTP error: ' +
|
88
|
+
TypeformData::Errors.stringify_error(error)
|
51
89
|
end
|
52
|
-
# rubocop:enable Metrics/MethodLength
|
53
|
-
|
54
|
-
private_class_method :request
|
55
90
|
|
56
91
|
end
|
57
92
|
end
|
@@ -142,24 +142,24 @@ module TypeformData
|
|
142
142
|
params = input_params.dup
|
143
143
|
|
144
144
|
params.keys.select { |key| key.is_a?(Symbol) }.each do |sym|
|
145
|
-
raise
|
145
|
+
raise TypeformData::ArgumentError, 'Duplicate keys' if params.key?(sym.to_s)
|
146
146
|
params[sym.to_s] = params[sym]
|
147
147
|
params.delete(sym)
|
148
148
|
end
|
149
149
|
|
150
150
|
params.keys.each do |key|
|
151
151
|
next if PERMITTED_KEYS.key?(key) && params[key].is_a?(PERMITTED_KEYS[key])
|
152
|
-
raise
|
152
|
+
raise TypeformData::ArgumentError, "Invalid/unsupported param: #{key}"
|
153
153
|
end
|
154
154
|
|
155
155
|
if params['limit'] && params['limit'] > MAX_PAGE_SIZE
|
156
|
-
raise
|
156
|
+
raise TypeformData::ArgumentError, "The maximum limit is #{MAX_PAGE_SIZE}. You "\
|
157
157
|
"provided: #{params['limit']}"
|
158
158
|
end
|
159
159
|
|
160
160
|
if params['token']
|
161
161
|
if params.keys.length > 1
|
162
|
-
raise
|
162
|
+
raise TypeformData::ArgumentError, "'token' may not be combined with other filters"
|
163
163
|
end
|
164
164
|
else
|
165
165
|
params['offset'] ||= 0
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Utils
|
3
|
+
|
4
|
+
# Repeats the block until it succeeds or a limit is reached, waiting twice as long as it
|
5
|
+
# previously did after each failure.
|
6
|
+
# @param rescued_exceptions [Class] Subclasses of Exception.
|
7
|
+
# @param max_retries [Integer]
|
8
|
+
# @param initial_wait [Integer] In seconds.
|
9
|
+
def self.retry_with_exponential_backoff(rescued_exceptions, max_retries: 5, initial_wait: 1)
|
10
|
+
seconds_to_wait = initial_wait
|
11
|
+
|
12
|
+
max_retries.times do |iteration|
|
13
|
+
begin
|
14
|
+
break yield
|
15
|
+
rescue *rescued_exceptions
|
16
|
+
sleep seconds_to_wait
|
17
|
+
seconds_to_wait *= 2
|
18
|
+
|
19
|
+
raise if iteration == max_retries - 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -6,7 +6,8 @@ module TypeformData
|
|
6
6
|
|
7
7
|
def initialize(config, attrs)
|
8
8
|
unless config && config.is_a?(TypeformData::Config)
|
9
|
-
raise ArgumentError, 'Expected a TypeformData::Config instance as the first
|
9
|
+
raise TypeformData::ArgumentError, 'Expected a TypeformData::Config instance as the first '\
|
10
|
+
'argument'
|
10
11
|
end
|
11
12
|
@config = config
|
12
13
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: typeform_data
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Max Wallace
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-02-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -169,6 +169,7 @@ files:
|
|
169
169
|
- lib/typeform_data/typeform/question.rb
|
170
170
|
- lib/typeform_data/typeform/response.rb
|
171
171
|
- lib/typeform_data/typeform/stats.rb
|
172
|
+
- lib/typeform_data/utils.rb
|
172
173
|
- lib/typeform_data/value_class.rb
|
173
174
|
- lib/typeform_data/version.rb
|
174
175
|
- typeform_data.gemspec
|