flydata 0.2.1 → 0.2.2

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: c9c27e5aaf80b5e422b45d86afd09ce77e50e89a
4
- data.tar.gz: cf978a752a26f2700aefc8882c9240278a75f051
3
+ metadata.gz: 0a3fbdc5a0e42708b1dd6f173c366810e4f58892
4
+ data.tar.gz: 85a647490961e9b85baea5524b9c0f4f395a9d64
5
5
  SHA512:
6
- metadata.gz: bfc17189f1057dc79b2e1e969d005bcc587f28bff69340e444d2e6a11176688a7b26ef3074045b8df212a7b652a471b626d1a36a0e470064a9fdc5f2515e2170
7
- data.tar.gz: 4578998bd6fb444acc86acb82ae141396f279661d4c5aacb4592c67540951eb5651212566ba3fd61f6259c4b0201ca9e16f4c9c64d98ab981557bd46e25389d6
6
+ metadata.gz: 200c29e2a64610cd4561ae127365f934b9cef0ef9dad264fed54d191bc3ec779eb22fe507d4d55c6373928a560c807dafa0cc0883243b0865723a62649349d40
7
+ data.tar.gz: 2a5b4a1e246b448e6fffe76c6fff8416298d22c4e46719109d6d9c2fca99c3f2ddb1cc3e70cb5b5bf9afbac388c3048b1b840c05952bcc0ab9e14a53c89b901f
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1
1
+ 0.2.2
data/bin/serverinfo ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+ detect_system() {
3
+ # refer to https://github.com/wayneeseguin/rvm/blob/master/scripts/functions/utility_system
4
+ unset _system_info _system_lib_type
5
+ export _system_info _system_lib_type
6
+ _system_info="$(command uname -a)"
7
+ _system_lib_type="unknown"
8
+ case "$(command uname)" in
9
+ (Linux|GNU*)
10
+ if [[ -f /etc/lsb-release ]]; then
11
+ _system_lib_type="debian"
12
+ elif [[ -f /etc/debian_version ]]; then
13
+ _system_lib_type="debian"
14
+ elif [[ -f /etc/os-release ]]; then
15
+ _system_lib_type="debian"
16
+ elif [[ -f /etc/system-release ]]; then
17
+ _system_lib_type="centos"
18
+ elif [[ -f /etc/centos-release ]]; then
19
+ _system_lib_type="centos"
20
+ elif [[ -f /etc/redhat-release ]]; then
21
+ _system_lib_type="centos"
22
+ fi
23
+ ;;
24
+ #(Darwin)
25
+ # _system_lib_type="osx"
26
+ # ;;
27
+ (*)
28
+ ;;
29
+ esac
30
+ }
31
+
32
+ log_server_details()
33
+ {
34
+ echo "Logging ip address"
35
+ ip addr show
36
+
37
+ echo "Logging ulimit and uname"
38
+ ulimit -a
39
+ uname -a
40
+
41
+ echo "Logging repo details"
42
+ detect_system
43
+ if [ "${_system_lib_type}" = "debian" ]; then
44
+ grep -RoPish --include="*.list" "(?<=^deb\s).*?(?=#|$)" /etc/apt
45
+ else
46
+ yum repolist all
47
+ fi
48
+
49
+ echo "Logging release details"
50
+ cat /etc/*-release
51
+ }
52
+
53
+ log_server_details
data/flydata.gemspec CHANGED
@@ -2,19 +2,19 @@
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: flydata 0.2.1 ruby lib
5
+ # stub: flydata 0.2.2 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "flydata"
9
- s.version = "0.2.1"
9
+ s.version = "0.2.2"
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 = ["Koichi Fujikawa"]
14
- s.date = "2014-08-20"
14
+ s.date = "2014-09-12"
15
15
  s.description = "FlyData Command Line Interface"
16
16
  s.email = "sysadmin@flydata.co"
17
- s.executables = ["fdmysqldump", "flydata"]
17
+ s.executables = ["fdmysqldump", "flydata", "serverinfo"]
18
18
  s.files = [
19
19
  ".gitignore",
20
20
  ".rspec",
@@ -24,6 +24,7 @@ Gem::Specification.new do |s|
24
24
  "VERSION",
25
25
  "bin/fdmysqldump",
26
26
  "bin/flydata",
27
+ "bin/serverinfo",
27
28
  "flydata.gemspec",
28
29
  "lib/fly_data_model.rb",
29
30
  "lib/flydata.rb",
data/lib/flydata/cli.rb CHANGED
@@ -33,6 +33,7 @@ module Flydata
33
33
  puts
34
34
  print_usage
35
35
  raise e if FLYDATA_DEBUG
36
+ exit 1
36
37
  end
37
38
  end
38
39
 
@@ -6,7 +6,7 @@ module Flydata
6
6
  on 'n', 'no-daemon', 'Start FlyData agent as a regular program'
7
7
  end
8
8
  end
9
- def start(options_or_show_final_message = {show_final_message: true}) # For backward compatibility. Use only as options going forward
9
+ def start(options_or_show_final_message = {show_final_message: true}) # For backward compatibility. Use only as options going forward
10
10
  if options_or_show_final_message.kind_of? Hash
11
11
  options = options_or_show_final_message
12
12
  else
@@ -23,6 +23,7 @@ module Flydata
23
23
  # Start sender(fluentd) process
24
24
  say('Starting sender process.') unless options[:quiet]
25
25
  Dir.chdir(FLYDATA_HOME){
26
+ Kernel.system("bash #{File.dirname(__FILE__)}/../../../bin/serverinfo", :out => ["#{FLYDATA_HOME}/flydata.log",'a'], :err => ["#{FLYDATA_HOME}/flydata.log",'a'])
26
27
  daemon_option = opts.no_daemon? ? "" : "-d #{FLYDATA_HOME}/flydata.pid"
27
28
  Kernel.system("fluentd #{daemon_option} -l #{FLYDATA_HOME}/flydata.log -c #{FLYDATA_HOME}/flydata.conf -p #{File.dirname(__FILE__)}/../fluent-plugins")
28
29
  }
@@ -35,16 +35,10 @@ module Flydata
35
35
  end
36
36
  exit 1
37
37
  end
38
- de = retrieve_data_entries.first
39
- raise "There are no data entry." unless de
40
- case de['type']
41
- when 'RedshiftMysqlDataEntry'
42
- de = load_sync_info(override_tables(de, tables))
43
- flush_buffer_and_stop unless de['mysql_data_entry_preference']['initial_sync']
44
- sync_mysql_to_redshift(de)
45
- else
46
- raise "No supported data entry. Only mysql-redshift sync is supported."
47
- end
38
+ de = retrieve_data_entry
39
+ de = load_sync_info(override_tables(de, tables))
40
+ flush_buffer_and_stop unless de['mysql_data_entry_preference']['initial_sync']
41
+ sync_mysql_to_redshift(de)
48
42
  end
49
43
 
50
44
  def flush
@@ -65,7 +59,7 @@ module Flydata
65
59
  sender.flush_client_buffer # TODO We should rather delete buffer files
66
60
  sender.stop
67
61
 
68
- de = retrieve_data_entries.first
62
+ de = retrieve_data_entry
69
63
  wait_for_server_buffer
70
64
  cleanup_sync_server(de, tables) unless opts.client?
71
65
  sync_fm = Flydata::FileUtil::SyncFileManager.new(de)
@@ -114,7 +108,7 @@ module Flydata
114
108
  end
115
109
 
116
110
  def check
117
- de = retrieve_data_entries.first
111
+ de = retrieve_data_entry
118
112
  retry_on(RestClient::Exception) do
119
113
  status = do_check(de)
120
114
  if status['complete']
@@ -127,7 +121,7 @@ module Flydata
127
121
 
128
122
  # skip initial sync
129
123
  def skip
130
- de = retrieve_data_entries.first
124
+ de = retrieve_data_entry
131
125
  sync_fm = Flydata::FileUtil::SyncFileManager.new(de)
132
126
  binlog_path = sync_fm.binlog_path
133
127
  `touch #{binlog_path}`
@@ -137,20 +131,28 @@ module Flydata
137
131
  end
138
132
 
139
133
  def generate_table_ddl(*tables)
140
- de = retrieve_data_entries.first
134
+ de = retrieve_data_entry
141
135
  Flydata::Mysql::CompatibilityCheck.new(de['mysql_data_entry_preference']).check
136
+ do_generate_table_ddl(override_tables(de, tables))
137
+ end
142
138
 
139
+ private
140
+
141
+ def retrieve_data_entry
142
+ de = retrieve_data_entries.first
143
143
  raise "There are no data entry." unless de
144
144
  case de['type']
145
145
  when 'RedshiftMysqlDataEntry'
146
- do_generate_table_ddl(override_tables(de, tables))
146
+ mp = de['mysql_data_entry_preference']
147
+ if mp['tables_append_only']
148
+ mp['tables'] = (mp['tables'].split(",") + mp['tables_append_only'].split(",")).uniq.join(",")
149
+ end
147
150
  else
148
151
  raise "No supported data entry. Only mysql-redshift sync is supported."
149
152
  end
153
+ de
150
154
  end
151
155
 
152
- private
153
-
154
156
  def cleanup_sync_server(de, tables = [])
155
157
  puts "Cleaning the server"
156
158
  flydata.data_entry.cleanup_sync(de['id'], tables)
@@ -412,7 +414,7 @@ What's next?
412
414
  Thank you for using FlyData!
413
415
  EOM
414
416
  def complete
415
- de = load_sync_info(retrieve_data_entries.first)
417
+ de = load_sync_info(retrieve_data_entry)
416
418
  sync_fm = Flydata::FileUtil::SyncFileManager.new(de)
417
419
  info = sync_fm.load_dump_pos
418
420
  if info[:status] == STATUS_COMPLETE
@@ -47,6 +47,7 @@ class MysqlBinlogFlydataInput < MysqlBinlogInput
47
47
 
48
48
  config_param :database, :string
49
49
  config_param :tables, :string
50
+ config_param :tables_append_only, :string
50
51
 
51
52
  def configure(conf)
52
53
  super
@@ -55,13 +56,18 @@ class MysqlBinlogFlydataInput < MysqlBinlogInput
55
56
  raise "No position file(#{@position_file}). Initial synchronization is required before starting."
56
57
  end
57
58
  load_custom_conf
58
- $log.info "mysql host:\"#{@host}\" username:\"#{@username}\" database:\"#{@database}\" tables:\"#{@tables}\""
59
+ $log.info "mysql host:\"#{@host}\" username:\"#{@username}\" database:\"#{@database}\" tables:\"#{@tables}\" tables_append_only:\"#{tables_append_only}\""
59
60
  @tables = @tables.split(/,\s*/)
