dca 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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