remi 0.2.42 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +7 -0
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +13 -26
  5. data/README.md +1 -1
  6. data/features/step_definitions/remi_step.rb +33 -13
  7. data/features/sub_job_example.feature +24 -0
  8. data/features/sub_transform_example.feature +35 -0
  9. data/features/sub_transform_many_to_many.feature +49 -0
  10. data/features/support/env_app.rb +1 -1
  11. data/jobs/all_jobs_shared.rb +19 -16
  12. data/jobs/copy_source_job.rb +11 -9
  13. data/jobs/csv_file_target_job.rb +10 -9
  14. data/jobs/json_job.rb +18 -14
  15. data/jobs/metadata_job.rb +33 -28
  16. data/jobs/parameters_job.rb +14 -11
  17. data/jobs/sample_job.rb +106 -77
  18. data/jobs/sftp_file_target_job.rb +14 -13
  19. data/jobs/sub_job_example_job.rb +86 -0
  20. data/jobs/sub_transform_example_job.rb +43 -0
  21. data/jobs/sub_transform_many_to_many_job.rb +46 -0
  22. data/jobs/transforms/concatenate_job.rb +16 -12
  23. data/jobs/transforms/data_frame_sieve_job.rb +24 -19
  24. data/jobs/transforms/date_diff_job.rb +15 -11
  25. data/jobs/transforms/nvl_job.rb +16 -12
  26. data/jobs/transforms/parse_date_job.rb +17 -14
  27. data/jobs/transforms/partitioner_job.rb +27 -19
  28. data/jobs/transforms/prefix_job.rb +13 -10
  29. data/jobs/transforms/truncate_job.rb +14 -10
  30. data/jobs/transforms/truthy_job.rb +11 -8
  31. data/lib/remi.rb +25 -11
  32. data/lib/remi/data_frame.rb +4 -4
  33. data/lib/remi/data_frame/daru.rb +1 -37
  34. data/lib/remi/data_subject.rb +234 -48
  35. data/lib/remi/data_subjects/csv_file.rb +171 -0
  36. data/lib/remi/data_subjects/data_frame.rb +106 -0
  37. data/lib/remi/data_subjects/file_system.rb +115 -0
  38. data/lib/remi/data_subjects/local_file.rb +109 -0
  39. data/lib/remi/data_subjects/none.rb +31 -0
  40. data/lib/remi/data_subjects/postgres.rb +186 -0
  41. data/lib/remi/data_subjects/s3_file.rb +84 -0
  42. data/lib/remi/data_subjects/salesforce.rb +211 -0
  43. data/lib/remi/data_subjects/sftp_file.rb +196 -0
  44. data/lib/remi/data_subjects/sub_job.rb +50 -0
  45. data/lib/remi/dsl.rb +74 -0
  46. data/lib/remi/encoder.rb +45 -0
  47. data/lib/remi/extractor.rb +21 -0
  48. data/lib/remi/field_symbolizers.rb +1 -0
  49. data/lib/remi/job.rb +279 -113
  50. data/lib/remi/job/parameters.rb +90 -0
  51. data/lib/remi/job/sub_job.rb +35 -0
  52. data/lib/remi/job/transform.rb +165 -0
  53. data/lib/remi/loader.rb +22 -0
  54. data/lib/remi/monkeys/daru.rb +4 -0
  55. data/lib/remi/parser.rb +44 -0
  56. data/lib/remi/testing/business_rules.rb +17 -23
  57. data/lib/remi/testing/data_stub.rb +2 -2
  58. data/lib/remi/version.rb +1 -1
  59. data/remi.gemspec +3 -0
  60. data/spec/data_subject_spec.rb +475 -11
  61. data/spec/data_subjects/csv_file_spec.rb +69 -0
  62. data/spec/data_subjects/data_frame_spec.rb +52 -0
  63. data/spec/{extractor → data_subjects}/file_system_spec.rb +0 -0
  64. data/spec/{extractor → data_subjects}/local_file_spec.rb +0 -0
  65. data/spec/data_subjects/none_spec.rb +41 -0
  66. data/spec/data_subjects/postgres_spec.rb +80 -0
  67. data/spec/{extractor → data_subjects}/s3_file_spec.rb +0 -0
  68. data/spec/data_subjects/salesforce_spec.rb +117 -0
  69. data/spec/{extractor → data_subjects}/sftp_file_spec.rb +16 -0
  70. data/spec/data_subjects/sub_job_spec.rb +33 -0
  71. data/spec/encoder_spec.rb +38 -0
  72. data/spec/extractor_spec.rb +11 -0
  73. data/spec/fixtures/sf_bulk_helper_stubs.rb +443 -0
  74. data/spec/job/transform_spec.rb +257 -0
  75. data/spec/job_spec.rb +507 -0
  76. data/spec/loader_spec.rb +11 -0
  77. data/spec/parser_spec.rb +38 -0
  78. data/spec/sf_bulk_helper_spec.rb +117 -0
  79. data/spec/testing/data_stub_spec.rb +5 -3
  80. metadata +109 -27
  81. data/features/aggregate.feature +0 -42
  82. data/jobs/aggregate_job.rb +0 -31
  83. data/jobs/transforms/transform_jobs.rb +0 -4
  84. data/lib/remi/data_subject/csv_file.rb +0 -162
  85. data/lib/remi/data_subject/data_frame.rb +0 -52
  86. data/lib/remi/data_subject/postgres.rb +0 -134
  87. data/lib/remi/data_subject/salesforce.rb +0 -136
  88. data/lib/remi/data_subject/sftp_file.rb +0 -65
  89. data/lib/remi/extractor/file_system.rb +0 -92
  90. data/lib/remi/extractor/local_file.rb +0 -43
  91. data/lib/remi/extractor/s3_file.rb +0 -57
  92. data/lib/remi/extractor/sftp_file.rb +0 -83
  93. data/spec/data_subject/csv_file_spec.rb +0 -79
  94. data/spec/data_subject/data_frame.rb +0 -27
