sleek 0.0.1

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 (45) hide show
  1. data/.gitignore +17 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +22 -0
  6. data/README.md +256 -0
  7. data/Rakefile +6 -0
  8. data/lib/sleek/base.rb +52 -0
  9. data/lib/sleek/core_ext/range.rb +44 -0
  10. data/lib/sleek/core_ext/time.rb +2 -0
  11. data/lib/sleek/event.rb +37 -0
  12. data/lib/sleek/filter.rb +24 -0
  13. data/lib/sleek/interval.rb +41 -0
  14. data/lib/sleek/queries/average.rb +21 -0
  15. data/lib/sleek/queries/count.rb +17 -0
  16. data/lib/sleek/queries/count_unique.rb +21 -0
  17. data/lib/sleek/queries/maximum.rb +21 -0
  18. data/lib/sleek/queries/minimum.rb +21 -0
  19. data/lib/sleek/queries/query.rb +105 -0
  20. data/lib/sleek/queries/sum.rb +21 -0
  21. data/lib/sleek/queries/targetable.rb +13 -0
  22. data/lib/sleek/queries.rb +8 -0
  23. data/lib/sleek/query_collection.rb +25 -0
  24. data/lib/sleek/timeframe.rb +85 -0
  25. data/lib/sleek/version.rb +3 -0
  26. data/lib/sleek.rb +23 -0
  27. data/sleek.gemspec +28 -0
  28. data/sleek.png +0 -0
  29. data/spec/lib/sleek/base_spec.rb +48 -0
  30. data/spec/lib/sleek/event_spec.rb +21 -0
  31. data/spec/lib/sleek/filter_spec.rb +26 -0
  32. data/spec/lib/sleek/interval_spec.rb +24 -0
  33. data/spec/lib/sleek/queries/average_spec.rb +13 -0
  34. data/spec/lib/sleek/queries/count_spec.rb +13 -0
  35. data/spec/lib/sleek/queries/count_unique_spec.rb +15 -0
  36. data/spec/lib/sleek/queries/maximum_spec.rb +13 -0
  37. data/spec/lib/sleek/queries/minimum_spec.rb +13 -0
  38. data/spec/lib/sleek/queries/query_spec.rb +226 -0
  39. data/spec/lib/sleek/queries/sum_spec.rb +13 -0
  40. data/spec/lib/sleek/queries/targetable_spec.rb +29 -0
  41. data/spec/lib/sleek/query_collection_spec.rb +27 -0
  42. data/spec/lib/sleek/timeframe_spec.rb +86 -0
  43. data/spec/lib/sleek_spec.rb +10 -0
  44. data/spec/spec_helper.rb +17 -0
  45. metadata +203 -0
