dca 0.1.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.
Files changed (70) hide show
  1. data/.document +5 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +48 -0
  4. data/Gemfile.lock +126 -0
  5. data/Guardfile +8 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.rdoc +19 -0
  8. data/Rakefile +48 -0
  9. data/VERSION +1 -0
  10. data/bin/dca +5 -0
  11. data/dca.gemspec +160 -0
  12. data/lib/dca.rb +64 -0
  13. data/lib/dca/cli.rb +32 -0
  14. data/lib/dca/commands/area.rb +133 -0
  15. data/lib/dca/commands/templates/area/analyzer.rb.erb +34 -0
  16. data/lib/dca/commands/templates/area/area.rb.erb +2 -0
  17. data/lib/dca/commands/templates/area/models.rb.erb +2 -0
  18. data/lib/dca/commands/templates/area/page.rb.erb +17 -0
  19. data/lib/dca/commands/templates/area/position.rb.erb +8 -0
  20. data/lib/dca/commands/templates/config.yml.erb +38 -0
  21. data/lib/dca/commands/templates/spec/analyzer_spec.rb.erb +15 -0
  22. data/lib/dca/commands/templates/spec/spec_helper.rb.erb +2 -0
  23. data/lib/dca/config.rb +20 -0
  24. data/lib/dca/helpers.rb +2 -0
  25. data/lib/dca/helpers/logger.rb +50 -0
  26. data/lib/dca/jobs.rb +3 -0
  27. data/lib/dca/jobs/analyzer_job.rb +119 -0
  28. data/lib/dca/jobs/job.rb +62 -0
  29. data/lib/dca/models.rb +5 -0
  30. data/lib/dca/models/base_model.rb +73 -0
  31. data/lib/dca/models/binder.rb +68 -0
  32. data/lib/dca/models/binder_helper.rb +48 -0
  33. data/lib/dca/models/nokogiri_binder.rb +43 -0
  34. data/lib/dca/models/position.rb +15 -0
  35. data/lib/dca/net.rb +1 -0
  36. data/lib/dca/net/browser_helper.rb +20 -0
  37. data/lib/dca/notifier.rb +2 -0
  38. data/lib/dca/notifier/notifier.rb +11 -0
  39. data/lib/dca/notifier/redis/models/analyze_notify.rb +12 -0
  40. data/lib/dca/notifier/redis/models/failure_notify.rb +8 -0
  41. data/lib/dca/notifier/redis/models/fetch_notify.rb +15 -0
  42. data/lib/dca/notifier/redis/models/session.rb +52 -0
  43. data/lib/dca/notifier/redis/notifier.rb +25 -0
  44. data/lib/dca/notifier/redis_notifier.rb +9 -0
  45. data/lib/dca/storage.rb +3 -0
  46. data/lib/dca/storage/elasticsearch_storage.rb +80 -0
  47. data/lib/dca/storage/mongo_storage.rb +51 -0
  48. data/lib/dca/storage/storage.rb +55 -0
  49. data/spec/analyzer_spec.rb +64 -0
  50. data/spec/area_task_spec.rb +45 -0
  51. data/spec/base_model_spec.rb +34 -0
  52. data/spec/binder_spec.rb +69 -0
  53. data/spec/config.yml +18 -0
  54. data/spec/elasticsearch_storage_spec.rb +28 -0
  55. data/spec/fixtures/page.html +12 -0
  56. data/spec/fixtures/positions.yml +13 -0
  57. data/spec/fixtures/positions_with_error.yml +14 -0
  58. data/spec/fixtures/states.yml +3 -0
  59. data/spec/job_spec.rb +31 -0
  60. data/spec/mock/analyzer_job.rb +30 -0
  61. data/spec/mock/file_storage.rb +28 -0
  62. data/spec/mock/notify_object.rb +13 -0
  63. data/spec/mock/page.rb +13 -0
  64. data/spec/mock/position.rb +40 -0
  65. data/spec/mock/web_notifier.rb +30 -0
  66. data/spec/mongo_storage_spec.rb +20 -0
  67. data/spec/redis_notifier_spec.rb +98 -0
  68. data/spec/spec_helper.rb +27 -0
  69. data/spec/support/storage_examples.rb +103 -0
  70. metadata +408 -0
