jobler 0.0.8 → 0.0.13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: a515f6a2b9fd3b0fbf184f612d2665c2b8934bda
4
- data.tar.gz: f0384836dcf388c9d5f5bfdc60d4e524a9539792
2
+ SHA256:
3
+ metadata.gz: 549a9f294ff023f7c67091e91120706db134cd9639d9f8b2d60d9860c5562788
4
+ data.tar.gz: '097746283982a71eeffdd1a006e2e8a6d178c5b847e6854ddae0696075da4490'
5
5
  SHA512:
6
- metadata.gz: 6a7e9bd7fba0d0a0bd14d82d3075cf120a69dfe51f6986855ee4b2bfee08a6f0ba5f6add226ea0f3b47ab6d1f9cdcbc33790f907709c267f214ccb36ff8ce136
7
- data.tar.gz: 501a4c6a6a5455d4e54afac0a14e891dc098bb2747882ee5c462dd9001bd4218665b1533874b4db7605603a1b34fb725348f992bbc3a89f20fb686d786f3f348
6
+ metadata.gz: 3204ebaef1538454ea89a42066fc498d554317f14a9dd72e03356c1c1cb58ef5aac58909246c750b17c785b4610b33cef3a7b19ab93cc3877297ae0b659025e6
7
+ data.tar.gz: e7ffb22951a39418e2ffd48c7f58b69b7465dbf41419583c5b49ed8ae71fbcbd15ca5dd7f58491df70ec7769c2e1dadb2416bb7ceac974910d50ef8af7617a23
data/README.md CHANGED
@@ -34,7 +34,11 @@ class ApplicationJobler < Jobler::BaseJobler
34
34
  end
35
35
  ```
36
36
 
37
- Jobler is going to queue its jobs through the ActiveJob queue called `:jobler`, so make sure a worker is listening to that queue.
37
+ Jobler is going to queue its jobs through the ActiveJob queue called `:jobler`, so make sure a worker is listening to that queue. This is done like this in Sidekiq:
38
+
39
+ ```bash
40
+ bundle exec sidekiq --queue default --queue jobler --queue mailers
41
+ ```
38
42
 
39
43
 
40
44
  ## Usage
@@ -111,7 +115,7 @@ You should then create a controller something like this:
111
115
  ```ruby
112
116
  class JoblerJobsController < ApplicationController
113
117
  def show
114
- @job = Jobler::Job.find_by!(slug: param[:id])
118
+ @job = Jobler::Job.find_by!(slug: params[:id])
115
119
  @result = @job.results.find_by!(name: "render")
116
120
  end
117
121
  end
@@ -119,7 +123,7 @@ end
119
123
 
120
124
  And a view in "app/views/jobler_jobs/show.html.erb":
121
125
  ```erb
122
- <%= @result.result.force_encoding("utf-8").html_safe
126
+ <%= @result.result.force_encoding("utf-8").html_safe %>
123
127
  ```
124
128
 
125
129
  You should also add a route like this:
@@ -129,6 +133,33 @@ Rails.application.routes.draw do
129
133
  end