@@ -1,21 +1,25 @@
1
1
  require_relative 'all_jobs_shared'
2
2
 
3
- class JsonJob
4
- include AllJobsShared
5
-
6
- define_source :source_data, Remi::DataSource::DataFrame,
7
- fields: {
8
- :json_array => { type: :json },
9
- :json_hash => { type: :json }
10
- }
3
+ class JsonJob < Remi::Job
4
+ source :source_data do
5
+ fields(
6
+ {
7
+ :json_array => { type: :json },
8
+ :json_hash => { type: :json }
9
+ }
10
+ )
11
+ end
11
12
 
12
- define_target :target_data, Remi::DataTarget::DataFrame,
13
- fields: {
14
- :second_element => {},
15
- :name_field => {}
16
- }
13
+ target :target_data do
14
+ fields(
15
+ {
16
+ :second_element => {},
17
+ :name_field => {}
18
+ }
19
+ )
20
+ end
17
21
 
18
- define_transform :main do
22
+ transform :main do
19
23
  Remi::SourceToTargetMap.apply(source_data.df, target_data.df, source_metadata: source_data.fields) do
20
24
  map source(:json_array) .target(:second_element)
21
25
  .transform(->(values) { values[1] })
@@ -1,37 +1,42 @@
1
1
  require_relative 'all_jobs_shared'
2
2
  ENV['TZ'] = 'UTC'
3
3
 
4
- class MetadataJob
5
- include AllJobsShared
4
+ class MetadataJob < Remi::Job
5
+ source :source_data do
6
+ fields(
7
+ {
8
+ :activity_id => { from: 'in', in: true, cdc_type: 2 },
9
+ :student_id => { from: 'in', in: true, type: :string, cdc_type: 2 },
10
+ :student_dob => { from: 'in', in: true, type: :date, in_format: '%m/%d/%Y', out_format: '%Y-%m-%d', cdc_type: 2 },
11
+ :activity_type => { from: 'in', in: true, type: :string, valid_values: ['A', 'B', 'C'], cdc_type: 2 },
12
+ :activity_counter => { from: 'in', in: true, type: :integer, cdc_type: 2 },
13
+ :activity_score => { from: 'in', in: true, type: :float, cdc_type: 2 },
14
+ :activity_cost => { from: 'in', in: true, type: :decimal, precision: 8, scale: 2, cdc_type: 2 },
15
+ :activity_date => { from: 'in', in: true, type: :datetime, in_format: '%m/%d/%Y %H:%M:%S', out_format: '%Y-%m-%dT%H:%M:%S', cdc_type: 2 },
16
+ :source_filename => { from: 'in', in: true, type: :string, cdc_type: 1 }
17
+ }
18
+ )
19
+ end
6
20
 
