baza.rb 0.0.8 → 0.0.10
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/.0pdd.yml +2 -19
- data/.github/workflows/actionlint.yml +5 -21
- data/.github/workflows/codecov.yml +9 -23
- data/.github/workflows/copyrights.yml +10 -21
- data/.github/workflows/markdown-lint.yml +5 -20
- data/.github/workflows/pdd.yml +5 -20
- data/.github/workflows/rake.yml +6 -22
- data/.github/workflows/reuse.yml +19 -0
- data/.github/workflows/xcop.yml +9 -20
- data/.github/workflows/yamllint.yml +5 -20
- data/.gitignore +7 -5
- data/.rubocop.yml +8 -20
- data/.rultor.yml +4 -20
- data/.simplecov +2 -19
- data/.yamllint.yml +2 -19
- data/Gemfile +16 -31
- data/Gemfile.lock +84 -165
- data/LICENSE.txt +1 -1
- data/LICENSES/MIT.txt +21 -0
- data/README.md +1 -1
- data/REUSE.toml +34 -0
- data/Rakefile +3 -29
- data/baza.rb.gemspec +2 -19
- data/lib/baza-rb/version.rb +3 -20
- data/lib/baza-rb.rb +153 -59
- data/test/test__helper.rb +2 -19
- data/test/test_baza-rb.rb +94 -42
- metadata +6 -6
data/lib/baza-rb.rb
CHANGED
@@ -1,24 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright (c) 2024 Zerocracy
|
4
|
-
#
|
5
|
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
-
# of this software and associated documentation files (the 'Software'), to deal
|
7
|
-
# in the Software without restriction, including without limitation the rights
|
8
|
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
-
# copies of the Software, and to permit persons to whom the Software is
|
10
|
-
# furnished to do so, subject to the following conditions:
|
11
|
-
#
|
12
|
-
# The above copyright notice and this permission notice shall be included in all
|
13
|
-
# copies or substantial portions of the Software.
|
14
|
-
#
|
15
|
-
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
-
# SOFTWARE.
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
|
4
|
+
# SPDX-License-Identifier: MIT
|
22
5
|
|
23
6
|
require 'base64'
|
24
7
|
require 'elapsed'
|
@@ -26,8 +9,11 @@ require 'fileutils'
|
|
26
9
|
require 'iri'
|
27
10
|
require 'loog'
|
28
11
|
require 'retries'
|
12
|
+
require 'stringio'
|
29
13
|
require 'tago'
|
14
|
+
require 'tempfile'
|
30
15
|
require 'typhoeus'
|
16
|
+
require 'zlib'
|
31
17
|
require_relative 'baza-rb/version'
|
32
18
|
|
33
19
|
# Interface to the API of zerocracy.com.
|
@@ -37,9 +23,18 @@ require_relative 'baza-rb/version'
|
|
37
23
|
# results returned.
|
38
24
|
#
|
39
25
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
40
|
-
# Copyright:: Copyright (c) 2024 Yegor Bugayenko
|
26
|
+
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
41
27
|
# License:: MIT
|
42
28
|
class BazaRb
|
29
|
+
# When the server failed (503).
|
30
|
+
class ServerFailure < StandardError; end
|
31
|
+
|
32
|
+
# When request timeout.
|
33
|
+
class TimedOut < StandardError; end
|
34
|
+
|
35
|
+
# Unexpected response arrived from the server.
|
36
|
+
class BadResponse < StandardError; end
|
37
|
+
|
43
38
|
# Ctor.
|
44
39
|
#
|
45
40
|
# @param [String] host Host name
|
@@ -78,7 +73,7 @@ class BazaRb
|
|
78
73
|
'Content-Length' => data.size
|
79
74
|
)
|
80
75
|
unless meta.empty?
|
81
|
-
hdrs = hdrs.merge('X-Zerocracy-Meta' => meta.map { |v| Base64.encode64(v).
|
76
|
+
hdrs = hdrs.merge('X-Zerocracy-Meta' => meta.map { |v| Base64.encode64(v).delete("\n") }.join(' '))
|
82
77
|
end
|
83
78
|
params = {
|
84
79
|
connecttimeout: @timeout,
|
@@ -88,7 +83,7 @@ class BazaRb
|
|
88
83
|
}
|
89
84
|
elapsed(@loog) do
|
90
85
|
ret =
|
91
|
-
with_retries(max_tries: @retries) do
|
86
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
92
87
|
checked(
|
93
88
|
Typhoeus::Request.put(
|
94
89
|
home.append('push').append(name).to_s,
|
@@ -109,7 +104,7 @@ class BazaRb
|
|
109
104
|
def pull(id)
|
110
105
|
raise 'The ID of the job is nil' if id.nil?
|
111
106
|
raise 'The ID of the job must be a positive integer' unless id.positive?
|
112
|
-
data =
|
107
|
+
data = ''
|
113
108
|
elapsed(@loog) do
|
114
109
|
Tempfile.open do |file|
|
115
110
|
File.open(file, 'wb') do |f|
|
@@ -126,7 +121,7 @@ class BazaRb
|
|
126
121
|
request.on_body do |chunk|
|
127
122
|
f.write(chunk)
|
128
123
|
end
|
129
|
-
with_retries(max_tries: @retries) do
|
124
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
130
125
|
request.run
|
131
126
|
end
|
132
127
|
checked(request.response)
|
@@ -148,7 +143,7 @@ class BazaRb
|
|
148
143
|
finished = false
|
149
144
|
elapsed(@loog) do
|
150
145
|
ret =
|
151
|
-
with_retries(max_tries: @retries) do
|
146
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
152
147
|
checked(
|
153
148
|
Typhoeus::Request.get(
|
154
149
|
home.append('finished').append(id).to_s,
|
@@ -172,7 +167,7 @@ class BazaRb
|
|
172
167
|
stdout = ''
|
173
168
|
elapsed(@loog) do
|
174
169
|
ret =
|
175
|
-
with_retries(max_tries: @retries) do
|
170
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
176
171
|
checked(
|
177
172
|
Typhoeus::Request.get(
|
178
173
|
home.append('stdout').append("#{id}.txt").to_s,
|
@@ -196,7 +191,7 @@ class BazaRb
|
|
196
191
|
code = 0
|
197
192
|
elapsed(@loog) do
|
198
193
|
ret =
|
199
|
-
with_retries(max_tries: @retries) do
|
194
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
200
195
|
checked(
|
201
196
|
Typhoeus::Request.get(
|
202
197
|
home.append('exit').append("#{id}.txt").to_s,
|
@@ -217,10 +212,10 @@ class BazaRb
|
|
217
212
|
def verified(id)
|
218
213
|
raise 'The ID of the job is nil' if id.nil?
|
219
214
|
raise 'The ID of the job must be a positive integer' unless id.positive?
|
220
|
-
verdict =
|
215
|
+
verdict = ''
|
221
216
|
elapsed(@loog) do
|
222
217
|
ret =
|
223
|
-
with_retries(max_tries: @retries) do
|
218
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
224
219
|
checked(
|
225
220
|
Typhoeus::Request.get(
|
226
221
|
home.append('jobs').append(id).append('verified.txt').to_s,
|
@@ -243,16 +238,18 @@ class BazaRb
|
|
243
238
|
raise 'The "name" of the job may not be empty' if name.empty?
|
244
239
|
raise 'The "owner" of the lock is nil' if owner.nil?
|
245
240
|
elapsed(@loog) do
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
241
|
+
ret =
|
242
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
243
|
+
checked(
|
244
|
+
Typhoeus::Request.get(
|
245
|
+
home.append('lock').append(name).add(owner:).to_s,
|
246
|
+
headers:
|
247
|
+
),
|
248
|
+
[302, 409]
|
249
|
+
)
|
250
|
+
end
|
251
|
+
throw :"Job name '#{name}' locked at #{@host}" if ret.code == 302
|
252
|
+
raise "Failed to lock '#{name}' job at #{@host}, it's most probably already locked"
|
256
253
|
end
|
257
254
|
end
|
258
255
|
|
@@ -265,7 +262,7 @@ class BazaRb
|
|
265
262
|
raise 'The "name" of the job may not be empty' if name.empty?
|
266
263
|
raise 'The "owner" of the lock is nil' if owner.nil?
|
267
264
|
elapsed(@loog) do
|
268
|
-
with_retries(max_tries: @retries) do
|
265
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
269
266
|
checked(
|
270
267
|
Typhoeus::Request.get(
|
271
268
|
home.append('unlock').append(name).add(owner:).to_s,
|
@@ -285,10 +282,10 @@ class BazaRb
|
|
285
282
|
def recent(name)
|
286
283
|
raise 'The "name" of the job is nil' if name.nil?
|
287
284
|
raise 'The "name" of the job may not be empty' if name.empty?
|
288
|
-
job =
|
285
|
+
job = nil
|
289
286
|
elapsed(@loog) do
|
290
287
|
ret =
|
291
|
-
with_retries(max_tries: @retries) do
|
288
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
292
289
|
checked(
|
293
290
|
Typhoeus::Request.get(
|
294
291
|
home.append('recent').append("#{name}.txt").to_s,
|
@@ -309,10 +306,10 @@ class BazaRb
|
|
309
306
|
def name_exists?(name)
|
310
307
|
raise 'The "name" of the job is nil' if name.nil?
|
311
308
|
raise 'The "name" of the job may not be empty' if name.empty?
|
312
|
-
exists =
|
309
|
+
exists = false
|
313
310
|
elapsed(@loog) do
|
314
311
|
ret =
|
315
|
-
with_retries(max_tries: @retries) do
|
312
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
316
313
|
checked(
|
317
314
|
Typhoeus::Request.get(
|
318
315
|
home.append('exists').append(name).to_s,
|
@@ -338,11 +335,12 @@ class BazaRb
|
|
338
335
|
id = nil
|
339
336
|
elapsed(@loog) do
|
340
337
|
ret =
|
341
|
-
with_retries(max_tries: @retries) do
|
338
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
342
339
|
checked(
|
343
340
|
Typhoeus::Request.post(
|
344
341
|
home.append('durables').append('place').to_s,
|
345
342
|
body: {
|
343
|
+
'_csrf' => csrf,
|
346
344
|
'jname' => jname,
|
347
345
|
'file' => File.basename(file),
|
348
346
|
'zip' => File.open(file, 'rb')
|
@@ -370,7 +368,7 @@ class BazaRb
|
|
370
368
|
raise 'The "file" of the durable is nil' if file.nil?
|
371
369
|
raise "The file '#{file}' is absent" unless File.exist?(file)
|
372
370
|
elapsed(@loog) do
|
373
|
-
with_retries(max_tries: @retries) do
|
371
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
374
372
|
checked(
|
375
373
|
Typhoeus::Request.put(
|
376
374
|
home.append('durables').append(id).to_s,
|
@@ -408,7 +406,7 @@ class BazaRb
|
|
408
406
|
request.on_body do |chunk|
|
409
407
|
f.write(chunk)
|
410
408
|
end
|
411
|
-
with_retries(max_tries: @retries) do
|
409
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
412
410
|
request.run
|
413
411
|
end
|
414
412
|
checked(request.response)
|
@@ -427,7 +425,7 @@ class BazaRb
|
|
427
425
|
raise 'The "owner" of the lock is nil' if owner.nil?
|
428
426
|
raise 'The "owner" of the lock may not be empty' if owner.empty?
|
429
427
|
elapsed(@loog) do
|
430
|
-
with_retries(max_tries: @retries) do
|
428
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
431
429
|
checked(
|
432
430
|
Typhoeus::Request.get(
|
433
431
|
home.append('durables').append(id).append('lock').add(owner:).to_s,
|
@@ -450,7 +448,7 @@ class BazaRb
|
|
450
448
|
raise 'The "owner" of the lock is nil' if owner.nil?
|
451
449
|
raise 'The "owner" of the lock may not be empty' if owner.empty?
|
452
450
|
elapsed(@loog) do
|
453
|
-
with_retries(max_tries: @retries) do
|
451
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
454
452
|
checked(
|
455
453
|
Typhoeus::Request.get(
|
456
454
|
home.append('durables').append(id).append('unlock').add(owner:).to_s,
|
@@ -463,6 +461,38 @@ class BazaRb
|
|
463
461
|
end
|
464
462
|
end
|
465
463
|
|
464
|
+
# Transfer some funds to another user.
|
465
|
+
#
|
466
|
+
# @param [String] recipient GitHub name (e.g. "yegor256") of the recipient
|
467
|
+
# @param [Float] amount The amount in Z/USDT (not zents!)
|
468
|
+
# @param [String] summary The description of the payment
|
469
|
+
def transfer(recipient, amount, summary)
|
470
|
+
raise 'The "recipient" is nil' if recipient.nil?
|
471
|
+
raise 'The "amount" is nil' if amount.nil?
|
472
|
+
raise 'The "amount" must be Float' unless amount.is_a?(Float)
|
473
|
+
raise 'The "summary" is nil' if summary.nil?
|
474
|
+
elapsed(@loog) do
|
475
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
476
|
+
checked(
|
477
|
+
Typhoeus::Request.post(
|
478
|
+
home.append('account').append('transfer').to_s,
|
479
|
+
body: {
|
480
|
+
'_csrf' => csrf,
|
481
|
+
'human' => recipient,
|
482
|
+
'amount' => amount.to_s,
|
483
|
+
'summary' => summary
|
484
|
+
},
|
485
|
+
headers:,
|
486
|
+
connecttimeout: @timeout,
|
487
|
+
timeout: @timeout
|
488
|
+
),
|
489
|
+
302
|
490
|
+
)
|
491
|
+
end
|
492
|
+
throw :"Transferred ##{amount} to @#{recipient} at #{@host}"
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
466
496
|
# Pop job from the server.
|
467
497
|
#
|
468
498
|
# @param [String] owner Who is acting (could be any text)
|
@@ -470,7 +500,6 @@ class BazaRb
|
|
470
500
|
# @return [Boolean] TRUE if job taken, otherwise false
|
471
501
|
def pop(owner, zip)
|
472
502
|
raise 'The "zip" of the job is nil' if zip.nil?
|
473
|
-
raise "The 'zip' file is absent: #{zip}" unless File.exist?(zip)
|
474
503
|
success = false
|
475
504
|
FileUtils.rm_f(zip)
|
476
505
|
elapsed(@loog) do
|
@@ -487,7 +516,7 @@ class BazaRb
|
|
487
516
|
request.on_body do |chunk|
|
488
517
|
f.write(chunk)
|
489
518
|
end
|
490
|
-
with_retries(max_tries: @retries) do
|
519
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
491
520
|
request.run
|
492
521
|
end
|
493
522
|
ret = request.response
|
@@ -503,17 +532,17 @@ class BazaRb
|
|
503
532
|
success
|
504
533
|
end
|
505
534
|
|
506
|
-
# Submit a ZIP
|
535
|
+
# Submit a ZIP archive to finish a job.
|
507
536
|
#
|
508
537
|
# @param [Integer] id The ID of the job on the server
|
509
538
|
# @param [String] zip The path to the ZIP file with the content of the archive
|
510
539
|
def finish(id, zip)
|
511
|
-
raise 'The
|
512
|
-
raise 'The
|
540
|
+
raise 'The ID of the job is nil' if id.nil?
|
541
|
+
raise 'The ID of the job must be a positive integer' unless id.positive?
|
513
542
|
raise 'The "zip" of the job is nil' if zip.nil?
|
514
543
|
raise "The 'zip' file is absent: #{zip}" unless File.exist?(zip)
|
515
544
|
elapsed(@loog) do
|
516
|
-
with_retries(max_tries: @retries) do
|
545
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
517
546
|
checked(
|
518
547
|
Typhoeus::Request.put(
|
519
548
|
home.append('finish').add(id:).to_s,
|
@@ -531,6 +560,65 @@ class BazaRb
|
|
531
560
|
end
|
532
561
|
end
|
533
562
|
|
563
|
+
# Enter a valve.
|
564
|
+
#
|
565
|
+
# @param [String] name Name of the job
|
566
|
+
# @param [String] badge Unique badge of the valve
|
567
|
+
# @param [String] why The reason
|
568
|
+
# @param [nil|Integer] job The ID of the job
|
569
|
+
# @return [String] The result just calculated or retrieved
|
570
|
+
def enter(name, badge, why, job)
|
571
|
+
elapsed(@loog, intro: "Entered valve #{badge} to #{name}") do
|
572
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
573
|
+
ret = checked(
|
574
|
+
Typhoeus::Request.get(
|
575
|
+
home.append('valves').append('result').add(badge:).to_s,
|
576
|
+
headers:
|
577
|
+
),
|
578
|
+
[200, 204]
|
579
|
+
)
|
580
|
+
return ret.body if ret.code == 200
|
581
|
+
r = yield
|
582
|
+
uri = home.append('valves').append('add')
|
583
|
+
uri = uri.add(job:) unless job.nil?
|
584
|
+
checked(
|
585
|
+
Typhoeus::Request.post(
|
586
|
+
uri.to_s,
|
587
|
+
body: {
|
588
|
+
'_csrf' => csrf,
|
589
|
+
'name' => name,
|
590
|
+
'badge' => badge,
|
591
|
+
'why' => why,
|
592
|
+
'result' => r.to_s
|
593
|
+
},
|
594
|
+
headers:
|
595
|
+
),
|
596
|
+
302
|
597
|
+
)
|
598
|
+
r
|
599
|
+
end
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
# Get CSRF token from the server.
|
604
|
+
# @return [String] The token for this user
|
605
|
+
def csrf
|
606
|
+
token = nil
|
607
|
+
elapsed(@loog) do
|
608
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
609
|
+
token = checked(
|
610
|
+
Typhoeus::Request.get(
|
611
|
+
home.append('csrf').to_s,
|
612
|
+
headers:
|
613
|
+
),
|
614
|
+
200
|
615
|
+
).body
|
616
|
+
end
|
617
|
+
throw :"CSRF token retrieved (#{token.length} chars)"
|
618
|
+
end
|
619
|
+
token
|
620
|
+
end
|
621
|
+
|
534
622
|
private
|
535
623
|
|
536
624
|
def headers
|
@@ -556,7 +644,7 @@ class BazaRb
|
|
556
644
|
end
|
557
645
|
|
558
646
|
def gzip(data)
|
559
|
-
''.
|
647
|
+
(+'').tap do |result|
|
560
648
|
io = StringIO.new(result)
|
561
649
|
gz = Zlib::GzipWriter.new(io)
|
562
650
|
gz.write(data)
|
@@ -571,14 +659,19 @@ class BazaRb
|
|
571
659
|
.scheme(@ssl ? 'https' : 'http')
|
572
660
|
end
|
573
661
|
|
662
|
+
# Check the HTTP response and return it.
|
663
|
+
#
|
664
|
+
# @param [Typhoeus::Response] ret The response
|
665
|
+
# @param [Array<Integer>] allowed List of acceptable HTTP codes
|
666
|
+
# @return [Typhoeus::Response] The same response
|
574
667
|
def checked(ret, allowed = [200])
|
575
668
|
allowed = [allowed] unless allowed.is_a?(Array)
|
576
669
|
mtd = (ret.request.original_options[:method] || '???').upcase
|
577
670
|
url = ret.effective_url
|
578
671
|
if ret.return_code == :operation_timedout
|
579
672
|
msg = "#{mtd} #{url} timed out in #{ret.total_time}s"
|
580
|
-
@loog.
|
581
|
-
raise msg
|
673
|
+
@loog.error(msg)
|
674
|
+
raise TimedOut, msg
|
582
675
|
end
|
583
676
|
log = "#{mtd} #{url} -> #{ret.code} (#{format('%0.2f', ret.total_time)}s)"
|
584
677
|
if allowed.include?(ret.code)
|
@@ -608,6 +701,7 @@ class BazaRb
|
|
608
701
|
when 0
|
609
702
|
msg += ', most likely an internal error'
|
610
703
|
end
|
611
|
-
|
704
|
+
@loog.error(msg)
|
705
|
+
raise ServerFailure, msg
|
612
706
|
end
|
613
707
|
end
|
data/test/test__helper.rb
CHANGED
@@ -1,24 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright (c) 2024 Zerocracy
|
4
|
-
#
|
5
|
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
-
# of this software and associated documentation files (the 'Software'), to deal
|
7
|
-
# in the Software without restriction, including without limitation the rights
|
8
|
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
-
# copies of the Software, and to permit persons to whom the Software is
|
10
|
-
# furnished to do so, subject to the following conditions:
|
11
|
-
#
|
12
|
-
# The above copyright notice and this permission notice shall be included in all
|
13
|
-
# copies or substantial portions of the Software.
|
14
|
-
#
|
15
|
-
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
|
18
|
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
-
# SOFTWARE.
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
|
4
|
+
# SPDX-License-Identifier: MIT
|
22
5
|
|
23
6
|
$stdout.sync = true
|
24
7
|
|