@@ -0,0 +1,105 @@
1
+ module Sleek
2
+ module Queries
3
+ class Query
4
+ attr_reader :namespace, :bucket, :options
5
+
6
+ # Internal: Initialize the query.
7
+ #
8
+ # namespace - the Symbol namespace name.
9
+ # bucket - the String bucket name.
10
+ # options - the optional Hash of options.
11
+ # :timeframe - the optional timeframe description.
12
+ # :interval - the optional interval description.
13
+ #
14
+ # Raises ArgumentError if passed options are invalid.
15
+ def initialize(namespace, bucket, options = {})
16
+ @namespace = namespace
17
+ @bucket = bucket
18
+ @options = options
19
+
20
+ raise ArgumentError, "options are invalid" unless valid_options?
21
+ end
22
+
23
+ # Internal: Get Mongoid::Criteria for events to perform the query.
24
+ #
25
+ # time_range - the optional range of Time objects.
26
+ def events(time_range = time_range)
27
+ evts = Event.where(namespace: namespace, bucket: bucket)
28
+ evts = evts.between("s.t" => time_range) if time_range
29
+ evts = apply_filters(evts) if filter?
30
+ evts
31
+ end
32
+
33
+ # Internal: Apply all the filters to the criteria.
34
+ def apply_filters(criteria)
35
+ filters.inject(criteria) { |crit, filter| filter.apply(crit) }
36
+ end
37
+
38
+ # Internal: Get filters.
39
+ def filters
40
+ filters = options[:filter]
41
+
42
+ if filters.is_a?(Array) && filters.size == 3 && filters.none? { |f| f.is_a?(Array) }
43
+ filters = [filters]
44
+ elsif !filters.is_a?(Array) || !filters.all? { |f| f.is_a?(Array) && f.size == 3 }
45
+ raise ArgumentError, "wrong filter - #{filters}"
46
+ end
47
+
48
+ filters.map { |f| Sleek::Filter.new(*f) }
49
+ end
50
+
51
+ # Internal: Get timeframe for the query.
52
+ #
53
+ # Returns nil if no timeframe was passed to initializer.
54
+ def timeframe
55
+ Sleek::Timeframe.new(options[:timeframe]) if timeframe?
56
+ end
57
+
58
+ # Internal: Get timeframe range.
59
+ def time_range
60
+ timeframe.try(:to_time_range)
61
+ end
62
+
63
+ def series
64
+ Sleek::Interval.new(options[:interval], timeframe).timeframes if series? && timeframe?
65
+ end
66
+
67
+ # Internal: Check if options include filter.
68
+ def filter?
69
+ options[:filter].present?
70
+ end
71
+
72
+ # Internal: Check if options include timeframe.
73
+ def timeframe?
74
+ options[:timeframe].present?
75
+ end
76
+
77
+ # Internal: Check if options include interval.
78
+ def series?
79
+ options[:interval].present?
80
+ end
81
+
82
+ # Internal: Run the query.
83
+ def run
84
+ if series?
85
+ series.map do |tf|
86
+ { timeframe: tf, value: perform(events(tf.to_time_range)) }
87
+ end
88
+ else
89
+ perform(events)
90
+ end
91
+ end
92
+
93
+ # Internal: Perform the query on a set of events.
94
+ def perform(events)
95
+ raise NotImplementedError
96
+ end
97
+
98
+ # Internal: Validate the options.
99
+ def valid_options?
100
+ options.is_a?(Hash) && (filter? ? options[:filter].is_a?(Array) : true) &&
101
+ (series? ? timeframe? && series : true)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,21 @@
1
+ module Sleek
2
+ module Queries
3
+ # Internal: Sum query.
4
+ #
5
+ # Finds the average value for a given property.
6
+ #
7
+ # target_property - the String name of target property on event.
8
+ #
9
+ # Examples
10
+ #
11
+ # sleek.queries.sum(:purchases, target_property: "total")
12
+ # # => 2_072_70
13
+ class Sum < Query
14
+ include Targetable
15
+
16
+ def perform(events)
17
+ events.sum target_property
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ module Sleek
2
+ module Queries
3
+ module Targetable
4
+ def valid_options?
5
+ super && options[:target_property].present?
6
+ end
7
+
8
+ def target_property
9
+ "d.#{options[:target_property]}"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ require 'sleek/queries/query'
2
+ require 'sleek/queries/targetable'
3
+ require 'sleek/queries/count'
4
+ require 'sleek/queries/count_unique'
5
+ require 'sleek/queries/minimum'
6
+ require 'sleek/queries/maximum'
7
+ require 'sleek/queries/average'
8
+ require 'sleek/queries/sum'
@@ -0,0 +1,25 @@
1
+ module Sleek
2
+ class QueryCollection
3
+ attr_reader :namespace
4
+
5
+ # Inernal: Initialize query collection.
6
+ #
7
+ # namespace - the Symbol namespace name.
8
+ def initialize(namespace)
9
+ @namespace = namespace
10
+ end
11
+
12
+ # Define method for each query
13
+ [:count, :count_unique, :minimum, :maximum, :sum, :average].each do |name|
14
+ klass = "Sleek::Queries::#{name.to_s.camelize}".constantize
15
+
16
+ define_method(name) do |bucket, options = {}|
17
+ klass.new(namespace, bucket, options).run
18
+ end
19
+ end
20
+
21
+ def inspect
22
+ "#<Sleek::QueryCollection ns=#{namespace}>"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,85 @@
1
+ module Sleek
2
+ class Timeframe
3
+ # Internal: Initialize timeframe.
4
+ #
5
+ # timeframe - either a range of Time objects, an array of two Time
6
+ # objects, or a special string.
7
+ #
8
+ # Possible special string format:
9
+ # (this|previous)_((\d+)_)?(minute|hour|day|week|month)s?
10
+ #
11
+ # Examples
12
+ #
13
+ # Timeframe.new "this_2_days"
14
+ #
15
+ # Timeframe.new "previous_hour"
16
+ def initialize(timeframe)
17
+ @timeframe = timeframe
18
+ end
19
+
20
+ # Public: Get timeframe start.
21
+ def start
22
+ to_time_range.begin
23
+ end
24
+
25
+ # Public: Get timeframe end.
26
+ def end
27
+ to_time_range.end
28
+ end
29
+
30
+ # Internal: Convert Timeframe instance to a range of Time objects.
31
+ def to_time_range
32
+ @range ||= self.class.to_range(@timeframe)
33
+ end
34
+
35
+ def inspect
36
+ "#<Sleek::Timeframe #{to_time_range}>"
37
+ end
38
+
39
+ class << self
40
+ # Internal: Transform the object passed to Timeframe initializer
41
+ # into a range of Time objects.
42
+ #
43
+ # timeframe - either a Range of Time objects, a two-element array
44
+ # of Time Objects, or a special string.
45
+ #
46
+ # Raises ArgumentError if passed object can't be processed.
47
+ def to_range(timeframe)
48
+ case timeframe
49
+ when Range
50
+ t = timeframe if timeframe.time_range?
51
+ when Array
52
+ t = timeframe.first..timeframe.last if timeframe.size == 2 && timeframe.count { |tf| tf.is_a?(Time) } == 2
53
+ when String, Symbol
54
+ t = parse(timeframe.to_s)
55
+ end
56
+
57
+ raise ArgumentError, "wrong timeframe" unless t
58
+ t
59
+ end
60
+
61
+ # Internal: Process timeframe string to make up a range.
62
+ #
63
+ # timeframe - the String matching
64
+ # (this|previous)_((\d+)_)?(minute|hour|day|week|month)s?
65
+ def parse(timeframe)
66
+ regexp = /(this|previous)_((\d+)_)?(minute|hour|day|week|month)s?/
67
+ _, category, _, number, interval = *timeframe.match(regexp)
68
+
69
+ unless category && interval
70
+ raise ArgumentError, "special timeframe string is malformed"
71
+ end
72
+
73
+ number ||= 1
74
+ number = number.to_i
75
+
76
+ end_point = Time.now.send("end_of_#{interval}").round
77
+ start_point = end_point - number.send(interval)
78
+
79
+ range = start_point..end_point
80
+ range = range - 1.send(interval) if category == 'previous'
81
+ range
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,3 @@
1
+ module Sleek
2
+ VERSION = "0.0.1"
3
+ end
data/lib/sleek.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'mongoid'
2
+
3
+ require 'sleek/core_ext/range'
4
+ require 'sleek/core_ext/time'
5
+
6
+ require 'sleek/version'
7
+ require 'sleek/timeframe'
8
+ require 'sleek/interval'
9
+ require 'sleek/filter'
10
+ require 'sleek/event'
11
+ require 'sleek/queries'
12
+ require 'sleek/query_collection'
13
+ require 'sleek/base'
14
+
15
+ module Sleek
16
+ def self.for_namespace(namespace)
17
+ Base.new namespace
18
+ end
19
+
20
+ def self.[](namespace)
21
+ for_namespace(namespace)
22
+ end
23
+ end
data/sleek.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sleek/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sleek"
8
+ spec.version = Sleek::VERSION
9
+ spec.authors = ["Gosha Arinich"]
10
+ spec.email = ["me@goshakkk.name"]
11
+ spec.description = %q{Sleek is a library for doing analytics.}
12
+ spec.summary = %q{Sleek is a library for doing analytics.}
13
+ spec.homepage = "http://github.com/goshakkk/sleek"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency 'mongoid', '~> 3.1'
22
+ spec.add_runtime_dependency 'activesupport', '~> 3.2'
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.3"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency 'rspec', '~> 2.13'
27
+ spec.add_development_dependency 'database_cleaner', '~> 0.9'
28
+ end
data/sleek.png ADDED
Binary file
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Base do
4
+ subject(:sleek) { Sleek::Base.new(:default) }
5
+
6
+ describe "#initialize" do
7
+ it "sets the namespace" do
8
+ sleek = Sleek::Base.new(:my_namespace)
9
+ expect(sleek.namespace).to eq :my_namespace
10
+ end
11
+ end
12
+
13
+ describe "#record" do
14
+ it "creates an event record" do
15
+ data = { name: 'John Doe', email: 'j@d.com' }
16
+
17
+ Sleek::Event.should_receive(:create_with_namespace).with(:default, "signups", data)
18
+
19
+ sleek.record("signups", data)
20
+ end
21
+ end
22
+
23
+ describe "#queries" do
24
+ it "returns QueryCollection for current namespace" do
25
+ Sleek::QueryCollection.should_receive(:new).with(:default).and_call_original
26
+ qc = sleek.queries
27
+ expect(qc.namespace).to eq :default
28
+ end
29
+ end
30
+
31
+ describe "#delete_bucket" do
32
+ it "deletes everything from the bucket" do
33
+ events = stub('events')
34
+ sleek.should_receive(:events).with(:abc).and_return(events)
35
+ events.should_receive(:delete_all)
36
+ sleek.delete_bucket(:abc)
37
+ end
38
+ end
39
+
40
+ describe "#delete_property" do
41
+ it "it deletes the property from all events in bucket" do
42
+ events = stub('events')
43
+ sleek.should_receive(:events).with(:abc).and_return(events)
44
+ events.should_receive(:unset).with("d.test")
45
+ sleek.delete_property(:abc, "test")
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Event do
4
+ describe ".create_with_namespace" do
5
+ it "creates event record" do
6
+ expect { Sleek::Event.create_with_namespace(:default, "signups", { name: 'John Doe', email: 'john@doe.com' }) }.to change { Sleek::Event.count }.by(1)
7
+ end
8
+
9
+ it "sets namespace and bucket" do
10
+ evt = Sleek::Event.create_with_namespace(:default, "signups", {})
11
+ expect(evt.namespace).to eq :default
12
+ expect(evt.bucket).to eq "signups"
13
+ end
14
+
15
+ it "sets the data" do
16
+ data = { name: 'John Doe', email: 'j@d.com' }
17
+ evt = Sleek::Event.create_with_namespace(:ns, "buc", data)
18
+ expect(evt.data).to eq data
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Filter do
4
+ describe "#initialize" do
5
+ context "when valid operator is passed" do
6
+ it "does not raise an exception" do
7
+ expect { Sleek::Filter.new(:test, :eq, 1) }.to_not raise_exception ArgumentError
8
+ end
9
+ end
10
+
11
+ context "when invalid operator is passed" do
12
+ it "raises an exception" do
13
+ expect { Sleek::Filter.new(:test, :lol, 1) }.to raise_exception ArgumentError, "unsupported operator - lol"
14
+ end
15
+ end
16
+ end
17
+
18
+ describe "#apply" do
19
+ it "appleis the filter to the criteria" do
20
+ filter = Sleek::Filter.new(:test, :eq, 1)
21
+ criteria = stub('criteria')
22
+ criteria.should_receive(:eq).with("d.test" => 1)
23
+ filter.apply(criteria)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Interval do
4
+ describe "#initialize" do
5
+ it "transforms interval description into value" do
6
+ Sleek::Interval.should_receive(:interval_value).with(:hourly)
7
+ Sleek::Interval.new(:hourly, Sleek::Timeframe.new(Time.now.all_day))
8
+ end
9
+ end
10
+
11
+ describe "#timeframes" do
12
+ it "slices timeframe into interval-long timeframes" do
13
+ bd = Time.now.beginning_of_day
14
+ interval = Sleek::Interval.new(:hourly, Sleek::Timeframe.new(Time.now.all_day))
15
+ expect(interval.timeframes.count).to eq 23
16
+
17
+ 23.times do |i|
18
+ subtf = interval.timeframes[i]
19
+ expect(subtf.start).to eq bd + 1.hour * i
20
+ expect(subtf.end).to eq bd + 1.hour * (i + 1)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Queries::Average do
4
+ subject(:query) { Sleek::Queries::Average.new(:default, :purchases, target_property: "total") }
5
+
6
+ describe "#perform" do
7
+ it "counts the events" do
8
+ events = stub('events')
9
+ events.should_receive(:avg).with("d.total").and_return(49_35)
10
+ expect(query.perform(events)).to eq 49_35
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Queries::Count do
4
+ subject(:query) { Sleek::Queries::Count.new(:default, :purchases) }
5
+
6
+ describe "#perform" do
7
+ it "counts the events" do
8
+ events = stub('events')
9
+ events.should_receive(:count).and_return(42)
10
+ expect(query.perform(events)).to eq 42
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Queries::CountUnique do
4
+ subject(:query) { Sleek::Queries::CountUnique.new(:default, :purchases, target_property: "customer.id") }
5
+
6
+ describe "#perform" do
7
+ it "counts the events" do
8
+ events = stub('events')
9
+ distinct_events = stub('distinct_events')
10
+ events.should_receive(:distinct).with("d.customer.id").and_return(distinct_events)
11
+ distinct_events.should_receive(:count).and_return(4)
12
+ expect(query.perform(events)).to eq 4
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Queries::Maximum do
4
+ subject(:query) { Sleek::Queries::Maximum.new(:default, :purchases, target_property: "total") }
5
+
6
+ describe "#perform" do
7
+ it "counts the events" do
8
+ events = stub('events')
9
+ events.should_receive(:max).with("d.total").and_return(199_99)
10
+ expect(query.perform(events)).to eq 199_99
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sleek::Queries::Minimum do
4
+ subject(:query) { Sleek::Queries::Minimum.new(:default, :purchases, target_property: "total") }
5
+
6
+ describe "#perform" do
7
+ it "counts the events" do
8
+ events = stub('events')
9
+ events.should_receive(:min).with("d.total").and_return(19_99)
10
+ expect(query.perform(events)).to eq 19_99
11
+ end
12
+ end
13
+ end