baza.rb 0.0.1

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 ADDED
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
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.
22
+
23
+ require 'typhoeus'
24
+ require 'retries'
25
+ require 'iri'
26
+ require 'loog'
27
+ require 'base64'
28
+ require_relative 'baza/elapsed'
29
+
30
+ # Interface to the API of zerocracy.com.
31
+ #
32
+ # You make an instance of this class and then call one of its methods.
33
+ # The object will make HTTP request to api.zerocracy.com and interpret the
34
+ # results returned.
35
+ #
36
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
37
+ # Copyright:: Copyright (c) 2024 Yegor Bugayenko
38
+ # License:: MIT
39
+ class Baza
40
+ VERSION = '0.0.0'
41
+
42
+ def initialize(host, port, token, ssl: true, timeout: 30, retries: 3, loog: Loog::NULL, compression: true)
43
+ @host = host
44
+ @port = port
45
+ @ssl = ssl
46
+ @token = token
47
+ @timeout = timeout
48
+ @loog = loog
49
+ @retries = retries
50
+ @compression = compression
51
+ end
52
+
53
+ # Push factbase to the server.
54
+ # @param [String] name The name of the job on the server
55
+ # @param [Bytes] data The data to push to the server (binary)
56
+ # @param [Array<String>] meta List of metas, possibly empty
57
+ # @return [Integer] Job ID on the server
58
+ def push(name, data, meta)
59
+ id = 0
60
+ hdrs = headers.merge(
61
+ 'Content-Type' => 'application/octet-stream',
62
+ 'Content-Length' => data.size
63
+ )
64
+ unless meta.empty?
65
+ hdrs = hdrs.merge('X-Zerocracy-Meta' => meta.map { |v| Base64.encode64(v).gsub("\n", '') }.join(' '))
66
+ end
67
+ params = {
68
+ connecttimeout: @timeout,
69
+ timeout: @timeout,
70
+ body: data,
71
+ headers: hdrs
72
+ }
73
+ elapsed(@loog) do
74
+ ret =
75
+ with_retries(max_tries: @retries) do
76
+ checked(
77
+ Typhoeus::Request.put(
78
+ home.append('push').append(name).to_s,
79
+ @compression ? zipped(params) : params
80
+ )
81
+ )
82
+ end
83
+ id = ret.body.to_i
84
+ throw :"Pushed #{data.size} bytes to #{@host}, job ID is ##{id}"
85
+ end
86
+ id
87
+ end
88
+
89
+ # Pull factbase from the server.
90
+ # @param [Integer] id The ID of the job on the server
91
+ # @return [Bytes] Binary data pulled
92
+ def pull(id)
93
+ data = 0
94
+ elapsed(@loog) do
95
+ Tempfile.open do |file|
96
+ File.open(file, 'wb') do |f|
97
+ request = Typhoeus::Request.new(
98
+ home.append('pull').append("#{id}.fb").to_s,
99
+ headers: headers.merge(
100
+ 'Accept' => 'application/octet-stream'
101
+ ),
102
+ connecttimeout: @timeout,
103
+ timeout: @timeout
104
+ )
105
+ request.on_body do |chunk|
106
+ f.write(chunk)
107
+ end
108
+ with_retries(max_tries: @retries) do
109
+ request.run
110
+ end
111
+ checked(request.response)
112
+ end
113
+ data = File.binread(file)
114
+ throw :"Pulled #{data.size} bytes of job ##{id} factbase at #{@host}"
115
+ end
116
+ end
117
+ data
118
+ end
119
+
120
+ # The job with this ID is finished already?
121
+ # @param [Integer] id The ID of the job on the server
122
+ # @return [Boolean] TRUE if the job is already finished
123
+ def finished?(id)
124
+ finished = false
125
+ elapsed(@loog) do
126
+ ret =
127
+ with_retries(max_tries: @retries) do
128
+ checked(
129
+ Typhoeus::Request.get(
130
+ home.append('finished').append(id).to_s,
131
+ headers:
132
+ )
133
+ )
134
+ end
135
+ finished = ret.body == 'yes'
136
+ throw :"The job ##{id} is #{finished ? '' : 'not yet '}finished at #{@host}"
137
+ end
138
+ finished
139
+ end
140
+
141
+ # Read and return the stdout of the job.
142
+ # @param [Integer] id The ID of the job on the server
143
+ # @return [String] The stdout, as a text
144
+ def stdout(id)
145
+ stdout = ''
146
+ elapsed(@loog) do
147
+ ret =
148
+ with_retries(max_tries: @retries) do
149
+ checked(
150
+ Typhoeus::Request.get(
151
+ home.append('stdout').append("#{id}.txt").to_s,
152
+ headers:
153
+ )
154
+ )
155
+ end
156
+ stdout = ret.body
157
+ throw :"The stdout of the job ##{id} has #{stdout.split("\n")} lines"
158
+ end
159
+ stdout
160
+ end
161
+
162
+ # Read and return the exit code of the job.
163
+ # @param [Integer] id The ID of the job on the server
164
+ # @return [Integer] The exit code
165
+ def exit_code(id)
166
+ code = 0
167
+ elapsed(@loog) do
168
+ ret =
169
+ with_retries(max_tries: @retries) do
170
+ checked(
171
+ Typhoeus::Request.get(
172
+ home.append('exit').append("#{id}.txt").to_s,
173
+ headers:
174
+ )
175
+ )
176
+ end
177
+ code = ret.body.to_i
178
+ throw :"The exit code of the job ##{id} is #{code}"
179
+ end
180
+ code
181
+ end
182
+
183
+ # Lock the name.
184
+ # @param [String] name The name of the job on the server
185
+ # @param [String] owner The owner of the lock (any string)
186
+ def lock(name, owner)
187
+ elapsed(@loog) do
188
+ with_retries(max_tries: @retries) do
189
+ checked(
190
+ Typhoeus::Request.get(
191
+ home.append('lock').append(name).add(owner:).to_s,
192
+ headers:
193
+ ),
194
+ 302
195
+ )
196
+ end
197
+ end
198
+ end
199
+
200
+ # Unlock the name.
201
+ # @param [String] name The name of the job on the server
202
+ # @param [String] owner The owner of the lock (any string)
203
+ def unlock(name, owner)
204
+ elapsed(@loog) do
205
+ with_retries(max_tries: @retries) do
206
+ checked(
207
+ Typhoeus::Request.get(
208
+ home.append('unlock').append(name).add(owner:).to_s,
209
+ headers:
210
+ ),
211
+ 302
212
+ )
213
+ end
214
+ end
215
+ end
216
+
217
+ # Get the ID of the job by the name.
218
+ # @param [String] name The name of the job on the server
219
+ # @return [Integer] The ID of the job on the server
220
+ def recent(name)
221
+ job = 0
222
+ elapsed(@loog) do
223
+ ret =
224
+ with_retries(max_tries: @retries) do
225
+ checked(
226
+ Typhoeus::Request.get(
227
+ home.append('recent').append("#{name}.txt").to_s,
228
+ headers:
229
+ )
230
+ )
231
+ end
232
+ job = ret.body.to_i
233
+ throw :"The recent \"#{name}\" job's ID is ##{job} at #{@host}"
234
+ end
235
+ job
236
+ end
237
+
238
+ # Check whether the name of the job exists on the server.
239
+ # @param [String] name The name of the job on the server
240
+ # @return [Boolean] TRUE if such name exists
241
+ def name_exists?(name)
242
+ exists = 0
243
+ elapsed(@loog) do
244
+ ret =
245
+ with_retries(max_tries: @retries) do
246
+ checked(
247
+ Typhoeus::Request.get(
248
+ home.append('exists').append(name).to_s,
249
+ headers:
250
+ )
251
+ )
252
+ end
253
+ exists = ret.body == 'yes'
254
+ throw :"The name \"#{name}\" #{exists ? 'exists' : "doesn't exist"} at #{@host}"
255
+ end
256
+ exists
257
+ end
258
+
259
+ private
260
+
261
+ def headers
262
+ {
263
+ 'User-Agent' => "baza.rb #{Baza::VERSION}",
264
+ 'Connection' => 'close',
265
+ 'X-Zerocracy-Token' => @token
266
+ }
267
+ end
268
+
269
+ def zipped(params)
270
+ body = gzip(params.fetch(:body))
271
+ headers = params
272
+ .fetch(:headers)
273
+ .merge(
274
+ {
275
+ 'Content-Type' => 'application/zip',
276
+ 'Content-Encoding' => 'gzip',
277
+ 'Content-Length' => body.size
278
+ }
279
+ )
280
+ params.merge(body:, headers:)
281
+ end
282
+
283
+ def gzip(data)
284
+ ''.dup.tap do |result|
285
+ io = StringIO.new(result)
286
+ gz = Zlib::GzipWriter.new(io)
287
+ gz.write(data)
288
+ gz.close
289
+ end
290
+ end
291
+
292
+ def home
293
+ Iri.new('')
294
+ .host(@host)
295
+ .port(@port)
296
+ .scheme(@ssl ? 'https' : 'http')
297
+ end
298
+
299
+ def checked(ret, allowed = [200])
300
+ allowed = [allowed] unless allowed.is_a?(Array)
301
+ mtd = (ret.request.original_options[:method] || '???').upcase
302
+ url = ret.effective_url
303
+ log = "#{mtd} #{url} -> #{ret.code}"
304
+ if allowed.include?(ret.code)
305
+ @loog.debug(log)
306
+ return ret
307
+ end
308
+ @loog.debug("#{log}\n #{(ret.headers || {}).map { |k, v| "#{k}: #{v}" }.join("\n ")}")
309
+ msg = [
310
+ "Invalid response code ##{ret.code} ",
311
+ "at #{mtd} #{url}",
312
+ ret.headers['X-Zerocracy-Flash'] ? " (#{ret.headers['X-Zerocracy-Flash'].inspect})" : ''
313
+ ].join
314
+ case ret.code
315
+ when 500
316
+ msg +=
317
+ ', most probably it\'s an internal error on the server, ' \
318
+ 'please report this to https://github.com/zerocracy/baza'
319
+ when 503
320
+ msg +=
321
+ ", most probably it's an internal error on the server (#{ret.headers['X-Zerocracy-Failure'].inspect}), " \
322
+ 'please report this to https://github.com/zerocracy/baza.rb'
323
+ when 404
324
+ msg +=
325
+ ', most probably you are trying to reach a wrong server, which doesn\'t ' \
326
+ 'have the URL that it is expected to have'
327
+ when 0
328
+ msg += ', most likely an internal error'
329
+ end
330
+ raise msg
331
+ end
332
+ end
data/renovate.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": [
4
+ "config:base"
5
+ ]
6
+ }
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
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.
22
+
23
+ $stdout.sync = true
24
+
25
+ require 'minitest/reporters'
26
+ Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
27
+
28
+ require 'simplecov'
29
+ SimpleCov.start
30
+
31
+ require 'simplecov-cobertura'
32
+ SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
33
+
34
+ require 'minitest/autorun'
data/test/test_baza.rb ADDED
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
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.
22
+
23
+ require 'minitest/autorun'
24
+ require 'webmock/minitest'
25
+ require 'webrick'
26
+ require 'loog'
27
+ require 'socket'
28
+ require 'stringio'
29
+ require 'random-port'
30
+ require 'factbase'
31
+ require 'securerandom'
32
+ require 'net/ping'
33
+ require_relative '../lib/baza'
34
+
35
+ # Test.
36
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
37
+ # Copyright:: Copyright (c) 2024 Yegor Bugayenko
38
+ # License:: MIT
39
+ class TestBaza < Minitest::Test
40
+ TOKEN = '00000000-0000-0000-0000-000000000000'
41
+ HOST = 'api.zerocracy.com'
42
+ PORT = 443
43
+ LIVE = Baza.new(HOST, PORT, TOKEN)
44
+
45
+ def test_live_recent_check
46
+ WebMock.enable_net_connect!
47
+ skip unless we_are_online
48
+ assert(LIVE.recent('zerocracy').positive?)
49
+ end
50
+
51
+ def test_live_name_exists_check
52
+ WebMock.enable_net_connect!
53
+ skip unless we_are_online
54
+ assert(LIVE.name_exists?('zerocracy'))
55
+ end
56
+
57
+ def test_live_push
58
+ WebMock.enable_net_connect!
59
+ skip unless we_are_online
60
+ fb = Factbase.new
61
+ fb.insert.foo = 'test-' * 10_000
62
+ fb.insert
63
+ assert(LIVE.push(fake_name, fb.export, []).positive?)
64
+ end
65
+
66
+ def test_live_push_no_compression
67
+ WebMock.enable_net_connect!
68
+ skip unless we_are_online
69
+ fb = Factbase.new
70
+ fb.insert.foo = 'test-' * 10_000
71
+ fb.insert
72
+ baza = Baza.new(HOST, PORT, TOKEN, compression: false)
73
+ assert(baza.push(fake_name, fb.export, []).positive?)
74
+ end
75
+
76
+ def test_live_pull
77
+ WebMock.enable_net_connect!
78
+ skip unless we_are_online
79
+ id = LIVE.recent('zerocracy')
80
+ assert(!LIVE.pull(id).nil?)
81
+ end
82
+
83
+ def test_live_check_finished
84
+ WebMock.enable_net_connect!
85
+ skip unless we_are_online
86
+ id = LIVE.recent('zerocracy')
87
+ assert(!LIVE.finished?(id).nil?)
88
+ end
89
+
90
+ def test_live_read_stdout
91
+ WebMock.enable_net_connect!
92
+ skip unless we_are_online
93
+ id = LIVE.recent('zerocracy')
94
+ assert(!LIVE.stdout(id).nil?)
95
+ end
96
+
97
+ def test_live_read_exit_code
98
+ WebMock.enable_net_connect!
99
+ skip unless we_are_online
100
+ id = LIVE.recent('zerocracy')
101
+ assert(!LIVE.exit_code(id).nil?)
102
+ end
103
+
104
+ def test_live_lock_unlock
105
+ WebMock.enable_net_connect!
106
+ skip unless we_are_online
107
+ n = fake_name
108
+ owner = 'judges teesting'
109
+ assert(!LIVE.lock(n, owner).nil?)
110
+ assert(!LIVE.unlock(n, owner).nil?)
111
+ end
112
+
113
+ def test_simple_push
114
+ WebMock.disable_net_connect!
115
+ stub_request(:put, 'https://example.org/push/simple').to_return(
116
+ status: 200, body: '42'
117
+ )
118
+ assert_equal(
119
+ 42,
120
+ Baza.new('example.org', 443, '000').push('simple', 'hello, world!', [])
121
+ )
122
+ end
123
+
124
+ def test_simple_recent_check
125
+ WebMock.disable_net_connect!
126
+ stub_request(:get, 'https://example.org/recent/simple.txt')
127
+ .with(body: '', headers: { 'User-Agent' => /^baza.rb .*$/ })
128
+ .to_return(status: 200, body: '42')
129
+ assert_equal(
130
+ 42,
131
+ Baza.new('example.org', 443, '000').recent('simple')
132
+ )
133
+ end
134
+
135
+ def test_simple_exists_check
136
+ WebMock.disable_net_connect!
137
+ stub_request(:get, 'https://example.org/exists/simple').to_return(
138
+ status: 200, body: 'yes'
139
+ )
140
+ assert(
141
+ Baza.new('example.org', 443, '000').name_exists?('simple')
142
+ )
143
+ end
144
+
145
+ def test_exit_code_check
146
+ WebMock.disable_net_connect!
147
+ stub_request(:get, 'https://example.org/exit/42.txt').to_return(
148
+ status: 200, body: '0'
149
+ )
150
+ assert(
151
+ Baza.new('example.org', 443, '000').exit_code(42).zero?
152
+ )
153
+ end
154
+
155
+ def test_stdout_read
156
+ WebMock.disable_net_connect!
157
+ stub_request(:get, 'https://example.org/stdout/42.txt').to_return(
158
+ status: 200, body: 'hello!'
159
+ )
160
+ assert(
161
+ !Baza.new('example.org', 443, '000').stdout(42).empty?
162
+ )
163
+ end
164
+
165
+ def test_simple_pull
166
+ WebMock.disable_net_connect!
167
+ stub_request(:get, 'https://example.org/pull/333.fb').to_return(
168
+ status: 200, body: 'hello, world!'
169
+ )
170
+ assert(
171
+ Baza.new('example.org', 443, '000').pull(333).start_with?('hello')
172
+ )
173
+ end
174
+
175
+ def test_real_http
176
+ req =
177
+ with_http_server(200, 'yes') do |baza|
178
+ baza.name_exists?('simple')
179
+ end
180
+ assert_equal("baza.rb #{Baza::VERSION}", req['user-agent'])
181
+ end
182
+
183
+ def test_push_with_meta
184
+ req =
185
+ with_http_server(200, 'yes') do |baza|
186
+ baza.push('simple', 'hello, world!', ['boom!', 'хей!'])
187
+ end
188
+ assert_equal('Ym9vbSE= 0YXQtdC5IQ==', req['x-zerocracy-meta'])
189
+ end
190
+
191
+ def test_push_with_big_meta
192
+ req =
193
+ with_http_server(200, 'yes') do |baza|
194
+ baza.push(
195
+ 'simple',
196
+ 'hello, world!',
197
+ [
198
+ 'pages_url:https://zerocracy.github.io/zerocracy.html',
199
+ 'others:https://zerocracy.github.io/zerocracy.html',
200
+ 'duration:59595'
201
+ ]
202
+ )
203
+ end
204
+ assert(req['x-zerocracy-meta'])
205
+ end
206
+
207
+ def test_push_compressed_content
208
+ skip # this test is not stable, see https://github.com/yegor256/judges/issues/105
209
+ req =
210
+ with_http_server(200, 'yes') do |baza|
211
+ baza.push('simple', 'hello, world!', %w[meta1 meta2 meta3])
212
+ end
213
+ assert_equal('application/zip', req.content_type)
214
+ assert_equal('gzip', req['content-encoding'])
215
+ body = Zlib::GzipReader.zcat(StringIO.new(req.body))
216
+ assert_equal('hello, world!', body)
217
+ end
218
+
219
+ def test_push_compression_disabled
220
+ req =
221
+ with_http_server(200, 'yes', compression: false) do |baza|
222
+ baza.push('simple', 'hello, world!', %w[meta1 meta2 meta3])
223
+ end
224
+ assert_equal('application/octet-stream', req.content_type)
225
+ assert_equal('hello, world!', req.body)
226
+ end
227
+
228
+ private
229
+
230
+ def with_http_server(code, response, opts = {})
231
+ opts = { ssl: false, timeout: 1 }.merge(opts)
232
+ WebMock.enable_net_connect!
233
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
234
+ host = '127.0.0.1'
235
+ RandomPort::Pool::SINGLETON.acquire do |port|
236
+ server = TCPServer.new(host, port)
237
+ t =
238
+ Thread.new do
239
+ socket = server.accept
240
+ req.parse(socket)
241
+ req.body
242
+ socket.puts "HTTP/1.1 #{code} OK\r\nContent-Length: #{response.length}\r\n\r\n#{response}"
243
+ socket.close
244
+ end
245
+ yield Baza.new(host, port, '0000', **opts)
246
+ t.join
247
+ end
248
+ req
249
+ end
250
+
251
+ def fake_name
252
+ "fake-#{SecureRandom.hex(8)}"
253
+ end
254
+
255
+ def we_are_online
256
+ Net::Ping::External.new('8.8.8.8').ping?
257
+ end
258
+ end