filbunke 2.0.9 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c42ae52d1bc54da6a935b95b029a6bf1797d6d34
4
- data.tar.gz: 1435482387aa797f0a0780aa65d79a3ecacd17a3
3
+ metadata.gz: bdf27520bab96738e5a12465ccd526ee4d3d837a
4
+ data.tar.gz: dbb247721d46a2b8aafe2d5942120850b304fd3a
5
5
  SHA512:
6
- metadata.gz: 4b268216e4e4db244c77d734f935507061631f4ac568ce6e9009e991837af4dd25fd9dd1bd205895c438c9380005b10b1c7c9aa6694e49ad63bbe3b15743762f
7
- data.tar.gz: fbae049f4a7e51a0635348d1923fa6b0bb7b46bd672f982317614fdd51c8e96f882922cb28eebca12300c8b036d4dd78f7458cb5491b426a80430241abbf9f9b
6
+ metadata.gz: 6754b1ac1376a1f491d7159fe09ab95b474637b144d2ab09fdbea87fd724234e2cf9ee5d535a6678cfbcca585dd681f186df7c193cceb6e0480d3f154536825c
7
+ data.tar.gz: 17084f8cc91464c6a9a2d1a47f5b2df0c732bc338bbd877f2795af9e16d44c80c1aea2d0d38304adee1bd5e8c06dce0d8503518f720da45253e4dea1123c3ee9
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
- Gemfile.lock
1
+ Gemfile.lock
2
+ pkg/
data/Gemfile CHANGED
@@ -1,3 +1,7 @@
1
1
  source 'http://rubygems.org'
2
2
 
3
- gem 'jeweler'
3
+
4
+ group :test do
5
+ gem 'shoulda'
6
+ gem 'test-unit'
7
+ end
data/Rakefile CHANGED
@@ -3,6 +3,13 @@ require 'rake'
3
3
 
4
4
  begin
5
5
  require 'jeweler'
6
+ required_dependencies = {
7
+ "json" => "1.8.3",
8
+ "typhoeus" => "1.0.1",
9
+ "open4" => "1.3.4",
10
+ "mime-types" => "2.6.2",
11
+ "parallel" => "1.6.1"
12
+ }
6
13
  Jeweler::Tasks.new do |gem|
7
14
  gem.name = "filbunke"
8
15
  gem.summary = %Q{Filbunke client}
@@ -10,29 +17,33 @@ begin
10
17
  gem.email = "technical@deltaprojects.com"
11
18
  gem.homepage = "https://rubygems.org/gems/filbunke"
12
19
  gem.authors = ["Wouter de Bie", "Bjorn Sperber", "Karl Ravn", "Magnus Spangdal"]
13
- gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
20
+ gem.add_development_dependency "shoulda", "~> 0"
14
21
  gem.files.exclude 'pkg'
15
22
  gem.executables = ['filbunked']
16
- gem.add_dependency 'json', '= 1.8.3'
17
- gem.add_dependency 'typhoeus', '= 0.7.3'
18
- gem.add_dependency 'open4', '= 1.3.4'
19
- gem.add_dependency 'mime-types', '= 2.6.2'
20
- gem.add_dependency 'parallel', '= 1.6.1'
23
+ required_dependencies.each do |name, version|
24
+ gem.add_dependency "#{name}", "= #{version}"
25
+ end
21
26
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
22
27
  end
23
28
  Jeweler::GemcutterTasks.new
24
29
  rescue LoadError => e
25
- puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
30
+ puts "Jeweler (or a dependency) not available. Install it with: \n\tgem install jeweler \nadditional dependencies;"
31
+ required_dependencies.each do |name,version|
32
+ puts "\tgem install #{name} -v #{version}"
33
+ end
26
34
  raise e
27
35
  end
28
36
 
37
+
29
38
  require 'rake/testtask'
30
39
  Rake::TestTask.new(:test) do |test|
