art_vandelay 0.1.0

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.
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: []