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 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,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/budget_bytes_cli'
4
+
5
+ BudgetBytesCli::CLI.new.call
@@ -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,4 @@
1
+ module BudgetBytesCli
2
+ end
3
+
4
+ require_relative "../config/environment"
@@ -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: []