red_trend 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.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .rspec
19
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in red_trend.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2012 Brendon Murphy
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.
23
+
data/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # RedTrend
2
+
3
+ Store your trend data in redis.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'red_trend'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install red_trend
18
+
19
+ ## Usage
20
+
21
+ ```ruby
22
+ require 'red_trend'
23
+
24
+ red_trend = RedTrend.new(:prefix => "project:9")
25
+ red_trend.record("post_views", 42)
26
+ red_trend.record("post_views", 53)
27
+ red_trend.record("post_views", 53)
28
+ red_trend.top("post_views") # => ["53", "42"]
29
+ ```
30
+
31
+ TODO add configuration examples here
32
+
33
+ ## How it works
34
+
35
+ TODO
36
+
37
+ ## Todos
38
+
39
+ Rip ActiveSupport entirely if possible; otherwise narrow the inclusion.
40
+
41
+ ## Contributing
42
+
43
+ 1. Fork it
44
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
45
+ 3. Create your specs and feature code
46
+ 4. Commit your changes (`git commit -am 'Added some feature'`)
47
+ 5. Push to the branch (`git push origin my-new-feature`)
48
+ 6. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
@@ -0,0 +1,3 @@
1
+ class RedTrend
2
+ VERSION = "0.0.1"
3
+ end
data/lib/red_trend.rb ADDED
@@ -0,0 +1,132 @@
1
+ require 'redis'
2
+ require "red_trend/version"
3
+ require 'active_support/all'
4
+
5
+ class RedTrend
6
+ CYCLE_UNITS = {
7
+ :minute => 60,
8
+ :hour => 3600,
9
+ :day => 86400
10
+ }.freeze
11
+
12
+ class << self
13
+ attr_writer :redis
14
+
15
+ def redis
16
+ @redis ||= Redis.new
17
+ end
18
+ end
19
+
20
+ attr_reader :options
21
+
22
+ def initialize(options = {})
23
+ @options = options
24
+ unless CYCLE_UNITS.include?(cycle_unit)
25
+ raise ArgumentError, "cycle unit must be in #{CYCLE_UNITS.keys}"
26
+ end
27
+ end
28
+
29
+ # How many cycles we'll persist data for
30
+ def cycles_count
31
+ @options.fetch(:cycles_count, 3)
32
+ end
33
+
34
+ def cycle_length
35
+ CYCLE_UNITS[cycle_unit]
36
+ end
37
+
38
+ # The length of 1 cycle
39
+ def cycle_unit
40
+ @options.fetch(:cycle_unit, :hour)
41
+ end
42
+
43
+ # The interval length of each cycle, probably
44
+ # days for production
45
+ def cycle_interval
46
+ cycle_length * cycles_count
47
+ end
48
+
49
+ def current_cycle
50
+ method = case cycle_unit
51
+ when :minute then :min
52
+ when :hour then :hour
53
+ when :day then :yday
54
+ end
55
+
56
+ time = @time || Time.now
57
+ (time.send(method) % cycles_count) + 1
58
+ end
59
+
60
+ def cycle_positions
61
+ 1.upto(cycles_count - 1).inject([current_cycle]) do |n_cycles, offset|
62
+ n = current_cycle - offset
63
+ n = n + cycles_count if n < 1
64
+ n_cycles << n
65
+ end
66
+ end
67
+
68
+ # Increment the leaderboard score for the object on
69
+ # the sorted set for the current cycle. Make the key
70
+ # volitile so that it doesn't persist past the number
71
+ # of cycles. Store the union score set after each
72
+ # new score
73
+ def record(key, member)
74
+ @time = Time.now
75
+
76
+ n_key = make_key(key, current_cycle)
77
+
78
+ zcard = redis.zcard(n_key)
79
+
80
+ redis.multi do
81
+ redis.zincrby n_key, 1, member
82
+ # FIXME I'm not certain the second redis.ttl is working in the multi
83
+ if zcard < 1 || redis.ttl(n_key) == -1
84
+ redis.expire n_key, calculate_expire_seconds
85
+ end
86
+ unionize_sets(key)
87
+ end
88
+
89
+ @time = nil
90
+
91
+ true
92
+ end
93
+
94
+ # Returns top scoring ids for the current interval
95
+ def top(key, limit = 10)
96
+ redis.zrevrange make_key(key), 0, limit - 1
97
+ end
98
+
99
+ def weight_offset
100
+ ("%0.1f" % (1 / cycles_count.to_f)).to_f
101
+ end
102
+
103
+ def cycle_weights
104
+ cycles_count.times.inject([]) do |weights, n|
105
+ weights << 1 - n * weight_offset
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ def make_key(*args)
112
+ key_parts = [options[:prefix]]
113
+ key_parts.concat Array(args)
114
+ key_parts.compact.join(":")
115
+ end
116
+
117
+ def redis
118
+ self.class.redis
119
+ end
120
+
121
+ # The time in seconds which the current cycle key should expire in
122
+ def calculate_expire_seconds
123
+ ((@time.to_f / cycle_length).floor * cycle_length + cycle_interval) - @time.to_i
124
+ end
125
+
126
+ # Take the scores from all current cycle sets and store
127
+ # them as a union
128
+ def unionize_sets(key)
129
+ keys = cycle_positions.collect { |n| make_key(key, n) }
130
+ redis.zunionstore(make_key(key), keys, :weights => cycle_weights)
131
+ end
132
+ end
data/red_trend.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/red_trend/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Brendon Murphy"]
6
+ gem.email = ["xternal1+github@gmail.com"]
7
+ gem.description = %q{Store your trend data in redis}
8
+ gem.summary = gem.description
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "red_trend"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = RedTrend::VERSION
17
+
18
+ gem.add_dependency "redis"
19
+ gem.add_dependency "active_support"
20
+ gem.add_dependency "i18n"
21
+
22
+ gem.add_development_dependency "rake"
23
+ gem.add_development_dependency "rspec"
24
+ gem.add_development_dependency "integration_test_redis"
25
+ gem.add_development_dependency "timecop"
26
+ end
@@ -0,0 +1,275 @@
1
+ require 'spec_helper'
2
+
3
+ describe RedTrend, "redis connection config at the class level" do
4
+ let(:redis) { stub }
5
+
6
+ it "can instantiate a default localhost redis instance" do
7
+ RedTrend.redis = nil
8
+ Redis.should_receive(:new).with().and_return(redis)
9
+ RedTrend.redis.should == redis
10
+ end
11
+
12
+ it "can be overrided with a redis connection" do
13
+ RedTrend.redis = redis
14
+ RedTrend.redis.should == redis
15
+ end
16
+ end
17
+
18
+ describe RedTrend, "defaults" do
19
+ subject { RedTrend.new }
20
+
21
+ it "cycles_count to 3" do
22
+ subject.cycles_count.should == 3
23
+ end
24
+
25
+ it "cycle_unit to :hour" do
26
+ subject.cycle_unit.should == :hour
27
+ end
28
+
29
+ it "cycle_length to 3 hours" do
30
+ subject.cycle_interval.to_i.should == 3 * 60 * 60
31
+ end
32
+ end
33
+
34
+ describe RedTrend, "overriding defaults" do
35
+ subject { RedTrend.new(:cycles_count => 2, :cycle_unit => :minute) }
36
+
37
+ it "allows setting the cycles_count" do
38
+ subject.cycles_count.should == 2
39
+ end
40
+
41
+ it "allows setting the cycle_length" do
42
+ subject.cycle_length.should == 60
43
+ end
44
+
45
+ it "accepts the cycle_length of :minute, :hour, or :day" do
46
+ [:minute, :hour, :day].each do |unit|
47
+ lambda {
48
+ RedTrend.new(:cycle_length => unit)
49
+ }.should_not raise_error
50
+ end
51
+ end
52
+
53
+ it "raises an argument error for an unknown cycle length" do
54
+ lambda {
55
+ RedTrend.new(:cycle_unit => :foobar)
56
+ }.should raise_error(ArgumentError)
57
+ end
58
+ end
59
+
60
+ describe RedTrend, "cycle interval" do
61
+ context "for a cycle_count of 2 and cycle_unit of :minute" do
62
+ it "is 120" do
63
+ subject = RedTrend.new(:cycles_count => 2, :cycle_unit => :minute)
64
+ subject.cycle_interval.should == 120
65
+ end
66
+ end
67
+
68
+ context "for a cycle_count of 4 and cycle_length of 1 day" do
69
+ it "is 345600" do
70
+ subject = RedTrend.new(:cycles_count => 4, :cycle_unit => :day)
71
+ subject.cycle_interval.should == 345600
72
+ end
73
+ end
74
+ end
75
+
76
+ describe RedTrend, "getting the current cycle" do
77
+ before do
78
+ Timecop.freeze("2012-07-30 18:00:00 -0700")
79
+ end
80
+
81
+ context "for a default RedTrend" do
82
+ subject { RedTrend.new }
83
+
84
+ it "cycles from 1 through 3 on intervals based on the hour" do
85
+ subject.current_cycle.should == 1
86
+ Timecop.travel(3600)
87
+ subject.current_cycle.should == 2
88
+ Timecop.travel(3600)
89
+ subject.current_cycle.should == 3
90
+ Timecop.travel(3600)
91
+ subject.current_cycle.should == 1
92
+ end
93
+
94
+ it "cycles from 2, 3, 1 on intervals based on the hour" do
95
+ Timecop.freeze("2012-07-30 19:00:00 -0700")
96
+ subject.current_cycle.should == 2
97
+ Timecop.travel(3600)
98
+ subject.current_cycle.should == 3
99
+ Timecop.travel(3600)
100
+ subject.current_cycle.should == 1
101
+ Timecop.travel(3600)
102
+ subject.current_cycle.should == 2
103
+ end
104
+
105
+ it "behaves when the time does not fall on the boundary" do
106
+ subject.current_cycle.should == 1
107
+ Timecop.travel(1800)
108
+ subject.current_cycle.should == 1
109
+ Timecop.travel(1800)
110
+ subject.current_cycle.should == 2
111
+ end
112
+ end
113
+
114
+ context "for a RedTrend of 4 cycles of 60 seconds" do
115
+ subject { RedTrend.new(:cycles_count => 4, :cycle_unit => :minute) }
116
+
117
+ it "cycles from 1 through 4 on intervals based on the minute" do
118
+ subject.current_cycle.should == 1
119
+ Timecop.travel(60)
120
+ subject.current_cycle.should == 2
121
+ Timecop.travel(60)
122
+ subject.current_cycle.should == 3
123
+ Timecop.travel(60)
124
+ subject.current_cycle.should == 4
125
+ Timecop.travel(60)
126
+ subject.current_cycle.should == 1
127
+ end
128
+ end
129
+
130
+ context "for a RedTrend of 2 cycles of 1 day" do
131
+ subject { RedTrend.new(:cycles_count => 2, :cycle_unit => :day) }
132
+
133
+ it "cycles from 1 through 2 on intervals based on the day" do
134
+ subject.current_cycle.should == 1
135
+ Timecop.travel(86400)
136
+ subject.current_cycle.should == 2
137
+ Timecop.travel(86400)
138
+ subject.current_cycle.should == 1
139
+ end
140
+ end
141
+
142
+ context "for a RedTrend of 3 cycles of 1 day starting with Dec 31" do
143
+ subject { RedTrend.new(:cycles_count => 3, :cycle_unit => :day) }
144
+
145
+ it "cycles from 1 through 2 on intervals based on the day" do
146
+ Timecop.freeze("2012-12-31 18:00:00 -0700")
147
+ subject.current_cycle.should == 1
148
+ Timecop.travel(86400)
149
+ subject.current_cycle.should == 2
150
+ Timecop.travel(86400)
151
+ subject.current_cycle.should == 3
152
+ Timecop.travel(86400)
153
+ subject.current_cycle.should == 1
154
+ end
155
+ end
156
+ end
157
+
158
+ describe RedTrend, "getting an array of cycle numbers" do
159
+ subject { RedTrend.new :cycles_count => 4 }
160
+
161
+ before do
162
+ Timecop.freeze("2012-07-30 18:00:00 -0700")
163
+ end
164
+
165
+ it "starts with the current cycle to the last cycle and wraps back around" do
166
+ subject.cycle_positions.should == [3, 2, 1, 4]
167
+ end
168
+ end
169
+
170
+ describe RedTrend, "recording" do
171
+ subject { RedTrend.new }
172
+ let(:redis) { IntegrationTestRedis.client }
173
+
174
+ before do
175
+ Timecop.freeze("2012-07-30 18:00:00 -0700")
176
+ end
177
+
178
+ it "increments the score for the member at the current zset key" do
179
+ subject.record("foobar", 42)
180
+ redis.zscore("foobar:1", "42").should == 1
181
+ subject.record("foobar", 42)
182
+ redis.zscore("foobar:1", "42").should == 2
183
+ end
184
+
185
+ it "prefixes the key if the prefix option was set" do
186
+ subject.options[:prefix] = "fizz:buzz"
187
+ subject.record("foobar", 42)
188
+ redis.zscore("fizz:buzz:foobar:1", "42").should == 1
189
+ end
190
+
191
+ it "sets the zset to expire when the cycle wraps back around to it" do
192
+ subject.record("foobar", 42)
193
+ redis.ttl("foobar:1").should == 10800
194
+
195
+ Timecop.travel(1800)
196
+ subject.record("foobar", 42)
197
+ redis.ttl("foobar:1").should == 10800
198
+
199
+ Timecop.travel(1800)
200
+ subject.record("foobar", 42)
201
+ redis.ttl("foobar:2").should == 10800
202
+ end
203
+
204
+ it "creates a union set from the current zsets" do
205
+ redis.exists("foobar").should be_false
206
+ subject.record("foobar", 42)
207
+ redis.exists("foobar").should be_true
208
+ end
209
+ end
210
+
211
+ describe RedTrend, "storing a union off all cycles" do
212
+ subject { RedTrend.new }
213
+ let(:redis) { IntegrationTestRedis.client }
214
+
215
+ before do
216
+ Timecop.freeze("2012-07-30 18:00:00 -0700")
217
+
218
+ # Cycle 1
219
+ subject.record("foobar", 1)
220
+ Timecop.travel(1800)
221
+ subject.record("foobar", 2)
222
+
223
+ # Cycle 2
224
+ Timecop.travel(1800)
225
+ subject.record("foobar", 3)
226
+
227
+ # Cycle 3
228
+ Timecop.travel(3600)
229
+ subject.record("foobar", 4)
230
+
231
+ # Cycle 1
232
+ Timecop.travel(3600)
233
+ subject.record("foobar", 5)
234
+
235
+ # Cycle 2
236
+ Timecop.travel(3600)
237
+ subject.record("foobar", 1)
238
+ end
239
+
240
+ it "unionizes the data from all the available cycles" do
241
+ redis.zrange("foobar", 0, -1).should =~ %w[1 2 3 4 5]
242
+ end
243
+
244
+ it "weights the unionization to prefer newer data" do
245
+ redis.zrevrangebyscore("foobar", '+inf', '-inf').should == %w[1 3 5 2 4]
246
+ end
247
+
248
+ it "builds a weight offset by dividing 1 by the cycles count and rounding to 1/10 precision" do
249
+ subject.weight_offset.should == 0.3
250
+ subject.options[:cycles_count] = 4
251
+ subject.weight_offset.should == 0.2
252
+ end
253
+
254
+ it "uses the weight offset to build decreasing weights by subtracting multiples of the offset" do
255
+ redis.zrevrangebyscore("foobar", '+inf', '-inf', :with_scores => true).map(&:last).should == [1.7, 1, 0.7, 0.7, 0.4]
256
+ end
257
+ end
258
+
259
+ describe RedTrend, "getting the current top ids" do
260
+ subject { RedTrend.new }
261
+
262
+ before do
263
+ 1.upto(11) do |n|
264
+ n.times { subject.record("foobar", n) }
265
+ end
266
+ end
267
+
268
+ it "returns the top 10 zrevrange of the union set" do
269
+ subject.top("foobar").should == %w[11 10 9 8 7 6 5 4 3 2]
270
+ end
271
+
272
+ it "can be given an optional limit" do
273
+ subject.top("foobar", 3).should == %w[11 10 9]
274
+ end
275
+ end
@@ -0,0 +1,16 @@
1
+ require 'rspec'
2
+ require 'timecop'
3
+
4
+ require File.expand_path("../../lib/red_trend", __FILE__)
5
+
6
+ require "integration_test_redis"
7
+ IntegrationTestRedis.start
8
+
9
+ RSpec.configure do |config|
10
+ config.mock_with :rspec
11
+
12
+ config.before(:each) do
13
+ RedTrend.redis = IntegrationTestRedis.client
14
+ IntegrationTestRedis.client.flushdb
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: red_trend
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Brendon Murphy
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: &2164433440 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2164433440
25
+ - !ruby/object:Gem::Dependency
26
+ name: active_support
27
+ requirement: &2164433000 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *2164433000
36
+ - !ruby/object:Gem::Dependency
37
+ name: i18n
38
+ requirement: &2164432580 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *2164432580
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: &2164657580 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2164657580
58
+ - !ruby/object:Gem::Dependency
59
+ name: rspec
60
+ requirement: &2164657120 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *2164657120
69
+ - !ruby/object:Gem::Dependency
70
+ name: integration_test_redis
71
+ requirement: &2164656140 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *2164656140
80
+ - !ruby/object:Gem::Dependency
81
+ name: timecop
82
+ requirement: &2164655580 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *2164655580
91
+ description: Store your trend data in redis
92
+ email:
93
+ - xternal1+github@gmail.com
94
+ executables: []
95
+ extensions: []
96
+ extra_rdoc_files: []
97
+ files:
98
+ - .gitignore
99
+ - Gemfile
100
+ - LICENSE
101
+ - README.md
102
+ - Rakefile
103
+ - lib/red_trend.rb
104
+ - lib/red_trend/version.rb
105
+ - red_trend.gemspec
106
+ - spec/red_trend_spec.rb
107
+ - spec/spec_helper.rb
108
+ homepage: ''
109
+ licenses: []
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ segments:
121
+ - 0
122
+ hash: 3032661298513805062
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ none: false
125
+ requirements:
126
+ - - ! '>='
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ segments:
130
+ - 0
131
+ hash: 3032661298513805062
132
+ requirements: []
133
+ rubyforge_project:
134
+ rubygems_version: 1.8.15
135
+ signing_key:
136
+ specification_version: 3
137
+ summary: Store your trend data in redis
138
+ test_files:
139
+ - spec/red_trend_spec.rb
140
+ - spec/spec_helper.rb