Class: BazaRb

Inherits:
Object
  • Object
show all
Defined in:
lib/baza-rb.rb,
lib/baza-rb/version.rb

Overview

Just a version.

We keep this file separate from the “baza-rb.rb” in order to have an ability to include it from the “.gemspec” script, without including all other packages (thus failing the build).

Author

Yegor Bugayenko ([email protected])

Copyright

Copyright © 2024-2026 Yegor Bugayenko

License

MIT

Defined Under Namespace

Classes: BadCompression, BadResponse, Fake, ServerFailure, TimedOut

Constant Summary collapse

DEFAULT_CHUNK_SIZE =

How big are the chunks we send, by default, in bytes. Numbers larger than 1Mb may lead to problems with the server, since sending time will be too long and the server may drop connections. Better keep it as is: 1Mb.

1_000_000
VERSION =
'0.0.0'

Instance Method Summary collapse

Constructor Details

#initialize(host, port, token, ssl: true, timeout: 30, retries: 5, pause: 1, loog: Loog::NULL, compress: true) ⇒ BazaRb

Initialize a new Zerocracy API client.

Parameters:

  • host (String)

    The API host name (e.g., ‘api.zerocracy.com’)

  • port (Integer)

    The TCP port to connect to (usually 443 for HTTPS)

  • token (String)

    Your Zerocracy API authentication token

  • ssl (Boolean) (defaults to: true)

    Whether to use SSL/HTTPS (default: true)

  • timeout (Float) (defaults to: 30)

    Connection and request timeout in seconds (default: 30)

  • retries (Integer) (defaults to: 5)

    Number of retries on connection failure (default: 3)

  • pause (Integer) (defaults to: 1)

    The factor on pause (<1 means faster, >1 means slower)

  • loog (Loog) (defaults to: Loog::NULL)

    The logging facility (default: Loog::NULL)

  • compress (Boolean) (defaults to: true)

    Whether to use GZIP compression for requests/responses (default: true)



64
65
66
67
68
69
70
71
72
73
74
# File 'lib/baza-rb.rb', line 64

def initialize(host, port, token, ssl: true, timeout: 30, retries: 5, pause: 1, loog: Loog::NULL, compress: true)
  @host = host
  @port = port
  @ssl = ssl
  @token = token
  @timeout = timeout
  @loog = loog
  @retries = retries
  @pause = pause
  @compress = compress
end

Instance Method Details

#balanceFloat

Get current balance of the authenticated user.

Returns:

  • (Float)

    The balance in zents (Ƶ), where 1 Ƶ = 1 USDT

Raises:

  • (ServerFailure)

    If authentication fails or server returns an error



94
95
96
97
98
99
100
101
102
# File 'lib/baza-rb.rb', line 94

def balance
  z = nil
  elapsed(@loog, level: Logger::INFO) do
    ret = get(home.append('account').append('balance'))
    z = ret.body.to_f
    throw :"The balance is Ƶ#{z}, at #{@host}"
  end
  z
end

#csrfString

Get CSRF token from the server for authenticated requests.

The CSRF token is required for POST requests to prevent cross-site request forgery attacks.

Returns:

  • (String)

    The CSRF token for the authenticated user

Raises:



572
573
574
575
576
577
578
579
# File 'lib/baza-rb.rb', line 572

def csrf
  token = nil
  elapsed(@loog, level: Logger::INFO) do
    token = get(home.append('csrf')).body
    throw :"CSRF token retrieved (#{token.length} chars)"
  end
  token
end

#durable_find(pname, file) ⇒ Integer?

Find a durable by job name and file name.

Parameters:

  • pname (String)

    The name of the job

  • file (String)

    The file name

Returns:

  • (Integer, nil)

    The ID of the durable if found, nil if not found



407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/baza-rb.rb', line 407

