baza.rb 0.4.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.
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
- # Interface to the API of zerocracy.com.
19
+ # Ruby client for the Zerocracy API.
20
20
  #
21
- # You make an instance of this class and then call one of its methods.
22
- # The object will make HTTP request to api.zerocracy.com and interpret the
23
- # results returned.
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
- # Ctor.
43
+ # Initialize a new Zerocracy API client.
39
44
  #
40
- # @param [String] host Host name
41
- # @param [Integer] port TCP port
42
- # @param [String] token Secret token of zerocracy.com
43
- # @param [Boolean] ssl Should we use SSL?
44
- # @param [Float] timeout Connect timeout and session timeout, in seconds
45
- # @param [Integer] retries How many times to retry on connection failure?
46
- # @param [Loog] loog The logging facility
47
- # @param [Boolean] compress Set to TRUE if need to use GZIP while pulling and sending
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
@@ -58,7 +63,8 @@ class BazaRb
58
63
 
59
64
  # Get GitHub login name of the logged in user.
60
65
  #
61
- # @return [String] GitHub nickname
66
+ # @return [String] GitHub nickname of the authenticated user
67
+ # @raise [ServerFailure] If authentication fails or server returns an error
62
68
  def whoami
63
69
  nick = nil
64
70
  elapsed(@loog) do
@@ -77,9 +83,10 @@ class BazaRb
77
83
  nick
78
84
  end
79
85
 
80
- # Get current balance, in Ƶ.
86
+ # Get current balance of the authenticated user.
81
87
  #
82
- # @return [Float] The balance, as float
88
+ # @return [Float] The balance in zents (Ƶ), where 1 Ƶ = 1 USDT
89
+ # @raise [ServerFailure] If authentication fails or server returns an error
83
90
  def balance
84
91
  z = nil
85
92
  elapsed(@loog) do
@@ -98,12 +105,13 @@ class BazaRb
98
105
  z
99
106
  end
100
107
 
101
- # Push factbase to the server.
108
+ # Push factbase to the server to create a new job.
102
109
  #
103
- # @param [String] name The name of the job on the server
104
- # @param [Bytes] data The data to push to the server (binary)
105
- # @param [Array<String>] meta List of metas, possibly empty
106
- # @return [Integer] Job ID on the server
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
107
115
  def push(name, data, meta)
108
116
  raise 'The "name" of the job is nil' if name.nil?
109
117
  raise 'The "name" of the job may not be empty' if name.empty?
@@ -139,10 +147,11 @@ class BazaRb
139
147
  id
140
148
  end
141
149
 
142
- # Pull factbase from the server.
150
+ # Pull factbase from the server for a specific job.
143
151
  #
144
152
  # @param [Integer] id The ID of the job on the server
145
- # @return [Bytes] Binary data pulled
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
146
155
  def pull(id)
147
156
  raise 'The ID of the job is nil' if id.nil?
148
157
  raise 'The ID of the job must be a positive integer' unless id.positive?
@@ -175,10 +184,11 @@ class BazaRb
175
184
  data
176
185
  end
177
186
 
178
- # The job with this ID is finished already?
187
+ # Check if the job with this ID is finished already.
179
188
  #
180
189
  # @param [Integer] id The ID of the job on the server
181
- # @return [Boolean] TRUE if the job is already finished
190
+ # @return [Boolean] TRUE if the job has completed execution, FALSE otherwise
191
+ # @raise [ServerFailure] If the job doesn't exist
182
192
  def finished?(id)
183
193
  raise 'The ID of the job is nil' if id.nil?
184
194
  raise 'The ID of the job must be a positive integer' unless id.positive?
@@ -291,7 +301,7 @@ class BazaRb
291
301
  )
292
302
  end
293
303
  throw :"Job name '#{name}' locked at #{@host}" if ret.code == 302
294
- raise "Failed to lock '#{name}' job at #{@host}, it's most probably already locked"
304
+ raise "Failed to lock '#{name}' job at #{@host}, it's already locked"
295
305
  end
296
306
  end
297
307
 
@@ -303,6 +313,7 @@ class BazaRb
303
313
  raise 'The "name" of the job is nil' if name.nil?
304
314
  raise 'The "name" of the job may not be empty' if name.empty?
305
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?
306
317
  elapsed(@loog) do
307
318
  with_retries(max_tries: @retries, rescue: TimedOut) do
308
319
  checked(
@@ -365,10 +376,12 @@ class BazaRb
365
376
  exists
366
377
  end
367
378
 
368
- # Place a single durable.
379
+ # Place a single durable file on the server.
369
380
  #
370
381
  # @param [String] jname The name of the job on the server
371
- # @param [String] file The file name
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
372
385
  def durable_place(jname, file)
373
386
  raise 'The "jname" of the durable is nil' if jname.nil?
374
387
  raise 'The "jname" of the durable may not be empty' if jname.empty?
@@ -503,13 +516,14 @@ class BazaRb
503
516
  end
504
517
  end
505
518
 
506
- # Transfer some funds to another user.
519
+ # Transfer funds to another user.
507
520
  #
508
- # @param [String] recipient GitHub name (e.g. "yegor256") of the recipient
509
- # @param [Float] amount The amount in Z/USDT (not zents!)
510
- # @param [String] summary The description of the payment
511
- # @param [Integer] job The ID of the job or NIL
512
- # @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
513
527
  def transfer(recipient, amount, summary, job: nil)
514
528
  raise 'The "recipient" is nil' if recipient.nil?
515
529
  raise 'The "amount" is nil' if amount.nil?
@@ -538,18 +552,19 @@ class BazaRb
538
552
  )
