jbr 3.5.0 → 3.6.1
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/CHANGELOG.md +32 -0
- data/README.md +15 -9
- data/lib/graphql/client.rb +13 -1
- data/lib/graphql/throttled.rb +6 -0
- data/lib/jbr/asking.rb +40 -0
- data/lib/jbr/job.rb +5 -5
- data/lib/jbr/line_item.rb +3 -13
- data/lib/jbr/mock/line_item.rb +0 -2
- data/lib/jbr/oauth.rb +2 -16
- data/lib/jbr/throttle.rb +8 -0
- data/lib/jbr/version.rb +1 -1
- data/lib/jbr.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8ab569aab529193190b37620954eb2e95db5ac39ccb2a085a7caa2b6cf66981
|
|
4
|
+
data.tar.gz: 4d331b9c225a2d0fb8a978f166985f416528a23eac283c4afb31924aecf7bc8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fbe6e4b68d03262584a83279ca00eff9a4a5886413498d5db6cbec643e070d12a863188914e95c87a9365fe8f7aa419796c4238824d3346847fdbbd71960c74e
|
|
7
|
+
data.tar.gz: c0a9624a47f0c4f3706acdabf95bab82cb9d6e2deef035e88edb08daa89d4cdb686bf6489ea6e3c5fcd5b67dcc2cae4388ed19d4f6db036144366a6bcf995864
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
## [3.6.1] - 2026-08-17
|
|
2
|
+
|
|
3
|
+
- [Fix] 3.6.0 shipped without the two files it added, `graphql/throttled` and `jbr/asking`, so
|
|
4
|
+
requiring the gem raised `LoadError` and nothing worked at all. The gem's file list comes
|
|
5
|
+
from `git ls-files` and the release commit never added them. 3.6.0 is yanked
|
|
6
|
+
|
|
7
|
+
## [3.6.0] - 2026-08-17
|
|
8
|
+
|
|
9
|
+
- [Fix] A refusal for cost the bucket can recover from is waited out and asked again, up to
|
|
10
|
+
four times, rather than raised. A walk of many pages drains the bucket faster than it
|
|
11
|
+
refills and gets `Throttled (cost 1885, 1254 of 10000 available, restoring 500/s)` — 631
|
|
12
|
+
points short of a query the bucket holds five times over. The refusal prices the query, so
|
|
13
|
+
the throttle already knows the shortfall: 1.26 seconds at 500 a second, and the same
|
|
14
|
+
question is answered
|
|
15
|
+
- [Change] Only a query costing more than the bucket *ever* holds is given up on, since
|
|
16
|
+
waiting cannot help it. Where Jobber names no ceiling, one wait is tried rather than the
|
|
17
|
+
worst assumed
|
|
18
|
+
- [New] `GraphQL::Throttled`, a `GraphQL::Error` for a query refused over what it costs rather
|
|
19
|
+
than over anything about the query. It never leaves the gem — a caller still sees
|
|
20
|
+
`Jbr::Error` — but it is what tells the two refusals apart inside it
|
|
21
|
+
- [Change] What credentials do when they ask Jobber something is `Jbr::Asking`, mixed into
|
|
22
|
+
`Jbr::OAuth`, which was over a hundred lines with the retry in it
|
|
23
|
+
|
|
24
|
+
## [3.5.1] - 2026-08-17
|
|
25
|
+
|
|
26
|
+
- [Change] A line item is how many of what, and nothing else. `description` was answered and
|
|
27
|
+
read out in `to_s` alongside the quantity and the name, and no caller ever wanted it — so it
|
|
28
|
+
is not asked of Jobber any more, which is a smaller query as well as a smaller class.
|
|
29
|
+
`quantified` goes with it: it existed only to be the half of `to_s` without a description,
|
|
30
|
+
and `to_s` is that on its own now. A job's summary is `line_items.to_sentence`, since
|
|
31
|
+
`to_sentence` reads each line's own string form
|
|
32
|
+
|
|
1
33
|
## [3.5.0] - 2026-08-17
|
|
2
34
|
|
|
3
35
|
- [Fix] A walk of jobs carrying their line items was refused outright: `Throttled`. Jobber
|
data/README.md
CHANGED
|
@@ -119,11 +119,9 @@ job.line_items # => an Array of the lines the job is made of
|
|
|
119
119
|
line = job.line_items.first
|
|
120
120
|
line.quantity # => 3, whole where Jobber's own Float has nothing after the point, and 3.5
|
|
121
121
|
# where it has: `3 Faucets`, or `3.5 Hours` for what was really billed
|
|
122
|
-
line.quantified # => '3 Bathroom Faucet Installation', how many of what
|
|
123
122
|
line.name # => 'Bathroom Faucet Installation'
|
|
124
|
-
line.
|
|
125
|
-
|
|
126
|
-
# faucet)', and without the parenthesis where nobody described it
|
|
123
|
+
line.to_s # => '3 Bathroom Faucet Installation', how many of what, and the name alone where
|
|
124
|
+
# Jobber holds no quantity for the line
|
|
127
125
|
```
|
|
128
126
|
|
|
129
127
|
Every line Jobber holds is in the list, up to twenty of them, in the order it holds them and
|
|
@@ -206,11 +204,19 @@ done about either — every request waits for itself:
|
|
|
206
204
|
A request that follows no other waits for nothing, so a single `find` is as quick as it ever
|
|
207
205
|
was. Only a walk long enough to be a problem is slowed, and only by as much as it must be.
|
|
208
206
|
|
|
209
|
-
Where Jobber refuses
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
207
|
+
Where Jobber refuses anyway, the refusal says what the query would have cost against what was
|
|
208
|
+
available, and what happens next follows from those two numbers:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
Throttled (cost 1885, 1254 of 10000 available, restoring 500/s)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
631 points short of a query the bucket holds five times over, so the shortfall is waited out —
|
|
215
|
+
1.26 seconds at 500 a second — and the same question asked again, up to four times. Only a
|
|
216
|
+
query costing more than the bucket *ever* holds is given up on, since no wait would help it;
|
|
217
|
+
that one arrives as a `Jbr::Error` carrying the line above. Every connection this gem asks for
|
|
218
|
+
is bounded, to keep a query on the affordable side of that: twenty lines to a job, and twenty
|
|
219
|
+
jobs or visits to a page.
|
|
214
220
|
|
|
215
221
|
### Events
|
|
216
222
|
|
data/lib/graphql/client.rb
CHANGED
|
@@ -26,12 +26,24 @@ module GraphQL
|
|
|
26
26
|
# Before the refusal, not after it: an endpoint that reports what it will still answer
|
|
27
27
|
# reports it when it says no, which is when a caller most needs to know.
|
|
28
28
|
yield body['extensions'] if block_given?
|
|
29
|
-
raise
|
|
29
|
+
raise refusal_for(body), refusal(body) if body['errors'].present?
|
|
30
30
|
body.fetch('data')
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
|
+
# Refused for cost, or refused for the query. An endpoint that priced the query above what
|
|
36
|
+
# was left says so by naming the code, and says it again in the two numbers it reports —
|
|
37
|
+
# either is enough, since only one of them is documented and only one has been seen.
|
|
38
|
+
def refusal_for(body)
|
|
39
|
+
cost = body['extensions'].to_h['cost'].to_h
|
|
40
|
+
available = cost['throttleStatus'].to_h['currentlyAvailable']
|
|
41
|
+
coded = body['errors'].any? { |error| error.to_h.dig('extensions', 'code') == 'THROTTLED' }
|
|
42
|
+
priced = available && cost['requestedQueryCost'].to_f > available.to_f
|
|
43
|
+
|
|
44
|
+
coded || priced ? Throttled : Error
|
|
45
|
+
end
|
|
46
|
+
|
|
35
47
|
# What the endpoint refused, and — where it priced the refusal — what the query would have
|
|
36
48
|
# cost against what was available. `Throttled` on its own leaves a caller unable to tell a
|
|
37
49
|
# query too big to ever run from a bucket that only needed a moment.
|
data/lib/jbr/asking.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Jbr
|
|
2
|
+
# What credentials do when they ask Jobber something: pace the request against both limits
|
|
3
|
+
# Jobber holds an app to, wait out a refusal it can recover from, and refresh a token that
|
|
4
|
+
# went stale on the way.
|
|
5
|
+
module Asking
|
|
6
|
+
# How many times a query refused for cost is asked again. Once is the answer where the
|
|
7
|
+
# bucket only had to refill, since the wait is worked out to cover exactly the shortfall.
|
|
8
|
+
# The rest are for a bucket the whole app shares: another process may drain it again while
|
|
9
|
+
# this one waits, and each attempt costs only the seconds Jobber says it needs.
|
|
10
|
+
ATTEMPTS = 4
|
|
11
|
+
|
|
12
|
+
# Run a statement, waiting for what Jobber will still answer and refreshing a stale token.
|
|
13
|
+
# @return [Hash] the data Jobber answered, or empty when the credentials are dead.
|
|
14
|
+
def query(statement, variables: {})
|
|
15
|
+
attempts = 0
|
|
16
|
+
begin
|
|
17
|
+
throttle.wait
|
|
18
|
+
client.query(statement, variables: variables) { |extensions| throttle.read extensions }
|
|
19
|
+
rescue GraphQL::Unauthorized
|
|
20
|
+
refresh ? retry : {}
|
|
21
|
+
rescue GraphQL::Throttled => error
|
|
22
|
+
# The refusal priced the query, so the throttle now knows the shortfall and `wait` at
|
|
23
|
+
# the top of the retry sits out exactly that. Only a query costing more than the bucket
|
|
24
|
+
# ever holds is hopeless; the rest is a walk that asked a moment too early.
|
|
25
|
+
raise Error, error.message unless throttle.affordable? && (attempts += 1) < ATTEMPTS
|
|
26
|
+
|
|
27
|
+
retry
|
|
28
|
+
rescue GraphQL::Error => error
|
|
29
|
+
# The transport's own class never leaves the gem: a caller told to rescue `Jbr::Error`
|
|
30
|
+
# was not catching a throttle, a 500 or an unreadable answer, and had its own job blow
|
|
31
|
+
# up instead of hearing that Jobber would not answer.
|
|
32
|
+
raise Error, error.message
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def throttle = @throttle ||= Throttle.new
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/jbr/job.rb
CHANGED
|
@@ -10,11 +10,11 @@ module Jbr
|
|
|
10
10
|
def instructions = @node['instructions']
|
|
11
11
|
|
|
12
12
|
# What the job's lines add up to, each as how many of what: `3 Faucet install and 2 Valve
|
|
13
|
-
# change
|
|
14
|
-
#
|
|
15
|
-
# where the query never asked for them.
|
|
16
|
-
# @return [String] the lines as a sentence, or the title, or the ID.
|
|
17
|
-
def summary = line_items.
|
|
13
|
+
# change` — `to_sentence` reading each line's own string form. The lines say what the work
|
|
14
|
+
# was where a title only says what it was called, so this reads better than one, and falls
|
|
15
|
+
# back to {#name} where the job has no lines or the query never asked for them.
|
|
16
|
+
# @return [String] the lines as a sentence, or the title, or the ID. Never nil, never empty.
|
|
17
|
+
def summary = line_items.to_sentence.presence || name
|
|
18
18
|
|
|
19
19
|
# @return [String, nil] where Jobber files the job in its own workflow.
|
|
20
20
|
def status = @node['jobStatus']
|
data/lib/jbr/line_item.rb
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
module Jbr
|
|
2
|
-
# One line of the work a job is made of: how many of a thing, what it is called
|
|
3
|
-
# doing it involves.
|
|
2
|
+
# One line of the work a job is made of: how many of a thing, and what it is called.
|
|
4
3
|
class LineItem < Resource
|
|
5
4
|
# What Jobber calls each field of a line.
|
|
6
|
-
FIELDS = %w[quantity name
|
|
5
|
+
FIELDS = %w[quantity name]
|
|
7
6
|
|
|
8
7
|
# The most lines to read off one record. Bounded because Jobber prices a connection by the
|
|
9
8
|
# page it is asked for and prices an unbounded one at its own maximum, so the lines of a
|
|
@@ -23,16 +22,9 @@ module Jbr
|
|
|
23
22
|
# @return [String, nil] what the line is called.
|
|
24
23
|
def name = @node['name']
|
|
25
24
|
|
|
26
|
-
# @return [String, nil] what doing it involves, in whoever wrote the line's own words.
|
|
27
|
-
def description = @node['description']
|
|
28
|
-
|
|
29
25
|
# @return [String] how many of what: `3 Bathroom Faucet Installation`, and the name alone
|
|
30
26
|
# where Jobber holds no quantity for the line.
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
-
# @return [String] how a line reads: `3 Faucet install (Fits a new faucet)`, and without
|
|
34
|
-
# the parenthesis where nobody described it.
|
|
35
|
-
def to_s = [ quantified, described ].compact.join ' '
|
|
27
|
+
def to_s = [ quantity, name ].compact.join ' '
|
|
36
28
|
|
|
37
29
|
private
|
|
38
30
|
|
|
@@ -40,7 +32,5 @@ module Jbr
|
|
|
40
32
|
# `3 Faucets` rather than `3.0 Faucets`. A fraction keeps its point — `3.5 Faucets` —
|
|
41
33
|
# since rounding it would lie about what was billed.
|
|
42
34
|
def whole(number) = number && ((number % 1).zero? ? number.to_i : number)
|
|
43
|
-
|
|
44
|
-
def described = ("(#{description})" if description.present?)
|
|
45
35
|
end
|
|
46
36
|
end
|
data/lib/jbr/mock/line_item.rb
CHANGED
data/lib/jbr/oauth.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module Jbr
|
|
2
2
|
# Credentials for one Jobber account, and the gateway to all they read or write.
|
|
3
3
|
class OAuth
|
|
4
|
+
include Asking
|
|
5
|
+
|
|
4
6
|
# The mutation that revokes the app on the account.
|
|
5
7
|
DISCONNECT_MUTATION = <<~GRAPHQL
|
|
6
8
|
mutation Disconnect {
|
|
@@ -34,20 +36,6 @@ module Jbr
|
|
|
34
36
|
def requests = Request.new oauth: self
|
|
35
37
|
def visits = Visits.new oauth: self
|
|
36
38
|
|
|
37
|
-
# Run a statement, waiting for what Jobber will still answer and refreshing a stale token.
|
|
38
|
-
# @return [Hash] the data Jobber answered, or empty when the credentials are dead.
|
|
39
|
-
def query(statement, variables: {})
|
|
40
|
-
throttle.wait
|
|
41
|
-
client.query(statement, variables: variables) { |extensions| throttle.read extensions }
|
|
42
|
-
rescue GraphQL::Unauthorized
|
|
43
|
-
refresh ? retry : {}
|
|
44
|
-
rescue GraphQL::Error => error
|
|
45
|
-
# The transport's own class never leaves the gem: a caller told to rescue `Jbr::Error`
|
|
46
|
-
# was not catching a throttle, a 500 or an unreadable answer, and had its own job blow
|
|
47
|
-
# up instead of hearing that Jobber would not answer.
|
|
48
|
-
raise Error, error.message
|
|
49
|
-
end
|
|
50
|
-
|
|
51
39
|
# Delete a token. If the token is invalid, do nothing.
|
|
52
40
|
def delete
|
|
53
41
|
client.query DISCONNECT_MUTATION
|
|
@@ -76,8 +64,6 @@ module Jbr
|
|
|
76
64
|
|
|
77
65
|
private
|
|
78
66
|
|
|
79
|
-
def throttle = @throttle ||= Throttle.new
|
|
80
|
-
|
|
81
67
|
def refresh
|
|
82
68
|
output = self.class.post refresh_token: @refresh_token, grant_type: 'refresh_token'
|
|
83
69
|
@access_token = output[:access_token]
|
data/lib/jbr/throttle.rb
CHANGED
|
@@ -25,9 +25,17 @@ module Jbr
|
|
|
25
25
|
# and only an answered one reports an actual.
|
|
26
26
|
@cost = (cost['actualQueryCost'] || cost['requestedQueryCost']).to_f
|
|
27
27
|
@available = status['currentlyAvailable'].to_f
|
|
28
|
+
@maximum = status['maximumAvailable'].to_f
|
|
28
29
|
@restore_rate = status['restoreRate'].to_f
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
# Whether the bucket could ever pay for the query it last priced. A refusal is worth
|
|
33
|
+
# waiting out where it could: the shortfall refills and the same query goes through. Where
|
|
34
|
+
# the query costs more than the bucket ever holds, no wait will do — and where Jobber said
|
|
35
|
+
# nothing about the ceiling, one attempt at waiting is cheaper than assuming the worst.
|
|
36
|
+
# @return [Boolean] false only where the ceiling is known and the query is over it.
|
|
37
|
+
def affordable? = !@maximum.to_f.positive? || @cost.to_f <= @maximum
|
|
38
|
+
|
|
31
39
|
private
|
|
32
40
|
|
|
33
41
|
# Nothing is owed to the first caller: a request that follows no other is not too soon.
|
data/lib/jbr/version.rb
CHANGED
data/lib/jbr.rb
CHANGED
|
@@ -11,6 +11,7 @@ require 'active_support/core_ext/array/conversions'
|
|
|
11
11
|
|
|
12
12
|
require 'graphql/error'
|
|
13
13
|
require 'graphql/unauthorized'
|
|
14
|
+
require 'graphql/throttled'
|
|
14
15
|
require 'graphql/client'
|
|
15
16
|
|
|
16
17
|
require 'jbr/mock'
|
|
@@ -26,6 +27,7 @@ require 'jbr/cliental'
|
|
|
26
27
|
require 'jbr/named'
|
|
27
28
|
require 'jbr/resource'
|
|
28
29
|
require 'jbr/throttle'
|
|
30
|
+
require 'jbr/asking'
|
|
29
31
|
require 'jbr/request'
|
|
30
32
|
require 'jbr/oauth'
|
|
31
33
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jbr
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Claudio Baccigalupo
|
|
@@ -106,9 +106,11 @@ files:
|
|
|
106
106
|
- README.md
|
|
107
107
|
- lib/graphql/client.rb
|
|
108
108
|
- lib/graphql/error.rb
|
|
109
|
+
- lib/graphql/throttled.rb
|
|
109
110
|
- lib/graphql/unauthorized.rb
|
|
110
111
|
- lib/jbr.rb
|
|
111
112
|
- lib/jbr/account.rb
|
|
113
|
+
- lib/jbr/asking.rb
|
|
112
114
|
- lib/jbr/client.rb
|
|
113
115
|
- lib/jbr/cliental.rb
|
|
114
116
|
- lib/jbr/error.rb
|