31
40
  test.libs << 'lib' << 'test'
32
41
  test.pattern = 'test/**/test_*.rb'
33
42
  test.verbose = true
43
+ test.warning = true
34
44
  end
35
45
 
46
+
36
47
  begin
37
48
  require 'rcov/rcovtask'
38
49
  Rcov::RcovTask.new do |test|
@@ -46,6 +57,10 @@ rescue LoadError
46
57
  end
47
58
  end
48
59
 
60
+ task :check_dependencies do
61
+
62
+ end
63
+
49
64
  task :test => :check_dependencies
50
65
 
51
66
  task :default => :test
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.9
1
+ 2.1.0
data/filbunke.gemspec CHANGED
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: filbunke 2.0.9 ruby lib
5
+ # stub: filbunke 2.1.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "filbunke"
9
- s.version = "2.0.9"
9
+ s.version = "2.1.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib"]
13
13
  s.authors = ["Wouter de Bie", "Bjorn Sperber", "Karl Ravn", "Magnus Spangdal"]
14
- s.date = "2015-11-05"
14
+ s.date = "2016-03-05"
15
15
  s.description = "Filbunke client and library"
16
16
  s.email = "technical@deltaprojects.com"
17
17
  s.executables = ["filbunked"]
@@ -42,31 +42,31 @@ Gem::Specification.new do |s|
42
42
  "test/test_filbunke.rb"
43
43
  ]
44
44
  s.homepage = "https://rubygems.org/gems/filbunke"
45
- s.rubygems_version = "2.5.0"
45
+ s.rubygems_version = "2.4.5.1"
46
46
  s.summary = "Filbunke client"
47
47
 
48
48
  if s.respond_to? :specification_version then
49
49
  s.specification_version = 4
50
50
 
51
51
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
52
- s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
52
+ s.add_development_dependency(%q<shoulda>, ["~> 0"])
53
53
  s.add_runtime_dependency(%q<json>, ["= 1.8.3"])
54
- s.add_runtime_dependency(%q<typhoeus>, ["= 0.7.3"])
54
+ s.add_runtime_dependency(%q<typhoeus>, ["= 1.0.1"])
55
55
  s.add_runtime_dependency(%q<open4>, ["= 1.3.4"])
56
56
  s.add_runtime_dependency(%q<mime-types>, ["= 2.6.2"])
57
57
  s.add_runtime_dependency(%q<parallel>, ["= 1.6.1"])
58
58
  else
59
- s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
59
+ s.add_dependency(%q<shoulda>, ["~> 0"])
60
60
  s.add_dependency(%q<json>, ["= 1.8.3"])
61
- s.add_dependency(%q<typhoeus>, ["= 0.7.3"])
61
+ s.add_dependency(%q<typhoeus>, ["= 1.0.1"])
62
62
  s.add_dependency(%q<open4>, ["= 1.3.4"])
63
63
  s.add_dependency(%q<mime-types>, ["= 2.6.2"])
64
64
  s.add_dependency(%q<parallel>, ["= 1.6.1"])
65
65
  end
66
66
  else
67
- s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
67
+ s.add_dependency(%q<shoulda>, ["~> 0"])
68
68
  s.add_dependency(%q<json>, ["= 1.8.3"])
69
- s.add_dependency(%q<typhoeus>, ["= 0.7.3"])
69
+ s.add_dependency(%q<typhoeus>, ["= 1.0.1"])
70
70
  s.add_dependency(%q<open4>, ["= 1.3.4"])
71
71
  s.add_dependency(%q<mime-types>, ["= 2.6.2"])
72
72
  s.add_dependency(%q<parallel>, ["= 1.6.1"])
@@ -10,7 +10,6 @@ module Filbunke
10
10
  URL_KEY = 'url'
11
11
  FROM_CHECKPOINT_KEY = 'from_checkpoint'
12
12
  HASH_KEY = 'hash'
