Merge branch 'release/v0.0.1'

This commit is contained in:
2010-11-22 12:31:42 +00:00
21 changed files with 308 additions and 76 deletions

1
.gitignore vendored
View File

@@ -19,6 +19,7 @@ rdoc
pkg
## PROJECT::SPECIFIC
.bundle/*
.yardoc/*
spec/db/*
doc

12
Gemfile Normal file
View File

@@ -0,0 +1,12 @@
source 'http://rubygems.org/'
gem 'activesupport', '>= 2.3.0'
gem 'json', '>= 1.0.0'
gem 'redis', '>= 2.0.0'
gem 'time_ext', '>= 0.2.6'
group :development do
gem 'rspec', '>= 2.0.1'
gem 'yard', '>= 0.6.1'
gem 'i18n'
end

31
Gemfile.lock Normal file
View File

@@ -0,0 +1,31 @@
GEM
remote: http://rubygems.org/
specs:
activesupport (3.0.3)
diff-lcs (1.1.2)
i18n (0.4.2)
json (1.4.6)
redis (2.1.1)
rspec (2.1.0)
rspec-core (~> 2.1.0)
rspec-expectations (~> 2.1.0)
rspec-mocks (~> 2.1.0)
rspec-core (2.1.0)
rspec-expectations (2.1.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.1.0)
time_ext (0.2.6)
activesupport (>= 2.3.0)
yard (0.6.3)
PLATFORMS
ruby
DEPENDENCIES
activesupport (>= 2.3.0)
i18n
json (>= 1.0.0)
redis (>= 2.0.0)
rspec (>= 2.0.1)
time_ext (>= 0.2.6)
yard (>= 0.6.1)

View File

@@ -1,6 +1,12 @@
# redistat
# Redistat
Description goes here.
A Redis-backed statistics storage and querying library written in Ruby.
## Early Beta
Currently this is an early beta release. Readme and documentation is forthcoming.
For now, please check `spec/model_spec.rb` and `spec/model_helper.rb` to get started with how to use Redistat.
## Note on Patches/Pull Requests

View File

@@ -4,19 +4,18 @@ require 'rake'
begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "redistat"
gem.summary = %Q{TODO: one-line summary of your gem}
gem.description = %Q{TODO: longer description of your gem}
gem.email = "contact@jimeh.me"
gem.homepage = "http://github.com/jimeh/redistat"
gem.authors = ["Jim Myhrberg"]
gem.add_dependency "activesupport", ">= 2.3.0"
gem.add_dependency "json", ">= 1.0.0"
gem.add_dependency "redis", ">= 2.0.0"
gem.add_dependency "time_ext", ">= 0.2.6"
gem.add_development_dependency "rspec", ">= 1.2.9"
gem.add_development_dependency "yard", ">= 0"
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
gem.name = 'redistat'
gem.summary = 'TODO: one-line summary of your gem'
gem.description = 'TODO: longer description of your gem'
gem.email = 'contact@jimeh.me'
gem.homepage = 'http://github.com/jimeh/redistat'
gem.authors = ['Jim Myhrberg']
gem.add_dependency 'activesupport', '>= 2.3.0'
gem.add_dependency 'json', '>= 1.0.0'
gem.add_dependency 'redis', '>= 2.0.0'
gem.add_dependency 'time_ext', '>= 0.2.6'
gem.add_development_dependency 'rspec', '>= 2.0.1'
gem.add_development_dependency 'yard', '>= 0.6.1'
end
Jeweler::GemcutterTasks.new
rescue LoadError
@@ -24,14 +23,13 @@ rescue LoadError
end
require 'spec/rake/spectask'
Spec::Rake::SpecTask.new(:spec) do |spec|
spec.libs << 'lib' << 'spec'
spec.spec_files = FileList['spec/**/*_spec.rb']
# Rspec
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec) do |spec|
spec.pattern = 'spec/**/*_spec.rb'
end
Spec::Rake::SpecTask.new(:rcov) do |spec|
spec.libs << 'lib' << 'spec'
RSpec::Core::RakeTask.new(:rcov) do |spec|
spec.pattern = 'spec/**/*_spec.rb'
spec.rcov = true
end

1
VERSION Normal file
View File

@@ -0,0 +1 @@
0.0.1

View File

@@ -1,14 +1,15 @@
require 'rubygems'
require 'active_support'
require 'active_support/time' if !Time.respond_to?(:days_in_month) # Active Support 2.x and 3.x
require 'active_support/hash_with_indifferent_access' if !Hash.respond_to?(:with_indifferent_access) # Active Support 2.x and 3.x
require 'redis'
require 'date'
require 'time'
require 'time/ext'
require 'time_ext'
require 'json'
require 'digest/sha1'
require 'redistat/collection'
require 'redistat/database'
require 'redistat/date'
require 'redistat/event'