61
+ @omit_events = Hash.new
62
+ @tables_append_only.split(/,\s*/).each do |table|
63
+ @tables << table unless @tables.include?(table)
64
+ @omit_events[table] = [:delete]
65
+ end
60
66
  sync_fm = Flydata::FileUtil::SyncFileManager.new(nil) # Passing nil for data_entry as this class does not use methods which require data_entry
61
67
 
62
68
  @context = Mysql::Context.new(
63
69
  database: @database, tables: @tables,
64
- tag: @tag, sync_fm: sync_fm
70
+ tag: @tag, sync_fm: sync_fm, omit_events: @omit_events
65
71
  )
66
72
  @record_dispatcher = Mysql::FlydataBinlogRecordDispatcher.new(@context)
67
73
  end
@@ -50,6 +50,10 @@ module Mysql
50
50
  acceptable
51
51
  end
52
52
 
53
+ def acceptable_event?(type, table)
54
+ @context.omit_events[table].nil? || !@context.omit_events[table].include?(type)
55
+ end
56
+
53
57
  def emit_record(type, record, opt = {})
54
58
  return unless acceptable_db?(record)
55
59
  return unless record["table_name"].nil? or acceptable_table?(record, record["table_name"])
@@ -62,7 +66,7 @@ module Mysql
62
66
 
