cbf_calendario 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9f80f797d23bf64dad179c1baf17f12c3bada78eb661eeda597bd121d0567ac
4
- data.tar.gz: d5a7130a8c70bb594e014b0d82f557ab3f87e14025b9cafcec3b6db269844b0d
3
+ metadata.gz: 8df4a89b5bf5374d476138e47f0be488c3034dd67ad858fca6ac15ae842f7181
4
+ data.tar.gz: 1bbb1b57977d00b54ea99ce64a9abf0b6f45f665c20edcf342711ceb94b25a9f
5
5
  SHA512:
6
- metadata.gz: e161e22aaf7d4445367436f72dedc23a16b3fd4bc5adf0fdc53bf79ed818eca3481aa8f6c2a8780a201b5322b0b184ce22c84771d428ffbab0e85e029b7cbb80
7
- data.tar.gz: 403ce1fcccb386a7507e2740df54dd7c008f8088005cba73cc92eb0015bc0177ad36b8f4ffd388ef0fe3b1845b468452c07f4adb0a024d5629c5a645ea8dcfd9
6
+ metadata.gz: b3827b32b1dafa53a360854fb7bbc7e486381fb0dd22e8c03cabb384df89998864374b1ce4f4422d25c6179e6f62fe5e3af80df2c41efb7f73b615c01ecca175
7
+ data.tar.gz: 7225d5da60496872dc09c83aa79b96d2583a80d7a396fc57ec30d4a96b99124a53f2015055e403e266566c451ae3e68f42cdde9afae391d4353fa3992197f861
data/CHANGELOG.md CHANGED
@@ -1,11 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.1] - 2026-05-11
4
+
5
+ - Renomeação: `jogos_pendentes_no_dia` → `jogos_do_dia` em `CbfCalendario` e `CbfCalendario::Client` (sem alias)
6
+
7
+ ## [0.4.0] - 2026-05-11
8
+
9
+ - `Client#jogos_pendentes_no_dia` / `CbfCalendario.jogos_pendentes_no_dia`: passa a listar **todos** os jogos do dia (não só os sem placar na API)
10
+ - Cada linha inclui `placar` (`"M x V"` quando mandante e visitante têm `gols` na API; caso contrário `nil`) além de `horario`
11
+ - README e gemspec atualizados para refletir o novo comportamento
12
+
13
+ ## [0.3.3] - 2026-05-11
14
+
15
+ - Suíte de testes Minitest completa no padrão Rails (`test/`) cobrindo módulo principal, `Client`, `PartidaStats` e `Urls`
16
+ - `Rakefile` com task `rake test` para execução local da suíte
17
+ - CI no GitHub Actions (`.github/workflows/test.yml`) para rodar testes em push/PR nas versões Ruby 3.2, 3.3 e 3.4
18
+ - Dependências de desenvolvimento adicionadas: `rake` e `minitest (~> 5.22)`
19
+
20
+ ## [0.3.2] - 2026-05-11
21
+
22
+ - Ajustes internos de robustez HTTP no cliente
23
+ - Suporte a redirecionamentos HTTP (ex.: 308) nas requisições internas do cliente
24
+ - README atualizado
25
+
26
+ ## [0.3.1] - 2026-05-11
27
+
28
+ - `Client#jogos_pendentes_no_dia`: chave renomeada de `placar_ou_horario` para `horario`
29
+ - README atualizado para refletir o novo formato de retorno dos jogos pendentes
30
+
3
31
  ## [0.3.0] - 2026-05-11
4
32
 
5
- - `Client#atletas_do_clube` e atalho `CbfCalendario.atletas_do_clube`
6
- - `Client#atleta_por_id` e atalho `CbfCalendario.atleta_por_id`
7
- - `Client#clube_por_id` e atalho `CbfCalendario.clube_por_id`
8
- - Novos erros de validação: `InvalidClubIdError` e `InvalidAthleteIdError`
33
+ - Melhorias incrementais no cliente HTTP e na documentação
9
34
  - README simplificado e atualizado com as novas funções e exemplos de retorno
10
35
 
11
36
  ## [0.2.0] - 2026-05-10
data/README.md CHANGED
@@ -4,11 +4,8 @@ Gem Ruby para consultar a API pública da CBF e retornar dados prontos para uso.
4
4
 
5
5
  ## O que a gem faz
6
6
 
7
- - Lista jogos pendentes (sem placar) por dia.
7
+ - Lista todos os jogos do dia (calendário da API).
8
8
  - Busca a partida completa por `id_jogo`.
9
- - Busca atletas por `id_clube`.
10
- - Busca atleta por `id_atleta`.
11
- - Busca clube por `id_clube`.
12
9
  - Gera estatísticas agregadas da súmula.
13
10
  - Monta URLs públicas da CBF (partida e times).
14
11
  - Retorna hashes Ruby para facilitar integração.
@@ -39,20 +36,32 @@ require 'cbf_calendario'
39
36
  ```ruby
40
37
  require 'cbf_calendario'
41
38
 
42
- jogos = CbfCalendario.jogos_pendentes_no_dia('10/05/2026')
39
+ jogos = CbfCalendario.jogos_do_dia('10/05/2026')
43
40
  partida = CbfCalendario.partida_completa('832031')
44
41
  jogo = CbfCalendario.jogo_partida('832031')
45
- atletas = CbfCalendario.atletas_do_clube('20001')
46
- atleta = CbfCalendario.atleta_por_id('12345')
47
- clube = CbfCalendario.clube_por_id('20001')
48
42
  stats = CbfCalendario.estatisticas_agregadas(jogo)
49
43
  ```
50
44
 