def durable_find(pname, file)
  raise 'The "pname" is nil' if pname.nil?
  raise 'The "pname" may not be empty' if pname.empty?
  raise 'The "file" is nil' if file.nil?
  raise 'The "file" may not be empty' if file.empty?
  id = nil
  elapsed(@loog, level: Logger::INFO) do
    ret = get(home.append('durable-find').add(jname: pname, pname:, file:), [200, 404])
    if ret.code == 200
      id = ret.body.to_i
      throw :"Found durable ##{id} for job \"#{pname}\" file \"#{file}\" at #{@host}"
    else
      throw :"Durable not found for job \"#{pname}\" file \"#{file}\" at #{@host}"
    end
  end
  id
end

#durable_load(id, file) ⇒ Object

Load a single durable from server to local file.

Parameters:

  • id (Integer)

    The ID of the durable

  • file (String)

    The local file path to save the downloaded durable

Raises:



351
352
353
354
355
356
357
358
359
360
# File 'lib/baza-rb.rb', line 351

def durable_load(id, file)
  raise 'The ID of the durable is nil' if id.nil?
  raise 'The ID of the durable must be an Integer' unless id.is_a?(Integer)
  raise 'The ID of the durable must be a positive integer' unless id.positive?
  raise 'The "file" of the durable is nil' if file.nil?
  elapsed(@loog, level: Logger::INFO) do
    download(home.append('durables').append(id), file)
    throw :"Durable ##{id} loaded #{File.size(file)} bytes from #{@host}"
  end
end

#durable_lock(id, owner) ⇒ Object

Lock a single durable.

Parameters:

  • id (Integer)

    The ID of the durable

  • owner (String)

    The owner of the lock

Raises:



367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/baza-rb.rb', line 367

def durable_lock(id, owner)
  raise 'The ID of the durable is nil' if id.nil?
  raise 'The ID of the durable must be an Integer' unless id.is_a?(Integer)
  raise 'The ID of the durable must be a positive integer' unless id.positive?
  raise 'The "owner" of the lock is nil' if owner.nil?
  raise 'The "owner" of the lock may not be empty' if owner.empty?
  elapsed(@loog, level: Logger::INFO) do
    post(
      home.append('durables').append(id).append('lock'),
      { 'owner' => owner }
    )
    throw :"Durable ##{id} locked at #{@host}"
  end
end

#durable_place(pname, file) ⇒ Integer

Place a single durable file on the server.

The file provided will only be uploaded to the server if the durable is currently absent. If the durable is present, the file will be ignored. It is expected to use only small placeholder files, not real data.

Parameters:

  • pname (String)

    The name of the product on the server

  • file (String)

    The path to the file to upload

Returns:

  • (Integer)

    The ID of the created durable

Raises:



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/baza-rb.rb', line 303

def durable_place(pname, file)
  raise 'The "pname" of the durable is nil' if pname.nil?
  raise 'The "pname" of the durable may not be empty' if pname.empty?
  raise 'The "file" of the durable is nil' if file.nil?
  raise "The file '#{file}' is absent" unless File.exist?(file)
  if File.size(file) > 1024
    raise "The file '#{file}' is too big (#{File.size(file)} bytes) for durable_place(), use durable_save() instead"
  end
  id = nil
  elapsed(@loog, level: Logger::INFO) do
    ret = post(
      home.append('durable-place'),
      {
        'pname' => pname,
        'jname' => pname,
        'file' => File.basename(file),
        'zip' => File.open(file, 'rb')
      }
    )
    id = ret.headers['X-Zerocracy-DurableId'].to_i
    throw :"Durable ##{id} (#{file}, #{File.size(file)} bytes) placed for job \"#{pname}\" at #{@host}"
  end
  id
end

#durable_save(id, file, chunk_size: DEFAULT_CHUNK_SIZE) ⇒ Object

Save a single durable from local file to server.

Parameters:

  • id (Integer)

    The ID of the durable

  • file (String)

    The file to upload

  • chunk_size (Integer) (defaults to: DEFAULT_CHUNK_SIZE)

    Maximum size of one chunk

Raises:



334
335
336
337
338
339
340
341
342
343
344
# File 'lib/baza-rb.rb', line 334