63
67
  table = records.first[TABLE_NAME] || record['table_name']
64
68
  raise "Missing table name. #{record}" if table.to_s.empty?
65
- return unless acceptable_table?(record, table)
69
+ return unless acceptable_table?(record, table) && acceptable_event?(type, table)
66
70
 
67
71
  table_rev = @context.sync_fm.table_rev(table)
68
72
  position = record['next_position'] - record['event_length']
@@ -1,10 +1,10 @@
1
1
  module Mysql
2
2
  class Context
3
3
  MANDATORY_OPTS = [
4
- :database, :tables, :tag, :sync_fm,
4
+ :database, :tables, :tag, :sync_fm, :omit_events
5
5
  ]
6
6
  OPTIONAL_OPTS = [
7
- :current_binlog_file,
7
+ :current_binlog_file
8
8
  ]
9
9
 
10
10
  (MANDATORY_OPTS + OPTIONAL_OPTS).each do |opt|
@@ -72,6 +72,7 @@ module Fluent
72
72
  mysql_data_entry_preference: {
73
73
  database: {},
74
74
  tables: {},
75
+ tables_append_only: {},
75
76
  host: {},
76
77
  username: {},
77
78
  password: {encrypted: true},
@@ -20,7 +20,7 @@ module Flydata
20
20
  expect(a.to_hash).to be_empty
21
21
  }.and_return(command_obj)