45
+ ## `partida_completa` e `jogo_partida`
46
+
47
+ Os dois consultam o mesmo endpoint da CBF: `GET /api/cbf/jogos/:id`. A diferença é **o que é devolvido**:
48
+
49
+ - **`partida_completa(id_jogo)`** — retorna o **payload inteiro** da API (hash com chaves string), em geral no formato `{ "jogo" => { ... } }`. Use quando precisar do JSON completo da resposta (incluindo o envelope com `"jogo"` e qualquer outra chave de topo que a API enviar).
50
+
51
+ - **`jogo_partida(id_jogo)`** — retorna **apenas** o hash interno `"jogo"` (mandante, visitante, `registros`, etc.). É o equivalente a `partida_completa(id_jogo)['jogo']`, útil quando você só trabalha com os dados da partida.
52
+
53
+ | Método | Retorno típico |
54
+ |--------|----------------|
55
+ | `partida_completa` | `{ "jogo" => { ... } }` |
56
+ | `jogo_partida` | `{ "id_jogo" => ..., "mandante" => {...}, ... }` |
57
+
58
+ Se você já chamou `partida = CbfCalendario.partida_completa(id)`, prefira `partida['jogo']` em vez de chamar `jogo_partida(id)` de novo na mesma rotina (evita uma segunda requisição HTTP idêntica).
59
+
51
60
  ## Funcionalidades principais
52
61
 
53
- ### 1) Jogos pendentes por data
62
+ ### 1) Jogos do dia
54
63
 
55
- Retorna apenas partidas ainda sem placar no dia informado.
64
+ Retorna todas as partidas previstas ou já disputadas naquela data, conforme o calendário da CBF. Quando a API já trouxer placar, o campo `placar` vem preenchido (ex.: `"2 x 1"`); caso contrário, `placar` fica `nil` e `horario` reflete o horário previsto, se existir.
56
65
 
57
66
  ### 2) Partida completa por ID
58
67
 
@@ -71,29 +80,21 @@ Monta links da página da partida e das páginas de times.
71
80
  ### Módulo `CbfCalendario` (atalhos)
72
81
 
73
82
  - `CbfCalendario.parse_data_br!(str)`
74
- - `CbfCalendario.jogos_pendentes_no_dia(data, **opts)`
83
+ - `CbfCalendario.jogos_do_dia(data, **opts)`
75
84
  - `CbfCalendario.partida_completa(id_jogo, **opts)`
76
85
  - `CbfCalendario.jogo_partida(id_jogo, **opts)`
77
- - `CbfCalendario.atletas_do_clube(id_clube, **opts)`
78
- - `CbfCalendario.atleta_por_id(id_atleta, **opts)`
79
- - `CbfCalendario.clube_por_id(id_clube, **opts)`
80
86
  - `CbfCalendario.estatisticas_agregadas(jogo)`
81
87
 
82
88
  ### Classe `CbfCalendario::Client`
83
89
 
84
90
  - `CbfCalendario::Client.new(base_url: ..., read_timeout: ..., open_timeout: ...)`
85
- - `client.jogos_pendentes_no_dia(data)`
91
+ - `client.jogos_do_dia(data)`
86
92
  - `client.calendario_json(data)`
87
93
  - `client.partida_completa(id_jogo)`
88
94
  - `client.jogo_partida(id_jogo)`
89
- - `client.atletas_do_clube(id_clube)`
90
- - `client.atleta_por_id(id_atleta)`
91
- - `client.clube_por_id(id_clube)`
92
95
  - `CbfCalendario::Client.parse_data_br!(str)`
93
96
  - `CbfCalendario::Client.coerce_date!(data)`
94
97
  - `CbfCalendario::Client.normalize_id_jogo!(id_jogo)`
95
- - `CbfCalendario::Client.normalize_id_clube!(id_clube)`
96
- - `CbfCalendario::Client.normalize_id_atleta!(id_atleta)`
97
98
 
98
99
  ### Módulo `CbfCalendario::PartidaStats`
99
100
 
@@ -120,7 +121,7 @@ Monta links da página da partida e das páginas de times.
120
121
  # => #<Date: 2026-12-25 ...>
121
122
  ```
122
123
 
123
- ### `CbfCalendario.jogos_pendentes_no_dia('10/05/2026')`
124
+ ### `CbfCalendario.jogos_do_dia('10/05/2026')`
124
125
 
125
126
  ```ruby
126
127
  # => [
@@ -129,12 +130,26 @@ Monta links da página da partida e das páginas de times.
129
130
  # serie: "Série A",
130
131
  # mandante: "Flamengo",
131
132
  # visitante: "Bahia",
132
- # placar_ou_horario: "16:00",
133
+ # horario: "16:00",
134
+ # placar: nil,
133
135
  # data: "10/05/2026",
134
136
  # data_iso: "2026-05-10",
135
137
  # local: "Maracanã",
136
138
  # rodada: "6",
137
139
  # id_jogo: "832031"
140
+ # },
141
+ # {
142
+ # campeonato: "Campeonato Brasileiro",
143
+ # serie: "Série A",
144
+ # mandante: "Palmeiras",
145
+ # visitante: "São Paulo",
146
+ # horario: "",
147
+ # placar: "1 x 0",
148
+ # data: "10/05/2026",
149
+ # data_iso: "2026-05-10",
150
+ # local: "Allianz Parque",
151
+ # rodada: "6",
152
+ # id_jogo: "832032"
138
153
  # }
139
154
  # ]
140
155
  ```
@@ -179,53 +194,6 @@ Monta links da página da partida e das páginas de times.
179
194
  # }
180
195
  ```
181
196
 