7
- define_source :source_data, Remi::DataSource::DataFrame,
8
- fields: {
9
- :activity_id => { from: 'in', in: true, cdc_type: 2 },
10
- :student_id => { from: 'in', in: true, type: :string, cdc_type: 2 },
11
- :student_dob => { from: 'in', in: true, type: :date, in_format: '%m/%d/%Y', out_format: '%Y-%m-%d', cdc_type: 2 },
12
- :activity_type => { from: 'in', in: true, type: :string, valid_values: ['A', 'B', 'C'], cdc_type: 2 },
13
- :activity_counter => { from: 'in', in: true, type: :integer, cdc_type: 2 },
14
- :activity_score => { from: 'in', in: true, type: :float, cdc_type: 2 },
15
- :activity_cost => { from: 'in', in: true, type: :decimal, precision: 8, scale: 2, cdc_type: 2 },
16
- :activity_date => { from: 'in', in: true, type: :datetime, in_format: '%m/%d/%Y %H:%M:%S', out_format: '%Y-%m-%dT%H:%M:%S', cdc_type: 2 },
17
- :source_filename => { from: 'in', in: true, type: :string, cdc_type: 1 }
18
- }
21
+ target :target_data do
22
+ encoder Remi::Encoder::CsvFile.new path: "#{Remi::Settings.work_dir}/target_data.csv"
19
23
 
20
- define_target :target_data, Remi::DataTarget::CsvFile,
21
- path: "#{Remi::Settings.work_dir}/target_data.csv",
22
- fields: {
23
- :activity_id => { from: 'out', out: true },
24
- :student_id => { from: 'out', out: true, type: :string },
25
- :student_dob => { from: 'out', out: true, type: :date, in_format: '%m/%d/%Y', out_format: '%Y-%m-%d' },
26
- :activity_type => { from: 'out', out: true, type: :string, valid_values: ['A', 'B', 'C'] },
27
- :activity_counter => { from: 'out', out: true, type: :integer },
28
- :activity_score => { from: 'out', out: true, type: :float },
29
- :activity_cost => { from: 'out', out: true, type: :decimal, precision: 8, scale: 2 },
30
- :activity_date => { from: 'out', out: true, type: :datetime, in_format: '%m/%d/%Y %H:%M:%S', out_format: '%Y-%m-%dT%H:%M:%S' },
31
- :source_filename => { from: 'out', out: true, type: :string, cdc_type: 1 }
32
- }
24
+ fields(
25
+ {
26
+ :activity_id => { from: 'out', out: true },
27
+ :student_id => { from: 'out', out: true, type: :string },
28
+ :student_dob => { from: 'out', out: true, type: :date, in_format: '%m/%d/%Y', out_format: '%Y-%m-%d' },
29
+ :activity_type => { from: 'out', out: true, type: :string, valid_values: ['A', 'B', 'C'] },
30
+ :activity_counter => { from: 'out', out: true, type: :integer },
31
+ :activity_score => { from: 'out', out: true, type: :float },
32
+ :activity_cost => { from: 'out', out: true, type: :decimal, precision: 8, scale: 2 },
33
+ :activity_date => { from: 'out', out: true, type: :datetime, in_format: '%m/%d/%Y %H:%M:%S', out_format: '%Y-%m-%dT%H:%M:%S' },
34
+ :source_filename => { from: 'out', out: true, type: :string, cdc_type: 1 }
35
+ }
36
+ )
37
+ end
33
38
 
34
- define_transform :main do
39
+ transform :main do
35
40
  source_data.enforce_types
36
41
 
37
42
  Remi::SourceToTargetMap.apply(source_data.df, target_data.df, source_metadata: source_data.fields, target_metadata: target_data.fields) do
@@ -1,22 +1,25 @@
1
1
  require_relative 'all_jobs_shared'
2
2
 
3
- class ParametersJob
4
- include AllJobsShared
3
+ class ParametersJob < Remi::Job
4
+ param(:myparam) {}
5
+ param(:test_parameter) { "my test parameter value" }
5
6
 
