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
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# frozen_string_literal: true
|
|
5
5
|
|
|
6
6
|
module DeepL
|
|
7
|
+
# @deprecated Use the translation memory methods of DeepL::Client instead.
|
|
7
8
|
class TranslationMemoryApi # rubocop:disable Metrics/ClassLength
|
|
8
9
|
# Time to wait between two status queries of an import or export job.
|
|
9
10
|
JOB_POLLING_INTERVAL_SECONDS = 5
|
|
@@ -19,6 +20,8 @@ module DeepL
|
|
|
19
20
|
# @param [Hash] options Additional options for the request. Supports `page` (page number for
|
|
20
21
|
# pagination, 0-indexed) and `page_size` (number of items per page).
|
|
21
22
|
# @return [Array<DeepL::Resources::TranslationMemory>] The translation memories of the page.
|
|
23
|
+
#
|
|
24
|
+
# @deprecated Use DeepL::Client#list_translation_memories instead.
|
|
22
25
|
|
|
23
26
|
def list(options = {})
|
|
24
27
|
DeepL::Requests::TranslationMemory::List.new(@api, options).request
|
|
@@ -31,6 +34,8 @@ module DeepL
|
|
|
31
34
|
# ID or object.
|
|
32
35
|
# @param [Hash] options Additional options for the request.
|
|
33
36
|
# @return [DeepL::Resources::TranslationMemory] The requested translation memory.
|
|
37
|
+
#
|
|
38
|
+
# @deprecated Use DeepL::Client#get_translation_memory instead.
|
|
34
39
|
|
|
35
40
|
def find(translation_memory, options = {})
|
|
36
41
|
DeepL::Requests::TranslationMemory::Find.new(
|
|
@@ -52,6 +57,8 @@ module DeepL
|
|
|
52
57
|
# `filter_case_sensitive` (whether the filter is case-sensitive,
|
|
53
58
|
# defaults to false).
|
|
54
59
|
# @return [DeepL::Resources::TranslationMemorySegments] The requested page of segments.
|
|
60
|
+
#
|
|
61
|
+
# @deprecated Use DeepL::Client#list_translation_memory_segments instead.
|
|
55
62
|
|
|
56
63
|
def segments(translation_memory, options = {})
|
|
57
64
|
DeepL::Requests::TranslationMemory::Segments.new(
|
|
@@ -66,6 +73,8 @@ module DeepL
|
|
|
66
73
|
# ID or object.
|
|
67
74
|
# @param [Hash] options Additional options for the request.
|
|
68
75
|
# @return [String] The ID of the deleted translation memory.
|
|
76
|
+
#
|
|
77
|
+
# @deprecated Use DeepL::Client#delete_translation_memory instead.
|
|
69
78
|
|
|
70
79
|
def destroy(translation_memory, options = {})
|
|
71
80
|
DeepL::Requests::TranslationMemory::Destroy.new(
|
|
@@ -83,14 +92,19 @@ module DeepL
|
|
|
83
92
|
# @param [String, nil] content_type MIME type of the file, defaults to "application/xml".
|
|
84
93
|
# @param [String, nil] display_name Name of the resulting translation memory, defaults to the
|
|
85
94
|
# file name.
|
|
95
|
+
# @param [Hash, nil] extra_body_parameters Fields added to the request body last; they
|
|
96
|
+
# replace fields of the same name.
|
|
86
97
|
# @param [Hash] additional_headers Additional HTTP headers for the request.
|
|
87
98
|
# @return [DeepL::Resources::TranslationMemoryImport] The job ID and the upload URL.
|
|
99
|
+
#
|
|
100
|
+
# @deprecated Use DeepL::Client#create_translation_memory_import instead.
|
|
88
101
|
|
|
89
102
|
def create_import(file_name, content_length, content_type: nil, display_name: nil,
|
|
90
|
-
additional_headers: {})
|
|
103
|
+
extra_body_parameters: nil, additional_headers: {})
|
|
104
|
+
options = { content_type: content_type, display_name: display_name,
|
|
105
|
+
extra_body_parameters: extra_body_parameters }.compact
|
|
91
106
|
DeepL::Requests::TranslationMemory::CreateImport.new(
|
|
92
|
-
@api, file_name, content_length,
|
|
93
|
-
{ content_type: content_type, display_name: display_name }.compact, additional_headers
|
|
107
|
+
@api, file_name, content_length, options, additional_headers
|
|
94
108
|
).request
|
|
95
109
|
end
|
|
96
110
|
|
|
@@ -99,12 +113,18 @@ module DeepL
|
|
|
99
113
|
# upload URL is a pre-signed storage URL outside of the DeepL API, so no authorization header
|
|
100
114
|
# is sent with this request.
|
|
101
115
|
#
|
|
116
|
+
# @raise [DeepL::Exceptions::RequestError] If the storage URL replies with a non-2xx status:
|
|
117
|
+
# the API error class for that status, e.g.
|
|
118
|
+
# `AuthorizationFailed` for 401 and 403.
|
|
119
|
+
#
|
|
102
120
|
# @param [String, DeepL::Resources::TranslationMemoryImport] translation_memory_import Import
|
|
103
121
|
# returned by `create_import`, or its upload URL.
|
|
104
122
|
# @param [String] file_content Content of the TMX file.
|
|
105
123
|
# @param [String] content_type MIME type of the file. Must match the `content_type` declared
|
|
106
124
|
# when the import job was created.
|
|
107
125
|
# @return [nil]
|
|
126
|
+
#
|
|
127
|
+
# @deprecated Use DeepL::Client#upload_translation_memory_file instead.
|
|
108
128
|
|
|
109
129
|
def upload_file(translation_memory_import, file_content,
|
|
110
130
|
content_type: Requests::TranslationMemory::UploadFile::DEFAULT_CONTENT_TYPE)
|
|
@@ -122,6 +142,8 @@ module DeepL
|
|
|
122
142
|
# @param [Hash] options Additional options for the request.
|
|
123
143
|
# @return [DeepL::Resources::TranslationMemoryExport] The job ID, and whether the API reused a
|
|
124
144
|
# previously completed export.
|
|
145
|
+
#
|
|
146
|
+
# @deprecated Use DeepL::Client#create_translation_memory_export instead.
|
|
125
147
|
|
|
126
148
|
def create_export(translation_memory, options = {})
|
|
127
149
|
DeepL::Requests::TranslationMemory::CreateExport.new(
|
|
@@ -135,6 +157,8 @@ module DeepL
|
|
|
135
157
|
# @param [String, DeepL::Resources::TranslationMemoryJob] job Job ID or object.
|
|
136
158
|
# @param [Hash] options Additional options for the request.
|
|
137
159
|
# @return [DeepL::Resources::TranslationMemoryJob] The current status of the job.
|
|
160
|
+
#
|
|
161
|
+
# @deprecated Use DeepL::Client#get_translation_memory_job instead.
|
|
138
162
|
|
|
139
163
|
def find_job(job, options = {})
|
|
140
164
|
DeepL::Requests::TranslationMemory::FindJob.new(@api, extract_job_id(job), options).request
|
|
@@ -154,19 +178,20 @@ module DeepL
|
|
|
154
178
|
#
|
|
155
179
|
# @param [String, DeepL::Resources::TranslationMemoryJob] job Job ID or object.
|
|
156
180
|
# @param [Hash] options Additional options for the status queries.
|
|
157
|
-
# @param [Numeric, nil] timeout_s Maximum time in seconds to wait for the job to finish.
|
|
158
|
-
#
|
|
159
|
-
#
|
|
181
|
+
# @param [Numeric, nil] timeout_s Maximum time in seconds to wait for the job to finish. The
|
|
182
|
+
# status is queried every five seconds; the last wait is cut
|
|
183
|
+
# so that the call never waits past `timeout_s`.
|
|
160
184
|
# @return [DeepL::Resources::TranslationMemoryJob] The finished job.
|
|
185
|
+
#
|
|
186
|
+
# @deprecated Use DeepL::Client#wait_until_translation_memory_job_done instead.
|
|
161
187
|
|
|
162
188
|
def wait_until_job_done(job, options = {}, timeout_s: nil)
|
|
189
|
+
deadline = timeout_s.nil? ? nil : monotonic_time + timeout_s
|
|
163
190
|
job_status = find_job(job, options)
|
|
164
|
-
started_at = monotonic_time
|
|
165
191
|
until job_status.finished?
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
sleep(JOB_POLLING_INTERVAL_SECONDS)
|
|
192
|
+
interval = polling_interval_until(deadline, timeout_s)
|
|
193
|
+
log_job_polling(interval)
|
|
194
|
+
sleep(interval)
|
|
170
195
|
job_status = find_job(job, options)
|
|
171
196
|
end
|
|
172
197
|
raise_job_error(job_status) if job_status.error?
|
|
@@ -180,11 +205,17 @@ module DeepL
|
|
|
180
205
|
#
|
|
181
206
|
# @raise [DeepL::Exceptions::Error] If the job carries no download URL, for example because
|
|
182
207
|
# it has not completed yet.
|
|
208
|
+
# @raise [DeepL::Exceptions::RequestError] If the storage URL replies with a non-2xx status:
|
|
209
|
+
# the API error class for that status, e.g.
|
|
210
|
+
# `AuthorizationFailed` for 401 and 403.
|
|
211
|
+
# @raise [SystemCallError, IOError] If the file cannot be written.
|
|
183
212
|
#
|
|
184
213
|
# @param [DeepL::Resources::TranslationMemoryJob, String] job Completed export job carrying
|
|
185
214
|
# the download URL, or the download URL itself.
|
|
186
215
|
# @param [String] output_path Path to the file to write to. Will be overwritten if the file
|
|
187
216
|
# already exists.
|
|
217
|
+
#
|
|
218
|
+
# @deprecated Use DeepL::Client#download_translation_memory_export instead.
|
|
188
219
|
|
|
189
220
|
def download_export(job, output_path)
|
|
190
221
|
DeepL::Requests::TranslationMemory::DownloadExport.new(@api, extract_download_url(job),
|
|
@@ -195,7 +226,9 @@ module DeepL
|
|
|
195
226
|
# Imports a TMX file as a new translation memory: creates the import job, uploads the file
|
|
196
227
|
# and waits for the processing to finish.
|
|
197
228
|
#
|
|
198
|
-
# @raise [DeepL::Exceptions::Error] If
|
|
229
|
+
# @raise [DeepL::Exceptions::Error] If no file exists at `input_file_path` (before any request
|
|
230
|
+
# is sent) or the import fails.
|
|
231
|
+
# @raise [SystemCallError, IOError] If the file cannot be read.
|
|
199
232
|
#
|
|
200
233
|
# @param [String] input_file_path Path to the TMX file to import.
|
|
201
234
|
# @param [String, nil] display_name Name of the resulting translation memory, defaults to the
|
|
@@ -203,18 +236,23 @@ module DeepL
|
|
|
203
236
|
# @param [Numeric, nil] timeout_s Maximum time in seconds to wait for the import to finish.
|
|
204
237
|
# Note that the API keeps reporting `awaiting_input` for a
|
|
205
238
|
# while after the upload, so allow for a generous timeout.
|
|
239
|
+
# @param [String, nil] content_type MIME type of the file, declared in the import job and sent
|
|
240
|
+
# with the upload. Defaults to "application/xml".
|
|
241
|
+
# @param [Hash, nil] extra_body_parameters Fields added to the body of the import job request
|
|
242
|
+
# last; they replace fields of the same name.
|
|
206
243
|
# @return [DeepL::Resources::TranslationMemoryJob] The finished import job, its result carries
|
|
207
244
|
# the ID of the new translation memory.
|
|
245
|
+
#
|
|
246
|
+
# @deprecated Use DeepL::Client#import_translation_memory_from_filepath instead.
|
|
208
247
|
|
|
209
|
-
def import_from_filepath(input_file_path, display_name: nil, timeout_s: nil
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
file_content = File.binread(input_file_path)
|
|
248
|
+
def import_from_filepath(input_file_path, display_name: nil, timeout_s: nil,
|
|
249
|
+
content_type: nil, extra_body_parameters: nil)
|
|
250
|
+
check_input_file(input_file_path)
|
|
251
|
+
file_content = read_input_file(input_file_path)
|
|
215
252
|
created = create_import(File.basename(input_file_path), file_content.bytesize,
|
|
216
|
-
display_name: display_name
|
|
217
|
-
|
|
253
|
+
content_type: content_type, display_name: display_name,
|
|
254
|
+
extra_body_parameters: extra_body_parameters)
|
|
255
|
+
upload_file(created, file_content, content_type: content_type)
|
|
218
256
|
wait_until_job_done(created.job_id, timeout_s: timeout_s)
|
|
219
257
|
end
|
|
220
258
|
|
|
@@ -222,7 +260,10 @@ module DeepL
|
|
|
222
260
|
# Exports a translation memory to a TMX file: creates the export job, waits for it to finish
|
|
223
261
|
# and writes the result to +output_path+.
|
|
224
262
|
#
|
|
263
|
+
# @raise [Errno::ENOENT] If the directory of `output_path` does not exist, checked before the
|
|
264
|
+
# export job is created.
|
|
225
265
|
# @raise [DeepL::Exceptions::Error] If the export fails.
|
|
266
|
+
# @raise [SystemCallError, IOError] If the file cannot be written.
|
|
226
267
|
#
|
|
227
268
|
# @param [String, DeepL::Resources::TranslationMemory] translation_memory Translation memory
|
|
228
269
|
# ID or object.
|
|
@@ -230,8 +271,11 @@ module DeepL
|
|
|
230
271
|
# already exists.
|
|
231
272
|
# @param [Numeric, nil] timeout_s Maximum time in seconds to wait for the export to finish.
|
|
232
273
|
# @return [DeepL::Resources::TranslationMemoryJob] The finished export job.
|
|
274
|
+
#
|
|
275
|
+
# @deprecated Use DeepL::Client#export_translation_memory_to_filepath instead.
|
|
233
276
|
|
|
234
277
|
def export_to_filepath(translation_memory, output_path, timeout_s: nil)
|
|
278
|
+
check_output_directory(output_path)
|
|
235
279
|
created = create_export(translation_memory)
|
|
236
280
|
job = wait_until_job_done(created.job_id, timeout_s: timeout_s)
|
|
237
281
|
download_export(job, output_path)
|
|
@@ -282,17 +326,53 @@ module DeepL
|
|
|
282
326
|
value.nil? || value.empty?
|
|
283
327
|
end
|
|
284
328
|
|
|
285
|
-
|
|
329
|
+
# Until the next major version, the module-level API raises the error classes of v3.9.0 for
|
|
330
|
+
# local file failures; DeepL::Client raises ArgumentError and wrapped errors.
|
|
331
|
+
def check_input_file(input_file_path)
|
|
332
|
+
return if File.exist?(input_file_path)
|
|
333
|
+
|
|
334
|
+
message = "No file found at #{input_file_path}"
|
|
335
|
+
raise ArgumentError, message if @api.client?
|
|
336
|
+
|
|
337
|
+
raise Exceptions::Error, message
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def read_input_file(input_file_path)
|
|
341
|
+
File.binread(input_file_path)
|
|
342
|
+
rescue SystemCallError, IOError => e
|
|
343
|
+
raise e unless @api.client?
|
|
344
|
+
|
|
345
|
+
raise Exceptions::Error, "Could not read #{input_file_path}: #{e.message}"
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def check_output_directory(output_path)
|
|
349
|
+
directory = File.dirname(output_path)
|
|
350
|
+
return if File.directory?(directory)
|
|
351
|
+
|
|
352
|
+
message = "Output directory #{directory} does not exist"
|
|
353
|
+
raise Exceptions::Error, message if @api.client?
|
|
354
|
+
|
|
355
|
+
raise Errno::ENOENT, message
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def log_job_polling(interval)
|
|
286
359
|
@api.configuration.logger&.info('Rechecking translation memory job status after sleeping ' \
|
|
287
|
-
"for #{
|
|
360
|
+
"for #{format('%.2f', interval)} seconds.")
|
|
288
361
|
end
|
|
289
362
|
|
|
290
363
|
def monotonic_time
|
|
291
364
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
292
365
|
end
|
|
293
366
|
|
|
294
|
-
|
|
295
|
-
|
|
367
|
+
# The polling interval, cut to the time left until +deadline+ so that a wait never runs
|
|
368
|
+
# past +timeout_s+.
|
|
369
|
+
def polling_interval_until(deadline, timeout_s)
|
|
370
|
+
return JOB_POLLING_INTERVAL_SECONDS if deadline.nil?
|
|
371
|
+
|
|
372
|
+
remaining = deadline - monotonic_time
|
|
373
|
+
raise_timeout_error(timeout_s) if remaining <= 0
|
|
374
|
+
|
|
375
|
+
[JOB_POLLING_INTERVAL_SECONDS, remaining].min
|
|
296
376
|
end
|
|
297
377
|
|
|
298
378
|
def raise_timeout_error(timeout_s)
|
|
@@ -0,0 +1,54 @@
|
|
|
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 Utils
|
|
8
|
+
# Builds the API connection of a DeepL::Client or DeepL::AdminClient from its constructor
|
|
9
|
+
# options. Nothing is read from the environment or from module state.
|
|
10
|
+
# @api private
|
|
11
|
+
module ClientBuilder
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
def build_api(key, key_name:, server_url:, default_server_url:, headers:, max_retries:,
|
|
15
|
+
timeout:, proxy:, send_platform_info:, app_info:, http_client_options:, logger:) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/ParameterLists
|
|
16
|
+
key = key.to_s.strip
|
|
17
|
+
raise ArgumentError, "#{key_name} must not be empty" if key.empty?
|
|
18
|
+
|
|
19
|
+
headers = (headers || {}).to_h
|
|
20
|
+
app_info = AppInfo.from(app_info)
|
|
21
|
+
configuration = Configuration.new(
|
|
22
|
+
{ auth_key: key, host: server_url || default_server_url,
|
|
23
|
+
max_network_retries: max_retries, timeout: timeout, logger: logger,
|
|
24
|
+
headers: headers.reject { |name, _| user_agent_header?(name) },
|
|
25
|
+
user_agent: headers.find { |name, _| user_agent_header?(name) }&.last },
|
|
26
|
+
app_info&.app_name, app_info&.app_version, send_platform_info
|
|
27
|
+
)
|
|
28
|
+
API.new(configuration, merge_proxy(http_client_options, proxy), client: true)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def user_agent_header?(name)
|
|
34
|
+
name.to_s.casecmp?('user-agent')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def merge_proxy(options, proxy) # rubocop:disable Metrics/MethodLength
|
|
38
|
+
return options if proxy.nil?
|
|
39
|
+
if options && !(options.proxy.nil? || options.proxy.empty?)
|
|
40
|
+
raise ArgumentError,
|
|
41
|
+
'Set the proxy either with proxy: or in http_client_options:, not both'
|
|
42
|
+
end
|
|
43
|
+
return HTTPClientOptions.new(proxy) if options.nil?
|
|
44
|
+
|
|
45
|
+
HTTPClientOptions.new(proxy, options.cert_path,
|
|
46
|
+
enable_ssl_verification: options.enable_ssl_verification,
|
|
47
|
+
open_timeout: options.open_timeout,
|
|
48
|
+
read_timeout: options.read_timeout,
|
|
49
|
+
write_timeout: options.write_timeout,
|
|
50
|
+
ssl_timeout: options.ssl_timeout)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
module DeepL
|
|
7
|
+
module Utils
|
|
8
|
+
# @api private
|
|
9
|
+
# Polls the status of a document translation every 5 seconds.
|
|
10
|
+
module DocumentPoller
|
|
11
|
+
INTERVAL_S = 5
|
|
12
|
+
|
|
13
|
+
extend self
|
|
14
|
+
|
|
15
|
+
# Polls until the translation is done and returns that status. Raises
|
|
16
|
+
# DeepL::Exceptions::Error with the API's message when the translation fails, and when
|
|
17
|
+
# +timeout_s+ is set and expires. An unknown status keeps polling.
|
|
18
|
+
def wait_until_done(api, handle, timeout_s: nil)
|
|
19
|
+
deadline = timeout_s.nil? ? nil : monotonic_now + timeout_s
|
|
20
|
+
loop do
|
|
21
|
+
status = get_status(api, handle)
|
|
22
|
+
return status if status.done?
|
|
23
|
+
|
|
24
|
+
raise_if_failed(status)
|
|
25
|
+
|
|
26
|
+
sleep(interval_until(deadline, timeout_s))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Polls until the translation is done or failed and returns that status, or the last status
|
|
31
|
+
# after +max_queries+ + 1 queries if +max_queries+ is set.
|
|
32
|
+
def wait_until_finished(api, handle, max_queries: nil)
|
|
33
|
+
num_queries = 0
|
|
34
|
+
loop do
|
|
35
|
+
sleep(INTERVAL_S) if num_queries.positive?
|
|
36
|
+
num_queries += 1
|
|
37
|
+
status = get_status(api, handle)
|
|
38
|
+
return status if status.finished? || (!max_queries.nil? && num_queries > max_queries)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def raise_if_failed(status)
|
|
45
|
+
return unless status.error?
|
|
46
|
+
|
|
47
|
+
raise Exceptions::Error, "Document translation failed: #{status.error_message}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def get_status(api, handle)
|
|
51
|
+
Requests::Document::GetStatus.new(api, handle.document_id, handle.document_key).request
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def interval_until(deadline, timeout_s)
|
|
55
|
+
return INTERVAL_S if deadline.nil?
|
|
56
|
+
|
|
57
|
+
remaining = deadline - monotonic_now
|
|
58
|
+
if remaining <= 0
|
|
59
|
+
raise Exceptions::Error, "Document translation did not finish within #{timeout_s} seconds"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
[INTERVAL_S, remaining].min
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def monotonic_now
|
|
66
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -21,13 +21,14 @@ module DeepL
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def initialize(response)
|
|
24
|
+
def initialize(response, request = nil)
|
|
25
25
|
@response = response
|
|
26
|
+
@request = request
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def build
|
|
29
30
|
error_class = self.class.error_class_from_response_code(response.code.to_i)
|
|
30
|
-
error_class.new(response)
|
|
31
|
+
error_class.new(response, request)
|
|
31
32
|
end
|
|
32
33
|
end
|
|
33
34
|
end
|
|
@@ -0,0 +1,79 @@
|
|
|
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 Utils
|
|
8
|
+
module HTTPClientBuilder
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
##
|
|
12
|
+
# Builds an unstarted Net::HTTP client for +host+ configured with +options+. Net::HTTP does
|
|
13
|
+
# not retry on its own (+max_retries+ 0): only the library's retries apply.
|
|
14
|
+
#
|
|
15
|
+
# @param [String] host Base URL of the API.
|
|
16
|
+
# @param [DeepL::HTTPClientOptions] options Proxy, SSL and timeout options.
|
|
17
|
+
# @return [Net::HTTP]
|
|
18
|
+
def build(host, options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
19
|
+
uri = URI(host)
|
|
20
|
+
http = Net::HTTP.new(uri.host, uri.port, *proxy_arguments(options.proxy))
|
|
21
|
+
http.use_ssl = uri.scheme == 'https'
|
|
22
|
+
disable_lenient_defaults(http)
|
|
23
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless options.enable_ssl_verification
|
|
24
|
+
http.ca_file = options.cert_path if options.cert_path
|
|
25
|
+
http.open_timeout = options.open_timeout unless options.open_timeout.nil?
|
|
26
|
+
http.read_timeout = options.read_timeout unless options.read_timeout.nil?
|
|
27
|
+
http.write_timeout = options.write_timeout unless options.write_timeout.nil?
|
|
28
|
+
http.ssl_timeout = options.ssl_timeout unless options.ssl_timeout.nil?
|
|
29
|
+
http
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Net::HTTP would re-send idempotent requests once on its own, outside max_retries, and would
|
|
34
|
+
# return a body shorter than its Content-Length as complete (+ignore_eof+, net-http 0.3+).
|
|
35
|
+
def disable_lenient_defaults(http)
|
|
36
|
+
http.max_retries = 0
|
|
37
|
+
http.ignore_eof = false if http.respond_to?(:ignore_eof=)
|
|
38
|
+
http
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# The proxy is either a URL string, e.g. +http://user:pass@proxy.example:8080+, or a hash
|
|
44
|
+
# with +proxy_addr+ and +proxy_port+. Returns the proxy arguments of +Net::HTTP.new+.
|
|
45
|
+
def proxy_arguments(proxy)
|
|
46
|
+
if proxy.is_a?(String) && !proxy.empty?
|
|
47
|
+
proxy_uri = parse_proxy_url(proxy)
|
|
48
|
+
|
|
49
|
+
return [proxy_uri.host, proxy_uri.port, decode(proxy_uri.user),
|
|
50
|
+
decode(proxy_uri.password)]
|
|
51
|
+
end
|
|
52
|
+
unless proxy.is_a?(Hash) && proxy.key?('proxy_addr') && proxy.key?('proxy_port')
|
|
53
|
+
return [nil,
|
|
54
|
+
nil]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
[proxy['proxy_addr'], proxy['proxy_port']]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Error messages show the URL without its user info; the URI error, which repeats the URL,
|
|
61
|
+
# is not kept as cause.
|
|
62
|
+
def parse_proxy_url(proxy)
|
|
63
|
+
proxy_uri = begin
|
|
64
|
+
URI(proxy)
|
|
65
|
+
rescue URI::InvalidURIError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
return proxy_uri unless proxy_uri&.host.nil?
|
|
69
|
+
|
|
70
|
+
raise ArgumentError, "Invalid proxy URL: #{HTTPClientOptions.redact_proxy(proxy)}",
|
|
71
|
+
cause: nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def decode(value)
|
|
75
|
+
value && URI::DEFAULT_PARSER.unescape(value)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
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 Utils
|
|
8
|
+
##
|
|
9
|
+
# Reads numbers from API responses without failing on unexpected values.
|
|
10
|
+
module NumberParser
|
|
11
|
+
extend self
|
|
12
|
+
|
|
13
|
+
# @return [Integer] The value if it is an Integer, otherwise 0.
|
|
14
|
+
def count(value)
|
|
15
|
+
value.is_a?(Integer) ? value : 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [Float] The value as a Float if it is numeric, otherwise 0.0.
|
|
19
|
+
def minutes(value)
|
|
20
|
+
value.is_a?(Numeric) ? value.to_f : 0.0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Float, nil] The value as a Float if it is numeric, otherwise nil.
|
|
24
|
+
def optional_minutes(value)
|
|
25
|
+
value.is_a?(Numeric) ? value.to_f : nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Integer, nil] The value if it is an Integer, otherwise nil.
|
|
29
|
+
def optional_count(value)
|
|
30
|
+
value.is_a?(Integer) ? value : nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
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 Utils
|
|
8
|
+
module TermValidator
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
# C0 and C1 control characters plus the Unicode line and paragraph separators.
|
|
12
|
+
INVALID_CHARACTERS = /[\u0000-\u001F\u0080-\u009F
]/.freeze
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# Strips surrounding whitespace from a single glossary or spoken term, then checks it
|
|
16
|
+
# before it is serialized into a request.
|
|
17
|
+
#
|
|
18
|
+
# @param [String] term The term to check.
|
|
19
|
+
# @return [String] The stripped term, which is what the request sends.
|
|
20
|
+
# @raise [DeepL::Exceptions::ValidationError] If the stripped term is empty or contains a
|
|
21
|
+
# control character.
|
|
22
|
+
def validate!(term)
|
|
23
|
+
term = term.to_s.strip
|
|
24
|
+
raise Exceptions::ValidationError, 'Term must not be empty' if term.empty?
|
|
25
|
+
if INVALID_CHARACTERS.match?(term)
|
|
26
|
+
raise Exceptions::ValidationError, "Term #{term.inspect} contains an invalid character"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
term
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -21,6 +21,20 @@ module DeepL
|
|
|
21
21
|
|
|
22
22
|
Time.parse(time_string)
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# Like +parse_optional_time+, but treats timestamps without a zone offset as UTC.
|
|
27
|
+
#
|
|
28
|
+
# @param [Object] time_string Timestamp in ISO 8601 format, or nil.
|
|
29
|
+
# @return [Time, nil] The parsed time, or nil if no valid timestamp was given.
|
|
30
|
+
def parse_optional_utc_time(time_string)
|
|
31
|
+
return nil unless time_string.is_a?(String) && !time_string.empty?
|
|
32
|
+
return Time.parse(time_string) if Date._parse(time_string).key?(:offset)
|
|
33
|
+
|
|
34
|
+
Time.parse("#{time_string}Z")
|
|
35
|
+
rescue ArgumentError
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
24
38
|
end
|
|
25
39
|
end
|
|
26
40
|
end
|