13
- URI_UNSAFE_CHARACTERS = '/[^.:\/\w-]/'
14
13
 
15
14
 
16
15
  def initialize(repository, logger, callbacks = [], failed_request_log_file_name = nil)
@@ -26,18 +25,17 @@ module Filbunke
26
25
  def with_updated_files(last_checkpoint)
27
26
  updates = get_updated_file_list(last_checkpoint)
28
27
  updated_files = updates["files"] || []
29
- failure = false
30
-
31
- new_checkpoint = updates["checkpoint"]
32
-
33
- @logger.info "Updating repository: #{@repository.name}: #{updated_files.size} files. Checkpoint: #{last_checkpoint} ==> #{new_checkpoint}" if updated_files.size > 0
28
+ new_checkpoint = updates["checkpoint"] || 0
29
+ if updated_files.empty?
30
+ return new_checkpoint
31
+ end
32
+ @logger.info "Updating repository: #{@repository.name}: #{updated_files.size} files. Checkpoint: #{last_checkpoint} ==> #{new_checkpoint}"
34
33
 
35
34
  @async_requests = []
36
-
37
35
  callbacks_on_update = []
38
36
  callbacks_on_no_change = []
39
37
  callbacks_on_delete = []
40
-
38
+ has_update_file_failure = false
41
39
  updated_files.each do |raw_file|
42
40
  file = File.new(raw_file)
43
41
  local_file_path = ::File.join(@repository.local_path, file.path)
@@ -50,42 +48,57 @@ module Filbunke
50
48
  yield file
51
49
  callbacks_on_update << OpenStruct.new({ :file => file, :local_file_path => local_file_path })
52
50
  else
53
- @logger.error "Unable to get file #{file.url} ==> #{file.path}!"
54
- failure = true
51
+ @logger.error "Unable to fetch file #{file.url} ==> #{file.path}!"
52
+ has_update_file_failure = true
53
+ break;
55
54
  end
56
-
55
+
57
56
  else
58
57
  @logger.debug "File exists with correct hash: #{local_file_path}"
59
58
  callbacks_on_no_change << OpenStruct.new({:file => file, :local_file_path => local_file_path})
60
59
  end
61
60
  end
62
61
  end
63
- @hydra.run
64
62
 
65
- pfailure = failure || @async_requests.any? do |request|
66
- @logger.warn "request did not handle response: #{request.inspect}" if request.response.nil? || request.response.code != 200
67
- request.response.nil? || request.response.code != 200
63
+ if has_update_file_failure
64
+ @logger.error "FAILED to fetch files for #{@repository.name} last_checkpoint = #{last_checkpoint}"
65
+ return last_checkpoint
68
66
  end
67
+ @logger.info "Done setting up async requests for #{@repository.name}, starting fetch..."
69
68
 
70
- if pfailure == false
71
- @logger.info "Done fetching files for #{@repository.name}, processing callbacks..."
72
- begin
73
- run_callbacks_delete(callbacks_on_delete)
74
- run_callbacks(callbacks_on_update)
75
- run_callbacks_no_change(callbacks_on_no_change)
76
-
77
- new_checkpoint || last_checkpoint
78
- rescue RuntimeError, SystemCallError, StandardError => e
79
- msg = ["Callbacks failed to run; #{e.class} - #{e.message}", *e.backtrace].join("\n\t")
80
- @logger.error "FAILED to update files for #{@repository.name} last_checkpoint = #{last_checkpoint}; #{msg}"
81
- last_checkpoint
69
+ has_fetch_files_failure = begin
70
+ @hydra.run
71
+ @async_requests.any? do |request|
72
+ @logger.warn "request did not handle response: #{request.inspect}" if request.response.nil? || request.response.code != 200
73
+ request.response.nil? || request.response.code != 200
82
74
  end