View File

@@ -1,7 +1,16 @@
module Redistat
class Collection < Array
class Collection < ::Array
attr_accessor :from
attr_accessor :till
attr_accessor :depth
attr_accessor :total
def initialize(options = {})
@from = options[:from] ||= nil
@till = options[:till] ||= nil
@depth = options[:depth] ||= nil
end
end
end

View File

@@ -78,6 +78,7 @@ module Redistat
end
def from_string(input)
input += "19700101000000"[input.size..-1] if input =~ /^\d\d\d[\d]+$/i
from_time(::Time.parse(input))
end

View File

@@ -9,7 +9,7 @@ module Redistat
attr_accessor :meta
attr_accessor :options
def initialize(scope, label = nil, date = nil, stats = {}, meta = {}, options = {}, is_new = true)
def initialize(scope, label = nil, date = nil, stats = {}, options = {}, meta = {}, is_new = true)
@options = default_options.merge(options)
@key = Key.new(scope, label, date, @options)
@stats = stats ||= {}

View File

@@ -26,27 +26,47 @@ module Redistat
def find_by_interval(options = {})
@options.merge!(options)
raise InvalidOptions.new if !valid_options?
date_sets = Finder::DateSet.new(@options[:from], @options[:till], @options[:depth], @options[:interval])
key = build_key
col = Collection.new(@options)
col.total = Result.new(@options)
build_date_sets.each do |set|
set[:add].each do |date|
result = Result.new
result.date = Date.new(date).to_time
db.hgetall("#{key.prefix}#{date}").each do |k, v|
result[k] = v
col.total.set_or_incr(k, v.to_i)
end
col << result
end
end
col
end
def find_by_magic(options = {})
@options.merge!(options)
raise InvalidOptions.new if !valid_options?
date_sets = Finder::DateSet.new(@options[:from], @options[:till], @options[:depth], @options[:interval])
key = Key.new(@options[:scope], @options[:label])
total_sum = Result.new
date_sets.each do |set|
col = Collection.new(@options)
col.total = Result.new(@options)
col << col.total
build_date_sets.each do |set|
sum = Result.new
sum = summarize_add_keys(set[:add], key, sum)
sum = summarize_rem_keys(set[:rem], key, sum)
sum.each do |k, v|
total_sum.set_or_incr(k, v.to_i)
col.total.set_or_incr(k, v.to_i)
end
end
total_sum.date = Date.new(@options[:from], @options[:depth])
total_sum.till = Date.new(@options[:till], @options[:depth])
total_sum
col
end
def build_date_sets
Finder::DateSet.new(@options[:from], @options[:till], @options[:depth], @options[:interval])
end
def build_key
Key.new(@options[:scope], @options[:label])
end
def summarize_add_keys(sets, key, sum)

View File

@@ -1,13 +1,50 @@
module Redistat
class Model
module Model
def self.create(*args)
Event.new(self.name, self.options, *args)
def self.included(base)
base.extend(self)
end
def self.options
def store(label, stats = {}, date = nil, meta = {}, opts = {})
Event.new(name, label, date, stats, options.merge(opts), meta).save
end
alias :event :store
def fetch(label, from, till, opts = {})
Finder.find({
:scope => name,
:label => label,
:from => from,
:till => till
}.merge(options.merge(opts)))
end
alias :find :fetch
def depth(depth = nil)
if !depth.nil?
options[:depth] = depth
else
options[:depth] || nil
end
end
def store_event(boolean = nil)
if !boolean.nil?
options[:store_event] = boolean
else
options[:store_event] || nil
end
end
def options
@options ||= {}
end
private
def name
@name ||= self.to_s
end
end
end

View File

@@ -1,9 +1,18 @@
module Redistat
class Result < ::Hash
class Result < ::ActiveSupport::HashWithIndifferentAccess
attr_accessor :date
attr_accessor :from
attr_accessor :till
alias :date :from
alias :date= :from=
def initialize(options = {})
@from = options[:from] ||= nil
@till = options[:till] ||= nil
end
def set_or_incr(key, value)
self[key] = 0 if !self.has_key?(key)
self[key] += value

13
spec/collection_spec.rb Normal file
View File

@@ -0,0 +1,13 @@
require "spec_helper"
describe Redistat::Collection do
it "should should initialize properly" do
options = {:from => "from", :till => "till", :depth => "depth"}
result = Redistat::Collection.new(options)
result.from.should == options[:from]
result.till.should == options[:till]
result.depth.should == options[:depth]
end
end

