lara-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/lara/audio.rb +100 -0
- data/lib/lara/auth_token.rb +17 -0
- data/lib/lara/client.rb +309 -0
- data/lib/lara/credentials.rb +26 -0
- data/lib/lara/documents.rb +105 -0
- data/lib/lara/errors.rb +43 -0
- data/lib/lara/glossaries.rb +146 -0
- data/lib/lara/images.rb +78 -0
- data/lib/lara/memories.rb +139 -0
- data/lib/lara/models/audio.rb +46 -0
- data/lib/lara/models/base.rb +19 -0
- data/lib/lara/models/documents.rb +63 -0
- data/lib/lara/models/glossaries.rb +47 -0
- data/lib/lara/models/images.rb +88 -0
- data/lib/lara/models/memories.rb +42 -0
- data/lib/lara/models/text.rb +144 -0
- data/lib/lara/s3_client.rb +53 -0
- data/lib/lara/translator.rb +145 -0
- data/lib/lara/version.rb +5 -0
- data/lib/lara.rb +27 -0
- metadata +198 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f327f84f1775f706e868dcb2a843ce2074778a988e35a5b56949301df4b9fb9b
|
|
4
|
+
data.tar.gz: 8cc17e63bf6f79b0b85e273f2559873985339a1eab1948529a195fd59b164b47
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2d962b4da02a66c126dfcd0740a5fd7894e125d9a97d0f016ac6dab3d41136b1025db02549b95c19a86a3a9253d0a154ab44b6171b8dce06a7a4a392f258b41b
|
|
7
|
+
data.tar.gz: 86f152fc3c6891561a9cc4d0f18696d86451cd130023bd1d186ac1cb261fed3b04837ff1c59096075af66f753800c1fc27c0ec7b878e03192c45549b85ad4ca7
|
data/lib/lara/audio.rb
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lara
|
|
4
|
+
class AudioTranslator
|
|
5
|
+
ALLOWED_AUDIO_PARAMS = %i[
|
|
6
|
+
id
|
|
7
|
+
status
|
|
8
|
+
target
|
|
9
|
+
filename
|
|
10
|
+
source
|
|
11
|
+
created_at
|
|
12
|
+
updated_at
|
|
13
|
+
options
|
|
14
|
+
translated_seconds
|
|
15
|
+
total_seconds
|
|
16
|
+
error_reason
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
def initialize(client, s3_client = S3Client.new)
|
|
20
|
+
@client = client
|
|
21
|
+
@s3 = s3_client
|
|
22
|
+
@polling_interval = 2
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Uploads an audio file to S3 and creates a translation job.
|
|
26
|
+
# @return [Lara::Models::Audio]
|
|
27
|
+
def upload(file_path:, filename:, target:, source: nil, adapt_to: nil, glossaries: nil,
|
|
28
|
+
no_trace: false, style: nil)
|
|
29
|
+
response_data = @client.get("/v2/audio/upload-url", params: { filename: filename })
|
|
30
|
+
url = response_data["url"]
|
|
31
|
+
fields = response_data["fields"]
|
|
32
|
+
|
|
33
|
+
@s3.upload(url: url, fields: fields, io: file_path)
|
|
34
|
+
|
|
35
|
+
body = {
|
|
36
|
+
s3key: fields["key"],
|
|
37
|
+
target: target,
|
|
38
|
+
source: source,
|
|
39
|
+
adapt_to: adapt_to,
|
|
40
|
+
glossaries: glossaries,
|
|
41
|
+
style: style
|
|
42
|
+
}.compact
|
|
43
|
+
|
|
44
|
+
headers = {}
|
|
45
|
+
headers["X-No-Trace"] = "true" if no_trace
|
|
46
|
+
|
|
47
|
+
response = @client.post("/v2/audio/translate", body: body, headers: headers)
|
|
48
|
+
response_params = response.transform_keys(&:to_sym)
|
|
49
|
+
Lara::Models::Audio.new(**filter_audio_params(response_params))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Fetch audio translation status.
|
|
53
|
+
# @return [Lara::Models::Audio]
|
|
54
|
+
def status(id)
|
|
55
|
+
response = @client.get("/v2/audio/#{id}")
|
|
56
|
+
response_params = response.transform_keys(&:to_sym)
|
|
57
|
+
Lara::Models::Audio.new(**filter_audio_params(response_params))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Download translated audio bytes.
|
|
61
|
+
# @return [String] bytes
|
|
62
|
+
def download(id)
|
|
63
|
+
url = @client.get("/v2/audio/#{id}/download-url")["url"]
|
|
64
|
+
@s3.download(url: url)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Translates an audio file end-to-end
|
|
68
|
+
# @return [String] translated audio bytes
|
|
69
|
+
def translate(file_path:, filename:, target:, source: nil, adapt_to: nil, glossaries: nil,
|
|
70
|
+
no_trace: false, style: nil)
|
|
71
|
+
audio = upload(file_path: file_path, filename: filename, target: target, source: source,
|
|
72
|
+
adapt_to: adapt_to, glossaries: glossaries, no_trace: no_trace, style: style)
|
|
73
|
+
|
|
74
|
+
max_wait_time = 60 * 15 # 15 minutes
|
|
75
|
+
start = Time.now
|
|
76
|
+
|
|
77
|
+
loop do |_|
|
|
78
|
+
current = status(audio.id)
|
|
79
|
+
|
|
80
|
+
case current.status
|
|
81
|
+
when Lara::Models::AudioStatus::TRANSLATED
|
|
82
|
+
return download(current.id)
|
|
83
|
+
when Lara::Models::AudioStatus::ERROR
|
|
84
|
+
raise Lara::LaraApiError.new(500, "AudioError",
|
|
85
|
+
current.error_reason || "Unknown error")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
raise Timeout::Error if Time.now - start > max_wait_time
|
|
89
|
+
|
|
90
|
+
sleep @polling_interval
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def filter_audio_params(params)
|
|
97
|
+
params.slice(*ALLOWED_AUDIO_PARAMS)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lara
|
|
4
|
+
# JWT authentication token for API access
|
|
5
|
+
class AuthToken
|
|
6
|
+
attr_reader :token, :refresh_token
|
|
7
|
+
|
|
8
|
+
def initialize(token, refresh_token)
|
|
9
|
+
@token = token
|
|
10
|
+
@refresh_token = refresh_token
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_s
|
|
14
|
+
token
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/lara/client.rb
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/multipart"
|
|
5
|
+
require "json"
|
|
6
|
+
require "openssl"
|
|
7
|
+
require "base64"
|
|
8
|
+
require "digest"
|
|
9
|
+
require "uri"
|
|
10
|
+
|
|
11
|
+
module Lara
|
|
12
|
+
# This class is used to interact with Lara via the REST API.
|
|
13
|
+
|
|
14
|
+
class Client
|
|
15
|
+
DEFAULT_BASE_URL = "https://api.laratranslate.com"
|
|
16
|
+
|
|
17
|
+
def initialize(auth_method, base_url: DEFAULT_BASE_URL, connection_timeout: nil,
|
|
18
|
+
read_timeout: nil)
|
|
19
|
+
case auth_method
|
|
20
|
+
when Credentials
|
|
21
|
+
@credentials = auth_method
|
|
22
|
+
@auth_token = nil
|
|
23
|
+
when AuthToken
|
|
24
|
+
@credentials = nil
|
|
25
|
+
@auth_token = auth_method
|
|
26
|
+
else
|
|
27
|
+
raise ArgumentError, "auth_method must be Credentials or AuthToken"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
@base_url = base_url.to_s.sub(%r{/+$}, "")
|
|
31
|
+
@connection_timeout = connection_timeout
|
|
32
|
+
@read_timeout = read_timeout
|
|
33
|
+
@extra_headers = {}
|
|
34
|
+
@auth_mutex = Mutex.new
|
|
35
|
+
|
|
36
|
+
@connection = build_connection
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_reader :base_url
|
|
40
|
+
|
|
41
|
+
# Sets an extra header
|
|
42
|
+
# @param name [String] Header name
|
|
43
|
+
# @param value [String] Header value
|
|
44
|
+
def set_extra_header(name, value)
|
|
45
|
+
@extra_headers[name] = value
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Sends a GET request to the Lara API.
|
|
49
|
+
# @param path [String] The path to send the request to.
|
|
50
|
+
# @param params [Hash,nil] The parameters to send with the request.
|
|
51
|
+
# @param headers [Hash,nil] Additional headers to include in the request.
|
|
52
|
+
# @return [Hash, Array, String, nil] The JSON 'content' from the API or CSV body for csv responses.
|
|
53
|
+
def get(path, params: nil, headers: nil)
|
|
54
|
+
request(:get, path, body: nil, headers: headers, params: params)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Sends a POST request to the Lara API.
|
|
58
|
+
# @param path [String] The path to send the request to.
|
|
59
|
+
# @param body [Hash,nil] The parameters to send with the request.
|
|
60
|
+
# @param files [Hash,nil] The files to send with the request. If present, request will be sent as multipart/form-data.
|
|
61
|
+
# @param headers [Hash,nil] Additional headers to include in the request.
|
|
62
|
+
# @param raw_response [Boolean] If true, returns the raw response body (useful for binary data like images).
|
|
63
|
+
# @yield [Hash] Each partial JSON result from the stream (if streaming)
|
|
64
|
+
# @return [Hash, Array, String, nil] The JSON 'content' from the API, CSV body, or raw bytes.
|
|
65
|
+
def post(path, body: nil, files: nil, headers: nil, raw_response: false, &callback)
|
|
66
|
+
request(:post, path, body: body, files: files, headers: headers, raw_response: raw_response, &callback)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Sends a PUT request to the Lara API.
|
|
70
|
+
# @param path [String] The path to send the request to.
|
|
71
|
+
# @param body [Hash,nil] The parameters to send with the request.
|
|
72
|
+
# @param files [Hash,nil] The files to send with the request. If present, request will be sent as multipart/form-data.
|
|
73
|
+
# @param headers [Hash,nil] Additional headers to include in the request.
|
|
74
|
+
# @return [Hash, Array, String, nil] The JSON 'content' from the API or CSV body for csv responses.
|
|
75
|
+
def put(path, body: nil, files: nil, headers: nil)
|
|
76
|
+
request(:put, path, body: body, files: files, headers: headers)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Sends a DELETE request to the Lara API.
|
|
80
|
+
# @param path [String] The path to send the request to.
|
|
81
|
+
# @param params [Hash,nil] The parameters to send with the request.
|
|
82
|
+
# @param headers [Hash,nil] Additional headers to include in the request.
|
|
83
|
+
# @return [Hash, Array, String, nil] The JSON 'content' from the API or CSV body for csv responses.
|
|
84
|
+
def delete(path, params: nil, body: nil, headers: nil)
|
|
85
|
+
request(:delete, path, body: body, headers: headers, params: params)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def request(method, path, body: nil, files: nil, headers: nil, params: nil, raw_response: false, &callback)
|
|
91
|
+
ensure_valid_token
|
|
92
|
+
|
|
93
|
+
make_request(method, path, body: body, files: files, headers: headers, params: params,
|
|
94
|
+
raw_response: raw_response, &callback)
|
|
95
|
+
rescue LaraApiError => e
|
|
96
|
+
# Auto-refresh token on 401 with jwt expired
|
|
97
|
+
raise unless e.status_code == 401 && e.message.include?("jwt expired")
|
|
98
|
+
|
|
99
|
+
refresh_token
|
|
100
|
+
# Retry once with new token
|
|
101
|
+
make_request(method, path, body: body, files: files, headers: headers, params: params,
|
|
102
|
+
raw_response: raw_response, &callback)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def ensure_valid_token
|
|
106
|
+
@auth_mutex.synchronize do
|
|
107
|
+
return if @auth_token
|
|
108
|
+
|
|
109
|
+
raise LaraError, "No authentication method available" unless @credentials
|
|
110
|
+
|
|
111
|
+
@auth_token = authenticate
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def authenticate
|
|
116
|
+
path = "/v2/auth"
|
|
117
|
+
method = "POST"
|
|
118
|
+
timestamp = Time.now.utc.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
|
119
|
+
|
|
120
|
+
body = { id: @credentials&.access_key_id }
|
|
121
|
+
body_json = body.to_json
|
|
122
|
+
content_md5 = Digest::MD5.hexdigest(body_json)
|
|
123
|
+
|
|
124
|
+
headers = {
|
|
125
|
+
"Date" => timestamp,
|
|
126
|
+
"X-Lara-SDK-Name" => "lara-ruby",
|
|
127
|
+
"X-Lara-SDK-Version" => Lara::VERSION,
|
|
128
|
+
"Content-Type" => "application/json",
|
|
129
|
+
"Content-MD5" => content_md5,
|
|
130
|
+
"Authorization" => "Lara:#{generate_hmac_signature(method, path, content_md5,
|
|
131
|
+
'application/json', timestamp)}"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
conn = Faraday.new(url: @base_url) do |c|
|
|
135
|
+
c.adapter Faraday.default_adapter
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
response = conn.post(path) do |req|
|
|
139
|
+
req.headers = headers
|
|
140
|
+
req.body = body_json
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
raise LaraApiError.from_response(response) unless response.success?
|
|
144
|
+
|
|
145
|
+
data = JSON.parse(response.body)
|
|
146
|
+
refresh_token = response.headers["x-lara-refresh-token"]
|
|
147
|
+
|
|
148
|
+
raise LaraError, "Missing refresh token in authentication response" unless refresh_token
|
|
149
|
+
|
|
150
|
+
AuthToken.new(data["token"], refresh_token)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def refresh_token
|
|
154
|
+
@auth_mutex.synchronize do
|
|
155
|
+
return unless @auth_token
|
|
156
|
+
|
|
157
|
+
conn = Faraday.new(url: @base_url) do |c|
|
|
158
|
+
c.adapter Faraday.default_adapter
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
response = conn.post("/v2/auth/refresh") do |req|
|
|
162
|
+
req.headers = {
|
|
163
|
+
"Date" => Time.now.utc.strftime("%a, %d %b %Y %H:%M:%S GMT"),
|
|
164
|
+
"X-Lara-SDK-Name" => "lara-ruby",
|
|
165
|
+
"X-Lara-SDK-Version" => Lara::VERSION,
|
|
166
|
+
"Authorization" => "Bearer #{@auth_token&.refresh_token}"
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if response.success?
|
|
171
|
+
data = JSON.parse(response.body)
|
|
172
|
+
refresh_token = response.headers["x-lara-refresh-token"]
|
|
173
|
+
|
|
174
|
+
if refresh_token
|
|
175
|
+
@auth_token = AuthToken.new(data["token"], refresh_token)
|
|
176
|
+
else
|
|
177
|
+
# Refresh failed, force re-authentication
|
|
178
|
+
@auth_token = nil
|
|
179
|
+
ensure_valid_token
|
|
180
|
+
end
|
|
181
|
+
else
|
|
182
|
+
# Refresh failed, force re-authentication
|
|
183
|
+
@auth_token = nil
|
|
184
|
+
ensure_valid_token
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def make_request(method, path, body: nil, files: nil, headers: nil, params: nil, raw_response: false, &callback)
|
|
190
|
+
path = "/#{path}" unless path.start_with?("/")
|
|
191
|
+
request_headers = build_request_headers(body, files, headers)
|
|
192
|
+
|
|
193
|
+
# Make the API call
|
|
194
|
+
response = if files&.any?
|
|
195
|
+
@connection.post(path) do |req|
|
|
196
|
+
req.headers.merge!(request_headers)
|
|
197
|
+
req.params = params if params
|
|
198
|
+
req.body = body.is_a?(Hash) ? body.dup : {}
|
|
199
|
+
files.each { |key, value| req.body[key] = value }
|
|
200
|
+
end
|
|
201
|
+
else
|
|
202
|
+
@connection.send(method, path) do |req|
|
|
203
|
+
req.headers.merge!(request_headers)
|
|
204
|
+
req.params = params if params
|
|
205
|
+
req.body = body.to_json if body.is_a?(Hash) && !body.empty?
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
raise LaraApiError.from_response(response) unless response.success?
|
|
210
|
+
|
|
211
|
+
return response.body if raw_response
|
|
212
|
+
|
|
213
|
+
content_type = response.headers["content-type"] || response.headers["Content-Type"]
|
|
214
|
+
return response.body if content_type&.include?("text/csv")
|
|
215
|
+
|
|
216
|
+
if callback || (body && body[:reasoning])
|
|
217
|
+
parse_stream_response(response.body, &callback)
|
|
218
|
+
else
|
|
219
|
+
parse_json(response.body)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def parse_json(body)
|
|
224
|
+
parsed = body.nil? || body.empty? ? {} : JSON.parse(body)
|
|
225
|
+
parsed.is_a?(Hash) && parsed.key?("content") ? parsed["content"] : parsed
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def parse_stream_response(body, &block)
|
|
229
|
+
return {} if body.nil? || body.empty?
|
|
230
|
+
|
|
231
|
+
buffer = ""
|
|
232
|
+
last_result = nil
|
|
233
|
+
|
|
234
|
+
body.each_line do |line|
|
|
235
|
+
buffer += line
|
|
236
|
+
next unless line.end_with?("\n")
|
|
237
|
+
|
|
238
|
+
trimmed_line = buffer.strip
|
|
239
|
+
buffer = ""
|
|
240
|
+
|
|
241
|
+
next if trimmed_line.empty?
|
|
242
|
+
|
|
243
|
+
begin
|
|
244
|
+
parsed = JSON.parse(trimmed_line)
|
|
245
|
+
result = parsed["content"] || parsed
|
|
246
|
+
block.call(result) if block
|
|
247
|
+
last_result = result
|
|
248
|
+
rescue JSON::ParserError
|
|
249
|
+
next
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
if !buffer.empty? && buffer.strip != ""
|
|
254
|
+
begin
|
|
255
|
+
parsed = JSON.parse(buffer.strip)
|
|
256
|
+
result = parsed["content"] || parsed
|
|
257
|
+
block.call(result) if block
|
|
258
|
+
last_result = result
|
|
259
|
+
rescue JSON::ParserError
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
last_result || {}
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def build_request_headers(body, files, extra_headers)
|
|
267
|
+
timestamp = Time.now.utc.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
|
268
|
+
|
|
269
|
+
headers = {
|
|
270
|
+
"Date" => timestamp,
|
|
271
|
+
"X-Lara-SDK-Name" => "lara-ruby",
|
|
272
|
+
"X-Lara-SDK-Version" => Lara::VERSION,
|
|
273
|
+
"Authorization" => "Bearer #{@auth_token&.token}",
|
|
274
|
+
**@extra_headers
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
headers.merge!(extra_headers) if extra_headers
|
|
278
|
+
|
|
279
|
+
if !files&.any? && body.is_a?(Hash) && !body.empty?
|
|
280
|
+
headers["Content-Type"] = "application/json"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
headers
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def build_connection
|
|
287
|
+
Faraday.new(url: @base_url) do |conn|
|
|
288
|
+
conn.request :multipart
|
|
289
|
+
conn.request :url_encoded
|
|
290
|
+
conn.adapter Faraday.default_adapter
|
|
291
|
+
conn.options.timeout = @connection_timeout if @connection_timeout
|
|
292
|
+
conn.options.open_timeout = @read_timeout if @read_timeout
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def generate_hmac_signature(method, path, content_md5, content_type, timestamp)
|
|
297
|
+
string_to_sign = [
|
|
298
|
+
method.to_s.upcase,
|
|
299
|
+
path,
|
|
300
|
+
content_md5,
|
|
301
|
+
content_type,
|
|
302
|
+
timestamp
|
|
303
|
+
].join("\n")
|
|
304
|
+
|
|
305
|
+
digest = OpenSSL::HMAC.digest("sha256", @credentials&.access_key_secret || "", string_to_sign)
|
|
306
|
+
Base64.strict_encode64(digest)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lara
|
|
4
|
+
# Credentials for accessing the Lara API. A credentials object has two properties:
|
|
5
|
+
# - access_key_id: The access key ID.
|
|
6
|
+
# - access_key_secret: The access key secret.
|
|
7
|
+
|
|
8
|
+
# IMPORTANT: Do not hard-code your access key ID and secret in your code. Always use environment variables or
|
|
9
|
+
# a credentials file. Please note also that the access key secret is never sent directly via HTTP, but it is used to
|
|
10
|
+
# sign the request. If you suspect that your access key secret has been compromised, you can revoke it in the Lara
|
|
11
|
+
# dashboard.
|
|
12
|
+
class Credentials
|
|
13
|
+
# @!attribute [r] access_key_id
|
|
14
|
+
# @return [String] The access key ID.
|
|
15
|
+
# @!attribute [r] access_key_secret
|
|
16
|
+
# @return [String] The access key secret.
|
|
17
|
+
attr_reader :access_key_id, :access_key_secret
|
|
18
|
+
|
|
19
|
+
# @param access_key_id [String] The access key ID.
|
|
20
|
+
# @param access_key_secret [String] The access key secret.
|
|
21
|
+
def initialize(access_key_id, access_key_secret)
|
|
22
|
+
@access_key_id = access_key_id
|
|
23
|
+
@access_key_secret = access_key_secret
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lara
|
|
4
|
+
class Documents
|
|
5
|
+
ALLOWED_DOCUMENT_PARAMS = %i[
|
|
6
|
+
id
|
|
7
|
+
status
|
|
8
|
+
target
|
|
9
|
+
filename
|
|
10
|
+
source
|
|
11
|
+
created_at
|
|
12
|
+
updated_at
|
|
13
|
+
options
|
|
14
|
+
translated_chars
|
|
15
|
+
total_chars
|
|
16
|
+
error_reason
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
def initialize(client, s3_client = S3Client.new)
|
|
20
|
+
@client = client
|
|
21
|
+
@s3 = s3_client
|
|
22
|
+
@polling_interval = 2
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Uploads a file to S3
|
|
26
|
+
# @return [Lara::Models::Document]
|
|
27
|
+
def upload(file_path:, filename:, target:, source: nil, adapt_to: nil, glossaries: nil,
|
|
28
|
+
no_trace: false, style: nil, password: nil, extraction_params: nil)
|
|
29
|
+
response_data = @client.get("/v2/documents/upload-url", params: { filename: filename })
|
|
30
|
+
url = response_data["url"]
|
|
31
|
+
fields = response_data["fields"]
|
|
32
|
+
|
|
33
|
+
@s3.upload(url: url, fields: fields, io: file_path)
|
|
34
|
+
|
|
35
|
+
body = {
|
|
36
|
+
s3key: fields["key"],
|
|
37
|
+
target: target,
|
|
38
|
+
source: source,
|
|
39
|
+
adapt_to: adapt_to,
|
|
40
|
+
glossaries: glossaries,
|
|
41
|
+
style: style,
|
|
42
|
+
password: password,
|
|
43
|
+
extraction_params: extraction_params&.to_h
|
|
44
|
+
}.compact
|
|
45
|
+
|
|
46
|
+
headers = {}
|
|
47
|
+
headers["X-No-Trace"] = "true" if no_trace
|
|
48
|
+
|
|
49
|
+
response = @client.post("/v2/documents", body: body, headers: headers)
|
|
50
|
+
response_params = response.transform_keys(&:to_sym)
|
|
51
|
+
Lara::Models::Document.new(**filter_document_params(response_params))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Fetch document status
|
|
55
|
+
# @return [Lara::Models::Document]
|
|
56
|
+
def status(id)
|
|
57
|
+
response = @client.get("/v2/documents/#{id}")
|
|
58
|
+
response_params = response.transform_keys(&:to_sym)
|
|
59
|
+
Lara::Models::Document.new(**filter_document_params(response_params))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Download translated document bytes
|
|
63
|
+
# @return [String] bytes
|
|
64
|
+
def download(id, output_format: nil)
|
|
65
|
+
params = {}
|
|
66
|
+
params[:output_format] = output_format if output_format
|
|
67
|
+
url = @client.get("/v2/documents/#{id}/download-url", params: params)["url"]
|
|
68
|
+
@s3.download(url: url)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Translates a document end-to-end
|
|
72
|
+
# @return [String] translated file bytes
|
|
73
|
+
def translate(file_path:, filename:, target:, source: nil, adapt_to: nil, glossaries: nil, output_format: nil,
|
|
74
|
+
no_trace: false, style: nil, password: nil, extraction_params: nil)
|
|
75
|
+
document = upload(file_path: file_path, filename: filename, target: target, source: source,
|
|
76
|
+
adapt_to: adapt_to, glossaries: glossaries, no_trace: no_trace, style: style, password: password,
|
|
77
|
+
extraction_params: extraction_params)
|
|
78
|
+
|
|
79
|
+
max_wait_time = 60 * 15 # 15 minutes
|
|
80
|
+
start = Time.now
|
|
81
|
+
|
|
82
|
+
loop do |_|
|
|
83
|
+
current = status(document.id)
|
|
84
|
+
|
|
85
|
+
case current.status
|
|
86
|
+
when Lara::Models::DocumentStatus::TRANSLATED
|
|
87
|
+
return download(current.id, output_format: output_format)
|
|
88
|
+
when Lara::Models::DocumentStatus::ERROR
|
|
89
|
+
raise Lara::LaraApiError.new(500, "DocumentError",
|
|
90
|
+
current.error_reason || "Unknown error")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
raise Timeout::Error if Time.now - start > max_wait_time
|
|
94
|
+
|
|
95
|
+
sleep @polling_interval
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def filter_document_params(params)
|
|
102
|
+
params.slice(*ALLOWED_DOCUMENT_PARAMS)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/lara/errors.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Lara
|
|
6
|
+
# Base SDK error.
|
|
7
|
+
class LaraError < StandardError
|
|
8
|
+
attr_reader :status_code
|
|
9
|
+
|
|
10
|
+
def initialize(message, status_code = nil)
|
|
11
|
+
super(message)
|
|
12
|
+
@status_code = status_code
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# API error with HTTP status, type and message.
|
|
17
|
+
class LaraApiError < LaraError
|
|
18
|
+
attr_reader :type
|
|
19
|
+
|
|
20
|
+
# Builds an error from an HTTP response with JSON body:
|
|
21
|
+
# { "error": { "type": "...", "message": "..." } }
|
|
22
|
+
def self.from_response(response)
|
|
23
|
+
data = begin
|
|
24
|
+
JSON.parse(response.body)
|
|
25
|
+
rescue StandardError
|
|
26
|
+
{}
|
|
27
|
+
end
|
|
28
|
+
error = data["error"] || {}
|
|
29
|
+
error_type = error["type"] || "UnknownError"
|
|
30
|
+
error_message = error["message"] || "An unknown error occurred"
|
|
31
|
+
new(response.status, error_type, error_message)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param status_code [Integer]
|
|
35
|
+
# @param type [String]
|
|
36
|
+
# @param message [String]
|
|
37
|
+
def initialize(status_code, type, message)
|
|
38
|
+
super("(HTTP #{status_code}) #{type}: #{message}", status_code)
|
|
39
|
+
@type = type
|
|
40
|
+
@message = message
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|