83
- else
75
+ rescue RuntimeError, SystemCallError, StandardError => e
76
+ msg = ["#{e.class} - #{e.message}", *e.backtrace].join("\n\t")
77
+ @logger.error "FAILED to fetch files for #{@repository.name} last_checkpoint = #{last_checkpoint}; #{msg}"
78
+ true
79
+ end
80
+
81
+ if has_fetch_files_failure
84
82
  @logger.error "FAILED to update files for #{@repository.name} last_checkpoint = #{last_checkpoint}"
83
+ return last_checkpoint
84
+ end
85
+
86
+ @logger.info "Done fetching files for #{@repository.name}, processing callbacks..."
87
+ new_or_last_checkpoint = begin
88
+ run_callbacks_delete(callbacks_on_delete)
89
+ run_callbacks(callbacks_on_update)
90
+ run_callbacks_no_change(callbacks_on_no_change)
91
+
92
+ new_checkpoint || last_checkpoint
93
+ rescue RuntimeError, SystemCallError, StandardError => e
94
+ msg = ["#{e.class} - #{e.message}", *e.backtrace].join("\n\t")
95
+ @logger.error "FAILED to process callbacks for #{@repository.name} last_checkpoint = #{last_checkpoint}; #{msg}"
85
96
  last_checkpoint
86
97
  end
98
+
99
+ new_or_last_checkpoint
87
100
  end
88
-
101
+
89
102
  def update_files!(last_checkpoint)
90
103
  with_updated_files(last_checkpoint) {}
91
104
  end
@@ -138,7 +151,7 @@ module Filbunke
138
151
  end
139
152
 
140
153
  def last_checkpoint
141
- last_checkpoint_http = Net::HTTP.new(@repository.host, @repository.port)
154
+ last_checkpoint_http = Net::HTTP.new(@repository.host, @repository.port)
142
155
  last_checkpoint_http.start do |http|
143
156
  last_checkpoint_path = "/#{UPDATES_ACTION}/#{@repository.name}/#{LAST_CHECKPOINT_ACTION}"
144
157
  request = Net::HTTP::Get.new(last_checkpoint_path)
@@ -149,7 +162,7 @@ module Filbunke
149
162
  return response.body.chomp.to_i
150
163
  end
151
164
  end
152
-
165
+
153
166
  private
154
167
 
155
168
  def log_failed_request(failed_request_command, e)
@@ -161,7 +174,7 @@ module Filbunke
161
174
  end
162
175
 
163
176
  def update_file!(file, local_file_path)
164
-
177
+
165
178
  if file.url =~ /^http:\/\//
166
179
  update_http_file!(file, local_file_path)
