jbr 3.7.2 → 3.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e017275cf5a3a41647f3817608327ddf8fa909c2fdf25e1f8c2a43ee1ad3b044
4
- data.tar.gz: 1d01c835b75bd16cd21b8d4e658644520cc065c0659b1f394f105e02775b8f69
3
+ metadata.gz: 815b57ea81f9bda41efb8b7d09a0767224763b895126a3ee56acabab6266b3fc
4
+ data.tar.gz: 127a174761ddb6bc0cc00cb34ffd9e49044152d575f3f5e4f42a030f222023a5
5
5
  SHA512:
6
- metadata.gz: 89e55dd309b839918c67d79ee4c761d28cacb3f2c0dba30daa996354b0a22c4436aa52928dbfb7208df62609073157f77e42f7349943f4f58a2910dea1f43f00
7
- data.tar.gz: 2cd3bf9d534e4fbb349debe3643aad4a62982e5780e27e558214594ffe9d536413bc5e8d837976d832422011b5da9be146bd9998492f6c33d99011274c213ef5
6
+ metadata.gz: 3be4d269a045c6d9e0629a27922697f93c102259eee21762fcef02b0e5257f1b4ebd05e56812faa47e41b68a94ea440624c02621eaa95a4449db42dda94d62f4
7
+ data.tar.gz: d34a6393d787cb20628cf4a18e0c63ffe63bdecaa3cf00e7e84016f8423fca125567549cda5ced525770b07d6354103be476c00d54b21b936e6769a728efe8b8
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [3.8.0] - 2026-08-18
2
+
3
+ - [New] `Jbr::Retriable`, a `Jbr::Error` for a query Jobber refused over what it costs rather
4
+ than over anything about the query. Every refusal read alike before, so an app had nothing to
5
+ branch on: the one worth asking again a moment later looked exactly like the one that will
6
+ say the same thing forever. It carries what Jobber reported it with — `cost`, `available`,
7
+ `maximum` and `restore_rate` — so a caller can tell a bucket that needed a second from a
8
+ query too big to ever fit in it
9
+
1
10
  ## [3.7.2] - 2026-08-17
2
11
 
3
12
  - [Change] Jobber's token endpoint answers in prose, so that is all this reads. 3.7.1 kept a
data/README.md CHANGED
@@ -113,13 +113,14 @@ job.completed_at # => 2026-05-18 11:36:13
113
113
 
114
114
  Or walk the account's jobs, oldest first. Jobber is asked for a page at a time, and only
115
115
  once the page before it runs out, so `first` costs one request where `to_a` costs as many