View File

@@ -31,6 +31,14 @@ describe Redistat::Date do
[:year, :month, :day, :hour, :min, :sec].each { |k| rdate.send(k).should == now.send(k) }
end
it "should initialize from Redistat date String" do
now = Time.now
rdate = Redistat::Date.new(now.to_s)
[:year, :month, :day, :hour, :min, :sec].each { |k|
rdate.to_s(k).should == Redistat::Date.new(rdate.to_s(k)).to_s(k)
}
end
it "should convert to Time object" do
now = Time.now
rdate = Redistat::Date.new(now)

View File

@@ -12,7 +12,7 @@ describe Redistat::Event do
@meta = {:user_id => 239}
@options = {:depth => :hour}
@date = Time.now
@event = Redistat::Event.new(@scope, @label, @date, @stats, @meta, @options)
@event = Redistat::Event.new(@scope, @label, @date, @stats, @options, @meta)
end
it "should initialize properly" do
@@ -43,7 +43,7 @@ describe Redistat::Event do
end
it "should increment next_id" do
event = Redistat::Event.new("VisitorCount", @label, @date, @stats, @meta, @options)
event = Redistat::Event.new("VisitorCount", @label, @date, @stats, @options, @meta)
@event.next_id.should == 1
event.next_id.should == 1
@event.next_id.should == 2
@@ -51,7 +51,7 @@ describe Redistat::Event do
end
it "should store event properly" do
@event = Redistat::Event.new(@scope, @label, @date, @stats, @meta, @options.merge({:store_event => true}))
@event = Redistat::Event.new(@scope, @label, @date, @stats, @options.merge({:store_event => true}), @meta)
@event.new?.should be_true
@event.save
@event.new?.should be_false
@@ -61,7 +61,7 @@ describe Redistat::Event do
end
it "should find event by id" do
@event = Redistat::Event.new(@scope, @label, @date, @stats, @meta, @options.merge({:store_event => true})).save
@event = Redistat::Event.new(@scope, @label, @date, @stats, @options.merge({:store_event => true}), @meta).save
fetched = Redistat::Event.find(@scope, @event.id)
@event.scope.should == fetched.scope
@event.label.should == fetched.label
@@ -70,7 +70,7 @@ describe Redistat::Event do
it "should store summarized statistics" do
2.times do |i|
@event = Redistat::Event.new(@scope, @label, @date, @stats, @meta, @options).save
@event = Redistat::Event.new(@scope, @label, @date, @stats, @options, @meta).save
Redistat::Date::DEPTHS.each do |depth|
summary = db.hgetall @event.key.to_s(depth)
summary.should have_at_least(1).items

View File

@@ -44,52 +44,60 @@ describe Redistat::Finder do
end
it "should fetch stats properly" do
create_example_stats
first_stat, last_stat = create_example_stats
three_hours_ago = 3.hours.ago
two_hours_from_now = 2.hours.from_now
depth = :hour
stats = Redistat::Finder.find({:from => first_stat, :till => last_stat, :scope => @scope, :label => @label, :depth => :hour})
stats.from.should == first_stat
stats.till.should == last_stat
stats.depth.should == :hour
stats = Redistat::Finder.find({:from => three_hours_ago, :till => two_hours_from_now, :scope => @scope, :label => @label, :depth => depth})
stats.should == { "views" => 9, "visitors" => 6 }
stats.date.to_s.should == three_hours_ago.to_rs.to_s(depth)
stats.till.to_s.should == two_hours_from_now.to_rs.to_s(depth)
stats.total.should == { "views" => 12, "visitors" => 8 }
stats.total.from.should == first_stat
stats.total.till.should == last_stat
stats.first.should == stats.total
end
it "should fetch data per unit when interval option is specified" do
first_stat, last_stat = create_example_stats
stats = Redistat::Finder.find(:from => first_stat, :till => last_stat, :scope => @scope, :label => @label, :depth => :hour, :interval => :hour)
stats.from.should == first_stat
stats.till.should == last_stat
stats.total.should == { "views" => 12, "visitors" => 8 }
stats[0].should == {}
stats[0].date.should == Time.parse("2010-05-14 12:00")
stats[1].should == {"visitors"=>"4", "views"=>"6"}
stats[1].date.should == Time.parse("2010-05-14 13:00")
stats[2].should == {"visitors"=>"2", "views"=>"3"}
stats[2].date.should == Time.parse("2010-05-14 14:00")
stats[3].should == {"visitors"=>"2", "views"=>"3"}
stats[3].date.should == Time.parse("2010-05-14 15:00")
stats[4].should == {}
stats[4].date.should == Time.parse("2010-05-14 16:00")
end
it "should return empty hash when attempting to fetch non-existent results" do
stats = Redistat::Finder.find({:from => 3.hours.ago, :till => 2.hours.from_now, :scope => @scope, :label => @label, :depth => :hour})
stats.should == {}
end
it "should fetch data per unit when interval option is specified" do
create_example_stats
three_hours_ago = 3.hours.ago
two_hours_from_now = 2.hours.from_now
depth = :hour
stats = Redistat::Finder.find(:from => 3.hours.ago, :till => 2.hours.ago, :scope => @scope, :label => @label, :depth => :hour, :interval => :hour)
puts "\n>>>>>> stats: " + stats.inspect + "\n"
stats.total.should == {}
end
it "should throw error on invalid options" do
begin
stats = Redistat::Finder.find(:from => 3.hours.ago)
rescue ArgumentError => e
e.class.to_s.should == "Redistat::InvalidOptions"
end
lambda { Redistat::Finder.find(:from => 3.hours.ago) }.should raise_error(Redistat::InvalidOptions)
end
# helper methods
def create_example_stats
key = Redistat::Key.new(@scope, @label, 2.hours.ago)
key = Redistat::Key.new(@scope, @label, (first = Time.parse("2010-05-14 13:43")))
Redistat::Summary.update(key, @stats, :hour)
key = Redistat::Key.new(@scope, @label, 1.hours.ago)
key = Redistat::Key.new(@scope, @label, Time.parse("2010-05-14 13:53"))
Redistat::Summary.update(key, @stats, :hour)
key = Redistat::Key.new(@scope, @label, 24.minutes.ago)
key = Redistat::Key.new(@scope, @label, Time.parse("2010-05-14 14:32"))
Redistat::Summary.update(key, @stats, :hour)
key = Redistat::Key.new(@scope, @label, (last = Time.parse("2010-05-14 15:02")))
Redistat::Summary.update(key, @stats, :hour)
[first - 1.hour, last + 1.hour]
end
end