167
180
  elsif (file.url =~ /^hdfs:\/\//)
@@ -204,6 +217,7 @@ module Filbunke
204
217
  updates_http.read_timeout = 300 # default is 60 seconds
205
218
  updates_http.start do |http|
206
219
  updates_path = "/#{UPDATES_ACTION}/#{@repository.name}?#{FROM_CHECKPOINT_KEY}=#{last_checkpoint}"
220
+ updates_path = "#{updates_path}&batch_size=#{@repository.batch_size}" if @repository.batch_size > 0
207
221
  begin
208
222
  @logger.info "Fetching updated file list from #{updates_path}"
209
223
  request = Net::HTTP::Get.new(updates_path)
@@ -228,38 +242,59 @@ module Filbunke
228
242
  def update_http_file!(file, local_file_path)
229
243
  begin
230
244
  async_request = if @repository.user
231
- Typhoeus::Request.new(URI.encode(file.url, URI_UNSAFE_CHARACTERS), :followlocation => true, :username => @repository.user, :password => @repository.pass)
245
+ Typhoeus::Request.new(
246
+ URI.escape(file.url),
247
+ :followlocation => true,
248
+ :username => @repository.user,
249
+ :password => @repository.pass
250
+ )
232
251
  else
233
- Typhoeus::Request.new(URI.encode(file.url, URI_UNSAFE_CHARACTERS), :followlocation => true)
252
+ Typhoeus::Request.new(
253
+ URI.escape(file.url),
254
+ :followlocation => true
255
+ )
256
+ end
257
+
258
+ downloaded_file = nil
259
+ async_request.on_headers do |response|
260
+ if response.code != 200
261
+ raise "Downloading file #{response.effective_url} failed with status code #{response.code} --- #{response.inspect}"
262
+ end
263
+ ::FileUtils.mkdir_p(::File.dirname(local_file_path))
264
+ downloaded_file = ::File.new("#{local_file_path}.tmp", "wb")
265
+ @logger.debug("Updating file #{local_file_path}")
266
+ end
267
+
268
+
269
+ async_request.on_body do |chunk, response|
270
+ downloaded_file.write(chunk) if response.code == 200
234
271
  end
272
+
235
273
  async_request.on_complete do |response|
236
- success = false
237
- begin
238
- success = response.code.to_i == 200
239
- if success
240
- write_file!(local_file_path, response.body)
274
+ unless downloaded_file.nil?
275
+ downloaded_file.close unless downloaded_file.closed?
276
+ if response.code == 200
277
+ ::FileUtils.mv "#{local_file_path}.tmp", local_file_path
241
278
  else
242
- body_if_error = response.code >= 500 ? ", body = #{response.body}" : ""
243
- @logger.warn "Failed to update file #{file.url}, got status code = #{response.code}#{body_if_error}"
279
+ ::FileUtils.rm "#{local_file_path}.tmp" if ::File.exist? "#{local_file_path}.tmp"
244
280
  end
245
- rescue SystemCallError, StandardError => e
246
- msg = ["#{e.class} - #{e.message}", *e.backtrace].join("\n\t")
247
- @logger.error "Failed to update file #{file.url}: #{msg}"
248
281
  end
249
- # return the async_request.handled_response value here
250
- success
282
+ true
251
283
  end
252
284
  @hydra.queue async_request
253
285
  @async_requests << async_request
254
- rescue StandardError => e
286
+ true
287
+ rescue RuntimeError, SystemCallError, StandardError => e
255
288
  msg = ["#{e.class} - #{e.message}", *e.backtrace].join("\n\t")
256
289
  @logger.error "Failed to update file #{file.url}: #{msg}"
257
- return false
290
+ unless downloaded_file.nil?
291
+ downloaded_file.close unless downloaded_file.closed?
292
+ ::FileUtils.rm "#{local_file_path}.tmp" if ::File.exist? "#{local_file_path}.tmp"
293
+ end
294
+ false
258
295
  end
259
-
260
- return true
261
296
  end
262
-
297
+
263
298
  def update_hdfs_file!(file, local_file_path)
264
299
  begin
265
300
  ::FileUtils.mkdir_p(::File.dirname(local_file_path))
@@ -268,10 +303,10 @@ module Filbunke
268
303
  url.gsub!(/hdfs:\/\/([^\/]*)(.*)/, "hdfs://\\2")
269
304
  hdfs_cmd = "#{@repository.hadoop_binary} dfs -copyToLocal #{url} #{local_file_path}.tmp"
270
305
  #@logger.debug "Trying to update #{local_file_path} with '#{hdfs_cmd}'"
271
-
306
+
272
307
  pid, stdin, stdout, stderr = Open4::popen4 hdfs_cmd
273
308
  ignored, status = Process::waitpid2 pid
274
-
309
+
275
310
  if status.exitstatus == 0 then
276
311
  begin
277
312
  ::FileUtils.mv "#{local_file_path}.tmp", local_file_path
@@ -280,7 +315,7 @@ module Filbunke
280
315
  msg = ["#{e.class} - #{e.message}", *e.backtrace].join("\n\t")
281
316
  @logger.error "Failed to move hdfs file #{file.url}: #{msg}"
282
317
  return false
283
- end
318
+ end
284
319
  else
285
320
  @logger.error "Failed to update hdfs file #{file.url}! Unable to execute #{hdfs_cmd}"
286
321
  return false
@@ -292,30 +327,11 @@ module Filbunke
292
327
  end
293
328
  end
294
329
 
295
- def write_file!(file_path, contents)
296
- ::FileUtils.mkdir_p(::File.dirname(file_path))
297
- @logger.debug("Updating: #{file_path}")
298
- begin
299
- ::File.open("#{file_path}.tmp", 'w') do |file|
300
- file.write(contents)
301
- file.close
302
- end
303
- ::FileUtils.mv "#{file_path}.tmp", file_path
304
- return true
305
- rescue StandardError => e
306
- msg = ["#{e.class} - #{e.message}", *e.backtrace].join("\n\t")
307
- @logger.error "Failed to move file #{file_path}: #{msg}"
308
- return false
309
- end
310
- end
311
-
312
330
  def delete_file!(file_path)
313
331
  if ::File.exists?(file_path) then
314
332
  @logger.debug("Deleting: #{file_path}")
315
333
  ::File.delete(file_path)
316
334
  end
317
335
  end
318
-
319
336
  end
320
337
  end
321
-
@@ -15,6 +15,7 @@ module Filbunke
15
15
  @logger.log("Initializing repository: #{repository_name}")
16
16
  @clients << begin
17
17
  repository_config["run_every"] = repository_config.fetch("run_every", @config.fetch("run_every", 10))
18
+ repository_config["batch_size"] = repository_config.fetch("batch_size", @config.fetch("batch_size", 0))
18
19
  repository = Repository.new(repository_config)
19
20
  callbacks = []
20
21
  repository_config["callbacks"].each do |callback_name, callback_config|
@@ -11,7 +11,8 @@ module Filbunke
11
11
  :pass,
12
12
  :hadoop_binary,
13
13
  :run_every,
14
- :hydra_concurrency
14
+ :hydra_concurrency,
15
+ :batch_size
15
16
 
16
17
  def initialize(repository_config)
17
18
  @name = repository_config["filbunke_server_repository"]
@@ -25,6 +26,7 @@ module Filbunke
25
26
  @hadoop_binary = repository_config["hadoop_binary"]
26
27
  @run_every = repository_config.fetch("run_every", 10).to_i
27
28
  @hydra_concurrency = repository_config.fetch("hydra_concurrency", 100).to_i
29
+ @batch_size = repository_config.fetch("batch_size", 0).to_i
28
30
  end
29
31
 
30
32
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: filbunke
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.9
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter de Bie
@@ -11,20 +11,20 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2015-11-05 00:00:00.000000000 Z
14
+ date: 2016-03-05 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
- name: thoughtbot-shoulda
17
+ name: shoulda
18
18
  requirement: !ruby/object:Gem::Requirement
19
19
  requirements:
20
- - - ">="
20
+ - - "~>"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '0'
23
23
  type: :development
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
- - - ">="
27
+ - - "~>"
28
28
  - !ruby/object:Gem::Version
29
29
  version: '0'
30
30
  - !ruby/object:Gem::Dependency
@@ -47,14 +47,14 @@ dependencies:
47
47
  requirements:
48
48
  - - '='
49
49
  - !ruby/object:Gem::Version
50
- version: 0.7.3
50
+ version: 1.0.1
51
51
  type: :runtime
52
52
  prerelease: false
53
53
  version_requirements: !ruby/object:Gem::Requirement
54
54
  requirements:
55
55
  - - '='
56
56
  - !ruby/object:Gem::Version
57
- version: 0.7.3
57
+ version: 1.0.1
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: open4
60
60
  requirement: !ruby/object:Gem::Requirement
@@ -147,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
147
  version: '0'
148
148
  requirements: []
149
149
  rubyforge_project:
150
- rubygems_version: 2.5.0
150
+ rubygems_version: 2.4.5.1
151
151
  signing_key:
152
152
  specification_version: 4
153
153
  summary: Filbunke client