130
134
  ```
131
135
 
136
+ # Progress bar
137
+
138
+ In order to utilize the progress bar and return some feedback on the progress to the user, you can implement a couple of calls to do that:
139
+
140
+ ```ruby
141
+ class MyJobler < ApplicationJobler
142
+ def execute!
143
+ progress_total collection.size
144
+
145
+ collection.find_each do |model|
146
+ increment_progress!
147
+ end
148
+ end
149
+ end
150
+ ```
151
+
152
+ You can also call it from a view, if you a doing a render like this:
153
+ ```erb
154
+ <% jobler.increment_progress! %>
155
+ ```
156
+
157
+ You can also specify a custom value if it isn't 1:
158
+ ```erb
159
+ <% jobler.increment_progress!(value: 5.0) %>
160
+ ```
161
+
132
162
  ## License
133
163
 
134
164
  This project rocks and uses MIT-LICENSE.
165
+
data/Rakefile CHANGED
@@ -14,7 +14,7 @@ RDoc::Task.new(:rdoc) do |rdoc|
14
14
  rdoc.rdoc_files.include("lib/**/*.rb")
15
15
  end
16
16
 
17
- APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
17
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
18
18
  load "rails/tasks/engine.rake"
19
19
 
20
20
  load "rails/tasks/statistics.rake"
@@ -1,3 +1,2 @@
1
- //= require jquery
2
1
  //= require jobler/timeago
3
2
  //= require jobler/jobs
@@ -4,7 +4,11 @@ class Jobler::DownloadsController < Jobler::ApplicationController
4
4
  @result = @job.jobler.result
5
5
 
6
6
  if @result.is_a?(Jobler::FileDownload)
7
- send_file @result.temp_file.path, disposition: "attachment", filename: @result.file_name
7
+ if @result.url.present?
8
+ redirect_to @result.url
9
+ else
10
+ send_file @result.temp_file.path, disposition: "attachment", filename: @result.file_name
11
+ end
8
12
  else
9
13
  flash[:error] = "The result wasn't a file download"
10
14
  redirect_to :back
@@ -1,7 +1,6 @@
1
1
  class Jobler::JobsController < Jobler::ApplicationController
2
2
  def show
3
3
  @job = Jobler::Job.find_by!(slug: params[:id])
4
- @result = @job.jobler.result if @job.completed?
5
4
 
6
5
  respond_to do |format|
7
6
  format.json do
@@ -14,8 +13,17 @@ class Jobler::JobsController < Jobler::ApplicationController
14
13
  }
15
14
  end
16
15
 
17
- if @result.is_a?(Jobler::RedirectTo)
18
- format.html { redirect_to @result.url }
16
+ if @job.completed?
17
+ @job.jobler.controller = self
18
+ @job.jobler.format = format
19
+
20
+ @result = @job.jobler.result
21
+
22
+ if @result.is_a?(Jobler::RedirectTo)
23
+ format.html { redirect_to @result.url }
24
+ else
25
+ format.html
26
+ end
19
27
  else
20
28
  format.html
21
29
  end
@@ -0,0 +1,65 @@
1
+ class Jobler::Models::DestroyerJobler < Jobler::BaseJobler
2
+ def execute!
3
+ calculate_numbers
4
+
5
+ model_class.transaction do
6
+ destroy_relationships
7
+ model.destroy!
8
+ end
9
+ end
10
+
11
+ def result
12
+ format.html { controller.redirect_to args.fetch(:redirect_to) }
13
+ end
14
+
15
+ private
16
+
17
+ def calculate_numbers
18
+ Rails.logger.debug "Calculate numbers"
19
+
20
+ @total = 0
21
+ relationships.each do |relationship|
22
+ Rails.logger.debug "Calculate size for #{relationship}"
23
+ @total += model.__send__(relationship).size
24
+ end
25
+
26
+ progress_total @total
27
+
28
+ Rails.logger.debug "Done calculating numbers: #{@total}"
29
+ end
30
+
31
+ def destroy_relationships
32
+ Rails.logger.debug "Destroying relationships"
33
+ relationships.each do |relationship|
34
+ Rails.logger.debug "Destroying #{relationship}"
35
+ model.__send__(relationship).find_each do |sub_model|
36
+ Rails.logger.debug "Destroying #{sub_model.id}"
37
+ sub_model.destroy!
38
+ increment_progress!
39
+ end
40
+ end
41
+ end
42
+
43
+ def model_class
44
+ @model_class ||= args.fetch(:model).constantize
45
+ end
46
+
47
+ def model
48
+ @model ||= model_class.find(args.fetch(:model_id))
49
+ end
50
+
51
+ def relationships
52
+ @relationships ||= begin
53
+ result = []
54
+
55
+ model_class.reflections.each_value do |reflection|
56
+ next unless reflection.is_a?(ActiveRecord::Reflection::HasManyReflection)
57
+ next unless reflection.options[:dependent] == :destroy
58
+
59
+ result << reflection.name
60
+ end
61
+
62
+ result
63
+ end
64
+ end
65
+ end
@@ -1,15 +1,12 @@
1
- class Jobler::Job < ActiveRecord::Base
2
- has_many :results, class_name: "Jobler::Result", dependent: :destroy
1
+ class Jobler::Job < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord
2
+ has_many :results, class_name: "Jobler::Result", dependent: :destroy, inverse_of: :job
3
3
 
4
4
  validates :jobler_type, :slug, :state, presence: true
5
5
 
6
6
  before_validation :set_slug
7
7
 
8
8
  def jobler
9
- user_jobler = jobler_type.constantize.new
10
- user_jobler.instance_variable_set(:@args, YAML.load(parameters)) # rubocop:disable Security/YAMLLoad
11
- user_jobler.instance_variable_set(:@job, self)
12
- user_jobler
9
+ @jobler ||= jobler_type.constantize.new(args: YAML.load(parameters), job: self) # rubocop:disable Security/YAMLLoad
13
10
  end
14
11
 
15
12
  def completed?
@@ -22,6 +19,7 @@ class Jobler::Job < ActiveRecord::Base
22
19
 
23
20
  def to_param
24
21
  raise "No slug" unless slug?
22
+
25
23
  slug
26
24
  end
27
25
 
@@ -1,5 +1,7 @@
1
- class Jobler::Result < ActiveRecord::Base
2
- belongs_to :job, class_name: "Jobler::Job"
1
+ class Jobler::Result < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord
2
+ belongs_to :job, class_name: "Jobler::Job", inverse_of: :results
3
+
4
+ has_one_attached :file
3
5
 
4
6
  validates :job, :name, presence: true
5
7
  end
@@ -4,7 +4,11 @@
4
4
 
5
5
  <% if @job.completed? %>
6
6
  <% if @result.is_a?(Jobler::FileDownload) %>
7
- <%= link_to "Download", download_path(@job) %>
7
+ <% if @result.url.present? %>
8
+ <%= link_to "Download", @result.url %>
9
+ <% else %>
10
+ <%= link_to "Download", download_path(@job) %>
11
+ <% end %>
8
12
  <% elsif @result.is_a?(Jobler::PageRender) %>
9
13
  <%= @result.body.html_safe %>
10
14
  <% end %>
@@ -3,13 +3,15 @@
3
3
  <head>
4
4
  <title>Jobler</title>
5
5
 
6
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha256-916EbMg70RQy9LHiGkXzG8hSg9EdNy97GazNG/aiY1w=" crossorigin="anonymous" />
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=" crossorigin="anonymous" />
6
8
  <%= stylesheet_link_tag "jobler/application", media: "all" %>
7
- <%= stylesheet_link_tag "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css", integrity: "sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u", crossorigin: "anonymous" %>
8
9
 
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-ujs/1.2.2/rails.min.js" integrity="sha256-BbyWhCn0G+F6xbWJ2pcI5LnnpsnpSzyjJNVtl7ABp+M=" crossorigin="anonymous"></script>
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.6.0/jquery.timeago.min.js" integrity="sha256-JAdlnMiYFZRwqQqgiYwLjSkV94QtVzcbvasQMm05THk=" crossorigin="anonymous"></script>
13
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha256-U5ZEeKfGNOja007MMD3YBI0A3OSZOQbeG6z2f2Y0hu8=" crossorigin="anonymous"></script>
9
14
  <%= javascript_include_tag "jobler/application" %>
10
- <%= javascript_include_tag "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js", integrity: "sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa", crossorigin: "anonymous" %>
11
- <%= javascript_include_tag "https://use.fontawesome.com/de02b76735.js" %>
12
- <%= javascript_include_tag "https://cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.5.4/jquery.timeago.min.js" %>
13
15
 
14
16
  <%= csrf_meta_tags %>
15
17
  </head>
@@ -1,4 +1,4 @@
1
- class CreateJobs < ActiveRecord::Migration
1
+ class CreateJobs < ActiveRecord::Migration[5.1]
2
2
  def change
3
3
  create_table :jobler_jobs do |t|
4
4
  t.string :jobler_type, null: false
@@ -1,4 +1,4 @@
1
- class CreateJoblerResults < ActiveRecord::Migration
1
+ class CreateJoblerResults < ActiveRecord::Migration[5.1]
2
2
  def change
3
3
  create_table :jobler_results do |t|
4
4
  t.belongs_to :job, index: true, null: false
@@ -1,4 +1,4 @@
1
- class AddLocaleToJobs < ActiveRecord::Migration
1
+ class AddLocaleToJobs < ActiveRecord::Migration[5.1]
2
2
  def change
3
3
  add_column :jobler_jobs, :locale, :string
4
4
  end
@@ -1,4 +1,4 @@
1
- class AddErrorMessageAndErrorBacktraceToJobs < ActiveRecord::Migration
1
+ class AddErrorMessageAndErrorBacktraceToJobs < ActiveRecord::Migration[5.1]
2
2
  def change
3
3
  add_column :jobler_jobs, :error_message, :text
4
4
  add_column :jobler_jobs, :error_type, :string
@@ -1,4 +1,4 @@
1
- class AddSlugToJobs < ActiveRecord::Migration
1
+ class AddSlugToJobs < ActiveRecord::Migration[5.1]
2
2
  def change
3
3
  add_column :jobler_jobs, :slug, :string
4
4
  add_index :jobler_jobs, :slug, unique: true
@@ -1,4 +1,4 @@
1
- class AddHostProtocolAndPortToJoblerJobs < ActiveRecord::Migration
1
+ class AddHostProtocolAndPortToJoblerJobs < ActiveRecord::Migration[5.1]
2
2
  def change
3
3
  add_column :jobler_jobs, :host, :string
4
4
  add_column :jobler_jobs, :protocol, :string
@@ -1,2 +1,2 @@
1
- class Jobler::BaseController < ActionController::Base
1
+ class Jobler::BaseController < ActionController::Base # rubocop:disable Rails/ApplicationController
2
2
  end
@@ -1,19 +1,53 @@
1
1
  class Jobler::BaseJobler
2
+ attr_accessor :controller, :format
2
3
  attr_reader :args, :job
3
4
 
4
- def create_result!(args)
5
- if args[:temp_file]
6
- temp_file = args.fetch(:temp_file)
7
- temp_file.close unless temp_file.closed?
8
- content = File.read(temp_file.path)
5
+ def self.before_jobling(&blk)
6
+ @@before_jobling ||= [] # rubocop:disable Style/ClassVars
7
+ @@before_jobling << blk
8
+ end
9
+
10
+ def self.after_jobling(&blk)
11
+ @@after_jobling ||= [] # rubocop:disable Style/ClassVars
12
+ @@after_jobling << blk
13
+ end
14
+
15
+ def initialize(args:, job:)
16
+ @args = args
17
+ @job = job
18
+ end
19
+
20
+ def call_before_callbacks
21
+ @@before_jobling&.each do |before_callback|
22
+ instance_eval(&before_callback)
23
+ end
24
+ end
25
+
26
+ def call_after_callbacks
27
+ @@after_jobling&.each do |after_callback|
28
+ instance_eval(&after_callback)
29
+ end
30
+ end
31
+
32
+ def create_result!(content: nil, name:, result: nil, temp_file: nil, save_in_database: false)
33
+ jobler_result = job.results.new(name: name)
34
+
35
+ if content && !temp_file
36
+ temp_file = Tempfile.new(name)
37
+ temp_file.write(content)
38
+ temp_file.close
39
+ end
40
+
41
+ if result
42
+ jobler_result.result = result
9
43
  else
10
- content = args.fetch(:content)
44
+ raise "No tempfile could be found" unless temp_file
45
+
46
+ handle_file(jobler_result: jobler_result, save_in_database: save_in_database, temp_file: temp_file)
11
47
  end
12
48
 
13
- job.results.create!(
14
- name: args.fetch(:name),
15
- result: content
16
- )
49
+ jobler_result.save!
50
+ jobler_result
17
51
  end
18
52
 
19
53
  def execute!
@@ -46,7 +80,7 @@ class Jobler::BaseJobler
46
80
  end
47
81
 
48
82
  if update
49
- job.update_attributes!(progress: new_progress)
83
+ job.update!(progress: new_progress)
50
84
  @_current_progress = new_progress
51
85
  end
52
86
  end
@@ -56,9 +90,7 @@ class Jobler::BaseJobler
56
90
  end
57
91
 
58
92
  def render(template_path, locals = {})
59
- if template_path.is_a?(Symbol)
60
- template_path = "joblers/#{jobler_name}/#{template_path}"
61
- end
93
+ template_path = "joblers/#{jobler_name}/#{template_path}" if template_path.is_a?(Symbol)
62
94
 
63
95
  request = ActionDispatch::Request.new(
64
96
  "HTTP_HOST" => "#{job.host}:#{job.port}",
@@ -70,7 +102,7 @@ class Jobler::BaseJobler
70
102
  controller.request = request
71
103
  controller.response = ActionDispatch::Response.new
72
104
 
73
- render_result = controller.render(template_path, formats: Mime::EXTENSION_LOOKUP.keys, layout: false, locals: {jobler: self}.merge(locals))
105
+ render_result = controller.render(template_path, layout: false, locals: {jobler: self}.merge(locals))
74
106
 
75
107
  if render_result.is_a?(String)
76
108
  # Rails 5 behaviour
@@ -85,15 +117,37 @@ class Jobler::BaseJobler
85
117
  raise NoMethodError, "You should define the 'result' method on #{self.class.name}"
86
118
  end
87
119
 
88
- def temp_file_for_result(args)
89
- job_result = job.results.where(name: args.fetch(:name)).first
120
+ def temp_file_for_result(name:)
121
+ job_result = job.results.where(name: name).first
122
+ raise "No result by that name: #{name}" unless job_result
90
123
 
91
- raise "No result by that name: #{args.fetch(:name)}" unless job_result
92
-
93
- temp_file = Tempfile.new
124
+ temp_file = ::Tempfile.new("jobler_tempfile")
94
125
  temp_file.binmode
95
126
  temp_file.write(job_result.result)
96
127
  temp_file.close
97
128
  temp_file
98
129
  end
130
+
131
+ def url_for_result(name:)
132
+ job_result = job.results.where(name: name).first
133
+ raise "No result by that name: #{name}" unless job_result
134
+
135
+ Rails.application.routes.url_helpers.rails_blob_path(job_result.file.attachment, only_path: true)
136
+ end
137
+
138
+ private
139
+
140
+ def handle_file(jobler_result:, save_in_database:, temp_file:)
141
+ if save_in_database
142
+ temp_file = temp_file
143
+ temp_file.close unless temp_file.closed?
144
+ content = File.read(temp_file.path)
145
+ jobler_result.result = content
146
+ else
147
+ jobler_result.file.attach(
148
+ filename: File.basename(temp_file.path),
149
+ io: File.open(temp_file.path)
150
+ )
151
+ end
152
+ end
99
153
  end
@@ -1,8 +1,12 @@
1
1
  class Jobler::FileDownload
2
- attr_reader :file_name, :temp_file
2
+ attr_reader :file_name, :temp_file, :url
3
3
 
4
- def initialize(args)
5
- @file_name = args.fetch(:file_name)
6
- @temp_file = args.fetch(:temp_file)
4
+ def initialize(file_name: nil, temp_file: nil, url: nil)
5
+ raise "Temp file or URL should be given" if !temp_file && !url
6
+ raise "No filename given with temp-file" if temp_file && file_name.blank?
7
+
8
+ @file_name = file_name
9
+ @temp_file = temp_file
10
+ @url = url
7
11
  end
8
12
  end
@@ -1,18 +1,24 @@
1
- class Jobler::JobRunner < ActiveJob::Base
1
+ class Jobler::JobRunner < ActiveJob::Base # rubocop:disable Rails/ApplicationJob
2
2
  queue_as :jobler
3
3
 
4
4
  def perform(job_id)
5
5
  @job = Jobler::Job.find(job_id)
6
- @job.update_attributes!(started_at: Time.zone.now, state: "started")
6
+ @job.update!(started_at: Time.zone.now, state: "started")
7
7
 
8
8
  begin
9
9
  with_locale do
10
- @job.jobler.execute!
10
+ @job.jobler.call_before_callbacks
11
+
12
+ begin
13
+ @job.jobler.execute!
14
+ ensure
15
+ @job.jobler.call_after_callbacks
16
+ end
11
17
  end
12
18
 
13
- @job.update_attributes!(ended_at: Time.zone.now, progress: 1.0, state: "completed")
19
+ @job.update!(ended_at: Time.zone.now, progress: 1.0, state: "completed")
14
20
  rescue Exception => e # rubocop:disable Lint/RescueException
15
- @job.update_attributes!(
21
+ @job.update!(
16
22
  ended_at: Time.zone.now,
17
23
  error_message: e.message,
18
24
  error_type: e.class.name,
@@ -1,3 +1,3 @@
1
1
  module Jobler
2
- VERSION = "0.0.8".freeze
2
+ VERSION = "0.0.13".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jobler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - kaspernj
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-10 00:00:00.000000000 Z
11
+ date: 2020-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4'
19
+ version: 6.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '4'
26
+ version: 6.0.0
27
27
  description: Generate pages or files in the background
28
28
  email:
29
29
  - kaspernj@gmail.com
@@ -43,6 +43,7 @@ files:
43
43
  - app/controllers/jobler/downloads_controller.rb
44
44
  - app/controllers/jobler/jobs_controller.rb
45
45
  - app/helpers/jobler/application_helper.rb
46
+ - app/joblers/jobler/models/destroyer_jobler.rb
46
47
  - app/models/jobler/job.rb
47
48
  - app/models/jobler/result.rb
48
49
  - app/views/jobler/jobs/show.html.erb
@@ -84,8 +85,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
84
85
  - !ruby/object:Gem::Version
85
86
  version: '0'
86
87
  requirements: []
87
- rubyforge_project:
88
- rubygems_version: 2.6.8
88
+ rubygems_version: 3.0.6
89
89
  signing_key:
90
90
  specification_version: 4
91
91
  summary: Generate pages or files in the background