brapi-ruby-sdk 0.3.0 → 0.4.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: 05b2bc6fb4061903f31d59574c62333d8410d7228d3e9b1664a3efc04e024eee
4
- data.tar.gz: 299938eb28c85c3b53d0c56e78f40f3ff552bc662b513ea1bb2db051db3d35f6
3
+ metadata.gz: 2f184be4deccafcbafd23f9275a7801290357e72c13d78eee0d5604aaa60edd2
4
+ data.tar.gz: f3e70b267473bc7dc2222463a7f175578b8ad3099657dccddf71571237e884e7
5
5
  SHA512:
6
- metadata.gz: 2aeaa5ecd337d64825b59a7ff6617a74c9de2f8b14a0d83891f23c51ca79a5c7dec5918da537604d43fe13ee23eb602c50295b851d83258f1453f71c9ccf6929
7
- data.tar.gz: 10792774a48f8862197fa6abfa230822121d1d1d519d75d0021dbab3597963dd1caf5e7acfd8f3da589e186a8ddfbe18fd65f0330ff6ddae34d190676dbacd9e
6
+ metadata.gz: 116a1de195f79a3b4943ae68ad6f0b9133f66145282e0f124564f2083a5a7b87d5325e65502fcaf059f49b2538bda73a3ef304c0ef9b8ffb8b3746bc41f4c8ad
7
+ data.tar.gz: f6516906c904e17d2732d108f4b989c30bba0a9477144aeeb59db534f640391b1d0937df282e600c6837ff39899bb52eb0f51eecebbcea7ea8b19b4993684b1b
data/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-05-28
11
+
12
+ ### Added
13
+
14
+ - **Auto-pagination** on every paginated resource. Three new public methods
15
+ show up on `client.quote`, `client.v2.fii` and `client.v2.treasury`:
16
+ - `#each_page { |page| ... }` — yields one full page response per iteration
17
+ until the upstream signals `has_next_page = false` (or after `max_pages:`
18
+ iterations, whichever comes first).
19
+ - `#each { |item| ... }` — auto-flattens across pages, yielding each row
20
+ (FII, bond or QuoteListItem) individually.
21
+ - Without a block, both return an `Enumerator` so the full Ruby
22
+ Enumerable surface is available: `client.v2.fii.first(10)`,
23
+ `client.v2.fii.select { |f| f.dividend_yield12m > 0.1 }`,
24
+ `client.v2.treasury.lazy.find { |b| b.indexer == "ipca" }`, etc.
25
+ - `Brapi::Resources::Paginated` mixin (declarative `paginates items: :fiis`
26
+ in the resource class) ready to be reused by any future paginated
27
+ endpoint.
28
+ - `max_pages:` keyword on `#each` / `#each_page` (default `10_000`)
29
+ protects against runaway loops if a buggy upstream forgets to flip
30
+ `has_next_page = false`.
31
+ - Resources are now `Enumerable`, so the standard `map` / `select` /
32
+ `take` / `find` / `lazy` / `count` methods Just Work on paginated
33
+ endpoints.
34
+
10
35
  ## [0.3.0] - 2026-05-28
11
36
 
12
37
  ### Added
data/README.md CHANGED
@@ -220,6 +220,47 @@ resp.stocks.each { |s| puts "#{s.stock} (#{s.name}): R$ #{s.close}" }
220
220
  puts "Page #{resp.current_page}/#{resp.total_pages}"