22
22
  expect(command_obj).to receive(:run)
23
-
23
+
24
24
  subject.run
25
25
  end
26
26
  end
@@ -30,7 +30,7 @@ module Flydata
30
30
  expect(subject).to receive(:puts).with('! Unknown options -a, --host-name').once
31
31
  allow(subject).to receive(:puts)
32
32
 
33
- subject.run
33
+ expect{ subject.run }.to terminate.with_code(1)
34
34
  end
35
35
  end
36
36
  context 'with arguments' do
@@ -65,7 +65,7 @@ module Flydata
65
65
  expect(subject).to receive(:puts).with('! Unknown options -a, --host-name').once
66
66
  allow(subject).to receive(:puts)
67
67
 
68
- subject.run
68
+ expect{ subject.run }.to terminate.with_code(1)
69
69
  end
70
70
  end
71
71
  end
@@ -21,6 +21,8 @@ module Flydata
21
21
  let(:args) { [] }
22
22
  it "starts fluend with daemon option" do
23
23
  expect(Kernel).to receive(:system).with( Regexp.new(
24
+ "bash .+/../../../bin/serverinfo"), :out => [Regexp.new(".+/flydata.log"),'a'], :err => [Regexp.new(".+/flydata.log"),'a'])
25
+ expect(Kernel).to receive(:system).with( Regexp.new(
24
26
  "fluentd -d .+/flydata.pid -l .+/flydata.log -c .+/flydata.conf -p .+/\.\./fluent-plugins"))
25
27
  subject.start(false)
26
28
  end
@@ -28,6 +30,8 @@ module Flydata
28
30
  context "as regular process" do
29
31
  let(:args) { ["-n"] }
30
32
  it "starts fluentd with no daemon option" do
33
+ expect(Kernel).to receive(:system).with( Regexp.new(
34
+ "bash .+/../../../bin/serverinfo"), :out => [Regexp.new(".+/flydata.log"),'a'], :err => [Regexp.new(".+/flydata.log"),'a'])
31
35
  expect(Kernel).to receive(:system).with(Regexp.new(
32
36
  "fluentd -l .+/flydata.log -c .+/flydata.conf -p .+/\.\./fluent-plugins"))
33
37
  subject.start(false)
@@ -36,6 +40,8 @@ module Flydata
36
40
  context "as regular process with long option" do
37
41
  let(:args) { ["--no-daemon"] }
38
42
  it "starts fluentd with no daemon option" do
