congrega_plenum 0.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 +7 -0
- data/CHANGELOG.md +35 -0
- data/LICENSE +338 -0
- data/README.md +137 -0
- data/lib/adapters/http_adapter.rb +59 -0
- data/lib/adapters/response_handler.rb +35 -0
- data/lib/adapters/retry_policy.rb +68 -0
- data/lib/client.rb +109 -0
- data/lib/congrega_plenum.rb +83 -0
- data/lib/modules/congressmen_service.rb +147 -0
- data/lib/modules/legislatures_service.rb +115 -0
- data/lib/modules/parties_service.rb +123 -0
- data/lib/modules/votings_service.rb +224 -0
- data/lib/version.rb +6 -0
- data/sig/congrega_plenum.rbs +205 -0
- metadata +60 -0
data/lib/client.rb
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CongregaPlenum
|
|
4
|
+
# HTTP client responsible for talking to the Câmara API endpoints and dealing
|
|
5
|
+
# with retries, pagination and response parsing.
|
|
6
|
+
#
|
|
7
|
+
# All methods are thread-safe because the heavy collaborators ({HttpAdapter},
|
|
8
|
+
# {RetryPolicy} and {ResponseHandler}) are stateless.
|
|
9
|
+
class Client
|
|
10
|
+
include Singleton
|
|
11
|
+
|
|
12
|
+
attr_reader :http_adapter, :retry_policy, :response_handler
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
configuration = CongregaPlenum.configuration
|
|
16
|
+
@http_adapter = HttpAdapter.new(configuration: configuration)
|
|
17
|
+
@retry_policy = RetryPolicy.new(configuration: configuration)
|
|
18
|
+
@response_handler = ResponseHandler.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Performs a single HTTP GET to the given +endpoint+ and returns the parsed
|
|
22
|
+
# JSON body.
|
|
23
|
+
#
|
|
24
|
+
# @param endpoint [String] the relative path (e.g. +deputados+)
|
|
25
|
+
# @param params [Hash] query parameters merged into the request
|
|
26
|
+
# @return [Hash] parsed response body
|
|
27
|
+
def get(endpoint, params = {})
|
|
28
|
+
url = build_url(endpoint, params)
|
|
29
|
+
|
|
30
|
+
retry_policy.with_retries(url) { execute_request(url) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Retrieves all pages for an endpoint, flattening the +dados+ payloads into a
|
|
34
|
+
# single array. Automatically respects the configured rate limit delay.
|
|
35
|
+
#
|
|
36
|
+
# @param endpoint [String]
|
|
37
|
+
# @param params [Hash]
|
|
38
|
+
# @return [Array<Hash>]
|
|
39
|
+
def get_paginated(endpoint, params = {})
|
|
40
|
+
# @type var results: Array[Hash[String, untyped]]
|
|
41
|
+
results = []
|
|
42
|
+
each_response_page(endpoint, params) { |page_data| results.concat(page_data) }
|
|
43
|
+
|
|
44
|
+
results
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def each_response_page(endpoint, params)
|
|
50
|
+
page = 1
|
|
51
|
+
|
|
52
|
+
loop do
|
|
53
|
+
response = request_page(endpoint, params, page)
|
|
54
|
+
data = response.fetch('dados', [])
|
|
55
|
+
break if data.empty?
|
|
56
|
+
|
|
57
|
+
yield(data)
|
|
58
|
+
break unless next_page?(response)
|
|
59
|
+
|
|
60
|
+
page += 1
|
|
61
|
+
apply_rate_limit_delay
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Fetches a single page enforcing +formato=json+ and the requested page index.
|
|
66
|
+
def request_page(endpoint, params, page)
|
|
67
|
+
get(endpoint, params.merge(pagina: page, formato: 'json'))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def next_page?(response)
|
|
71
|
+
response.fetch('links', []).any? { |link| link['rel'] == 'next' }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Sleep between page fetches to respect the rate limit exposed by the API.
|
|
75
|
+
# This is configurable because not every consumer has the same tolerance.
|
|
76
|
+
def apply_rate_limit_delay
|
|
77
|
+
delay = configuration.rate_limit_delay
|
|
78
|
+
|
|
79
|
+
sleep(delay) if delay.positive?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def execute_request(url)
|
|
83
|
+
response = http_adapter.get(url)
|
|
84
|
+
|
|
85
|
+
response_handler.handle(response, url)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Builds the full URL pointing to the Câmara API, ensuring +formato=json+ is present.
|
|
89
|
+
#
|
|
90
|
+
# @param endpoint [String]
|
|
91
|
+
# @param params [Hash]
|
|
92
|
+
# @return [String]
|
|
93
|
+
def build_url(endpoint, params = {})
|
|
94
|
+
uri = URI("#{configuration.base_url}/#{endpoint.gsub(%r{^/}, '')}")
|
|
95
|
+
|
|
96
|
+
# Ensure formato=json is always present
|
|
97
|
+
normalized_params = params.transform_keys(&:to_sym)
|
|
98
|
+
normalized_params[:formato] ||= 'json'
|
|
99
|
+
|
|
100
|
+
uri.query = URI.encode_www_form(normalized_params)
|
|
101
|
+
|
|
102
|
+
uri.to_s
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def configuration
|
|
106
|
+
CongregaPlenum.configuration
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'version'
|
|
4
|
+
|
|
5
|
+
require 'net/http'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'uri'
|
|
8
|
+
require 'singleton'
|
|
9
|
+
require 'logger'
|
|
10
|
+
|
|
11
|
+
# CongregaPlenum centralizes access to the Brazilian Chamber of Deputies public API.
|
|
12
|
+
# It exposes a global configuration and service objects for each resource.
|
|
13
|
+
module CongregaPlenum
|
|
14
|
+
# Base error class for all domain-specific exceptions in the gem.
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
# Raised when connectivity fails before receiving a valid HTTP response.
|
|
17
|
+
class ConnectionError < Error; end
|
|
18
|
+
# Raised when the API responds with an unexpected status code or payload.
|
|
19
|
+
class APIError < Error; end
|
|
20
|
+
# Raised for transient 5xx responses that are safe to retry.
|
|
21
|
+
class ServerError < APIError; end
|
|
22
|
+
# Raised when the API signals that the rate limit has been exceeded.
|
|
23
|
+
class RateLimitError < Error; end
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
attr_accessor :configuration
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Yields the current {Configuration} so callers can override defaults.
|
|
30
|
+
#
|
|
31
|
+
# @yieldparam config [Configuration] the mutable configuration object
|
|
32
|
+
def self.configure
|
|
33
|
+
self.configuration ||= Configuration.new
|
|
34
|
+
|
|
35
|
+
yield(configuration)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Shared configuration used by the client and services to tweak base URL,
|
|
39
|
+
# timeouts and retry policies. Defaults focus on the public APIs hosted by the
|
|
40
|
+
# Câmara so a regular application can simply call {CongregaPlenum.configure} and
|
|
41
|
+
# override what differs (timeouts, logger, etc.).
|
|
42
|
+
class Configuration
|
|
43
|
+
# @return [String] Base endpoint for all requests.
|
|
44
|
+
attr_accessor :base_url
|
|
45
|
+
# @return [Integer] Timeout (seconds) applied to open/read operations.
|
|
46
|
+
attr_accessor :timeout
|
|
47
|
+
# @return [Integer] How many times requests are retried on transient errors.
|
|
48
|
+
attr_accessor :retries
|
|
49
|
+
# @return [Float] Initial backoff delay in seconds.
|
|
50
|
+
attr_accessor :retry_delay
|
|
51
|
+
# @return [Float] Delay between paginated calls to respect rate limits.
|
|
52
|
+
attr_accessor :rate_limit_delay
|
|
53
|
+
# @return [Logger] Logger used across adapters/services.
|
|
54
|
+
attr_accessor :logger
|
|
55
|
+
|
|
56
|
+
# Builds a configuration instance with safe defaults tuned for the Câmara APIs.
|
|
57
|
+
# Those endpoints are known to be HTTPS-only, relatively slow, and rate-limited,
|
|
58
|
+
# so the defaults try to smooth that out.
|
|
59
|
+
def initialize
|
|
60
|
+
@base_url = 'https://dadosabertos.camara.leg.br/api/v2'
|
|
61
|
+
@timeout = 30
|
|
62
|
+
@retries = 3
|
|
63
|
+
@retry_delay = 1.0
|
|
64
|
+
@rate_limit_delay = 0.1
|
|
65
|
+
@logger = Logger.new($stdout)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Initialize with default configuration
|
|
70
|
+
configure do |_config|
|
|
71
|
+
# Defaults are already set in Configuration#initialize
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Load all sub-modules
|
|
76
|
+
require_relative 'adapters/http_adapter'
|
|
77
|
+
require_relative 'adapters/retry_policy'
|
|
78
|
+
require_relative 'adapters/response_handler'
|
|
79
|
+
require_relative 'client'
|
|
80
|
+
require_relative 'modules/congressmen_service'
|
|
81
|
+
require_relative 'modules/parties_service'
|
|
82
|
+
require_relative 'modules/legislatures_service'
|
|
83
|
+
require_relative 'modules/votings_service'
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CongregaPlenum
|
|
4
|
+
# Service responsible for interacting with congressmen endpoints, wrapping both
|
|
5
|
+
# list and detail requests into high level helpers.
|
|
6
|
+
class CongressmenService
|
|
7
|
+
# Default page size for list endpoints.
|
|
8
|
+
ITEMS_PER_PAGE = 100
|
|
9
|
+
# Interval used to emit progress logs while iterating.
|
|
10
|
+
PROGRESS_INTERVAL = 50
|
|
11
|
+
# Tag prefix for structured logging.
|
|
12
|
+
SERVICE_TAG = 'CongregaPlenum::CongressmenService'
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# Shared client instance configured via {CongregaPlenum.configure}.
|
|
16
|
+
#
|
|
17
|
+
# @return [CongregaPlenum::Client]
|
|
18
|
+
def client
|
|
19
|
+
@client ||= CongregaPlenum::Client.instance
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Wraps {Client#get} to add service context without hiding request failures.
|
|
23
|
+
def api_get(endpoint, params = {})
|
|
24
|
+
client.get(endpoint, params)
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
log_error("Erro ao acessar API em #{endpoint}: #{e.message}")
|
|
27
|
+
raise
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Same as {.api_get}, but for bulk pagination calls. Failures are propagated
|
|
31
|
+
# so consumers can abort an incomplete synchronization.
|
|
32
|
+
def api_get_paginated(endpoint, params = {})
|
|
33
|
+
client.get_paginated(endpoint, params)
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
log_error("Erro paginando API em #{endpoint}: #{e.message}")
|
|
36
|
+
raise
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Fetches the entire list of congressmen regardless of legislature.
|
|
40
|
+
#
|
|
41
|
+
# @return [Array<Hash>]
|
|
42
|
+
def fetch_all
|
|
43
|
+
fetch_deputies_collection(context: 'todos os deputados', params: { itens: ITEMS_PER_PAGE })
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Fetches all congressmen for a given legislature, enriching each entry with
|
|
47
|
+
# its detailed payload.
|
|
48
|
+
#
|
|
49
|
+
# @param legislature_id [Integer]
|
|
50
|
+
# @return [Array<Hash>]
|
|
51
|
+
def fetch_all_by_legislature(legislature_id)
|
|
52
|
+
fetch_deputies_collection(
|
|
53
|
+
context: "legislatura #{legislature_id}",
|
|
54
|
+
params: { itens: ITEMS_PER_PAGE, idLegislatura: legislature_id }
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Retrieves the detailed payload of a single congressman.
|
|
59
|
+
#
|
|
60
|
+
# @param deputy_id [Integer]
|
|
61
|
+
# @return [Hash, nil]
|
|
62
|
+
# @raise [CongregaPlenum::APIError] if +dados+ has an unexpected type
|
|
63
|
+
def fetch_by_id(deputy_id)
|
|
64
|
+
log_debug("Buscando deputado #{deputy_id}")
|
|
65
|
+
|
|
66
|
+
response = api_get("deputados/#{deputy_id}")
|
|
67
|
+
extract_detail(response, "deputado #{deputy_id}")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Returns a paginated list of congressmen straight from the API, without
|
|
71
|
+
# making detail calls.
|
|
72
|
+
#
|
|
73
|
+
# @param page [Integer]
|
|
74
|
+
# @param items_per_page [Integer]
|
|
75
|
+
# @param legislature_id [Integer,nil]
|
|
76
|
+
# @return [Array<Hash>]
|
|
77
|
+
def fetch_list(page: 1, items_per_page: ITEMS_PER_PAGE, legislature_id: nil)
|
|
78
|
+
log_debug("Buscando lista de deputados página #{page}")
|
|
79
|
+
|
|
80
|
+
params = { pagina: page, itens: items_per_page }
|
|
81
|
+
params[:idLegislatura] = legislature_id if legislature_id
|
|
82
|
+
|
|
83
|
+
response = api_get('deputados', params)
|
|
84
|
+
response['dados'] || []
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def extract_detail(response, resource)
|
|
90
|
+
detail = response['dados']
|
|
91
|
+
return detail if detail.nil? || detail.is_a?(Hash)
|
|
92
|
+
|
|
93
|
+
raise APIError, "Resposta inválida para #{resource}: esperado Hash ou nil em dados"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Coordinates pagination and detail fetches so the long running process can
|
|
97
|
+
# log progress and reuse error handling in one place.
|
|
98
|
+
def fetch_deputies_collection(context:, params:)
|
|
99
|
+
log_info("Iniciando coleta de #{context}")
|
|
100
|
+
|
|
101
|
+
deputies = api_get_paginated('deputados', params)
|
|
102
|
+
|
|
103
|
+
log_info("Coletamos #{deputies.length} registros de #{context}")
|
|
104
|
+
|
|
105
|
+
detailed_deputies = build_detailed_deputies(deputies, context)
|
|
106
|
+
|
|
107
|
+
log_info("Finalizamos a coleta detalhada de #{detailed_deputies.length} registros para #{context}")
|
|
108
|
+
detailed_deputies
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Walks the basic list and replaces each entry with its detailed payload.
|
|
112
|
+
# Having this call separated from {#fetch_deputies_collection} keeps the
|
|
113
|
+
# public method compact and enables targeted testing of the detail logic.
|
|
114
|
+
def build_detailed_deputies(deputies, context)
|
|
115
|
+
deputies.each_with_index.with_object([]) do |(deputy, index), collected|
|
|
116
|
+
detailed_deputy = fetch_by_id(deputy['id'])
|
|
117
|
+
collected << detailed_deputy if detailed_deputy
|
|
118
|
+
log_progress(index, deputies.length, context)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Emits progress logs every {PROGRESS_INTERVAL} items so long running
|
|
123
|
+
# synchronisations give users feedback without spamming the console.
|
|
124
|
+
def log_progress(index, total, context)
|
|
125
|
+
return unless ((index + 1) % PROGRESS_INTERVAL).zero?
|
|
126
|
+
|
|
127
|
+
log_info("Processamos #{index + 1}/#{total} registros para #{context}")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def logger
|
|
131
|
+
CongregaPlenum.configuration.logger
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def log_info(message)
|
|
135
|
+
logger.info("#{SERVICE_TAG}: #{message}")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def log_error(message)
|
|
139
|
+
logger.error("#{SERVICE_TAG}: #{message}")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def log_debug(message)
|
|
143
|
+
logger.debug("#{SERVICE_TAG}: #{message}")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CongregaPlenum
|
|
4
|
+
# Service responsible for interacting with legislature endpoints including mesa
|
|
5
|
+
# composition and deputies per legislature.
|
|
6
|
+
class LegislaturesService
|
|
7
|
+
# Default pagination size for legislature listings.
|
|
8
|
+
PAGE_SIZE = 50
|
|
9
|
+
# Pagination size for deputies per legislature.
|
|
10
|
+
DEPUTIES_PAGE_SIZE = 100
|
|
11
|
+
# Tag prefix for structured logging.
|
|
12
|
+
SERVICE_TAG = 'CongregaPlenum::LegislaturesService'
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# Shared client instance configured via {CongregaPlenum.configure}.
|
|
16
|
+
#
|
|
17
|
+
# @return [CongregaPlenum::Client]
|
|
18
|
+
def client
|
|
19
|
+
@client ||= CongregaPlenum::Client.instance
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Wraps {Client#get} to add service context without hiding request failures.
|
|
23
|
+
def api_get(endpoint, params = {})
|
|
24
|
+
client.get(endpoint, params)
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
log_error("Erro ao acessar API em #{endpoint}: #{e.message}")
|
|
27
|
+
raise
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Bulk variant of {.api_get}. Failures are propagated so consumers can
|
|
31
|
+
# abort an incomplete synchronization.
|
|
32
|
+
def api_get_paginated(endpoint, params = {})
|
|
33
|
+
client.get_paginated(endpoint, params)
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
log_error("Erro paginando API em #{endpoint}: #{e.message}")
|
|
36
|
+
raise
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Returns every legislature exposed by a API.
|
|
40
|
+
#
|
|
41
|
+
# @return [Array<Hash>]
|
|
42
|
+
def fetch_all
|
|
43
|
+
log_info('Iniciando busca de todas as legislaturas')
|
|
44
|
+
|
|
45
|
+
legislatures = api_get_paginated('legislaturas', itens: PAGE_SIZE)
|
|
46
|
+
|
|
47
|
+
log_info("Total de #{legislatures.size} legislaturas encontradas")
|
|
48
|
+
legislatures
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Fetches a single legislature payload.
|
|
52
|
+
#
|
|
53
|
+
# @param legislature_id [Integer]
|
|
54
|
+
# @return [Hash, nil]
|
|
55
|
+
# @raise [CongregaPlenum::APIError] if +dados+ has an unexpected type
|
|
56
|
+
def fetch_by_id(legislature_id)
|
|
57
|
+
log_debug("Buscando legislatura #{legislature_id}")
|
|
58
|
+
|
|
59
|
+
response = api_get("legislaturas/#{legislature_id}")
|
|
60
|
+
extract_detail(response, "legislatura #{legislature_id}")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Retrieves the mesa composition for the provided legislature ID.
|
|
64
|
+
#
|
|
65
|
+
# @param legislature_id [Integer]
|
|
66
|
+
# @return [Array<Hash>]
|
|
67
|
+
def fetch_mesa(legislature_id)
|
|
68
|
+
response = api_get("legislaturas/#{legislature_id}/mesa")
|
|
69
|
+
mesa_data = response['dados'] || []
|
|
70
|
+
|
|
71
|
+
log_info("Mesa da legislatura #{legislature_id} coletada com #{mesa_data.size} integrantes")
|
|
72
|
+
mesa_data
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Lists every deputy that served/serves in the given legislature using the
|
|
76
|
+
# +idLegislatura+ filter supported by the deputies endpoint.
|
|
77
|
+
#
|
|
78
|
+
# @param legislature_id [Integer]
|
|
79
|
+
# @return [Array<Hash>]
|
|
80
|
+
def fetch_deputies(legislature_id)
|
|
81
|
+
log_info("Iniciando coleta de deputados da legislatura #{legislature_id}")
|
|
82
|
+
|
|
83
|
+
deputies = api_get_paginated('deputados', itens: DEPUTIES_PAGE_SIZE, idLegislatura: legislature_id)
|
|
84
|
+
|
|
85
|
+
log_info("Coletamos #{deputies.size} deputados para a legislatura #{legislature_id}")
|
|
86
|
+
deputies
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def extract_detail(response, resource)
|
|
92
|
+
detail = response['dados']
|
|
93
|
+
return detail if detail.nil? || detail.is_a?(Hash)
|
|
94
|
+
|
|
95
|
+
raise APIError, "Resposta inválida para #{resource}: esperado Hash ou nil em dados"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def logger
|
|
99
|
+
CongregaPlenum.configuration.logger
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def log_info(message)
|
|
103
|
+
logger.info("#{SERVICE_TAG}: #{message}")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def log_error(message)
|
|
107
|
+
logger.error("#{SERVICE_TAG}: #{message}")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def log_debug(message)
|
|
111
|
+
logger.debug("#{SERVICE_TAG}: #{message}")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CongregaPlenum
|
|
4
|
+
# Service responsible for interacting with party endpoints. It exposes class
|
|
5
|
+
# methods that hide pagination, retries and detail lookups.
|
|
6
|
+
class PartiesService
|
|
7
|
+
# Default page size for party listings.
|
|
8
|
+
ITEMS_PER_PAGE = 100
|
|
9
|
+
# Interval used to emit progress logs while iterating.
|
|
10
|
+
PROGRESS_INTERVAL = 20
|
|
11
|
+
# Tag prefix for structured logging.
|
|
12
|
+
SERVICE_TAG = 'CongregaPlenum::PartiesService'
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# Shared client instance configured via {CongregaPlenum.configure}.
|
|
16
|
+
#
|
|
17
|
+
# @return [CongregaPlenum::Client]
|
|
18
|
+
def client
|
|
19
|
+
@client ||= CongregaPlenum::Client.instance
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Wraps {Client#get} to add service context without hiding request failures.
|
|
23
|
+
def api_get(endpoint, params = {})
|
|
24
|
+
client.get(endpoint, params)
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
log_error("Erro ao acessar API em #{endpoint}: #{e.message}")
|
|
27
|
+
raise
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Bulk variant of {.api_get}. Failures are propagated so consumers can
|
|
31
|
+
# abort an incomplete synchronization.
|
|
32
|
+
def api_get_paginated(endpoint, params = {})
|
|
33
|
+
client.get_paginated(endpoint, params)
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
log_error("Erro paginando API em #{endpoint}: #{e.message}")
|
|
36
|
+
raise
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Fetches every party registered na API, enriquecendo com payload detalhado.
|
|
40
|
+
#
|
|
41
|
+
# @return [Array<Hash>]
|
|
42
|
+
def fetch_all
|
|
43
|
+
log_info('Iniciando coleta de todos os partidos')
|
|
44
|
+
|
|
45
|
+
parties = api_get_paginated('partidos', itens: ITEMS_PER_PAGE)
|
|
46
|
+
|
|
47
|
+
log_info("Coletamos #{parties.length} partidos da listagem")
|
|
48
|
+
|
|
49
|
+
detailed_parties = build_detailed_parties(parties)
|
|
50
|
+
|
|
51
|
+
log_info("Finalizamos a coleta detalhada de #{detailed_parties.length} partidos")
|
|
52
|
+
detailed_parties
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Retrieves the detailed payload for a single party.
|
|
56
|
+
#
|
|
57
|
+
# @param party_id [Integer]
|
|
58
|
+
# @return [Hash, nil]
|
|
59
|
+
# @raise [CongregaPlenum::APIError] if +dados+ has an unexpected type
|
|
60
|
+
def fetch_by_id(party_id)
|
|
61
|
+
log_debug("Buscando partido #{party_id}")
|
|
62
|
+
|
|
63
|
+
response = api_get("partidos/#{party_id}")
|
|
64
|
+
extract_detail(response, "partido #{party_id}")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns a paginated list of parties, without triggering detail lookups.
|
|
68
|
+
#
|
|
69
|
+
# @param page [Integer]
|
|
70
|
+
# @param items_per_page [Integer]
|
|
71
|
+
# @return [Array<Hash>]
|
|
72
|
+
def fetch_list(page: 1, items_per_page: ITEMS_PER_PAGE)
|
|
73
|
+
log_debug("Buscando lista de partidos página #{page}")
|
|
74
|
+
|
|
75
|
+
response = api_get('partidos', pagina: page, itens: items_per_page)
|
|
76
|
+
response['dados'] || []
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def extract_detail(response, resource)
|
|
82
|
+
detail = response['dados']
|
|
83
|
+
return detail if detail.nil? || detail.is_a?(Hash)
|
|
84
|
+
|
|
85
|
+
raise APIError, "Resposta inválida para #{resource}: esperado Hash ou nil em dados"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Converts the lightweight list into the detailed payload expected by
|
|
89
|
+
# consumers. Extracted to simplify instrumentation/tests.
|
|
90
|
+
def build_detailed_parties(parties)
|
|
91
|
+
parties.each_with_index.with_object([]) do |(party, index), collected|
|
|
92
|
+
detailed_party = fetch_by_id(party['id'])
|
|
93
|
+
collected << detailed_party if detailed_party
|
|
94
|
+
log_progress(index, parties.length)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Emits periodic progress updates so users know long iterations are still
|
|
99
|
+
# alive.
|
|
100
|
+
def log_progress(index, total)
|
|
101
|
+
return unless ((index + 1) % PROGRESS_INTERVAL).zero?
|
|
102
|
+
|
|
103
|
+
log_info("Processamos #{index + 1}/#{total} partidos")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def logger
|
|
107
|
+
CongregaPlenum.configuration.logger
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def log_info(message)
|
|
111
|
+
logger.info("#{SERVICE_TAG}: #{message}")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def log_error(message)
|
|
115
|
+
logger.error("#{SERVICE_TAG}: #{message}")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def log_debug(message)
|
|
119
|
+
logger.debug("#{SERVICE_TAG}: #{message}")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|