baza.rb 0.3.0 → 0.5.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/.github/workflows/copyrights.yml +1 -1
- data/.github/workflows/markdown-lint.yml +1 -1
- data/.github/workflows/typos.yml +19 -0
- data/.gitignore +3 -2
- data/.rubocop.yml +1 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +22 -23
- data/README.md +5 -5
- data/REUSE.toml +7 -7
- data/Rakefile +10 -6
- data/lib/baza-rb/fake.rb +72 -30
- data/lib/baza-rb/version.rb +2 -2
- data/lib/baza-rb.rb +200 -67
- data/test/test__helper.rb +20 -6
- data/test/test_baza-rb.rb +292 -21
- data/test/test_fake.rb +19 -1
- metadata +5 -4
data/lib/baza-rb.rb
CHANGED
@@ -16,11 +16,16 @@ require 'typhoeus'
|
|
16
16
|
require 'zlib'
|
17
17
|
require_relative 'baza-rb/version'
|
18
18
|
|
19
|
-
#
|
19
|
+
# Ruby client for the Zerocracy API.
|
20
20
|
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
21
|
+
# This class provides a complete interface to interact with the Zerocracy
|
22
|
+
# platform API. Create an instance with your authentication token and use
|
23
|
+
# its methods to manage jobs, transfer funds, handle durables, and more.
|
24
|
+
#
|
25
|
+
# @example Basic usage
|
26
|
+
# baza = BazaRb.new('api.zerocracy.com', 443, 'your-token-here')
|
27
|
+
# puts baza.whoami # => "your-github-username"
|
28
|
+
# puts baza.balance # => 100.5
|
24
29
|
#
|
25
30
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
26
31
|
# Copyright:: Copyright (c) 2024-2025 Yegor Bugayenko
|
@@ -35,16 +40,16 @@ class BazaRb
|
|
35
40
|
# Unexpected response arrived from the server.
|
36
41
|
class BadResponse < StandardError; end
|
37
42
|
|
38
|
-
#
|
43
|
+
# Initialize a new Zerocracy API client.
|
39
44
|
#
|
40
|
-
# @param [String] host
|
41
|
-
# @param [Integer] port TCP port
|
42
|
-
# @param [String] token
|
43
|
-
# @param [Boolean] ssl
|
44
|
-
# @param [Float] timeout
|
45
|
-
# @param [Integer] retries
|
46
|
-
# @param [Loog] loog The logging facility
|
47
|
-
# @param [Boolean] compress
|
45
|
+
# @param [String] host The API host name (e.g., 'api.zerocracy.com')
|
46
|
+
# @param [Integer] port The TCP port to connect to (usually 443 for HTTPS)
|
47
|
+
# @param [String] token Your Zerocracy API authentication token
|
48
|
+
# @param [Boolean] ssl Whether to use SSL/HTTPS (default: true)
|
49
|
+
# @param [Float] timeout Connection and request timeout in seconds (default: 30)
|
50
|
+
# @param [Integer] retries Number of retries on connection failure (default: 3)
|
51
|
+
# @param [Loog] loog The logging facility (default: Loog::NULL)
|
52
|
+
# @param [Boolean] compress Whether to use GZIP compression for requests/responses (default: true)
|
48
53
|
def initialize(host, port, token, ssl: true, timeout: 30, retries: 3, loog: Loog::NULL, compress: true)
|
49
54
|
@host = host
|
50
55
|
@port = port
|
@@ -56,12 +61,57 @@ class BazaRb
|
|
56
61
|
@compress = compress
|
57
62
|
end
|
58
63
|
|
59
|
-
#
|
64
|
+
# Get GitHub login name of the logged in user.
|
60
65
|
#
|
61
|
-
# @
|
62
|
-
# @
|
63
|
-
|
64
|
-
|
66
|
+
# @return [String] GitHub nickname of the authenticated user
|
67
|
+
# @raise [ServerFailure] If authentication fails or server returns an error
|
68
|
+
def whoami
|
69
|
+
nick = nil
|
70
|
+
elapsed(@loog) do
|
71
|
+
ret =
|
72
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
73
|
+
checked(
|
74
|
+
Typhoeus::Request.get(
|
75
|
+
home.append('whoami').to_s,
|
76
|
+
headers:
|
77
|
+
)
|
78
|
+
)
|
79
|
+
end
|
80
|
+
nick = ret.body
|
81
|
+
throw :"I know that I am @#{nick}, at #{@host}"
|
82
|
+
end
|
83
|
+
nick
|
84
|
+
end
|
85
|
+
|
86
|
+
# Get current balance of the authenticated user.
|
87
|
+
#
|
88
|
+
# @return [Float] The balance in zents (Ƶ), where 1 Ƶ = 1 USDT
|
89
|
+
# @raise [ServerFailure] If authentication fails or server returns an error
|
90
|
+
def balance
|
91
|
+
z = nil
|
92
|
+
elapsed(@loog) do
|
93
|
+
ret =
|
94
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
95
|
+
checked(
|
96
|
+
Typhoeus::Request.get(
|
97
|
+
home.append('account').append('balance').to_s,
|
98
|
+
headers:
|
99
|
+
)
|
100
|
+
)
|
101
|
+
end
|
102
|
+
z = ret.body.to_f
|
103
|
+
throw :"The balance is Ƶ#{z}, at #{@host}"
|
104
|
+
end
|
105
|
+
z
|
106
|
+
end
|
107
|
+
|
108
|
+
# Push factbase to the server to create a new job.
|
109
|
+
#
|
110
|
+
# @param [String] name The unique name of the job on the server
|
111
|
+
# @param [String] data The binary data to push to the server (factbase content)
|
112
|
+
# @param [Array<String>] meta List of metadata strings to attach to the job
|
113
|
+
# @return [Integer] Job ID assigned by the server
|
114
|
+
# @raise [ServerFailure] If the push operation fails
|
65
115
|
def push(name, data, meta)
|
66
116
|
raise 'The "name" of the job is nil' if name.nil?
|
67
117
|
raise 'The "name" of the job may not be empty' if name.empty?
|
@@ -97,10 +147,11 @@ class BazaRb
|
|
97
147
|
id
|
98
148
|
end
|
99
149
|
|
100
|
-
# Pull factbase from the server.
|
150
|
+
# Pull factbase from the server for a specific job.
|
101
151
|
#
|
102
152
|
# @param [Integer] id The ID of the job on the server
|
103
|
-
# @return [
|
153
|
+
# @return [String] Binary data of the factbase (can be saved to file)
|
154
|
+
# @raise [ServerFailure] If the job doesn't exist or pull fails
|
104
155
|
def pull(id)
|
105
156
|
raise 'The ID of the job is nil' if id.nil?
|
106
157
|
raise 'The ID of the job must be a positive integer' unless id.positive?
|
@@ -133,10 +184,11 @@ class BazaRb
|
|
133
184
|
data
|
134
185
|
end
|
135
186
|
|
136
|
-
#
|
187
|
+
# Check if the job with this ID is finished already.
|
137
188
|
#
|
138
189
|
# @param [Integer] id The ID of the job on the server
|
139
|
-
# @return [Boolean] TRUE if the job
|
190
|
+
# @return [Boolean] TRUE if the job has completed execution, FALSE otherwise
|
191
|
+
# @raise [ServerFailure] If the job doesn't exist
|
140
192
|
def finished?(id)
|
141
193
|
raise 'The ID of the job is nil' if id.nil?
|
142
194
|
raise 'The ID of the job must be a positive integer' unless id.positive?
|
@@ -249,7 +301,7 @@ class BazaRb
|
|
249
301
|
)
|
250
302
|
end
|
251
303
|
throw :"Job name '#{name}' locked at #{@host}" if ret.code == 302
|
252
|
-
raise "Failed to lock '#{name}' job at #{@host}, it's
|
304
|
+
raise "Failed to lock '#{name}' job at #{@host}, it's already locked"
|
253
305
|
end
|
254
306
|
end
|
255
307
|
|
@@ -261,6 +313,7 @@ class BazaRb
|
|
261
313
|
raise 'The "name" of the job is nil' if name.nil?
|
262
314
|
raise 'The "name" of the job may not be empty' if name.empty?
|
263
315
|
raise 'The "owner" of the lock is nil' if owner.nil?
|
316
|
+
raise 'The "owner" of the lock may not be empty' if owner.empty?
|
264
317
|
elapsed(@loog) do
|
265
318
|
with_retries(max_tries: @retries, rescue: TimedOut) do
|
266
319
|
checked(
|
@@ -323,10 +376,12 @@ class BazaRb
|
|
323
376
|
exists
|
324
377
|
end
|
325
378
|
|
326
|
-
# Place a single durable.
|
379
|
+
# Place a single durable file on the server.
|
327
380
|
#
|
328
381
|
# @param [String] jname The name of the job on the server
|
329
|
-
# @param [String] file The file
|
382
|
+
# @param [String] file The path to the file to upload
|
383
|
+
# @return [Integer] The ID of the created durable
|
384
|
+
# @raise [ServerFailure] If the upload fails
|
330
385
|
def durable_place(jname, file)
|
331
386
|
raise 'The "jname" of the durable is nil' if jname.nil?
|
332
387
|
raise 'The "jname" of the durable may not be empty' if jname.empty?
|
@@ -461,13 +516,14 @@ class BazaRb
|
|
461
516
|
end
|
462
517
|
end
|
463
518
|
|
464
|
-
# Transfer
|
519
|
+
# Transfer funds to another user.
|
465
520
|
#
|
466
|
-
# @param [String] recipient GitHub
|
467
|
-
# @param [Float] amount The amount in
|
468
|
-
# @param [String] summary The description
|
469
|
-
# @param [Integer] job
|
470
|
-
# @return [Integer] Receipt ID
|
521
|
+
# @param [String] recipient GitHub username of the recipient (e.g. "yegor256")
|
522
|
+
# @param [Float] amount The amount to transfer in Ƶ (zents)
|
523
|
+
# @param [String] summary The description/reason for the payment
|
524
|
+
# @param [Integer] job Optional job ID to associate with this transfer
|
525
|
+
# @return [Integer] Receipt ID for the transaction
|
526
|
+
# @raise [ServerFailure] If the transfer fails
|
471
527
|
def transfer(recipient, amount, summary, job: nil)
|
472
528
|
raise 'The "recipient" is nil' if recipient.nil?
|
473
529
|
raise 'The "amount" is nil' if amount.nil?
|
@@ -477,7 +533,7 @@ class BazaRb
|
|
477
533
|
body = {
|
478
534
|
'_csrf' => csrf,
|
479
535
|
'human' => recipient,
|
480
|
-
'amount' =>
|
536
|
+
'amount' => format('%0.6f', amount),
|
481
537
|
'summary' => summary
|
482
538
|
}
|
483
539
|
body['job'] = job unless job.nil?
|
@@ -496,40 +552,105 @@ class BazaRb
|
|
496
552
|
)
|
497
553
|
end
|
498
554
|
id = ret.headers['X-Zerocracy-ReceiptId'].to_i
|
499
|
-
throw :"Transferred
|
555
|
+
throw :"Transferred Ƶ#{format('%0.6f', amount)} to @#{recipient} at #{@host}"
|
556
|
+
end
|
557
|
+
id
|
558
|
+
end
|
559
|
+
|
560
|
+
# Pay a fee associated with a job.
|
561
|
+
#
|
562
|
+
# @param [String] tab The category/type of the fee (use "unknown" if not sure)
|
563
|
+
# @param [Float] amount The fee amount in Ƶ (zents)
|
564
|
+
# @param [String] summary The description/reason for the fee
|
565
|
+
# @param [Integer] job The ID of the job this fee is for
|
566
|
+
# @return [Integer] Receipt ID for the fee payment
|
567
|
+
# @raise [ServerFailure] If the payment fails
|
568
|
+
def fee(tab, amount, summary, job)
|
569
|
+
raise 'The "tab" is nil' if tab.nil?
|
570
|
+
raise 'The "amount" is nil' if amount.nil?
|
571
|
+
raise 'The "amount" must be Float' unless amount.is_a?(Float)
|
572
|
+
raise 'The "job" is nil' if job.nil?
|
573
|
+
raise 'The "job" must be Integer' unless job.is_a?(Integer)
|
574
|
+
raise 'The "summary" is nil' if summary.nil?
|
575
|
+
id = nil
|
576
|
+
body = {
|
577
|
+
'_csrf' => csrf,
|
578
|
+
'tab' => tab,
|
579
|
+
'amount' => format('%0.6f', amount),
|
580
|
+
'summary' => summary,
|
581
|
+
'job' => job.to_s
|
582
|
+
}
|
583
|
+
elapsed(@loog) do
|
584
|
+
ret =
|
585
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
586
|
+
checked(
|
587
|
+
Typhoeus::Request.post(
|
588
|
+
home.append('account').append('fee').to_s,
|
589
|
+
body:,
|
590
|
+
headers:,
|
591
|
+
connecttimeout: @timeout,
|
592
|
+
timeout: @timeout
|
593
|
+
),
|
594
|
+
302
|
595
|
+
)
|
596
|
+
end
|
597
|
+
id = ret.headers['X-Zerocracy-ReceiptId'].to_i
|
598
|
+
throw :"Fee Ƶ#{format('%0.6f', amount)} paid at #{@host}"
|
500
599
|
end
|
501
600
|
id
|
502
601
|
end
|
503
602
|
|
504
|
-
# Pop job from the server.
|
603
|
+
# Pop the next available job from the server's queue.
|
505
604
|
#
|
506
|
-
# @param [String] owner
|
507
|
-
# @param [String] zip The path
|
508
|
-
# @return [Boolean] TRUE if job
|
605
|
+
# @param [String] owner Identifier of who is taking the job (any descriptive text)
|
606
|
+
# @param [String] zip The local file path where the job's ZIP archive will be saved
|
607
|
+
# @return [Boolean] TRUE if a job was successfully popped, FALSE if queue is empty
|
608
|
+
# @raise [ServerFailure] If the pop operation fails
|
509
609
|
def pop(owner, zip)
|
510
610
|
raise 'The "zip" of the job is nil' if zip.nil?
|
511
611
|
success = false
|
512
612
|
FileUtils.rm_f(zip)
|
613
|
+
job = nil
|
513
614
|
elapsed(@loog) do
|
514
|
-
File.open(zip, 'wb') do |f|
|
515
|
-
|
516
|
-
home.append('pop').add(owner:)
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
request.
|
615
|
+
File.open(zip, 'wb+') do |f|
|
616
|
+
loop do
|
617
|
+
uri = home.append('pop').add(owner:)
|
618
|
+
uri = uri.add(job:) if job
|
619
|
+
request = Typhoeus::Request.new(
|
620
|
+
uri.to_s,
|
621
|
+
method: :get,
|
622
|
+
headers: headers.merge(
|
623
|
+
'Accept' => 'application/octet-stream',
|
624
|
+
'Range' => "bytes=#{f.size}-"
|
625
|
+
),
|
626
|
+
connecttimeout: @timeout,
|
627
|
+
timeout: @timeout
|
628
|
+
)
|
629
|
+
request.on_body do |chunk|
|
630
|
+
f.write(chunk)
|
631
|
+
end
|
632
|
+
with_retries(max_tries: @retries, rescue: TimedOut) do
|
633
|
+
request.run
|
634
|
+
end
|
635
|
+
ret = request.response
|
636
|
+
checked(ret, [200, 204, 206])
|
637
|
+
success = ret.code != 204
|
638
|
+
break unless ret.code == 206
|
639
|
+
job = ret.headers['X-Zerocracy-JobId']
|
640
|
+
raise 'Job ID is not returned in X-Zerocracy-JobId' if job.nil?
|
641
|
+
raise "Job ID returned in X-Zerocracy-JobId is not valid (#{job.inspect})" unless job.match?(/^[0-9]+$/)
|
642
|
+
_, v = ret.headers['Content-Range'].split
|
643
|
+
range, total = v.split('/')
|
644
|
+
raise "Total size is not valid (#{total.inspect})" unless total.match?(/^\*|[0-9]+$/)
|
645
|
+
b, e = range.split('-')
|
646
|
+
raise "Range is not valid (#{range.inspect})" unless e.match?(/^[0-9]+$/)
|
647
|
+
len = ret.headers['Content-Length'].to_i
|
648
|
+
unless len.zero?
|
649
|
+
raise "Range size (#{range.inspect}) is not equal to Content-Length" unless len - 1 == e.to_i - b.to_i
|
650
|
+
raise "Range end (#{range.inspect}) is not equal to #{f.size}" if e.to_i != f.size - 1
|
651
|
+
end
|
652
|
+
break if e.to_i == total.to_i - 1
|
529
653
|
end
|
530
|
-
ret = request.response
|
531
|
-
checked(ret, [200, 204])
|
532
|
-
success = ret.code == 200
|
533
654
|
end
|
534
655
|
unless success
|
535
656
|
FileUtils.rm_f(zip)
|
@@ -540,10 +661,11 @@ class BazaRb
|
|
540
661
|
success
|
541
662
|
end
|
542
663
|
|
543
|
-
# Submit a ZIP archive to finish a job.
|
664
|
+
# Submit a ZIP archive to finish a previously popped job.
|
544
665
|
#
|
545
|
-
# @param [Integer] id The ID of the job
|
546
|
-
# @param [String] zip The path to the ZIP file
|
666
|
+
# @param [Integer] id The ID of the job to finish
|
667
|
+
# @param [String] zip The path to the ZIP file containing job results
|
668
|
+
# @raise [ServerFailure] If the submission fails
|
547
669
|
def finish(id, zip)
|
548
670
|
raise 'The ID of the job is nil' if id.nil?
|
549
671
|
raise 'The ID of the job must be a positive integer' unless id.positive?
|
@@ -568,13 +690,19 @@ class BazaRb
|
|
568
690
|
end
|
569
691
|
end
|
570
692
|
|
571
|
-
# Enter a valve.
|
693
|
+
# Enter a valve to cache or retrieve a computation result.
|
694
|
+
#
|
695
|
+
# Valves prevent duplicate computations by caching results. If a result
|
696
|
+
# for the given badge already exists, it's returned. Otherwise, the block
|
697
|
+
# is executed and its result is cached.
|
572
698
|
#
|
573
699
|
# @param [String] name Name of the job
|
574
|
-
# @param [String] badge Unique
|
575
|
-
# @param [String] why The reason
|
576
|
-
# @param [nil|Integer] job
|
577
|
-
# @
|
700
|
+
# @param [String] badge Unique identifier for this valve/computation
|
701
|
+
# @param [String] why The reason/description for entering this valve
|
702
|
+
# @param [nil|Integer] job Optional job ID to associate with this valve
|
703
|
+
# @yield Block that computes the result if not cached
|
704
|
+
# @return [String] The cached result or newly computed result from the block
|
705
|
+
# @raise [ServerFailure] If the valve operation fails
|
578
706
|
def enter(name, badge, why, job)
|
579
707
|
elapsed(@loog, intro: "Entered valve #{badge} to #{name}") do
|
580
708
|
with_retries(max_tries: @retries, rescue: TimedOut) do
|
@@ -608,8 +736,13 @@ class BazaRb
|
|
608
736
|
end
|
609
737
|
end
|
610
738
|
|
611
|
-
# Get CSRF token from the server.
|
612
|
-
#
|
739
|
+
# Get CSRF token from the server for authenticated requests.
|
740
|
+
#
|
741
|
+
# The CSRF token is required for POST requests to prevent cross-site
|
742
|
+
# request forgery attacks.
|
743
|
+
#
|
744
|
+
# @return [String] The CSRF token for the authenticated user
|
745
|
+
# @raise [ServerFailure] If token retrieval fails
|
613
746
|
def csrf
|
614
747
|
token = nil
|
615
748
|
elapsed(@loog) do
|
@@ -696,7 +829,7 @@ class BazaRb
|
|
696
829
|
case ret.code
|
697
830
|
when 500
|
698
831
|
msg +=
|
699
|
-
|
832
|
+
", most probably it's an internal error on the server, " \
|
700
833
|
'please report this to https://github.com/zerocracy/baza'
|
701
834
|
when 503
|
702
835
|
msg +=
|
@@ -704,7 +837,7 @@ class BazaRb
|
|
704
837
|
'please report this to https://github.com/zerocracy/baza.rb'
|
705
838
|
when 404
|
706
839
|
msg +=
|
707
|
-
|
840
|
+
", most probably you are trying to reach a wrong server, which doesn't " \
|
708
841
|
'have the URL that it is expected to have'
|
709
842
|
when 0
|
710
843
|
msg += ', most likely an internal error'
|
data/test/test__helper.rb
CHANGED
@@ -5,14 +5,28 @@
|
|
5
5
|
|
6
6
|
$stdout.sync = true
|
7
7
|
|
8
|
-
require 'minitest/reporters'
|
9
|
-
Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
|
10
|
-
|
11
8
|
require 'simplecov'
|
12
|
-
SimpleCov.start
|
13
|
-
|
14
9
|
require 'simplecov-cobertura'
|
15
|
-
SimpleCov.
|
10
|
+
unless SimpleCov.running || ARGV.include?('--no-cov')
|
11
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
|
12
|
+
[
|
13
|
+
SimpleCov::Formatter::HTMLFormatter,
|
14
|
+
SimpleCov::Formatter::CoberturaFormatter
|
15
|
+
]
|
16
|
+
)
|
17
|
+
SimpleCov.minimum_coverage 95
|
18
|
+
SimpleCov.minimum_coverage_by_file 95
|
19
|
+
SimpleCov.start do
|
20
|
+
add_filter 'vendor/'
|
21
|
+
add_filter 'target/'
|
22
|
+
track_files 'judges/**/*.rb'
|
23
|
+
track_files 'lib/**/*.rb'
|
24
|
+
track_files '*.rb'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
require 'minitest/reporters'
|
29
|
+
Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
|
16
30
|
|
17
31
|
require 'webmock/minitest'
|
18
32
|
require 'minitest/autorun'
|