6
- define_param :test_parameter, "my test parameter value"
7
+ source :source_data do
8
+ fields(
9
+ {
10
+ :parameter_name => {}
11
+ }
12
+ )
13
+ end
7
14
 
8
- define_target :source_data, Remi::DataSource::DataFrame,
9
- fields: {
10
- :parameter_name => {}
11
- }
12
- define_target :target_data, Remi::DataTarget::DataFrame
15
+ target :target_data
13
16
 
14
- define_transform :main do
17
+ transform :main do
15
18
  Remi::SourceToTargetMap.apply(source_data.df, target_data.df) do
16
19
  map target(:myparam)
17
- .transform(Remi::Transform::Constant.new(params[:myparam]))
20
+ .transform(Remi::Transform::Constant.new(job.params[:myparam]))
18
21
  map source(:parameter_name) .target(:parameter_name)
19
- .transform(->(v) { params[v.to_sym] })
22
+ .transform(->(v) { job.params[v.to_sym] })
20
23
  end
21
24
  end
22
25
 
@@ -1,88 +1,118 @@
1
1
  # This is an example Remi job that was auto-generated by Remi.
2
2
  require_relative 'all_jobs_shared'
3
- require 'remi/data_subject/salesforce'
4
-
5
- class SampleJob
6
- include AllJobsShared
7
-
8
- define_source :existing_contacts, Remi::DataSource::Salesforce,
9
- object: :Contact,
10
- credentials: params[:salesforce_credentials],
11
- api: :bulk,
12
- fields: {
13
- :Id => {},
14
- :External_ID__c => {},
15
- :IsActive => { type: :boolean },
16
- :CreatedDate => { type: :date, in_format: '%Y-%m-%d %H:%M:%S' }
17
- },
18
- query: <<-EOQ
19
- SELECT
20
- Id,
21
- External_ID__c
22
- FROM
23
- Contact
24
- EOQ
25
-
26
-
27
- define_source :sample_file, Remi::DataSource::CsvFile,
28
- extractor: Remi::Extractor::SftpFile.new(
3
+ require 'remi/data_subjects/salesforce'
4
+
5
+ class SampleJob < AllJobsShared
6
+
7
+ param :program_name_lookup do
8
+ RegexSieve.new(
9
+ {
10
+ /^BIO$/ => "Biology",
11
+ /^Fake Biology$/ => nil,
12
+ /(?:B|Microb)iology/ => "Biology",
13
+ /^CHEM$/ => "Chemistry",
14
+ /Chemistry/ => "Chemistry",
15
+ /Physics/ => "Physics"
16
+ }
17
+ )
18
+ end
19
+
20
+ source :existing_contacts do
21
+ extractor Remi::Extractor::Salesforce.new(
22
+ object: :Contact,
23
+ credentials: params[:salesforce_credentials],
24
+ api: :bulk,
25
+ query: <<-EOQ
26
+ SELECT
27
+ Id,
28
+ External_ID__c,
29
+ IsActive,
30
+ CreatedDate
31
+ FROM
32
+ Contact
33
+ EOQ
34
+ )
35
+ parser Remi::Parser::Salesforce.new
36
+
37
+ field_symbolizer :salesforce
38
+ fields(
39
+ {
40
+ :Id => {},
41
+ :External_ID__c => {},
42
+ :IsActive => { type: :boolean },
43
+ :CreatedDate => { type: :date, in_format: '%Y-%m-%d %H:%M:%S' }
44
+ }
45
+ )
46
+ end
47
+
48
+ source :sample_file do
49
+ extractor Remi::Extractor::SftpFile.new(
29
50
  credentials: params[:sftp],
30
51
  remote_path: '/',
31
52
  pattern: /^SampleFile_(\d+)\.txt/,
32
53
  most_recent_only: true
33
- ),
34
- csv_options: {
35
- headers: true,
36
- col_sep: ",",
37
- encoding: "ISO-8859-1:UTF-8"
38
- },
39
- fields: {
40
- :student_id => {},
41
- :school_id => {},
42
- :school_name => {},
43
- :program => {},
44
- :last_name => {},
45
- :first_name => {},
46
- :current_email => {},
47
- :mailing_address_line_1 => {},
48
- :mailing_address_line_2 => {},
49
- :mailing_city => {},
50
- :mailing_state => {},
51
- :mailing_postal_code => {},
52
- :birthdate => { type: :date, in_format: '%m/%d/%Y'},
53
- :applied_date => { type: :date, in_format: '%m/%d/%Y'}
54
- }
55
-
56
- define_target :all_contacts, Remi::DataTarget::DataFrame
57
-
58
- define_target :contact_updates, Remi::DataTarget::Salesforce,
59
- credentials: params[:salesforce_credentials],
60
- object: :Contact,
61
- operation: :update,
62
- api: :bulk
63
-
64
- define_target :contact_creates, Remi::DataTarget::Salesforce,
65
- credentials: params[:salesforce_credentials],
66
- object: :Contact,
67
- operation: :create,
68
- api: :bulk
69
-
70
- define_param :program_name_lookup, RegexSieve.new({
71
- /^BIO$/ => "Biology",
72
- /^Fake Biology$/ => nil,
73
- /(?:B|Microb)iology/ => "Biology",
74
- /^CHEM$/ => "Chemistry",
75
- /Chemistry/ => "Chemistry",
76
- /Physics/ => "Physics"
77
- })
78
-
79
- define_transform :map_common_fields, sources: [:sample_file, :existing_contacts], targets: :all_contacts do
54
+ )
55
+
56
+ parser Remi::Parser::CsvFile.new(
57
+ csv_options: {
58
+ headers: true,
59
+ col_sep: ",",
60
+ encoding: "ISO-8859-1:UTF-8"
61
+ }
62
+ )
63
+
64
+ fields(
65
+ {
66
+ :student_id => {},
67
+ :school_id => {},
68
+ :school_name => {},
69
+ :program => {},
70
+ :last_name => {},
71
+ :first_name => {},
72
+ :current_email => {},
73
+ :mailing_address_line_1 => {},
74
+ :mailing_address_line_2 => {},
75
+ :mailing_city => {},
76
+ :mailing_state => {},
77
+ :mailing_postal_code => {},
78
+ :birthdate => { type: :date, in_format: '%m/%d/%Y'},
79
+ :applied_date => { type: :date, in_format: '%m/%d/%Y'}
80
+ }
81
+ )
82
+ end
83
+
84
+
85
+ target :all_contacts
80
86
 