539
553
  end
540
554
  id = ret.headers['X-Zerocracy-ReceiptId'].to_i
541
- throw :"Transferred ##{amount} to @#{recipient} at #{@host}"
555
+ throw :"Transferred Ƶ#{format('%0.6f', amount)} to @#{recipient} at #{@host}"
542
556
  end
543
557
  id
544
558
  end
545
559
 
546
- # Pay fee, while working with a job.
560
+ # Pay a fee associated with a job.
547
561
  #
548
- # @param [String] tab The tab of the fee (use "unknown" if not sure)
549
- # @param [Float] amount The amount in Z/USDT (not zents!)
550
- # @param [String] summary The description of the payment
551
- # @param [Integer] job The ID of the job
552
- # @return [Integer] Receipt ID
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
553
568
  def fee(tab, amount, summary, job)
554
569
  raise 'The "tab" is nil' if tab.nil?
555
570
  raise 'The "amount" is nil' if amount.nil?
@@ -580,40 +595,62 @@ class BazaRb
580
595
  )
581
596
  end
582
597
  id = ret.headers['X-Zerocracy-ReceiptId'].to_i
583
- throw :"Fee ##{format('%.02f', amount)} paid at #{@host}"
598
+ throw :"Fee Ƶ#{format('%0.6f', amount)} paid at #{@host}"
584
599
  end
585
600
  id
586
601
  end
587
602
 
588
- # Pop job from the server.
603
+ # Pop the next available job from the server's queue.
589
604
  #
590
- # @param [String] owner Who is acting (could be any text)
591
- # @param [String] zip The path to ZIP archive to take
592
- # @return [Boolean] TRUE if job taken, otherwise false
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
593
609
  def pop(owner, zip)
594
610
  raise 'The "zip" of the job is nil' if zip.nil?
595
611
  success = false
596
612
  FileUtils.rm_f(zip)
613
+ job = nil
597
614
  elapsed(@loog) do
598
- File.open(zip, 'wb') do |f|
599
- request = Typhoeus::Request.new(
600
- home.append('pop').add(owner:).to_s,
601
- method: :get,
602
- headers: headers.merge(
603
- 'Accept' => 'application/octet-stream'
604
- ),
605
- connecttimeout: @timeout,
606
- timeout: @timeout
607
- )
608
- request.on_body do |chunk|
609
- f.write(chunk)
610
- end
611
- with_retries(max_tries: @retries, rescue: TimedOut) do
612
- request.run
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
613
653
  end
614
- ret = request.response
615
- checked(ret, [200, 204])
616
- success = ret.code == 200
617
654
  end
618
655
  unless success
619
656
  FileUtils.rm_f(zip)
@@ -624,10 +661,11 @@ class BazaRb
624
661
  success
625
662
  end
626
663
 
627
- # Submit a ZIP archive to finish a job.
664
+ # Submit a ZIP archive to finish a previously popped job.
628
665
  #
629
- # @param [Integer] id The ID of the job on the server
630
- # @param [String] zip The path to the ZIP file with the content of the archive
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
631
669
  def finish(id, zip)
632
670
  raise 'The ID of the job is nil' if id.nil?
633
671
  raise 'The ID of the job must be a positive integer' unless id.positive?
@@ -652,13 +690,19 @@ class BazaRb
652
690
  end
653
691
  end
654
692
 
655
- # 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.
656
698
  #
657
699
  # @param [String] name Name of the job
658
- # @param [String] badge Unique badge of the valve
659
- # @param [String] why The reason
660
- # @param [nil|Integer] job The ID of the job
661
- # @return [String] The result just calculated or retrieved
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
662
706
  def enter(name, badge, why, job)
663
707
  elapsed(@loog, intro: "Entered valve #{badge} to #{name}") do
664
708
  with_retries(max_tries: @retries, rescue: TimedOut) do
@@ -692,8 +736,13 @@ class BazaRb
692
736
  end
693
737
  end
694
738
 
695
- # Get CSRF token from the server.
696
- # @return [String] The token for this user
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
697
746
  def csrf
698
747
  token = nil
699
748
  elapsed(@loog) do
@@ -780,7 +829,7 @@ class BazaRb
780
829
  case ret.code
781
830
  when 500
782
831
  msg +=
783
- ', most probably it\'s an internal error on the server, ' \
832
+ ", most probably it's an internal error on the server, " \
784
833
  'please report this to https://github.com/zerocracy/baza'
785
834
  when 503
786
835
  msg +=
@@ -788,7 +837,7 @@ class BazaRb
788
837
  'please report this to https://github.com/zerocracy/baza.rb'
789
838
  when 404
790
839
  msg +=
791
- ', most probably you are trying to reach a wrong server, which doesn\'t ' \
840
+ ", most probably you are trying to reach a wrong server, which doesn't " \
792
841
  'have the URL that it is expected to have'
793
842
  when 0
794
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.formatter = SimpleCov::Formatter::CoberturaFormatter
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'