quolle 1.0.0 → 1.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 +4 -4
- data/README.md +8 -2
- data/lib/quolle/client.rb +33 -5
- data/lib/quolle/error.rb +6 -1
- data/lib/quolle/resources/emails.rb +17 -4
- data/lib/quolle/version.rb +1 -1
- metadata +4 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a0658c46b0cbf87fde8e453e3c79da2444565b8a97360807ae24d7865125e57b
|
|
4
|
+
data.tar.gz: ca10637ae139bfe029c0b98608e0b3edde03a0f0f3b9a54cbd75ba2c7201a6a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 46130d471c82ef7e8e1fc6f4a940d8a6a8937f039309d4fb75e45422923c7be48edc61953ba6fba44aa0a8d00db4b427747d0f4c43fa2a5e93b092606e1f4ad9
|
|
7
|
+
data.tar.gz: ee558f680e6311a1eb05b56a611ca493c0bd24cbe57828ae1119e11b51dbb30b6bd9c4bc1dbffb8e613ed7cc66a5bb0f69ca71a55e61201695e321058c68e332
|
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Or in your Gemfile:
|
|
|
14
14
|
gem "quolle"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Requires Ruby
|
|
17
|
+
Requires Ruby 3.3+. No third-party dependencies.
|
|
18
18
|
|
|
19
19
|
## Quick start
|
|
20
20
|
|
|
@@ -33,6 +33,11 @@ result = quolle.emails.send(
|
|
|
33
33
|
puts "Queued: #{result['id']}"
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
`emails.create(...)` does exactly the same thing and doesn't shadow Ruby's
|
|
37
|
+
`Object#send`, so prefer it if you use `send` for dynamic dispatch. Pass
|
|
38
|
+
`type: "marketing"` for marketing mail (adds unsubscribe headers); the default
|
|
39
|
+
is transactional.
|
|
40
|
+
|
|
36
41
|
## Sending
|
|
37
42
|
|
|
38
43
|
### Multiple recipients
|
|
@@ -135,6 +140,7 @@ begin
|
|
|
135
140
|
)
|
|
136
141
|
rescue Quolle::Error => e
|
|
137
142
|
puts e.status_code # e.g. 402
|
|
143
|
+
puts e.code # e.g. "monthly_quota_exceeded" — branch on this
|
|
138
144
|
puts e.message # e.g. "Monthly limit reached"
|
|
139
145
|
puts e.data # extra fields, e.g. {"limit" => 3000}
|
|
140
146
|
end
|
|
@@ -167,7 +173,7 @@ HMAC-SHA256 with a 5-minute timestamp window (replay protection).
|
|
|
167
173
|
|
|
168
174
|
## Automatic retries
|
|
169
175
|
|
|
170
|
-
Transient failures — HTTP 429 (rate limit) and 5xx, plus network errors — are retried automatically with exponential backoff, honoring the `Retry-After` header. To avoid double-sending, a POST is only retried on a 5xx/network error when you pass an idempotency key; a 429 is always safe to retry (the request was never processed). Tune with `max_retries:` on `Quolle.new` (default 3).
|
|
176
|
+
Transient failures — HTTP 429 (rate limit) and 5xx, plus network errors — are retried automatically with exponential backoff, honoring the `Retry-After` header. To avoid double-sending, a POST is only retried on a 5xx/network error when you pass an idempotency key; a 429 is always safe to retry (the request was never processed). Quota limits are the exception: `daily_quota_exceeded` and `warmup_limit_exceeded` (and any `Retry-After` over 60 seconds) are raised immediately, because they reset at a fixed time, not in seconds; check `e.code` and queue the send for later. Tune with `max_retries:` on `Quolle.new` (default 3).
|
|
171
177
|
|
|
172
178
|
## License
|
|
173
179
|
|
data/lib/quolle/client.rb
CHANGED
|
@@ -28,6 +28,14 @@ module Quolle
|
|
|
28
28
|
# @return [Quolle::Resources::Webhooks]
|
|
29
29
|
attr_reader :webhooks
|
|
30
30
|
|
|
31
|
+
# 429s that retrying can't fix: the limit resets at a fixed time (midnight
|
|
32
|
+
# UTC, the next billing period), not in seconds. Raised at once so the
|
|
33
|
+
# caller can queue the send instead of blocking in a retry loop.
|
|
34
|
+
NO_RETRY_CODES = %w[daily_quota_exceeded warmup_limit_exceeded monthly_quota_exceeded].freeze
|
|
35
|
+
# A Retry-After longer than this (seconds) means "not soon": raise instead
|
|
36
|
+
# of sleeping through it.
|
|
37
|
+
MAX_RETRY_WAIT = 60
|
|
38
|
+
|
|
31
39
|
# @param api_key [String, nil] your API key (falls back to QUOLLE_API_KEY)
|
|
32
40
|
# @param base_url [String]
|
|
33
41
|
# @param timeout [Integer] per-request timeout in seconds
|
|
@@ -89,18 +97,38 @@ module Quolle
|
|
|
89
97
|
raise last_error
|
|
90
98
|
end
|
|
91
99
|
|
|
92
|
-
parsed = resp.body.to_s.empty? ? {} : (JSON.parse(resp.body) rescue {})
|
|
93
100
|
status = resp.code.to_i
|
|
101
|
+
json_ok = true
|
|
102
|
+
parsed =
|
|
103
|
+
if resp.body.to_s.empty?
|
|
104
|
+
{}
|
|
105
|
+
else
|
|
106
|
+
begin
|
|
107
|
+
JSON.parse(resp.body)
|
|
108
|
+
rescue JSON::ParserError
|
|
109
|
+
json_ok = false
|
|
110
|
+
{}
|
|
111
|
+
end
|
|
112
|
+
end
|
|
94
113
|
|
|
95
114
|
if (200..299).cover?(status)
|
|
115
|
+
# A 2xx that isn't JSON (a proxy page, a truncated body) is an error,
|
|
116
|
+
# not an empty success.
|
|
117
|
+
unless json_ok
|
|
118
|
+
raise Quolle::Error.new("Unexpected non-JSON response from the Quolle API",
|
|
119
|
+
status_code: status, data: { "body" => resp.body.to_s[0, 500] })
|
|
120
|
+
end
|
|
96
121
|
return parsed
|
|
97
122
|
end
|
|
98
123
|
|
|
99
124
|
message = parsed.delete("error") || "Request failed with status #{status}"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
125
|
+
code = parsed.delete("code")
|
|
126
|
+
last_error = Quolle::Error.new(message, status_code: status, data: parsed, code: code)
|
|
127
|
+
retryable = (status == 429 && !NO_RETRY_CODES.include?(code)) ||
|
|
128
|
+
((500..504).cover?(status) && (idempotent || has_idem))
|
|
129
|
+
wait = backoff(attempt, resp["Retry-After"])
|
|
130
|
+
if attempt < @max_retries && retryable && wait <= MAX_RETRY_WAIT
|
|
131
|
+
sleep(wait)
|
|
104
132
|
next
|
|
105
133
|
end
|
|
106
134
|
raise last_error
|
data/lib/quolle/error.rb
CHANGED
|
@@ -8,11 +8,16 @@ module Quolle
|
|
|
8
8
|
attr_reader :status_code
|
|
9
9
|
# @return [Hash] extra fields returned alongside +error+.
|
|
10
10
|
attr_reader :data
|
|
11
|
+
# @return [String, nil] the API's stable, machine-readable error code
|
|
12
|
+
# (e.g. "validation_error", "rate_limit_exceeded"). Branch on this, not
|
|
13
|
+
# on the message. nil for transport failures.
|
|
14
|
+
attr_reader :code
|
|
11
15
|
|
|
12
|
-
def initialize(message, status_code: nil, data: {})
|
|
16
|
+
def initialize(message, status_code: nil, data: {}, code: nil)
|
|
13
17
|
super(message)
|
|
14
18
|
@status_code = status_code
|
|
15
19
|
@data = data || {}
|
|
20
|
+
@code = code
|
|
16
21
|
end
|
|
17
22
|
end
|
|
18
23
|
end
|
|
@@ -12,15 +12,26 @@ module Quolle
|
|
|
12
12
|
#
|
|
13
13
|
# Provide either +:html+ (with +:subject+) or a +:template+ slug (with
|
|
14
14
|
# optional +:variables+). Pass +idempotency_key:+ to make retries safe.
|
|
15
|
+
# +type:+ (or +message_type:+) is "transactional" (default) or
|
|
16
|
+
# "marketing"; marketing mail gets unsubscribe headers and honours
|
|
17
|
+
# unsubscribes.
|
|
15
18
|
#
|
|
16
19
|
# @param params [Hash] from:, to:, subject:, html:, text:, template:,
|
|
17
|
-
# variables:, reply_to:/replyTo:, scheduled_at:/scheduledAt:, metadata
|
|
20
|
+
# variables:, reply_to:/replyTo:, scheduled_at:/scheduledAt:, metadata:,
|
|
21
|
+
# attachments:, type:
|
|
18
22
|
# @return [Hash] at least "id" and "message"
|
|
19
|
-
def
|
|
23
|
+
def create(idempotency_key: nil, **params)
|
|
20
24
|
headers = idempotency_key ? { "Idempotency-Key" => idempotency_key } : {}
|
|
21
25
|
@client.request("POST", "/v1/emails/send", body: normalize(params), headers: headers)
|
|
22
26
|
end
|
|
23
27
|
|
|
28
|
+
# Same as {#create}. Kept because it's the documented name; note that it
|
|
29
|
+
# shadows Object#send on this object, so prefer +create+ in new code
|
|
30
|
+
# (and +public_send+ / +__send__+ for dynamic dispatch).
|
|
31
|
+
def send(idempotency_key: nil, **params)
|
|
32
|
+
create(idempotency_key: idempotency_key, **params)
|
|
33
|
+
end
|
|
34
|
+
|
|
24
35
|
# Send up to 100 emails in one all-or-nothing request.
|
|
25
36
|
#
|
|
26
37
|
# @param emails [Array<Hash>]
|
|
@@ -54,14 +65,16 @@ module Quolle
|
|
|
54
65
|
k = key.to_s
|
|
55
66
|
k = "replyTo" if k == "reply_to"
|
|
56
67
|
k = "scheduledAt" if k == "scheduled_at"
|
|
68
|
+
k = "type" if k == "message_type"
|
|
57
69
|
out[k] = value
|
|
58
70
|
end
|
|
59
71
|
out
|
|
60
72
|
end
|
|
61
73
|
|
|
74
|
+
# Path-segment encoding. (encode_www_form_component turned a space into
|
|
75
|
+
# "+", which is a literal plus in a URL path.)
|
|
62
76
|
def escape(id)
|
|
63
|
-
|
|
64
|
-
URI.encode_www_form_component(id.to_s)
|
|
77
|
+
id.to_s.b.gsub(/[^A-Za-z0-9\-._~]/) { |ch| format("%%%02X", ch.ord) }
|
|
65
78
|
end
|
|
66
79
|
end
|
|
67
80
|
end
|
data/lib/quolle/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: quolle
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Quolle
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: minitest
|
|
@@ -39,7 +38,6 @@ dependencies:
|
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
39
|
version: '13.0'
|
|
41
40
|
description: Send transactional email through Quolle from Ruby.
|
|
42
|
-
email:
|
|
43
41
|
executables: []
|
|
44
42
|
extensions: []
|
|
45
43
|
extra_rdoc_files: []
|
|
@@ -59,7 +57,6 @@ metadata:
|
|
|
59
57
|
homepage_uri: https://quolle.com
|
|
60
58
|
documentation_uri: https://docs.quolle.com
|
|
61
59
|
source_code_uri: https://github.com/Quolle-main/quolle-ruby
|
|
62
|
-
post_install_message:
|
|
63
60
|
rdoc_options: []
|
|
64
61
|
require_paths:
|
|
65
62
|
- lib
|
|
@@ -67,15 +64,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
67
64
|
requirements:
|
|
68
65
|
- - ">="
|
|
69
66
|
- !ruby/object:Gem::Version
|
|
70
|
-
version: '
|
|
67
|
+
version: '3.3'
|
|
71
68
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
69
|
requirements:
|
|
73
70
|
- - ">="
|
|
74
71
|
- !ruby/object:Gem::Version
|
|
75
72
|
version: '0'
|
|
76
73
|
requirements: []
|
|
77
|
-
rubygems_version:
|
|
78
|
-
signing_key:
|
|
74
|
+
rubygems_version: 4.0.20
|
|
79
75
|
specification_version: 4
|
|
80
76
|
summary: Official Ruby SDK for the Quolle email API
|
|
81
77
|
test_files: []
|