43
+ expect(Kernel).to receive(:system).with( Regexp.new(
44
+ "bash .+/../../../bin/serverinfo"), :out => [Regexp.new(".+/flydata.log"),'a'], :err => [Regexp.new(".+/flydata.log"),'a'])
39
45
  expect(Kernel).to receive(:system).with(Regexp.new(
40
46
  "fluentd -l .+/flydata.log -c .+/flydata.conf -p .+/\.\./fluent-plugins"))
41
47
  subject.start(false)
@@ -18,10 +18,27 @@ module Fluent
18
18
  TEST_POSITION_FILE = "test_position.log"
19
19
  TEST_REVISION_FILE = File.join(FLYDATA_HOME, "positions/#{TEST_TABLE}.rev")
20
20
  TEST_TIMESTAMP = 1389214083
21
+ TEST_TABLE_APPEND_ONLY = "test_table_4"
22
+ TEST_SEQUENCE_FILE_1 = File.join(FLYDATA_HOME, "positions/#{TEST_TABLE_APPEND_ONLY}.pos")
21
23
  TEST_CONFIG = <<EOT
22
24
  tag #{TEST_TAG}
23
25
  database #{TEST_DB}
24
26
  tables #{TEST_TABLES}
27
+ tables_append_only
28
+ position_file #{TEST_POSITION_FILE}
29
+ EOT
30
+ TEST_TABLES_APPEND_ONLY_CONFIG = <<EOT
31
+ tag #{TEST_TAG}
32
+ database #{TEST_DB}
33
+ tables #{TEST_TABLES}
34
+ tables_append_only #{TEST_TABLE_APPEND_ONLY}
35
+ position_file #{TEST_POSITION_FILE}
36
+ EOT
37
+ TEST_TABLES_DUPLICATE_CONFIG = <<EOT
38
+ tag #{TEST_TAG}
39
+ database #{TEST_DB}
40
+ tables #{TEST_TABLES},#{TEST_TABLE_APPEND_ONLY}
41
+ tables_append_only #{TEST_TABLE_APPEND_ONLY}
25
42
  position_file #{TEST_POSITION_FILE}
26
43
  EOT
27
44
 
@@ -189,11 +206,13 @@ EOT
189
206
  def setup_initial_flydata_files
190
207
  %w(positions dump conf).each{|f| FileUtils.mkdir_p(File.join(FLYDATA_HOME, f))}
191
208
  create_file(TEST_SEQUENCE_FILE, TEST_SEQUENCE_NUM.to_s)
209
+ create_file(TEST_SEQUENCE_FILE_1, TEST_SEQUENCE_NUM.to_s)
192
210
  end
193
211
 
194
212
  def cleanup_flydata_files
195
213
  %w(positions dump conf).each{|f| FileUtils.rm_rf(File.join(FLYDATA_HOME, f))}
196
214
  delete_file(TEST_SEQUENCE_FILE)
215
+ delete_file(TEST_SEQUENCE_FILE_1)
197
216
  end
198
217
 
199
218
  before do
@@ -409,6 +428,56 @@ EOT
409
428
  expect_no_emitted_record(event)
410
429
  end
411
430
  end