87
+ target :contact_updates do
88
+ encoder Remi::Encoder::Salesforce.new
89
+ loader Remi::Loader::Salesforce.new(
90
+ credentials: params[:salesforce_credentials],
91
+ object: :Contact,
92
+ operation: :update,
93
+ api: :bulk
94
+ )
95
+ field_symbolizer :salesforce
96
+ end
97
+
98
+ target :contact_creates do
99
+ encoder Remi::Encoder::Salesforce.new
100
+ loader Remi::Loader::Salesforce.new(
101
+ credentials: params[:salesforce_credentials],
102
+ object: :Contact,
103
+ operation: :create,
104
+ api: :bulk
105
+ )
106
+ field_symbolizer :salesforce
107
+ end
108
+
109
+
110
+ transform :map_common_fields do
81
111
  # Exclude all source records with an invalid program name
82
112
  all_contacts.df = sample_file.df.dup
83
113
  Remi::SourceToTargetMap.apply(all_contacts.df) do
84
114
  map source(:program) .target(:Major__c)
85
- .transform(Remi::Transform::Lookup.new(params[:program_name_lookup]))
115
+ .transform(Remi::Transform::Lookup.new(job.params[:program_name_lookup]))
86
116
  end
87
117
  all_contacts.df = all_contacts.df.where(all_contacts.df[:Major__c].not_eq(nil))
88
118
 
@@ -102,8 +132,7 @@ class SampleJob
102
132
  end
103
133
 
104
134
 
105
- define_transform :map_creates, sources: :all_contacts, targets: :contact_creates do
106
-
135
+ transform :map_creates do
107
136
  work_contact_creates = all_contacts.df.where(all_contacts.df[:Id].eq(nil))
108
137
 
109
138
  Remi::SourceToTargetMap.apply(work_contact_creates) do
@@ -166,7 +195,7 @@ class SampleJob
166
195
  ]
167
196
  end
168
197
 