221
221
  ```
222
222
 
223
+ ### Auto-pagination
224
+
225
+ Every paginated resource (`client.quote.list`, `client.v2.fii.list`,
226
+ `client.v2.treasury.list`) gains `#each_page`, `#each` and the full
227
+ Ruby `Enumerable` surface:
228
+
229
+ ```ruby
230
+ # Yield each row across all pages — lazy by default, stops when
231
+ # the API reports has_next_page = false.
232
+ Brapi.v2.fii.each { |f| puts "#{f.symbol}: DY12m=#{f.dividend_yield12m}" }
233
+
234
+ # Or yield one page at a time:
235
+ Brapi.v2.treasury.each_page(max_pages: 5) do |page|
236
+ puts "Page #{page.pagination.page}/#{page.pagination.total_pages}"
237
+ end
238
+
239
+ # Enumerable methods Just Work — `each` without a block returns
240
+ # an Enumerator, so you can use first/select/lazy/etc.:
241
+ top_yield = Brapi.v2.fii
242
+ .first(100)
243
+ .sort_by { |f| -(f.dividend_yield12m || 0) }
244
+ .first(5)
245
+
246
+ ipca_bonds = Brapi.v2.treasury
247
+ .lazy
248
+ .select { |b| b.indexer == "ipca" }
249
+ .first(10)
250
+
251
+ # Custom params pass through to the underlying `list` call:
252
+ Brapi.quote.each(sort_by: "volume", sort_order: "desc") { |s| ... }
253
+ ```
254
+
255
+ `max_pages:` (default `10_000`) caps the walk in case the upstream
256
+ forgets to signal the end. `page:` lets you start from a specific page.
257
+
258
+ `count` and `size` take a fast path: they fetch only the first page and
259
+ read `pagination.total_items` (or `item_count` on Quote), so
260
+ `Brapi.v2.fii.count` is one HTTP call, not 81. Pass a block to `count`
261
+ when you want Enumerable filtering semantics — that still walks every
262
+ page, as expected.
263
+
223
264
  ## Error handling
224
265
 
225
266
  All errors inherit from `Brapi::Error`:
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brapi
4
+ module Resources
5
+ # Adds auto-pagination helpers (`#each_page`, `#each`, plus a fast
6
+ # `#count` / `#size`) to a Resource around an existing list-style method.
7
+ #
8
+ # Usage:
9
+ #
10
+ # class Brapi::Resources::V2::Fii < Brapi::Resource
11
+ # include Brapi::Resources::Paginated
12
+ # paginates items: :fiis
13
+ #
14
+ # def list(**params)
15
+ # # ...
16
+ # end
17
+ # end
18
+ #
19
+ # ## Requirements on the host
20
+ #
21
+ # The list-style method named by `via:` (default `:list`) must accept a
22
+ # `page:` keyword argument. The mixin sets this on every iteration to walk
23
+ # through pages; if the host's `list` rejects `page:` the first iteration
24
+ # will raise `ArgumentError`.
25
+ #
26
+ # ## Pagination shapes
27
+ #
28
+ # Two shapes are supported:
29
+ #
30
+ # 1. **Nested** (FII / Treasury): the response has a `pagination` sub-object
31
+ # with `page`, `has_next_page`, `total_items`. This is the default.
32
+ # 2. **Flat** (Quote#list): the response exposes `current_page` /
33
+ # `has_next_page` / `item_count` directly. Pass `has_next:`, `next_page:`
34
+ # and `count_from:` lambdas to override the readers.
35
+ #
36
+ # ## Safety
37
+ #
38
+ # `max_pages:` (default `DEFAULT_MAX_PAGES`) caps any walk — protects
39
+ # against runaway loops if the upstream forgets to set
40
+ # `has_next_page = false`. The cap is checked **before** each fetch so
41
+ # `max_pages: 0` yields nothing.
42
+ #
43
+ # ## #count behaviour
44
+ #
45
+ # When called with no args and no block, `#count` fetches **only the first
46
+ # page** and reads `pagination.total_items` (or whatever `count_from:`
47
+ # returns), avoiding a full walk. When called with an item or a block, it
48
+ # delegates to the standard `Enumerable#count` (which walks every page).
49
+ module Paginated
50
+ DEFAULT_MAX_PAGES = 10_000
51
+
52
+ def self.included(base)
53
+ base.include(Enumerable)
54
+ base.extend(ClassMethods)
55
+ end
56
+
57
+ module ClassMethods
58
+ def paginates(items:, via: :list, has_next: nil, next_page: nil, count_from: nil)
59
+ has_next ||= ->(resp) { resp.pagination&.has_next_page }
60
+ next_page ||= ->(resp) { (resp.pagination&.page || 0) + 1 }
61
+ count_from ||= ->(resp) { resp.pagination&.total_items }
62
+
63
+ define_each_page(via: via, has_next: has_next, next_page: next_page)
64
+ define_each(items: items)
65
+ define_count(via: via, count_from: count_from)
66
+ end
67
+
68
+ private
69
+
70
+ def define_each_page(via:, has_next:, next_page:)
71
+ define_method(:each_page) do |max_pages: Brapi::Resources::Paginated::DEFAULT_MAX_PAGES, **params, &block|
72
+ return enum_for(:each_page, max_pages: max_pages, **params) unless block
73
+
74
+ current = params.delete(:page) || 1
75
+ pages_seen = 0
76
+ loop do
77
+ break if pages_seen >= max_pages
78
+
79
+ resp = public_send(via, **params, page: current)
80
+ block.call(resp)
81
+ pages_seen += 1
82
+ break unless has_next.call(resp)
83
+
84
+ current = next_page.call(resp)
85
+ end
86
+ end
87
+ end
88
+
89
+ def define_each(items:)
90
+ define_method(:each) do |max_pages: Brapi::Resources::Paginated::DEFAULT_MAX_PAGES, **params, &block|
91
+ return enum_for(:each, max_pages: max_pages, **params) unless block
92
+
93
+ each_page(max_pages: max_pages, **params) do |resp|
94
+ resp.public_send(items).each { |item| block.call(item) }
95
+ end
96
+ end
97
+ end
98
+
99
+ def define_count(via:, count_from:)
100
+ define_method(:count) do |*args, &block|
101
+ # count(item) and count { block } use Enumerable's filtering
102
+ # semantics — fall back to the standard walk.
103
+ return super(*args, &block) unless args.empty? && block.nil?
104
+
105
+ total = count_from.call(public_send(via, page: 1))
106
+ total.nil? ? super(*args, &block) : total
107
+ end
108
+
109
+ define_method(:size) { count }
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -5,6 +5,15 @@ require "cgi"
5
5
  module Brapi
