rudisco 1.0.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.
@@ -0,0 +1,74 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ module Rudisco
4
+ module Helpers
5
+ ##
6
+ # Opens +url+ in a browser.
7
+ #
8
+ # @param [String] url
9
+
10
+ def self.open_in_browser(url)
11
+ return if url.to_s.empty?
12
+
13
+ if defined?(Launchy) && Launchy.respond_to?(:open)
14
+ Launchy.open url
15
+ else
16
+ if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
17
+ system "start #{url}"
18
+ elsif RbConfig::CONFIG['host_os'] =~ /darwin/
19
+ system "open #{url}"
20
+ elsif RbConfig::CONFIG['host_os'] =~ /linux|bsd/
21
+ system "xdg-open #{url}"
22
+ end
23
+ end
24
+ end
25
+
26
+ ##
27
+ # Downloads file from +url+ to +path+.
28
+ #
29
+ # @param [String] url
30
+ #
31
+ # @param [String, Pathname] path
32
+ #
33
+ # @exception NotAUrl
34
+ # Raised when +url+ not a string or empty.
35
+ # @exception DirNotExists
36
+ # Raised when +path+ is not a path to directory.
37
+
38
+ def self.download(url, path)
39
+ raise NotAUrl, url unless url.is_a?(String) && !url.empty?
40
+ raise DirNotExists, path unless Dir.exists? path
41
+
42
+ system "wget -P #{path} #{url} > /dev/null 2>&1"
43
+ end
44
+
45
+ ##
46
+ # Clones git project from +url+ to +path+.
47
+ #
48
+ # @param [String] url
49
+ #
50
+ # @param [String, Pathname] path
51
+ #
52
+ # @exception NotAUrl
53
+ # Raised when +url+ not a string or empty.
54
+ # @exception DirShouldNotExists
55
+ # Raised when git can not clone project, since directory
56
+ # where it should be saved already exists.
57
+
58
+ def self.git_clone(url, path)
59
+ raise NotAUrl, url unless url.is_a?(String) && !url.empty?
60
+ raise DirShouldNotExists, path if Dir.exists? path
61
+
62
+ system "git clone #{url} #{path} > /dev/null 2>&1"
63
+ end
64
+
65
+ class NotAUrl < Error # no-doc
66
+ def initialize(url); super "Url '#{url}' is not a string or empty!" end; end
67
+
68
+ class DirNotExists < Error # no-doc
69
+ def initialize(dir); super "Directory '#{dir}' not exists!" end; end
70
+
71
+ class DirShouldNotExists < Error # no-doc
72
+ def initialize(dir); super "Directory '#{dir}' should not exist!" end; end
73
+ end # module Helpers
74
+ end # module Rudisco
@@ -0,0 +1,116 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ module Rudisco
4
+ class GemActions
5
+ # @param [Rudisco::Gem] gem
6
+ #
7
+ # @return [Rudisco::GemActions]
8
+
9
+ def initialize(gem)
10
+ @gem = gem
11
+ end
12
+
13
+ ##
14
+ # Creates an instance of GemActions class.
15
+ #
16
+ # @param [Symbol] action
17
+ # Described in Gem#action
18
+ #
19
+ # @param [Hash] params
20
+ # Additional parameters. Optional.
21
+ #
22
+ # @exception Rudisco::GemActions::ActionUnknown
23
+ # Raised when action is unknown.
24
+
25
+ def complete(action, params = {})
26
+ if respond_to? action
27
+ send action, params
28
+ else
29
+ raise Unknown, action
30
+ end
31
+ end
32
+
33
+ # ----------------------------------------------------
34
+ # ------------- External actions methods -------------
35
+ # ----------------------------------------------------
36
+
37
+ ##
38
+ # Updates gems.
39
+ #
40
+ # Used to get latest information about gem. This need because some
41
+ # db.attributes getting update even when gem version not changed.
42
+
43
+ def update(params = {})
44
+ Gem.corteges_scanning gem
45
+ end
46
+
47
+ ##
48
+ # Opens in browser documentation page
49
+
50
+ def open_documentation(params = {})
51
+ Helpers.open_in_browser gem.documentation_url
52
+ end
53
+
54
+ ##
55
+ # Opens in browser bug tracker page
56
+
57
+ def open_bug_tracker(params = {})
58
+ Helpers.open_in_browser gem.bug_tracker_url
59
+ end
60
+
61
+ ##
62
+ # Opens in browser code sources page.
63
+
64
+ def open_sources(params = {})
65
+ Helpers.open_in_browser gem.source_code_url
66
+ end
67
+
68
+ ##
69
+ # Opens in browser wiki page.
70
+
71
+ def open_wiki(params = {})
72
+ Helpers.open_in_browser gem.wiki_url
73
+ end
74
+
75
+ ##
76
+ # Opens in browser gem page on rubygems.org
77
+
78
+ def open_rubygems(params = {})
79
+ Helpers.open_in_browser gem.project_url
80
+ end
81
+
82
+ ##
83
+ # Downloads gem.
84
+ #
85
+ # @param [Hash] params
86
+ # @option params [String] :path (ENV['HOME'])
87
+
88
+ def download(params = {})
89
+ path = params[:path] || ENV['HOME']
90
+
91
+ Helpers::download gem.gem_url, path
92
+ end
93
+
94
+ ##
95
+ # Clones gem from git.
96
+ #
97
+ # @param [Hash] params
98
+ # @option params [String] :path (ENV['HOME'])
99
+
100
+ def git_clone(params = {})
101
+ path = params[:path] || ENV['HOME']
102
+ path = File.join(path, gem.name)
103
+
104
+ Helpers::git_clone gem.source_code_url, path
105
+ end
106
+
107
+ private
108
+
109
+ # @return [Rudisco::Gem]
110
+
111
+ attr_reader :gem
112
+
113
+ class Unknown < Error # no-doc
114
+ def initialize(action); super "Action '#{action}' is unknown!" end; end
115
+ end # class GemActions
116
+ end # module Rudisco
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ module Sequel::Plugins::GemExtendedDataset
4
+ module DatasetMethods
5
+ # @see Rudisco::Gem#action
6
+ #
7
+ # @return [Gem::Dataset]
8
+
9
+ def action(command, params = {})
10
+ each { |cortege| cortege.action command, params }
11
+
12
+ return self
13
+ end
14
+
15
+ # @see Rudisco::Gem#find_phrase
16
+ #
17
+ # @return [Gem::Dataset]
18
+
19
+ def find_phrase(word)
20
+ search_filter =
21
+ Sequel.ilike(:name, "%#{word}%") || Sequel.ilike(:description, "%#{word}%")
22
+
23
+ dataset = where search_filter
24
+
25
+ return dataset
26
+ end
27
+ end # module DatasetMethods
28
+ end # Sequel::Plugins::GemExtendedDataset
@@ -0,0 +1,195 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ module Rudisco
4
+ module RubyGemsScanner # no-doc
5
+ ##
6
+ # Scans rubygems.org for new gems. Also marks gems as outdated when
7
+ # new gem version is out.
8
+ #
9
+ # To load an actual information for newest gems use #deep_scanning.
10
+
11
+ def surface_scanning
12
+ _surface_scanning
13
+ end
14
+
15
+ ##
16
+ # Long-term task. Updates database with an actual information. Multithread.
17
+ #
18
+ # Note: speed limited by rubygems.org.
19
+ #
20
+ # Can be increased with advanced concurrent processes setup for
21
+ # sqlite database.
22
+ #
23
+ # However, this is slow since rubygems.org allowing certain requests
24
+ # per second count.
25
+ #
26
+ # @param [Proc<Integer>] callback
27
+ # Returns count of updated gems, optional.
28
+
29
+ def deep_scanning(&callback)
30
+ _deep_scanning &callback
31
+ end
32
+
33
+ ##
34
+ # Updates cortege(s).
35
+ #
36
+ # Used to get latest information about gem(s) total downloads
37
+ # count and so on.
38
+ #
39
+ # @param [Rudisco::Gem, Array<Rudisco::Gem>] corteges
40
+
41
+ def corteges_scanning(corteges)
42
+ rubygems_manage_corteges Array(corteges)
43
+ end
44
+
45
+ private
46
+
47
+ def _surface_scanning # no-doc
48
+ rubygems =
49
+ begin
50
+ tmp_file = Tempfile.new 'gems.tmp'
51
+
52
+ system "gem list --remote > #{tmp_file.path}"
53
+ rubygems = CSV.open tmp_file.path
54
+ rubygems = rubygems.map { |r| r.join.delete('()').split }
55
+ tmp_file.unlink
56
+
57
+ rubygems
58
+ end
59
+ sqlite_gems = select_hash :name, :version
60
+
61
+ ## ADD NEW GEMS TO DATABASE
62
+ db.transaction do
63
+ new_gems = rubygems.select { |gem| sqlite_gems[gem[0]].nil? }
64
+
65
+ new_gems.each do |gem|
66
+ insert name: gem[0], version: gem[1]
67
+ end
68
+ end
69
+
70
+ ## UPDATE RECORDS IF NEED
71
+ db.transaction do
72
+ update_gems = rubygems.reject { |gem| sqlite_gems[gem[0]].nil? }
73
+ .select { |gem| sqlite_gems[gem[0]] != gem[1] }
74
+
75
+ update_gems.each do |gem|
76
+ cortege = where(:name => gem[0]).first
77
+ next if cortege[:version] > gem[1] # skip downgraded gems
78
+
79
+ cortege[:version] = gem[1]
80
+ cortege[:need_update] = true
81
+ cortege.save
82
+ end
83
+ end
84
+ end
85
+
86
+ def _deep_scanning(&callback) # no-doc
87
+ _surface_scanning # todo this call should be optional
88
+
89
+ bunch =
90
+ begin
91
+ gem_update_count = where(need_update: true).count
92
+ threads_count = gem_update_count > 105 ? 35 : 1
93
+ bunch_sub_arr_size = gem_update_count / threads_count + 1
94
+
95
+ bunch_tmp =
96
+ Array.new(threads_count) do |i|
97
+ self.where(need_update: true)
98
+ .order(:id)
99
+ .limit(bunch_sub_arr_size).offset(i * bunch_sub_arr_size)
100
+ end
101
+
102
+ bunch_tmp
103
+ end # bunch = [ [], [], [], [], ... [] ]
104
+
105
+ threads = []
106
+ bunch.count.times do |i|
107
+ threads << Thread.new(bunch[i], callback) do |sub_array, callback_proc|
108
+ sub_array.each_slice(20) do |gems|
109
+ db.transaction { rubygems_manage_corteges gems }
110
+ callback_proc.call gems.count unless callback_proc.nil?
111
+ end
112
+ end
113
+ end
114
+ threads.each &:join
115
+ end
116
+
117
+ # @param [String] name
118
+ # Gem name.
119
+ #
120
+ # @return [Nil, String]
121
+ # Returns +nil+ when rubygems.org not responded, otherwise it
122
+ # returns encoded json as a string.
123
+
124
+ def send_request_to_rubygems(name)
125
+ for try_count in 1...25 do
126
+ begin
127
+ sleep try_count * 0.35
128
+ url = URI.parse "https://rubygems.org/api/v1/gems/#{name}.json"
129
+ response = Net::HTTP.get_response(url)
130
+
131
+ return response.body unless response.body.empty?
132
+ rescue Errno::ECONNRESET
133
+ next
134
+ end
135
+ end
136
+
137
+ return nil
138
+ end
139
+
140
+ ##
141
+ # Updates +cortege+ with a data from +hsh+.
142
+ #
143
+ # @param [Rudisco::Gem] cortege
144
+ #
145
+ # @param [Hash] hsh
146
+
147
+ def update_cortege(cortege, hsh)
148
+ cortege[:description] = hsh["info"].to_s
149
+ cortege[:authors] = hsh["authors"].to_s
150
+ cortege[:license] = Array(hsh["licenses"]).join
151
+ cortege[:sha] = hsh["sha"].to_s
152
+
153
+ # "$ gem -list" and rubygems.org/api can send different information about
154
+ # last version for a gem. To prevent collision this code not downgrades
155
+ # gem version.
156
+ #
157
+ # In other words "$ gem list" have higher priority under rubygems.org/api
158
+ if hsh["version"].to_s > cortege[:version]
159
+ cortege[:version] = hsh["version"].to_s
160
+ end
161
+
162
+ cortege[:source_code_url] = hsh["source_code_uri"].to_s
163
+ cortege[:project_url] = hsh["project_uri"].to_s
164
+ cortege[:gem_url] = hsh["gem_uri"].to_s
165
+ cortege[:wiki_url] = hsh["wiki_uri"].to_s
166
+ cortege[:documentation_url] = hsh["documentation_uri"].to_s
167
+ cortege[:mailing_list_url] = hsh["mailing_list_uri"].to_s
168
+ cortege[:bug_tracker_url] = hsh["bug_tracker_uri"].to_s
169
+
170
+ cortege[:total_downloads] = hsh["downloads"].to_i
171
+ cortege[:version_downloads] = hsh["version_downloads"].to_i
172
+
173
+ cortege[:need_update] = false
174
+ end
175
+
176
+ # @param [Array<Rudisco::Gem>] corteges
177
+
178
+ def rubygems_manage_corteges(corteges)
179
+ corteges.each do |cortege|
180
+ response = send_request_to_rubygems cortege[:name]
181
+
182
+ if response.nil?
183
+ next
184
+ elsif response =~ /could not be found/
185
+ cortege.destroy # gem was deleted from rubygems.org
186
+ else
187
+ data = JSON.parse response
188
+ update_cortege cortege, data
189
+
190
+ cortege.save
191
+ end
192
+ end
193
+ end
194
+ end # module RubyGemsScanner
195
+ end # module Rudisco
@@ -0,0 +1,95 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ module Rudisco
4
+
5
+ ##
6
+ # == Table +gems+ structure:
7
+ #
8
+ # CREATE TABLE `gems` (
9
+ # `id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,
10
+ #
11
+ # `name` Text,
12
+ # `description` Text,
13
+ # `authors` Text,
14
+ # `version` Text,
15
+ # `license` Text,
16
+ # `sha` Text,
17
+ #
18
+ # `source_code_url` Text,
19
+ # `project_url` Text,
20
+ # `gem_url` Text,
21
+ # `wiki_url` Text,
22
+ # `documentation_url` Text,
23
+ # `mailing_list_url` Text,
24
+ # `bug_tracker_url` Text,
25
+ #
26
+ # `total_downloads` Integer,
27
+ # `version_downloads` Integer,
28
+ #
29
+ # `need_update` Boolean DEFAULT (1)
30
+ # );
31
+
32
+ class Gem < Sequel::Model
33
+ extend RubyGemsScanner
34
+ plugin :gem_extended_dataset
35
+
36
+ ##
37
+ # Case insensitive phrase search in :description, :name columns.
38
+ #
39
+ # @example
40
+ # phrase = 'rails'
41
+ # gems = Rudisco::Gem.find_phrase(phrase)
42
+ # .select(:name, :description)
43
+ #
44
+ # gems.each do |record|
45
+ # puts record[:name]
46
+ # puts record[:description]
47
+ # puts
48
+ # end
49
+ #
50
+ # @param [String] phrase
51
+ #
52
+ # @return [Gem::Dataset]
53
+
54
+ def self.find_phrase(phrase)
55
+ result = from(:gems).find_phrase phrase
56
+
57
+ return result
58
+ end
59
+
60
+ ##
61
+ # Provides external actions for specified cortege.
62
+ #
63
+ # @example
64
+ # path_to_load = File.join(__dir__, '..', 'tmp')
65
+ #
66
+ # sample = Rudisco::Gem.exclude(source_code_url: '').first
67
+ # sample.action(:open_sources)
68
+ # .action(:git_clone, path: path_to_load)
69
+ #
70
+ # sample2 = Rudisco::Gem.limit(2)
71
+ # sample2.action(:download, path: path_to_load)
72
+ #
73
+ # @param [Symbol] command
74
+ # Expecting +command+ values:
75
+ # :update @see GemActions#update
76
+ # :open_documentation @see GemActions#open_documentation
77
+ # :open_bug_tracker @see GemActions#open_bug_tracker
78
+ # :open_sources @see GemActions#open_sources
79
+ # :open_wiki @see GemActions#open_wiki
80
+ # :open_rubygems @see GemActions#open_rubygmes
81
+ # :download @see GemActions#download
82
+ # :git_clone @see GemActions#git_clone
83
+ #
84
+ # @param [Hash] params
85
+ # Additional parameters. Optional.
86
+ #
87
+ # @return [Rudisco::Gem]
88
+
89
+ def action(command, params = {})
90
+ GemActions.new(self).complete command, params
91
+
92
+ return self
93
+ end
94
+ end # class Gem
95
+ end # module Rudisco
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ module Rudisco
4
+ # === Dependencies from core lib
5
+
6
+ require 'csv'
7
+ require 'net/http'
8
+ require 'uri'
9
+ require 'json'
10
+ require 'tempfile'
11
+
12
+ # === Dependencies from rubygems.org
13
+
14
+ require 'sqlite3'
15
+ require 'sequel'
16
+ require 'thor'
17
+ require 'command_line_reporter'
18
+
19
+ # === Project structure
20
+
21
+ ##
22
+ # Ancestor class for Rudisco's exceptions.
23
+
24
+ class Error < StandardError; end
25
+
26
+ ##
27
+ # Loads *.rb files in requested order.
28
+
29
+ def self.load(**params)
30
+ params[:files].each do |f|
31
+ require File.join(__dir__, params[:folder].to_s, f)
32
+ end
33
+ end
34
+ private_class_method :load
35
+
36
+ # === Core
37
+
38
+ load files: %w(helpers sqlite)
39
+
40
+ load folder: 'models/gem',
41
+ files: %w(rubygems_scanner actions dataset_methods)
42
+
43
+ load folder: 'models',
44
+ files: %w(gem)
45
+
46
+ # === Command-Line-Interface
47
+
48
+ load folder: 'cli',
49
+ files: %w(presentation)
50
+
51
+ load folder: 'cli/presentation',
52
+ files: %w(find show update download git_clone open)
53
+
54
+ load files: %w(cli/cli) # cli/routes
55
+ end # module Rudisco
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ module Rudisco
4
+ path_to_database =
5
+ File.join(__dir__, '../../sources/database/rudisco.db')
6
+
7
+ Sequel.connect "sqlite://#{path_to_database}",
8
+ max_connections: 40,
9
+ pool_sleep_time: 0.015,
10
+ single_threaded: true,
11
+ pool_timeout: 60000
12
+ end # module Rudisco
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ #
4
+ # Rubygems database (https://rubygems.org)
5
+ #
6
+ # @see https://github.com/Medvedu/rudisco
7
+ # @see readme.md
8
+ #
9
+ # @author Kuzichev Michael
10
+ # @license MIT
11
+ module Rudisco
12
+ require_relative 'rudisco/project_structure'
13
+ end # module Rudisco
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ module Rudisco
5
+ describe Gem do
6
+ it 'includes certain columns' do
7
+ expect(described_class.columns)
8
+ .to include(:id, :name, :description, :source_code_url)
9
+ end
10
+
11
+ it 'works as Sequel ORM' do
12
+ expect(described_class.where {total_downloads > 10000000 }.count)
13
+ .to be > 0
14
+ end
15
+
16
+ # ----------------------------------------------------
17
+
18
+ describe "#find_phrase" do
19
+ it 'case insensitive searches in :description, :name columns' do
20
+ gems = described_class.find_phrase 'monkey'
21
+
22
+ gems.each do |gem|
23
+ expect((gem.name =~ /monkey/i) || (gem.description =~ /monkey/i))
24
+ .to_not be_nil
25
+ end
26
+ end
27
+ end # describe "#find_phrase"
28
+
29
+ # ----------------------------------------------------
30
+
31
+ describe "#action" do
32
+ it 'raises an exception when action is unknown' do
33
+ sample = described_class.first
34
+
35
+ expect{ sample.action :misspelled_action }
36
+ .to raise_exception GemActions::Unknown
37
+ end
38
+ end # describe "#action"
39
+ end # describe Gem
40
+ end # module Rudisco
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+ require_relative '../sources/rudisco'
3
+
4
+ RSpec.configure do |config|
5
+ end