freight_kit 0.1.21 → 0.1.22
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 +4 -4
- data/README.md +5 -3
- data/Rakefile +1 -3
- data/VERSION +1 -1
- data/freight_kit-0.1.21.gem +0 -0
- data/freight_kit.gemspec +8 -19
- data/lib/freight_kit/address_normalizer.rb +1 -3
- data/lib/freight_kit/api_clients/soap_client.rb +30 -5
- data/lib/freight_kit/carrier.rb +6 -7
- data/lib/freight_kit/carriers/abf_freight_system.rb +9 -26
- data/lib/freight_kit/carriers/aloha_freight_forwarders.rb +1 -5
- data/lib/freight_kit/carriers/amado_trucking.rb +1 -5
- data/lib/freight_kit/carriers/clear_lane_freight_systems.rb +6 -15
- data/lib/freight_kit/carriers/cpl_express.rb +1 -5
- data/lib/freight_kit/carriers/cross_country_freight_solutions.rb +6 -17
- data/lib/freight_kit/carriers/dependable_highway_express.rb +6 -18
- data/lib/freight_kit/carriers/forward_air.rb +4 -12
- data/lib/freight_kit/carriers/on_trac.rb +7 -25
- data/lib/freight_kit/carriers/peninsula_truck_lines.rb +4 -20
- data/lib/freight_kit/carriers/roadrunner_transportation_services.rb +2 -8
- data/lib/freight_kit/carriers/saia.rb +4 -19
- data/lib/freight_kit/carriers/southeastern_freight_lines.rb +4 -13
- data/lib/freight_kit/carriers/t_force_worldwide.rb +7 -19
- data/lib/freight_kit/carriers/total_quality_logistics.rb +2 -1
- data/lib/freight_kit/carriers/western_regional_delivery_service.rb +5 -11
- data/lib/freight_kit/carriers/xpo_logistics.rb +5 -17
- data/lib/freight_kit/errors/rate_limit_error.rb +12 -0
- data/lib/freight_kit/errors/unserviceable_accessorials_error.rb +1 -3
- data/lib/freight_kit/helpers/pickupable.rb +1 -10
- data/lib/freight_kit/http_client/httpi_adapter.rb +37 -0
- data/lib/freight_kit/http_client/safe_retries.rb +17 -1
- data/lib/freight_kit/http_client.rb +13 -9
- data/lib/freight_kit/models/credential.rb +2 -15
- data/lib/freight_kit/models/shipment.rb +1 -8
- data/lib/freight_kit/package.rb +3 -13
- data/lib/freight_kit/packaging.rb +1 -16
- data/lib/freight_kit/platforms/carrier_logistics.rb +5 -10
- data/lib/freight_kit/platforms/dsdc.rb +82 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/mutation/quote_create.rb +32 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/mutation/shipment_create.rb +29 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/broker_carrier.rb +37 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/broker_carriers.rb +45 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/broker_customer.rb +54 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/broker_customers.rb +62 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/carrier.rb +22 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/carriers.rb +30 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/customer.rb +20 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/customers.rb +28 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/invoice.rb +31 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/invoices.rb +39 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/shipment.rb +102 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/shipment_documents.rb +29 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client/query/shipments.rb +110 -0
- data/lib/freight_kit/platforms/next_tms/graphql_client.rb +22 -0
- data/lib/freight_kit/platforms/next_tms/orm/address.rb +22 -0
- data/lib/freight_kit/platforms/next_tms/orm/base.rb +120 -0
- data/lib/freight_kit/platforms/next_tms/orm/broker_carrier.rb +18 -0
- data/lib/freight_kit/platforms/next_tms/orm/broker_customer.rb +22 -0
- data/lib/freight_kit/platforms/next_tms/orm/carrier.rb +14 -0
- data/lib/freight_kit/platforms/next_tms/orm/collection.rb +335 -0
- data/lib/freight_kit/platforms/next_tms/orm/customer.rb +12 -0
- data/lib/freight_kit/platforms/next_tms/orm/invoice.rb +21 -0
- data/lib/freight_kit/platforms/next_tms/orm/record_not_found_error.rb +9 -0
- data/lib/freight_kit/platforms/next_tms/orm/shipment.rb +50 -0
- data/lib/freight_kit/platforms/next_tms/orm.rb +38 -0
- data/lib/freight_kit/platforms/next_tms.rb +414 -55
- data/lib/freight_kit/platforms/the_great_information_factory.rb +4 -12
- data/lib/freight_kit.rb +19 -0
- data/schemas/next_tms.json +110710 -0
- metadata +83 -5
|
@@ -17,11 +17,7 @@ module FreightKit
|
|
|
17
17
|
|
|
18
18
|
def contacts
|
|
19
19
|
[
|
|
20
|
-
Contact.new(
|
|
21
|
-
department: 'Dispatch - Mira Loma, CA',
|
|
22
|
-
id: 'dc14714f-e381-4b8b-97a6-f002c0897409',
|
|
23
|
-
phone: '+19513615900'
|
|
24
|
-
),
|
|
20
|
+
Contact.new(department: 'Dispatch - Mira Loma, CA', id: 'dc14714f-e381-4b8b-97a6-f002c0897409', phone: '+19513615900'),
|
|
25
21
|
Contact.new(department: 'Main', id: '22203680-060c-41db-ba65-9925e8fa381d', phone: '+14146151696')
|
|
26
22
|
]
|
|
27
23
|
end
|
|
@@ -398,9 +394,7 @@ module FreightKit
|
|
|
398
394
|
def parse_amount(amount)
|
|
399
395
|
negative = amount.start_with?('-$') || amount.start_with?('-')
|
|
400
396
|
|
|
401
|
-
['$', '-', ','].each
|
|
402
|
-
amount = amount.sub(char, '')
|
|
403
|
-
end
|
|
397
|
+
['$', '-', ','].each { |char| amount = amount.sub(char, '') }
|
|
404
398
|
|
|
405
399
|
return 0 if amount.blank?
|
|
406
400
|
|
|
@@ -265,9 +265,7 @@ module FreightKit
|
|
|
265
265
|
end
|
|
266
266
|
|
|
267
267
|
if declared_value.present?
|
|
268
|
-
request = request.deep_merge(
|
|
269
|
-
{ request: { DeclaredValue: declared_value, FullValueCoverage: declared_value } }
|
|
270
|
-
)
|
|
268
|
+
request = request.deep_merge({ request: { DeclaredValue: declared_value, FullValueCoverage: declared_value } })
|
|
271
269
|
end
|
|
272
270
|
|
|
273
271
|
save_request(request)
|
|
@@ -281,12 +279,7 @@ module FreightKit
|
|
|
281
279
|
end
|
|
282
280
|
|
|
283
281
|
def commit(action, request)
|
|
284
|
-
client_args = {
|
|
285
|
-
wsdl: request_url(action),
|
|
286
|
-
convert_request_keys_to: :none,
|
|
287
|
-
env_namespace: :soap,
|
|
288
|
-
element_form_default: :qualified
|
|
289
|
-
}
|
|
282
|
+
client_args = { wsdl: request_url(action), convert_request_keys_to: :none, env_namespace: :soap, element_form_default: :qualified }
|
|
290
283
|
|
|
291
284
|
call_args = { message: request_blueprint.deep_merge(request) }
|
|
292
285
|
|
|
@@ -307,11 +300,7 @@ module FreightKit
|
|
|
307
300
|
end
|
|
308
301
|
|
|
309
302
|
def parse_api_location(api_event)
|
|
310
|
-
Location.new(
|
|
311
|
-
city: api_event[:city]&.titleize,
|
|
312
|
-
province: api_event[:state]&.upcase,
|
|
313
|
-
country: ActiveUtils::Country.find('USA')
|
|
314
|
-
)
|
|
303
|
+
Location.new(city: api_event[:city]&.titleize, province: api_event[:state]&.upcase, country: ActiveUtils::Country.find('USA'))
|
|
315
304
|
end
|
|
316
305
|
|
|
317
306
|
def parse_rate_response(shipment:, response:)
|
|
@@ -477,11 +466,7 @@ module FreightKit
|
|
|
477
466
|
api_events = search_result.dig(:history, :history_item)
|
|
478
467
|
|
|
479
468
|
if api_events.blank?
|
|
480
|
-
shipment_events << ShipmentEvent.new(
|
|
481
|
-
date_time: pickup_date,
|
|
482
|
-
location: shipper_location,
|
|
483
|
-
type_code: :picked_up
|
|
484
|
-
)
|
|
469
|
+
shipment_events << ShipmentEvent.new(date_time: pickup_date, location: shipper_location, type_code: :picked_up)
|
|
485
470
|
else
|
|
486
471
|
api_events = [api_events] if api_events.is_a?(Hash)
|
|
487
472
|
|
|
@@ -160,11 +160,7 @@ module FreightKit
|
|
|
160
160
|
methods: { get_rate: :get, rates: :post },
|
|
161
161
|
use_ssl: false
|
|
162
162
|
}.freeze
|
|
163
|
-
JSON_HEADERS = {
|
|
164
|
-
Accept: 'application/json',
|
|
165
|
-
charset: 'utf-8',
|
|
166
|
-
'Content-Type' => 'application/x-www-form-urlencoded'
|
|
167
|
-
}.freeze
|
|
163
|
+
JSON_HEADERS = { Accept: 'application/json', charset: 'utf-8', 'Content-Type' => 'application/x-www-form-urlencoded' }.freeze
|
|
168
164
|
|
|
169
165
|
# Documents
|
|
170
166
|
|
|
@@ -296,12 +292,7 @@ module FreightKit
|
|
|
296
292
|
headers = headers.merge(options[:headers]) if options[:headers].present?
|
|
297
293
|
body = URI.encode_www_form(options[:body]) if options[:body].present?
|
|
298
294
|
|
|
299
|
-
request = {
|
|
300
|
-
url: options[:url].presence || build_url(action),
|
|
301
|
-
headers:,
|
|
302
|
-
method: API.dig(:methods, action),
|
|
303
|
-
body:
|
|
304
|
-
}
|
|
295
|
+
request = { url: options[:url].presence || build_url(action), headers:, method: API.dig(:methods, action), body: }
|
|
305
296
|
|
|
306
297
|
save_request(request)
|
|
307
298
|
request
|
|
@@ -317,9 +308,9 @@ module FreightKit
|
|
|
317
308
|
|
|
318
309
|
case method
|
|
319
310
|
when :post
|
|
320
|
-
HTTPClient.post(url, headers:, body:)
|
|
311
|
+
HTTPClient.post(url, headers:, body:, proxy_url:)
|
|
321
312
|
else
|
|
322
|
-
HTTPClient.get(url, headers:)
|
|
313
|
+
HTTPClient.get(url, headers:, proxy_url:)
|
|
323
314
|
end
|
|
324
315
|
end
|
|
325
316
|
|
|
@@ -190,16 +190,7 @@ module FreightKit
|
|
|
190
190
|
service:,
|
|
191
191
|
shipment:
|
|
192
192
|
)
|
|
193
|
-
request = build_pickup_request(
|
|
194
|
-
delivery_from:,
|
|
195
|
-
delivery_to:,
|
|
196
|
-
dispatcher:,
|
|
197
|
-
pickup_from:,
|
|
198
|
-
pickup_to:,
|
|
199
|
-
scac:,
|
|
200
|
-
service:,
|
|
201
|
-
shipment:
|
|
202
|
-
)
|
|
193
|
+
request = build_pickup_request(delivery_from:, delivery_to:, dispatcher:, pickup_from:, pickup_to:, scac:, service:, shipment:)
|
|
203
194
|
|
|
204
195
|
parse_pickup_response(commit(request))
|
|
205
196
|
end
|
|
@@ -399,9 +390,9 @@ module FreightKit
|
|
|
399
390
|
|
|
400
391
|
case method
|
|
401
392
|
when :post
|
|
402
|
-
HTTPClient.post(url, headers:, body:)
|
|
393
|
+
HTTPClient.post(url, headers:, body:, proxy_url:)
|
|
403
394
|
else
|
|
404
|
-
HTTPClient.get(url, headers:)
|
|
395
|
+
HTTPClient.get(url, headers:, proxy_url:)
|
|
405
396
|
end
|
|
406
397
|
end
|
|
407
398
|
|
|
@@ -468,8 +459,7 @@ module FreightKit
|
|
|
468
459
|
logout_url = 'https://rrd.mercurygate.net/MercuryGate/login/urlRedirect.jsp?Logout=true'
|
|
469
460
|
|
|
470
461
|
begin
|
|
471
|
-
browser.element(xpath: '//*[@id="__AppFrameBaseTable"]/tbody/tr[2]/td/div[4]')
|
|
472
|
-
.click
|
|
462
|
+
browser.element(xpath: '//*[@id="__AppFrameBaseTable"]/tbody/tr[2]/td/div[4]').click
|
|
473
463
|
rescue Selenium::WebDriver::Error::UnexpectedAlertOpenError => e
|
|
474
464
|
browser.close
|
|
475
465
|
|
|
@@ -484,8 +474,7 @@ module FreightKit
|
|
|
484
474
|
browser.iframe(name: 'AppBody').frame(id: 'Header').select(name: 'column').select('Primary Reference')
|
|
485
475
|
browser.iframe(name: 'AppBody').frame(id: 'Header').select(name: 'condition').select('=')
|
|
486
476
|
browser.iframe(name: 'AppBody').frame(id: 'Header').text_field(name: 'filter').set(tracking_number)
|
|
487
|
-
browser.iframe(name: 'AppBody').frame(id: 'Header').button(value: 'Find')
|
|
488
|
-
.click
|
|
477
|
+
browser.iframe(name: 'AppBody').frame(id: 'Header').button(value: 'Find').click
|
|
489
478
|
|
|
490
479
|
begin
|
|
491
480
|
browser.iframe(name: 'AppBody').frame(id: 'Detail').iframe(id: 'transportsWin')
|
|
@@ -505,8 +494,7 @@ module FreightKit
|
|
|
505
494
|
return document_response
|
|
506
495
|
end
|
|
507
496
|
|
|
508
|
-
html = browser.iframe(name: 'AppBody').frame(id: 'Detail').iframes[1].element(xpath: '/html/body/table[3]')
|
|
509
|
-
.html
|
|
497
|
+
html = browser.iframe(name: 'AppBody').frame(id: 'Detail').iframes[1].element(xpath: '/html/body/table[3]').html
|
|
510
498
|
html = Nokogiri::HTML(html)
|
|
511
499
|
|
|
512
500
|
browser.goto(logout_url)
|
|
@@ -529,7 +517,7 @@ module FreightKit
|
|
|
529
517
|
end
|
|
530
518
|
|
|
531
519
|
begin
|
|
532
|
-
response = HTTPClient.get(url)
|
|
520
|
+
response = HTTPClient.get(url, proxy_url:)
|
|
533
521
|
rescue StandardError => e
|
|
534
522
|
document_response.error = e
|
|
535
523
|
return document_response
|
|
@@ -214,7 +214,8 @@ module FreightKit
|
|
|
214
214
|
request[:url],
|
|
215
215
|
headers: request[:headers].merge(subscription_key_headers),
|
|
216
216
|
query: request[:query],
|
|
217
|
-
body: request[:body]
|
|
217
|
+
body: request[:body],
|
|
218
|
+
proxy_url:
|
|
218
219
|
)
|
|
219
220
|
|
|
220
221
|
parsed_response = JSON.parse(response.body)
|
|
@@ -70,9 +70,9 @@ module FreightKit
|
|
|
70
70
|
url = request_url(action)
|
|
71
71
|
|
|
72
72
|
response = if API.dig(:methods, action) == :post
|
|
73
|
-
options[:params].blank? ? HTTPClient.post(url) : HTTPClient.post(url, query: options[:params])
|
|
73
|
+
options[:params].blank? ? HTTPClient.post(url, proxy_url:) : HTTPClient.post(url, proxy_url:, query: options[:params])
|
|
74
74
|
else
|
|
75
|
-
HTTPClient.get(url)
|
|
75
|
+
HTTPClient.get(url, proxy_url:)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
response.parsed_response if response&.parsed_response
|
|
@@ -81,11 +81,7 @@ module FreightKit
|
|
|
81
81
|
def parse_api_city_state(str)
|
|
82
82
|
return if str.blank?
|
|
83
83
|
|
|
84
|
-
Location.new(
|
|
85
|
-
city: str[..-3].strip.titleize,
|
|
86
|
-
province: str[-2..].upcase,
|
|
87
|
-
country: ActiveUtils::Country.find('USA')
|
|
88
|
-
)
|
|
84
|
+
Location.new(city: str[..-3].strip.titleize, province: str[-2..].upcase, country: ActiveUtils::Country.find('USA'))
|
|
89
85
|
end
|
|
90
86
|
|
|
91
87
|
def parse_api_city_state_zip(str)
|
|
@@ -118,7 +114,7 @@ module FreightKit
|
|
|
118
114
|
document_response = DocumentResponse.new(request: URI.parse(url))
|
|
119
115
|
|
|
120
116
|
begin
|
|
121
|
-
response = HTTPClient.get(url)
|
|
117
|
+
response = HTTPClient.get(url, proxy_url:)
|
|
122
118
|
rescue StandardError => e
|
|
123
119
|
document_response.error = e
|
|
124
120
|
return document_response
|
|
@@ -152,9 +148,7 @@ module FreightKit
|
|
|
152
148
|
image_url = nil
|
|
153
149
|
browser.switch_window.use do
|
|
154
150
|
page_count = browser.element(xpath: '/html/body/form/div[3]/b/span').text.strip.to_i
|
|
155
|
-
(page_count - 1).times
|
|
156
|
-
browser.element(xpath: '/html/body/form/div[3]/input[2]').wait_until(&:present?).click
|
|
157
|
-
end
|
|
151
|
+
(page_count - 1).times { browser.element(xpath: '/html/body/form/div[3]/input[2]').wait_until(&:present?).click }
|
|
158
152
|
image_url = browser.element(css: '#cphMain_imgImage').attribute_value('src')
|
|
159
153
|
end
|
|
160
154
|
browser.close
|
|
@@ -17,11 +17,7 @@ module FreightKit
|
|
|
17
17
|
|
|
18
18
|
def contacts
|
|
19
19
|
[
|
|
20
|
-
Contact.new(
|
|
21
|
-
department: 'Customer Service - Kingman, AZ',
|
|
22
|
-
id: '5bb9caaa-ae84-4ee6-90ef-3c14f6d434c6',
|
|
23
|
-
phone: '+17609228255'
|
|
24
|
-
)
|
|
20
|
+
Contact.new(department: 'Customer Service - Kingman, AZ', id: '5bb9caaa-ae84-4ee6-90ef-3c14f6d434c6', phone: '+17609228255')
|
|
25
21
|
]
|
|
26
22
|
end
|
|
27
23
|
|
|
@@ -159,11 +155,7 @@ module FreightKit
|
|
|
159
155
|
def website_url = 'https://www.xpo.com'
|
|
160
156
|
end
|
|
161
157
|
|
|
162
|
-
API = {
|
|
163
|
-
domain: 'api.ltl.xpo.com',
|
|
164
|
-
endpoints: { rates: '/rating/1.0/ratequotes', token: '/token' },
|
|
165
|
-
use_ssl: true
|
|
166
|
-
}.freeze
|
|
158
|
+
API = { domain: 'api.ltl.xpo.com', endpoints: { rates: '/rating/1.0/ratequotes', token: '/token' }, use_ssl: true }.freeze
|
|
167
159
|
|
|
168
160
|
# Documents
|
|
169
161
|
|
|
@@ -223,11 +215,7 @@ module FreightKit
|
|
|
223
215
|
def build_bearer_token_request
|
|
224
216
|
api_credentials = fetch_credential(:api)
|
|
225
217
|
|
|
226
|
-
body = URI.encode_www_form(
|
|
227
|
-
grant_type: 'password',
|
|
228
|
-
password: api_credentials.password,
|
|
229
|
-
username: api_credentials.username
|
|
230
|
-
)
|
|
218
|
+
body = URI.encode_www_form(grant_type: 'password', password: api_credentials.password, username: api_credentials.username)
|
|
231
219
|
|
|
232
220
|
{
|
|
233
221
|
body:,
|
|
@@ -301,9 +289,9 @@ module FreightKit
|
|
|
301
289
|
|
|
302
290
|
response = case method
|
|
303
291
|
when :post
|
|
304
|
-
HTTPClient.post(url, headers:, body:)
|
|
292
|
+
HTTPClient.post(url, headers:, body:, proxy_url:)
|
|
305
293
|
else
|
|
306
|
-
HTTPClient.get(url, headers:)
|
|
294
|
+
HTTPClient.get(url, headers:, proxy_url:)
|
|
307
295
|
end
|
|
308
296
|
|
|
309
297
|
begin
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module FreightKit
|
|
2
|
+
# The API refused a request because the caller sent too many. Wait {#retry_after} seconds before the next one.
|
|
3
|
+
class RateLimitError < HTTPError
|
|
4
|
+
attr_reader :retry_after
|
|
5
|
+
|
|
6
|
+
def initialize(body:, code:, retry_after: nil)
|
|
7
|
+
@retry_after = retry_after
|
|
8
|
+
|
|
9
|
+
super(body:, code:)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -9,9 +9,7 @@ module FreightKit
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def message
|
|
12
|
-
@message ||= "Unable to service #{@accessorials.map
|
|
13
|
-
accessorial.to_s.gsub('_', ' ')
|
|
14
|
-
end.join(', ')}"
|
|
12
|
+
@message ||= "Unable to service #{@accessorials.map { |accessorial| accessorial.to_s.gsub('_', ' ') }.join(', ')}"
|
|
15
13
|
end
|
|
16
14
|
end
|
|
17
15
|
end
|
|
@@ -10,16 +10,7 @@ module FreightKit
|
|
|
10
10
|
service:,
|
|
11
11
|
shipment:
|
|
12
12
|
)
|
|
13
|
-
request = build_pickup_request(
|
|
14
|
-
delivery_from:,
|
|
15
|
-
delivery_to:,
|
|
16
|
-
dispatcher:,
|
|
17
|
-
pickup_from:,
|
|
18
|
-
pickup_to:,
|
|
19
|
-
scac:,
|
|
20
|
-
service:,
|
|
21
|
-
shipment:
|
|
22
|
-
)
|
|
13
|
+
request = build_pickup_request(delivery_from:, delivery_to:, dispatcher:, pickup_from:, pickup_to:, scac:, service:, shipment:)
|
|
23
14
|
|
|
24
15
|
begin
|
|
25
16
|
# For SOAP APIs, the :action parameter is required
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module FreightKit
|
|
2
|
+
class HTTPClient
|
|
3
|
+
# HTTPI adapter sending Savon's requests through {HTTPClient}, so SOAP calls get its compression,
|
|
4
|
+
# connection reuse, retries, cache, logger and User-Agent:
|
|
5
|
+
#
|
|
6
|
+
# Savon.client(adapter: FreightKit::HTTPClient::HTTPIAdapter::NAME, wsdl: url)
|
|
7
|
+
class HTTPIAdapter < HTTPI::Adapter::Base
|
|
8
|
+
NAME = :freight_kit
|
|
9
|
+
|
|
10
|
+
register NAME
|
|
11
|
+
|
|
12
|
+
attr_reader :client
|
|
13
|
+
|
|
14
|
+
def initialize(request)
|
|
15
|
+
super
|
|
16
|
+
raise HTTPI::NotSupportedError, "#{NAME} does not support #{request.auth.type} authentication" if request.auth.type
|
|
17
|
+
|
|
18
|
+
@client = HTTPClient
|
|
19
|
+
@request = request
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def request(method)
|
|
23
|
+
response = client.request(
|
|
24
|
+
method,
|
|
25
|
+
@request.url.to_s,
|
|
26
|
+
body: @request.body,
|
|
27
|
+
headers: @request.headers.to_h,
|
|
28
|
+
proxy_url: @request.proxy&.to_s,
|
|
29
|
+
read_timeout: @request.read_timeout
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# HTTPX already decoded the body, and HTTPI would gunzip it again
|
|
33
|
+
HTTPI::Response.new(response.code, response.headers.to_h.except('content-encoding'), response.body)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -6,13 +6,29 @@ module FreightKit
|
|
|
6
6
|
# The persistent plugin retries any request failing with a connection error, but a POST may
|
|
7
7
|
# already have been processed by then, so only idempotent requests are retried.
|
|
8
8
|
# Streams refused by an HTTP/2 GOAWAY are resent by HTTPX itself, independently of retries.
|
|
9
|
+
#
|
|
10
|
+
# A 429 means the server did not process the request, so any request is retried once after its
|
|
11
|
+
# Retry-After seconds, unless that is longer than {MAX_RETRY_AFTER}.
|
|
9
12
|
module SafeRetries
|
|
10
13
|
IDEMPOTENT_METHODS = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT'].freeze
|
|
14
|
+
# Longest wait in seconds. Longer waits return the 429, so callers can reschedule instead of blocking.
|
|
15
|
+
MAX_RETRY_AFTER = 60
|
|
11
16
|
|
|
12
17
|
module InstanceMethods
|
|
13
18
|
private
|
|
14
19
|
|
|
15
|
-
def
|
|
20
|
+
def rate_limited?(response)
|
|
21
|
+
response.is_a?(HTTPX::Response) && response.status == 429 && retry_after(response)&.between?(0, MAX_RETRY_AFTER)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def retry_after(response) = Integer(response.headers['retry-after'], 10, exception: false)
|
|
25
|
+
|
|
26
|
+
def retryable_request?(request, response, *)
|
|
27
|
+
rate_limited?(response) || (super && IDEMPOTENT_METHODS.include?(request.verb))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def retryable_response?(response, *) = super || rate_limited?(response)
|
|
31
|
+
def when_to_retry(request, response, *) = rate_limited?(response) ? retry_after(response) : super
|
|
16
32
|
end
|
|
17
33
|
|
|
18
34
|
class << self
|
|
@@ -2,7 +2,7 @@ module FreightKit
|
|
|
2
2
|
# Thin wrapper around HTTPX that negotiates Brotli/Zstandard compression, keeps connections
|
|
3
3
|
# open between requests and revalidates cached responses with conditional requests (ETag/Last-Modified).
|
|
4
4
|
#
|
|
5
|
-
# The cache
|
|
5
|
+
# The cache defaults to {FreightKit.cache}, and the logger to the host Rails/Hanami app's logger. Both are hookable:
|
|
6
6
|
#
|
|
7
7
|
# FreightKit::HTTPClient.cache = Rails.cache
|
|
8
8
|
# FreightKit::HTTPClient.logger = Rails.logger
|
|
@@ -12,11 +12,11 @@ module FreightKit
|
|
|
12
12
|
@sessions = Concurrent::Map.new
|
|
13
13
|
|
|
14
14
|
class << self
|
|
15
|
-
def cache = @cache
|
|
15
|
+
def cache = @cache || FreightKit.cache
|
|
16
16
|
|
|
17
17
|
def cache=(store)
|
|
18
18
|
@cache = store
|
|
19
|
-
|
|
19
|
+
reset
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def get(url, **) = request(:get, url, **)
|
|
@@ -24,14 +24,15 @@ module FreightKit
|
|
|
24
24
|
|
|
25
25
|
def logger=(logger)
|
|
26
26
|
@logger = logger
|
|
27
|
-
|
|
27
|
+
reset
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def post(url, **) = request(:post, url, **)
|
|
31
31
|
|
|
32
|
-
def request(verb, url, body: nil, headers: {},
|
|
32
|
+
def request(verb, url, body: nil, headers: {}, proxy_url: nil, query: nil, read_timeout: nil)
|
|
33
33
|
options = { headers: }
|
|
34
34
|
options[:params] = query if query.present?
|
|
35
|
+
options[:timeout] = { read_timeout: } if read_timeout
|
|
35
36
|
|
|
36
37
|
# Like HTTParty, hashes are form-encoded regardless of the Content-Type header
|
|
37
38
|
if body.is_a?(Hash)
|
|
@@ -40,12 +41,15 @@ module FreightKit
|
|
|
40
41
|
options[:body] = body
|
|
41
42
|
end
|
|
42
43
|
|
|
43
|
-
response = session(
|
|
44
|
+
response = session(proxy_url.presence).request(verb.to_s.upcase, url, **options)
|
|
44
45
|
raise response.error if response.is_a?(HTTPX::ErrorResponse)
|
|
45
46
|
|
|
46
47
|
Response.new(response)
|
|
47
48
|
end
|
|
48
49
|
|
|
50
|
+
# Rebuilds sessions on their next request, e.g. after the cache or logger changes.
|
|
51
|
+
def reset = @sessions.clear
|
|
52
|
+
|
|
49
53
|
private
|
|
50
54
|
|
|
51
55
|
def framework_logger
|
|
@@ -57,8 +61,8 @@ module FreightKit
|
|
|
57
61
|
end
|
|
58
62
|
|
|
59
63
|
# Sessions own their connection pool, so they are built once (per proxy) and reused
|
|
60
|
-
def session(
|
|
61
|
-
@sessions.compute_if_absent(
|
|
64
|
+
def session(proxy_url)
|
|
65
|
+
@sessions.compute_if_absent(proxy_url) do
|
|
62
66
|
session = HTTPX.plugin(:brotli).plugin(Zstd).plugin(:follow_redirects)
|
|
63
67
|
.plugin(:response_cache, response_cache_store: CacheStore.new(cache))
|
|
64
68
|
.plugin(Revalidation)
|
|
@@ -68,7 +72,7 @@ module FreightKit
|
|
|
68
72
|
logger = self.logger
|
|
69
73
|
session = session.with(debug: LogDevice.new(logger), debug_level: 2) if logger.level <= ::Logger::DEBUG
|
|
70
74
|
|
|
71
|
-
|
|
75
|
+
proxy_url ? session.plugin(:proxy).with(proxy: { uri: proxy_url }) : session
|
|
72
76
|
end
|
|
73
77
|
end
|
|
74
78
|
end
|
|
@@ -51,7 +51,7 @@ module FreightKit
|
|
|
51
51
|
when :api, :website
|
|
52
52
|
{ password: String }
|
|
53
53
|
when :api_proxy
|
|
54
|
-
{
|
|
54
|
+
{ proxy_url: String }
|
|
55
55
|
when :oauth2
|
|
56
56
|
{ access_token: String, expires_at: ::DateTime, scope: String }
|
|
57
57
|
when :selenoid
|
|
@@ -96,20 +96,7 @@ module FreightKit
|
|
|
96
96
|
url.path = '/wd/hub/'
|
|
97
97
|
url = url.to_s
|
|
98
98
|
|
|
99
|
-
@watir_args = [
|
|
100
|
-
browser,
|
|
101
|
-
{
|
|
102
|
-
options: {
|
|
103
|
-
prefs: {
|
|
104
|
-
download: {
|
|
105
|
-
directory_upgrade: true,
|
|
106
|
-
prompt_for_download: false
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
url:
|
|
111
|
-
}
|
|
112
|
-
]
|
|
99
|
+
@watir_args = [browser, { options: { prefs: { download: { directory_upgrade: true, prompt_for_download: false } } }, url: }]
|
|
113
100
|
end
|
|
114
101
|
end
|
|
115
102
|
end
|
|
@@ -11,14 +11,7 @@ module FreightKit
|
|
|
11
11
|
# @!attribute po_number [String] Purchase order number (also known as PO #).
|
|
12
12
|
# @!attribute pickup_at [FreightKit::DateTime] Pickup date/time.
|
|
13
13
|
class Shipment < Model
|
|
14
|
-
attr_accessor :accessorials,
|
|
15
|
-
:declared_value_cents,
|
|
16
|
-
:destination,
|
|
17
|
-
:origin,
|
|
18
|
-
:order_number,
|
|
19
|
-
:packages,
|
|
20
|
-
:po_number,
|
|
21
|
-
:pro
|
|
14
|
+
attr_accessor :accessorials, :declared_value_cents, :destination, :origin, :order_number, :packages, :po_number, :pro
|
|
22
15
|
attr_reader :pickup_at
|
|
23
16
|
|
|
24
17
|
def hazmat?
|
data/lib/freight_kit/package.rb
CHANGED
|
@@ -64,13 +64,7 @@ module FreightKit # :nodoc:
|
|
|
64
64
|
|
|
65
65
|
@quantity = options[:quantity] || 1
|
|
66
66
|
|
|
67
|
-
@total_weight = attribute_from_metric_or_imperial(
|
|
68
|
-
total_grams_or_ounces,
|
|
69
|
-
Measured::Weight,
|
|
70
|
-
@weight_unit_system,
|
|
71
|
-
:grams,
|
|
72
|
-
:ounces
|
|
73
|
-
)
|
|
67
|
+
@total_weight = attribute_from_metric_or_imperial(total_grams_or_ounces, Measured::Weight, @weight_unit_system, :grams, :ounces)
|
|
74
68
|
|
|
75
69
|
@each_weight = attribute_from_metric_or_imperial(
|
|
76
70
|
total_grams_or_ounces / @quantity.to_f,
|
|
@@ -91,9 +85,7 @@ module FreightKit # :nodoc:
|
|
|
91
85
|
@dimensions = @dimensions.map do |l|
|
|
92
86
|
attribute_from_metric_or_imperial(l, Measured::Length, @dimensions_unit_system, :centimetres, :inches)
|
|
93
87
|
end
|
|
94
|
-
2.downto(@dimensions.length)
|
|
95
|
-
@dimensions.unshift(@dimensions[0])
|
|
96
|
-
end
|
|
88
|
+
2.downto(@dimensions.length) { |_n| @dimensions.unshift(@dimensions[0]) }
|
|
97
89
|
end
|
|
98
90
|
|
|
99
91
|
@value = Package.cents_from(options[:value])
|
|
@@ -274,9 +266,7 @@ module FreightKit # :nodoc:
|
|
|
274
266
|
# [1,2] => [1,1,2]
|
|
275
267
|
# [5] => [5,5,5]
|
|
276
268
|
# etc..
|
|
277
|
-
2.downto(@dimensions.length)
|
|
278
|
-
@dimensions.unshift(@dimensions[0])
|
|
279
|
-
end
|
|
269
|
+
2.downto(@dimensions.length) { |_n| @dimensions.unshift(@dimensions[0]) }
|
|
280
270
|
end
|
|
281
271
|
|
|
282
272
|
def sanitized_freight_class(freight_class)
|
|
@@ -1,22 +1,7 @@
|
|
|
1
1
|
module FreightKit
|
|
2
2
|
class Packaging
|
|
3
3
|
PALLET_TYPES = %i[crate drum pallet tote].freeze
|
|
4
|
-
VALID_TYPES = %i[
|
|
5
|
-
box
|
|
6
|
-
bundle
|
|
7
|
-
container
|
|
8
|
-
crate
|
|
9
|
-
cylinder
|
|
10
|
-
drum
|
|
11
|
-
luggage
|
|
12
|
-
pail
|
|
13
|
-
pallet
|
|
14
|
-
piece
|
|
15
|
-
roll
|
|
16
|
-
tote
|
|
17
|
-
truckload
|
|
18
|
-
tote
|
|
19
|
-
].freeze
|
|
4
|
+
VALID_TYPES = %i[box bundle container crate cylinder drum luggage pail pallet piece roll tote truckload tote].freeze
|
|
20
5
|
|
|
21
6
|
attr_accessor :type
|
|
22
7
|
|
|
@@ -114,14 +114,13 @@ module FreightKit
|
|
|
114
114
|
url = build_url(action, query:)
|
|
115
115
|
save_request(url)
|
|
116
116
|
|
|
117
|
-
HTTPClient.get(url)
|
|
117
|
+
HTTPClient.get(url, proxy_url:)
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
def map_response_errors(response, not_found_error: DocumentNotFoundError)
|
|
121
121
|
return ResponseError.new('Unknown response') if response.blank?
|
|
122
122
|
|
|
123
|
-
webspeed_error = (response.is_a?(String) || response.is_a?(HTTPClient::Response)) &&
|
|
124
|
-
response.include?('WebSpeed error')
|
|
123
|
+
webspeed_error = (response.is_a?(String) || response.is_a?(HTTPClient::Response)) && response.include?('WebSpeed error')
|
|
125
124
|
return ResponseError.new('Temporary error (WebSpeed error)') if webspeed_error
|
|
126
125
|
|
|
127
126
|
return if response.code == 200
|
|
@@ -132,9 +131,7 @@ module FreightKit
|
|
|
132
131
|
def parse_amount(amount)
|
|
133
132
|
negative = amount.include?('-')
|
|
134
133
|
|
|
135
|
-
['$', ',', '-'].each
|
|
136
|
-
amount = amount.sub(char, '')
|
|
137
|
-
end
|
|
134
|
+
['$', ',', '-'].each { |char| amount = amount.sub(char, '') }
|
|
138
135
|
|
|
139
136
|
return 0 if amount.blank?
|
|
140
137
|
|
|
@@ -220,7 +217,7 @@ module FreightKit
|
|
|
220
217
|
end
|
|
221
218
|
|
|
222
219
|
begin
|
|
223
|
-
response = HTTPClient.get(url)
|
|
220
|
+
response = HTTPClient.get(url, proxy_url:)
|
|
224
221
|
rescue StandardError => e
|
|
225
222
|
document_response.error = e
|
|
226
223
|
return document_response
|
|
@@ -283,9 +280,7 @@ module FreightKit
|
|
|
283
280
|
if self.class.overlength_fees_require_tariff?
|
|
284
281
|
cents = 0
|
|
285
282
|
|
|
286
|
-
shipment.packages.each
|
|
287
|
-
cents += overlength_fee(tariff, package)
|
|
288
|
-
end
|
|
283
|
+
shipment.packages.each { |package| cents += overlength_fee(tariff, package) }
|
|
289
284
|
|
|
290
285
|
prices << Price.new(blame: :tariff, cents:, description: 'Overlength fees') if cents.nonzero?
|
|
291
286
|
end
|