deepl-rb 3.9.0 → 3.10.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 +4 -4
- data/.gitlab-ci.yml +70 -52
- data/.rubocop.yml +26 -0
- data/CHANGELOG.md +429 -18
- data/Gemfile +4 -0
- data/README.md +981 -223
- data/Rakefile +18 -5
- data/VERSION +1 -1
- data/deepl-rb.gemspec +6 -6
- data/lib/deepl/admin_client.rb +183 -0
- data/lib/deepl/api.rb +30 -6
- data/lib/deepl/app_info.rb +20 -0
- data/lib/deepl/client.rb +969 -0
- data/lib/deepl/configuration.rb +56 -10
- data/lib/deepl/constants/admin_usage_group_by.rb +13 -0
- data/lib/deepl/constants/custom_tag_usage_aggregate_by.rb +13 -0
- data/lib/deepl/constants/document_status_code.rb +17 -0
- data/lib/deepl/constants/language_include.rb +17 -0
- data/lib/deepl/constants/language_resource_name.rb +21 -0
- data/lib/deepl/constants/tag_handling_version.rb +14 -0
- data/lib/deepl/deprecation.rb +42 -0
- data/lib/deepl/document_api.rb +41 -22
- data/lib/deepl/exceptions/bad_request.rb +1 -3
- data/lib/deepl/exceptions/connection_error.rb +37 -0
- data/lib/deepl/exceptions/document_not_ready_error.rb +17 -0
- data/lib/deepl/exceptions/document_translation_error.rb +17 -1
- data/lib/deepl/exceptions/error.rb +13 -1
- data/lib/deepl/exceptions/not_found.rb +8 -3
- data/lib/deepl/exceptions/not_supported.rb +6 -0
- data/lib/deepl/exceptions/request_error.rb +19 -2
- data/lib/deepl/exceptions/server_error.rb +7 -1
- data/lib/deepl/exceptions/validation_error.rb +14 -0
- data/lib/deepl/requests/admin/base.rb +94 -0
- data/lib/deepl/requests/admin/create_developer_key.rb +38 -0
- data/lib/deepl/requests/admin/custom_tag_usage_analytics.rb +38 -0
- data/lib/deepl/requests/admin/list_developer_keys.rb +28 -0
- data/lib/deepl/requests/admin/update_developer_key.rb +35 -0
- data/lib/deepl/requests/admin/usage_analytics.rb +36 -0
- data/lib/deepl/requests/base.rb +229 -59
- data/lib/deepl/requests/correct.rb +57 -0
- data/lib/deepl/requests/document/download.rb +11 -3
- data/lib/deepl/requests/document/get_status.rb +2 -2
- data/lib/deepl/requests/document/upload.rb +43 -10
- data/lib/deepl/requests/glossary/create.rb +6 -2
- data/lib/deepl/requests/glossary/language_pairs.rb +20 -4
- data/lib/deepl/requests/language_resources.rb +41 -0
- data/lib/deepl/requests/languages.rb +28 -4
- data/lib/deepl/requests/languages_for_resource.rb +54 -0
- data/lib/deepl/requests/multilingual_glossary/create.rb +37 -0
- data/lib/deepl/requests/multilingual_glossary/destroy.rb +36 -0
- data/lib/deepl/requests/multilingual_glossary/destroy_dictionary.rb +51 -0
- data/lib/deepl/requests/multilingual_glossary/entries.rb +66 -0
- data/lib/deepl/requests/multilingual_glossary/find.rb +35 -0
- data/lib/deepl/requests/multilingual_glossary/helpers.rb +63 -0
- data/lib/deepl/requests/multilingual_glossary/list.rb +39 -0
- data/lib/deepl/requests/multilingual_glossary/replace_dictionary.rb +41 -0
- data/lib/deepl/requests/multilingual_glossary/update.rb +40 -0
- data/lib/deepl/requests/rephrase.rb +5 -9
- data/lib/deepl/requests/spoken_terms/create.rb +37 -0
- data/lib/deepl/requests/spoken_terms/destroy.rb +36 -0
- data/lib/deepl/requests/spoken_terms/destroy_term_list.rb +41 -0
- data/lib/deepl/requests/spoken_terms/entries.rb +46 -0
- data/lib/deepl/requests/spoken_terms/find.rb +35 -0
- data/lib/deepl/requests/spoken_terms/helpers.rb +67 -0
- data/lib/deepl/requests/spoken_terms/list.rb +35 -0
- data/lib/deepl/requests/spoken_terms/replace_term_list.rb +41 -0
- data/lib/deepl/requests/spoken_terms/update.rb +40 -0
- data/lib/deepl/requests/style_profile/base.rb +83 -0
- data/lib/deepl/requests/style_profile/create.rb +35 -0
- data/lib/deepl/requests/style_profile/destroy.rb +34 -0
- data/lib/deepl/requests/style_profile/find.rb +33 -0
- data/lib/deepl/requests/style_profile/list.rb +43 -0
- data/lib/deepl/requests/style_profile/update.rb +35 -0
- data/lib/deepl/requests/style_rule/create.rb +1 -1
- data/lib/deepl/requests/style_rule/create_custom_instruction.rb +4 -2
- data/lib/deepl/requests/style_rule/destroy.rb +4 -2
- data/lib/deepl/requests/style_rule/destroy_custom_instruction.rb +5 -3
- data/lib/deepl/requests/style_rule/find.rb +4 -2
- data/lib/deepl/requests/style_rule/find_custom_instruction.rb +5 -3
- data/lib/deepl/requests/style_rule/helpers.rb +35 -0
- data/lib/deepl/requests/style_rule/list.rb +1 -1
- data/lib/deepl/requests/style_rule/update.rb +4 -2
- data/lib/deepl/requests/style_rule/update_configured_rules.rb +4 -2
- data/lib/deepl/requests/style_rule/update_custom_instruction.rb +5 -3
- data/lib/deepl/requests/translate.rb +52 -20
- data/lib/deepl/requests/translation_memory/base.rb +1 -4
- data/lib/deepl/requests/translation_memory/create_export.rb +1 -1
- data/lib/deepl/requests/translation_memory/create_import.rb +1 -1
- data/lib/deepl/requests/translation_memory/destroy.rb +1 -1
- data/lib/deepl/requests/translation_memory/download_export.rb +4 -0
- data/lib/deepl/requests/translation_memory/find.rb +1 -1
- data/lib/deepl/requests/translation_memory/find_job.rb +1 -1
- data/lib/deepl/requests/translation_memory/list.rb +2 -1
- data/lib/deepl/requests/translation_memory/segments.rb +3 -2
- data/lib/deepl/requests/translation_memory/storage_base.rb +40 -8
- data/lib/deepl/requests/translation_memory/upload_file.rb +1 -1
- data/lib/deepl/requests/usage.rb +28 -1
- data/lib/deepl/requests/write_improvements.rb +35 -0
- data/lib/deepl/resources/admin_usage_report.rb +73 -0
- data/lib/deepl/resources/custom_tag_usage_report.rb +59 -0
- data/lib/deepl/resources/developer_key.rb +57 -0
- data/lib/deepl/resources/document_handle.rb +24 -25
- data/lib/deepl/resources/document_translation_status.rb +23 -4
- data/lib/deepl/resources/language.rb +10 -2
- data/lib/deepl/resources/language_resource.rb +57 -0
- data/lib/deepl/resources/language_support.rb +56 -0
- data/lib/deepl/resources/multilingual_glossary.rb +112 -0
- data/lib/deepl/resources/spoken_terms.rb +61 -0
- data/lib/deepl/resources/style_profile.rb +44 -0
- data/lib/deepl/resources/style_rule.rb +3 -0
- data/lib/deepl/resources/text.rb +18 -2
- data/lib/deepl/resources/translation_memory.rb +8 -2
- data/lib/deepl/resources/translation_memory_import.rb +1 -1
- data/lib/deepl/resources/translation_memory_job.rb +23 -3
- data/lib/deepl/resources/translation_memory_segments.rb +8 -6
- data/lib/deepl/resources/usage.rb +93 -4
- data/lib/deepl/style_rule_api.rb +11 -0
- data/lib/deepl/translation_memory_api.rb +104 -24
- data/lib/deepl/utils/client_builder.rb +54 -0
- data/lib/deepl/utils/document_poller.rb +70 -0
- data/lib/deepl/utils/exception_builder.rb +3 -2
- data/lib/deepl/utils/http_client_builder.rb +79 -0
- data/lib/deepl/utils/number_parser.rb +34 -0
- data/lib/deepl/utils/term_validator.rb +33 -0
- data/lib/deepl/utils/time_parser.rb +14 -0
- data/lib/deepl.rb +166 -41
- data/lib/http_client_options.rb +21 -1
- data/lib/version.rb +1 -1
- data/spec/fixtures.rb +33 -0
- data/spec/offline/admin_client_spec.rb +551 -0
- data/spec/offline/client_spec.rb +273 -0
- data/spec/{api → offline}/configuration_spec.rb +45 -3
- data/spec/{constants → offline}/constants_spec.rb +23 -1
- data/spec/offline/correct_text_spec.rb +50 -0
- data/spec/offline/deprecation_spec.rb +32 -0
- data/spec/offline/errors_spec.rb +92 -0
- data/spec/offline/glossaries_spec.rb +98 -0
- data/spec/offline/http_client_spec.rb +319 -0
- data/spec/offline/languages_spec.rb +155 -0
- data/spec/offline/module_methods_spec.rb +13 -0
- data/spec/offline/multilingual_glossaries_spec.rb +358 -0
- data/spec/offline/offline_helper.rb +71 -0
- data/spec/offline/rephrase_text_spec.rb +73 -0
- data/spec/offline/spoken_terms_spec.rb +215 -0
- data/spec/offline/style_profiles_spec.rb +266 -0
- data/spec/offline/style_rules_spec.rb +318 -0
- data/spec/offline/translate_document_spec.rb +606 -0
- data/spec/offline/translate_text_spec.rb +286 -0
- data/spec/offline/translation_memories_client_spec.rb +256 -0
- data/spec/offline/translation_memories_spec.rb +463 -0
- data/spec/offline/usage_spec.rb +151 -0
- data/spec/real_api/admin_client_spec.rb +77 -0
- data/spec/real_api/correct_text_spec.rb +20 -0
- data/spec/real_api/glossaries_spec.rb +28 -0
- data/spec/real_api/languages_spec.rb +27 -0
- data/spec/real_api/multilingual_glossaries_spec.rb +53 -0
- data/spec/real_api/proxy_spec.rb +41 -0
- data/spec/real_api/real_api_helper.rb +65 -0
- data/spec/real_api/rephrase_text_spec.rb +16 -0
- data/spec/real_api/spoken_terms_spec.rb +56 -0
- data/spec/real_api/style_profiles_spec.rb +46 -0
- data/spec/real_api/style_rules_spec.rb +46 -0
- data/spec/real_api/translate_document_spec.rb +18 -0
- data/spec/real_api/translate_text_spec.rb +16 -0
- data/spec/real_api/translation_memories_spec.rb +56 -0
- data/spec/real_api/usage_spec.rb +20 -0
- data/spec/spec_helper.rb +1 -50
- metadata +116 -76
- data/.circleci/config.yml +0 -27
- data/.github/workflows/add_issues_to_kanban.yml +0 -16
- data/spec/api/api_spec.rb +0 -20
- data/spec/api/deepl_spec.rb +0 -262
- data/spec/integration_tests/document_api_spec.rb +0 -211
- data/spec/integration_tests/document_error_paths_spec.rb +0 -33
- data/spec/integration_tests/glossary_api_spec.rb +0 -114
- data/spec/integration_tests/glossary_error_paths_spec.rb +0 -107
- data/spec/integration_tests/integration_test_utils.rb +0 -178
- data/spec/integration_tests/languages_api_spec.rb +0 -54
- data/spec/integration_tests/languages_error_paths_spec.rb +0 -25
- data/spec/integration_tests/rephrase_api_spec.rb +0 -90
- data/spec/integration_tests/rephrase_error_paths_spec.rb +0 -53
- data/spec/integration_tests/smoke_test_spec.rb +0 -24
- data/spec/integration_tests/style_rule_api_spec.rb +0 -117
- data/spec/integration_tests/style_rule_error_paths_spec.rb +0 -45
- data/spec/integration_tests/translate_api_spec.rb +0 -117
- data/spec/integration_tests/translate_error_paths_spec.rb +0 -48
- data/spec/integration_tests/translation_memory_api_spec.rb +0 -244
- data/spec/integration_tests/translation_memory_error_paths_spec.rb +0 -87
- data/spec/integration_tests/usage_api_spec.rb +0 -29
- data/spec/integration_tests/usage_error_paths_spec.rb +0 -18
- data/spec/requests/document/upload_spec.rb +0 -130
- data/spec/requests/extra_body_parameters_types_spec.rb +0 -82
- data/spec/requests/glossary/create_spec.rb +0 -44
- data/spec/requests/glossary/destroy_spec.rb +0 -27
- data/spec/requests/glossary/entries_spec.rb +0 -27
- data/spec/requests/glossary/find_spec.rb +0 -28
- data/spec/requests/glossary/language_pairs_spec.rb +0 -27
- data/spec/requests/glossary/list_spec.rb +0 -27
- data/spec/requests/languages_spec.rb +0 -27
- data/spec/requests/rephrase_spec.rb +0 -46
- data/spec/requests/style_rule/create_custom_instruction_spec.rb +0 -30
- data/spec/requests/style_rule/create_spec.rb +0 -29
- data/spec/requests/style_rule/destroy_custom_instruction_spec.rb +0 -28
- data/spec/requests/style_rule/destroy_spec.rb +0 -27
- data/spec/requests/style_rule/find_custom_instruction_spec.rb +0 -29
- data/spec/requests/style_rule/find_spec.rb +0 -28
- data/spec/requests/style_rule/list_spec.rb +0 -27
- data/spec/requests/style_rule/update_configured_rules_spec.rb +0 -31
- data/spec/requests/style_rule/update_custom_instruction_spec.rb +0 -32
- data/spec/requests/style_rule/update_spec.rb +0 -29
- data/spec/requests/translate_spec.rb +0 -338
- data/spec/requests/translation_memory/create_export_spec.rb +0 -37
- data/spec/requests/translation_memory/create_import_spec.rb +0 -61
- data/spec/requests/translation_memory/destroy_spec.rb +0 -36
- data/spec/requests/translation_memory/download_export_spec.rb +0 -54
- data/spec/requests/translation_memory/find_job_spec.rb +0 -37
- data/spec/requests/translation_memory/find_spec.rb +0 -36
- data/spec/requests/translation_memory/list_spec.rb +0 -27
- data/spec/requests/translation_memory/segments_spec.rb +0 -58
- data/spec/requests/translation_memory/upload_file_spec.rb +0 -55
- data/spec/requests/usage_spec.rb +0 -27
- data/spec/resources/custom_instruction_spec.rb +0 -32
- data/spec/resources/glossary_spec.rb +0 -38
- data/spec/resources/language_pair_spec.rb +0 -23
- data/spec/resources/language_spec.rb +0 -45
- data/spec/resources/style_rule_spec.rb +0 -68
- data/spec/resources/text_spec.rb +0 -23
- data/spec/resources/translation_memory_export_spec.rb +0 -38
- data/spec/resources/translation_memory_import_spec.rb +0 -30
- data/spec/resources/translation_memory_job_spec.rb +0 -109
- data/spec/resources/translation_memory_segments_spec.rb +0 -78
- data/spec/resources/translation_memory_spec.rb +0 -52
- data/spec/resources/usage_spec.rb +0 -35
- data/spec/support/live_mock_server.rb +0 -12
- data/spec/support/managed_glossary.rb +0 -65
- data/spec/support/managed_style_rule.rb +0 -17
- data/spec/support/managed_translation_memory.rb +0 -55
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Copyright 2026 DeepL SE (https://www.deepl.com)
|
|
2
|
+
# Use of this source code is governed by an MIT
|
|
3
|
+
# license that can be found in the LICENSE file.
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
require 'logger'
|
|
7
|
+
require 'stringio'
|
|
8
|
+
require_relative 'offline_helper'
|
|
9
|
+
|
|
10
|
+
describe 'DeepL HTTP client' do
|
|
11
|
+
def stub_usage(*replies)
|
|
12
|
+
stub_request(:get, url('/v2/usage')).to_return(*replies)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe 'retries' do
|
|
16
|
+
it 'retries a 429 reply' do
|
|
17
|
+
stub = stub_usage(reply({}, 429), usage_reply)
|
|
18
|
+
|
|
19
|
+
DeepL.usage
|
|
20
|
+
expect(stub).to have_been_requested.twice
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'retries a 500 reply' do
|
|
24
|
+
stub = stub_usage(reply({}, 500), usage_reply)
|
|
25
|
+
|
|
26
|
+
DeepL.usage
|
|
27
|
+
|
|
28
|
+
expect(stub).to have_been_requested.twice
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'retries a 503 reply' do
|
|
32
|
+
stub = stub_usage(reply({}, 503), usage_reply)
|
|
33
|
+
|
|
34
|
+
DeepL.usage
|
|
35
|
+
|
|
36
|
+
expect(stub).to have_been_requested.twice
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'retries refused and reset connections, broken pipes and EOF' do
|
|
40
|
+
[Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE, EOFError].each do |error|
|
|
41
|
+
WebMock.reset!
|
|
42
|
+
stub = stub_request(:get, url('/v2/usage')).to_raise(error).then.to_return(usage_reply)
|
|
43
|
+
|
|
44
|
+
DeepL.usage
|
|
45
|
+
|
|
46
|
+
expect(stub).to have_been_requested.twice
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'does not retry a TLS failure or a malformed response' do
|
|
51
|
+
[OpenSSL::SSL::SSLError, Net::HTTPBadResponse].each do |error|
|
|
52
|
+
WebMock.reset!
|
|
53
|
+
stub = stub_request(:get, url('/v2/usage')).to_raise(error).then.to_return(usage_reply)
|
|
54
|
+
|
|
55
|
+
expect { DeepL.usage }.to raise_error(error)
|
|
56
|
+
expect(stub).to have_been_requested.once
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'raises a failed proxy CONNECT without retrying' do
|
|
61
|
+
error = Net::HTTPClientException.new('407 "Proxy Authentication Required"', nil)
|
|
62
|
+
stub = stub_request(:get, url('/v2/usage')).to_raise(error).then.to_return(usage_reply)
|
|
63
|
+
|
|
64
|
+
expect { DeepL.usage }.to raise_error(Net::HTTPClientException)
|
|
65
|
+
expect(stub).to have_been_requested.once
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'retries a body shorter than its Content-Length and raises EOFError' do
|
|
69
|
+
short = { status: 200, body: '{"char', headers: { 'Content-Length' => '46' } }
|
|
70
|
+
stub = stub_usage(short)
|
|
71
|
+
|
|
72
|
+
expect { DeepL.usage }.to raise_error(EOFError)
|
|
73
|
+
expect(stub).to have_been_requested.times(3)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'does not let Net::HTTP re-send a request or accept a short body' do # rubocop:disable RSpec/ExampleLength
|
|
77
|
+
clients = []
|
|
78
|
+
allow(Net::HTTP).to receive(:new).and_wrap_original do |new, *args|
|
|
79
|
+
new.call(*args).tap { clients << _1 }
|
|
80
|
+
end
|
|
81
|
+
upload_url = 'https://storage.example.test/upload?sig=abc'
|
|
82
|
+
stub_request(:put, upload_url).to_return(status: 200)
|
|
83
|
+
|
|
84
|
+
[nil, DeepL::HTTPClientOptions.new({})].each do |options|
|
|
85
|
+
client = DeepL::Client.new('abc', http_client_options: options)
|
|
86
|
+
client.upload_translation_memory_file(upload_url, 'tmx')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
expect(clients.map(&:max_retries)).to eq([0, 0, 0, 0])
|
|
90
|
+
# Net::HTTP#ignore_eof= exists from net-http 0.3 (Ruby 3.2).
|
|
91
|
+
next unless Net::HTTP.method_defined?(:ignore_eof=)
|
|
92
|
+
|
|
93
|
+
expect(clients.map(&:ignore_eof)).to eq([false, false, false, false])
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'trims the auth key' do
|
|
97
|
+
DeepL.configuration.auth_key = " k \n"
|
|
98
|
+
stub_request(:get, url('/v2/usage')).with(headers: auth_headers).to_return(usage_reply)
|
|
99
|
+
|
|
100
|
+
DeepL.usage
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'retries a network error' do
|
|
104
|
+
stub = stub_request(:get, url('/v2/usage')).to_raise(SocketError).then.to_return(usage_reply)
|
|
105
|
+
|
|
106
|
+
DeepL.usage
|
|
107
|
+
|
|
108
|
+
expect(stub).to have_been_requested.twice
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'retries a timeout' do
|
|
112
|
+
stub = stub_request(:get, url('/v2/usage')).to_timeout.then.to_return(usage_reply)
|
|
113
|
+
|
|
114
|
+
DeepL.usage
|
|
115
|
+
|
|
116
|
+
expect(stub).to have_been_requested.twice
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'raises the network error after max_network_retries retries' do
|
|
120
|
+
stub = stub_request(:get, url('/v2/usage')).to_timeout
|
|
121
|
+
|
|
122
|
+
expect { DeepL.usage }.to raise_error(Net::OpenTimeout)
|
|
123
|
+
expect(stub).to have_been_requested.times(3)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'raises LimitExceeded after max_network_retries retries' do
|
|
127
|
+
stub = stub_usage(reply({}, 429))
|
|
128
|
+
|
|
129
|
+
expect { DeepL.usage }.to raise_error(DeepL::Exceptions::LimitExceeded)
|
|
130
|
+
expect(stub).to have_been_requested.times(3)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'does not retry when max_network_retries is 0' do
|
|
134
|
+
DeepL.configuration.max_network_retries = 0
|
|
135
|
+
stub = stub_usage(reply({}, 429))
|
|
136
|
+
|
|
137
|
+
expect { DeepL.usage }.to raise_error(DeepL::Exceptions::LimitExceeded)
|
|
138
|
+
expect(stub).to have_been_requested.once
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
describe '.with_session' do
|
|
143
|
+
it 'runs the block without client options' do
|
|
144
|
+
stub = stub_usage(usage_reply)
|
|
145
|
+
|
|
146
|
+
DeepL.with_session { DeepL.usage }
|
|
147
|
+
|
|
148
|
+
expect(stub).to have_been_requested.once
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'raises ArgumentError without a block' do
|
|
152
|
+
expect { DeepL.with_session }.to raise_error(ArgumentError, /requires a block/)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'applies the proxy and the timeouts to the client inside the block' do
|
|
156
|
+
options = DeepL::HTTPClientOptions.new({ 'proxy_addr' => 'proxy.example.test',
|
|
157
|
+
'proxy_port' => 8080 },
|
|
158
|
+
nil, open_timeout: 3, read_timeout: 4)
|
|
159
|
+
stub_usage(usage_reply)
|
|
160
|
+
|
|
161
|
+
client = DeepL.with_session(options) do
|
|
162
|
+
DeepL.usage
|
|
163
|
+
DeepL.api.http_client
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
expect([client.proxy_address, client.proxy_port, client.open_timeout, client.read_timeout])
|
|
167
|
+
.to eq(['proxy.example.test', 8080, 3, 4])
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it 'enables SSL from the URL scheme and verifies certificates unless disabled' do
|
|
171
|
+
session_client = lambda do |host, verify|
|
|
172
|
+
DeepL.configuration.host = host
|
|
173
|
+
options = DeepL::HTTPClientOptions.new({}, nil, enable_ssl_verification: verify)
|
|
174
|
+
DeepL.with_session(options) { DeepL.api.http_client }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
expect(session_client.call('http://localhost:3000', true)).to have_attributes(use_ssl?: false)
|
|
178
|
+
expect(session_client.call('https://api.deepl.com', false))
|
|
179
|
+
.to have_attributes(use_ssl?: true, verify_mode: OpenSSL::SSL::VERIFY_NONE)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it 'uses no proxy when only the proxy address is given' do
|
|
183
|
+
options = DeepL::HTTPClientOptions.new({ 'proxy_addr' => 'proxy.example.test' })
|
|
184
|
+
|
|
185
|
+
expect(DeepL.with_session(options) { DeepL.api.http_client }.proxy_address).to be_nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'raises SocketError when the session cannot connect' do
|
|
189
|
+
http = Net::HTTP.new('unresolvable.invalid', 443)
|
|
190
|
+
allow(http).to receive(:start).and_raise(SocketError, 'getaddrinfo: Name not known')
|
|
191
|
+
allow(DeepL::Utils::HTTPClientBuilder).to receive(:build).and_return(http)
|
|
192
|
+
|
|
193
|
+
expect { DeepL.with_session { nil } }.to raise_error(SocketError)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it 'opens the session connection with the configured timeout' do
|
|
197
|
+
DeepL.configuration.timeout = 4
|
|
198
|
+
options = DeepL::HTTPClientOptions.new({}, nil, read_timeout: 7)
|
|
199
|
+
|
|
200
|
+
client = DeepL.with_session(options) { DeepL.api.http_client }
|
|
201
|
+
|
|
202
|
+
expect([client.open_timeout, client.read_timeout]).to eq([4, 7])
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it 'restores the previous client when the block ends' do
|
|
206
|
+
previous = DeepL.api.http_client
|
|
207
|
+
options = DeepL::HTTPClientOptions.new({ 'proxy_addr' => 'proxy.example.test',
|
|
208
|
+
'proxy_port' => 8080 })
|
|
209
|
+
stub_usage(usage_reply)
|
|
210
|
+
|
|
211
|
+
DeepL.with_session(options) { DeepL.usage }
|
|
212
|
+
|
|
213
|
+
expect(DeepL.api.http_client).to equal(previous)
|
|
214
|
+
expect(DeepL.api.http_client.proxy_address).to be_nil
|
|
215
|
+
expect(DeepL.http_client).to be_nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it 'restores the previous client when the block raises' do
|
|
219
|
+
previous = DeepL.api.http_client
|
|
220
|
+
|
|
221
|
+
expect { DeepL.with_session { raise 'boom' } }.to raise_error('boom')
|
|
222
|
+
expect(DeepL.api.http_client).to equal(previous)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
describe 'logging' do
|
|
227
|
+
let(:log) { StringIO.new }
|
|
228
|
+
|
|
229
|
+
it 'logs the request and the retry' do
|
|
230
|
+
DeepL.configuration.logger = Logger.new(log)
|
|
231
|
+
stub_usage(reply({}, 429), usage_reply)
|
|
232
|
+
|
|
233
|
+
DeepL.usage
|
|
234
|
+
|
|
235
|
+
expect(log.string).to include('Request to the DeepL API: GET /v2/usage')
|
|
236
|
+
.and include('Starting retry 1 for request GET /v2/usage')
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
context 'with a debug logger' do
|
|
240
|
+
before do
|
|
241
|
+
DeepL.configuration.auth_key = 'sdktest-secret-key'
|
|
242
|
+
DeepL.configuration.logger = Logger.new(log, level: Logger::DEBUG)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def expect_key_redacted
|
|
246
|
+
expect(log.string).not_to include('sdktest-secret-key')
|
|
247
|
+
expect(log.string).to include('DeepL-Auth-Key [REDACTED]')
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
it 'redacts the auth key in the request details' do
|
|
251
|
+
stub_usage(usage_reply)
|
|
252
|
+
|
|
253
|
+
DeepL.usage
|
|
254
|
+
|
|
255
|
+
expect_key_redacted
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it 'redacts the auth key in the details of a request type that overrides them' do
|
|
259
|
+
stub_request(:post, url('/v2/translate'))
|
|
260
|
+
.to_return(reply(translations: [{ text: 'Hallo' }]))
|
|
261
|
+
|
|
262
|
+
DeepL.translate('Hello', nil, 'DE')
|
|
263
|
+
|
|
264
|
+
expect_key_redacted
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
it 'redacts an Authorization header given with another casing' do
|
|
268
|
+
stub_request(:post, url('/v2/translate'))
|
|
269
|
+
.to_return(reply(translations: [{ text: 'Hallo' }]))
|
|
270
|
+
|
|
271
|
+
other = { 'authorization' => 'DeepL-Auth-Key sdktest-other-key' }
|
|
272
|
+
DeepL.translate('Hello', nil, 'DE', {}, other)
|
|
273
|
+
|
|
274
|
+
expect(log.string).not_to include('sdktest-other-key')
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
describe 'headers' do
|
|
280
|
+
it 'lets per-call headers replace the Authorization header in any casing' do
|
|
281
|
+
stub_request(:post, url('/v2/translate'))
|
|
282
|
+
.with(headers: { 'Authorization' => 'DeepL-Auth-Key other', 'X-Custom' => 'v' })
|
|
283
|
+
.to_return(reply(translations: [{ text: 'Hallo' }]))
|
|
284
|
+
|
|
285
|
+
DeepL.translate('Hello', nil, 'DE', {}, { 'authorization' => 'DeepL-Auth-Key other',
|
|
286
|
+
'X-Custom' => 'v' })
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
it "keeps the SDK's Host and Content-Type over per-call headers" do
|
|
290
|
+
sdk_host = ->(request) { [nil, 'api.deepl.com'].include?(request.headers['Host']) }
|
|
291
|
+
stub_request(:post, 'https://api.deepl.com/v2/translate')
|
|
292
|
+
.with(headers: { 'Content-Type' => 'application/json' }, &sdk_host)
|
|
293
|
+
.to_return(reply(translations: [{ text: 'Hallo' }]))
|
|
294
|
+
|
|
295
|
+
DeepL.translate('Hello', nil, 'DE', {}, { 'host' => 'other.example.test',
|
|
296
|
+
'content-type' => 'text/plain' })
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it 'sends no caller Content-Type with a request without a body' do
|
|
300
|
+
DeepL.configuration.headers = { 'content-type' => 'text/plain' }
|
|
301
|
+
stub_request(:get, url('/v2/usage'))
|
|
302
|
+
.with { |request| !request.headers.key?('Content-Type') }.to_return(usage_reply)
|
|
303
|
+
|
|
304
|
+
DeepL.usage
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
describe 'HTTPClientBuilder' do
|
|
309
|
+
it 'uses the proxy only when both address and port are given' do
|
|
310
|
+
partial = DeepL::HTTPClientOptions.new({ 'proxy_addr' => 'proxy.example.test' })
|
|
311
|
+
full = DeepL::HTTPClientOptions.new({ 'proxy_addr' => 'proxy.example.test',
|
|
312
|
+
'proxy_port' => 8080 })
|
|
313
|
+
build = ->(options) { DeepL::Utils::HTTPClientBuilder.build('https://api.deepl.com', options) }
|
|
314
|
+
|
|
315
|
+
expect(build.call(partial).proxy_address).to be_nil
|
|
316
|
+
expect(build.call(full).proxy_address).to eq('proxy.example.test')
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Copyright 2026 DeepL SE (https://www.deepl.com)
|
|
2
|
+
# Use of this source code is governed by an MIT
|
|
3
|
+
# license that can be found in the LICENSE file.
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
require_relative 'offline_helper'
|
|
7
|
+
|
|
8
|
+
describe 'languages' do
|
|
9
|
+
describe 'DeepL.languages' do
|
|
10
|
+
let(:languages_reply) do
|
|
11
|
+
reply([
|
|
12
|
+
{ lang: 'en', name: 'English', usable_as_source: true, usable_as_target: false },
|
|
13
|
+
{ lang: 'en-gb', name: 'English (British)', usable_as_source: false,
|
|
14
|
+
usable_as_target: true, features: { formality: { status: 'stable' } } },
|
|
15
|
+
{ lang: 'de', name: 'German', usable_as_source: true, usable_as_target: true,
|
|
16
|
+
features: { formality: { status: 'stable' } } },
|
|
17
|
+
{ lang: 'ja', name: 'Japanese', usable_as_source: true, usable_as_target: true }
|
|
18
|
+
])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def stub_languages
|
|
22
|
+
stub_request(:get, url('/v3/languages?resource=translate_text'))
|
|
23
|
+
.with(headers: json_headers).to_return(languages_reply)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'lists the source languages with upper-case codes by default' do
|
|
27
|
+
stub_languages
|
|
28
|
+
languages = DeepL.languages
|
|
29
|
+
|
|
30
|
+
expect(languages.map(&:code)).to eq(%w[EN DE JA])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'lists the target languages with the formality support of each' do
|
|
34
|
+
stub_languages
|
|
35
|
+
languages = DeepL.languages(type: :target)
|
|
36
|
+
|
|
37
|
+
expect(languages.map { |l| [l.code, l.supports_formality?] })
|
|
38
|
+
.to eq([['EN-GB', true], ['DE', true], ['JA', false]])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'accepts the type as a string' do
|
|
42
|
+
stub_languages
|
|
43
|
+
expect(DeepL.languages('type' => 'target').map(&:code)).to eq(%w[EN-GB DE JA])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'raises NotSupported for the formality support of a source language' do
|
|
47
|
+
stub_languages
|
|
48
|
+
expect { DeepL.languages(type: :source).first.supports_formality? }
|
|
49
|
+
.to raise_error(DeepL::Exceptions::NotSupported, /only available on target languages/)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'raises ValidationError for an unknown type without sending a request' do
|
|
53
|
+
expect { DeepL.languages(type: :both) }
|
|
54
|
+
.to raise_error(DeepL::Exceptions::ValidationError, /Unsupported `type`/)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe DeepL::Client do
|
|
59
|
+
let(:client) { described_class.new('client-key', server_url: 'https://client.example.test') }
|
|
60
|
+
let(:client_headers) do
|
|
61
|
+
{ 'Authorization' => 'DeepL-Auth-Key client-key', 'Content-Type' => 'application/json' }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def stub_client_get(path, body)
|
|
65
|
+
stub_request(:get, "https://client.example.test#{path}")
|
|
66
|
+
.with(headers: client_headers).to_return(reply(body))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def stub_client_languages
|
|
70
|
+
stub_client_get('/v3/languages?resource=translate_text',
|
|
71
|
+
[{ lang: 'en', name: 'English', usable_as_source: true,
|
|
72
|
+
usable_as_target: false },
|
|
73
|
+
{ lang: 'en-gb', name: 'English (British)', usable_as_source: false,
|
|
74
|
+
usable_as_target: true, features: { formality: { status: 'stable' } } },
|
|
75
|
+
{ lang: 'ja', name: 'Japanese', usable_as_source: true,
|
|
76
|
+
usable_as_target: true }])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it '#get_source_languages returns the source languages without formality support' do
|
|
80
|
+
stub_client_languages
|
|
81
|
+
|
|
82
|
+
languages = client.get_source_languages
|
|
83
|
+
|
|
84
|
+
expect(languages.map { |l| [l.code, l.name, l.supports_formality?] })
|
|
85
|
+
.to eq([['EN', 'English', nil], ['JA', 'Japanese', nil]])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it '#get_target_languages returns the target languages with their formality support' do
|
|
89
|
+
stub_client_languages
|
|
90
|
+
|
|
91
|
+
languages = client.get_target_languages
|
|
92
|
+
|
|
93
|
+
expect(languages.map { |l| [l.code, l.supports_formality?] })
|
|
94
|
+
.to eq([['EN-GB', true], ['JA', false]])
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it '#get_language_resources parses the resource name and its features' do
|
|
98
|
+
stub_client_get('/v3/languages/resources',
|
|
99
|
+
[{ name: 'glossary', features: [{ name: 'formality' }] }])
|
|
100
|
+
|
|
101
|
+
resource = client.get_language_resources.first
|
|
102
|
+
|
|
103
|
+
expect([resource.name, resource.to_s, resource.supports?(:formality)])
|
|
104
|
+
.to eq(['glossary', 'glossary', true])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it '#get_languages_for_resource sends the resource and returns the codes unchanged' do
|
|
108
|
+
stub_client_get('/v3/languages?resource=translate_document',
|
|
109
|
+
[{ lang: 'en-GB', name: 'English (British)', status: 'stable',
|
|
110
|
+
usable_as_source: false, usable_as_target: true }])
|
|
111
|
+
|
|
112
|
+
languages = client.get_languages_for_resource(
|
|
113
|
+
DeepL::Constants::LanguageResourceName::TRANSLATE_DOCUMENT
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
expect(languages.map(&:code)).to eq(['en-GB'])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it '#get_languages_for_resource sends one include parameter per value' do
|
|
120
|
+
# WebMock matches repeated query keys by their last value only, so the path of the sent
|
|
121
|
+
# request is asserted.
|
|
122
|
+
stub_client_get('/v3/languages?resource=voice&include=beta&include=external',
|
|
123
|
+
[{ lang: 'de', name: 'German' }])
|
|
124
|
+
|
|
125
|
+
languages = client.get_languages_for_resource(
|
|
126
|
+
:voice, include: [DeepL::Constants::LanguageInclude::BETA,
|
|
127
|
+
DeepL::Constants::LanguageInclude::EXTERNAL]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
expect(languages.first.request.path)
|
|
131
|
+
.to eq('/v3/languages?resource=voice&include=beta&include=external')
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it '#get_languages_for_resource sends a resource and include value without a constant' do
|
|
135
|
+
stub_client_get('/v3/languages?resource=new_resource&include=preview', [])
|
|
136
|
+
|
|
137
|
+
client.get_languages_for_resource('new_resource', include: 'preview')
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe 'DeepL.glossaries.language_pairs' do
|
|
142
|
+
it 'requests the glossary languages and pairs every source with every other target' do
|
|
143
|
+
stub_request(:get,
|
|
144
|
+
url('/v3/languages?resource=glossary')).with(headers: json_headers).to_return(
|
|
145
|
+
reply([{ lang: 'en', usable_as_source: true, usable_as_target: true },
|
|
146
|
+
{ lang: 'de', usable_as_source: true, usable_as_target: true },
|
|
147
|
+
{ lang: 'fr', usable_as_source: false, usable_as_target: true }])
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
pairs = DeepL.glossaries.language_pairs
|
|
151
|
+
|
|
152
|
+
expect(pairs.map(&:to_s)).to eq(['en - de', 'en - fr', 'de - en', 'de - fr'])
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2026 DeepL SE (https://www.deepl.com)
|
|
2
|
+
# Use of this source code is governed by an MIT
|
|
3
|
+
# license that can be found in the LICENSE file.
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
require_relative 'offline_helper'
|
|
7
|
+
|
|
8
|
+
describe DeepL do
|
|
9
|
+
it 'has no module-level methods for the operations added after 3.9.0' do
|
|
10
|
+
expect(%i[correct language_resources languages_for_resource multilingual_glossaries]
|
|
11
|
+
.map { |name| described_class.respond_to?(name) }).to eq([false] * 4)
|
|
12
|
+
end
|
|
13
|
+
end
|