169
- define_transform :map_updates, sources: :all_contacts, targets: :contact_updates do
198
+ transform :map_updates do
170
199
  contact_updates.df = all_contacts.df[
171
200
  :Id,
172
201
  :Major__c
@@ -1,18 +1,19 @@
1
1
  require_relative 'all_jobs_shared'
2
2
 
3
- class SftpFileTargetJob
4
- include AllJobsShared
5
-
6
-
7
- define_target :some_file, Remi::DataTarget::SftpFile,
8
- credentials: {
9
- host: 'example.com',
10
- username: 'user',
11
- password: 'secret'
12
- },
13
- local_path: "#{Remi::Settings.work_dir}/some_file.csv",
14
- remote_path: "some_file_#{DateTime.current.strftime('%Y%m%d')}.csv"
3
+ class SftpFileTargetJob < Remi::Job
4
+ target :some_file do
5
+ encoder Remi::Encoder::CsvFile.new
6
+ loader Remi::Loader::SftpFile.new(
7
+ credentials: {
8
+ host: 'example.com',
9
+ username: 'user',
10
+ password: 'secret'
11
+ },
12
+ local_path: "#{Remi::Settings.work_dir}/some_file.csv",
13
+ remote_path: "some_file_#{DateTime.current.strftime('%Y%m%d')}.csv"
14
+ )
15
+ end
15
16
 
16
- define_transform :main do
17
+ transform :main do
17
18
  end
18
19
  end
@@ -0,0 +1,86 @@
1
+ require_relative 'all_jobs_shared'
2
+
3
+ class BeersJob < Remi::Job
4
+ source :beers do
5
+ extractor Remi::Extractor::DataFrame.new(
6
+ data: [
7
+ [ 'Baerlic', 'IPA' ],
8
+ [ 'Ex Novo', 'Red' ]
9
+ ]
10
+ )
11
+ parser Remi::Parser::DataFrame.new
12
+ fields(
13
+ {
14
+ brewer: {},
15
+ style: {}
16
+ }
17
+ )
18
+ end
19
+
20
+ transform :main do
21
+ # In the real world, add lots of complex stuff here, possibly grabbing
22
+ # from multiple sources.
23
+ beers.df
24
+ end
25
+ end
26
+
27
+ class ZombifyJob < Remi::Job
28
+ source :beers
29
+ target :zombie_beers
30
+
31
+ transform :main do
32
+ Remi::SourceToTargetMap.apply(beers.df, zombie_beers.df) do
33
+ map source(:brewer) .target(:brewer)
34
+ .transform(Remi::Transform::Prefix.new('Zombie '))
35
+ map source(:style) .target(:style)
36
+ .transform(Remi::Transform::Prefix.new('Zombie '))
37
+ end
38
+ end
39
+ end
40
+
41
+ class SubJobExampleJob < Remi::Job
42
+ sub_job(:beers_job) { BeersJob.new }
43
+ sub_job(:zombify_job) { ZombifyJob.new }
44
+
45
+ # This originates from a source in the sub job
46
+ source :beer_fridge do
47
+ extractor Remi::Extractor::SubJob.new(
48
+ sub_job: beers_job,
49
+ data_subject: :beers
50
+ )
51
+ fields beers_job.fields :beers
52
+ end
53
+
54
+ # This target is used as a source in the sub job
55
+ target :beers_to_zombify do
56
+ loader Remi::Loader::SubJob.new(
57
+ sub_job: zombify_job,
58
+ data_subject: :beers
59
+ )
60
+ end
61
+
62
+ # This source is obtained from the target of the sub job
63
+ source :zombie_fridge do
64
+ extractor Remi::Extractor::SubJob.new(
65
+ sub_job: zombify_job,
66
+ data_subject: :zombie_beers
67
+ )
68
+ fields zombify_job.fields :zombie_beers
69
+ end
70
+
71
+ # These are the ultimate targets of this job
72
+ target :just_beers
73
+ target :zombified_beers
74
+
75
+ transform :zombification do
76
+ # Sub jobs must be executed before their sources are available
77
+ beers_job.execute
78
+ just_beers.df = beer_fridge.df
79
+
80
+ # Sub job targets must be loaded before they are available to subjobs
81
+ beers_to_zombify.df = just_beers.df
82
+ beers_to_zombify.load
83
+ zombify_job.execute
84
+ zombified_beers.df = zombie_fridge.df
85
+ end
86
+ end