tea_shopper 0.3.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/.DS_Store +0 -0
- data/.gitignore +50 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +47 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/PROCESS.md +185 -0
- data/README.md +45 -0
- data/Rakefile +6 -0
- data/assets/Tea_Shopper-welcome.png +0 -0
- data/bin/console +61 -0
- data/bin/setup +8 -0
- data/bin/tea_shopper +4 -0
- data/config/environment.rb +9 -0
- data/lib/cli.rb +262 -0
- data/lib/song_scraper.rb +128 -0
- data/lib/tea.rb +64 -0
- data/lib/tea_shopper.rb +4 -0
- data/lib/tea_shopper/version.rb +3 -0
- data/tea_shopper.gemspec +32 -0
- metadata +112 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 621092287bb9081fb013b75011e01e05682560c24e05de32f0850af2d155b43f
|
|
4
|
+
data.tar.gz: bd6be1d7ec7d2ba68e7b159c68eb79cfa892de0543b6fb064a6351d3e0bf20c1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0db8a83db852036c5d09c2414eb8c75d92b8a752a07fe0f15ff6f4dc5420e33d0e433ed52b61ef453ec95c867b93693266e923bcdcc6c87d1564cd1aadc0bb34
|
|
7
|
+
data.tar.gz: 26680cc506c864b823c73301a66006cbc4ea039c3bcbdce1d150b6d23fe9ef2bc68e982518ab2077d311ce576b7b370805dd87dc44acc51dad8aa864e01095a1
|
data/.DS_Store
ADDED
|
Binary file
|
data/.gitignore
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
*.gem
|
|
2
|
+
*.rbc
|
|
3
|
+
/.config
|
|
4
|
+
/coverage/
|
|
5
|
+
/InstalledFiles
|
|
6
|
+
/pkg/
|
|
7
|
+
/spec/reports/
|
|
8
|
+
/spec/examples.txt
|
|
9
|
+
/test/tmp/
|
|
10
|
+
/test/version_tmp/
|
|
11
|
+
/tmp/
|
|
12
|
+
|
|
13
|
+
# Used by dotenv library to load environment variables.
|
|
14
|
+
# .env
|
|
15
|
+
|
|
16
|
+
## Specific to RubyMotion:
|
|
17
|
+
.dat*
|
|
18
|
+
.repl_history
|
|
19
|
+
build/
|
|
20
|
+
*.bridgesupport
|
|
21
|
+
build-iPhoneOS/
|
|
22
|
+
build-iPhoneSimulator/
|
|
23
|
+
|
|
24
|
+
## Specific to RubyMotion (use of CocoaPods):
|
|
25
|
+
#
|
|
26
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
|
27
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
|
28
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
|
29
|
+
#
|
|
30
|
+
# vendor/Pods/
|
|
31
|
+
|
|
32
|
+
## Documentation cache and generated files:
|
|
33
|
+
/.yardoc/
|
|
34
|
+
/_yardoc/
|
|
35
|
+
/doc/
|
|
36
|
+
/rdoc/
|
|
37
|
+
|
|
38
|
+
## Environment normalization:
|
|
39
|
+
/.bundle/
|
|
40
|
+
/vendor/bundle
|
|
41
|
+
/lib/bundler/man/
|
|
42
|
+
|
|
43
|
+
# for a library or gem, you might want to ignore these files since the code is
|
|
44
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
45
|
+
# Gemfile.lock
|
|
46
|
+
# .ruby-version
|
|
47
|
+
# .ruby-gemset
|
|
48
|
+
|
|
49
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
|
50
|
+
.rvmrc
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
|
10
|
+
orientation.
|
|
11
|
+
|
|
12
|
+
## Our Standards
|
|
13
|
+
|
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
|
15
|
+
include:
|
|
16
|
+
|
|
17
|
+
* Using welcoming and inclusive language
|
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
|
19
|
+
* Gracefully accepting constructive criticism
|
|
20
|
+
* Focusing on what is best for the community
|
|
21
|
+
* Showing empathy towards other community members
|
|
22
|
+
|
|
23
|
+
Examples of unacceptable behavior by participants include:
|
|
24
|
+
|
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
26
|
+
advances
|
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
28
|
+
* Public or private harassment
|
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
|
30
|
+
address, without explicit permission
|
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
32
|
+
professional setting
|
|
33
|
+
|
|
34
|
+
## Our Responsibilities
|
|
35
|
+
|
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
|
38
|
+
response to any instances of unacceptable behavior.
|
|
39
|
+
|
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
|
44
|
+
threatening, offensive, or harmful.
|
|
45
|
+
|
|
46
|
+
## Scope
|
|
47
|
+
|
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
|
49
|
+
when an individual is representing the project or its community. Examples of
|
|
50
|
+
representing a project or community include using an official project e-mail
|
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
|
53
|
+
further defined and clarified by project maintainers.
|
|
54
|
+
|
|
55
|
+
## Enforcement
|
|
56
|
+
|
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
58
|
+
reported by contacting the project team at aaron.parkening@gmail.com. All
|
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
|
63
|
+
|
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
|
66
|
+
members of the project's leadership.
|
|
67
|
+
|
|
68
|
+
## Attribution
|
|
69
|
+
|
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
|
72
|
+
|
|
73
|
+
[homepage]: http://contributor-covenant.org
|
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
tea_shopper (0.3.0)
|
|
5
|
+
|
|
6
|
+
GEM
|
|
7
|
+
remote: https://rubygems.org/
|
|
8
|
+
specs:
|
|
9
|
+
coderay (1.1.2)
|
|
10
|
+
colorize (0.8.1)
|
|
11
|
+
diff-lcs (1.3)
|
|
12
|
+
method_source (0.9.2)
|
|
13
|
+
mini_portile2 (2.4.0)
|
|
14
|
+
nokogiri (1.10.3)
|
|
15
|
+
mini_portile2 (~> 2.4.0)
|
|
16
|
+
pry (0.12.2)
|
|
17
|
+
coderay (~> 1.1.0)
|
|
18
|
+
method_source (~> 0.9.0)
|
|
19
|
+
rake (10.5.0)
|
|
20
|
+
rspec (3.8.0)
|
|
21
|
+
rspec-core (~> 3.8.0)
|
|
22
|
+
rspec-expectations (~> 3.8.0)
|
|
23
|
+
rspec-mocks (~> 3.8.0)
|
|
24
|
+
rspec-core (3.8.0)
|
|
25
|
+
rspec-support (~> 3.8.0)
|
|
26
|
+
rspec-expectations (3.8.3)
|
|
27
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
28
|
+
rspec-support (~> 3.8.0)
|
|
29
|
+
rspec-mocks (3.8.0)
|
|
30
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
31
|
+
rspec-support (~> 3.8.0)
|
|
32
|
+
rspec-support (3.8.0)
|
|
33
|
+
|
|
34
|
+
PLATFORMS
|
|
35
|
+
ruby
|
|
36
|
+
|
|
37
|
+
DEPENDENCIES
|
|
38
|
+
bundler (~> 2.0)
|
|
39
|
+
colorize
|
|
40
|
+
nokogiri (~> 1.6)
|
|
41
|
+
pry
|
|
42
|
+
rake (~> 10.0)
|
|
43
|
+
rspec (~> 3.0)
|
|
44
|
+
tea_shopper!
|
|
45
|
+
|
|
46
|
+
BUNDLED WITH
|
|
47
|
+
2.0.1
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Aaron Parkening
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 aparkening
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/PROCESS.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# My Gem Development Process
|
|
2
|
+
|
|
3
|
+
1. Plan gem, imagine interface
|
|
4
|
+
|
|
5
|
+
- Welcome to Tea Shopper! There are many teas to choose from. Choose a way to explore from the list below:
|
|
6
|
+
- Type
|
|
7
|
+
- Country of origin
|
|
8
|
+
- Flavor
|
|
9
|
+
|
|
10
|
+
-> "Type" chosen
|
|
11
|
+
Type
|
|
12
|
+
We have eight main tea categories to choose from. Select from the list below to explore available teas.
|
|
13
|
+
- Green
|
|
14
|
+
- White
|
|
15
|
+
- Yellow
|
|
16
|
+
- Oolong
|
|
17
|
+
- Black
|
|
18
|
+
- Pu-er
|
|
19
|
+
- Rooibos
|
|
20
|
+
- Herbal
|
|
21
|
+
|
|
22
|
+
-> "Green" chosen
|
|
23
|
+
[General description (scraped from Virtuous Tea?)]
|
|
24
|
+
[Ordering and selection text in single module/method]
|
|
25
|
+
|
|
26
|
+
"The teas below are ordered alphabetically.
|
|
27
|
+
Type a specific tea name to learn more about its flavors, steeping instructions, description, and where to buy it."
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
-> "Country" chosen
|
|
31
|
+
Country of origin
|
|
32
|
+
Select from the tea-producing countries below to explore available teas.
|
|
33
|
+
- China
|
|
34
|
+
- Japan
|
|
35
|
+
- Taiwan
|
|
36
|
+
- [other scraped countries]
|
|
37
|
+
|
|
38
|
+
-> "China" chosen
|
|
39
|
+
[General description?]
|
|
40
|
+
[Ordering and selection text in single module/method]
|
|
41
|
+
|
|
42
|
+
"The teas below are ordered alphabetically.
|
|
43
|
+
Type a specific tea name to learn more about its flavors, steeping instructions, description, and where to buy it."
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
-> "Flavor" selected
|
|
47
|
+
Select from the flavor profiles below to explore available teas.
|
|
48
|
+
- Fruity
|
|
49
|
+
- Vegetal
|
|
50
|
+
- [other screaped categories]
|
|
51
|
+
|
|
52
|
+
-> "Fruity" selected
|
|
53
|
+
[General description?]
|
|
54
|
+
[Ordering and selection text in single module/method]
|
|
55
|
+
|
|
56
|
+
"The teas below are ordered alphabetically.
|
|
57
|
+
Type a specific tea name to learn more about its flavors, steeping instructions, description, and where to buy it."
|
|
58
|
+
|
|
59
|
+
-> Specific tea view
|
|
60
|
+
[Tea object]
|
|
61
|
+
- Title (w/ tea type)
|
|
62
|
+
- Tea shop (w/ url)
|
|
63
|
+
- Price per oz (w/ stock)
|
|
64
|
+
- Flavors
|
|
65
|
+
- Steep instructions
|
|
66
|
+
- Number of infusions (if exist)
|
|
67
|
+
- Full description
|
|
68
|
+
- Ingredients (if exist)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
2. Outline project structure
|
|
72
|
+
Bin:
|
|
73
|
+
Run
|
|
74
|
+
Console
|
|
75
|
+
Config:
|
|
76
|
+
Environment
|
|
77
|
+
Lib:
|
|
78
|
+
CLI interaction file
|
|
79
|
+
Scraper classes
|
|
80
|
+
Tea class
|
|
81
|
+
Rspec:
|
|
82
|
+
Spec files (if exist)
|
|
83
|
+
Rakefile
|
|
84
|
+
Gemfile
|
|
85
|
+
|
|
86
|
+
Build initial structure via Bundler: https://bundler.io/v2.0/guides/creating_gem.html
|
|
87
|
+
`$ bundle gem tea_shopper`
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
3. Push project to Github, since Bundler creates Github repo.
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
4. Start with run file and interface file
|
|
94
|
+
Run:
|
|
95
|
+
bin/tea_shopper
|
|
96
|
+
- view permissions of file
|
|
97
|
+
`ls -lah`
|
|
98
|
+
- add execute permissions
|
|
99
|
+
`chmod +x tea_shopper`
|
|
100
|
+
|
|
101
|
+
Start interface instance/CLI controller: `TeaShopper::CLI.new.run`
|
|
102
|
+
|
|
103
|
+
Interface:
|
|
104
|
+
`lib/cli.rb`
|
|
105
|
+
a) `puts "hello world"` to verify working correctly
|
|
106
|
+
b) Stub run method to welcome user and list core methods that will run
|
|
107
|
+
c) Stub menu method to give user initial menu and take input
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
5. Stub remaining interface
|
|
111
|
+
a) Hardcode to show display details
|
|
112
|
+
b) Replace each section with dynamic code.
|
|
113
|
+
|
|
114
|
+
- Create submenus
|
|
115
|
+
- Create Tea class
|
|
116
|
+
- Scrape first site: Song Tea & Ceramics
|
|
117
|
+
- Create tea display filters for:
|
|
118
|
+
- Type
|
|
119
|
+
[Removed] Region
|
|
120
|
+
[Removed] Flavors
|
|
121
|
+
[Removed] Price
|
|
122
|
+
- Display Tea object categories in Type menu
|
|
123
|
+
- Display selected Tea object
|
|
124
|
+
- Screencast code along
|
|
125
|
+
- Find screencast tool
|
|
126
|
+
- Record screencast
|
|
127
|
+
- Refactor to split primary and secondary scrapes
|
|
128
|
+
[Removed] Display Tea objects in Region menus
|
|
129
|
+
[Removed] Display Tea objects in Flavors menus
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
6. Run program and tweak
|
|
133
|
+
- Improve error handling and responding to edge cases
|
|
134
|
+
- Refactor to remove duplicated work and separate concerns
|
|
135
|
+
- Clean up tea_shopper.gemspec for production
|
|
136
|
+
- Refactor for gem best practices (module names, file locations, etc.)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
7. Update Readme to include installation, about, etc., following pattern in https://gist.github.com/PurpleBooth/109311bb0361f32d87a2
|
|
140
|
+
- Update Code_of_Conduct.md
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
8. Run Bundler release (https://bundler.io/v2.0/guides/creating_gem.html) to move into production mode and push to github.
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
9. Record demonstration w/ narration
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
10. Write blog post about process
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
11. Submit Flatiron checklist
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
12. Upload gem after 1:1 code review
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
## Stretch goals:
|
|
159
|
+
|
|
160
|
+
[done!] Type 'exit' any time to leave program
|
|
161
|
+
|
|
162
|
+
[done!] Allow user to type tea name or number from list
|
|
163
|
+
|
|
164
|
+
3. Scrape more sites:
|
|
165
|
+
- Dobra
|
|
166
|
+
1. 8 index pages: https://www.dobratea.com/
|
|
167
|
+
div#second-menu ul#menu-second-nav-header li (don't get accessories)
|
|
168
|
+
- Get second page if not "showing all" results: https://www.dobratea.com/category/green/page/2/
|
|
169
|
+
2. Detail pages:
|
|
170
|
+
https://www.dobratea.com/product/assam-brahmaputra/
|
|
171
|
+
|
|
172
|
+
- Smith - https://www.smithtea.com/collections/all-tea?sort_by=title-ascending
|
|
173
|
+
|
|
174
|
+
Perhaps in the future
|
|
175
|
+
- Virtuous
|
|
176
|
+
- Harney
|
|
177
|
+
- Happy Lucky's
|
|
178
|
+
- TeaSource
|
|
179
|
+
- Tea Spot
|
|
180
|
+
|
|
181
|
+
- Refactor Tea instantiaton for multiple scrapers
|
|
182
|
+
|
|
183
|
+
4. Pagination of long lists
|
|
184
|
+
|
|
185
|
+
5. Write rspec tests
|
data/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Tea Shopper
|
|
2
|
+
|
|
3
|
+
Tea Shopper is a command line interface that scrapes tea data from the web and allows users to compare teas by name, price per ounce, and tea shop. When the user chooses a tea, it displays specific details, such as purchase URL, flavors, region, and description.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
```ruby
|
|
9
|
+
gem 'tea_shopper'
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
And then execute:
|
|
13
|
+
```
|
|
14
|
+
$ bundle
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or install it yourself as:
|
|
18
|
+
```
|
|
19
|
+
$ gem install tea_shopper
|
|
20
|
+
```
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Type the code below and follow the prompts to find your next great tea.
|
|
24
|
+
```
|
|
25
|
+
$ bin/tea_shopper
|
|
26
|
+
```
|
|
27
|
+
<img src="/assets/Tea_Shopper-welcome.png" alt="Tea Shopper welcome screen" />
|
|
28
|
+
|
|
29
|
+
## Development
|
|
30
|
+
|
|
31
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
32
|
+
|
|
33
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
34
|
+
|
|
35
|
+
## Contributing
|
|
36
|
+
|
|
37
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/aparkening/tea_shopper. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
42
|
+
|
|
43
|
+
## Code of Conduct
|
|
44
|
+
|
|
45
|
+
Everyone interacting in the TeaShopper project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/aparkening/tea_shopper/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
|
Binary file
|
data/bin/console
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
require "bundler/setup"
|
|
3
|
+
require "tea_shopper"
|
|
4
|
+
|
|
5
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
6
|
+
# require "pry"
|
|
7
|
+
# Pry.start
|
|
8
|
+
|
|
9
|
+
# Reload to grab newest class info
|
|
10
|
+
def reload!
|
|
11
|
+
load 'bin/console'
|
|
12
|
+
# load_all "./lib"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
##### Testing Methods #####
|
|
16
|
+
|
|
17
|
+
# Test initial Tea object build
|
|
18
|
+
def test_build
|
|
19
|
+
# Build tea array from inititial scrape and create Tea objects
|
|
20
|
+
tea_array = TeaShopper::SongScraper.new.scrape_teas
|
|
21
|
+
TeaShopper::Tea.create_from_collection(tea_array)
|
|
22
|
+
|
|
23
|
+
# View array
|
|
24
|
+
TeaShopper::Tea.all
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Add all attributes to array
|
|
28
|
+
def test_attributes
|
|
29
|
+
TeaShopper::Tea.all.each do |tea|
|
|
30
|
+
attributes = TeaShopper::SongScraper.new.scrape_profile_page(tea.url)
|
|
31
|
+
tea.add_tea_attributes(attributes)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# View array
|
|
35
|
+
TeaShopper::Tea.all
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Show full tea array
|
|
39
|
+
def all_tea
|
|
40
|
+
TeaShopper::Tea.all
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# To reactivate:
|
|
44
|
+
# 1. Change cli.rb def display_tea to def display_tea(tea)
|
|
45
|
+
# 2. Change cli.rb self.display_tea to self.display_tea(@selected_tea)
|
|
46
|
+
# 3. Change cli.rb :category and :selected_tea to attr_accessor
|
|
47
|
+
# # Display specific detail page
|
|
48
|
+
# def test_detail
|
|
49
|
+
# tea = TeaShopper::Tea.all.find{|obj| obj.name == "Snow Jasmine"}
|
|
50
|
+
# TeaShopper::CLI.new.display_tea(tea)
|
|
51
|
+
# end
|
|
52
|
+
|
|
53
|
+
# # Break detail page
|
|
54
|
+
# def no_detail
|
|
55
|
+
# tea = "Snow Jasmine"
|
|
56
|
+
# TeaShopper::CLI.new.display_tea(tea)
|
|
57
|
+
# end
|
|
58
|
+
|
|
59
|
+
require "irb"
|
|
60
|
+
puts "\nWelcome to your new CLI Environment"
|
|
61
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/bin/tea_shopper
ADDED
data/lib/cli.rb
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
class TeaShopper::CLI
|
|
2
|
+
# Present data and get input from user
|
|
3
|
+
|
|
4
|
+
attr_reader :category, :selected_tea
|
|
5
|
+
|
|
6
|
+
##### Controller #####
|
|
7
|
+
def run
|
|
8
|
+
self.welcome
|
|
9
|
+
|
|
10
|
+
# Make initial tea objects
|
|
11
|
+
self.make_teas
|
|
12
|
+
|
|
13
|
+
# Start process of selecting a tea category and tea
|
|
14
|
+
self.find_teas
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
##### Build Tea Objects #####
|
|
19
|
+
# Create initial Tea objects
|
|
20
|
+
def make_teas
|
|
21
|
+
puts "We're pulling today's tea categories from the web. This may take a few moments...\n"
|
|
22
|
+
tea_array = TeaShopper::SongScraper.new.scrape_teas
|
|
23
|
+
TeaShopper::Tea.create_from_collection(tea_array)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Add additional attributes to Tea objects from scraped profile pages
|
|
27
|
+
def add_scraped_attributes(tea_array)
|
|
28
|
+
self.separator
|
|
29
|
+
|
|
30
|
+
# Include note for potentially long scrape time
|
|
31
|
+
puts "We're pulling today's teas from the web. This may take a few moments...\n"
|
|
32
|
+
|
|
33
|
+
# Only add attributes to teas we need to display. Scrape from category array, not full Tea.all array.
|
|
34
|
+
tea_array.each do |tea|
|
|
35
|
+
attributes = TeaShopper::SongScraper.new.scrape_profile_page(tea.url)
|
|
36
|
+
tea.add_tea_attributes(attributes)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
##### Find Teas #####
|
|
42
|
+
# Set and show category, tea list, and tea selection
|
|
43
|
+
def find_teas
|
|
44
|
+
|
|
45
|
+
# Display categories, get selection
|
|
46
|
+
self.display_categories
|
|
47
|
+
|
|
48
|
+
# Show list of teas, get tea selection
|
|
49
|
+
self.display_list if @category
|
|
50
|
+
|
|
51
|
+
# Display tea profile
|
|
52
|
+
self.display_tea if @selected_tea
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Display menu for tea type, set @category
|
|
56
|
+
def display_categories
|
|
57
|
+
|
|
58
|
+
# Display title and instructions
|
|
59
|
+
self.section_title("Main Menu")
|
|
60
|
+
self.main_instructions
|
|
61
|
+
|
|
62
|
+
# Display today's tea types, get input
|
|
63
|
+
TeaShopper::Tea.types.each { |obj| puts "- #{obj.capitalize}".colorize(:light_blue)}
|
|
64
|
+
puts "\n"
|
|
65
|
+
input = gets.strip.downcase
|
|
66
|
+
|
|
67
|
+
# If input is exit, return with nil @category
|
|
68
|
+
if self.exit?(input)
|
|
69
|
+
@category = nil
|
|
70
|
+
return self.goodbye
|
|
71
|
+
# Shortcut: if input is "black", set category to "black/red"
|
|
72
|
+
elsif input.include?("black")
|
|
73
|
+
@category = "black/red"
|
|
74
|
+
# Error check: if input isn't a current tea type, set to "black/red"
|
|
75
|
+
elsif TeaShopper::Tea.types.none?{|obj| obj == input}
|
|
76
|
+
puts "\nHmmm, we don't recognize that type of tea, so we'll show our favorite: Black/red teas..."
|
|
77
|
+
@category = "black/red"
|
|
78
|
+
else
|
|
79
|
+
@category = input
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Display ordered list of teas (alphabetically sorted), set @selected_tea
|
|
84
|
+
def display_list
|
|
85
|
+
# Assign teas to display
|
|
86
|
+
teas = TeaShopper::Tea.teas_by_type(@category)
|
|
87
|
+
|
|
88
|
+
# If user didn't already view this tea type, scrape profile attributes
|
|
89
|
+
self.add_scraped_attributes(teas) if TeaShopper::Tea.no_description?(@category)
|
|
90
|
+
|
|
91
|
+
# Display title and instructions
|
|
92
|
+
title = @category.capitalize + " Tea"
|
|
93
|
+
self.section_title(title)
|
|
94
|
+
self.list_instructions
|
|
95
|
+
|
|
96
|
+
# Repeat list and selection process until valid input received
|
|
97
|
+
until !@selected_tea.nil?
|
|
98
|
+
|
|
99
|
+
# Display ordered list of teas and get input
|
|
100
|
+
self.num_list(teas)
|
|
101
|
+
puts "\n"
|
|
102
|
+
input = gets.strip.downcase
|
|
103
|
+
|
|
104
|
+
# If input is a number, check for range and find tea by index
|
|
105
|
+
if self.convert_to_index(input).between?(0,teas.length-1)
|
|
106
|
+
@selected_tea = teas[self.convert_to_index(input)]
|
|
107
|
+
# Else return tea if name exists
|
|
108
|
+
elsif TeaShopper::Tea.find_by_name(input, teas)
|
|
109
|
+
@selected_tea = TeaShopper::Tea.find_by_name(input, teas)
|
|
110
|
+
# If exit, set @selected_tea nil, send goodbye
|
|
111
|
+
elsif self.exit?(input)
|
|
112
|
+
@selected_tea = nil
|
|
113
|
+
return self.goodbye
|
|
114
|
+
else
|
|
115
|
+
self.separator
|
|
116
|
+
puts "We don't recognize that tea, so we'll show the list again...\n\n"
|
|
117
|
+
self.list_instructions
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Take in name of tea, find tea object, display all details
|
|
123
|
+
def display_tea
|
|
124
|
+
tea = @selected_tea
|
|
125
|
+
|
|
126
|
+
# Name
|
|
127
|
+
title = "#{tea.name} (#{tea.type} tea)"
|
|
128
|
+
|
|
129
|
+
# self.section_title(title)
|
|
130
|
+
self.separator
|
|
131
|
+
puts title.colorize(:green)
|
|
132
|
+
puts tea.url.colorize(:light_blue)
|
|
133
|
+
puts "\n"
|
|
134
|
+
|
|
135
|
+
# Out of stock warning
|
|
136
|
+
puts "Oh no! It's #{tea.stock}!\n".colorize(:red) if tea.stock != ""
|
|
137
|
+
|
|
138
|
+
# Shop name and price
|
|
139
|
+
puts "Shop:" + " #{tea.shop_name}".colorize(:light_blue)
|
|
140
|
+
puts "Price:" + " $#{tea.price} for #{tea.size} grams (#{tea.price_per_oz} per oz.)".colorize(:light_blue) if tea.price != "Sold Out"
|
|
141
|
+
|
|
142
|
+
# Region, harvest, flavors
|
|
143
|
+
puts "Region:" + " #{tea.region}".colorize(:light_blue)
|
|
144
|
+
puts "Harvest:" + " #{tea.date}".colorize(:light_blue)
|
|
145
|
+
puts "Flavors:" + " #{tea.flavors}\n".colorize(:light_blue)
|
|
146
|
+
|
|
147
|
+
# # Description
|
|
148
|
+
# puts tea.description
|
|
149
|
+
# puts "\n"
|
|
150
|
+
description_displayed = nil
|
|
151
|
+
|
|
152
|
+
# Display next steps, get input
|
|
153
|
+
until @selected_tea.nil?
|
|
154
|
+
puts "What now? Choose:"
|
|
155
|
+
puts "- D to view this tea's (potentially long) description".colorize(:light_blue) if description_displayed.nil?
|
|
156
|
+
self.b_m_x_menu
|
|
157
|
+
|
|
158
|
+
input = gets.strip.downcase
|
|
159
|
+
|
|
160
|
+
# If D, display description
|
|
161
|
+
if input == "d" && description_displayed.nil?
|
|
162
|
+
desc_title = tea.name + " Description:".colorize(:green)
|
|
163
|
+
self.section_title(desc_title)
|
|
164
|
+
puts tea.description
|
|
165
|
+
# puts "\n" + tea.instructions.colorize(:light_blue)
|
|
166
|
+
# puts tea.detailed_instructions
|
|
167
|
+
puts "\n"
|
|
168
|
+
description_displayed = "yes"
|
|
169
|
+
else
|
|
170
|
+
# Reset @selected_tea to nil
|
|
171
|
+
@selected_tea = nil
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Open in browser, go to menu, or say goodbye
|
|
176
|
+
case input
|
|
177
|
+
when "b"
|
|
178
|
+
self.open_browser(tea.url)
|
|
179
|
+
puts "\nThe tea should now be up in your browser. We'll go back to the main menu to select more teas..."
|
|
180
|
+
return self.find_teas
|
|
181
|
+
when "m"
|
|
182
|
+
return self.find_teas
|
|
183
|
+
else
|
|
184
|
+
puts "\nWe don't recognize that selection, so we'll exit..." if !exit?(input)
|
|
185
|
+
return self.goodbye
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
##### Logic Helpers #####
|
|
191
|
+
|
|
192
|
+
# Convert input into array index
|
|
193
|
+
def convert_to_index(input)
|
|
194
|
+
return input.to_i - 1
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Check if input is exit
|
|
198
|
+
def exit?(input)
|
|
199
|
+
input.downcase == "x" || input.downcase == "exit"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
##### Display Helpers #####
|
|
204
|
+
|
|
205
|
+
# Welcome message
|
|
206
|
+
def welcome
|
|
207
|
+
self.section_title("Welcome to Tea Shopper!")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Goodbye message
|
|
211
|
+
def goodbye
|
|
212
|
+
self.separator
|
|
213
|
+
puts "\nThanks for stopping by. Happy tea drinking!\n\n"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Main menu instructions
|
|
217
|
+
def main_instructions
|
|
218
|
+
puts "Choose a category below to find your next great tea:\n(Or type 'X' to exit)"
|
|
219
|
+
puts "\n"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Submenu instructions
|
|
223
|
+
def list_instructions
|
|
224
|
+
puts "Choose a number or tea name from the list below get the details.\n(Or type 'X' to exit)"
|
|
225
|
+
puts "\n"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Display B, M, X menu items
|
|
229
|
+
def b_m_x_menu
|
|
230
|
+
puts "- B to visit this tea's URL".colorize(:light_blue)
|
|
231
|
+
puts "- M to start again at the main menu".colorize(:light_blue)
|
|
232
|
+
puts "- X to exit".colorize(:light_blue)
|
|
233
|
+
puts "\n"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Section separator
|
|
237
|
+
def separator
|
|
238
|
+
puts "\n----------\n\n"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Title for section, including separator
|
|
242
|
+
def section_title(title)
|
|
243
|
+
self.separator
|
|
244
|
+
puts title.colorize(:green) + "\n\n"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Display numbered list from array input. Replace price per oz with sold out as needed.
|
|
248
|
+
def num_list(array)
|
|
249
|
+
printer = ""
|
|
250
|
+
array.each.with_index(1) do |obj, index|
|
|
251
|
+
printer = "#{index}. #{obj.name} (".colorize(:light_blue)
|
|
252
|
+
obj.price_per_oz == "Sold Out"? printer << "Sold Out".colorize(:red) : printer << "$#{obj.price_per_oz} per oz.".colorize(:light_blue)
|
|
253
|
+
printer << ", #{obj.shop_name})".colorize(:light_blue)
|
|
254
|
+
puts printer
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Open URL in browser
|
|
259
|
+
def open_browser(url)
|
|
260
|
+
system("open '#{url}'")
|
|
261
|
+
end
|
|
262
|
+
end
|
data/lib/song_scraper.rb
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
class TeaShopper::SongScraper
|
|
2
|
+
|
|
3
|
+
# Path definitions
|
|
4
|
+
BASE_URL = "https://songtea.com"
|
|
5
|
+
INDEX_URL = BASE_URL + "/pages/tea-by-type"
|
|
6
|
+
# TEST_PROFILE_URL = BASE_URL + "/collections/oolong-tea/products/dragon-phoenix-tender-heart"
|
|
7
|
+
|
|
8
|
+
#####
|
|
9
|
+
# 1. Scrape teas from Song Teas by Type page: https://songtea.com/pages/tea-by-type
|
|
10
|
+
# Example return values:
|
|
11
|
+
# {
|
|
12
|
+
# :name=>"Aged Baozhong, 1960s",
|
|
13
|
+
# :type=>"aged",
|
|
14
|
+
# :shop_name=>"Song Tea & Ceramics",
|
|
15
|
+
# :url=>"/collections/aged-tea/products/aged-baozhong-1960s",
|
|
16
|
+
# :stock=>""
|
|
17
|
+
# }
|
|
18
|
+
def scrape_teas
|
|
19
|
+
teas = []
|
|
20
|
+
|
|
21
|
+
# Store html, get tea profile container
|
|
22
|
+
doc = Nokogiri::HTML(open(INDEX_URL))
|
|
23
|
+
tea_types = doc.css("div.product-section")
|
|
24
|
+
|
|
25
|
+
# Get shop name from meta tag
|
|
26
|
+
shop_name = ""
|
|
27
|
+
doc.css('meta').each { |meta| shop_name = meta.attr("content") if meta.attr("property") == "og:site_name" }
|
|
28
|
+
|
|
29
|
+
# Iterate through tea types, then iterate through teas to create tea hash
|
|
30
|
+
tea_types.each do |type|
|
|
31
|
+
type.css(".grid__item a.grid-link").each do |tea|
|
|
32
|
+
|
|
33
|
+
# Replace "red" tea type with "black/red", to remove user confusion
|
|
34
|
+
tea_type = type.attr("id").split("/").last.split("-").first
|
|
35
|
+
tea_type = "black/red" if tea_type == "red"
|
|
36
|
+
|
|
37
|
+
# If tea is out of stock, store in hash
|
|
38
|
+
tea.css("span.badge").text.include?("Sold Out")?stock = "sold out" : stock = ""
|
|
39
|
+
|
|
40
|
+
# Add tea hash to array
|
|
41
|
+
teas <<
|
|
42
|
+
{
|
|
43
|
+
:name => tea.css("p.grid-link__title").text,
|
|
44
|
+
:type => tea_type,
|
|
45
|
+
:shop_name => shop_name,
|
|
46
|
+
:url => BASE_URL + tea.attr("href"),
|
|
47
|
+
:stock => stock
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Return array
|
|
53
|
+
return teas
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#####
|
|
57
|
+
# 2. Scrape individual tea pages, such as https://songtea.com/collections/oolong-tea/products/dragon-phoenix-tender-heart
|
|
58
|
+
# Example return values:
|
|
59
|
+
# self.scrape_profile_page(profile_url)
|
|
60
|
+
# {
|
|
61
|
+
# :size=>30.0,
|
|
62
|
+
# :price=>19.0,
|
|
63
|
+
# :price_per_oz=>20.10618,
|
|
64
|
+
# :flavors=>"Notes of orchid, spruce, and ghee.",
|
|
65
|
+
# :date=>"2019",
|
|
66
|
+
# :region=>"Taiwan",
|
|
67
|
+
### Removed for now
|
|
68
|
+
# :detailed_instructions=>"This tea accommodates a range of brew styles...",
|
|
69
|
+
# :instructions=>"Brew: 6 grams・150 ml・203° F・2 min",
|
|
70
|
+
####
|
|
71
|
+
# :description=>"2019 marks our first year offering this oolong from Taiwan’s Dragon Phoenix Gorge. The cooler temperatures and mist-shrouded gardens of this region product tea with clarity, aromatics, and texture.\nDragon Phoenix Tender Heart is produced by a small farm operated by the Zhang family..."
|
|
72
|
+
# }
|
|
73
|
+
def scrape_profile_page(profile_url)
|
|
74
|
+
profile = {}
|
|
75
|
+
|
|
76
|
+
# Store html document
|
|
77
|
+
doc = Nokogiri::HTML(open(profile_url))
|
|
78
|
+
container = doc.css("div#ProductSection div.product-single")
|
|
79
|
+
|
|
80
|
+
# Get first selection from size and price select list
|
|
81
|
+
size_price = container.css("form#AddToCartForm option").first.text.strip.split(" - ")
|
|
82
|
+
|
|
83
|
+
# Get size, remove g, convert to float
|
|
84
|
+
size = size_price.first[/\d+/].to_f
|
|
85
|
+
profile[:size] = size
|
|
86
|
+
|
|
87
|
+
# Get price, grab digits and decimal, convert to float. If price is 0.0, replace with "Sold Out".
|
|
88
|
+
price = size_price.last[/\d+./].to_f
|
|
89
|
+
price = "Sold Out" if price == 0.0
|
|
90
|
+
profile[:price] = price
|
|
91
|
+
|
|
92
|
+
# Calculate price per oz from initial price and size.
|
|
93
|
+
# 30g size * 0.035274 conversion * price
|
|
94
|
+
# If price is sold out, set to price_per_oz, as well.
|
|
95
|
+
if price.is_a?(String)
|
|
96
|
+
price_per_oz = price
|
|
97
|
+
profile[:price_per_oz] = price_per_oz
|
|
98
|
+
else
|
|
99
|
+
price_per_oz = size * 0.035274 * price
|
|
100
|
+
profile[:price_per_oz] = price_per_oz.round(2)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Gather all description paragraphs and separate into flavors, date, region. (And instructions and detailed instructions for future.)
|
|
104
|
+
desc_array = container.css("div.product-description p").collect { |p| p.text }
|
|
105
|
+
|
|
106
|
+
# Flavors
|
|
107
|
+
profile[:flavors] = desc_array.shift
|
|
108
|
+
|
|
109
|
+
# Remove second paragraph and separate into region and date
|
|
110
|
+
region_year = desc_array.shift.split("・")
|
|
111
|
+
profile[:date] = region_year[1]
|
|
112
|
+
|
|
113
|
+
# Region. Grab text after "from" until the end
|
|
114
|
+
profile[:region] = region_year.first[/(?<=from ).*/]
|
|
115
|
+
|
|
116
|
+
# Future: when separating steep instructions, activate:
|
|
117
|
+
# Steep instructions
|
|
118
|
+
# Get detailed instructions first
|
|
119
|
+
# profile[:detailed_instructions] = desc_array.pop
|
|
120
|
+
# Get summary instructions next
|
|
121
|
+
# profile[:instructions] = desc_array.pop
|
|
122
|
+
|
|
123
|
+
# Full description
|
|
124
|
+
profile[:description] = desc_array.join("\n\n")
|
|
125
|
+
|
|
126
|
+
return profile
|
|
127
|
+
end
|
|
128
|
+
end
|
data/lib/tea.rb
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
class TeaShopper::Tea
|
|
2
|
+
|
|
3
|
+
attr_accessor :name, :type, :shop_name, :url, :stock, :size, :price, :price_per_oz, :flavors, :region, :date, :description
|
|
4
|
+
|
|
5
|
+
@@all = []
|
|
6
|
+
|
|
7
|
+
# Initialize multiple attributes with send
|
|
8
|
+
def initialize(attributes)
|
|
9
|
+
attributes.each {|key, value| self.send(("#{key}="), value)}
|
|
10
|
+
@@all << self
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Create tea instances from hashes inside tea_array
|
|
14
|
+
def self.create_from_collection(tea_array)
|
|
15
|
+
tea_array.each do |tea|
|
|
16
|
+
tea = TeaShopper::Tea.new({
|
|
17
|
+
:name => tea[:name],
|
|
18
|
+
:type => tea[:type],
|
|
19
|
+
:shop_name => tea[:shop_name],
|
|
20
|
+
:url => tea[:url],
|
|
21
|
+
:stock => tea[:stock]
|
|
22
|
+
})
|
|
23
|
+
# Future: reinstate :region => tea[:region],
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Add profile page hash attributes to Tea objects
|
|
28
|
+
def add_tea_attributes(attributes_hash)
|
|
29
|
+
attributes_hash.each {|key, value| self.send(("#{key}="), value)}
|
|
30
|
+
return self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @@all Teas reader
|
|
34
|
+
def self.all
|
|
35
|
+
return @@all
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Reset the @@all array
|
|
39
|
+
def self.reset_all
|
|
40
|
+
self.all.clear
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Find tea object by name
|
|
44
|
+
def self.find_by_name(name, array)
|
|
45
|
+
array.find{|obj| obj.name.downcase == name.downcase}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Return true if sample description doesn't exist for tea in input type
|
|
49
|
+
def self.no_description?(type)
|
|
50
|
+
self.teas_by_type(type).select{|obj| obj.description}.sample.nil?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Return array of tea types
|
|
54
|
+
def self.types
|
|
55
|
+
self.all.collect { |tea| tea.type }.uniq.sort
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Return array of teas for type
|
|
59
|
+
def self.teas_by_type(type)
|
|
60
|
+
teas = self.all.collect { |obj| obj if obj.type == type }.compact
|
|
61
|
+
# teas.sort_by { |tea| tea.price_per_oz} # for sorting by price, rather than alphabetical
|
|
62
|
+
teas.sort_by { |tea| tea.name}
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/tea_shopper.rb
ADDED
data/tea_shopper.gemspec
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require "tea_shopper/version"
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "tea_shopper"
|
|
8
|
+
spec.version = TeaShopper::VERSION
|
|
9
|
+
spec.authors = ["aparkening"]
|
|
10
|
+
spec.email = ["aaron.parkening@gmail.com"]
|
|
11
|
+
|
|
12
|
+
spec.summary = "Tea Shopper helps you shop for your next great tea."
|
|
13
|
+
spec.description = " Tea Shopper is a command line interface that scrapes tea data from the web and allows users to compare teas by name, price per ounce, and tea shop. When the user chooses a tea, it displays specific details, such as purchase URL, flavors, region, and description. Check out a short demonstration video. at https://www.loom.com/share/5d3cc369d7c243d4af5e665206b39a75."
|
|
14
|
+
spec.homepage = "https://github.com/aparkening/tea_shopper"
|
|
15
|
+
spec.license = "MIT"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
20
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
22
|
+
end
|
|
23
|
+
# spec.bindir = "exe"
|
|
24
|
+
# spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
25
|
+
spec.bindir = "bin"
|
|
26
|
+
spec.executables = ["tea_shopper"]
|
|
27
|
+
spec.require_paths = ["lib"]
|
|
28
|
+
|
|
29
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
|
30
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
|
31
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
32
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: tea_shopper
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- aparkening
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2019-06-26 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: '2.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
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
|
+
description: " Tea Shopper is a command line interface that scrapes tea data from
|
|
56
|
+
the web and allows users to compare teas by name, price per ounce, and tea shop.
|
|
57
|
+
When the user chooses a tea, it displays specific details, such as purchase URL,
|
|
58
|
+
flavors, region, and description. Check out a short demonstration video. at https://www.loom.com/share/5d3cc369d7c243d4af5e665206b39a75."
|
|
59
|
+
email:
|
|
60
|
+
- aaron.parkening@gmail.com
|
|
61
|
+
executables:
|
|
62
|
+
- tea_shopper
|
|
63
|
+
extensions: []
|
|
64
|
+
extra_rdoc_files: []
|
|
65
|
+
files:
|
|
66
|
+
- ".DS_Store"
|
|
67
|
+
- ".gitignore"
|
|
68
|
+
- ".rspec"
|
|
69
|
+
- ".travis.yml"
|
|
70
|
+
- CODE_OF_CONDUCT.md
|
|
71
|
+
- Gemfile
|
|
72
|
+
- Gemfile.lock
|
|
73
|
+
- LICENSE
|
|
74
|
+
- LICENSE.txt
|
|
75
|
+
- PROCESS.md
|
|
76
|
+
- README.md
|
|
77
|
+
- Rakefile
|
|
78
|
+
- assets/Tea_Shopper-welcome.png
|
|
79
|
+
- bin/console
|
|
80
|
+
- bin/setup
|
|
81
|
+
- bin/tea_shopper
|
|
82
|
+
- config/environment.rb
|
|
83
|
+
- lib/cli.rb
|
|
84
|
+
- lib/song_scraper.rb
|
|
85
|
+
- lib/tea.rb
|
|
86
|
+
- lib/tea_shopper.rb
|
|
87
|
+
- lib/tea_shopper/version.rb
|
|
88
|
+
- tea_shopper.gemspec
|
|
89
|
+
homepage: https://github.com/aparkening/tea_shopper
|
|
90
|
+
licenses:
|
|
91
|
+
- MIT
|
|
92
|
+
metadata: {}
|
|
93
|
+
post_install_message:
|
|
94
|
+
rdoc_options: []
|
|
95
|
+
require_paths:
|
|
96
|
+
- lib
|
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '0'
|
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '0'
|
|
107
|
+
requirements: []
|
|
108
|
+
rubygems_version: 3.0.4
|
|
109
|
+
signing_key:
|
|
110
|
+
specification_version: 4
|
|
111
|
+
summary: Tea Shopper helps you shop for your next great tea.
|
|
112
|
+
test_files: []
|