6
6
  module Resources
7
7
  class Quote < Brapi::Resource
8
+ include Brapi::Resources::Paginated
9
+
10
+ # rubocop:disable Style/SymbolProc -- arrow-form keeps the three lambdas visually aligned
11
+ paginates items: :stocks,
12
+ has_next: ->(r) { r.has_next_page },
13
+ next_page: ->(r) { (r.current_page || 0) + 1 },
14
+ count_from: ->(r) { r.item_count }
15
+ # rubocop:enable Style/SymbolProc
16
+
8
17
  # GET /api/quote/{tickers}
9
18
  def retrieve(tickers, **params)
10
19
  tickers_str = Array(tickers).join(",")
@@ -4,6 +4,10 @@ module Brapi
4
4
  module Resources
5
5
  class V2
6
6
  class Fii < Brapi::Resource
7
+ include Brapi::Resources::Paginated
8
+
9
+ paginates items: :fiis
10
+
7
11
  # GET /api/v2/fii/list
8
12
  def list(**params)
9
13
  raw = get("/api/v2/fii/list", params: params)
@@ -4,6 +4,10 @@ module Brapi
4
4
  module Resources
5
5
  class V2
6
6
  class Treasury < Brapi::Resource
7
+ include Brapi::Resources::Paginated
8
+
9
+ paginates items: :results
10
+
7
11
  # GET /api/v2/treasury/list
8
12
  def list(**params)
9
13
  raw = get("/api/v2/treasury/list", params: params)
data/lib/brapi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Brapi
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/brapi.rb CHANGED
@@ -63,7 +63,9 @@ require "brapi/models/v2/treasury_bond"
63
63
  require "brapi/models/v2/treasury_list_response"
64
64
  require "brapi/models/v2/treasury_indicators_response"
65
65
 
66
- # Resources — v2 (parent class) must load before its nested classes
66
+ # Resources — v2 (parent class) must load before its nested classes,
67
+ # and Paginated must load before any resource that includes it.
68
+ require "brapi/resources/paginated"
67
69
  require "brapi/resources/quote"
68
70
  require "brapi/resources/available"
69
71
  require "brapi/resources/v2"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brapi-ruby-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rômulo Storel
@@ -102,6 +102,7 @@ files:
102
102
  - lib/brapi/models/value_added_entry.rb
103
103
  - lib/brapi/resource.rb
104
104
  - lib/brapi/resources/available.rb
105
+ - lib/brapi/resources/paginated.rb
105
106
  - lib/brapi/resources/quote.rb
106
107
  - lib/brapi/resources/v2.rb
107
108
  - lib/brapi/resources/v2/crypto.rb