15
spec/model_helper.rb Normal file
View File

@@ -0,0 +1,15 @@
require "redistat"
class ModelHelper
include Redistat::Model
end
class ModelHelper2
include Redistat::Model
depth :day
store_event true
end

60
spec/model_spec.rb Normal file
View File

@@ -0,0 +1,60 @@
require "spec_helper"
require "model_helper"
describe Redistat::Model do
include Redistat::Database
before(:each) do
db.flushdb
end
it "should should name itself correctly" do
ModelHelper.send(:name).should == "ModelHelper"
ModelHelper2.send(:name).should == "ModelHelper2"
end
it "should listen to model-defined options" do
ModelHelper2.depth.should == :day
ModelHelper2.store_event.should == true
ModelHelper.depth.should == nil
ModelHelper.store_event.should == nil
ModelHelper.depth(:hour)
ModelHelper.depth.should == :hour
ModelHelper.store_event(true)
ModelHelper.store_event.should == true
ModelHelper.options[:depth] = nil
ModelHelper.options[:store_event] = nil
ModelHelper.depth.should == nil
ModelHelper.store_event.should == nil
end
it "should store and fetch stats" do
ModelHelper.store("sheep.black", {:count => 6, :weight => 461}, 4.hours.ago)
ModelHelper.store("sheep.black", {:count => 2, :weight => 156})
stats = ModelHelper.fetch("sheep.black", 2.hours.ago, 1.hour.from_now)
stats.total["count"].should == 2
stats.total["weight"].should == 156
stats.first.should == stats.total
stats = ModelHelper.fetch("sheep.black", 5.hours.ago, 1.hour.from_now)
stats.total[:count].should == 8
stats.total[:weight].should == 617
stats.first.should == stats.total
ModelHelper.store("sheep.white", {:count => 5, :weight => 393}, 4.hours.ago)
ModelHelper.store("sheep.white", {:count => 4, :weight => 316})
stats = ModelHelper.fetch("sheep.white", 2.hours.ago, 1.hour.from_now)
stats.total[:count].should == 4
stats.total[:weight].should == 316
stats.first.should == stats.total
stats = ModelHelper.fetch("sheep.white", 5.hours.ago, 1.hour.from_now)
stats.total[:count].should == 9
stats.total[:weight].should == 709
stats.first.should == stats.total
end
end

View File

@@ -2,9 +2,11 @@ require "spec_helper"
describe Redistat::Result do
before(:each) do
@name = "PageViews"
@scope = Redistat::Scope.new(@name)
it "should should initialize properly" do
options = {:from => "from", :till => "till"}
result = Redistat::Result.new(options)
result.from.should == "from"
result.till.should == "till"
end
it "should have set_or_incr method" do