182
- ### `CbfCalendario.atletas_do_clube('20001')`
183
-
184
- ```ruby
185
- # => {
186
- # clube_id: "20001",
187
- # atletas: [
188
- # {
189
- # "id_atleta" => 12345,
190
- # "nome_popular" => "Atleta Exemplo",
191
- # "nome_completo" => "Atleta Exemplo da Silva",
192
- # "posicao" => "MEI",
193
- # ...
194
- # }
195
- # ]
196
- # }
197
- ```
198
-
199
- ### `CbfCalendario.atleta_por_id('12345')`
200
-
201
- ```ruby
202
- # => {
203
- # atleta_id: "12345",
204
- # atleta: {
205
- # "id_atleta" => 12345,
206
- # "nome_popular" => "Atleta Exemplo",
207
- # "nome_completo" => "Atleta Exemplo da Silva",
208
- # "posicao" => "ATA",
209
- # ...
210
- # }
211
- # }
212
- ```
213
-
214
- ### `CbfCalendario.clube_por_id('20001')`
215
-
216
- ```ruby
217
- # => {
218
- # clube_id: "20001",
219
- # clube: {
220
- # "id_clube" => 20001,
221
- # "nome" => "Time Exemplo",
222
- # "sigla" => "TEX",
223
- # "escudo" => "https://...",
224
- # ...
225
- # }
226
- # }
227
- ```
228
-
229
197
  ### `CbfCalendario.estatisticas_agregadas(jogo)`
230
198
 
