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,94 @@
|
|
|
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.md file.
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
require 'date'
|
|
7
|
+
|
|
8
|
+
module DeepL
|
|
9
|
+
module Requests
|
|
10
|
+
module Admin
|
|
11
|
+
class Base < Requests::Base
|
|
12
|
+
def initialize(api, extra_body_parameters = nil)
|
|
13
|
+
options = {}
|
|
14
|
+
options[:extra_body_parameters] = extra_body_parameters unless extra_body_parameters.nil?
|
|
15
|
+
super(api, options)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_s
|
|
19
|
+
"#{http_method} #{uri.request_uri}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def http_method
|
|
25
|
+
'GET'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def get_request # rubocop:disable Naming/AccessorMethodName
|
|
29
|
+
Net::HTTP::Get.new(uri.request_uri, add_json_content_type(headers))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def uri
|
|
33
|
+
@uri ||= begin
|
|
34
|
+
base_uri = URI(url)
|
|
35
|
+
query_string = encode_query(query_params)
|
|
36
|
+
base_uri.query = query_string unless query_string.empty?
|
|
37
|
+
base_uri
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def query_params
|
|
42
|
+
{}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def date_range_params(start_date, end_date)
|
|
46
|
+
start_day = parse_date(start_date)
|
|
47
|
+
end_day = parse_date(end_date)
|
|
48
|
+
|
|
49
|
+
{ 'start_date' => start_day.iso8601, 'end_date' => end_day.iso8601 }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_date(value)
|
|
53
|
+
return value.new_offset(0).to_date if value.is_a?(DateTime)
|
|
54
|
+
return value if value.is_a?(Date)
|
|
55
|
+
return value.getutc.to_date if value.is_a?(Time)
|
|
56
|
+
|
|
57
|
+
date = parse_date_string(value) if value.is_a?(String)
|
|
58
|
+
return date if date
|
|
59
|
+
|
|
60
|
+
raise ArgumentError,
|
|
61
|
+
"Invalid date #{value.inspect}, expected a Date, a Time, a DateTime or " \
|
|
62
|
+
'a YYYY-MM-DD string'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse_date_string(value)
|
|
66
|
+
return nil unless value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
67
|
+
|
|
68
|
+
year, month, day = value.split('-').map(&:to_i)
|
|
69
|
+
Date.new(year, month, day) if Date.valid_date?(year, month, day)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def build_developer_key(request, response)
|
|
73
|
+
data = JSON.parse(response.body)
|
|
74
|
+
raise DeepL::Exceptions::Error, 'Developer key response is missing key_id' unless
|
|
75
|
+
usable_developer_key?(data)
|
|
76
|
+
|
|
77
|
+
Resources::DeveloperKey.new(data, request, response)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def usable_developer_key?(data)
|
|
81
|
+
data.is_a?(Hash) && data['key_id'].is_a?(String) && !data['key_id'].empty?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def parse_json_object(response)
|
|
85
|
+
data = JSON.parse(response.body)
|
|
86
|
+
raise DeepL::Exceptions::Error, 'Analytics response is not a JSON object' unless
|
|
87
|
+
data.is_a?(Hash)
|
|
88
|
+
|
|
89
|
+
data
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
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.md file.
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
module DeepL
|
|
7
|
+
module Requests
|
|
8
|
+
module Admin
|
|
9
|
+
class CreateDeveloperKey < Base
|
|
10
|
+
def initialize(api, label = nil, extra_body_parameters = nil)
|
|
11
|
+
super(api, extra_body_parameters)
|
|
12
|
+
@label = label
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def request
|
|
16
|
+
payload = {}
|
|
17
|
+
payload[:label] = @label unless @label.nil?
|
|
18
|
+
build_developer_key(*execute_request_with_retries(post_request(payload)))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def http_method
|
|
24
|
+
'POST'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def path
|
|
28
|
+
'admin/developer-keys'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# A repeated creation request could create a second key.
|
|
32
|
+
def retry_only_unprocessed?
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
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.md file.
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
module DeepL
|
|
7
|
+
module Requests
|
|
8
|
+
module Admin
|
|
9
|
+
class CustomTagUsageAnalytics < Base
|
|
10
|
+
def initialize(api, start_date, end_date, aggregate_by = nil, page = nil)
|
|
11
|
+
super(api)
|
|
12
|
+
@start_date = start_date
|
|
13
|
+
@end_date = end_date
|
|
14
|
+
@aggregate_by = aggregate_by
|
|
15
|
+
@page = page
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def request
|
|
19
|
+
request, response = execute_request_with_retries(get_request)
|
|
20
|
+
Resources::CustomTagUsageReport.new(parse_json_object(response), request, response)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def query_params
|
|
26
|
+
params = date_range_params(@start_date, @end_date)
|
|
27
|
+
params['aggregate_by'] = @aggregate_by.to_s unless @aggregate_by.nil?
|
|
28
|
+
params['page'] = @page.to_s unless @page.nil?
|
|
29
|
+
params
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def path
|
|
33
|
+
'admin/analytics/custom-tags'
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
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.md file.
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
module DeepL
|
|
7
|
+
module Requests
|
|
8
|
+
module Admin
|
|
9
|
+
class ListDeveloperKeys < Base
|
|
10
|
+
def request
|
|
11
|
+
request, response = execute_request_with_retries(get_request)
|
|
12
|
+
keys = JSON.parse(response.body)
|
|
13
|
+
raise DeepL::Exceptions::Error, 'Developer key list response is not a JSON array' unless
|
|
14
|
+
keys.is_a?(Array)
|
|
15
|
+
|
|
16
|
+
keys.select { |key| usable_developer_key?(key) }
|
|
17
|
+
.map { |key| Resources::DeveloperKey.new(key, request, response) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def path
|
|
23
|
+
'admin/developer-keys'
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
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.md file.
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
module DeepL
|
|
7
|
+
module Requests
|
|
8
|
+
module Admin
|
|
9
|
+
##
|
|
10
|
+
# Sends a PUT to +admin/developer-keys/<action>+. The payload is sent as is, so +nil+
|
|
11
|
+
# values are serialized as explicit JSON nulls; +extra_body_parameters+ are added last.
|
|
12
|
+
class UpdateDeveloperKey < Base
|
|
13
|
+
def initialize(api, action, payload, extra_body_parameters = nil)
|
|
14
|
+
super(api, extra_body_parameters)
|
|
15
|
+
@action = action
|
|
16
|
+
@payload = payload
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def request
|
|
20
|
+
build_developer_key(*execute_request_with_retries(put_request(@payload.dup)))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def http_method
|
|
26
|
+
'PUT'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def path
|
|
30
|
+
"admin/developer-keys/#{@action}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
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.md file.
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
module DeepL
|
|
7
|
+
module Requests
|
|
8
|
+
module Admin
|
|
9
|
+
class UsageAnalytics < Base
|
|
10
|
+
def initialize(api, start_date, end_date, group_by = nil)
|
|
11
|
+
super(api)
|
|
12
|
+
@start_date = start_date
|
|
13
|
+
@end_date = end_date
|
|
14
|
+
@group_by = group_by
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def request
|
|
18
|
+
request, response = execute_request_with_retries(get_request)
|
|
19
|
+
Resources::AdminUsageReport.new(parse_json_object(response), request, response)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def query_params
|
|
25
|
+
params = date_range_params(@start_date, @end_date)
|
|
26
|
+
params['group_by'] = @group_by.to_s unless @group_by.nil?
|
|
27
|
+
params
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def path
|
|
31
|
+
'admin/analytics'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/deepl/requests/base.rb
CHANGED
|
@@ -9,6 +9,41 @@ module DeepL
|
|
|
9
9
|
# Maximum number of glossaries that may be provided via the +glossary_ids+ option.
|
|
10
10
|
MAX_GLOSSARY_IDS = 5
|
|
11
11
|
|
|
12
|
+
# Transient network failures, which are retried: DNS (SocketError), timeouts
|
|
13
|
+
# (Net::OpenTimeout and Net::ReadTimeout are Timeout::Errors), refused or reset connections,
|
|
14
|
+
# broken pipes and unreachable hosts (SystemCallError), and connections closed early.
|
|
15
|
+
RETRYABLE_NETWORK_ERRORS = [Timeout::Error, SocketError, SystemCallError, EOFError].freeze
|
|
16
|
+
|
|
17
|
+
# Network failures without an HTTP response from the API. TLS failures, malformed responses
|
|
18
|
+
# and proxy failures (Net::ProtocolError, raised when a proxy refuses CONNECT, e.g. with 407)
|
|
19
|
+
# are not transient and are raised without retrying.
|
|
20
|
+
NETWORK_ERRORS = [*RETRYABLE_NETWORK_ERRORS, Net::HTTPBadResponse,
|
|
21
|
+
OpenSSL::SSL::SSLError, Net::ProtocolError].freeze
|
|
22
|
+
|
|
23
|
+
# Errors raised while building the result from a received response: the response has an
|
|
24
|
+
# unexpected format.
|
|
25
|
+
RESPONSE_FORMAT_ERRORS = [JSON::ParserError, NoMethodError, TypeError, KeyError].freeze
|
|
26
|
+
|
|
27
|
+
# Headers that describe the request the SDK builds; a caller's value is dropped.
|
|
28
|
+
TECHNICAL_HEADERS = %w[content-type content-length transfer-encoding host].freeze
|
|
29
|
+
|
|
30
|
+
# Wraps +request+ of every request class, so that a malformed response raises a
|
|
31
|
+
# DeepL::Exceptions::Error instead of a raw JSON or type error.
|
|
32
|
+
module ResponseFormatGuard
|
|
33
|
+
def request
|
|
34
|
+
super
|
|
35
|
+
rescue *RESPONSE_FORMAT_ERRORS => e
|
|
36
|
+
raise e unless @response_received
|
|
37
|
+
|
|
38
|
+
raise Exceptions::Error, "Unexpected response from the DeepL API: #{e.message}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.inherited(subclass)
|
|
43
|
+
super
|
|
44
|
+
subclass.prepend(ResponseFormatGuard)
|
|
45
|
+
end
|
|
46
|
+
|
|
12
47
|
attr_reader :api, :response, :options
|
|
13
48
|
|
|
14
49
|
def initialize(api, options = {}, additional_headers = {})
|
|
@@ -24,7 +59,7 @@ module DeepL
|
|
|
24
59
|
end
|
|
25
60
|
|
|
26
61
|
def details
|
|
27
|
-
"HTTP Headers #{
|
|
62
|
+
"HTTP Headers #{loggable_headers}"
|
|
28
63
|
end
|
|
29
64
|
|
|
30
65
|
private
|
|
@@ -34,11 +69,15 @@ module DeepL
|
|
|
34
69
|
end
|
|
35
70
|
|
|
36
71
|
def option(name)
|
|
37
|
-
options[name.to_s]
|
|
72
|
+
return options[name.to_s] if options.key?(name.to_s)
|
|
73
|
+
|
|
74
|
+
options[name.to_sym]
|
|
38
75
|
end
|
|
39
76
|
|
|
40
77
|
def delete_option(name)
|
|
41
|
-
options.delete(name.to_s)
|
|
78
|
+
return options.delete(name.to_s) if options.key?(name.to_s)
|
|
79
|
+
|
|
80
|
+
options.delete(name.to_sym)
|
|
42
81
|
end
|
|
43
82
|
|
|
44
83
|
def set_option(name, value)
|
|
@@ -64,7 +103,7 @@ module DeepL
|
|
|
64
103
|
|
|
65
104
|
extra_params = option(:extra_body_parameters)
|
|
66
105
|
extra_params.each do |key, value|
|
|
67
|
-
payload[key] = value
|
|
106
|
+
payload[key.to_s] = value
|
|
68
107
|
end
|
|
69
108
|
end
|
|
70
109
|
|
|
@@ -88,47 +127,116 @@ module DeepL
|
|
|
88
127
|
api.configuration.logger&.info("Request to the DeepL API: #{self}")
|
|
89
128
|
api.configuration.logger&.debug("Request details: #{details}")
|
|
90
129
|
loop do
|
|
91
|
-
resp =
|
|
92
|
-
|
|
130
|
+
resp = nil
|
|
131
|
+
client = http_client
|
|
132
|
+
apply_request_timeout(client)
|
|
133
|
+
resp = client.request(req) { |response| read_complete_body(response) }
|
|
134
|
+
validate_response!(resp, req)
|
|
135
|
+
@response_received = true
|
|
93
136
|
return [req, resp]
|
|
94
137
|
rescue DeepL::Exceptions::Error => e
|
|
95
138
|
raise e unless should_retry?(resp, e, @backoff_timer.num_retries)
|
|
96
139
|
|
|
97
|
-
|
|
98
|
-
api.configuration.logger&.info("Encountered a retryable exception: #{e.message}")
|
|
99
|
-
end
|
|
100
|
-
api.configuration.logger&.info("Starting retry #{@backoff_timer.num_retries + 1} for " \
|
|
101
|
-
"request #{self} after sleeping for " \
|
|
102
|
-
"#{format('%.2f', @backoff_timer.time_until_deadline)}")
|
|
103
|
-
files_to_reset.each(&:rewind)
|
|
104
|
-
@backoff_timer.sleep_until_deadline
|
|
140
|
+
wait_before_retry(e, files_to_reset)
|
|
105
141
|
next
|
|
106
|
-
rescue
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
end
|
|
111
|
-
api.configuration.logger&.info("Starting retry #{@backoff_timer.num_retries + 1} for " \
|
|
112
|
-
"request #{self} after sleeping for " \
|
|
113
|
-
"#{format('%.2f', @backoff_timer.time_until_deadline)}")
|
|
114
|
-
files_to_reset.each(&:rewind)
|
|
115
|
-
@backoff_timer.sleep_until_deadline
|
|
142
|
+
rescue *NETWORK_ERRORS => e
|
|
143
|
+
raise_network_error(e) unless retry_network_error?(e)
|
|
144
|
+
|
|
145
|
+
wait_before_retry(e, files_to_reset)
|
|
116
146
|
next
|
|
117
147
|
end
|
|
118
148
|
end
|
|
119
149
|
|
|
150
|
+
# Until the next major version, the module-level API raises the original network error.
|
|
151
|
+
def raise_network_error(error)
|
|
152
|
+
if api.client?
|
|
153
|
+
raise Exceptions::ConnectionError.from_network_error(
|
|
154
|
+
error, should_retry: !retry_only_unprocessed? || unprocessed_error?(error)
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
raise error
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Before net-http 0.3, Net::HTTP cannot raise at EOF of a short body (+ignore_eof+), so the
|
|
162
|
+
# body is compared with Content-Length. An inflated body has no comparable length.
|
|
163
|
+
def read_complete_body(response)
|
|
164
|
+
encoding = response['content-encoding']
|
|
165
|
+
expected = response.content_length unless response.chunked?
|
|
166
|
+
body = response.read_body
|
|
167
|
+
return if body.nil? || expected.nil? || !(encoding.nil? || encoding == 'identity')
|
|
168
|
+
return if body.bytesize >= expected
|
|
169
|
+
|
|
170
|
+
raise EOFError, "Response body ended after #{body.bytesize} of #{expected} bytes"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Per attempt, the timeout is the configured timeout or, if longer, the time until the
|
|
174
|
+
# backoff deadline. Timeouts given in HTTPClientOptions take precedence.
|
|
175
|
+
def apply_request_timeout(client) # rubocop:disable Metrics/AbcSize
|
|
176
|
+
timeout = api.configuration.timeout
|
|
177
|
+
return if timeout.nil? || !client.respond_to?(:read_timeout=)
|
|
178
|
+
|
|
179
|
+
timeout = [timeout.to_f, @backoff_timer.time_until_deadline].max
|
|
180
|
+
options = http_client_options_of(client)
|
|
181
|
+
client.open_timeout = timeout if options&.open_timeout.nil?
|
|
182
|
+
client.read_timeout = timeout if options&.read_timeout.nil?
|
|
183
|
+
client.write_timeout = timeout if options&.write_timeout.nil?
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# The HTTPClientOptions +client+ was built with, if any.
|
|
187
|
+
def http_client_options_of(client)
|
|
188
|
+
client.equal?(api.http_client) ? api.http_client_options : nil
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def wait_before_retry(error, files_to_reset)
|
|
192
|
+
logger = api.configuration.logger
|
|
193
|
+
logger&.info("Encountered a retryable exception: #{error.message}")
|
|
194
|
+
logger&.info("Starting retry #{@backoff_timer.num_retries + 1} for " \
|
|
195
|
+
"request #{self} after sleeping for " \
|
|
196
|
+
"#{format('%.2f', @backoff_timer.time_until_deadline)}")
|
|
197
|
+
files_to_reset.each(&:rewind)
|
|
198
|
+
@backoff_timer.sleep_until_deadline
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def retry_network_error?(error)
|
|
202
|
+
Exceptions::ConnectionError.transient?(error) &&
|
|
203
|
+
@backoff_timer.num_retries < api.configuration.max_network_retries &&
|
|
204
|
+
(!retry_only_unprocessed? || unprocessed_error?(error))
|
|
205
|
+
end
|
|
206
|
+
|
|
120
207
|
def should_retry?(response, exception, num_retries)
|
|
121
208
|
return false if num_retries >= api.configuration.max_network_retries
|
|
122
209
|
return exception.should_retry? if response.nil?
|
|
123
210
|
|
|
124
|
-
response.is_a?(Net::HTTPTooManyRequests)
|
|
211
|
+
return response.is_a?(Net::HTTPTooManyRequests) if retry_only_unprocessed?
|
|
212
|
+
|
|
213
|
+
response.is_a?(Net::HTTPTooManyRequests) || (500..599).cover?(response.code.to_i)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Requests that are unsafe to repeat return true to retry only failures that show the
|
|
217
|
+
# request was not processed: HTTP 429, DNS resolution errors (SocketError), refused
|
|
218
|
+
# connections and connection timeouts (Net::OpenTimeout).
|
|
219
|
+
def retry_only_unprocessed?
|
|
220
|
+
false
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def unprocessed_error?(error)
|
|
224
|
+
error.is_a?(SocketError) || error.is_a?(Net::OpenTimeout) ||
|
|
225
|
+
error.is_a?(Errno::ECONNREFUSED)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# The request body: the SDK's payload, then the options, then +extra_body_parameters+,
|
|
229
|
+
# which replace an SDK field of the same name.
|
|
230
|
+
def json_body(payload)
|
|
231
|
+
body = payload.merge(options_without_extra_params).transform_keys(&:to_s)
|
|
232
|
+
apply_extra_body_parameters_to_json(body)
|
|
233
|
+
body.to_json
|
|
125
234
|
end
|
|
126
235
|
|
|
127
236
|
def post_request(payload)
|
|
128
|
-
apply_extra_body_parameters_to_json(payload)
|
|
129
237
|
http_headers = add_json_content_type(headers)
|
|
130
238
|
post_req = Net::HTTP::Post.new(uri.path, http_headers)
|
|
131
|
-
post_req.body = payload
|
|
239
|
+
post_req.body = json_body(payload)
|
|
132
240
|
post_req
|
|
133
241
|
end
|
|
134
242
|
|
|
@@ -139,89 +247,131 @@ module DeepL
|
|
|
139
247
|
post_req
|
|
140
248
|
end
|
|
141
249
|
|
|
250
|
+
# GET and DELETE send no body, so no Content-Type either.
|
|
142
251
|
def get_request # rubocop:disable Naming/AccessorMethodName
|
|
143
|
-
|
|
144
|
-
get_req = Net::HTTP::Get.new(uri.path, http_headers)
|
|
145
|
-
get_req.body = options_without_extra_params.to_json
|
|
146
|
-
get_req
|
|
252
|
+
Net::HTTP::Get.new(uri.path, headers)
|
|
147
253
|
end
|
|
148
254
|
|
|
149
255
|
def patch_request(payload)
|
|
150
|
-
apply_extra_body_parameters_to_json(payload)
|
|
151
256
|
http_headers = add_json_content_type(headers)
|
|
152
257
|
req = Net::HTTP::Patch.new(uri.path, http_headers)
|
|
153
|
-
req.body = payload
|
|
258
|
+
req.body = json_body(payload)
|
|
154
259
|
req
|
|
155
260
|
end
|
|
156
261
|
|
|
157
262
|
def put_request(payload)
|
|
158
|
-
apply_extra_body_parameters_to_json(payload)
|
|
159
263
|
http_headers = add_json_content_type(headers)
|
|
160
264
|
req = Net::HTTP::Put.new(uri.path, http_headers)
|
|
161
|
-
req.body = payload
|
|
265
|
+
req.body = json_body(payload)
|
|
162
266
|
req
|
|
163
267
|
end
|
|
164
268
|
|
|
165
269
|
def delete_request
|
|
166
|
-
|
|
167
|
-
del_req = Net::HTTP::Delete.new(uri.path, http_headers)
|
|
168
|
-
del_req.body = options_without_extra_params.to_json
|
|
169
|
-
del_req
|
|
270
|
+
Net::HTTP::Delete.new(uri.path, headers)
|
|
170
271
|
end
|
|
171
272
|
|
|
172
|
-
def validate_response!(response)
|
|
273
|
+
def validate_response!(response, request = nil)
|
|
173
274
|
return if response.is_a?(Net::HTTPSuccess)
|
|
275
|
+
# A request that is unsafe to repeat may have been processed before the server failed.
|
|
276
|
+
if retry_only_unprocessed? && (500..599).cover?(response.code.to_i)
|
|
277
|
+
raise Exceptions::ServerError.new(response, request, should_retry: false)
|
|
278
|
+
end
|
|
174
279
|
|
|
175
|
-
raise Utils::ExceptionBuilder.new(response).build
|
|
280
|
+
raise Utils::ExceptionBuilder.new(response, request).build
|
|
176
281
|
end
|
|
177
282
|
|
|
178
283
|
# Validates the +glossary_ids+ option and resolves it to an array of glossary ID strings.
|
|
179
284
|
#
|
|
180
285
|
# The +glossary_ids+ option accepts an array of up to `MAX_GLOSSARY_IDS` glossary
|
|
181
|
-
# identifiers. Each entry may be a glossary ID string
|
|
182
|
-
# `DeepL::Resources::Glossary` object. It:
|
|
183
|
-
# *
|
|
184
|
-
# * cannot be combined with the singular +glossary_id+ option,
|
|
286
|
+
# identifiers. Each entry may be a glossary ID string, a
|
|
287
|
+
# `DeepL::Resources::Glossary` or a `DeepL::Resources::MultilingualGlossary` object. It:
|
|
288
|
+
# * cannot be combined with the singular +glossary+ or +glossary_id+ option,
|
|
185
289
|
# * allows at most `MAX_GLOSSARY_IDS` identifiers.
|
|
186
290
|
#
|
|
187
291
|
# The API expects +glossary_ids+ as an array (a JSON array in the JSON translate body).
|
|
188
292
|
# Callers sending multipart/form-data (e.g. document upload) should join the returned
|
|
189
293
|
# array into a comma-separated string.
|
|
190
294
|
#
|
|
191
|
-
# @param [String, nil] source_lang The source language for the request.
|
|
192
295
|
# @return [Array<String>, nil] The list of glossary IDs, or nil if the +glossary_ids+
|
|
193
296
|
# option was not provided.
|
|
194
297
|
# @raise [ArgumentError] If any of the validation rules above are violated.
|
|
195
|
-
def build_glossary_ids_param
|
|
298
|
+
def build_glossary_ids_param
|
|
196
299
|
return nil unless option?(:glossary_ids)
|
|
197
300
|
|
|
198
301
|
ids = Array(delete_option(:glossary_ids))
|
|
199
302
|
return nil if ids.empty?
|
|
200
303
|
|
|
201
|
-
validate_glossary_ids!(ids
|
|
202
|
-
ids.map
|
|
203
|
-
glossary.is_a?(DeepL::Resources::Glossary) ? glossary.id : glossary.to_s
|
|
204
|
-
end
|
|
304
|
+
validate_glossary_ids!(ids)
|
|
305
|
+
ids.map { |glossary| glossary_id_string(glossary) }
|
|
205
306
|
end
|
|
206
307
|
|
|
207
|
-
|
|
208
|
-
|
|
308
|
+
# Resolves the +glossary+ option (a glossary ID string, a `DeepL::Resources::Glossary` or
|
|
309
|
+
# a `DeepL::Resources::MultilingualGlossary`) to the ID sent as +glossary_id+, and warns
|
|
310
|
+
# about the deprecated +glossary_id+ option.
|
|
311
|
+
#
|
|
312
|
+
# @return [String, nil] The glossary ID, or nil if the +glossary+ option is unset.
|
|
313
|
+
# @raise [ArgumentError] If +glossary+ is combined with +glossary_id+.
|
|
314
|
+
def build_glossary_param
|
|
315
|
+
warn_glossary_id_deprecated unless option(:glossary_id).nil?
|
|
316
|
+
return nil unless option?(:glossary)
|
|
317
|
+
|
|
318
|
+
glossary = delete_option(:glossary)
|
|
319
|
+
return nil if glossary.nil?
|
|
320
|
+
|
|
321
|
+
unless option(:glossary_id).nil?
|
|
209
322
|
raise ArgumentError,
|
|
210
|
-
'The `
|
|
211
|
-
end
|
|
212
|
-
if source_lang.nil?
|
|
213
|
-
raise ArgumentError, 'The `glossary_ids` option requires `source_lang` to be set'
|
|
323
|
+
'The `glossary` option cannot be used together with the `glossary_id` option'
|
|
214
324
|
end
|
|
325
|
+
glossary_id_string(glossary)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def warn_glossary_id_deprecated
|
|
329
|
+
Deprecation.warn_once_at_caller(
|
|
330
|
+
'option glossary_id',
|
|
331
|
+
'[DEPRECATION] The glossary_id option is deprecated, use the glossary option instead.'
|
|
332
|
+
)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def glossary_id_string(glossary)
|
|
336
|
+
return glossary.id if glossary.is_a?(DeepL::Resources::Glossary)
|
|
337
|
+
return glossary.id if glossary.is_a?(DeepL::Resources::MultilingualGlossary)
|
|
338
|
+
|
|
339
|
+
glossary.to_s
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def validate_glossary_ids!(ids)
|
|
343
|
+
validate_no_single_glossary!
|
|
215
344
|
return unless ids.size > MAX_GLOSSARY_IDS
|
|
216
345
|
|
|
217
346
|
raise ArgumentError,
|
|
218
347
|
"A maximum of #{MAX_GLOSSARY_IDS} glossary IDs may be provided via `glossary_ids`"
|
|
219
348
|
end
|
|
220
349
|
|
|
350
|
+
def validate_no_single_glossary!
|
|
351
|
+
single = if !option(:glossary).nil? then 'glossary'
|
|
352
|
+
elsif !option(:glossary_id).nil? then 'glossary_id'
|
|
353
|
+
end
|
|
354
|
+
return unless single
|
|
355
|
+
|
|
356
|
+
raise ArgumentError,
|
|
357
|
+
"The `glossary_ids` option cannot be used together with the `#{single}` option"
|
|
358
|
+
end
|
|
359
|
+
|
|
221
360
|
def path
|
|
222
361
|
raise NotImplementedError
|
|
223
362
|
end
|
|
224
363
|
|
|
364
|
+
# URL-encodes a value for use as one path segment (a space as %20, not +).
|
|
365
|
+
def escape_path_segment(value)
|
|
366
|
+
URI.encode_www_form_component(value.to_s).gsub('+', '%20')
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# URL-encodes +params+ as a query string per RFC 3986. encode_www_form writes a space as +,
|
|
370
|
+
# which is right for a form body only; a literal + is already %2B, so every + is a space.
|
|
371
|
+
def encode_query(params)
|
|
372
|
+
URI.encode_www_form(params).gsub('+', '%20')
|
|
373
|
+
end
|
|
374
|
+
|
|
225
375
|
def url
|
|
226
376
|
"#{host}/#{api.configuration.version}/#{path}"
|
|
227
377
|
end
|
|
@@ -245,9 +395,29 @@ module DeepL
|
|
|
245
395
|
options
|
|
246
396
|
end
|
|
247
397
|
|
|
398
|
+
# The configured +headers+, then the per-call additional headers, each replace a header of
|
|
399
|
+
# the same name in any casing, Authorization and User-Agent included.
|
|
248
400
|
def headers
|
|
249
|
-
{ 'Authorization' => "DeepL-Auth-Key #{api.configuration.auth_key}",
|
|
250
|
-
|
|
401
|
+
library = { 'Authorization' => "DeepL-Auth-Key #{api.configuration.auth_key.to_s.strip}",
|
|
402
|
+
'User-Agent' => api.configuration.user_agent }
|
|
403
|
+
merge_headers(library, api.configuration.headers, @additional_headers)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def merge_headers(base, *given_headers)
|
|
407
|
+
given_headers.each_with_object(base) do |given, merged|
|
|
408
|
+
(given || {}).each do |name, value|
|
|
409
|
+
next if TECHNICAL_HEADERS.include?(name.to_s.downcase)
|
|
410
|
+
|
|
411
|
+
merged.delete_if { |existing, _| existing.to_s.casecmp?(name.to_s) }
|
|
412
|
+
merged[name] = value
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def loggable_headers
|
|
418
|
+
headers.to_h do |name, value|
|
|
419
|
+
name.to_s.casecmp?('authorization') ? [name, 'DeepL-Auth-Key [REDACTED]'] : [name, value]
|
|
420
|
+
end
|
|
251
421
|
end
|
|
252
422
|
|
|
253
423
|
def add_json_content_type(headers_to_add_to)
|