def durable_save(id, file, chunk_size: DEFAULT_CHUNK_SIZE)
  raise 'The ID of the durable is nil' if id.nil?
  raise 'The ID of the durable must be an Integer' unless id.is_a?(Integer)
  raise 'The ID of the durable must be a positive integer' unless id.positive?
  raise 'The "file" of the durable is nil' if file.nil?
  raise "The file '#{file}' is absent" unless File.exist?(file)
  elapsed(@loog, level: Logger::INFO) do
    upload(home.append('durables').append(id), file, chunk_size:)
    throw :"Durable ##{id} saved #{File.size(file)} bytes to #{@host}"
  end
end

#durable_unlock(id, owner) ⇒ Object

Unlock a single durable.

Parameters:

  • id (Integer)

    The ID of the durable

  • owner (String)

    The owner of the lock

Raises:



387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/baza-rb.rb', line 387

def durable_unlock(id, owner)
  raise 'The ID of the durable is nil' if id.nil?
  raise 'The ID of the durable must be an Integer' unless id.is_a?(Integer)
  raise 'The ID of the durable must be a positive integer' unless id.positive?
  raise 'The "owner" of the lock is nil' if owner.nil?
  raise 'The "owner" of the lock may not be empty' if owner.empty?
  elapsed(@loog, level: Logger::INFO) do
    post(
      home.append('durables').append(id).append('unlock'),
      { 'owner' => owner }
    )
    throw :"Durable ##{id} unlocked at #{@host}"
  end
end

#enter(pname, badge, why, job) { ... } ⇒ String

Enter a valve to cache or retrieve a computation result.

Valves prevent duplicate computations by caching results. If a result for the given badge already exists, it’s returned. Otherwise, the block is executed and its result is cached.

Parameters:

  • pname (String)

    Name of the product

  • badge (String)

    Unique identifier for this valve/computation

  • why (String)

    The reason/description for entering this valve

  • job (nil|Integer)

    Optional job ID to associate with this valve

Yields:

  • Block that computes the result if not cached

Returns:

  • (String)

    The cached result or newly computed result from the block

Raises:



542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
# File 'lib/baza-rb.rb', line 542

def enter(pname, badge, why, job)
  elapsed(@loog, good: "Entered valve #{badge} to #{pname}") do
    retry_it do
      ret = get(home.append('result').add(badge:), [200, 204])
      return ret.body if ret.code == 200
      r = yield
      uri = home.append('valves')
      uri = uri.add(job:) unless job.nil?
      post(
        uri,
        {
          'name' => pname,
          'pname' => pname,
          'badge' => badge,
          'why' => why,
          'result' => r.to_s
        }
      )
      r
    end
  end
end

#exit_code(id) ⇒ Integer

Read and return the exit code of the job.

Parameters:

  • id (Integer)

    The ID of the job on the server

Returns:

  • (Integer)

    The exit code

Raises:

  • (ServerFailure)

    If the job doesn’t exist or retrieval fails



190
191
192
193
194
195
196
197
198
199
200
# File 'lib/baza-rb.rb', line 190

def exit_code(id)
  raise 'The ID of the job is nil' if id.nil?
  raise 'The ID of the job must be a positive integer' unless id.positive?
  code = 0
  elapsed(@loog, level: Logger::INFO) do
    ret = get(home.append('exit').append("#{id}.txt"))
    code = ret.body.to_i
    throw :"The exit code of the job ##{id} is #{code}"
  end
  code
end

#fee(tab, amount, summary, job) ⇒ Integer

Pay a fee associated with a job.

Parameters:

  • tab (String)

    The category/type of the fee (use “unknown” if not sure)

  • amount (Float)

    The fee amount in Ƶ (zents)

  • summary (String)

    The description/reason for the fee

  • job (Integer)

    The ID of the job this fee is for

Returns:

  • (Integer)

    Receipt ID for the fee payment

Raises:



464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# File 'lib/baza-rb.rb', line 464

