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.
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
@@ -56,12 +61,57 @@ class BazaRb
56
61
  @compress = compress
57
62
  end
58
63
 
59
- # Push factbase to the server.
64
+ # Get GitHub login name of the logged in user.
60
65
  #
61
- # @param [String] name The name of the job on the server
62
- # @param [Bytes] data The data to push to the server (binary)
63
- # @param [Array<String>] meta List of metas, possibly empty
64
- # @return [Integer] Job ID on the server
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 [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
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
- # The job with this ID is finished already?
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 is already finished
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 most probably already locked"
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 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
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 some funds to another user.
519
+ # Transfer funds to another user.
465
520
  #
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
- # @param [Integer] job The ID of the job or NIL
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' => amount.to_s,
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 ##{amount} to @#{recipient} at #{@host}"
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 Who is acting (could be any text)
507
- # @param [String] zip The path to ZIP archive to take
508
- # @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
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
- request = Typhoeus::Request.new(
516
- home.append('pop').add(owner:).to_s,
517
- method: :get,
518
- headers: headers.merge(
519
- 'Accept' => 'application/octet-stream'
520
- ),
521
- connecttimeout: @timeout,
522
- timeout: @timeout
523
- )
524
- request.on_body do |chunk|
525
- f.write(chunk)
526
- end
527
- with_retries(max_tries: @retries, rescue: TimedOut) do
528
- 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
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 on the server
546
- # @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
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 badge of the valve
575
- # @param [String] why The reason
576
- # @param [nil|Integer] job The ID of the job
577
- # @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
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
- # @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
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
- ', most probably it\'s an internal error on the server, ' \
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
- ', 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 " \
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.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'