@@ -0,0 +1,25 @@
1
+ module DCA
2
+ module Redis
3
+ class Notifier
4
+ def initialize config
5
+ Ohm.connect :url => "redis://#{config[:host]}:#{config[:port]}"
6
+ end
7
+
8
+ def push(object, event, options = {})
9
+ session = Session.find(:uid => object.session, :project => DCA.project_name, :area => object.class.queue).first
10
+ unless session.present?
11
+ session = Session.create :uid => object.session, :created => Time.now, :project => DCA.project_name,
12
+ :area => object.class.queue
13
+ end
14
+
15
+ if event == :analyze
16
+ session.inc_analyze options[:state]
17
+ elsif event == :fetch
18
+ session.inc_fetch options[:state], options[:result] ? :success : :failure
19
+ elsif event == :failure
20
+ session.add_failure options[:exception]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ require File.expand_path('../redis/models/failure_notify', __FILE__)
2
+ require File.expand_path('../redis/models/analyze_notify', __FILE__)
3
+ require File.expand_path('../redis/models/fetch_notify', __FILE__)
4
+ require File.expand_path('../redis/models/session', __FILE__)
5
+ require File.expand_path('../redis/notifier', __FILE__)
6
+
7
+ module DCA
8
+ class RedisNotifier < Redis::Notifier; end
9
+ end
@@ -0,0 +1,3 @@
1
+ require File.expand_path('../storage/storage', __FILE__)
2
+ require File.expand_path('../storage/elasticsearch_storage', __FILE__)
3
+ require File.expand_path('../storage/mongo_storage', __FILE__)
@@ -0,0 +1,80 @@
1
+ module DCA
2
+ class ElasticSearchStorage
3
+ attr_reader :type
4
+
5
+ def initialize(connection, context, options = {})
6
+ @connection = connection
7
+ @index = options[:index] || DCA.project_name.underscore
8
+ @type = options[:type] || context.to_s.demodulize.downcase.pluralize
9
+ end
10
+
11
+ def self.establish_connection(config)
12
+ RestClient.get("http://#{config[:host]}:#{config[:port]}")
13
+ Tire.configure { url "http://#{config[:host]}:#{config[:port]}" }
14
+ end
15
+
16
+
17
+ def state position
18
+ item = find position
19
+ return :create if item.nil?
20
+
21
+ position.id = item[:id]
22
+ return :unmodified if item[:checksum] == position.checksum
23
+
24
+ :update
25
+ end
26
+
27
+ def find position
28
+ return nil if position.base_id.nil?
29
+ query = Tire.search(@index, :type => type) { query { term :base_id, position.base_id } }
30
+ query.results.first
31
+ end
32
+
33
+ def refresh(item, state)
34
+ send state, item
35
+ end
36
+
37
+ def create(item)
38
+ item.updated_at = item.created_at = Time.now.utc
39
+ data = item.to_hash
40
+ data[:type] = type
41
+
42
+ result = Tire.index(@index).store data
43
+ Tire.index(@index).refresh
44
+
45
+ item.id = result['_id']
46
+ end
47
+
48
+ def update(item)
49
+ data = item.to_hash
50
+ data[:type] = type
51
+
52
+ Tire.index(@index) do
53
+ store data
54
+ refresh
55
+ end
56
+ end
57
+
58
+ def remove(item)
59
+ data = item.to_hash
60
+ data[:type] = type
61
+
62
+ Tire.index(@index) do
63
+ remove data
64
+ refresh
65
+ end
66
+ end
67
+
68
+ def index(&block)
69
+ Tire.index @index do
70
+ instance_eval(&block) if block_given?
71
+ end
72
+ end
73
+
74
+ def context object
75
+ result = self.clone
76
+ result.instance_variable_set :@type, object.to_s.demodulize.downcase.pluralize
77
+ result
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,51 @@
1
+ module DCA
2
+ class MongoStorage
3
+ attr_reader :database, :collection
4
+
5
+ def initialize(connection, context, options = {})
6
+ @database = connection.db(options[:database] || DCA.project_name.underscore)
7
+ @connection = connection
8
+ @collection = database.collection(options[:collection] || context.to_s.demodulize.downcase.pluralize)
9
+ end
10
+
11
+ def self.establish_connection(config)
12
+ Mongo::Connection.new config[:host], config[:port]
13
+ end
14
+
15
+ def state(position)
16
+ item = find position
17
+ return :create if item.nil?
18
+
19
+ position.id = item['_id']
20
+ return :unmodified if item['checksum'] == position.checksum
21
+
22
+ return :update
23
+ end
24
+
25
+ def find position
26
+ collection.find_one base_id: position.base_id unless position.base_id.nil?
27
+ end
28
+
29
+ def refresh(item, state)
30
+ send state, item
31
+ end
32
+
33
+ def create(item)
34
+ item.id = collection.insert item.to_hash
35
+ end
36
+
37
+ def update(item)
38
+ collection.update({_id: item.id}, item.to_hash)
39
+ end
40
+
41
+ def remove(item)
42
+ collection.remove _id: item.id
43
+ end
44
+
45
+ def context object
46
+ result = self.clone
47
+ result.instance_variable_set :@collection, result.database.collection(object.to_s.demodulize.downcase.pluralize)
48
+ result
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,55 @@
1
+ module DCA
2
+ module Storage
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def establish_connection config_name = :db
7
+ @config = APP_CONFIG[config_name]
8
+ end
9
+
10
+ def root_storage
11
+ return @storage if @storage
12
+
13
+ return storage if @config
14
+
15
+ superclass.root_storage if superclass != Object
16
+ end
17
+
18
+ def storage
19
+ return @storage unless @storage.nil?
20
+
21
+ if @config
22
+ driver_class = "DCA::#{@config[:driver]}Storage".constantize
23
+ connection = driver_class.establish_connection @config
24
+ @storage = driver_class.new connection, self, @config
25
+ else
26
+ @storage = superclass.root_storage.context self
27
+ end
28
+ end
29
+ end
30
+
31
+ def state
32
+ self.class.storage.state self
33
+ end
34
+
35
+ def save
36
+ return false unless valid?
37
+
38
+ current_state = state
39
+
40
+ callback = "before_#{state}"
41
+ send callback if self.respond_to? callback
42
+
43
+ self.class.storage.refresh self, current_state
44
+
45
+ callback = "after_#{state}"
46
+ send callback if self.respond_to? callback
47
+
48
+ current_state
49
+ end
50
+
51
+ def destroy
52
+ self.class.storage.refresh self, :remove
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,64 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require File.expand_path('../mock/position', __FILE__)
3
+ require File.expand_path('../mock/analyzer_job', __FILE__)
4
+ require File.expand_path('../mock/file_storage', __FILE__)
5
+ require File.expand_path('../mock/web_notifier', __FILE__)
6
+
7
+ include DCA
8
+
9
+ describe 'Analyzer jobs' do
10
+ before :all do
11
+ Notifier.create APP_CONFIG[:notifier]
12
+ end
13
+
14
+ after :all do
15
+ Tire.index('test') do
16
+ delete
17
+ refresh
18
+ end
19
+ end
20
+
21
+ it "should notice about analyzed positions" do
22
+ Mock::WebNotifier.clean
23
+ Mock::AnalyzerJob.create :fixture => './spec/fixtures/positions.yml'
24
+ Mock::WebNotifier.queue[:analyze][:create].should equal 2
25
+ end
26
+
27
+ it "should notice about fetch positions" do
28
+ Mock::WebNotifier.clean
29
+ Mock::AnalyzerJob.create :fixture => './spec/fixtures/positions.yml'
30
+ Mock::WebNotifier.queue[:fetch][:create].should equal 2
31
+ end
32
+
33
+ it "should notice about failed fetch positions" do
34
+ Mock::WebNotifier.clean
35
+ Mock::AnalyzerJob.create :fixture => './spec/fixtures/positions.yml'
36
+ Mock::WebNotifier.queue[:failed][:create].should equal 1
37
+ end
38
+
39
+ it "should fetch only newly created and modified position" do
40
+ Mock::WebNotifier.clean
41
+ Mock::AnalyzerJob.create :fixture => './spec/fixtures/positions.yml'
42
+ Mock::WebNotifier.queue[:analyze][:unmodified].should equal 1
43
+ Mock::WebNotifier.queue[:fetch][:create].should equal 2
44
+ end
45
+
46
+ it "should fetch next position even if exception occurs" do
47
+ Mock::WebNotifier.clean
48
+ Mock::AnalyzerJob.create :fixture => './spec/fixtures/positions_with_error.yml'
49
+ Mock::WebNotifier.queue[:failed][:create].should equal 1
50
+ end
51
+
52
+ it "should analyzed only limit count of posiotions" do
53
+ Mock::WebNotifier.clean
54
+ Mock::AnalyzerJob.create :limit => 1, :fixture => './spec/fixtures/positions.yml'
55
+ Mock::WebNotifier.queue[:analyze][:create].should equal 1
56
+ end
57
+
58
+ it "should support distribute analyze" do
59
+ Mock::WebNotifier.clean
60
+ Mock::AnalyzerJob.create :fixture => './spec/fixtures/positions.yml', :distributed => true
61
+ Mock::WebNotifier.queue[:analyze][:unmodified].should equal 1
62
+ Mock::WebNotifier.queue[:fetch][:create].should equal 2
63
+ end
64
+ end
@@ -0,0 +1,45 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require File.expand_path('../mock/analyzer_job', __FILE__)
3
+
4
+ describe 'Area rake task' do
5
+ def workers_clean
6
+ `ps aux | grep -P "resque-\\d"`.split("\n").each do |line|
7
+ pid = line.split(' ')[1].to_i
8
+ Process.kill 'TERM', pid
9
+ end
10
+
11
+ sleep 1 while workers_count > 0
12
+ end
13
+
14
+ before :each do
15
+ Resque.inline = false
16
+ workers_clean
17
+ end
18
+
19
+ after :all do
20
+ Resque.remove_queue 'Mock'
21
+ Resque.inline = true
22
+ workers_clean
23
+ #FileUtils.rm_rf File.join(DCA.root, 'log')
24
+ end
25
+
26
+ def workers_count
27
+ `ps aux | grep -P "resque-\\d"`.split("\n").count
28
+ end
29
+
30
+ it 'should stop area analyze work' do
31
+ current_count = workers_count + 1
32
+ `rake resque:work BACKGROUND=1 QUEUE=Mock`
33
+ sleep 1
34
+ workers_count.should equal current_count
35
+
36
+ DCA::CLI.new.area 'stop', 'Mock'
37
+ workers_count.should equal 0
38
+ end
39
+
40
+ it 'should start area analyze work' do
41
+ DCA::CLI.new.area 'start', 'Mock'
42
+ sleep 2
43
+ workers_count.should equal 1
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require File.expand_path('../mock/position', __FILE__)
3
+
4
+ include DCA
5
+
6
+ describe Models::BaseModel do
7
+ let(:position) { Mock::RootPosition.new }
8
+ let(:child_position) { Mock::ChildPosition.new }
9
+ describe 'when validate has_one associations and model invalid' do
10
+ before do
11
+ position.one_child = child_position
12
+ position.valid?
13
+ end
14
+
15
+ it 'should contains associations errors' do
16
+ position.errors[:one_child].should_not be_nil
17
+ end
18
+ end
19
+
20
+ describe 'when validate has_many associations and model invalid' do
21
+ before do
22
+ position.child_position = [child_position, child_position]
23
+ position.valid?
24
+ end
25
+
26
+ it 'should contains associations errors' do
27
+ position.errors[:child_position].should_not be_nil
28
+ end
29
+
30
+ it 'should contains associations errors array' do
31
+ position.errors[:child_position].count.should equal 2
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,69 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require File.expand_path('../mock/position', __FILE__)
3
+ require File.expand_path('../mock/page', __FILE__)
4
+
5
+ include DCA
6
+
7
+ describe 'Binder' do
8
+ it 'should bind model fields' do
9
+ page = Mock::Page.new
10
+ page.bind Nokogiri::HTML open('./spec/fixtures/page.html')
11
+
12
+ page.positions.should have(3).items
13
+ page.positions.each_with_index do |position, index|
14
+ position.base_id.should == index + 1
15
+ position.title.should == "Position #{index + 1}"
16
+ end
17
+ end
18
+
19
+ it 'should bind polymorphic association' do
20
+ page = Mock::PageExt.new :category => :full
21
+ page.bind Nokogiri::HTML open('./spec/fixtures/page.html')
22
+
23
+ page.positions.should have(3).items
24
+ page.positions.each { |position| position.is_a?(Mock::FullPosition).should be_true }
25
+
26
+
27
+ page = Mock::PageExt.new :category => :ext
28
+ page.bind Nokogiri::HTML open('./spec/fixtures/page.html')
29
+
30
+ page.positions.should have(3).items
31
+ page.positions.each { |position| position.is_a?(Mock::ExtPosition).should be_true }
32
+ end
33
+
34
+ it 'should inherit binding options' do
35
+ Mock::ExtPosition.associations.should have(4).items
36
+ Mock::ExtPosition.associations.keys.should include(:base_id, :title, :description)
37
+ end
38
+
39
+ it 'should override binding options' do
40
+ Mock::FullPosition.associations.should have(2).items
41
+ Mock::FullPosition.associations[:base_id][:type].should == :string
42
+ Mock::FullPosition.associations[:base_id][:options].should == { :selector => 'a' }
43
+ end
44
+
45
+ it 'should serializable' do
46
+ position = Mock::ChildPosition.new name: 'position'
47
+ position.to_hash.should have_key 'name'
48
+ end
49
+
50
+ it 'should serializable with child' do
51
+ position = Mock::RootPosition.new
52
+ position.one_child = Mock::ChildPosition.new(name: 'child_position')
53
+ position.child_position = [Mock::ChildPosition.new(name: 'position')]
54
+
55
+ hash = position.to_hash
56
+ hash['one_child'].is_a?(Hash).should == true
57
+
58
+ hash['child_position'][0].is_a?(Hash).should == true
59
+ end
60
+
61
+ it 'should append to has many association' do
62
+ page = Mock::PageExt.new :category => :full
63
+ page.bind Nokogiri::HTML open('./spec/fixtures/page.html')
64
+ page.bind Nokogiri::HTML open('./spec/fixtures/page.html')
65
+
66
+ page.positions.should have(6).items
67
+
68
+ end
69
+ end