stars 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,26 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ stars (0.5.0)
5
+ httparty
6
+ keep (~> 0.0.3)
7
+ nokogiri
8
+ terminal-table
9
+
10
+ GEM
11
+ remote: http://rubygems.org/
12
+ specs:
13
+ crack (0.1.8)
14
+ httparty (0.7.6)
15
+ crack (= 0.1.8)
16
+ keep (0.0.3)
17
+ mocha (0.9.12)
18
+ nokogiri (1.4.4)
19
+ terminal-table (1.4.2)
20
+
21
+ PLATFORMS
22
+ ruby
23
+
24
+ DEPENDENCIES
25
+ mocha (~> 0.9.9)
26
+ stars!
data/README.markdown CHANGED
@@ -3,72 +3,58 @@
3
3
 
4
4
  ## why
5
5
 
6
- [Favstar](http://favstar.fm) is about opening another line of dialogue between you and your Twitter followers. Like the one where someone likes what you tweet but is too nervous to tell you to your face and they star your tweets instead, so instead you shake them down in the kitchen after a long tequila bender and steal their precious stars and keep them for yourself.
6
+ Stars are a global currency. You tweet something, you write something, you
7
+ otherwise put yourself out there. Sure, you feel a warm sense of accomplishment
8
+ when you do this, but how do you *really* know if the people like you?
7
9
 
8
- So anyway, I spend a lot of time on the command line, and switching over to the browser just to view [my stars](http://favstar.fm/users/holman) is so passé.
10
+ **FUCKING STARS, THAT'S HOW**
9
11
 
10
- ## how
11
-
12
- Install with `gem install stars`, then do a quick `stars`, fill in your Twitter username, and let it do its sultry magic. This is me lately:
13
-
14
- ★ by @holman
15
- +----+-----------+--------------+--------------------------------------------------------+
16
- | # | Stars | Time | Your Funnies |
17
- +----+-----------+--------------+--------------------------------------------------------+
18
- | 1 | * | 12 hours ago | Today's GitHub todo list: put cupholders in the for... |
19
- | 2 | * * * | 1 day ago | This bus is going so slowly that my AT&T 3G coverag... |
20
- | 3 | * * * * * | 2 days ago | Every now and then I bust out that I'm from North D... |
21
- | 4 | * * * | 3 days ago | Was told that "y'alls be stupid" by an adamant stre... |
22
- | 5 | * * | 4 days ago | Now that we've fixed healthcare forever, who wants ... |
23
- | 6 | * * * * | 4 days ago | Just wikipedia'd "Justin Bieber" and I still don't ... |
24
- | 7 | * * * * | 5 days ago | Look. I'm not saying David Copperfield is a little ... |
25
- | 8 | * | 6 days ago | SFO. Guys, we're at TERROR ALERT ORANGE. I don't t... |
26
- | 9 | * | 7 days ago | Dammit. 2012 looks like garbage so far, but if a mo... |
27
- | 10 | * | 10 days ago | At the MySQL + Rails combined meetup. I don't want ... |
28
- | 11 | * | 10 days ago | Oh, they're doing a MacGruber movie. This makes sen... |
29
- | 12 | * * * | 10 days ago | Just got my third free replacement earbuds from the... |
30
- | 13 | * x 13 | 11 days ago | So for the seven of us that are both nerdy *and* va... |
31
- | 14 | * * * * * | 11 days ago | I wouldn't say I'm "saving daylight" as much as I'm... |
32
- | 15 | * * * | 13 days ago | I pre-ordered an iPad, checked into lunch, and now ... |
33
- | 16 | * * | 13 days ago | In another life I want to be a pop star heartthrob ... |
34
- | 17 | * | 14 days ago | Oh hey, a year or so of my beer consumption: http:/... |
35
- | 18 | * | 14 days ago | Going to totally refork and rework my dotfiles soon... |
36
- | 19 | * * * | 15 days ago | You change your Facebook pic, that syncs to my iPho... |
37
- | 20 | * * | 17 days ago | just about finished my thesis on generating free en... |
38
- +----+-----------+--------------+--------------------------------------------------------+
39
-
40
-
41
- You can tell from the relative dearth of stars that I'm in my *screw followers I'm important so here's what I had for lunch* phase. Don't worry; I'll become a star whore again and tweet pop culture references in relation to presidential penile size soon enough.
42
-
43
- You can also specify which user you want to run the query against by passing it in as your one argument. This value is saved for the future, too. For example:
12
+ `stars` is a command-line client to check your precious stars at fine
13
+ establishments like [Twitter](http://twitter.com) (via the exquisite
14
+ [Favstar](http://favstar.fm)) and [Convore](http://convore.com). More services
15
+ will be added as they get added.
44
16
 
45
- stars holman # generates @holman's stars
46
- stars # generates @holman's stars
47
- stars goodtutorials # generates @goodtutorials' stars
48
- stars # generates @goodtutorials' stars
49
-
50
- You can also get some knowledge dropped all over your face about a specific toot:
51
-
52
- Type the number of the toot that you want to learn about
53
- (or hit return to view all again, you ego-maniac) >>
54
- 14
55
-
56
- 5 stars: I wouldn't say I'm "saving daylight" as much as
57
- I'm sequestering it in a small room underneath my stairs so
58
- its screams can't be heard.
59
-
60
- ★ twilighteyes08
61
- ★ itsjustEm
62
- ★ aklw
63
- ★ shariv67
64
- ★ IsJonas
65
- RT kneath
66
-
67
-
68
- ## state of affairs
17
+ ## how
69
18
 
70
- Fork this bad boy and shape it as you see fit. I just randomly built some shit that I might like; maybe that process will work for you, too. Send me a pull request if you ship something awesome. Reminder: something is never awesome without tests.
19
+ Install with `gem install stars`, then do a quick `stars`, fill in your
20
+ username for your service, and let it do its sultry magic. This is me lately:
21
+
22
+ ★ stars
23
+ +----+---------+-------+-----------------------------------------+
24
+ | | Service | Stars | The Hotness |
25
+ +----+---------+-------+-----------------------------------------+
26
+ | 1 | Convore | 29 | Number of stars received |
27
+ | 2 | Favstar | 5 | we're flawed because we want so much... |
28
+ | 3 | Favstar | 3 | Cherish your drunken coworker moment... |
29
+ | 4 | Favstar | 5 | Usually @kneath teaches me that a li... |
30
+ | 5 | Favstar | 2 | Whew. Done with cohosting #codeconf.... |
31
+ | 6 | Favstar | 2 | 1) Guys do lightning talk 2) Mention... |
32
+ | 7 | Favstar | 1 | I am in a suit, and I'm addressing t... |
33
+ | 8 | Favstar | 3 | @kneath you have to take a picture o... |
34
+ | 9 | Favstar | 3 | I won't work until GitHub defunds fr... |
35
+ | 10 | Favstar | 1 | Coordinating my attire for @codeconf... |
36
+ | 11 | Favstar | 1 | You are the wind beneath my wings! I... |
37
+ | 12 | Favstar | 1 | One of those rare San Francisco nigh... |
38
+ | 13 | Favstar | 1 | @GavinStark Me too! |
39
+ | 14 | Favstar | 30 | Why @github hacks on side projects: ... |
40
+ | 15 | Favstar | 1 | @luckiestmonkey all the borg would g... |
41
+ +----+---------+-------+-----------------------------------------+
42
+
43
+ ## BONUS ROUND
44
+
45
+ Plopped a whopper of a comparison on Convore between Python's whitespacing and
46
+ the piddly lawyer in Jurassic Park, and want to see the results *right now,
47
+ dammit??!??!?!?!*? Have no fear! Check your service stars directly with `stars
48
+ <service>`:
49
+
50
+ stars convore
51
+
52
+ ## whip it out
53
+
54
+ `stars` is kind of a silly little project. It's not as well-tested or
55
+ well-documented as I'd like, but so it goes. Fork, send me a pull request, and
56
+ we'll all be friends.
71
57
 
72
58
  ## who
73
59
 
74
- [@holman](http://twitter.com/holman) did this. Follow me and star my stuff incessantly so we can prove to Dean Allen that stars are as important as currency, much [to his logically-argued dismay](http://favrd.textism.com/).
60
+ [@holman](http://twitter.com/holman) did this. I look great in a Speedo.
data/Rakefile CHANGED
@@ -1,25 +1,50 @@
1
1
  require 'rubygems'
2
2
  require 'rake'
3
+ require 'date'
3
4
 
4
- begin
5
- require 'jeweler'
6
- Jeweler::Tasks.new do |gem|
7
- gem.name = "stars"
8
- gem.summary = %Q{Recent favstar faves on your command line}
9
- gem.description = %Q{Recent favstar faves on your command line.}
10
- gem.email = "github.com@zachholman.com"
11
- gem.homepage = "http://github.com/holman/stars"
12
- gem.authors = ["Zach Holman"]
13
- # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
- gem.add_dependency('httparty')
15
- gem.add_dependency('terminal-table')
16
- gem.add_dependency('nokogiri')
17
- end
18
- Jeweler::GemcutterTasks.new
19
- rescue LoadError
20
- puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
5
+ #############################################################################
6
+ #
7
+ # Helper functions
8
+ #
9
+ #############################################################################
10
+
11
+ def name
12
+ @name ||= Dir['*.gemspec'].first.split('.').first
13
+ end
14
+
15
+ def version
16
+ line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
17
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
18
+ end
19
+
20
+ def date
21
+ Date.today.to_s
22
+ end
23
+
24
+ def rubyforge_project
25
+ name
26
+ end
27
+
28
+ def gemspec_file
29
+ "#{name}.gemspec"
30
+ end
31
+
32
+ def gem_file
33
+ "#{name}-#{version}.gem"
34
+ end
35
+
36
+ def replace_header(head, header_name)
37
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
21
38
  end
22
39
 
40
+ #############################################################################
41
+ #
42
+ # Standard tasks
43
+ #
44
+ #############################################################################
45
+
46
+ task :default => :test
47
+
23
48
  require 'rake/testtask'
24
49
  Rake::TestTask.new(:test) do |test|
25
50
  test.libs << 'lib' << 'test'
@@ -27,29 +52,99 @@ Rake::TestTask.new(:test) do |test|
27
52
  test.verbose = true
28
53
  end
29
54
 
30
- begin
31
- require 'rcov/rcovtask'
32
- Rcov::RcovTask.new do |test|
33
- test.libs << 'test'
34
- test.pattern = 'test/**/test_*.rb'
35
- test.verbose = true
36
- end
37
- rescue LoadError
38
- task :rcov do
39
- abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
40
- end
55
+ desc "Generate RCov test coverage and open in your browser"
56
+ task :coverage do
57
+ require 'rcov'
58
+ sh "rm -fr coverage"
59
+ sh "rcov test/test_*.rb"
60
+ sh "open coverage/index.html"
41
61
  end
42
62
 
43
- task :test => :check_dependencies
44
-
45
- task :default => :test
46
-
47
63
  require 'rake/rdoctask'
48
64
  Rake::RDocTask.new do |rdoc|
49
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
50
-
51
65
  rdoc.rdoc_dir = 'rdoc'
52
- rdoc.title = "stars #{version}"
66
+ rdoc.title = "#{name} #{version}"
53
67
  rdoc.rdoc_files.include('README*')
54
68
  rdoc.rdoc_files.include('lib/**/*.rb')
55
69
  end
70
+
71
+ desc "Open an irb session preloaded with this library"
72
+ task :console do
73
+ sh "irb -rubygems -r ./lib/#{name}.rb"
74
+ end
75
+
76
+ #############################################################################
77
+ #
78
+ # Custom tasks (add your own tasks here)
79
+ #
80
+ #############################################################################
81
+
82
+
83
+
84
+ #############################################################################
85
+ #
86
+ # Packaging tasks
87
+ #
88
+ #############################################################################
89
+
90
+ desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
91
+ task :release => :build do
92
+ unless `git branch` =~ /^\* master$/
93
+ puts "You must be on the master branch to release!"
94
+ exit!
95
+ end
96
+ sh "git commit --allow-empty -a -m 'Release #{version}'"
97
+ sh "git tag v#{version}"
98
+ sh "git push origin master"
99
+ sh "git push origin v#{version}"
100
+ sh "gem push pkg/#{name}-#{version}.gem"
101
+ end
102
+
103
+ desc "Build #{gem_file} into the pkg directory"
104
+ task :build => :gemspec do
105
+ sh "mkdir -p pkg"
106
+ sh "gem build #{gemspec_file}"
107
+ sh "mv #{gem_file} pkg"
108
+ end
109
+
110
+ desc "Generate #{gemspec_file}"
111
+ task :gemspec => :validate do
112
+ # read spec file and split out manifest section
113
+ spec = File.read(gemspec_file)
114
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
115
+
116
+ # replace name version and date
117
+ replace_header(head, :name)
118
+ replace_header(head, :version)
119
+ replace_header(head, :date)
120
+ #comment this out if your rubyforge_project has a different name
121
+ replace_header(head, :rubyforge_project)
122
+
123
+ # determine file list from git ls-files
124
+ files = `git ls-files`.
125
+ split("\n").
126
+ sort.
127
+ reject { |file| file =~ /^\./ }.
128
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
129
+ map { |file| " #{file}" }.
130
+ join("\n")
131
+
132
+ # piece file back together and write
133
+ manifest = " s.files = %w[\n#{files}\n ]\n"
134
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
135
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
136
+ puts "Updated #{gemspec_file}"
137
+ end
138
+
139
+ desc "Validate #{gemspec_file}"
140
+ task :validate do
141
+ libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
142
+ unless libfiles.empty?
143
+ puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
144
+ exit!
145
+ end
146
+ unless Dir['VERSION*'].empty?
147
+ puts "A `VERSION` file at root level violates Gem best practices."
148
+ exit!
149
+ end
150
+ end
data/bin/stars CHANGED
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
-
3
2
  require 'rubygems'
4
3
 
5
4
  $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
6
5
 
7
6
  require 'stars'
8
7
 
9
- username = ARGV ? ARGV[0] : nil
10
- Stars::Client.load!(username)
8
+ Stars::Client.new(ARGV)
data/lib/stars/client.rb CHANGED
@@ -1,81 +1,98 @@
1
- # encoding: utf-8
2
-
1
+ # Client is our interface between user and terminal prompt. This does all of
2
+ # the heavy-lifting for formatting.
3
+ #
3
4
  module Stars
4
5
  class Client
5
-
6
- def self.load!(new_username=nil)
7
- remember_username(new_username) if new_username
8
- @recent = Stars::Favstar.new.recent(username)
9
- display
10
- end
11
6
 
12
- def self.display
13
- system 'clear'
14
- puts "\n ★ by @#{username}"
15
- puts Stars::Formatter.new(@recent)
16
- select_star
17
- end
7
+ attr_reader :posts
8
+ attr_writer :posts
9
+
10
+ # Initializes a new Client.
11
+ #
12
+ # Returns nothing.
13
+ def initialize(cmd)
14
+ Stars.config.prompt_for_username(cmd[1]) if cmd[0] == 'add'
18
15
 
19
- def self.input
20
- STDIN.gets
21
- end
22
-
23
- def self.username
24
- File.exists?(config_path) ? File.read(config_path) : prompt_for_username
25
- end
26
-
27
- def self.prompt_for_username
28
- puts ""
29
- puts ""
30
- puts " ★ stars"
31
- puts ""
32
- puts "Type your Twitter username:"
33
- remember_username(self.input.chomp)
34
- end
35
-
36
- def self.remember_username(username)
37
- File.open(config_path, 'w') {|f| f.write(username) }
38
- username
39
- end
40
-
41
- def self.config_path
42
- File.join(ENV['HOME'], '.stars')
16
+ system "clear"
17
+ puts "★ stars"
18
+
19
+ display(cmd[0])
20
+ star_loop
43
21
  end
44
-
45
- def self.select_star
22
+
23
+ # Run a loop FOREVER until we kill it or we make a selection.
24
+ #
25
+ # Returns nothing.
26
+ def star_loop
46
27
  selection = ''
47
28
  while true
48
- puts "Type the number of the toot that you want to learn about"
29
+ puts "Type the number of the post that you want to learn about"
49
30
  print " (or hit return to view all again, you ego-maniac) >> "
50
- selection = self.input.chomp
31
+ selection = $stdin.gets.chomp
51
32
  break if ['','q','quit','exit','fuckthis'].include?(selection.downcase)
52
- show_selection(selection)
33
+ show(selection)
53
34
  end
54
35
  display if selection == ''
55
36
  end
56
-
57
- def self.show_selection(id)
58
- id = id.to_i - 1
59
- if @recent[id]
60
- puts ''
61
- puts wrap_text(' ' + @recent[id]['title'], 60)
62
- puts parse_who(@recent[id]['guid'])
63
- puts ''
37
+
38
+ # Displays all of the star tables and information we have.
39
+ #
40
+ # Returns nothing.
41
+ def display(service=nil)
42
+ Stars.config.prompt_for_service if Stars.installed_services.empty?
43
+
44
+ if service
45
+ posts = service.constantize.posts
64
46
  else
65
- puts ''
66
- puts 'Oops, no such toot.'
67
- puts ''
47
+ posts = Stars.installed_services.collect{ |service|
48
+ service.constantize.posts }.flatten
68
49
  end
50
+ @posts = Post.filter(posts)
51
+ puts print_posts(@posts)
69
52
  end
70
-
71
- def self.parse_who(url)
72
- Stars::Favstar.new.show(url)
53
+
54
+ # Show more information about a particular post.
55
+ #
56
+ # id - the Integer id entered by the user, which we map to a Post
57
+ #
58
+ # Returns nothing (although does delegate to the Post to show #more).
59
+ def show(id)
60
+ post = @posts[id.to_i-1]
61
+ return puts("\nMake a valid selection. Pretty please?\n") unless post
62
+ puts post.more
63
+ display
73
64
  end
74
-
75
- def self.wrap_text(txt, col = 80)
76
- txt.gsub(/(.{1,#{col}})( +|$\n?)|(.{1,#{col}})/,
77
- "\\1\\3\n ")
65
+
66
+ # This does the actual printing of posts.
67
+ #
68
+ # posts - an Array of Post objects
69
+ #
70
+ # It loops through the Array of posts and sends them to `terminal-table`.
71
+ def print_posts(posts)
72
+ table do |t|
73
+ t.headings = headings
74
+ posts.each_with_index do |post,i|
75
+ t << [
76
+ { :value => i+1, :alignment => :right },
77
+ post.service.capitalize,
78
+ { :value => post.stars_count, :alignment => :center },
79
+ post.short_name
80
+ ]
81
+ end
82
+ end
78
83
  end
79
-
84
+
85
+ # The headings used in the resulting printed table.
86
+ #
87
+ # This returns an Array of headings.
88
+ def headings
89
+ [
90
+ '',
91
+ 'Service',
92
+ 'Stars',
93
+ {:value => 'The Hotness', :alignment => :center }
94
+ ]
95
+ end
96
+
80
97
  end
81
98
  end
@@ -0,0 +1,41 @@
1
+ module Stars
2
+ class Config
3
+
4
+ attr_reader :keep
5
+
6
+ attr_writer :keep
7
+
8
+ def initialize
9
+ @keep = Keep.new(config_path)
10
+ end
11
+
12
+ def config_path
13
+ "#{File.expand_path('~')}/.stars.yml"
14
+ end
15
+
16
+ def username(service)
17
+ prompt_for_username(service)
18
+ end
19
+
20
+ def prompt_for_service
21
+ puts "What service do you want to track?"
22
+ puts "Your options: #{Stars.uninstalled_services.join(', ')}"
23
+ service = $stdin.gets.chomp.downcase
24
+ prompt_for_username(service)
25
+ end
26
+
27
+ def prompt_for_username(service)
28
+ return @keep.get(service) if @keep.present?(service)
29
+
30
+ if !Stars.uninstalled_services.empty? and !Stars.uninstalled_services.include?(service.downcase)
31
+ puts("You need to pick something from: #{Stars.uninstalled_services.join(', ')}")
32
+ return exit
33
+ end
34
+
35
+ puts "What's your username for #{service}?"
36
+ username = $stdin.gets.chomp
37
+ @keep.set(service,username)
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ class String
2
+ # Poor man's constantize
3
+ def constantize
4
+ Stars.const_get(to_s.capitalize)
5
+ end
6
+ end
data/lib/stars/post.rb ADDED
@@ -0,0 +1,57 @@
1
+ module Stars
2
+ class Post
3
+ attr_reader :url
4
+ attr_reader :service
5
+ attr_reader :stars_count
6
+ attr_reader :date
7
+
8
+ attr_writer :name
9
+ attr_writer :service
10
+ attr_writer :url
11
+ attr_writer :stars
12
+ attr_writer :stars_count
13
+ attr_writer :date
14
+
15
+ def initialize(attributes)
16
+ @name = attributes[:name]
17
+ @url = attributes[:url]
18
+ @stars = attributes[:stars]
19
+ @stars_count = attributes[:stars_count]
20
+ @service = attributes[:service]
21
+ @date = attributes[:date]
22
+ end
23
+
24
+ # The String name of the Post.
25
+ #
26
+ # This returns the String of the content of the Post (which we just call
27
+ # "name"). We also strip whitespace, since it tends to screw up things on
28
+ # the command line.
29
+ def name
30
+ @name.gsub("\n",' ')
31
+ end
32
+
33
+ # The shorted String version of `name`.
34
+ #
35
+ # Returns a String of the name truncated at 35 characters.
36
+ def short_name
37
+ name.size > 35 ? "#{name[0..35]}..." : name
38
+ end
39
+
40
+ def stars
41
+ @stars
42
+ end
43
+
44
+ def more
45
+ service.constantize.more(self)
46
+ end
47
+
48
+ # Filter an Array of Post objects.
49
+ #
50
+ # posts - an Array of Post objects to filter
51
+ #
52
+ # This returns the Array sorted by the 15 most recent stars.
53
+ def self.filter(posts)
54
+ posts.sort{ |a,b| b.date <=> a.date }[0..14]
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,40 @@
1
+ module Stars
2
+ class Convore < Service
3
+
4
+ attr_reader :posts
5
+
6
+ def name
7
+ "convore"
8
+ end
9
+
10
+ def posts
11
+ [Post.new(:name => "Number of stars received",
12
+ :stars_count => stars,
13
+ :service => name,
14
+ :date => DateTime.now,
15
+ :url => "https://convore.com/users/#{Stars.config.username('convore')}")]
16
+ end
17
+
18
+ def html
19
+ Nokogiri::HTML(open("https://convore.com/users/#{username}"))
20
+ end
21
+
22
+ def stars
23
+ html.css('.stars-received strong').first.content.to_i
24
+ end
25
+
26
+ def self.more(post)
27
+ return <<-CONVORE
28
+
29
+ Convore doesn't have a stars API yet. So we're just scraping your total stars
30
+ for now. Kind of a bummer, isn't it? You should probably send @ericflo a tweet
31
+ and complain about it. Tell him I didn't send you.
32
+
33
+ Anyway, you have #{post.stars_count} stars with Convore right now. Check it:
34
+ #{post.url}
35
+
36
+ CONVORE
37
+ end
38
+
39
+ end
40
+ end