art_vandelay 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 26029667adccdd12b5098fb5e370050bb3bf4877dceb712cba6ce4f4032aec3f
4
+ data.tar.gz: '05569d49fa0ad8da29bb07947e77db6425ba12506699b073c27690a644592b52'
5
+ SHA512:
6
+ metadata.gz: 1ae5ff614ad52587111bf87f10ea736d1087100ff3c782e25c4a8f8e01732f2829e7cd7640172c3e20756f20d6f9faa733b97f6fa53da39b9f442b0d796cd149
7
+ data.tar.gz: 66c22f9871d06bc1a1d3f4852ef57d919b78960821eabd1f9c7e2c137954db3aa2e2d2d85d31c87f5038710d6a8bc66b139c5d753bc7df9abd3bf40236249a73
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Steve Polito and thoughtbot, inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # 🌐 Art Vandelay
2
+ [![GitHub Actions
3
+ Demo](https://github.com/thoughtbot/art_vandelay/actions/workflows/ci.yml/badge.svg)](https://github.com/thoughtbot/art_vandelay/actions/workflows/ci.yml)
4
+
5
+ Art Vandelay is an importer/exporter for Rails 6.0 and higher.
6
+
7
+ Have you ever been on a project where, out of nowhere, someone asks you to send them a CSV of data? You think to yourself, “Ok, cool. No big deal. Just gimme five minutes”, but then that five minutes turns into a few hours. Art Vandelay can help.
8
+
9
+ **At a high level, here’s what Art Vandelay can do:**
10
+
11
+ - 🕶 Automatically [filters out sensitive information](#%EF%B8%8F-configuration).
12
+ - 🔁 Export data [in batches](#exporting-in-batches).
13
+ - 📧 [Email](#artvandelayexportemail_csv) exported data.
14
+ - 📥 [Import data](#-importing) from a CSV.
15
+
16
+ ## ✅ Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem "art_vandelay", git: "https://github.com/thoughtbot/art_vandelay"
22
+ ```
23
+
24
+ And then execute:
25
+ ```bash
26
+ $ bundle
27
+ ```
28
+
29
+ ## ⚙️ Configuration
30
+
31
+ ```ruby
32
+ # config/initializers/art_vandelay.rb
33
+ ArtVandelay.setup do |config|
34
+ config.filtered_attributes = [:credit_card, :birthday]
35
+ config.from_address = "no-reply-export@example.com"
36
+ config.in_batches_of = 5000
37
+ end
38
+ ```
39
+ #### Default Values
40
+
41
+ |Attribute|Value|Description|
42
+ |---------|-----|-----------|
43
+ |`filtered_attributes`|`[:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn]`|Attributes that will be automatically filtered when exported|
44
+ |`from_address`|`nil`|The email address used when sending an email of exports|
45
+ |`in_batches_of`|`10000`|The number of records that will be exported into each CSV|
46
+
47
+ ## 🧰 Usage
48
+
49
+ ### 📤 Exporting
50
+
51
+ ```ruby
52
+ ArtVandelay::Export.new(records, export_sensitive_data: false, attributes: [], in_batches_of: ArtVandelay.in_batches_of)
53
+ ```
54
+
55
+ |Argument|Description|
56
+ |--------|-----------|
57
+ |`records`|An [Active Record Relation](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html) or an instance of an Active Record. E.g. `User.all`, `User.first`, `User.where(...)`, `User.find_by`|
58
+ |`export_sensitive_data`|Export sensitive data. Defaults to `false`. Can be configured with `ArtVandelay.filtered_attributes`.|
59
+ |`attributes`|An array attributes to export. Default to all.|
60
+ |`in_batches_of`|The number of records that will be exported into each CSV. Defaults to 10,000. Can be configured with `ArtVandelay.in_batches_of`|
61
+
62
+ #### ArtVandelay::Export#csv
63
+
64
+ Returns an instance of `ArtVandelay::Export::Result`.
65
+
66
+ ```ruby
67
+ result = ArtVandelay::Export.new(User.all).csv
68
+ # => #<ArtVandelay::Export::Result>
69
+
70
+ csv_exports = result.result.csv_exports
71
+ # => [#<CSV::Table>, #<CSV::Table>, ...]
72
+
73
+ csv = csv_exports.first.to_a
74
+ # => [["id", "email", "password", "created_at", "updated_at"], ["1", "user@example.com", "[FILTERED]", "2022-10-25 09:20:28 UTC", "2022-10-25 09:20:28 UTC"]]
75
+ ```
76
+
77
+ ##### Exporting Sensitive Data
78
+
79
+ ```ruby
80
+ result = ArtVandelay::Export.new(User.all, export_sensitive_data: true).csv
81
+ # => #<ArtVandelay::Export::Result>
82
+
83
+ password = result.csv_exports.first["password"]
84
+ # => ["bosco"]
85
+ ```
86
+
87
+ ##### Exporting Specific Attributes
88
+
89
+ ```ruby
90
+ result = ArtVandelay::Export.new(User.all, attributes: [:email]).csv
91
+ # => #<ArtVandelay::Export::Result>
92
+
93
+ csv = result.csv_exports.first.to_a
94
+ # => [["email"], ["george@vandelay_industries.com"]]
95
+ ```
96
+
97
+ ##### Exporting in Batches
98
+
99
+ ```ruby
100
+ result = ArtVandelay::Export.new(User.all, in_batches_of: 100).csv
101
+ # => #<ArtVandelay::Export::Result>
102
+
103
+ csv_size = result.csv_exports.first.size
104
+ # => 100
105
+ ```
106
+
107
+ #### ArtVandelay::Export#email_csv
108
+
109
+ Emails the recipient(s) CSV exports as attachments.
110
+
111
+ ```ruby
112
+ email_csv(to:, from: ArtVandelay.from_address, subject: "#{model_name} export", body: "#{model_name} export")
113
+ ```
114
+
115
+ |Argument|Description|
116
+ |---------|-----|
117
+ |`to`|An array of email addresses representing who should receive the email.|
118
+ |`from`|The email address of the sender.|
119
+ |`subject`|The email subject. Defaults to the following pattern: "User export"|
120
+ |`body`|The email body. Defaults to the following pattern: "User export"|
121
+
122
+ ```ruby
123
+ ArtVandelay::Export
124
+ .new(User.where.not(confirmed: nil))
125
+ .email_csv(
126
+ to: ["george@vandelay_industries.com", "kel_varnsen@vandelay_industries.com"],
127
+ from: "noreply@vandelay_industries.com",
128
+ subject: "List of confirmed users",
129
+ body: "Here's an export of all confirmed users in our database."
130
+ )
131
+ # => ActionMailer::Base#mail: processed outbound mail in...
132
+ ```
133
+
134
+ ### 📥 Importing
135
+
136
+ ```ruby
137
+ ArtVandelay::Import.new(model_name, **options)
138
+ ```
139
+
140
+ |Argument|Description|
141
+ |--------|-----------|
142
+ |`model_name`|The name of the model being imported. E.g. `:users`, `:user`, `"users"` or `"user"`|
143
+ |`**options`|A hash of options. Available options are `rollback:`|
144
+
145
+ #### Options
146
+
147
+ |Option|Description|
148
+ |------|-----------|
149
+ |`rollback:`|Whether the import should rollback if any of the records fails to save.|
150
+
151
+ #### ArtVandelay::Import#csv
152
+
153
+ Imports records from the supplied CSV. Returns an instance of `ArtVandelay::Import::Result`.
154
+
155
+ ```ruby
156
+ csv_string = CSV.generate do |csv|
157
+ csv << ["email", "password"]
158
+ csv << ["george@vandelay_industries.com", "bosco"]
159
+ csv << ["kel_varnsen@vandelay_industries.com", nil]
160
+ end
161
+
162
+ result = ArtVandelay::Import.new(:users).csv(csv_string)
163
+ # => #<ArtVandelay::Import::Result>
164
+
165
+ result.rows_accepted
166
+ # => [{:row=>["george@vandelay_industries.com", "bosco"], :id=>1}]
167
+
168
+ result.rows_rejected
169
+ # => [{:row=>["kel_varnsen@vandelay_industries.com", nil], :errors=>{:password=>["can't be blank"]}}]
170
+ ```
171
+
172
+ ```ruby
173
+ csv(csv_string, **options)
174
+ ```
175
+
176
+ |Argument|Description|
177
+ |--------|-----------|
178
+ |`csv_string`|Data in the form of a CSV string.|
179
+ |`**options`|A hash of options. Available options are `headers:` and `attributes:`|
180
+
181
+ #### Options
182
+
183
+ |Option|Description|
184
+ |------|-----------|
185
+ |`headers:`|The CSV headers. Use when the supplied CSV string does not have headers.|
186
+ |`attributes:`|The attributes the headers should map to. Useful if the headers do not match the model's attributes.|
187
+
188
+ ##### Rolling back if a record fails to save
189
+
190
+ ```ruby
191
+ csv_string = CSV.generate do |csv|
192
+ csv << ["email", "password"]
193
+ csv << ["george@vandelay_industries.com", "bosco"]
194
+ csv << ["kel_varnsen@vandelay_industries.com", nil]
195
+ end
196
+
197
+ result = ArtVandelay::Import.new(:users, rollback: true).csv(csv_string)
198
+ # => rollback transaction
199
+ ```
200
+
201
+ ##### Setting headers
202
+
203
+ ```ruby
204
+ csv_string = CSV.generate do |csv|
205
+ csv << ["george@vandelay_industries.com", "bosco"]
206
+ end
207
+
208
+ result = ArtVandelay::Import.new(:users).csv(csv_string, headers: [:email, :password])
209
+ # => #<ArtVandelay::Import::Result>
210
+ ```
211
+
212
+ ##### Mapping custom headers
213
+
214
+ ```ruby
215
+ csv_string = CSV.generate do |csv|
216
+ csv << ["email_address", "passcode"]
217
+ csv << ["george@vandelay_industries.com", "bosco"]
218
+ end
219
+
220
+ result = ArtVandelay::Import.new(:users).csv(csv_string, attributes: {email_address: :email, passcode: :password})
221
+ # => #<ArtVandelay::Import::Result>
222
+ ```
223
+
224
+ ## 🙏 Contributing
225
+
226
+ 1. Run `./bin/setup`.
227
+ 2. Make your changes.
228
+ 3. Ensure `./bin/ci` passes.
229
+ 4. Create a [pull request](https://github.com/thoughtbot/art_vandelay/compare).
230
+
231
+ ## 📜 License
232
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/art_vandelay .css
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,4 @@
1
+ module ArtVandelay
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ArtVandelay
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ArtVandelay
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module ArtVandelay
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module ArtVandelay
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Art vandelay</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "art_vandelay/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ ArtVandelay::Engine.routes.draw do
2
+ end
@@ -0,0 +1,5 @@
1
+ module ArtVandelay
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace ArtVandelay
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module ArtVandelay
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,204 @@
1
+ require "art_vandelay/version"
2
+ require "art_vandelay/engine"
3
+ require "csv"
4
+
5
+ module ArtVandelay
6
+ mattr_accessor :filtered_attributes, :from_address, :in_batches_of
7
+ @@filtered_attributes = [:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn]
8
+ @@in_batches_of = 10000
9
+
10
+ def self.setup
11
+ yield self
12
+ end
13
+
14
+ class Error < StandardError
15
+ end
16
+
17
+ class Export
18
+ class Result
19
+ attr_reader :csv_exports
20
+
21
+ def initialize(csv_exports)
22
+ @csv_exports = csv_exports
23
+ end
24
+ end
25
+
26
+ # TODO attributes: self.filtered_attributes
27
+ def initialize(records, export_sensitive_data: false, attributes: [], in_batches_of: ArtVandelay.in_batches_of)
28
+ @records = records
29
+ @export_sensitive_data = export_sensitive_data
30
+ @attributes = attributes
31
+ @in_batches_of = in_batches_of
32
+ end
33
+
34
+ def csv
35
+ csv_exports = []
36
+
37
+ if records.is_a?(ActiveRecord::Relation)
38
+ records.in_batches(of: in_batches_of) do |relation|
39
+ csv_exports << CSV.parse(generate_csv(relation), headers: true)
40
+ end
41
+ elsif records.is_a?(ActiveRecord::Base)
42
+ csv_exports << CSV.parse(generate_csv(records), headers: true)
43
+ end
44
+
45
+ Result.new(csv_exports)
46
+ end
47
+
48
+ def email_csv(to:, from: ArtVandelay.from_address, subject: "#{model_name} export", body: "#{model_name} export")
49
+ if from.nil?
50
+ raise ArtVandelay::Error, "missing keyword: :from. Alternatively, set a value on ArtVandelay.from_address"
51
+ end
52
+
53
+ mailer = ActionMailer::Base.mail(to: to, from: from, subject: subject, body: body)
54
+ csv_exports = csv.csv_exports
55
+
56
+ csv_exports.each.with_index(1) do |csv, index|
57
+ if csv_exports.one?
58
+ mailer.attachments[file_name] = csv
59
+ else
60
+ mailer.attachments[file_name(suffix: "-#{index}")] = csv
61
+ end
62
+ end
63
+
64
+ mailer.deliver
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :records, :export_sensitive_data, :attributes, :in_batches_of
70
+
71
+ def file_name(**options)
72
+ options = options.symbolize_keys
73
+ suffix = options[:suffix]
74
+ prefix = model_name.downcase
75
+ timestamp = Time.current.in_time_zone("UTC").strftime("%Y-%m-%d-%H-%M-%S-UTC")
76
+
77
+ "#{prefix}-export-#{timestamp}#{suffix}.csv"
78
+ end
79
+
80
+ def filtered_values(attributes)
81
+ if export_sensitive_data
82
+ ActiveSupport::ParameterFilter.new([]).filter(attributes).values
83
+ else
84
+ ActiveSupport::ParameterFilter.new(ArtVandelay.filtered_attributes).filter(attributes).values
85
+ end
86
+ end
87
+
88
+ def generate_csv(relation)
89
+ CSV.generate do |csv|
90
+ csv << header
91
+ if relation.is_a?(ActiveRecord::Relation)
92
+ relation.each do |record|
93
+ csv << row(record.attributes)
94
+ end
95
+ elsif relation.is_a?(ActiveRecord::Base)
96
+ csv << row(records.attributes)
97
+ end
98
+ end
99
+ end
100
+
101
+ def header
102
+ if attributes.any?
103
+ model.attribute_names.select do |column_name|
104
+ standardized_attributes.include?(column_name)
105
+ end
106
+ else
107
+ model.attribute_names
108
+ end
109
+ end
110
+
111
+ def model
112
+ model_name.constantize
113
+ end
114
+
115
+ def model_name
116
+ records.model_name.name
117
+ end
118
+
119
+ def row(attributes)
120
+ if self.attributes.any?
121
+ filtered_values(attributes.slice(*standardized_attributes))
122
+ else
123
+ filtered_values(attributes)
124
+ end
125
+ end
126
+
127
+ def standardized_attributes
128
+ attributes.map(&:to_s)
129
+ end
130
+ end
131
+
132
+ class Import
133
+ class Result
134
+ attr_reader :rows_accepted, :rows_rejected
135
+
136
+ def initialize(rows_accepted:, rows_rejected:)
137
+ @rows_accepted = rows_accepted
138
+ @rows_rejected = rows_rejected
139
+ end
140
+ end
141
+
142
+ def initialize(model_name, **options)
143
+ @options = options.symbolize_keys
144
+ @rollback = options[:rollback]
145
+ @model_name = model_name
146
+ end
147
+
148
+ def csv(csv_string, **options)
149
+ options = options.symbolize_keys
150
+ headers = options[:headers] || true
151
+ attributes = options[:attributes] || {}
152
+ rows = build_csv(csv_string, headers)
153
+
154
+ if rollback
155
+ # TODO: It would be nice to still return a result object during a
156
+ # failure
157
+ active_record.transaction do
158
+ parse_rows(rows, attributes, raise_on_error: true)
159
+ end
160
+ else
161
+ parse_rows(rows, attributes)
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ attr_reader :model_name, :rollback
168
+
169
+ def active_record
170
+ model_name.to_s.classify.constantize
171
+ end
172
+
173
+ def build_csv(csv_string, headers)
174
+ CSV.parse(csv_string, headers: headers)
175
+ end
176
+
177
+ def build_params(row, attributes)
178
+ attributes = attributes.stringify_keys
179
+
180
+ row.to_h.stringify_keys.transform_keys do |key|
181
+ attributes[key] || key
182
+ end
183
+ end
184
+
185
+ def parse_rows(rows, attributes, **options)
186
+ options = options.symbolize_keys
187
+ raise_on_error = options[:raise_on_error] || false
188
+ result = Result.new(rows_accepted: [], rows_rejected: [])
189
+
190
+ rows.each do |row|
191
+ params = build_params(row, attributes)
192
+ record = active_record.new(params)
193
+
194
+ if raise_on_error ? record.save! : record.save
195
+ result.rows_accepted << {row: row.fields, id: record.id}
196
+ else
197
+ result.rows_rejected << {row: row.fields, errors: record.errors.messages}
198
+ end
199
+ end
200
+
201
+ result
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :art_vandelay do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: art_vandelay
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Steve Polito
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-12-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Art Vandelay is an importer/exporter for Rails
28
+ email:
29
+ - stevepolito@hey.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - MIT-LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - app/assets/config/art_vandelay_manifest.js
38
+ - app/assets/stylesheets/art_vandelay/application.css
39
+ - app/controllers/art_vandelay/application_controller.rb
40
+ - app/helpers/art_vandelay/application_helper.rb
41
+ - app/jobs/art_vandelay/application_job.rb
42
+ - app/mailers/art_vandelay/application_mailer.rb
43
+ - app/models/art_vandelay/application_record.rb
44
+ - app/views/layouts/art_vandelay/application.html.erb
45
+ - config/routes.rb
46
+ - lib/art_vandelay.rb
47
+ - lib/art_vandelay/engine.rb
48
+ - lib/art_vandelay/version.rb
49
+ - lib/tasks/art_vandelay_tasks.rake
50
+ homepage: https://github.com/thoughtbot/art_vandelay
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ homepage_uri: https://github.com/thoughtbot/art_vandelay
55
+ source_code_uri: https://github.com/thoughtbot/art_vandelay
56
+ changelog_uri: https://github.com/thoughtbot/art_vandelay/NEWS.md
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 3.3.3
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: Art Vandelay is an importer/exporter for Rails
76
+ test_files: []