popular_stream 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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +86 -0
- data/Rakefile +11 -0
- data/lib/popular_stream/version.rb +3 -0
- data/lib/popular_stream.rb +56 -0
- data/popular_stream.gemspec +28 -0
- data/spec/lib/popular_stream_spec.rb +74 -0
- data/spec/spec_helper.rb +17 -0
- metadata +141 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6df6ce9f9726447977830802fe8a7e6f15cb8c00
|
4
|
+
data.tar.gz: 72253cc504abd85e4893ce25ac9d1310c3b5ddc3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3952dc948f1bef9002534db2d36891ad893e05ad5b790d49b6ab27b08e2cb90a217f8c09310c553f057617a99456f00dca7250d39946fd3c6de3ec410baf2b60
|
7
|
+
data.tar.gz: 5e7cdca746f55e2922ecdaec2ee25e06b1120562bf78222a86983c3c3f0e77221f9b2cce34226f051de3b42b952af032ddd35bba8c22458488032da9dadc01bc
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Nicolás Hock Isaza
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# Popular Stream
|
2
|
+
|
3
|
+
# THIS IS NOT READY YET!
|
4
|
+
|
5
|
+
Simple popular content tracker with a Redis backend.
|
6
|
+
|
7
|
+
Mostly taken from the "Popular Stream" code
|
8
|
+
[here](http://stdout.heyzap.com/2013/04/08/surfacing-interesting-content/) but
|
9
|
+
bundled up as a gem.
|
10
|
+
|
11
|
+
PopularStream tracks an "event" on a group of "fields" and returns the ones that
|
12
|
+
are currently popular.
|
13
|
+
|
14
|
+
For example, if you want to track the most popular tags right now you, the event
|
15
|
+
would be "tagging" and the field would be the tag name ("rubygems" for example).
|
16
|
+
|
17
|
+
The way this works is that "old" votes will count less than newer votes, that way
|
18
|
+
a tag that was used 20 times today will be more popular than a tag used 30 times
|
19
|
+
last week.
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
Add this line to your application's Gemfile:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
gem 'popular_stream'
|
27
|
+
```
|
28
|
+
|
29
|
+
And then execute:
|
30
|
+
|
31
|
+
$ bundle
|
32
|
+
|
33
|
+
Or install it yourself as:
|
34
|
+
|
35
|
+
$ gem install popular_stream
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
|
39
|
+
There are two main methods `vote` and `get`. `vote` adds an event to the list
|
40
|
+
and `get` gets the most popular fields on the distribution.
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
stream = PopularStream.new("popular_tags")
|
44
|
+
|
45
|
+
stream.vote(field: 'rubygems')
|
46
|
+
|
47
|
+
# You can also add an optional `weight` param.
|
48
|
+
stream.vote(field: 'ruby', weight: 2)
|
49
|
+
|
50
|
+
# And you can even specify when the event happened.
|
51
|
+
# Notice that time is a number, meaning seconds since Epoc.
|
52
|
+
time = Date.yesterday.to_time
|
53
|
+
stream.vote(field: 'rubygems', time: time.to_i)
|
54
|
+
|
55
|
+
stream.get # => ['rubygems', 'ruby']
|
56
|
+
|
57
|
+
# You can pass `limit` and `offset`
|
58
|
+
stream.get(limit: 1, offset: 1) # => ['ruby']
|
59
|
+
stream.get(offset: 10) # => []
|
60
|
+
```
|
61
|
+
|
62
|
+
## Seting up & Configuring Redis
|
63
|
+
|
64
|
+
Popular Stream uses Redis as the storage database. By default it will connect to
|
65
|
+
the redis client on `ENV['REDIS_URL']`. You can also specify what redis client to
|
66
|
+
use with `PopularStream.redis = Redis.new(host: 'example.com')`.
|
67
|
+
|
68
|
+
Notice that this is for *all* streams. This is because we don't want to create new
|
69
|
+
connections for every new stream instance we create, so the same client is used.
|
70
|
+
|
71
|
+
## TODO
|
72
|
+
|
73
|
+
* Sinatra application. I want to bundle an optional Sinatra application that
|
74
|
+
makes this gem super simple to setup as a service.
|
75
|
+
|
76
|
+
* Multiple storage databases. Right now everything's stored on Redis but it should
|
77
|
+
be simple to use more stuff. People should be able to create "adapters" and use
|
78
|
+
them as needed.
|
79
|
+
|
80
|
+
## Contributing
|
81
|
+
|
82
|
+
1. Fork it ( https://github.com/nhocki/popular_stream/fork )
|
83
|
+
2. Create your a new branch (`git checkout -b my-new-feature`)
|
84
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
85
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
86
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require "popular_stream/version"
|
2
|
+
|
3
|
+
# Most of this was taken from:
|
4
|
+
# http://stdout.heyzap.com/2013/04/08/surfacing-interesting-content/
|
5
|
+
class PopularStream
|
6
|
+
attr_reader :name, :epoch, :max_items
|
7
|
+
|
8
|
+
# 2.5 * half_life (in days) years from now. Make this far in the future!
|
9
|
+
DEFAULT_EPOC = Date.new(2017, 5, 29).to_time.to_i
|
10
|
+
DAY = 60 * 60 * 24
|
11
|
+
HALF_LIFE = 1 * DAY
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_accessor :redis
|
15
|
+
|
16
|
+
def redis
|
17
|
+
@redis ||= Redis.new(url: ENV['REDIS_URL'])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(name, **options)
|
22
|
+
@name = name
|
23
|
+
@epoch = options.fetch(:epoch) { DEFAULT_EPOC }
|
24
|
+
@max_items = options.fetch(:max_items) { 10_000 }
|
25
|
+
end
|
26
|
+
|
27
|
+
def vote(field:, time: Time.now.to_i, weight: 1)
|
28
|
+
time = time.to_i if time.respond_to?(:to_i)
|
29
|
+
|
30
|
+
delta = 2 ** ((time - epoch) / HALF_LIFE)
|
31
|
+
redis.zincrby(name, weight * delta.to_f, field)
|
32
|
+
trim
|
33
|
+
end
|
34
|
+
|
35
|
+
def get(limit: 20, offset: 0)
|
36
|
+
redis.zrevrange(name, offset, offset + limit - 1)
|
37
|
+
end
|
38
|
+
|
39
|
+
def clear!
|
40
|
+
redis.del(name)
|
41
|
+
end
|
42
|
+
|
43
|
+
def count
|
44
|
+
redis.zcard(name)
|
45
|
+
end
|
46
|
+
|
47
|
+
def trim
|
48
|
+
redis.zremrangebyrank(name, 0, -max_items)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def redis
|
54
|
+
self.class.redis
|
55
|
+
end
|
56
|
+
end
|
@@ -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 'popular_stream/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "popular_stream"
|
8
|
+
spec.version = PopularStream::VERSION
|
9
|
+
spec.authors = ["Nicolás Hock Isaza"]
|
10
|
+
spec.email = ["nhocki@gmail.com"]
|
11
|
+
spec.summary = %q{Ruby gem to track popular streams with a redis backend.}
|
12
|
+
spec.description = %q{Ruby gem to track popular streams with a redis backend.}
|
13
|
+
spec.homepage = "https://github.com/nhocki/popular_streams"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
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_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rspec", "~> 3.1.0"
|
24
|
+
spec.add_development_dependency "timecop", "~> 0.7.1"
|
25
|
+
spec.add_development_dependency "fakeredis", "~> 0.5.0"
|
26
|
+
|
27
|
+
spec.add_dependency "redis", "~> 3.1.0"
|
28
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe PopularStream do
|
4
|
+
let(:one_week) { 60 * 60 * 24 * 7 } # 1 week in seconds.
|
5
|
+
let(:stream) { PopularStream.new("popular_stream") }
|
6
|
+
|
7
|
+
describe ".redis" do
|
8
|
+
let(:redis) { double }
|
9
|
+
|
10
|
+
it "is possible to set a redis instace" do
|
11
|
+
expect { PopularStream.redis = redis }.not_to raise_error
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "#get" do
|
16
|
+
before do
|
17
|
+
stream.vote(field: '1', time: Time.now - one_week)
|
18
|
+
stream.vote(field: '2')
|
19
|
+
end
|
20
|
+
|
21
|
+
it "ranks older votes lower than newer votes" do
|
22
|
+
expect(stream.get).to eql(['2', '1'])
|
23
|
+
end
|
24
|
+
|
25
|
+
it "#get takes limit & offset arguments" do
|
26
|
+
expect(stream.get(limit: 1)).to eql(['2'])
|
27
|
+
expect(stream.get(offset: 1)).to eql(['1'])
|
28
|
+
end
|
29
|
+
|
30
|
+
it "paginates" do
|
31
|
+
stream.vote(field: '3', time: Time.now + one_week)
|
32
|
+
expect(stream.get(limit: 1, offset: 0)).to eql([ '3' ])
|
33
|
+
expect(stream.get(limit: 1, offset: 1)).to eql([ '2' ])
|
34
|
+
expect(stream.get(limit: 1, offset: 2)).to eql([ '1' ])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it "ranks most popular for same time as higher" do
|
39
|
+
Timecop.freeze do
|
40
|
+
stream.vote(field: '1')
|
41
|
+
stream.vote(field: '1')
|
42
|
+
stream.vote(field: '2')
|
43
|
+
expect(stream.get).to eql(['1', '2'])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it "accepts a weight for the vote of a specific field" do
|
48
|
+
Timecop.freeze do
|
49
|
+
stream.vote(field: '1')
|
50
|
+
stream.vote(field: '1')
|
51
|
+
stream.vote(field: '2', weight: 3)
|
52
|
+
expect(stream.get).to eql(['2', '1'])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it "keeps old votes with lower weight on values" do
|
57
|
+
Timecop.freeze do
|
58
|
+
stream.vote(field: '1', time: Time.now - 2 * one_week)
|
59
|
+
stream.vote(field: '2', time: Time.now - 3 * one_week)
|
60
|
+
|
61
|
+
stream.vote(field: '1')
|
62
|
+
stream.vote(field: '2')
|
63
|
+
end
|
64
|
+
expect(stream.get).to eql(['1', '2'])
|
65
|
+
end
|
66
|
+
|
67
|
+
it "#count gets the number of elements and #clear! removes them all" do
|
68
|
+
stream.vote(field: '1')
|
69
|
+
stream.vote(field: '1')
|
70
|
+
expect(stream.count).to eql(1)
|
71
|
+
stream.clear!
|
72
|
+
expect(stream.count).to eql(0)
|
73
|
+
end
|
74
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'popular_stream'
|
2
|
+
require 'fakeredis/rspec'
|
3
|
+
|
4
|
+
Bundler.require(:default, :test, :development)
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.expect_with :rspec do |expectations|
|
8
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
9
|
+
expectations.syntax = :expect
|
10
|
+
end
|
11
|
+
|
12
|
+
config.mock_with :rspec do |mocks|
|
13
|
+
mocks.verify_partial_doubles = true
|
14
|
+
end
|
15
|
+
|
16
|
+
config.disable_monkey_patching!
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: popular_stream
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nicolás Hock Isaza
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-11-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.1.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.1.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: timecop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.7.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.7.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: fakeredis
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.5.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.5.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: redis
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.1.0
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.1.0
|
97
|
+
description: Ruby gem to track popular streams with a redis backend.
|
98
|
+
email:
|
99
|
+
- nhocki@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rspec"
|
106
|
+
- Gemfile
|
107
|
+
- LICENSE.txt
|
108
|
+
- README.md
|
109
|
+
- Rakefile
|
110
|
+
- lib/popular_stream.rb
|
111
|
+
- lib/popular_stream/version.rb
|
112
|
+
- popular_stream.gemspec
|
113
|
+
- spec/lib/popular_stream_spec.rb
|
114
|
+
- spec/spec_helper.rb
|
115
|
+
homepage: https://github.com/nhocki/popular_streams
|
116
|
+
licenses:
|
117
|
+
- MIT
|
118
|
+
metadata: {}
|
119
|
+
post_install_message:
|
120
|
+
rdoc_options: []
|
121
|
+
require_paths:
|
122
|
+
- lib
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
requirements: []
|
134
|
+
rubyforge_project:
|
135
|
+
rubygems_version: 2.2.2
|
136
|
+
signing_key:
|
137
|
+
specification_version: 4
|
138
|
+
summary: Ruby gem to track popular streams with a redis backend.
|
139
|
+
test_files:
|
140
|
+
- spec/lib/popular_stream_spec.rb
|
141
|
+
- spec/spec_helper.rb
|