231
199
  ```ruby
@@ -256,8 +224,6 @@ Monta links da página da partida e das páginas de times.
256
224
 
257
225
  - `CbfCalendario::InvalidDateError`: data inválida (formato esperado: `dd/mm/aaaa`).
258
226
  - `CbfCalendario::InvalidGameIdError`: `id_jogo` inválido (somente dígitos).
259
- - `CbfCalendario::InvalidClubIdError`: `id_clube` inválido (somente dígitos).
260
- - `CbfCalendario::InvalidAthleteIdError`: `id_atleta` inválido (somente dígitos).
261
227
  - `CbfCalendario::HttpError`: erro HTTP ou payload inválido da API.
262
228
  - `CbfCalendario::Error`: classe base.
263
229
 
@@ -271,335 +237,15 @@ class CbfCalendarioSyncJob < ApplicationJob
271
237
 
272
238
  def perform(data_iso: nil)
273
239
  data = data_iso ? Date.iso8601(data_iso) : Date.current
274
- CbfCalendario.jogos_pendentes_no_dia(data)
275
- end
276
- end
277
- ```
278
-
279
- ## Requisitos
280
-
281
- - Ruby >= 3.0
282
-
283
- ## Licença
284
-
285
- MIT. Veja `LICENSE.txt`.
286
- # cbf_calendario
287
-
288
- Cliente Ruby para a API pública da [CBF](https://www.cbf.com.br): **calendário** (jogos pendentes por dia), **partida** (`/api/cbf/jogos/:id`), **estatísticas derivadas** da súmula e **montagem de URLs** do site. Retornos em **hashes** Ruby para uso em **Rails** ou scripts.
289
-
290
- ## Instalação
291
-
292
- ```ruby
293
- # Gemfile
294
- gem 'cbf_calendario', '~> 0.2'
295
- ```
296
-
297
- ```bash
298
- bundle install
299
- ```
300
-
301
- Em qualquer script Ruby (sem Bundler no load path):
302
-
303
- ```bash
304
- gem install cbf_calendario
305
- ```
306
-
307
- ```ruby
308
- require 'cbf_calendario'
309
- ```
310
-
311
- ---
312
-
313
- ## Uso direto (mesmo processo da requisição)
314
-
315
- Ideal só para testes ou endpoints muito leves. Para produção, prefira **enfileirar um job** (seção seguinte).
316
-
317
- ```ruby
318
- require 'cbf_calendario'
319
-
320
- # Data como String dd/mm/aaaa, Date ou Time
321
- jogos = CbfCalendario.jogos_pendentes_no_dia('10/05/2026')
322
- # => [{ campeonato: "...", serie: "...", mandante: "...", ... }, ...]
323
-
324
- jogos = CbfCalendario.jogos_pendentes_no_dia(Date.current)
325
-
326
- # Cliente com opções (timeout HTTP, etc.)
327
- client = CbfCalendario::Client.new(read_timeout: 45)
328
- client.jogos_pendentes_no_dia('01/06/2026')
329
-
330
- # Payload bruto da API (Hash) para o dia
331
- client.calendario_json(Date.today)
332
-
333
- # Parsing de data brasileira
334
- CbfCalendario.parse_data_br!('25/12/2026') # => Date
335
- ```
336
-
337
- ### Partida (resultado / súmula completa da API)
338
-
339
- Mesmo endpoint usado em `show_game.rb`: **`GET /api/cbf/jogos/:id`**. O retorno é o **JSON completo** como `Hash` em Ruby (chaves **string**, igual ao JSON da CBF).
340
-
341
- ```ruby
342
- # Payload inteiro: tipicamente { "jogo" => { ... registros, atletas, árbitros, ... } }
343
- payload = CbfCalendario.partida_completa('832031')
344
-
345
- jogo = payload['jogo']
346
- puts jogo.dig('mandante', 'nome')
347
- puts jogo.dig('mandante', 'gols')
348
- puts jogo.dig('visitante', 'gols')
349
-
350
- # Só o objeto jogo (atalho)
351
- jogo = CbfCalendario.jogo_partida(832031)
352
-
353
- # Estatísticas derivadas dos registros (gols por tipo, cartões, substituições…)
354
- stats = CbfCalendario.estatisticas_agregadas(jogo)
355
- # => { por_tipo_evento: {...}, gols_mandante_em_eventos: N, ... }
356
-
357
- # URLs públicas (mandante/visitante/página da partida), mesma regra do show_game
358
- cp = jogo['campeonato']
359
- ano = (cp['ano'] || jogo['ano']).to_s
360
- pagina = CbfCalendario::Urls.url_pagina_partida(jogo)
361
- mandante_url = CbfCalendario::Urls.url_time(cp['nome'], cp['nome_categoria'], ano, jogo.dig('mandante', 'id'))
362
- ```
363
-
364
- Timeouts maiores (como no script original):
365
-
366
- ```ruby
367
- CbfCalendario.partida_completa('832031', read_timeout: 45, open_timeout: 15)
368
- ```
369
-
370
- Erros extras: `CbfCalendario::InvalidGameIdError` (ID inválido), `CbfCalendario::HttpError` (HTTP ou resposta sem `jogo`).
371
-
372
- ---
373
-
374
- ## Referência das funções públicas
375
-
376
- ### Módulo `CbfCalendario` (atalhos)
377
-
378
- | Função | Descrição |
379
- |--------|-----------|
380
- | `parse_data_br!(str)` | Converte `"dd/mm/aaaa"` em `Date`. Lança `InvalidDateError`. |
381
- | `jogos_pendentes_no_dia(data, **opts)` | Jogos do dia **ainda sem placar** na API. `data`: `String` BR, `Date` ou `Time`. Retorna `Array<Hash>` com **chaves símbolo**. `**opts` repassa para `Client.new`. |
382
- | `partida_completa(id_jogo, **opts)` | `GET /api/cbf/jogos/:id` — payload JSON completo. Retorna `Hash` com **chaves string** (como o JSON). |
383
- | `jogo_partida(id_jogo, **opts)` | Mesmo endpoint; retorna só `payload["jogo"]` (`Hash` ou levanta erro se ausente). |
384
- | `estatisticas_agregadas(jogo)` | Agrega eventos da súmula (`registros`, substituições, etc.). Espera o **Hash `jogo`** vindo da API. Retorna `Hash` com **chaves símbolo**. |
385
-
386
- ### Classe `CbfCalendario::Client`
387
-
388
- Construtor: `Client.new(base_url: "https://www.cbf.com.br", read_timeout: 30, open_timeout: 15)`.
389
-
390
- | Método | Descrição |
391
- |--------|-----------|
392
- | `jogos_pendentes_no_dia(data)` | Igual ao atalho do módulo. |
393
- | `calendario_json(data)` | Payload bruto do calendário: `GET /api/cbf/calendario/jogos/AAAA/MM/DD` (`Hash`, chaves string). |
394
- | `partida_completa(id_jogo)` | Payload bruto da partida (`GET /api/cbf/jogos/:id`). |
395
- | `jogo_partida(id_jogo)` | Somente o objeto `jogo`. |
396
- | `Client.parse_data_br!(str)` | Igual `CbfCalendario.parse_data_br!`. |
397
- | `Client.coerce_date!(data)` | Normaliza `Date` / `Time` / string `dd/mm/aaaa`. |
398
- | `Client.normalize_id_jogo!(id)` | Valida ID numérico (`String` de dígitos) ou levanta `InvalidGameIdError`. |
399
-
400
- ### `CbfCalendario::PartidaStats`
401
-
402
- | Método | Descrição |
403
- |--------|-----------|
404
- | `agregadas(jogo)` | Implementação central das agregações; mesmo resultado que `CbfCalendario.estatisticas_agregadas(jogo)`. |
405
-
406
- Chaves típicas no **retorno** de `estatisticas_agregadas` / `PartidaStats.agregadas`:
407
-
408
- | Chave (símbolo) | Conteúdo |
409
- |-----------------|----------|
410
- | `:por_tipo_evento` | Contagem por `tipo` em `registros` (ex.: `GOL`, `PENALIDADE`). |
411
- | `:gols_por_classificacao_sumula` | Contagem por campo `resultado` nos eventos de gol. |
412
- | `:gols_mandante_em_eventos` | Gols do mandante contados nos registros `GOL`. |
413
- | `:gols_visitante_em_eventos` | Idem visitante. |
414
- | `:cartoes_por_resultado` | Contagem por `resultado` em eventos `PENALIDADE`. |
415
- | `:total_substituicoes_mandante` | Quantidade de substituições no mandante. |
416
- | `:total_substituicoes_visitante` | Idem visitante. |
417
-
418
- ### `CbfCalendario::Urls` (links do site, sem HTTP)
419
-
420
- Constante: `CbfCalendario::Urls::SITE_ROOT` (`"https://www.cbf.com.br"`).
421
-
422
- | Método | Descrição |
423
- |--------|-----------|
424
- | `slug_segment(str)` | Slug à URL (remove acentos, minúsculas, hífens). |
425
- | `segmento_campeonato(nome)` | Segmento do campeonato na URL (ex.: `campeonato-brasileiro`). |
426
- | `segmento_categoria(nome_serie)` | Segmento da categoria/série (`serie-a`, `sub-20`, etc.). |
427
- | `path_pagina_jogo(jogo)` | Path relativo da página da partida no site. |
428
- | `url_pagina_partida(jogo, base: SITE_ROOT)` | URL absoluta da partida. |
429
- | `url_time(campeonato_nome, categoria_nome, ano, clube_id, base: SITE_ROOT)` | URL da página do clube na temporada. |
430
-
431
- O objeto `jogo`/`payload` usado em `Urls` deve ser o **Hash da API** (como em `jogo_partida`), com `campeonato`, `mandante`, `visitante`, `id_jogo`, etc.
432
-
433
- ---
434
-
435
- ## Rails: Solid Queue e jobs em background (recomendado)
436
-
437
- A API da CBF é chamada via rede; **não bloqueie** requisições HTTP longas no processo web (ex.: Puma). Use **[Solid Queue](https://github.com/rails/solid_queue)** (Rails 8+):
438
-
439
- 1. Inclua `cbf_calendario` no `Gemfile`.
440
- 2. Configure o adapter **`solid_queue`** para o Active Job.
441
- 3. Implemente um **Active Job** que chama `CbfCalendario`.
442
- 4. Rode o worker **`bin/jobs`** (processo separado do servidor web).
443
-
444
- ### 1. Adapter Solid Queue
445
-
446
- No `Gemfile`, garanta a gem (no Rails 8 costuma vir por padrão; confira o guia do seu app):
447
-
448
- ```ruby
449
- gem 'cbf_calendario'
450
- gem 'solid_queue'
451
- ```
452
-
453
- `config/application.rb` (ou ambiente de produção):
454
-
455
- ```ruby
456
- config.active_job.queue_adapter = :solid_queue
457
- ```
458
-
459
- Em desenvolvimento e produção, mantenha `config.active_job.queue_adapter = :solid_queue` e rode `bin/jobs` quando precisar processar filas fora do ciclo de request.
460
-
461
- ### 2. Job com Active Job
462
-
463
- `app/jobs/cbf_calendario_sync_job.rb`:
464
-
465
- ```ruby
466
- # frozen_string_literal: true
467
-
468
- class CbfCalendarioSyncJob < ApplicationJob
469
- queue_as :default
470
-
471
- # data_iso: "2026-05-10" ou nil para usar Date.current
472
- def perform(data_iso: nil)
473
- data =
474
- if data_iso.present?
475
- Date.iso8601(data_iso.to_s)
476
- else
477
- Date.current
478
- end
479
-
480
- jogos = CbfCalendario.jogos_pendentes_no_dia(data)
481
-
482
- # Exemplo: persistir, notificar, cachear — adapte ao seu domínio
483
- Rails.logger.info("[CBF] #{jogos.size} jogos pendentes em #{data}")
484
-
485
- jogos.each do |jogo|
486
- ExternalFixtureUpsertService.call(jogo) # seu serviço
487
- end
488
-
489
- jogos
490
- rescue CbfCalendario::HttpError => e
491
- Rails.logger.error("[CBF] HTTP: #{e.message}")
492
- raise # deixa o Active Job aplicar retry conforme configuração
493
- rescue CbfCalendario::InvalidDateError => e
494
- Rails.logger.warn("[CBF] data inválida: #{e.message}")
495
- raise
240
+ CbfCalendario.jogos_do_dia(data)
496
241
  end
497
242
  end
498
243
  ```
499
244
 
500
- Enfileirar (controller, outro job, scheduler):
501
-
502
- ```ruby
503
- # data específica
504
- CbfCalendarioSyncJob.perform_later(data_iso: '2026-05-10')
505
-
506
- # “hoje” no servidor
507
- CbfCalendarioSyncJob.perform_later
508
-
509
- # com delay
510
- CbfCalendarioSyncJob.set(wait: 5.minutes).perform_later(data_iso: Date.tomorrow.iso8601)
511
- ```
512
-
513
- ### 3. Processar a fila (`bin/jobs`)
514
-
515
- Com Solid Queue, o worker padrão processa os jobs enfileirados:
516
-
517
- ```bash
518
- bin/jobs
519
- ```
520
-
521
- Rode em **processo separado** do `rails server` / Puma. Em produção, trate `bin/jobs` como serviço (systemd, container, Plataforma como serviço, etc.).
522
-
523
- ### 4. Agendamento periódico (Solid Queue)
524
-
525
- Use o **recurring** do Solid Queue (ex.: `config/recurring.yml` no seu app) para enfileirar `CbfCalendarioSyncJob.perform_later` no horário desejado. A sintaxe e os arquivos exatos dependem da versão do Solid Queue — veja a [documentação do Solid Queue](https://github.com/rails/solid_queue) (seção *Recurring / cron*).
526
-
527
- ### 5. Serviço systemd (exemplo): `bin/jobs` no ar
528
-
529
- Ajuste usuário, path e caminho do Ruby:
530
-
531
- ```ini
532
- [Unit]
533
- Description=Solid Queue — processador de jobs (Rails + cbf_calendario)
534
- After=network.target
535
-
536
- [Service]
537
- Type=simple
538
- User=deploy
539
- WorkingDirectory=/var/www/seu_app/current
540
- Environment=RAILS_ENV=production
541
- ExecStart=/home/deploy/.rbenv/shims/bundle exec bin/jobs
542
- Restart=always
543
- RestartSec=10
544
-
545
- [Install]
546
- WantedBy=multi-user.target
547
- ```
548
-
549
- ```bash
550
- sudo systemctl daemon-reload
551
- sudo systemctl enable --now solid-queue-jobs.service
552
- ```
553
-
554
- (O nome do arquivo `.service` pode ser o que preferir; o importante é executar `bundle exec bin/jobs`.)
555
-
556
- ---
557
-
558
- ## API dos hashes retornados (calendário / pendentes)
559
-
560
- A tabela abaixo vale para **cada item** de `jogos_pendentes_no_dia` (chaves **símbolo**). O objeto **`jogo`** de `partida_completa` / `jogo_partida` segue o JSON da CBF (chaves **string**: `"mandante"`, `"registros"`, `"arbitros"`, etc.) — veja a referência completa acima.
561
-
562
- | Chave | Significado |
563
- |-------|-------------|
564
- | `:campeonato` | Nome do campeonato |
565
- | `:serie` | Série / divisão |
566
- | `:mandante` | Time mandante |
567
- | `:visitante` | Time visitante |
568
- | `:placar_ou_horario` | Horário previsto ou placar, se houver |
569
- | `:data` | Data como string na API |
570
- | `:data_iso` | Data no formato `YYYY-MM-DD` |
571
- | `:local` | Local do jogo |
572
- | `:rodada` | Rodada |
573
- | `:id_jogo` | Identificador do jogo na CBF |
574
-
575
- ### Erros
576
-
577
- | Classe | Quando |
578
- |--------|--------|
579
- | `CbfCalendario::InvalidDateError` | Data em formato inválido |
580
- | `CbfCalendario::InvalidGameIdError` | ID de jogo não numérico |
581
- | `CbfCalendario::HttpError` | Falha HTTP ao chamar a API |
582
- | `CbfCalendario::Error` | Classe base |
583
-
584
- Em jobs, costuma-se **relançar** `HttpError` para retry exponencial; datas inválidas podem ser descartadas sem retry.
585
-
586
- ### Rails: responder JSON a partir dos hashes
587
-
588
- ```ruby
589
- def index
590
- jogos = CbfCalendario.jogos_pendentes_no_dia(params.require(:data))
591
- render json: jogos.map(&:stringify_keys)
592
- end
593
- ```
594
-
595
- Para não bloquear o request em produção, prefira só enfileirar o job e devolver `202 Accepted`, ou ler resultado de cache/banco preenchido pelo job.
596
-
597
- ---
598
-
599
245
  ## Requisitos
600
246
 
601
247
  - Ruby >= 3.0
602
248
 
603
249
  ## Licença
604
250
 
605
- MIT veja [LICENSE.txt](LICENSE.txt).
251
+ MIT. Veja `LICENSE.txt`.
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ['Betbrothers']
9
9
  spec.summary = 'Cliente Ruby para o calendário de jogos da CBF (hashes, uso em Rails)'
10
10
  spec.description = <<~DESC
11
- Consulta a API pública de calendário da CBF e devolve jogos pendentes (sem placar)
11
+ Consulta a API pública de calendário da CBF e devolve todos os jogos do dia
12
12
  para uma data, como Array de hashes Ruby — adequado para uso em Ruby on Rails.
13
13
  DESC
14
14
  spec.license = 'MIT'
@@ -24,4 +24,7 @@ Gem::Specification.new do |spec|
24
24
  %w[LICENSE.txt README.md CHANGELOG.md cbf_calendario.gemspec] + Dir['lib/**/*.rb']
25
25
  end
26
26
  spec.require_paths = ['lib']
27
+
28
+ spec.add_development_dependency 'minitest', '~> 5.22'
29
+ spec.add_development_dependency 'rake'
27
30
  end
@@ -13,8 +13,6 @@ module CbfCalendario
13
13
  class HttpError < Error; end
14
14
  class InvalidDateError < Error; end
15
15
  class InvalidGameIdError < Error; end
16
- class InvalidClubIdError < Error; end
17
- class InvalidAthleteIdError < Error; end
18
16
 
19
17
  class Client
20
18
  DEFAULT_BASE = 'https://www.cbf.com.br'
@@ -42,45 +40,6 @@ module CbfCalendario
42
40
  partida_completa(id_jogo)['jogo']
43
41
  end
44
42
 
45
- # GET /api/cbf/atletas/:id_clube — retorna hash com dados dos atletas.
46
- # Saída: { clube_id: "123", atletas: [ ... ] }
47
- def atletas_do_clube(id_clube)
48
- cid = Client.normalize_id_clube!(id_clube)
49
- payload = get_json(api_path_atletas_clube(cid))
50
- atletas =
51
- if payload.is_a?(Array)
52
- payload
53
- elsif payload.is_a?(Hash) && payload['atletas'].is_a?(Array)
54
- payload['atletas']
55
- else
56
- raise HttpError, 'Resposta sem lista de atletas'
57
- end
58
-
59
- { clube_id: cid, atletas: atletas }
60
- end
61
-
62
- # Busca dados completos de um clube por ID.
63
- # Saída: { clube_id: "123", clube: { ... } }
64
- def clube_por_id(id_clube)
65
- cid = Client.normalize_id_clube!(id_clube)
66
- payload = buscar_clube_payload(cid)
67
- clube = extrair_clube_do_payload(payload, cid)
68
- raise HttpError, 'Resposta sem dados do clube' unless clube.is_a?(Hash) && !clube.empty?
69
-
70
- { clube_id: cid, clube: clube }
71
- end
72
-
73
- # Busca dados completos de um atleta por ID.
74
- # Saída: { atleta_id: "123", atleta: { ... } }
75
- def atleta_por_id(id_atleta)
76
- aid = Client.normalize_id_atleta!(id_atleta)
77
- payload = buscar_atleta_payload(aid)
78
- atleta = extrair_atleta_do_payload(payload, aid)
79
- raise HttpError, 'Resposta sem dados do atleta' unless atleta.is_a?(Hash) && !atleta.empty?
80
-
81
- { atleta_id: aid, atleta: atleta }
82
- end
83
-
84
43
  def self.normalize_id_jogo!(id_jogo)
85
44
  s = id_jogo.to_s.strip
86
45
  raise InvalidGameIdError, 'id_jogo deve conter só dígitos (ex.: 832024)' unless s.match?(/\A\d+\z/)
@@ -88,28 +47,15 @@ module CbfCalendario
88
47
  s
89
48
  end
90
49
 
91
- def self.normalize_id_clube!(id_clube)
92
- s = id_clube.to_s.strip
93
- raise InvalidClubIdError, 'id_clube deve conter só dígitos (ex.: 20001)' unless s.match?(/\A\d+\z/)
94
-
95
- s
96
- end
97
-
98
- def self.normalize_id_atleta!(id_atleta)
99
- s = id_atleta.to_s.strip
100
- raise InvalidAthleteIdError, 'id_atleta deve conter só dígitos (ex.: 12345)' unless s.match?(/\A\d+\z/)
101
-
102
- s
103
- end
104
-
105
50
  # Aceita Date, Time ou String "dd/mm/aaaa".
106
- # Retorna Array<Hash> com símbolos como chaves, ordenado e sem IDs duplicados.
107
- # Cada hash: +campeonato+, +serie+, +mandante+, +visitante+, +placar_ou_horario+,
51
+ # Retorna todos os jogos do dia no calendário (pendentes e realizados).
52
+ # Array<Hash> com símbolos como chaves, ordenado e sem IDs duplicados.
53
+ # Cada hash: +campeonato+, +serie+, +mandante+, +visitante+, +horario+, +placar+,
108
54
  # +data+, +data_iso+, +local+, +rodada+, +id_jogo+.
109
- def jogos_pendentes_no_dia(data)
55
+ def jogos_do_dia(data)
110
56
  date = Client.coerce_date!(data)
111
57
  payload = get_json(api_path_calendario(date))
112
- jogos = extrair_jogos_pendentes(payload, date)
58
+ jogos = extrair_jogos_do_dia(payload, date)
113
59
  dedup_and_sort(jogos)
114
60
  end
115
61
 
@@ -150,36 +96,17 @@ module CbfCalendario
150
96
  "/api/cbf/jogos/#{id_jogo}"
151
97
  end
152
98
 
153
- def api_path_atletas_clube(id_clube)
154
- "/api/cbf/atletas/#{id_clube}"
155
- end
156
-
157
- def api_path_atleta(id_atleta)
158
- "/api/cbf/atleta/#{id_atleta}"
159
- end
160
-
161
- def api_path_atletas_detalhes(id_atleta)
162
- "/api/cbf/atletas/detalhes/#{id_atleta}"
163
- end
164
-
165
- def api_path_clube(id_clube)
166
- "/api/cbf/clubes/#{id_clube}"
167
- end
168
-
169
- def api_path_time(id_clube)
170
- "/api/cbf/times/#{id_clube}"
171
- end
172
-
173
- def api_path_clube_detalhes(id_clube)
174
- "/api/cbf/clubes/detalhes/#{id_clube}"
175
- end
176
-
177
99
  def get_json(path)
178
100
  uri = URI.join(base_url, path)
179
101
  request_json(uri)
180
102
  end
181
103
 
182
104
  def request_json(uri)
105
+ res = perform_get(uri, accept: 'application/json')
106
+ JSON.parse(res.body)
107
+ end
108
+
109
+ def perform_get(uri, accept:, redirects_left: 5)
183
110
  Net::HTTP.start(
184
111
  uri.host,
185
112
  uri.port,
@@ -190,13 +117,24 @@ module CbfCalendario
190
117
  cert_store: ssl_cert_store
191
118
  ) do |http|
192
119
  req = Net::HTTP::Get.new(uri)
193
- req['Accept'] = 'application/json'
120
+ req['Accept'] = accept
194
121
  res = http.request(req)
122
+
123
+ if res.is_a?(Net::HTTPRedirection)
124
+ raise HttpError, 'Muitas redireções na requisição HTTP' if redirects_left <= 0
125
+
126
+ location = res['location'].to_s
127
+ raise HttpError, 'Redirecionamento HTTP sem header Location' if location.empty?
128
+
129
+ next_uri = URI.join(uri.to_s, location)
130
+ return perform_get(next_uri, accept: accept, redirects_left: redirects_left - 1)
131
+ end
132
+
195
133
  unless res.is_a?(Net::HTTPSuccess)
196
134
  raise HttpError, "HTTP #{res.code}: #{res.message}"
197
135
  end
198
136
 
199
- JSON.parse(res.body)
137
+ res
200
138
  end
201
139
  end
202
140
 
@@ -210,22 +148,19 @@ module CbfCalendario
210
148
  end
211
149
  end
212
150
 
213
- def placar_ou_horario(jogo)
214
- gm = jogo.dig('mandante', 'gols')
215
- gv = jogo.dig('visitante', 'gols')
216
-
217
- return "#{gm} x #{gv}" unless gm.nil? || gv.nil?
218
-
151
+ def horario_jogo(jogo)
219
152
  jogo['hora'].to_s.strip
220
153
  end
221
154
 
222
- def jogo_sem_placar?(jogo)
155
+ def placar_partida(jogo)
223
156
  gm = jogo.dig('mandante', 'gols')
224
157
  gv = jogo.dig('visitante', 'gols')
225
- gm.nil? || gv.nil?
158
+ return nil if gm.nil? || gv.nil?
159
+
160
+ "#{gm} x #{gv}"
226
161
  end
227
162
 
228
- def extrair_jogos_pendentes(payload, data_calendario)
163
+ def extrair_jogos_do_dia(payload, data_calendario)
229
164
  raiz = payload['jogos'] || {}
230
165
  linhas = []
231
166
 
@@ -236,14 +171,13 @@ module CbfCalendario
236
171
  next unless lista.is_a?(Array)
237
172
 
238
173
  lista.each do |jogo|
239
- next unless jogo_sem_placar?(jogo)
240
-
241
174
  linhas << {
242
175
  campeonato: campeonato.to_s.strip,
243
176
  serie: serie.to_s.strip,
244
177
  mandante: jogo.dig('mandante', 'nome').to_s.strip,
245
178
  visitante: jogo.dig('visitante', 'nome').to_s.strip,
246
- placar_ou_horario: placar_ou_horario(jogo),
179
+ horario: horario_jogo(jogo),
180
+ placar: placar_partida(jogo),
247
181
  data: jogo['data'].to_s.strip,
248
182
  data_iso: data_calendario.strftime('%Y-%m-%d'),
249
183
  local: jogo['local'].to_s.strip,
@@ -270,80 +204,5 @@ module CbfCalendario
270
204
  out.sort_by { |j| [j[:campeonato], j[:serie], j[:rodada].to_i, j[:id_jogo]] }
271
205
  end
272
206
 
273
- def buscar_atleta_payload(id_atleta)
274
- paths = [
275
- api_path_atleta(id_atleta),
276
- api_path_atletas_detalhes(id_atleta),
277
- api_path_atletas_clube(id_atleta)
278
- ]
279
-
280
- errors = []
281
- paths.each do |path|
282
- begin
283
- return get_json(path)
284
- rescue HttpError => e
285
- errors << "#{path}: #{e.message}"
286
- end
287
- end
288
-
289
- raise HttpError, "Não foi possível buscar o atleta #{id_atleta}. Tentativas: #{errors.join(' | ')}"
290
- end
291
-
292
- def extrair_atleta_do_payload(payload, id_atleta)
293
- if payload.is_a?(Hash)
294
- return payload if campo_id_atleta(payload) == id_atleta
295
- return payload['atleta'] if payload['atleta'].is_a?(Hash)
296
- return payload
297
- end
298
-
299
- return {} unless payload.is_a?(Array)
300
-
301
- payload.find { |item| item.is_a?(Hash) && campo_id_atleta(item) == id_atleta } ||
302
- payload.find { |item| item.is_a?(Hash) } || {}
303
- end
304
-
305
- def campo_id_atleta(hash)
306
- hash['id_atleta'].to_s.strip.empty? ? hash['id'].to_s.strip : hash['id_atleta'].to_s.strip
307
- end
308
-
309
- def buscar_clube_payload(id_clube)
310
- paths = [
311
- api_path_clube(id_clube),
312
- api_path_time(id_clube),
313
- api_path_clube_detalhes(id_clube)
314
- ]
315
-
316
- errors = []
317
- paths.each do |path|
318
- begin
319
- return get_json(path)
320
- rescue HttpError => e
321
- errors << "#{path}: #{e.message}"
322
- end
323
- end
324
-
325
- raise HttpError, "Não foi possível buscar o clube #{id_clube}. Tentativas: #{errors.join(' | ')}"
326
- end
327
-
328
- def extrair_clube_do_payload(payload, id_clube)
329
- if payload.is_a?(Hash)
330
- return payload if campo_id_clube(payload) == id_clube
331
- return payload['clube'] if payload['clube'].is_a?(Hash)
332
- return payload['time'] if payload['time'].is_a?(Hash)
333
- return payload
334
- end
335
-
336
- return {} unless payload.is_a?(Array)
337
-
338
- payload.find { |item| item.is_a?(Hash) && campo_id_clube(item) == id_clube } ||
339
- payload.find { |item| item.is_a?(Hash) } || {}
340
- end
341
-
342
- def campo_id_clube(hash)
343
- return hash['id_clube'].to_s.strip unless hash['id_clube'].to_s.strip.empty?
344
- return hash['id_time'].to_s.strip unless hash['id_time'].to_s.strip.empty?
345
-
346
- hash['id'].to_s.strip
347
- end
348
207
  end
349
208
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CbfCalendario
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.1'
5
5
  end
@@ -12,9 +12,9 @@ module CbfCalendario
12
12
  Client.parse_data_br!(str)
13
13
  end
14
14
 
15
- # Atalho sem instanciar +Client+.
16
- def jogos_pendentes_no_dia(data, **client_options)
17
- Client.new(**client_options).jogos_pendentes_no_dia(data)
15
+ # Atalho sem instanciar +Client+. Lista todos os jogos do dia (calendário completo).
16
+ def jogos_do_dia(data, **client_options)
17
+ Client.new(**client_options).jogos_do_dia(data)
18
18
  end
19
19
 
20
20
  # GET /api/cbf/jogos/:id — Hash completo da API (chaves string).
@@ -27,21 +27,6 @@ module CbfCalendario
27
27
  Client.new(**client_options).jogo_partida(id_jogo)
28
28
  end
29
29
 
30
- # Hash com todos os atletas do clube.
31
- def atletas_do_clube(id_clube, **client_options)
32
- Client.new(**client_options).atletas_do_clube(id_clube)
33
- end
34
-
35
- # Hash com os dados de um atleta específico.
36
- def atleta_por_id(id_atleta, **client_options)
37
- Client.new(**client_options).atleta_por_id(id_atleta)
38
- end
39
-
40
- # Hash com os dados de um clube específico.
41
- def clube_por_id(id_clube, **client_options)
42
- Client.new(**client_options).clube_por_id(id_clube)
43
- end
44
-
45
30
  # Estatísticas derivadas dos registros (+show_game.rb+).
46
31
  def estatisticas_agregadas(jogo)
47
32
  PartidaStats.agregadas(jogo)
metadata CHANGED
@@ -1,16 +1,44 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cbf_calendario
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Betbrothers
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.22'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.22'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
12
40
  description: |
13
- Consulta a API pública de calendário da CBF e devolve jogos pendentes (sem placar)
41
+ Consulta a API pública de calendário da CBF e devolve todos os jogos do dia
14
42
  para uma data, como Array de hashes Ruby — adequado para uso em Ruby on Rails.
15
43
  executables: []
16
44
  extensions: []