clicksign-ruby-sdk 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +27 -4
- data/lib/clicksign/client.rb +6 -6
- data/lib/clicksign/configuration.rb +2 -2
- data/lib/clicksign/error_handler.rb +2 -1
- data/lib/clicksign/errors.rb +1 -1
- data/lib/clicksign/instrumentation.rb +5 -3
- data/lib/clicksign/json_api/bulk_operations_client.rb +8 -8
- data/lib/clicksign/json_api/operations/bulk_requirement.rb +1 -1
- data/lib/clicksign/json_api/query_builder.rb +2 -1
- data/lib/clicksign/request_instrumentation.rb +7 -7
- data/lib/clicksign/resource.rb +16 -8
- data/lib/clicksign/resources/notarial/envelope.rb +5 -5
- data/lib/clicksign/resources/notarial/event.rb +47 -1
- data/lib/clicksign/resources/notarial/signature_watcher.rb +1 -1
- data/lib/clicksign/resources/notarial/signer.rb +1 -1
- data/lib/clicksign/services.rb +1 -1
- data/lib/clicksign/webhook.rb +3 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a45728b201a9703e1d70cb6de98056f85da9a524d9d0c122a1ed951d6f98ef5b
|
|
4
|
+
data.tar.gz: 7a2aea99029254b4849b27090f40927d4cbf66d44cb516f121ea14c65544f141
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e9cb3bdaff221f8a8e58a9ef4ca47363962e19b7dcb4f22eb129cb1f5f3b60812550db76301ee8a36473be11490293932895d83c91ef60d10928206d090cd162
|
|
7
|
+
data.tar.gz: 8c3cd41e15ba5c21136fc3ee4b53fcb96c231620735c17401acbd046a032262b31a9a1617f07ec704e9a9593dedb8023dd82019afe156cee782356c78c297199
|
data/README.md
CHANGED
|
@@ -95,6 +95,8 @@ A API usa o header `Authorization: <seu-token>` **sem** o prefixo `Bearer`.
|
|
|
95
95
|
|
|
96
96
|
> **Segurança:** não commite tokens no código. Use variáveis de ambiente ou cofre de secrets (Rails credentials, etc.).
|
|
97
97
|
|
|
98
|
+
> **`api_key` é obrigatório em runtime:** o SDK não valida a presença do token no boot. Se `api_key` for `nil`, nenhum erro é levantado no `configure` — o `AuthenticationError` só aparece na primeira request HTTP. Use `ENV.fetch('CLICKSIGN_API_KEY')` (em vez de `ENV[]`) para detectar a ausência da variável no startup da aplicação.
|
|
99
|
+
|
|
98
100
|
> **Multi-conta / multi-tenant:** se cada requisição pode usar credenciais diferentes (SaaS, workers por cliente), prefira [`Clicksign::Services`](#multi-conta-e-cliente-instantiável) em vez da config global.
|
|
99
101
|
|
|
100
102
|
Para testar interativamente no console da gem:
|
|
@@ -188,7 +190,7 @@ Com `max_retries > 0`, o client reexecuta a requisição em erros **transitório
|
|
|
188
190
|
- `Clicksign::RateLimitError`
|
|
189
191
|
- `Clicksign::ServerError` (5xx)
|
|
190
192
|
|
|
191
|
-
Backoff exponencial com **full jitter
|
|
193
|
+
Backoff exponencial com **full jitter**: espera aleatória uniforme em `[0, teto)` onde o teto cresce como `0.5s × 2^(tentativa-1)` (0,5s → 1s → 2s…) com cap de **30s**. O zero é possível — o jitter distribui a espera para evitar thundering herd. Após esgotar as retentativas, a exceção original é relançada.
|
|
192
194
|
|
|
193
195
|
```ruby
|
|
194
196
|
Clicksign.configure do |c|
|
|
@@ -462,12 +464,33 @@ Envelope.list_events(envelope.id)
|
|
|
462
464
|
# Eventos de um documento
|
|
463
465
|
Document.list_events(document.id, envelope_id: envelope.id)
|
|
464
466
|
|
|
465
|
-
# Criar evento
|
|
466
|
-
Event.
|
|
467
|
+
# Criar evento de imagem no documento (comprovante JPEG)
|
|
468
|
+
Event.create_add_image(
|
|
469
|
+
envelope_id: envelope.id,
|
|
470
|
+
document_id: document.id,
|
|
471
|
+
title: 'Comprovante de identidade',
|
|
472
|
+
occurred_at: Time.now.iso8601,
|
|
473
|
+
content_base64: 'data:image/jpeg;base64,...'
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Criar evento customizado — token_email ou token_sms
|
|
477
|
+
Event.create_custom(
|
|
478
|
+
envelope_id: envelope.id,
|
|
479
|
+
document_id: document.id,
|
|
480
|
+
kind: 'token_email', # ou 'token_sms'
|
|
481
|
+
occurred_at: Time.now.iso8601,
|
|
482
|
+
signer_name: 'Maria Silva',
|
|
483
|
+
signer_email: 'maria@empresa.com',
|
|
484
|
+
# signer_phone_number: '11988887777' # obrigatório quando kind: 'token_sms'
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# API de baixo nível — qualquer name customizado
|
|
488
|
+
Event.create(
|
|
467
489
|
envelope_id: envelope.id,
|
|
468
490
|
document_id: document.id,
|
|
469
491
|
name: 'custom',
|
|
470
|
-
data: {
|
|
492
|
+
data: { kind: 'token_email', signer_name: 'Maria Silva',
|
|
493
|
+
signer_email: 'maria@empresa.com', occurred_at: Time.now.iso8601 }
|
|
471
494
|
)
|
|
472
495
|
```
|
|
473
496
|
|
data/lib/clicksign/client.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Clicksign
|
|
|
14
14
|
}.freeze
|
|
15
15
|
|
|
16
16
|
def initialize(api_key:, base_url:, open_timeout: 2, read_timeout: 10,
|
|
17
|
-
|
|
17
|
+
write_timeout: 10, max_retries: 0)
|
|
18
18
|
@api_key = api_key
|
|
19
19
|
@base_url = base_url
|
|
20
20
|
@open_timeout = open_timeout
|
|
@@ -88,11 +88,11 @@ module Clicksign
|
|
|
88
88
|
|
|
89
89
|
def http_request(request, uri)
|
|
90
90
|
Net::HTTP.start(uri.host, uri.port,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
use_ssl: uri.scheme == 'https',
|
|
92
|
+
open_timeout: @open_timeout,
|
|
93
|
+
read_timeout: @read_timeout,
|
|
94
|
+
write_timeout: @write_timeout,
|
|
95
|
+
&proc { |http| http.request(request) })
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
def handle_response(response, context, start)
|
|
@@ -8,7 +8,7 @@ module Clicksign
|
|
|
8
8
|
}.freeze
|
|
9
9
|
|
|
10
10
|
attr_accessor :api_key, :base_url, :open_timeout, :read_timeout,
|
|
11
|
-
|
|
11
|
+
:write_timeout, :max_retries, :logger
|
|
12
12
|
|
|
13
13
|
def initialize
|
|
14
14
|
@base_url = 'https://app.clicksign.com/api/v3'
|
|
@@ -21,7 +21,7 @@ module Clicksign
|
|
|
21
21
|
def environment=(env)
|
|
22
22
|
url = ENVIRONMENTS.fetch(env.to_sym) do
|
|
23
23
|
raise ArgumentError,
|
|
24
|
-
|
|
24
|
+
"Unknown environment: #{env}. Valid: #{ENVIRONMENTS.keys.join(', ')}"
|
|
25
25
|
end
|
|
26
26
|
self.base_url = url
|
|
27
27
|
end
|
|
@@ -50,7 +50,8 @@ module Clicksign
|
|
|
50
50
|
errors = body['errors']
|
|
51
51
|
return response.message unless errors.is_a?(Array)
|
|
52
52
|
|
|
53
|
-
errors.filter_map { |e| e['detail'] || e['title'] }.join(', ')
|
|
53
|
+
result = errors.filter_map { |e| e['detail'] || e['title'] }.join(', ')
|
|
54
|
+
result.empty? ? response.message : result
|
|
54
55
|
end
|
|
55
56
|
end
|
|
56
57
|
end
|
data/lib/clicksign/errors.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Clicksign
|
|
|
5
5
|
attr_reader :status_code, :request_id, :response_body, :response_headers
|
|
6
6
|
|
|
7
7
|
def initialize(message = nil, status_code: nil, request_id: nil,
|
|
8
|
-
|
|
8
|
+
response_body: nil, response_headers: {})
|
|
9
9
|
super(message)
|
|
10
10
|
@status_code = status_code
|
|
11
11
|
@request_id = request_id
|
|
@@ -5,6 +5,7 @@ module Clicksign
|
|
|
5
5
|
EVENTS = %i[request retry error].freeze
|
|
6
6
|
|
|
7
7
|
@callbacks = Hash.new { |h, k| h[k] = [] }
|
|
8
|
+
@mutex = Mutex.new
|
|
8
9
|
|
|
9
10
|
class << self
|
|
10
11
|
def on(event, &block)
|
|
@@ -12,11 +13,12 @@ module Clicksign
|
|
|
12
13
|
raise ArgumentError, "Unknown event: #{event}. Valid: #{EVENTS.join(', ')}"
|
|
13
14
|
end
|
|
14
15
|
|
|
15
|
-
@callbacks[event] << block
|
|
16
|
+
@mutex.synchronize { @callbacks[event] << block }
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def publish(event, payload)
|
|
19
|
-
@callbacks[event].
|
|
20
|
+
callbacks = @mutex.synchronize { @callbacks[event].dup }
|
|
21
|
+
callbacks.each do |cb|
|
|
20
22
|
cb.call(payload)
|
|
21
23
|
rescue StandardError => e
|
|
22
24
|
Clicksign.configuration.logger&.warn(
|
|
@@ -28,7 +30,7 @@ module Clicksign
|
|
|
28
30
|
|
|
29
31
|
# Removes all registered callbacks — intended for test teardown.
|
|
30
32
|
def clear
|
|
31
|
-
@callbacks = Hash.new { |h, k| h[k] = [] }
|
|
33
|
+
@mutex.synchronize { @callbacks = Hash.new { |h, k| h[k] = [] } }
|
|
32
34
|
end
|
|
33
35
|
end
|
|
34
36
|
end
|
|
@@ -15,7 +15,7 @@ module Clicksign
|
|
|
15
15
|
}.freeze
|
|
16
16
|
|
|
17
17
|
def initialize(api_key:, base_url:, open_timeout: 2, read_timeout: 10,
|
|
18
|
-
|
|
18
|
+
write_timeout: 10, max_retries: 0)
|
|
19
19
|
@api_key = api_key
|
|
20
20
|
@base_url = base_url
|
|
21
21
|
@open_timeout = open_timeout
|
|
@@ -78,11 +78,11 @@ module Clicksign
|
|
|
78
78
|
|
|
79
79
|
def http_post(request, uri)
|
|
80
80
|
Net::HTTP.start(uri.host, uri.port,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
use_ssl: uri.scheme == 'https',
|
|
82
|
+
open_timeout: @open_timeout,
|
|
83
|
+
read_timeout: @read_timeout,
|
|
84
|
+
write_timeout: @write_timeout,
|
|
85
|
+
&proc { |http| http.request(request) })
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def headers
|
|
@@ -97,8 +97,8 @@ module Clicksign
|
|
|
97
97
|
return nil if response.body.nil? || response.body.empty?
|
|
98
98
|
|
|
99
99
|
JSON.parse(response.body)
|
|
100
|
-
rescue JSON::ParserError
|
|
101
|
-
|
|
100
|
+
rescue JSON::ParserError => e
|
|
101
|
+
raise Error, "Invalid JSON response from bulk operations endpoint: #{e.message}"
|
|
102
102
|
end
|
|
103
103
|
end
|
|
104
104
|
end
|
|
@@ -33,7 +33,7 @@ module Clicksign
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def add_rubricate(signer_id:, document_id:, pages: nil, rubric_field: nil,
|
|
36
|
-
|
|
36
|
+
kind: nil)
|
|
37
37
|
validate_ids!(signer_id, document_id)
|
|
38
38
|
if pages.nil? && rubric_field.nil?
|
|
39
39
|
raise ArgumentError, 'pages or rubric_field is required'
|
|
@@ -27,13 +27,13 @@ module Clicksign
|
|
|
27
27
|
|
|
28
28
|
def publish_retry(request, uri, attempt, error, delay)
|
|
29
29
|
Instrumentation.publish(:retry, {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
method: request.method.downcase.to_sym,
|
|
31
|
+
path: resource_path(uri),
|
|
32
|
+
attempt: attempt,
|
|
33
|
+
max_retries: @max_retries,
|
|
34
|
+
error: error,
|
|
35
|
+
wait_ms: (delay * 1000).round,
|
|
36
|
+
})
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def handle_network_error(error, context, start)
|
data/lib/clicksign/resource.rb
CHANGED
|
@@ -133,8 +133,8 @@ module Clicksign
|
|
|
133
133
|
modules, jsonapi = types.partition { |t| t.is_a?(Module) }
|
|
134
134
|
if modules.any? && jsonapi.any?
|
|
135
135
|
raise ArgumentError,
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
'cannot mix Module with JSON:API ' \
|
|
137
|
+
'include types — use with_includes for sideload'
|
|
138
138
|
end
|
|
139
139
|
if modules.any?
|
|
140
140
|
modules.each { |mod| super(mod) }
|
|
@@ -193,8 +193,8 @@ module Clicksign
|
|
|
193
193
|
return if invalid.empty?
|
|
194
194
|
|
|
195
195
|
raise ArgumentError,
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
'JSON:API include types must be String or Symbol, ' \
|
|
197
|
+
"got: #{invalid.map(&:class).uniq.join(', ')}"
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
private
|
|
@@ -212,8 +212,8 @@ module Clicksign
|
|
|
212
212
|
|
|
213
213
|
loop do
|
|
214
214
|
raw = client.get(endpoint,
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
params: base.merge('page[number]' => page,
|
|
216
|
+
'page[size]' => per))
|
|
217
217
|
parsed = JsonApi::Parser.parse(raw)
|
|
218
218
|
items = parsed[:data].map { |item| build_instance(item) }
|
|
219
219
|
yield items
|
|
@@ -236,6 +236,8 @@ module Clicksign
|
|
|
236
236
|
end
|
|
237
237
|
|
|
238
238
|
def build_instance(data, parent_id: nil)
|
|
239
|
+
raise NotFoundError, 'API returned null data' if data.nil?
|
|
240
|
+
|
|
239
241
|
instance = allocate
|
|
240
242
|
instance.send(:load_data, data, parent_id: parent_id)
|
|
241
243
|
instance
|
|
@@ -261,7 +263,10 @@ module Clicksign
|
|
|
261
263
|
),
|
|
262
264
|
)
|
|
263
265
|
parsed = JsonApi::Parser.parse(raw)
|
|
264
|
-
|
|
266
|
+
data = parsed[:data].first
|
|
267
|
+
raise NotFoundError, 'API returned null data' if data.nil?
|
|
268
|
+
|
|
269
|
+
load_data(data, parent_id: @_parent_id)
|
|
265
270
|
self
|
|
266
271
|
end
|
|
267
272
|
|
|
@@ -273,7 +278,10 @@ module Clicksign
|
|
|
273
278
|
def reload
|
|
274
279
|
raw = self.class.client.get("#{base_path}/#{@id}")
|
|
275
280
|
parsed = JsonApi::Parser.parse(raw)
|
|
276
|
-
|
|
281
|
+
data = parsed[:data].first
|
|
282
|
+
raise NotFoundError, 'API returned null data' if data.nil?
|
|
283
|
+
|
|
284
|
+
load_data(data, parent_id: @_parent_id)
|
|
277
285
|
self
|
|
278
286
|
end
|
|
279
287
|
|
|
@@ -27,23 +27,23 @@ module Clicksign
|
|
|
27
27
|
|
|
28
28
|
def self.list_events(envelope_id, **filters)
|
|
29
29
|
nested_list(envelope_id, nested_type: 'events', as: Event,
|
|
30
|
-
|
|
30
|
+
params: filter_params(**filters))
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def self.list_documents(envelope_id, **filters)
|
|
34
34
|
nested_list(envelope_id, nested_type: 'documents', as: Document,
|
|
35
|
-
|
|
35
|
+
params: filter_params(**filters))
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def self.list_signers(envelope_id, **filters)
|
|
39
39
|
nested_list(envelope_id, nested_type: 'signers', as: Signer,
|
|
40
|
-
|
|
40
|
+
params: filter_params(**filters))
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def self.list_signature_watchers(envelope_id, **filters)
|
|
44
44
|
nested_list(envelope_id, nested_type: 'signature_watchers',
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
as: SignatureWatcher,
|
|
46
|
+
params: filter_params(**filters))
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def self.list_requirements(envelope_id, **filters)
|
|
@@ -6,7 +6,9 @@ module Clicksign
|
|
|
6
6
|
class Event < Clicksign::Resource
|
|
7
7
|
self.resource_type = 'events'
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
CUSTOM_KINDS = %w[token_email token_sms].freeze
|
|
10
|
+
|
|
11
|
+
def self.create(envelope_id:, document_id:, **attributes)
|
|
10
12
|
raw = client.post(
|
|
11
13
|
"/envelopes/#{envelope_id}/documents/#{document_id}/events",
|
|
12
14
|
body: JsonApi::Serializer.dump(type: resource_type, attributes: attributes),
|
|
@@ -14,6 +16,50 @@ module Clicksign
|
|
|
14
16
|
parsed = JsonApi::Parser.parse(raw)
|
|
15
17
|
build_instance(parsed[:data].first)
|
|
16
18
|
end
|
|
19
|
+
|
|
20
|
+
def self.create_add_image(envelope_id:, document_id:, title:, occurred_at:,
|
|
21
|
+
content_base64:)
|
|
22
|
+
create(
|
|
23
|
+
envelope_id: envelope_id,
|
|
24
|
+
document_id: document_id,
|
|
25
|
+
name: 'add_image',
|
|
26
|
+
content_base64: content_base64,
|
|
27
|
+
data: { title: title, occurred_at: occurred_at },
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.create_custom(envelope_id:, document_id:, kind:, occurred_at:,
|
|
32
|
+
signer_name:, signer_email: nil, signer_phone_number: nil)
|
|
33
|
+
unless CUSTOM_KINDS.include?(kind.to_s)
|
|
34
|
+
raise ArgumentError, "kind must be one of: #{CUSTOM_KINDS.join(', ')}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
create(
|
|
38
|
+
envelope_id: envelope_id,
|
|
39
|
+
document_id: document_id,
|
|
40
|
+
name: 'custom',
|
|
41
|
+
data: {
|
|
42
|
+
kind: kind,
|
|
43
|
+
occurred_at: occurred_at,
|
|
44
|
+
signer_name: signer_name,
|
|
45
|
+
signer_email: signer_email,
|
|
46
|
+
signer_phone_number: signer_phone_number,
|
|
47
|
+
}.compact,
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# API only exposes GET (list) and POST (create) for events — no singleton routes.
|
|
52
|
+
def update(**)
|
|
53
|
+
raise NotImplementedError, 'Event does not support update'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def delete
|
|
57
|
+
raise NotImplementedError, 'Event does not support delete'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def reload
|
|
61
|
+
raise NotImplementedError, 'Event does not support reload'
|
|
62
|
+
end
|
|
17
63
|
end
|
|
18
64
|
end
|
|
19
65
|
end
|
data/lib/clicksign/services.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Clicksign
|
|
4
4
|
class Services
|
|
5
5
|
def initialize(api_key:, environment: :production, base_url: nil,
|
|
6
|
-
|
|
6
|
+
open_timeout: 2, read_timeout: 10, write_timeout: 10, max_retries: 0)
|
|
7
7
|
resolved_url = base_url || resolve_environment(environment)
|
|
8
8
|
@client = Client.new(
|
|
9
9
|
api_key: api_key,
|
data/lib/clicksign/webhook.rb
CHANGED
|
@@ -30,8 +30,10 @@ module Clicksign
|
|
|
30
30
|
|
|
31
31
|
# Constant-time comparison to prevent timing attacks.
|
|
32
32
|
def self.secure_compare?(expected, actual)
|
|
33
|
+
return false if actual.nil? || actual.to_s.empty?
|
|
34
|
+
|
|
33
35
|
digest_a = OpenSSL::Digest::SHA256.hexdigest(expected)
|
|
34
|
-
digest_b = OpenSSL::Digest::SHA256.hexdigest(actual)
|
|
36
|
+
digest_b = OpenSSL::Digest::SHA256.hexdigest(actual.to_s)
|
|
35
37
|
result = 0
|
|
36
38
|
digest_a.bytes.zip(digest_b.bytes) { |x, y| result |= x ^ y }
|
|
37
39
|
result.zero?
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clicksign-ruby-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Clicksign
|
|
@@ -77,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
77
77
|
- !ruby/object:Gem::Version
|
|
78
78
|
version: '0'
|
|
79
79
|
requirements: []
|
|
80
|
-
rubygems_version:
|
|
80
|
+
rubygems_version: 3.6.9
|
|
81
81
|
specification_version: 4
|
|
82
82
|
summary: Ruby SDK for the Clicksign API
|
|
83
83
|
test_files: []
|