budget-bytes-cli 0.2.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 +7 -0
- data/bin/budget-bytes-cli +5 -0
- data/config/environment.rb +11 -0
- data/lib/budget_bytes_cli.rb +4 -0
- data/lib/budget_bytes_cli/array-prompter.rb +125 -0
- data/lib/budget_bytes_cli/category-scraper.rb +18 -0
- data/lib/budget_bytes_cli/category.rb +63 -0
- data/lib/budget_bytes_cli/cli.rb +116 -0
- data/lib/budget_bytes_cli/recipe.rb +34 -0
- metadata +137 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0bd890826cf37793882a38c8668b25bfb0c01829
|
4
|
+
data.tar.gz: 4045d839bf92d038f32cd595a0bb01467e5b6190
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b3cc756a4d4f64483f2d3a082052940fd6d64ac516524d467c0d3d3159eb51034173f744bb24b736dc3c2c609000572a781c08413b5f1d0544689cdebf585859
|
7
|
+
data.tar.gz: b7fef8f19afd5b4099bfd3e0fd5d5ea607d730ad067994202b746f8ee8c9baeba8692ec80558eb4e8b4eafc0c4571877c0cadf9855a321a91103dd96b9fdb508
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'pry'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'launchy'
|
5
|
+
require 'io/console'
|
6
|
+
|
7
|
+
require_relative '../lib/budget_bytes_cli/cli'
|
8
|
+
require_relative '../lib/budget_bytes_cli/category-scraper'
|
9
|
+
require_relative '../lib/budget_bytes_cli/category'
|
10
|
+
require_relative '../lib/budget_bytes_cli/recipe'
|
11
|
+
require_relative '../lib/budget_bytes_cli/array-prompter'
|
@@ -0,0 +1,125 @@
|
|
1
|
+
#ArrayPrompter uses an ArraySelector for blocks of 20 (if needed) and
|
2
|
+
#an ArraySelector for the block of 20 chosen, returns the overall selection
|
3
|
+
class BudgetBytesCli::ArrayPrompter
|
4
|
+
attr_accessor :prompt_text, :array_to_select
|
5
|
+
|
6
|
+
def initialize(prompt_text = "")
|
7
|
+
@block_selector = BudgetBytesCli::ArraySelector.new
|
8
|
+
@item_selector = BudgetBytesCli::ArraySelector.new(true)
|
9
|
+
@prompt_text = prompt_text
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_input
|
13
|
+
num_blocks = (self.array_to_select.length.to_f / 20.to_f).ceil
|
14
|
+
|
15
|
+
#makes sure variable is outside if statement scope
|
16
|
+
input_selected = 1
|
17
|
+
|
18
|
+
@block_selector.prompt_text = self.prompt_text + "\nPlease select a range below"
|
19
|
+
|
20
|
+
#create array for block selector
|
21
|
+
@block_selector.array_to_select = []
|
22
|
+
(1..num_blocks).each do |n|
|
23
|
+
to_add = nil
|
24
|
+
if ((n-1) * 20 + 1) == self.array_to_select.length
|
25
|
+
to_add = "#{(n-1) * 20 + 1}"
|
26
|
+
else
|
27
|
+
to_add = "#{(n - 1) * 20 + 1}-#{[(n * 20), self.array_to_select.length].min}"
|
28
|
+
end
|
29
|
+
|
30
|
+
@block_selector.array_to_select << to_add
|
31
|
+
end
|
32
|
+
|
33
|
+
#fixes issue with back being displayed in item selector when only one block
|
34
|
+
if @block_selector.array_to_select.length == 1
|
35
|
+
@item_selector.back_allowed = false
|
36
|
+
end
|
37
|
+
|
38
|
+
input_selected = @block_selector.get_input
|
39
|
+
|
40
|
+
if input_selected != 'Q'
|
41
|
+
#Gets input using item_selector ArraySelector
|
42
|
+
selected_value = input_selected.to_i
|
43
|
+
@item_selector.prompt_text = self.prompt_text + "\nPlease select an item below."
|
44
|
+
block_min = (selected_value - 1) * 20
|
45
|
+
block_max = [(selected_value * 20) - 1, self.array_to_select.length].min
|
46
|
+
@item_selector.array_to_select = self.array_to_select[block_min..block_max]
|
47
|
+
second_selection = @item_selector.get_input
|
48
|
+
if second_selection == 'Q'
|
49
|
+
input_selected = second_selection
|
50
|
+
elsif second_selection == 'B'
|
51
|
+
#call function recursively to go back to prior input
|
52
|
+
input_selected = self.get_input
|
53
|
+
else
|
54
|
+
input_selected = (second_selection.to_i + (selected_value - 1) * 20).to_s
|
55
|
+
end
|
56
|
+
end
|
57
|
+
input_selected
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
#ArraySelector selects from an array (helper class for ArrayPrompter)
|
62
|
+
class BudgetBytesCli::ArraySelector
|
63
|
+
attr_accessor :prompt_text, :array_to_select
|
64
|
+
|
65
|
+
def back_allowed= (value)
|
66
|
+
@back_allowed = value
|
67
|
+
if value
|
68
|
+
@allowed_chars = ['Q', 'B']
|
69
|
+
else
|
70
|
+
@allowed_chars = ['Q']
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def initialize(back_allowed = false)
|
75
|
+
@back_allowed = back_allowed
|
76
|
+
@allowed_chars = ['Q']
|
77
|
+
if @back_allowed
|
78
|
+
@allowed_chars << 'B'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def last_item
|
83
|
+
self.array_to_select.length
|
84
|
+
end
|
85
|
+
|
86
|
+
def prompt(text_prompt = nil)
|
87
|
+
if text_prompt
|
88
|
+
self.prompt_text = text_prompt
|
89
|
+
end
|
90
|
+
|
91
|
+
puts self.prompt_text
|
92
|
+
self.array_to_select.each_with_index do |menu_item, idx|
|
93
|
+
puts "#{idx + 1}. #{menu_item}"
|
94
|
+
end
|
95
|
+
if @back_allowed
|
96
|
+
puts "Or enter 'B' to go back to the previous menu"
|
97
|
+
end
|
98
|
+
|
99
|
+
puts "Or enter 'Q' to quit"
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_input
|
103
|
+
input = ""
|
104
|
+
valid_input = false
|
105
|
+
|
106
|
+
#for corner case where only one item in array
|
107
|
+
if array_to_select.length == 1
|
108
|
+
input = "1"
|
109
|
+
valid_input = true
|
110
|
+
end
|
111
|
+
|
112
|
+
while !valid_input
|
113
|
+
self.prompt
|
114
|
+
input = gets.strip.upcase
|
115
|
+
if @allowed_chars.include?(input)
|
116
|
+
valid_input = true
|
117
|
+
elsif input.to_i.to_s == input && input.to_i >= 1 && input.to_i <= self.last_item
|
118
|
+
valid_input = true
|
119
|
+
else
|
120
|
+
puts "Invalid input, please try again."
|
121
|
+
end
|
122
|
+
end
|
123
|
+
input
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class BudgetBytesCli::CategoryScraper
|
2
|
+
|
3
|
+
def open_page
|
4
|
+
Nokogiri::HTML(open("https://www.budgetbytes.com/recipes/"))
|
5
|
+
end
|
6
|
+
|
7
|
+
def locate_categories
|
8
|
+
open_page.css(".cat-item")
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_categories
|
12
|
+
locate_categories.each do |item|
|
13
|
+
url = item.css("a").attribute("href").value
|
14
|
+
title = item.css("a").children[0].text
|
15
|
+
BudgetBytesCli::Category.new(url, title)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class BudgetBytesCli::Category
|
2
|
+
attr_reader :url, :name
|
3
|
+
|
4
|
+
@@all = []
|
5
|
+
|
6
|
+
def self.all
|
7
|
+
@@all
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(url = nil, name = nil)
|
11
|
+
@name = name
|
12
|
+
@url = url
|
13
|
+
@@all << self
|
14
|
+
end
|
15
|
+
|
16
|
+
def recipes
|
17
|
+
self.get_recipes unless @recipes
|
18
|
+
@recipes
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_recipes
|
22
|
+
@recipes = []
|
23
|
+
first_page = Nokogiri::HTML(open(self.url))
|
24
|
+
|
25
|
+
page_nums = first_page.css(".page-numbers")
|
26
|
+
if page_nums.empty?
|
27
|
+
pages_total = 1
|
28
|
+
else
|
29
|
+
pages_total = page_nums.map{|p| p.text.to_i}.max
|
30
|
+
end
|
31
|
+
|
32
|
+
(1..pages_total).each do |p|
|
33
|
+
get_recipes_from(create_page_url(p))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_recipes_from(page_url)
|
38
|
+
recipe_page = Nokogiri::HTML(open(page_url))
|
39
|
+
recipe_links = recipe_page.css(".archive-post a")
|
40
|
+
recipe_links.each do |r|
|
41
|
+
recipe_title = r.attribute("title").value
|
42
|
+
recipe_url = r.attribute("href").value
|
43
|
+
@recipes << BudgetBytesCli::Recipe.new(recipe_url, recipe_title)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_page_url(num)
|
48
|
+
if num == 1
|
49
|
+
self.url
|
50
|
+
else
|
51
|
+
self.url + "page/" + num.to_s + "/"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def combine_recipes(cat_to_combine)
|
56
|
+
recipes_combined = cat_to_combine.recipes
|
57
|
+
recipe_urls = self.recipes.map {|r| r.url}
|
58
|
+
filtered_recipes = recipes_combined.select do |r|
|
59
|
+
recipe_urls.include?(r.url)
|
60
|
+
end
|
61
|
+
filtered_recipes
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
class BudgetBytesCli::CLI
|
2
|
+
|
3
|
+
def call
|
4
|
+
puts "Welcome to Budget Bytes CLI!"
|
5
|
+
scraper = BudgetBytesCli::CategoryScraper.new
|
6
|
+
scraper.create_categories
|
7
|
+
category_selector = BudgetBytesCli::ArrayPrompter.new("Selecting recipe category.")
|
8
|
+
category_selector.array_to_select = BudgetBytesCli::Category.all.map {|i| i.name}
|
9
|
+
|
10
|
+
recipe_selector = BudgetBytesCli::ArrayPrompter.new("Selecting recipe")
|
11
|
+
|
12
|
+
cat_combination_selector = BudgetBytesCli::ArrayPrompter.new("Selecting category to combine.")
|
13
|
+
|
14
|
+
current_selector = category_selector
|
15
|
+
selection = category_selector.get_input
|
16
|
+
|
17
|
+
#define variables outside of while loop scope
|
18
|
+
selected_category = nil
|
19
|
+
filtered_categories = []
|
20
|
+
recipe_array = []
|
21
|
+
|
22
|
+
while selection != 'Q'
|
23
|
+
if current_selector == category_selector
|
24
|
+
selected_category = BudgetBytesCli::Category.all[selection.to_i - 1]
|
25
|
+
whether_to_combine = yes_no_input("Combine with another category?\nIn other words, display only recipes in both the current category and another you select?\n")
|
26
|
+
if whether_to_combine == 'Y'
|
27
|
+
current_selector = cat_combination_selector
|
28
|
+
filtered_categories = BudgetBytesCli::Category.all.select do |c|
|
29
|
+
c != selected_category
|
30
|
+
end
|
31
|
+
cat_combination_selector.array_to_select = filtered_categories.map {|i| i.name}
|
32
|
+
else
|
33
|
+
current_selector = recipe_selector
|
34
|
+
recipe_array = selected_category.recipes
|
35
|
+
recipe_selector.array_to_select = selected_category.recipes.map {|i| i.name}
|
36
|
+
end
|
37
|
+
elsif current_selector == cat_combination_selector
|
38
|
+
combination_category = filtered_categories[selection.to_i - 1]
|
39
|
+
recipe_array = selected_category.combine_recipes(combination_category)
|
40
|
+
if recipe_array.empty?
|
41
|
+
valid_input_empty_array = false
|
42
|
+
while !valid_input_empty_array
|
43
|
+
puts "No recipes are in the two categories to combine."
|
44
|
+
puts "Enter 'B' to select a different category to combine,"
|
45
|
+
puts "'C' to start fresh with a different recipe category,"
|
46
|
+
puts "or 'I' to ignore the recipe combination and use the category you chose."
|
47
|
+
empty_array_input = gets.strip.upcase
|
48
|
+
if empty_array_input == 'B'
|
49
|
+
current_selector = cat_combination_selector
|
50
|
+
#not necessary, but makes explicit that we're running this again
|
51
|
+
valid_input_empty_array = true
|
52
|
+
elsif empty_array_input == 'C'
|
53
|
+
valid_input_empty_array = true
|
54
|
+
current_selector = category_selector
|
55
|
+
elsif empty_array_input == 'I'
|
56
|
+
valid_input_empty_array = true
|
57
|
+
current_selector = recipe_selector
|
58
|
+
recipe_array = selected_category.recipes
|
59
|
+
recipe_selector.array_to_select = selected_category.recipes.map {|i| i.name}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
else
|
63
|
+
current_selector = recipe_selector
|
64
|
+
recipe_selector.array_to_select = recipe_array.map {|i| i.name}
|
65
|
+
end
|
66
|
+
else
|
67
|
+
selected_recipe = recipe_array[selection.to_i - 1]
|
68
|
+
self.display_recipe(selected_recipe)
|
69
|
+
current_selector = category_selector
|
70
|
+
end
|
71
|
+
selection = current_selector.get_input
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def display_recipe(recipe_chosen)
|
76
|
+
puts recipe_chosen.name
|
77
|
+
puts "\nIngredients\n"
|
78
|
+
puts recipe_chosen.ingredients
|
79
|
+
puts ""
|
80
|
+
page_width = IO.console.winsize[1]
|
81
|
+
puts reformat_wrapped(recipe_chosen.instructions, page_width || 80)
|
82
|
+
puts ""
|
83
|
+
if yes_no_input("Do you want to open this recipe in your browser?") == 'Y'
|
84
|
+
Launchy.open(recipe_chosen.url)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def yes_no_input (prompt)
|
89
|
+
puts prompt + " Please answer 'y' or 'n'"
|
90
|
+
input = gets.strip.upcase
|
91
|
+
while !['Y', 'N'].include?(input)
|
92
|
+
puts "Invalid input."
|
93
|
+
puts prompt + " Please answer 'y' or 'n'"
|
94
|
+
input = gets.strip.upcase
|
95
|
+
end
|
96
|
+
input
|
97
|
+
end
|
98
|
+
|
99
|
+
#from https://www.safaribooksonline.com/library/view/ruby-cookbook/0596523696/ch01s15.html
|
100
|
+
def reformat_wrapped(s, width= 78)
|
101
|
+
lines = []
|
102
|
+
line = ""
|
103
|
+
s.split(/\s+/).each do |word|
|
104
|
+
if line.size + word.size >= width
|
105
|
+
lines << line
|
106
|
+
line = word
|
107
|
+
elsif line.empty?
|
108
|
+
line = word
|
109
|
+
else
|
110
|
+
line << " " << word
|
111
|
+
end
|
112
|
+
end
|
113
|
+
lines << line if line
|
114
|
+
return lines.join "\n"
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class BudgetBytesCli::Recipe
|
2
|
+
attr_reader :url, :name
|
3
|
+
|
4
|
+
def initialize(url = nil, name = nil)
|
5
|
+
@name = name
|
6
|
+
@url = url
|
7
|
+
end
|
8
|
+
|
9
|
+
def scrape_recipe
|
10
|
+
page = Nokogiri::HTML(open(@url))
|
11
|
+
|
12
|
+
ingredient_amounts = page.css('.wprm-recipe-ingredient-amount').map {|i| i.text}
|
13
|
+
ingredient_units = page.css('.wprm-recipe-ingredient-unit').map {|i| i.text}
|
14
|
+
ingredient_names = page.css('.wprm-recipe-ingredient-name').map {|i| i.text}
|
15
|
+
|
16
|
+
ingredient_array = ingredient_amounts.each_with_index.map do |ele, idx|
|
17
|
+
[ele, ingredient_units[idx], ingredient_names[idx]].join(' ').strip
|
18
|
+
end
|
19
|
+
|
20
|
+
@ingredients = ingredient_array.join("\n")
|
21
|
+
@instructions = page.css(".wprm-recipe-instruction-text").map {|i| i.text}.join("\n")
|
22
|
+
end
|
23
|
+
|
24
|
+
def ingredients
|
25
|
+
self.scrape_recipe unless @ingredients
|
26
|
+
@ingredients
|
27
|
+
end
|
28
|
+
|
29
|
+
def instructions
|
30
|
+
self.scrape_recipe unless @instructions
|
31
|
+
@instructions
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: budget-bytes-cli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Larry Weinstein
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-11-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.15'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.15'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: nokogiri
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: launchy
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Currently in Beta, please report bugs to the author.
|
98
|
+
email:
|
99
|
+
- lawrence.e.weinstein@gmail.com
|
100
|
+
executables:
|
101
|
+
- budget-bytes-cli
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- bin/budget-bytes-cli
|
106
|
+
- config/environment.rb
|
107
|
+
- lib/budget_bytes_cli.rb
|
108
|
+
- lib/budget_bytes_cli/array-prompter.rb
|
109
|
+
- lib/budget_bytes_cli/category-scraper.rb
|
110
|
+
- lib/budget_bytes_cli/category.rb
|
111
|
+
- lib/budget_bytes_cli/cli.rb
|
112
|
+
- lib/budget_bytes_cli/recipe.rb
|
113
|
+
homepage: https://github.com/Larry-42/budget-bytes-cli
|
114
|
+
licenses:
|
115
|
+
- MIT
|
116
|
+
metadata: {}
|
117
|
+
post_install_message:
|
118
|
+
rdoc_options: []
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
requirements: []
|
132
|
+
rubyforge_project:
|
133
|
+
rubygems_version: 2.4.8
|
134
|
+
signing_key:
|
135
|
+
specification_version: 4
|
136
|
+
summary: A CLI interface for Budget Bytes, a recipe blog.
|
137
|
+
test_files: []
|