def fee(tab, amount, summary, job)
  raise 'The "tab" is nil' if tab.nil?
  raise 'The "amount" is nil' if amount.nil?
  raise 'The "amount" must be Float' unless amount.is_a?(Float)
  raise 'The "job" is nil' if job.nil?
  raise 'The "job" must be Integer' unless job.is_a?(Integer)
  raise 'The "summary" is nil' if summary.nil?
  id = nil
  elapsed(@loog, level: Logger::INFO) do
    ret = post(
      home.append('account').append('fee'),
      {
        'tab' => tab,
        'amount' => format('%0.6f', amount),
        'summary' => summary,
        'job' => job.to_s
      }
    )
    id = ret.headers['X-Zerocracy-ReceiptId'].to_i
    throw :"Fee Ƶ#{format('%0.6f', amount)} paid at #{@host}"
  end
  id
end

#finish(id, zip) ⇒ Object

Submit a ZIP archive to finish a previously popped job.

Parameters:

  • id (Integer)

    The ID of the job to finish

  • zip (String)

    The path to the ZIP file containing job results

Raises:



518
519
520
521
522
523
524
525
526
527
# File 'lib/baza-rb.rb', line 518

def finish(id, zip)
  raise 'The ID of the job is nil' if id.nil?
  raise 'The ID of the job must be a positive integer' unless id.positive?
  raise 'The "zip" of the job is nil' if zip.nil?
  raise "The 'zip' file is absent: #{zip}" unless File.exist?(zip)
  elapsed(@loog, level: Logger::INFO) do
    upload(home.append('finish').add(id:), zip)
    throw :"Pushed #{File.size(zip)} bytes to #{@host}, finished job ##{id}"
  end
end

#finished?(id) ⇒ Boolean

Check if the job with this ID is finished already.

Parameters:

  • id (Integer)

    The ID of the job on the server

Returns:

  • (Boolean)

    TRUE if the job has completed execution, FALSE otherwise

Raises:



156
157
158
159
160
161
162
163
164
165
166
# File 'lib/baza-rb.rb', line 156