116
- as the account has pages. A walk paces itself, so a long one is never refused: see
117
- [Rate limits](#rate-limits).
116
+ as the account has pages. A walk is priced by what its pages carry, and a long one can be
117
+ refused for it: see [Rate limits](#rate-limits).
118
118
 
119
119
  ```ruby
120
120
  jobs = oauth.jobs # => an Enumerable of every job, nothing fetched yet
121
- oauth.jobs.past # => an Enumerator of the ones dated before now
122
- oauth.jobs.upcoming # => an Enumerator of the ones dated from now on
121
+ oauth.jobs.past # => the ones dated before now, nothing fetched yet
122
+ oauth.jobs.upcoming # => the ones dated from now on, nothing fetched yet
123
+ oauth.jobs.past.ids # => %w[Z2lkOi8vS ...], every page of them, and nothing else about them
123
124
 
124
125
  job = jobs.first
125
126
  job.name # => 'Furnace tune-up', or the job's ID where nobody titled it. Never nil or empty
@@ -175,8 +176,9 @@ Walk the account's visits, oldest first, the same way as its jobs:
175
176
 
176
177
  ```ruby
177
178
  visits = oauth.visits # => an Enumerable of every visit, nothing fetched yet
178
- oauth.visits.upcoming # => an Enumerator of the ones dated from now on
179
- oauth.visits.past # => an Enumerator of the ones dated before now
179
+ oauth.visits.upcoming # => the ones dated from now on, nothing fetched yet
180
+ oauth.visits.past # => the ones dated before now, nothing fetched yet
181
+ oauth.visits.upcoming.ids # => %w[Z2lkOi8vS ...], every page of them, and nothing else
180
182
 
181
183
  visit = visits.first
182
184
  visit.id # => 'Z2lkOi8vS'
@@ -228,6 +230,16 @@ what happened, so the caller can decide:
228
230
  Throttled (cost 1885, 1254 of 10000 available, restoring 500/s)
229
231
  ```
230
232
 
233
+ That one is a `Jbr::Retriable`, a `Jbr::Error` for a refusal worth asking again, carrying the
234
+ numbers to decide with:
235
+
236
+ ```ruby
237
+ error.cost # => 1885, what the query was priced at
238
+ error.available # => 1254, what the bucket held when it was asked
239
+ error.maximum # => 10000, what the bucket holds when full — a cost above it never fits
240
+ error.restore_rate # => 500, points a second
241
+ ```
242
+
231
243
  That arrives as a `Jbr::Error`. 631 points short of a query the bucket holds five times over,
232
244
  which a second would have refilled — worth asking again. A cost above `maximumAvailable` is
233
245
  worth nothing but a smaller query. Either way the decision belongs to whoever called: from a
@@ -237,6 +249,15 @@ holding a transaction open.
237
249
  Every connection the gem asks for is bounded, to keep a query on the affordable side of that:
238
250
  twenty lines to a job, and twenty jobs or visits to a page.
239
251
 
252
+ `ids` is the cheap way to walk an account. It asks for the ID and nothing else, which prices
253
+ a row at a fraction of a record and buys a hundred of them to a page — so five times the
254
+ account is read for a fraction of the budget. Reach for it where each record is then read on
255
+ its own with `find`, one background job at a time:
256
+
257
+ ```ruby
258
+ oauth.jobs.past.ids.each { |id| ImportJob.perform_later id }
259
+ ```
260
+
240
261
  ### Events
241
262
 
242
263
  Parse the payload of a Jobber event webhook:
@@ -22,12 +22,23 @@ module GraphQL
22
22
  raise Unauthorized, response.body if response.code == '401'
23
23
  raise Error, response.body unless response.is_a? Net::HTTPSuccess
24
24
  body = JSON.parse(response.body)
25
- raise Error, refusal(body) if body['errors'].present?
25
+ raise refusal_for(body) if body['errors'].present?
26
26
  body.fetch('data')
27
27
  end
28
28
 
29
29
  private
30
30
 
31
+ # Refused over cost where the endpoint names the code for it, or prices the query above
32
+ # what it says was left. Anything else is a refusal of the query itself.
33
+ def refusal_for(body)
34
+ cost = body['extensions'].to_h['cost'].to_h
35
+ available = cost['throttleStatus'].to_h['currentlyAvailable']
36
+ coded = body['errors'].any? { |error| error.to_h.dig('extensions', 'code') == 'THROTTLED' }
37
+ priced = available && cost['requestedQueryCost'].to_f > available.to_f
38
+
39
+ coded || priced ? Throttled.new(refusal(body), cost) : Error.new(refusal(body))
40
+ end
41
+
31
42
  # What the endpoint refused, and — where it priced the refusal — what the query would have
32
43
  # cost against what was available. `Throttled` on its own leaves a caller unable to tell a
33
44
  # query too big to ever run from a bucket that a moment would have refilled.
@@ -0,0 +1,16 @@
1
+ module GraphQL
2
+ # An endpoint refusing a query for what it costs rather than for anything about the query
3
+ # itself. Worth telling apart: the same query is answered once the budget it is priced
4
+ # against has refilled.
5
+ class Throttled < Error
6
+ # @param message [String] what the endpoint said, with the numbers it said it with.
7
+ # @param cost [Hash] what it priced the query at, and the budget it priced it against.
8
+ def initialize(message, cost = {})
9
+ super message
10
+ @cost = cost
11
+ end
12
+
13
+ # @return [Hash] those numbers, in the endpoint's own words.
14
+ attr_reader :cost
15
+ end
16
+ end
data/lib/jbr/jobs.rb CHANGED
@@ -1,23 +1,13 @@
1
1
  module Jbr
2
2
  # The jobs on a Jobber account, oldest first, walked a page at a time.
3
3
  class Jobs < Resource
4
- include Enumerable, Includable
4
+ include Includable, Listable
5
5
 
6
6
  # What a job answers with wherever one is read, before anything it was asked to bring
7
7
  # back with it.
8
8
  FIELDS = 'id title instructions jobStatus total createdAt startAt completedAt ' \
9
9
  'quote { id amounts { total } }'
10
10
 
11
- # Every job on the account, past and future alike. Nothing is read until the walk starts,
12
- # and a page is read only once the one before it runs out.
13
- def each(&) = walk.each(&)
14
-
15
- # @return [Enumerator<Job>] the jobs scheduled from now on.
16
- def upcoming = walk from_now
17
-
18
- # @return [Enumerator<Job>] the jobs that started before now.
19
- def past = walk until_now
20
-
21
11
  # Shadows Enumerable#find on purpose, the way Active Record does: a job is reached by the
22
12
  # ID Jobber files it under, not by asking every job on the account whether it is the one.
23
13
  # @param id [String] the Jobber ID of the job.
@@ -29,20 +19,7 @@ module Jbr
29
19
 
30
20
  private
31
21
 
32
- # Twenty a page, not forty and not a hundred: Jobber prices a query by its page size, and
33
- # what an includes brings back is charged for on top of every row of it — so a page of jobs
34
- # carrying their lines, their property and its client priced past what a bucket holds. Half
35
- # the page costs half the query and loses nothing, since a walk simply reads more pages.
36
- def page
37
- <<~GRAPHQL
38
- query($after: String, $filter: JobFilterAttributes) {
39
- jobs(first: 20, after: $after, filter: $filter) {
40
- nodes { #{FIELDS} #{selections} }
41
- pageInfo { hasNextPage endCursor }
42
- }
43
- }
44
- GRAPHQL
45
- end
22
+ def page = paged "#{FIELDS} #{selections}", PAGE
46
23
 
47
24
  def one
48
25
  <<~GRAPHQL
@@ -54,6 +31,8 @@ module Jbr
54
31
 
55
32
  def field = 'jobs'
56
33
 
34
+ def filtered = 'JobFilterAttributes'
35
+
57
36
  def item(node) = Job.new node: node
58
37
  end
59
38
  end
@@ -0,0 +1,73 @@
1
+ module Jbr
2
+ # What a list of records answers, and how much of an account it costs to ask. Jobber prices
3
+ # a query by the page it asks for and by what every row of that page carries, so a list is
4
+ # read either as records or as the IDs alone, each with a page sized to what it carries.
5
+ module Listable
6
+ include Enumerable
7
+
8
+ # Records a page, not forty and not a hundred: what an includes brings back is charged for
9
+ # on top of every row of it, so a page of jobs carrying their lines, their property and its
10
+ # client priced past what a bucket holds. Half the page costs half the query and loses
11
+ # nothing, since a walk simply reads more pages.
12
+ PAGE = 20
13
+
14
+ # IDs a page, which the same budget affords five times over where a row carries one field
15
+ # and no nesting at all.
16
+ IDS_PAGE = 100
17
+
18
+ # Every record the list is narrowed to, oldest first. Nothing is read until the walk
19
+ # starts, and a page is read only once the one before it runs out.
20
+ def each(&) = walk(page).each(&)
21
+
22
+ # @return [Listable] the same list, narrowed to what is scheduled from now on.
23
+ def upcoming = narrowed from_now
24
+
25
+ # @return [Listable] the same list, narrowed to what started before now.
26
+ def past = narrowed until_now
27
+
28
+ # The ID Jobber files each record under, and nothing else about it: the cheapest question
29
+ # an account can be walked with, and the one to ask where every record is then read on its
30
+ # own through {#find}.
31
+ # @return [Array<String>] every ID in the list, every page of them read.
32
+ def ids = walk(ids_page).map(&:id)
33
+
34
+ private
35
+
36
+ def narrowed(filter) = self.class.new(oauth: @oauth, includes: @includes, filter: filter)
37
+
38
+ # The two halves of a schedule, split at the moment they are asked for rather than per
39
+ # page: read page by page the boundary would slide, and something could cross it unseen.
40
+ def from_now = { startAt: { after: Time.now.iso8601 } }
41
+
42
+ def until_now = { startAt: { before: Time.now.iso8601 } }
43
+
44
+ # Every record a paged query answers, one at a time, a page read only once the one before
45
+ # it runs out. The filter is data: narrowed to nothing, the query narrows nothing.
46
+ def walk(statement)
47
+ Enumerator.new do |yielder|
48
+ after = nil
49
+ loop do
50
+ answered = @oauth.query statement, variables: { after: after, filter: @filter }.compact
51
+ current = answered.fetch field, {}
52
+ current.fetch('nodes', []).each { |node| yielder << item(node) }
53
+ break unless current.dig 'pageInfo', 'hasNextPage'
54
+
55
+ after = current.dig 'pageInfo', 'endCursor'
56
+ end
57
+ end
58
+ end
59
+
60
+ def ids_page = paged 'id', IDS_PAGE
61
+
62
+ def paged(fields, size)
63
+ <<~GRAPHQL
64
+ query($after: String, $filter: #{filtered}) {
65
+ #{field}(first: #{size}, after: $after, filter: $filter) {
66
+ nodes { #{fields} }
67
+ pageInfo { hasNextPage endCursor }
68
+ }
69
+ }
70
+ GRAPHQL
71
+ end
72
+ end
73
+ end
data/lib/jbr/mock/jobs.rb CHANGED
@@ -1,24 +1,25 @@
1
1
  module Jbr
2
- # The jobs an app under test asked {Jbr.mock} to answer with.
2
+ # The jobs an app under test asked {Jbr.mock} to answer with. Only the walk is mocked:
3
+ # narrowing a list, and reading it as records or as IDs, is the same code a real one runs.
3
4
  class Mock::Jobs < Jobs
4
5
  # The job filed under that ID where the app listed one, and otherwise the single job it
5
6
  # named — which is every app that mocks a lookup without mocking a list.
6
7
  # @return [Mock::Job] the job asked for.
7
8
  def find(id) = Mock::Job.new node: listed(id) || Jbr.mock.job
8
9
 
9
- # @return [Enumerator<Mock::Job>] every job the app named.
10
- def each(&) = mocked(Jbr.mock.jobs).each(&)
11
-
12
- # @return [Enumerator<Mock::Job>] those it dated from now on, and any it left undated.
13
- def upcoming = mocked Jbr.mock.jobs.reject { |job| started? job }
10
+ private
14
11
 
15
- # @return [Enumerator<Mock::Job>] those it dated before now.
16
- def past = mocked Jbr.mock.jobs.select { |job| started? job }
12
+ def walk(_statement)
13
+ Enumerator.new { |yielder| selected.each { |job| yielder << Mock::Job.new(node: job) } }
14
+ end
17
15
 
18
- private
16
+ # The half of the schedule the list was narrowed to, from what the app dated each job.
17
+ # One it left undated is one nothing has started, so it counts as upcoming.
18
+ def selected
19
+ return Jbr.mock.jobs unless @filter
19
20
 
20
- def mocked(jobs)
21
- Enumerator.new { |yielder| jobs.each { |job| yielder << Mock::Job.new(node: job) } }
21
+ started = @filter.dig(:startAt, :before).present?
22
+ Jbr.mock.jobs.select { |job| started?(job) == started }
22
23
  end
23
24
 
24
25
  def started?(job) = job[:scheduled_at] ? job[:scheduled_at] <= Time.now : false
@@ -1,22 +1,23 @@
1
1
  module Jbr
2
- # The visits an app under test asked {Jbr.mock} to answer with.
2
+ # The visits an app under test asked {Jbr.mock} to answer with. Only the walk is mocked:
3
+ # narrowing a list, and reading it as records or as IDs, is the same code a real one runs.
3
4
  class Mock::Visits < Visits
4
- # @return [Enumerator<Mock::Visit>] every visit the app named.
5
- def each(&) = mocked(Jbr.mock.visits).each(&)
6
-
7
- # @return [Enumerator<Mock::Visit>] those it dated from now on, and any it left undated.
8
- def upcoming = mocked Jbr.mock.visits.reject { |visit| started? visit }
9
-
10
- # @return [Enumerator<Mock::Visit>] those it dated before now.
11
- def past = mocked Jbr.mock.visits.select { |visit| started? visit }
12
-
13
5
  # @return [Mock::Visit] the visit the app listed under that ID.
14
6
  def find(id) = Mock::Visit.new node: listed(id)
15
7
 
16
8
  private
17
9
 
18
- def mocked(visits)
19
- Enumerator.new { |yielder| visits.each { |visit| yielder << Mock::Visit.new(node: visit) } }
10
+ def walk(_statement)
11
+ Enumerator.new { |yielder| selected.each { |it| yielder << Mock::Visit.new(node: it) } }
12
+ end
13
+
14
+ # The half of the schedule the list was narrowed to, from what the app dated each visit.
15
+ # One it left undated is one nothing has started, so it counts as upcoming.
16
+ def selected
17
+ return Jbr.mock.visits unless @filter
18
+
19
+ started = @filter.dig(:startAt, :before).present?
20
+ Jbr.mock.visits.select { |visit| started?(visit) == started }
20
21
  end
21
22
 
22
23
  def started?(visit) = visit[:starts_at] ? visit[:starts_at] <= Time.now : false
data/lib/jbr/oauth.rb CHANGED
@@ -47,6 +47,8 @@ module Jbr
47
47
  client.query statement, variables: variables
48
48
  rescue GraphQL::Unauthorized
49
49
  refresh ? retry : {}
50
+ rescue GraphQL::Throttled => error
51
+ raise Retriable.new(error.message, error.cost)
50
52
  rescue GraphQL::Error => error
51
53
  # The transport's own class never leaves the gem: a caller told to rescue `Jbr::Error`
52
54
  # was not catching a throttle, a 500 or an unreadable answer, and had its own job blow
data/lib/jbr/resource.rb CHANGED
@@ -1,15 +1,17 @@
1
1
  module Jbr
2
2
  # What every Jobber resource shares: the node Jobber answered with, the credentials it was
3
- # read through, and the paging that every list of them arrives in.
3
+ # read through, and what a list of them was asked and narrowed to.
4
4
  class Resource
5
5
  # @param oauth [OAuth] the credentials to reach Jobber with, where reaching it is needed.
6
6
  # @param node [Hash] the record as Jobber answered it.
7
7
  # @param includes [Hash] what a list was asked to bring back beside its records.
8
- def initialize(oauth: nil, node: {}, includes: {})
8
+ # @param filter [Hash] what a list was narrowed to, in the shape Jobber filters by.
9
+ def initialize(oauth: nil, node: {}, includes: {}, filter: nil)
9
10
  @oauth = oauth
10
11
  @node = node
11
12
  @id = node['id']
12
13
  @includes = includes
14
+ @filter = filter
13
15
  end
14
16
 
15
17
  # @return [String, nil] the Jobber ID, from the node it came in or once one was created.
@@ -20,27 +22,5 @@ module Jbr
20
22
  # @return [Time, nil] what Jobber answered under a key, as a time. An empty answer is no
21
23
  # answer: Time.iso8601 raises on one, where nothing at all it simply has none of.
22
24
  def time(key) = (Time.iso8601 @node[key] if @node[key].present?)
23
-
24
- # Every item a paged query answers, one at a time, a page read only once the one before
25
- # it runs out. The filter is data: handed none, the query narrows nothing.
26
- def walk(filter = nil)
27
- Enumerator.new do |yielder|
28
- after = nil
29
- loop do
30
- answered = @oauth.query(page, variables: { after: after, filter: filter }.compact)
31
- current = answered.fetch field, {}
32
- current.fetch('nodes', []).each { |node| yielder << item(node) }
33
- break unless current.dig 'pageInfo', 'hasNextPage'
34
-
35
- after = current.dig 'pageInfo', 'endCursor'
36
- end
37
- end
38
- end
39
-
40
- # The two halves of a schedule, split at the moment they are asked for rather than per
41
- # page: read page by page the boundary would slide, and something could cross it unseen.
42
- def from_now = { startAt: { after: Time.now.iso8601 } }
43
-
44
- def until_now = { startAt: { before: Time.now.iso8601 } }
45
25
  end
46
26
  end
@@ -0,0 +1,20 @@
1
+ module Jbr
2
+ # Jobber refusing a query over what it costs rather than over anything about the query. The
3
+ # bucket it is priced against refills, so the same question asked later is answered.
4
+ class Retriable < Error
5
+ # @param message [String] what Jobber said, with the numbers it said it with.
6
+ # @param cost [Hash] the `requestedQueryCost` and the `throttleStatus` beside it.
7
+ def initialize(message, cost = {})
8
+ super message
9
+ status = cost['throttleStatus'].to_h
10
+ @cost = cost['requestedQueryCost']
11
+ @available = status['currentlyAvailable']
12
+ @maximum = status['maximumAvailable']
13
+ @restore_rate = status['restoreRate']
14
+ end
15
+
16
+ # What the query would have cost, the budget it was priced against, and how fast that
17
+ # budget refills. A cost above the maximum is one no waiting will pay for.
18
+ attr_reader :cost, :available, :maximum, :restore_rate
19
+ end
20
+ end
data/lib/jbr/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # A Ruby client for the Jobber API.
2
2
  module Jbr
3
3
  # The version of this gem.
4
- VERSION = '3.7.2'
4
+ VERSION = '3.9.0'
5
5
  end
data/lib/jbr/visits.rb CHANGED
@@ -1,21 +1,11 @@
1
1
  module Jbr
2
2
  # The visits on a Jobber account, oldest first, walked a page at a time.
3
3
  class Visits < Resource
4
- include Enumerable, Includable
4
+ include Includable, Listable
5
5
 
6
6
  # What a visit answers with, before anything it was asked to bring back with it.
7
7
  FIELDS = 'id title startAt endAt allDay clientConfirmed job { id }'
8
8
 
9
- # Every visit on the account, past and future alike. Nothing is read until the walk
10
- # starts, and a page is read only once the one before it runs out.
11
- def each(&) = walk.each(&)
12
-
13
- # @return [Enumerator<Visit>] the visits scheduled from now on.
14
- def upcoming = walk from_now
15
-
16
- # @return [Enumerator<Visit>] the visits that started before now.
17
- def past = walk until_now
18
-
19
9
  # Shadows Enumerable#find on purpose, the way jobs do: a visit is reached by the ID Jobber
20
10
  # files it under, not by asking every visit on the account whether it is the one.
21
11
  # @param id [String] the Jobber ID of the visit.
@@ -27,6 +17,8 @@ module Jbr
27
17
 
28
18
  private
29
19
 
20
+ def page = paged "#{FIELDS} #{selections}", PAGE
21
+
30
22
  def one
31
23
  <<~GRAPHQL
32
24
  query($id: EncodedId!) {
@@ -35,21 +27,10 @@ module Jbr
35
27
  GRAPHQL
36
28
  end
37
29
 
38
- # Twenty a page, the same as jobs: Jobber prices a query by its page size, and what an
39
- # includes brings back is charged for on top of every row of it.
40
- def page
41
- <<~GRAPHQL
42
- query($after: String, $filter: VisitFilterAttributes) {
43
- visits(first: 20, after: $after, filter: $filter) {
44
- nodes { #{FIELDS} #{selections} }
45
- pageInfo { hasNextPage endCursor }
46
- }
47
- }
48
- GRAPHQL
49
- end
50
-
51
30
  def field = 'visits'
52
31
 
32
+ def filtered = 'VisitFilterAttributes'
33
+
53
34
  def item(node) = Visit.new node: node
54
35
  end
55
36
  end
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'
@@ -18,6 +19,7 @@ require 'jbr/mock'
18
19
  require 'jbr/url'
19
20
  require 'jbr/error'
20
21
  require 'jbr/refused'
22
+ require 'jbr/retriable'
21
23
  require 'jbr/token'
22
24
  require 'jbr/refreshing'
23
25
  # Phone before Cliental, and Cliental before the records that include it: what each asks
@@ -34,6 +36,7 @@ require 'jbr/account'
34
36
  require 'jbr/property'
35
37
  require 'jbr/properted'
36
38
  require 'jbr/includable'
39
+ require 'jbr/listable'
37
40
  # LineItem before Itemized, and both before Job: the lines a job is made of are asked for
38
41
  # by a constant the include reads as it loads.
39
42
  require 'jbr/line_item'
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.7.2
4
+ version: 3.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claudio Baccigalupo
@@ -106,6 +106,7 @@ 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
@@ -119,6 +120,7 @@ files:
119
120
  - lib/jbr/job.rb
120
121
  - lib/jbr/jobs.rb
121
122
  - lib/jbr/line_item.rb
123
+ - lib/jbr/listable.rb
122
124
  - lib/jbr/mock.rb
123
125
  - lib/jbr/mock/account.rb
124
126
  - lib/jbr/mock/client.rb
@@ -144,6 +146,7 @@ files:
144
146
  - lib/jbr/refused.rb
145
147
  - lib/jbr/request.rb
146
148
  - lib/jbr/resource.rb
149
+ - lib/jbr/retriable.rb
147
150
  - lib/jbr/token.rb
148
151
  - lib/jbr/url.rb
149
152
  - lib/jbr/version.rb