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.
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CongregaPlenum
4
+ # Service for voting records produced by the Chamber's Plenary and committees.
5
+ #
6
+ # A voting is a single, finished decision. It is not the legislative event in
7
+ # which it occurred, and a proposition can be affected by several distinct
8
+ # votings of amendments, reports, requests and other related propositions.
9
+ # rubocop:disable Metrics/ClassLength
10
+ class VotingsService
11
+ # Maximum page size documented for the voting list endpoints.
12
+ ITEMS_PER_PAGE = 100
13
+ # Default ordering used by the official API.
14
+ DEFAULT_ORDER = 'DESC'
15
+ # Default field used by the official API to order voting records.
16
+ DEFAULT_ORDER_BY = 'dataHoraRegistro'
17
+ # Tag prefix for structured logging.
18
+ SERVICE_TAG = 'CongregaPlenum::VotingsService'
19
+
20
+ class << self
21
+ # Shared client configured through {CongregaPlenum.configure}.
22
+ #
23
+ # @return [CongregaPlenum::Client]
24
+ def client
25
+ @client ||= CongregaPlenum::Client.instance
26
+ end
27
+
28
+ # Wraps {Client#get} with voting-specific logging while preserving errors.
29
+ def api_get(endpoint, params = {})
30
+ client.get(endpoint, params)
31
+ rescue StandardError => e
32
+ log_error("Erro ao acessar API em #{endpoint}: #{e.message}")
33
+ raise
34
+ end
35
+
36
+ # Paginated variant of {.api_get}. Incomplete synchronizations are never
37
+ # converted into empty results.
38
+ def api_get_paginated(endpoint, params = {})
39
+ client.get_paginated(endpoint, params)
40
+ rescue StandardError => e
41
+ log_error("Erro paginando API em #{endpoint}: #{e.message}")
42
+ raise
43
+ end
44
+
45
+ # Retrieves all pages matching the supplied filters.
46
+ #
47
+ # Without date or identifier filters the Câmara API limits the result to
48
+ # votings from the previous 30 days. When both dates are supplied they
49
+ # must belong to the same calendar year, as required by the upstream API.
50
+ #
51
+ # @option filters [String,Integer,Array<String>,Array<Integer>] :voting_ids
52
+ # @option filters [Integer,Array<Integer>] :proposition_ids
53
+ # @option filters [Integer,Array<Integer>] :event_ids
54
+ # @option filters [Integer,Array<Integer>] :body_ids identifiers from +/orgaos+
55
+ # @option filters [String,Date] :start_date ISO 8601 date
56
+ # @option filters [String,Date] :end_date ISO 8601 date
57
+ # @option filters [String] :order +ASC+ or +DESC+
58
+ # @option filters [String] :order_by field accepted by the upstream endpoint
59
+ # @return [Array<Hash>]
60
+ def fetch_all(**filters)
61
+ log_info('Iniciando coleta de votações')
62
+
63
+ filters[:items_per_page] = ITEMS_PER_PAGE
64
+ votings = api_get_paginated('votacoes', list_params(filters))
65
+
66
+ log_info("Coletamos #{votings.size} votações")
67
+ votings
68
+ end
69
+
70
+ # Retrieves one page of basic voting records.
71
+ #
72
+ # @param page [Integer]
73
+ # @param items_per_page [Integer]
74
+ # @see .fetch_all for supported filters
75
+ # @return [Array<Hash>]
76
+ def fetch_list(page: 1, items_per_page: ITEMS_PER_PAGE, **filters)
77
+ filters.merge!(page: page, items_per_page: items_per_page)
78
+ params = list_params(filters)
79
+ extract_collection(api_get('votacoes', params), 'lista de votações')
80
+ end
81
+
82
+ # Retrieves the detailed representation of a voting.
83
+ #
84
+ # The detail may contain possible voting objects and propositions affected
85
+ # by the result. These relationships have different legislative meanings.
86
+ #
87
+ # @param voting_id [String,Integer] alphanumeric voting identifier
88
+ # @return [Hash,nil]
89
+ def fetch_by_id(voting_id)
90
+ extract_detail(api_get("votacoes/#{voting_id}"), "votação #{voting_id}")
91
+ end
92
+
93
+ # Lists the individual positions registered in an open nominal voting.
94
+ # An empty response does not identify absent deputies and is normal for
95
+ # symbolic votings.
96
+ #
97
+ # @param voting_id [String,Integer]
98
+ # @return [Array<Hash>]
99
+ def fetch_votes(voting_id)
100
+ response = api_get("votacoes/#{voting_id}/votos")
101
+ extract_collection(response, "votos da votação #{voting_id}")
102
+ end
103
+
104
+ # Lists recommendations issued by parties, blocs and other leaderships.
105
+ # The upstream API currently provides orientations only for Plenary votes.
106
+ #
107
+ # @param voting_id [String,Integer]
108
+ # @return [Array<Hash>]
109
+ def fetch_orientations(voting_id)
110
+ response = api_get("votacoes/#{voting_id}/orientacoes")
111
+ extract_collection(response, "orientações da votação #{voting_id}")
112
+ end
113
+
114
+ # Lists votings that had a proposition as their object or affected it.
115
+ #
116
+ # @param proposition_id [Integer]
117
+ # @param order [String]
118
+ # @param order_by [String]
119
+ # @return [Array<Hash>]
120
+ def fetch_by_proposition(proposition_id, order: DEFAULT_ORDER, order_by: DEFAULT_ORDER_BY)
121
+ params = { ordem: order, ordenarPor: order_by }
122
+ response = api_get("proposicoes/#{proposition_id}/votacoes", params)
123
+ extract_collection(response, "votações da proposição #{proposition_id}")
124
+ end
125
+
126
+ # Lists votings completed during a deliberative event.
127
+ #
128
+ # @param event_id [Integer]
129
+ # @return [Array<Hash>]
130
+ def fetch_by_event(event_id)
131
+ response = api_get("eventos/#{event_id}/votacoes")
132
+ extract_collection(response, "votações do evento #{event_id}")
133
+ end
134
+
135
+ # Retrieves all voting pages for a Chamber body such as the Plenary or a
136
+ # committee.
137
+ #
138
+ # @param body_id [Integer] identifier from +/orgaos+
139
+ # @param proposition_ids [Integer,Array<Integer>,nil]
140
+ # @param start_date [String,Date,nil]
141
+ # @param end_date [String,Date,nil]
142
+ # @param order [String]
143
+ # @param order_by [String]
144
+ # @return [Array<Hash>]
145
+ # rubocop:disable Metrics/ParameterLists
146
+ def fetch_by_body(body_id, proposition_ids: nil, start_date: nil, end_date: nil,
147
+ order: DEFAULT_ORDER, order_by: DEFAULT_ORDER_BY)
148
+ params = {
149
+ idProposicao: normalize_filter(proposition_ids),
150
+ dataInicio: normalize_date(start_date),
151
+ dataFim: normalize_date(end_date),
152
+ itens: ITEMS_PER_PAGE,
153
+ ordem: order,
154
+ ordenarPor: order_by
155
+ }.compact
156
+
157
+ api_get_paginated("orgaos/#{body_id}/votacoes", params)
158
+ end
159
+ # rubocop:enable Metrics/ParameterLists
160
+
161
+ private
162
+
163
+ def list_params(filters)
164
+ identifiers = {
165
+ id: normalize_filter(filters[:voting_ids]),
166
+ idProposicao: normalize_filter(filters[:proposition_ids]),
167
+ idEvento: normalize_filter(filters[:event_ids]),
168
+ idOrgao: normalize_filter(filters[:body_ids]),
169
+ dataInicio: normalize_date(filters[:start_date]),
170
+ dataFim: normalize_date(filters[:end_date])
171
+ }
172
+
173
+ identifiers.merge(pagination_params(filters)).compact
174
+ end
175
+
176
+ def pagination_params(filters)
177
+ {
178
+ pagina: filters[:page],
179
+ itens: filters.fetch(:items_per_page, ITEMS_PER_PAGE),
180
+ ordem: filters.fetch(:order, DEFAULT_ORDER),
181
+ ordenarPor: filters.fetch(:order_by, DEFAULT_ORDER_BY)
182
+ }
183
+ end
184
+
185
+ def normalize_filter(value)
186
+ return if value.nil?
187
+
188
+ value.is_a?(Array) ? value.join(',') : value
189
+ end
190
+
191
+ def normalize_date(value)
192
+ value&.to_s
193
+ end
194
+
195
+ def extract_detail(response, resource)
196
+ detail = response['dados']
197
+ return detail if detail.nil? || detail.is_a?(Hash)
198
+
199
+ raise APIError, "Resposta inválida para #{resource}: esperado Hash ou nil em dados"
200
+ end
201
+
202
+ def extract_collection(response, resource)
203
+ collection = response['dados']
204
+ return [] if collection.nil?
205
+ return collection if collection.is_a?(Array)
206
+
207
+ raise APIError, "Resposta inválida para #{resource}: esperado Array ou nil em dados"
208
+ end
209
+
210
+ def logger
211
+ CongregaPlenum.configuration.logger
212
+ end
213
+
214
+ def log_info(message)
215
+ logger.info("#{SERVICE_TAG}: #{message}")
216
+ end
217
+
218
+ def log_error(message)
219
+ logger.error("#{SERVICE_TAG}: #{message}")
220
+ end
221
+ end
222
+ end
223
+ # rubocop:enable Metrics/ClassLength
224
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CongregaPlenum
4
+ # Current gem version.
5
+ VERSION = '0.1.0'
6
+ end
@@ -0,0 +1,205 @@
1
+ module CongregaPlenum
2
+ VERSION: String
3
+ type params_hash = ::Hash[::Symbol, untyped]
4
+ type payload_hash = ::Hash[String, untyped]
5
+ type payload_list = ::Array[payload_hash]
6
+ type detail_payload = payload_hash?
7
+ type identifier_filter = Integer | String | ::Array[Integer] | ::Array[String]
8
+ type numeric_filter = Integer | ::Array[Integer]
9
+ type date_filter = String | Date
10
+
11
+ class Error < ::StandardError
12
+ end
13
+
14
+ class ConnectionError < Error
15
+ end
16
+
17
+ class APIError < Error
18
+ end
19
+
20
+ class ServerError < APIError
21
+ end
22
+
23
+ class RateLimitError < Error
24
+ end
25
+
26
+ @configuration: Configuration
27
+
28
+ def self.configuration: () -> Configuration
29
+ def self.configuration=: (Configuration) -> Configuration
30
+ def self.configure: () { (Configuration) -> void } -> void
31
+
32
+ class Configuration
33
+ attr_accessor base_url: String
34
+ attr_accessor timeout: Integer
35
+ attr_accessor retries: Integer
36
+ attr_accessor retry_delay: Float
37
+ attr_accessor rate_limit_delay: Float
38
+ attr_accessor logger: Logger
39
+
40
+ def initialize: () -> void
41
+ end
42
+
43
+ class HttpAdapter
44
+ attr_reader configuration: Configuration
45
+
46
+ def initialize: (?configuration: Configuration) -> void
47
+ def get: (String) -> ::Net::HTTPResponse
48
+ def build_http_client: (::URI::Generic) -> ::Net::HTTP
49
+ def build_http_request: (::URI::Generic) -> ::Net::HTTP::Get
50
+ def logger: () -> Logger
51
+ end
52
+
53
+ class RetryPolicy
54
+ attr_reader configuration: Configuration
55
+
56
+ def initialize: (?configuration: Configuration) -> void
57
+ def with_retries: [T] (String) { () -> T } -> T
58
+ def retry_request: (Integer, String, Error) -> void
59
+ def handle_retry_failure: (String, Error) -> bot
60
+ def calculate_backoff_delay: (Integer) -> Float
61
+ def max_retries: () -> Integer
62
+ def logger: () -> Logger
63
+ end
64
+
65
+ class ResponseHandler
66
+ def handle: (::Net::HTTPResponse, String) -> payload_hash
67
+ def parse_json_response: (::Net::HTTPResponse, String) -> payload_hash
68
+ end
69
+
70
+ class Client
71
+ include ::Singleton
72
+
73
+ def self.instance: () -> Client
74
+ attr_reader http_adapter: HttpAdapter
75
+ attr_reader retry_policy: RetryPolicy
76
+ attr_reader response_handler: ResponseHandler
77
+
78
+ def initialize: () -> void
79
+ def get: (String, ?params_hash) -> payload_hash
80
+ def get_paginated: (String, ?params_hash) -> payload_list
81
+ def each_response_page: (String, params_hash) { (payload_list) -> void } -> void
82
+ def request_page: (String, params_hash, Integer) -> payload_hash
83
+ def next_page?: (payload_hash) -> bool
84
+ def apply_rate_limit_delay: () -> void
85
+ def execute_request: (String) -> payload_hash
86
+ def build_url: (String, ?params_hash) -> String
87
+ def configuration: () -> Configuration
88
+ end
89
+
90
+ class CongressmenService
91
+ ITEMS_PER_PAGE: Integer
92
+ PROGRESS_INTERVAL: Integer
93
+ SERVICE_TAG: String
94
+
95
+ def self.client: () -> CongregaPlenum::Client
96
+ def self.api_get: (String, ?params_hash) -> payload_hash
97
+ def self.api_get_paginated: (String, ?params_hash) -> payload_list
98
+ def self.fetch_all: () -> payload_list
99
+ def self.fetch_all_by_legislature: (Integer) -> payload_list
100
+ def self.fetch_by_id: (Integer) -> detail_payload
101
+ def self.fetch_list: (?page: Integer, ?items_per_page: Integer, ?legislature_id: Integer?) -> payload_list
102
+ def self.fetch_deputies_collection: (context: String, params: params_hash) -> payload_list
103
+ def self.extract_detail: (payload_hash, String) -> detail_payload
104
+ def self.build_detailed_deputies: (payload_list, String) -> payload_list
105
+ def self.log_progress: (Integer, Integer, String) -> void
106
+ def self.logger: () -> Logger
107
+ def self.log_info: (String) -> void
108
+ def self.log_error: (String) -> void
109
+ def self.log_debug: (String) -> void
110
+ end
111
+
112
+ class PartiesService
113
+ ITEMS_PER_PAGE: Integer
114
+ PROGRESS_INTERVAL: Integer
115
+ SERVICE_TAG: String
116
+
117
+ def self.client: () -> CongregaPlenum::Client
118
+ def self.api_get: (String, ?params_hash) -> payload_hash
119
+ def self.api_get_paginated: (String, ?params_hash) -> payload_list
120
+ def self.fetch_all: () -> payload_list
121
+ def self.fetch_by_id: (Integer) -> detail_payload
122
+ def self.fetch_list: (?page: Integer, ?items_per_page: Integer) -> payload_list
123
+ def self.extract_detail: (payload_hash, String) -> detail_payload
124
+ def self.build_detailed_parties: (payload_list) -> payload_list
125
+ def self.log_progress: (Integer, Integer) -> void
126
+ def self.logger: () -> Logger
127
+ def self.log_info: (String) -> void
128
+ def self.log_error: (String) -> void
129
+ def self.log_debug: (String) -> void
130
+ end
131
+
132
+ class LegislaturesService
133
+ PAGE_SIZE: Integer
134
+ DEPUTIES_PAGE_SIZE: Integer
135
+ SERVICE_TAG: String
136
+
137
+ def self.client: () -> CongregaPlenum::Client
138
+ def self.api_get: (String, ?params_hash) -> payload_hash
139
+ def self.api_get_paginated: (String, ?params_hash) -> payload_list
140
+ def self.fetch_all: () -> payload_list
141
+ def self.fetch_by_id: (Integer) -> detail_payload
142
+ def self.fetch_mesa: (Integer) -> payload_list
143
+ def self.fetch_deputies: (Integer) -> payload_list
144
+ def self.extract_detail: (payload_hash, String) -> detail_payload
145
+ def self.logger: () -> Logger
146
+ def self.log_info: (String) -> void
147
+ def self.log_error: (String) -> void
148
+ def self.log_debug: (String) -> void
149
+ end
150
+
151
+ class VotingsService
152
+ ITEMS_PER_PAGE: Integer
153
+ DEFAULT_ORDER: String
154
+ DEFAULT_ORDER_BY: String
155
+ SERVICE_TAG: String
156
+
157
+ def self.client: () -> CongregaPlenum::Client
158
+ def self.api_get: (String, ?params_hash) -> payload_hash
159
+ def self.api_get_paginated: (String, ?params_hash) -> payload_list
160
+ def self.fetch_all: (?
161
+ voting_ids: identifier_filter?,
162
+ proposition_ids: numeric_filter?,
163
+ event_ids: numeric_filter?,
164
+ body_ids: numeric_filter?,
165
+ start_date: date_filter?,
166
+ end_date: date_filter?,
167
+ order: String,
168
+ order_by: String
169
+ ) -> payload_list
170
+ def self.fetch_list: (?
171
+ page: Integer,
172
+ items_per_page: Integer,
173
+ voting_ids: identifier_filter?,
174
+ proposition_ids: numeric_filter?,
175
+ event_ids: numeric_filter?,
176
+ body_ids: numeric_filter?,
177
+ start_date: date_filter?,
178
+ end_date: date_filter?,
179
+ order: String,
180
+ order_by: String
181
+ ) -> payload_list
182
+ def self.fetch_by_id: (String | Integer) -> detail_payload
183
+ def self.fetch_votes: (String | Integer) -> payload_list
184
+ def self.fetch_orientations: (String | Integer) -> payload_list
185
+ def self.fetch_by_proposition: (Integer, ?order: String, ?order_by: String) -> payload_list
186
+ def self.fetch_by_event: (Integer) -> payload_list
187
+ def self.fetch_by_body: (
188
+ Integer,
189
+ ?proposition_ids: numeric_filter?,
190
+ ?start_date: date_filter?,
191
+ ?end_date: date_filter?,
192
+ ?order: String,
193
+ ?order_by: String
194
+ ) -> payload_list
195
+ def self.list_params: (params_hash) -> params_hash
196
+ def self.pagination_params: (params_hash) -> params_hash
197
+ def self.normalize_filter: (identifier_filter?) -> (String | Integer)?
198
+ def self.normalize_date: (date_filter?) -> String?
199
+ def self.extract_detail: (payload_hash, String) -> detail_payload
200
+ def self.extract_collection: (payload_hash, String) -> payload_list
201
+ def self.logger: () -> Logger
202
+ def self.log_info: (String) -> void
203
+ def self.log_error: (String) -> void
204
+ end
205
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: congrega_plenum
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - zarbielli
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: HTTP client with pagination, retries and logging for Câmara dos Deputados
13
+ open data endpoints.
14
+ email:
15
+ - joao.zarbielli@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - CHANGELOG.md
21
+ - LICENSE
22
+ - README.md
23
+ - lib/adapters/http_adapter.rb
24
+ - lib/adapters/response_handler.rb
25
+ - lib/adapters/retry_policy.rb
26
+ - lib/client.rb
27
+ - lib/congrega_plenum.rb
28
+ - lib/modules/congressmen_service.rb
29
+ - lib/modules/legislatures_service.rb
30
+ - lib/modules/parties_service.rb
31
+ - lib/modules/votings_service.rb
32
+ - lib/version.rb
33
+ - sig/congrega_plenum.rbs
34
+ homepage: https://github.com/zarbielli/CongregaPlenum
35
+ licenses:
36
+ - GPL-2.0-only
37
+ metadata:
38
+ allowed_push_host: https://rubygems.org
39
+ homepage_uri: https://github.com/zarbielli/CongregaPlenum
40
+ source_code_uri: https://github.com/zarbielli/CongregaPlenum
41
+ changelog_uri: https://github.com/zarbielli/CongregaPlenum/blob/main/CHANGELOG.md
42
+ rubygems_mfa_required: 'true'
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 3.4.0
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 3.6.9
58
+ specification_version: 4
59
+ summary: Ruby client for Câmara dos Deputados open data API
60
+ test_files: []