def finished?(id)
  raise 'The ID of the job is nil' if id.nil?
  raise 'The ID of the job must be a positive integer' unless id.positive?
  fin = false
  elapsed(@loog, level: Logger::INFO) do
    ret = get(home.append('finished').append(id))
    fin = ret.body == 'yes'
    throw :"The job ##{id} is #{'not yet ' unless fin}finished at #{@host}#{" (#{ret.body.inspect})" unless fin}"
  end
  fin
end

#lock(pname, owner) ⇒ Object

Lock the name.

Parameters:

  • pname (String)

    The name of the product on the server

  • owner (String)

    The owner of the lock (any string)

Raises:

  • (RuntimeError)

    If the name is already locked

  • (ServerFailure)

    If the lock operation fails



225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/baza-rb.rb', line 225

def lock(pname, owner)
  raise 'The "pname" of the product is nil' if pname.nil?
  raise 'The "pname" of the product may not be empty' if pname.empty?
  raise 'The "owner" of the lock is nil' if owner.nil?
  elapsed(@loog, level: Logger::INFO) do
    ret = post(
      home.append('lock').append(pname),
      { 'owner' => owner },
      [302, 409]
    )
    throw :"Product name #{pname.inspect} locked at #{@host}" if ret.code == 302
    raise "Failed to lock #{pname.inspect} product at #{@host}, it's already locked"
  end
end

#name_exists?(pname) ⇒ Boolean

Check whether the name of the job exists on the server.

Parameters:

  • pname (String)

    The name of the product on the server

Returns:

  • (Boolean)

    TRUE if such name exists



280
281
282
283
284
285
286
287
288
289
290
# File 'lib/baza-rb.rb', line 280

def name_exists?(pname)
  raise 'The "pname" of the product is nil' if pname.nil?
  raise 'The "pname" of the product may not be empty' if pname.empty?
  exists = false
  elapsed(@loog, level: Logger::INFO) do
    ret = get(home.append('exists').append(pname))
    exists = ret.body == 'yes'
    throw :"The name #{pname.inspect} #{exists ? 'exists' : "doesn't exist"} at #{@host}"
  end
  exists
end

#pop(owner, zip) ⇒ Boolean

Pop the next available job from the server’s queue.

Parameters:

  • owner (String)

    Identifier of who is taking the job (any descriptive text)

  • zip (String)

    The local file path where the job’s ZIP archive will be saved

Returns:

  • (Boolean)

    TRUE if a job was successfully popped, FALSE if queue is empty

Raises:



494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
# File 'lib/baza-rb.rb', line 494

def pop(owner, zip)
  success = false
  elapsed(@loog, level: Logger::INFO) do
    uri = home.append('pop').add(owner:)
    ret = get(uri, [204, 302])
    if ret.code == 204
      FileUtils.rm_f(zip)
      throw :"Nothing to pop at #{uri}"
    end
    job = ret.headers['X-Zerocracy-JobId']
    raise 'Job ID is not returned in X-Zerocracy-JobId' if job.nil?
    raise "Job ID returned in X-Zerocracy-JobId is not valid (#{job.inspect})" unless job.match?(/^[0-9]+$/)
    download(uri.add(job:), zip)
    success = true
    throw :"Popped #{File.size(zip)} bytes in ZIP archive at #{@host}"
  end
  success
end

#pull(id) ⇒ String

Pull factbase from the server for a specific job.

Parameters:

  • id (Integer)

    The ID of the job on the server

Returns:

  • (String)

    Binary data of the factbase (can be saved to file)

Raises:



137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/baza-rb.rb', line 137

def pull(id)
  raise 'The ID of the job is nil' if id.nil?
  raise 'The ID of the job must be a positive integer' unless id.positive?
  data = ''
  elapsed(@loog, level: Logger::INFO) do
    Tempfile.open do |file|
      download(home.append('pull').append("#{id}.fb"), file.path)
      data = File.binread(file)
      throw :"Pulled #{data.bytesize} bytes of job ##{id} factbase at #{@host}"
    end
  end
  data
end

#push(pname, data, meta, chunk_size: DEFAULT_CHUNK_SIZE) ⇒ Object

Push factbase to the server to create a new job.

Parameters:

  • pname (String)

    The unique name of the product on the server

  • data (String)

    The binary data to push to the server (factbase content)

  • meta (Array<String>)

    List of metadata strings to attach to the job

  • chunk_size (Integer) (defaults to: DEFAULT_CHUNK_SIZE)

    Maximum size of one chunk

Raises:



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/baza-rb.rb', line 111

def push(pname, data, meta, chunk_size: DEFAULT_CHUNK_SIZE)
  raise 'The "name" of the job is nil' if pname.nil?
  raise 'The "name" of the job may not be empty' if pname.empty?
  raise 'The "data" of the job is nil' if data.nil?
  raise 'The "meta" of the job is nil' if meta.nil?
  elapsed(@loog, level: Logger::INFO) do
    Tempfile.open do |file|
      File.binwrite(file.path, data)
      upload(
        home.append('push').append(pname),
        file.path,
        headers.merge(
          'X-Zerocracy-Meta' => meta.map { |v| Base64.encode64(v).delete("\n") }.join(' ')
        ),
        chunk_size:
      )
    end
    throw :"Pushed #{data.bytesize} bytes to #{@host}"
  end
end

#recent(name) ⇒ Integer

Get the ID of the job by the name.

Parameters:

  • name (String)

    The name of the job on the server

Returns:

  • (Integer)

    The ID of the job on the server

Raises:

  • (ServerFailure)

    If the job doesn’t exist or retrieval fails



264
265
266
267
268
269
270
271
272
273
274
# File 'lib/baza-rb.rb', line 264

def recent(name)
  raise 'The "name" of the job is nil' if name.nil?
  raise 'The "name" of the job may not be empty' if name.empty?
  job = nil
  elapsed(@loog, level: Logger::INFO) do
    ret = get(home.append('recent').append("#{name}.txt"))
    job = ret.body.to_i
    throw :"The recent \"#{name}\" job's ID is ##{job} at #{@host}"
  end
  job
end

#stdout(id) ⇒ String

Read and return the stdout of the job.

Parameters:

  • id (Integer)

    The ID of the job on the server

Returns:

  • (String)

    The stdout, as a text

Raises:

  • (ServerFailure)

    If the job doesn’t exist or retrieval fails



173
174
175
176
177
178
179
180
181
182
183
# File 'lib/baza-rb.rb', line 173

def stdout(id)
  raise 'The ID of the job is nil' if id.nil?
  raise 'The ID of the job must be a positive integer' unless id.positive?
  stdout = ''
  elapsed(@loog, level: Logger::INFO) do
    ret = get(home.append('stdout').append("#{id}.txt"))
    stdout = ret.body
    throw :"The stdout of the job ##{id} has #{stdout.split("\n").count} lines"
  end
  stdout
end

#transfer(recipient, amount, summary, job: nil) ⇒ Integer

Transfer funds to another user.

Parameters:

  • recipient (String)

    GitHub username of the recipient (e.g. “yegor256”)

  • amount (Float)

    The amount to transfer in Ƶ (zents)

  • summary (String)

    The description/reason for the payment

  • job (Integer) (defaults to: nil)

    Optional job ID to associate with this transfer

Returns:

  • (Integer)

    Receipt ID for the transaction

Raises:



433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/baza-rb.rb', line 433

def transfer(recipient, amount, summary, job: nil)
  raise 'The "recipient" is nil' if recipient.nil?
  raise 'The "amount" is nil' if amount.nil?
  raise 'The "amount" must be Float' unless amount.is_a?(Float)
  raise 'The "summary" is nil' if summary.nil?
  id = nil
  body = {
    'human' => recipient,
    'amount' => format('%0.6f', amount),
    'summary' => summary
  }
  body['job'] = job unless job.nil?
  elapsed(@loog, level: Logger::INFO) do
    ret = post(
      home.append('account').append('transfer'),
      body
    )
    id = ret.headers['X-Zerocracy-ReceiptId'].to_i
    throw :"Transferred Ƶ#{format('%0.6f', amount)} to @#{recipient} at #{@host}"
  end
  id
end

#unlock(pname, owner) ⇒ Object

Unlock the name.

Parameters:

  • pname (String)

    The name of the job on the server

  • owner (String)

    The owner of the lock (any string)

Raises:



245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/baza-rb.rb', line 245

def unlock(pname, owner)
  raise 'The "pname" of the job is nil' if pname.nil?
  raise 'The "pname" of the job may not be empty' if pname.empty?
  raise 'The "owner" of the lock is nil' if owner.nil?
  raise 'The "owner" of the lock may not be empty' if owner.empty?
  elapsed(@loog, level: Logger::INFO) do
    post(
      home.append('unlock').append(pname),
      { 'owner' => owner }
    )
    throw :"Job name #{pname.inspect} unlocked at #{@host}"
  end
end

#verified(id) ⇒ String

Read and return the verification verdict of the job.

Parameters:

  • id (Integer)

    The ID of the job on the server

Returns:

  • (String)

    The verdict

Raises:

  • (ServerFailure)

    If the job doesn’t exist or retrieval fails



207
208
209
210
211
212
213
214
215
216
217
# File 'lib/baza-rb.rb', line 207

def verified(id)
  raise 'The ID of the job is nil' if id.nil?
  raise 'The ID of the job must be a positive integer' unless id.positive?
  verdict = ''
  elapsed(@loog, level: Logger::INFO) do
    ret = get(home.append('jobs').append(id).append('verified.txt'))
    verdict = ret.body
    throw :"The verdict of the job ##{id} is #{verdict.inspect}"
  end
  verdict
end

#whoamiString

Get GitHub login name of the logged in user.

Returns:

  • (String)

    GitHub nickname of the authenticated user

Raises:

  • (ServerFailure)

    If authentication fails or server returns an error



80
81
82
83
84
85
86
87
88
# File 'lib/baza-rb.rb', line 80

def whoami
  nick = nil
  elapsed(@loog, level: Logger::INFO) do
    ret = get(home.append('whoami'))
    nick = ret.body
    throw :"I know that I am @#{nick}, at #{@host}"
  end
  nick
end