belpost 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/.env.example +0 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/README.md +334 -0
- data/Rakefile +12 -0
- data/belpost.gemspec +37 -0
- data/lib/belpost/api_service.rb +233 -0
- data/lib/belpost/client.rb +71 -0
- data/lib/belpost/configuration.rb +15 -0
- data/lib/belpost/errors.rb +27 -0
- data/lib/belpost/models/api_response.rb +23 -0
- data/lib/belpost/models/customs_declaration.rb +64 -0
- data/lib/belpost/models/parcel.rb +21 -0
- data/lib/belpost/models/parcel_builder.rb +216 -0
- data/lib/belpost/retry.rb +26 -0
- data/lib/belpost/validations/parcel_schema.rb +157 -0
- data/lib/belpost/version.rb +5 -0
- data/lib/belpost.rb +37 -0
- metadata +94 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c53d02403855e383fbb7d309ddeeeec66b2065303321cdde544d0e4fa6aba93e
|
4
|
+
data.tar.gz: aef5e4c2911ae80fca64dff3fb347cbf270ca66ea82da3905d3446a65efd543e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cfef1ca328d78d3ff55335a2271ba505e02ef2a95af81924a43acc736fc1a09828197a5075bc501d219ed60847dcf606eea082a68d7617769a5cbc6a94395ff4
|
7
|
+
data.tar.gz: ffe48fcfa53026d756eccc552db0cdac37c17aed4b14489852d51a51d2a6ae97e9f931bbd7babcd2842232003f807152c550e315f7c9bee34446955994cb05f4
|
data/.env.example
ADDED
File without changes
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/README.md
ADDED
@@ -0,0 +1,334 @@
|
|
1
|
+
# Belpost
|
2
|
+
|
3
|
+
Клиент для работы с Belpochta API (Белпочта).
|
4
|
+
|
5
|
+
## Установка
|
6
|
+
|
7
|
+
Добавьте эту строку в Gemfile вашего приложения:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'belpost'
|
11
|
+
```
|
12
|
+
|
13
|
+
И выполните:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
$ bundle install
|
17
|
+
```
|
18
|
+
|
19
|
+
Или установите самостоятельно:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
$ gem install belpost
|
23
|
+
```
|
24
|
+
|
25
|
+
## Настройка
|
26
|
+
|
27
|
+
Настройте клиент для работы с API Белпочты:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require 'belpost'
|
31
|
+
|
32
|
+
Belpost.configure do |config|
|
33
|
+
config.jwt_token = 'ваш_jwt_токен_от_Белпочты'
|
34
|
+
config.base_url = 'https://api.belpost.by'
|
35
|
+
config.timeout = 30 # Таймаут в секундах (по умолчанию 10)
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
Вы также можете использовать переменные окружения:
|
40
|
+
|
41
|
+
```
|
42
|
+
BELPOST_JWT_TOKEN=ваш_jwt_токен_от_Белпочты
|
43
|
+
BELPOST_BASE_URL=https://api.belpost.by
|
44
|
+
BELPOST_TIMEOUT=30
|
45
|
+
```
|
46
|
+
|
47
|
+
## Использование
|
48
|
+
|
49
|
+
### Создание посылки
|
50
|
+
|
51
|
+
#### Базовый пример
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
client = Belpost::Client.new
|
55
|
+
|
56
|
+
parcel_data = {
|
57
|
+
parcel: {
|
58
|
+
type: "package",
|
59
|
+
attachment_type: "products",
|
60
|
+
measures: {
|
61
|
+
weight: 12
|
62
|
+
},
|
63
|
+
departure: {
|
64
|
+
country: "BY",
|
65
|
+
place: "post_office"
|
66
|
+
},
|
67
|
+
arrival: {
|
68
|
+
country: "BY",
|
69
|
+
place: "post_office"
|
70
|
+
}
|
71
|
+
},
|
72
|
+
addons: {
|
73
|
+
declared_value: {
|
74
|
+
currency: "BYN",
|
75
|
+
value: 100
|
76
|
+
},
|
77
|
+
cash_on_delivery: {
|
78
|
+
currency: "BYN",
|
79
|
+
value: 10
|
80
|
+
}
|
81
|
+
},
|
82
|
+
sender: {
|
83
|
+
type: "legal_person",
|
84
|
+
info: {
|
85
|
+
organization_name: "ООО \"Компания\"",
|
86
|
+
taxpayer_number: "123456789",
|
87
|
+
IBAN: "BY26BAPB30123418400100000000",
|
88
|
+
BIC: "BAPBBY2X",
|
89
|
+
bank: "ОАО 'БЕЛАГРОПРОМБАНК'"
|
90
|
+
},
|
91
|
+
location: {
|
92
|
+
code: "225212",
|
93
|
+
region: "Брестская",
|
94
|
+
district: "Березовский",
|
95
|
+
locality: {
|
96
|
+
type: "город",
|
97
|
+
name: "Береза"
|
98
|
+
},
|
99
|
+
road: {
|
100
|
+
type: "улица",
|
101
|
+
name: "Ленина"
|
102
|
+
},
|
103
|
+
building: "1",
|
104
|
+
housing: "",
|
105
|
+
apartment: ""
|
106
|
+
},
|
107
|
+
email: "test@example.com",
|
108
|
+
phone: "375291234567"
|
109
|
+
},
|
110
|
+
recipient: {
|
111
|
+
type: "natural_person",
|
112
|
+
info: {
|
113
|
+
first_name: "Иван",
|
114
|
+
second_name: "Иванович",
|
115
|
+
last_name: "Иванов"
|
116
|
+
},
|
117
|
+
location: {
|
118
|
+
code: "231365",
|
119
|
+
region: "Гродненская",
|
120
|
+
district: "Ивьевский",
|
121
|
+
locality: {
|
122
|
+
type: "деревня",
|
123
|
+
name: "Дуды"
|
124
|
+
},
|
125
|
+
road: {
|
126
|
+
type: "улица",
|
127
|
+
name: "Центральная"
|
128
|
+
},
|
129
|
+
building: "1",
|
130
|
+
housing: "",
|
131
|
+
apartment: ""
|
132
|
+
},
|
133
|
+
email: "",
|
134
|
+
phone: "375291234567"
|
135
|
+
}
|
136
|
+
}
|
137
|
+
|
138
|
+
response = client.create_parcel(parcel_data)
|
139
|
+
puts "Трекинг код: #{response["data"]["parcel"]["s10code"]}"
|
140
|
+
```
|
141
|
+
|
142
|
+
#### Использование ParcelBuilder
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
client = Belpost::Client.new
|
146
|
+
|
147
|
+
# Создание внутренней посылки
|
148
|
+
parcel_data = Belpost::Models::ParcelBuilder.new
|
149
|
+
.with_type("package")
|
150
|
+
.with_attachment_type("products")
|
151
|
+
.with_weight(1500) # вес в граммах
|
152
|
+
.with_dimensions(300, 200, 100) # длина, ширина, высота в мм
|
153
|
+
.to_country("BY")
|
154
|
+
.with_declared_value(100)
|
155
|
+
.with_cash_on_delivery(50)
|
156
|
+
.add_service(:simple_notification)
|
157
|
+
.add_service(:email_notification)
|
158
|
+
.from_legal_person("ООО \"Компания\"")
|
159
|
+
.with_sender_details(
|
160
|
+
taxpayer_number: "123456789",
|
161
|
+
bank: "ОАО 'БЕЛАГРОПРОМБАНК'",
|
162
|
+
iban: "BY26BAPB30123418400100000000",
|
163
|
+
bic: "BAPBBY2X"
|
164
|
+
)
|
165
|
+
.with_sender_location(
|
166
|
+
postal_code: "225212",
|
167
|
+
region: "Брестская",
|
168
|
+
district: "Березовский",
|
169
|
+
locality_type: "город",
|
170
|
+
locality_name: "Береза",
|
171
|
+
road_type: "улица",
|
172
|
+
road_name: "Ленина",
|
173
|
+
building: "1"
|
174
|
+
)
|
175
|
+
.with_sender_contact(
|
176
|
+
email: "test@example.com",
|
177
|
+
phone: "375291234567"
|
178
|
+
)
|
179
|
+
.to_natural_person(
|
180
|
+
first_name: "Иван",
|
181
|
+
last_name: "Иванов",
|
182
|
+
second_name: "Иванович"
|
183
|
+
)
|
184
|
+
.with_recipient_location(
|
185
|
+
postal_code: "231365",
|
186
|
+
region: "Гродненская",
|
187
|
+
district: "Ивьевский",
|
188
|
+
locality_type: "деревня",
|
189
|
+
locality_name: "Дуды",
|
190
|
+
road_type: "улица",
|
191
|
+
road_name: "Центральная",
|
192
|
+
building: "1"
|
193
|
+
)
|
194
|
+
.with_recipient_contact(
|
195
|
+
phone: "375291234567"
|
196
|
+
)
|
197
|
+
.build
|
198
|
+
|
199
|
+
response = client.create_parcel(parcel_data)
|
200
|
+
puts "Трекинг код: #{response["data"]["parcel"]["s10code"]}"
|
201
|
+
```
|
202
|
+
|
203
|
+
#### Создание международной посылки с таможенной декларацией
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
client = Belpost::Client.new
|
207
|
+
|
208
|
+
# Создание таможенной декларации
|
209
|
+
customs_declaration = Belpost::Models::CustomsDeclaration.new
|
210
|
+
customs_declaration.set_category("gift")
|
211
|
+
customs_declaration.set_price("USD", 50)
|
212
|
+
customs_declaration.add_item(
|
213
|
+
{
|
214
|
+
name: "Книга",
|
215
|
+
local: "Книга",
|
216
|
+
unit: {
|
217
|
+
local: "ШТ",
|
218
|
+
en: "PCS"
|
219
|
+
},
|
220
|
+
count: 1,
|
221
|
+
weight: 500,
|
222
|
+
price: {
|
223
|
+
currency: "USD",
|
224
|
+
value: 50
|
225
|
+
},
|
226
|
+
country: "BY"
|
227
|
+
}
|
228
|
+
)
|
229
|
+
|
230
|
+
# Создание международной посылки
|
231
|
+
parcel_data = Belpost::Models::ParcelBuilder.new
|
232
|
+
.with_type("package")
|
233
|
+
.with_attachment_type("products")
|
234
|
+
.with_weight(500)
|
235
|
+
.to_country("DE") # Германия
|
236
|
+
.with_declared_value(50, "USD")
|
237
|
+
.from_legal_person("ООО \"Компания\"")
|
238
|
+
.with_sender_location(
|
239
|
+
postal_code: "225212",
|
240
|
+
region: "Брестская",
|
241
|
+
district: "Березовский",
|
242
|
+
locality_type: "город",
|
243
|
+
locality_name: "Береза",
|
244
|
+
road_type: "улица",
|
245
|
+
road_name: "Ленина",
|
246
|
+
building: "1"
|
247
|
+
)
|
248
|
+
.with_sender_contact(
|
249
|
+
email: "test@example.com",
|
250
|
+
phone: "375291234567"
|
251
|
+
)
|
252
|
+
.to_natural_person(
|
253
|
+
first_name: "John",
|
254
|
+
last_name: "Doe"
|
255
|
+
)
|
256
|
+
.with_foreign_recipient_location(
|
257
|
+
postal_code: "10115",
|
258
|
+
locality: "Berlin",
|
259
|
+
address: "Unter den Linden 77"
|
260
|
+
)
|
261
|
+
.with_recipient_contact(
|
262
|
+
phone: "4901234567890"
|
263
|
+
)
|
264
|
+
.with_customs_declaration(customs_declaration)
|
265
|
+
.build
|
266
|
+
|
267
|
+
response = client.create_parcel(parcel_data)
|
268
|
+
puts "Трекинг код: #{response["data"]["parcel"]["s10code"]}"
|
269
|
+
```
|
270
|
+
|
271
|
+
### Получение списка доступных стран
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
client = Belpost::Client.new
|
275
|
+
countries = client.fetch_available_countries
|
276
|
+
puts countries
|
277
|
+
```
|
278
|
+
|
279
|
+
### Получение данных для валидации почтового отправления
|
280
|
+
|
281
|
+
```ruby
|
282
|
+
client = Belpost::Client.new
|
283
|
+
validation_data = client.validate_postal_delivery("BY")
|
284
|
+
puts validation_data
|
285
|
+
```
|
286
|
+
|
287
|
+
### Получение кодов HS для таможенного декларирования
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
client = Belpost::Client.new
|
291
|
+
hs_codes = client.fetch_hs_codes
|
292
|
+
puts hs_codes
|
293
|
+
```
|
294
|
+
|
295
|
+
## Обработка ошибок
|
296
|
+
|
297
|
+
Клиент может выбрасывать следующие исключения:
|
298
|
+
|
299
|
+
- `Belpost::ConfigurationError` - ошибка конфигурации
|
300
|
+
- `Belpost::ValidationError` - ошибка валидации данных
|
301
|
+
- `Belpost::ApiError` - базовая ошибка API
|
302
|
+
- `Belpost::AuthenticationError` - ошибка аутентификации
|
303
|
+
- `Belpost::InvalidRequestError` - ошибка запроса
|
304
|
+
- `Belpost::RateLimitError` - превышен лимит запросов
|
305
|
+
- `Belpost::ServerError` - ошибка сервера
|
306
|
+
- `Belpost::NetworkError` - сетевая ошибка
|
307
|
+
- `Belpost::TimeoutError` - таймаут запроса
|
308
|
+
|
309
|
+
Пример обработки ошибок:
|
310
|
+
|
311
|
+
```ruby
|
312
|
+
begin
|
313
|
+
client = Belpost::Client.new
|
314
|
+
response = client.create_parcel(parcel_data)
|
315
|
+
rescue Belpost::ValidationError => e
|
316
|
+
puts "Ошибка валидации: #{e.message}"
|
317
|
+
rescue Belpost::AuthenticationError => e
|
318
|
+
puts "Ошибка аутентификации: #{e.message}"
|
319
|
+
rescue Belpost::ApiError => e
|
320
|
+
puts "Ошибка API: #{e.message}"
|
321
|
+
end
|
322
|
+
```
|
323
|
+
|
324
|
+
## Документация
|
325
|
+
|
326
|
+
Полная документация по API Белпочты доступна в официальной документации.
|
327
|
+
|
328
|
+
## Разработка
|
329
|
+
|
330
|
+
После клонирования репозитория выполните `bin/setup` для установки зависимостей. Затем выполните `rake spec` для запуска тестов.
|
331
|
+
|
332
|
+
## Contributing
|
333
|
+
|
334
|
+
Bug reports and pull requests are welcome on GitHub.
|
data/Rakefile
ADDED
data/belpost.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/belpost/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "belpost"
|
7
|
+
spec.version = Belpost::VERSION
|
8
|
+
spec.authors = ["KuberLite"]
|
9
|
+
spec.email = ["kuberlite@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Belpost API wrapper"
|
12
|
+
spec.description = "Gem for working with the 'Belpost' delivery service via API"
|
13
|
+
spec.homepage = "https://github.com/KuberLite/belpost"
|
14
|
+
spec.required_ruby_version = ">= 2.6.0"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.metadata = {
|
18
|
+
"bug_tracker_uri" => "https://github.com/KuberLite/belpost/issues",
|
19
|
+
"changelog_uri" => "https://github.com/KuberLite/belpost/releases",
|
20
|
+
"source_code_uri" => "https://github.com/belpost/evropochta",
|
21
|
+
"homepage_uri" => spec.homepage,
|
22
|
+
"rubygems_mfa_required" => "true"
|
23
|
+
}
|
24
|
+
|
25
|
+
spec.files = Dir.chdir(__dir__) do
|
26
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
(File.expand_path(f) == __FILE__) ||
|
28
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
spec.bindir = "exe"
|
32
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
33
|
+
spec.require_paths = ["lib"]
|
34
|
+
|
35
|
+
spec.add_dependency "dotenv"
|
36
|
+
spec.add_dependency "dry-validation", "~> 1.0"
|
37
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "json"
|
5
|
+
require "logger"
|
6
|
+
|
7
|
+
module Belpost
|
8
|
+
# Service class for handling HTTP requests to the BelPost API.
|
9
|
+
class ApiService
|
10
|
+
# Initializes a new instance of the ApiService.
|
11
|
+
#
|
12
|
+
# @param base_url [String] The base URL of the API.
|
13
|
+
# @param jwt_token [String] The JWT token for authentication.
|
14
|
+
# @param timeout [Integer] The request timeout in seconds (default: 10).
|
15
|
+
# @param logger [Logger] The logger for logging requests and responses.
|
16
|
+
def initialize(base_url:, jwt_token:, timeout: 10, logger: Logger.new($stdout))
|
17
|
+
@base_url = base_url
|
18
|
+
@jwt_token = jwt_token
|
19
|
+
@timeout = timeout
|
20
|
+
@logger = logger
|
21
|
+
end
|
22
|
+
|
23
|
+
# Performs a GET request to the specified path.
|
24
|
+
#
|
25
|
+
# @param path [String] The API endpoint path.
|
26
|
+
# @return [Models::ApiResponse] The parsed JSON response from the API.
|
27
|
+
def get(path)
|
28
|
+
Retry.with_retry do
|
29
|
+
uri = URI("#{@base_url}#{path}")
|
30
|
+
request = Net::HTTP::Get.new(uri)
|
31
|
+
add_headers(request)
|
32
|
+
|
33
|
+
log_request(request)
|
34
|
+
response = execute_request(uri, request)
|
35
|
+
log_response(response)
|
36
|
+
|
37
|
+
begin
|
38
|
+
Models::ApiResponse.new(
|
39
|
+
data: JSON.parse(response.body),
|
40
|
+
status_code: response.code.to_i,
|
41
|
+
headers: response.to_hash
|
42
|
+
)
|
43
|
+
rescue JSON::ParserError => e
|
44
|
+
raise ParseError, "Failed to parse JSON response: #{e.message}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Performs a POST request to the specified path with the given body.
|
50
|
+
#
|
51
|
+
# @param path [String] The API endpoint path.
|
52
|
+
# @param body [Hash] The request body as a hash.
|
53
|
+
# @return [Models::ApiResponse] The parsed JSON response from the API.
|
54
|
+
def post(path, body)
|
55
|
+
Retry.with_retry do
|
56
|
+
uri = URI("#{@base_url}#{path}")
|
57
|
+
request = Net::HTTP::Post.new(uri)
|
58
|
+
add_headers(request)
|
59
|
+
request.body = body.to_json
|
60
|
+
|
61
|
+
log_request(request)
|
62
|
+
response = execute_request(uri, request)
|
63
|
+
log_response(response)
|
64
|
+
|
65
|
+
begin
|
66
|
+
Models::ApiResponse.new(
|
67
|
+
data: JSON.parse(response.body),
|
68
|
+
status_code: response.code.to_i,
|
69
|
+
headers: response.to_hash
|
70
|
+
)
|
71
|
+
rescue JSON::ParserError => e
|
72
|
+
raise ParseError, "Failed to parse JSON response: #{e.message}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Performs a PUT request to the specified path with the given body.
|
78
|
+
#
|
79
|
+
# @param path [String] The API endpoint path.
|
80
|
+
# @param body [Hash] The request body as a hash.
|
81
|
+
# @return [Models::ApiResponse] The parsed JSON response from the API.
|
82
|
+
def put(path, body)
|
83
|
+
Retry.with_retry do
|
84
|
+
uri = URI("#{@base_url}#{path}")
|
85
|
+
request = Net::HTTP::Put.new(uri)
|
86
|
+
add_headers(request)
|
87
|
+
request.body = body.to_json
|
88
|
+
|
89
|
+
log_request(request)
|
90
|
+
response = execute_request(uri, request)
|
91
|
+
log_response(response)
|
92
|
+
|
93
|
+
begin
|
94
|
+
Models::ApiResponse.new(
|
95
|
+
data: JSON.parse(response.body),
|
96
|
+
status_code: response.code.to_i,
|
97
|
+
headers: response.to_hash
|
98
|
+
)
|
99
|
+
rescue JSON::ParserError => e
|
100
|
+
raise ParseError, "Failed to parse JSON response: #{e.message}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Performs a DELETE request to the specified path.
|
106
|
+
#
|
107
|
+
# @param path [String] The API endpoint path.
|
108
|
+
# @return [Models::ApiResponse] The parsed JSON response from the API.
|
109
|
+
def delete(path)
|
110
|
+
Retry.with_retry do
|
111
|
+
uri = URI("#{@base_url}#{path}")
|
112
|
+
request = Net::HTTP::Delete.new(uri)
|
113
|
+
add_headers(request)
|
114
|
+
|
115
|
+
log_request(request)
|
116
|
+
response = execute_request(uri, request)
|
117
|
+
log_response(response)
|
118
|
+
|
119
|
+
begin
|
120
|
+
Models::ApiResponse.new(
|
121
|
+
data: JSON.parse(response.body),
|
122
|
+
status_code: response.code.to_i,
|
123
|
+
headers: response.to_hash
|
124
|
+
)
|
125
|
+
rescue JSON::ParserError => e
|
126
|
+
raise ParseError, "Failed to parse JSON response: #{e.message}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
# Adds standard headers to the HTTP request.
|
134
|
+
#
|
135
|
+
# @param request [Net::HTTP::Request] The HTTP request object.
|
136
|
+
def add_headers(request)
|
137
|
+
request["Authorization"] = "Bearer #{@jwt_token}"
|
138
|
+
request["Accept"] = "application/json"
|
139
|
+
request["Content-Type"] = "application/json"
|
140
|
+
end
|
141
|
+
|
142
|
+
# Executes the HTTP request and processes the response.
|
143
|
+
#
|
144
|
+
# @param uri [URI] The URI of the request.
|
145
|
+
# @param request [Net::HTTP::Request] The HTTP request object.
|
146
|
+
# @return [Net::HTTP::Response] The HTTP response object.
|
147
|
+
# @raise [Belpost::ApiError] If the API returns an error response.
|
148
|
+
def execute_request(uri, request)
|
149
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
150
|
+
http.use_ssl = true if uri.scheme == "https"
|
151
|
+
http.read_timeout = @timeout
|
152
|
+
|
153
|
+
begin
|
154
|
+
response = http.request(request)
|
155
|
+
handle_response(response)
|
156
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
157
|
+
raise RequestError, "Request timed out after #{@timeout} seconds"
|
158
|
+
rescue Net::HTTPError => e
|
159
|
+
case e.response.code
|
160
|
+
when "401", "403"
|
161
|
+
raise AuthenticationError.new(
|
162
|
+
"Authentication failed",
|
163
|
+
status_code: e.response.code.to_i,
|
164
|
+
response_body: e.response.body
|
165
|
+
)
|
166
|
+
when "429"
|
167
|
+
raise RateLimitError.new(
|
168
|
+
"Rate limit exceeded",
|
169
|
+
status_code: e.response.code.to_i,
|
170
|
+
response_body: e.response.body
|
171
|
+
)
|
172
|
+
when "400"
|
173
|
+
raise InvalidRequestError.new(
|
174
|
+
"Invalid request",
|
175
|
+
status_code: e.response.code.to_i,
|
176
|
+
response_body: e.response.body
|
177
|
+
)
|
178
|
+
else
|
179
|
+
raise ServerError.new(
|
180
|
+
"Server error",
|
181
|
+
status_code: e.response.code.to_i,
|
182
|
+
response_body: e.response.body
|
183
|
+
)
|
184
|
+
end
|
185
|
+
rescue StandardError => e
|
186
|
+
raise NetworkError, "Network error: #{e.message}"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def handle_response(response)
|
191
|
+
case response.code
|
192
|
+
when "200"
|
193
|
+
response
|
194
|
+
when "401", "403"
|
195
|
+
raise AuthenticationError.new(
|
196
|
+
"Authentication failed",
|
197
|
+
status_code: response.code.to_i,
|
198
|
+
response_body: response.body
|
199
|
+
)
|
200
|
+
when "429"
|
201
|
+
raise RateLimitError.new(
|
202
|
+
"Rate limit exceeded",
|
203
|
+
status_code: response.code.to_i,
|
204
|
+
response_body: response.body
|
205
|
+
)
|
206
|
+
when "400"
|
207
|
+
raise InvalidRequestError.new(
|
208
|
+
"Invalid request",
|
209
|
+
status_code: response.code.to_i,
|
210
|
+
response_body: response.body
|
211
|
+
)
|
212
|
+
else
|
213
|
+
raise ServerError.new(
|
214
|
+
"Server error",
|
215
|
+
status_code: response.code.to_i,
|
216
|
+
response_body: response.body
|
217
|
+
)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def log_request(request)
|
222
|
+
@logger.info("Making #{request.method} request to #{request.uri}")
|
223
|
+
@logger.debug("Request headers: #{request.to_hash}")
|
224
|
+
@logger.debug("Request body: #{request.body}") if request.body
|
225
|
+
end
|
226
|
+
|
227
|
+
def log_response(response)
|
228
|
+
@logger.info("Received response with status #{response.code}")
|
229
|
+
@logger.debug("Response headers: #{response.to_hash}")
|
230
|
+
@logger.debug("Response body: #{response.body}")
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|