431
+
432
+ context 'for append only' do
433
+ shared_examples 'emits records correctly' do
434
+ it 'emits records when it receives an insert event for append only table' do
435
+ event = insert_event
436
+ event['table_name'] = TEST_TABLE_APPEND_ONLY
437
+ expect_emitted_records_with_rows(event, :insert, TEST_TABLE_APPEND_ONLY, 628, "mysql-bin.000048",
438
+ [{"1"=>"0SL00000001", "2"=>"foo"}, {"1"=>"0SL00000002", "2"=>"var"}, {"1"=>"0SL00000003", "2"=>"hoge"}])
439
+ end
440
+
441
+ it 'does not emit a record when it receives a delete event for append only table' do
442
+ event = delete_event
443
+ event['table_name'] = TEST_TABLE_APPEND_ONLY
444
+ expect_no_emitted_record(event)
445
+ end
446
+
447
+ it 'emits records when it receives an update event for append only table' do
448
+ event = update_event
449
+ event['table_name'] = TEST_TABLE_APPEND_ONLY
450
+ expect_emitted_records_with_rows(event, :update, TEST_TABLE_APPEND_ONLY, 2528, "mysql-bin.000048",
451
+ [{"1"=>"0SL00000001", "2"=>"wow"}, {"1"=>"0SL00000003", "2"=>"fuga"}])
452
+ end
453
+
454
+ it 'emits a record when it receives a delete event for non-append only table' do
455
+ expect_emitted_records_with_rows(delete_event, :delete, TEST_TABLE, 5324, "mysql-bin.000048",
456
+ [{"1"=>"0SL00000002", "2"=>"var"}, {"1"=>"0SL00000003", "2"=>"hoge"}])
457
+ end
458
+
459
+ it 'emits records when it receives an insert event for non-append only table' do
460
+ expect_emitted_records_with_rows(insert_event, :insert, TEST_TABLE, 628, "mysql-bin.000048",
461
+ [{"1"=>"0SL00000001", "2"=>"foo"}, {"1"=>"0SL00000002", "2"=>"var"}, {"1"=>"0SL00000003", "2"=>"hoge"}])
462
+ end
463
+ end
464
+
465
+ context 'no duplicate entries in tables and tables_append_only' do
466
+ before do
467
+ Test.configure_plugin(plugin, TEST_TABLES_APPEND_ONLY_CONFIG)
468
+ plugin.event_listener(rotate_event)
469
+ end
470
+ include_examples 'emits records correctly'
471
+ end
472
+
473
+ context 'duplicate entries in tables and tables_append_only' do
474
+ before do
475
+ Test.configure_plugin(plugin, TEST_TABLES_DUPLICATE_CONFIG)
476
+ plugin.event_listener(rotate_event)
477
+ end
478
+ include_examples 'emits records correctly'
479
+ end
480
+ end
412
481
  end
413
482
  end
414
483
 
data/spec/spec_helper.rb CHANGED
@@ -33,5 +33,43 @@ RSpec.configure do |config|
33
33
  end
34
34
  end
35
35
  end
36
+ end
37
+
38
+ # https://gist.github.com/stevenharman/2355172
39
+ RSpec::Matchers.define :terminate do |code|
40
+ actual = nil
41
+
42
+ def supports_block_expectations?
43
+ true
44
+ end
45
+
46
+ match do |block|
47
+ begin
48
+ block.call
49
+ rescue SystemExit => e
50
+ actual = e.status
51
+ end
52
+ actual and actual == status_code
53
+ end
54
+
55
+ chain :with_code do |status_code|
56
+ @status_code = status_code
57
+ end
58
+
59
+ failure_message_for_should do |block|
60
+ "expected block to call exit(#{status_code}) but exit" +
61
+ (actual.nil? ? " not called" : "(#{actual}) was called")
62
+ end
63
+
64
+ failure_message_for_should_not do |block|
65
+ "expected block not to call exit(#{status_code})"
66
+ end
67
+
68
+ description do
69
+ "expect block to call exit(#{status_code})"
70
+ end
36
71
 
72
+ def status_code
73
+ @status_code ||= 0
74
+ end
37
75
  end
@@ -8,4 +8,5 @@ mysql_data_entry_preference:
8
8
  #password: abcd
9
9
  #database: dev
10
10
  #tables: users,country,rows
11
+ #tables_append_only: employees
11
12
  #mysqldump_dir: /mnt/dump
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flydata
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Koichi Fujikawa
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-20 00:00:00.000000000 Z
11
+ date: 2014-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rest-client
@@ -377,6 +377,7 @@ email: sysadmin@flydata.co
377
377
  executables:
378
378
  - fdmysqldump
379
379
  - flydata
380
+ - serverinfo
380
381
  extensions: []
381
382
  extra_rdoc_files: []
382
383
  files:
@@ -388,6 +389,7 @@ files:
388
389
  - VERSION
389
390
  - bin/fdmysqldump
390
391
  - bin/flydata
392
+ - bin/serverinfo
391
393
  - flydata.gemspec
392
394
  - lib/fly_data_model.rb
393
395
  - lib/flydata.rb