tally 1.0.0.beta1

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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +217 -0
  3. data/Rakefile +32 -0
  4. data/app/assets/config/tally_manifest.js +2 -0
  5. data/app/assets/javascripts/tally/application.js +15 -0
  6. data/app/assets/stylesheets/tally/application.css +15 -0
  7. data/app/controllers/tally/application_controller.rb +17 -0
  8. data/app/controllers/tally/days_controller.rb +19 -0
  9. data/app/controllers/tally/keys_controller.rb +19 -0
  10. data/app/controllers/tally/records_controller.rb +21 -0
  11. data/app/helpers/tally/application_helper.rb +4 -0
  12. data/app/helpers/tally/increment_helper.rb +15 -0
  13. data/app/jobs/tally/application_job.rb +4 -0
  14. data/app/jobs/tally/calculator_runner_job.rb +12 -0
  15. data/app/models/tally/application_record.rb +7 -0
  16. data/app/models/tally/record.rb +30 -0
  17. data/app/presenters/tally/record_presenter.rb +29 -0
  18. data/bin/annotate +29 -0
  19. data/bin/appraisal +29 -0
  20. data/bin/bundle +114 -0
  21. data/bin/coderay +29 -0
  22. data/bin/htmldiff +29 -0
  23. data/bin/ldiff +29 -0
  24. data/bin/nokogiri +29 -0
  25. data/bin/pry +29 -0
  26. data/bin/rackup +29 -0
  27. data/bin/rails +14 -0
  28. data/bin/rake +29 -0
  29. data/bin/rspec +29 -0
  30. data/bin/rubocop +29 -0
  31. data/bin/ruby-parse +29 -0
  32. data/bin/ruby-rewrite +29 -0
  33. data/bin/sprockets +29 -0
  34. data/bin/thor +29 -0
  35. data/config/routes.rb +12 -0
  36. data/db/migrate/20181016172358_create_tally_records.rb +15 -0
  37. data/lib/tally.rb +77 -0
  38. data/lib/tally/archiver.rb +64 -0
  39. data/lib/tally/calculator.rb +30 -0
  40. data/lib/tally/calculator_runner.rb +56 -0
  41. data/lib/tally/calculators.rb +29 -0
  42. data/lib/tally/countable.rb +23 -0
  43. data/lib/tally/daily.rb +64 -0
  44. data/lib/tally/engine.rb +28 -0
  45. data/lib/tally/increment.rb +25 -0
  46. data/lib/tally/key_finder.rb +123 -0
  47. data/lib/tally/key_finder/entry.rb +66 -0
  48. data/lib/tally/keyable.rb +55 -0
  49. data/lib/tally/record_searcher.rb +110 -0
  50. data/lib/tally/sweeper.rb +46 -0
  51. data/lib/tally/version.rb +5 -0
  52. data/lib/tasks/tally_tasks.rake +16 -0
  53. metadata +256 -0
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rubocop", "rubocop")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'ruby-parse' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("parser", "ruby-parse")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'ruby-rewrite' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("parser", "ruby-rewrite")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'sprockets' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("sprockets", "sprockets")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'thor' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("thor", "thor")
@@ -0,0 +1,12 @@
1
+ Tally::Engine.routes.draw do
2
+
3
+ get "/days/:type/:id", to: "days#index", as: :recordable_days
4
+ resources :days, only: :index
5
+
6
+ get "/keys/:type/:id", to: "keys#index", as: :recordable_keys
7
+ resources :keys, only: :index
8
+
9
+ get "/records/:type/:id", to: "records#index", as: :recordable_records
10
+ resources :records, only: :index
11
+
12
+ end
@@ -0,0 +1,15 @@
1
+ class CreateTallyRecords < ActiveRecord::Migration[5.2]
2
+ def change
3
+ create_table :tally_records do |t|
4
+ t.date :day
5
+ t.bigint :recordable_id
6
+ t.string :recordable_type
7
+ t.string :key
8
+ t.integer :value, default: 0
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :tally_records, [ :day, :key, :recordable_id, :recordable_type ], name: "index_tally_records_on_day", unique: true
14
+ end
15
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tally/engine"
4
+ require "tally/version"
5
+ require "active_support/dependencies/autoload"
6
+ require "kaminari/activerecord"
7
+
8
+ require "redis"
9
+
10
+ begin
11
+ # attempt to load sidekiq if it is installed
12
+ # if not, just use plain Redis + ActiveJob
13
+ require "sidekiq"
14
+ rescue LoadError
15
+ nil
16
+ end
17
+
18
+ module Tally
19
+
20
+ extend ActiveSupport::Autoload
21
+ include ActiveSupport::Configurable
22
+
23
+ autoload :Archiver
24
+ autoload :Calculator
25
+ autoload :CalculatorRunner
26
+ autoload :Countable
27
+ autoload :Daily
28
+ autoload :Increment
29
+ autoload :Keyable
30
+ autoload :KeyFinder
31
+ autoload :RecordSearcher
32
+ autoload :Sweeper
33
+
34
+ eager_autoload do
35
+ autoload :Calculators
36
+ end
37
+
38
+ extend Calculators
39
+
40
+ configure do |config|
41
+ config.prefix = "tally"
42
+ config.date_format = "%Y-%m-%d"
43
+
44
+ # Amount of time a key lives by default
45
+ config.ttl = 4.days
46
+
47
+ # Archivers get queued into the background with ActiveJob by default
48
+ # Set to :now to run inline
49
+ config.perform_calculators = :later
50
+ end
51
+
52
+ # If sidekiq is available, piggyback on its pooling
53
+ #
54
+ # Otherwise, just use redis directly
55
+ def self.redis(&block)
56
+ raise ArgumentError, "requires a block" unless block_given?
57
+
58
+ if defined?(Sidekiq)
59
+ Sidekiq.redis(&block)
60
+ else
61
+ block.call(redis_connection)
62
+ end
63
+ end
64
+
65
+ def self.redis_connection
66
+ @redis_connection ||= Redis.current
67
+ end
68
+
69
+ def self.redis_connection=(connection)
70
+ @redis_connection = connection
71
+ end
72
+
73
+ def self.increment(*args)
74
+ Increment.public_send(:increment, *args)
75
+ end
76
+
77
+ end
@@ -0,0 +1,64 @@
1
+ module Tally
2
+ class Archiver
3
+
4
+ def initialize(key: nil, day: nil, record: nil, type: nil)
5
+ @key = key
6
+ @day = day
7
+ @record = record
8
+ @type = type
9
+ end
10
+
11
+ def archive!
12
+ remove_existing_records
13
+
14
+ finder.entries.each do |entry|
15
+ next if entry.type.present? && !entry.record
16
+
17
+ record = if entry.record
18
+ Record.find_or_initialize_by(day: entry.date, key: entry.key, recordable: entry.record)
19
+ else
20
+ Record.find_or_initialize_by(day: entry.date, key: entry.key, recordable: nil)
21
+ end
22
+
23
+ record.value = entry.value
24
+ record.save
25
+ end
26
+
27
+ enqueue_registered_calculators
28
+
29
+ true
30
+ end
31
+
32
+ def day
33
+ @day ||= Time.current.utc.to_date
34
+ end
35
+
36
+ def self.archive!(*args)
37
+ new(*args).archive!
38
+ end
39
+
40
+ private
41
+
42
+ def enqueue_registered_calculators
43
+ day_str = day.strftime("%Y-%m-%d")
44
+ calculate_method = Tally.config.perform_calculators == :now ? :perform_now : :perform_later
45
+
46
+ Tally.calculators.each do |class_name|
47
+ CalculatorRunnerJob.public_send(calculate_method, class_name, day_str)
48
+ end
49
+ end
50
+
51
+ def finder
52
+ @finder ||= Daily.new(key: @key, day: @day, record: @record, type: @type)
53
+ end
54
+
55
+ def remove_existing_records
56
+ return if @key.present?
57
+ return if @record.present?
58
+ return if @type.present?
59
+
60
+ Record.where(day: day).delete_all
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,30 @@
1
+ require "active_support/concern"
2
+
3
+ module Tally
4
+ module Calculator
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attr_reader :day
10
+ end
11
+
12
+ def initialize(day = Date.today)
13
+ @day = day
14
+ end
15
+
16
+ # Override in sub class, this is what gets called when the calculator
17
+ # is run. This method is run in the background so it can take a while
18
+ # if needed to summarize data.
19
+ def call
20
+ raise NotImplementedError
21
+ end
22
+
23
+ private
24
+
25
+ def record_scope
26
+ Record.where(day: day).includes(:recordable)
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,56 @@
1
+ module Tally
2
+ class CalculatorRunner
3
+
4
+ attr_reader :date
5
+
6
+ def initialize(class_name, date)
7
+ @date = date
8
+ @class_name = class_name
9
+ end
10
+
11
+ def klass
12
+ @klass ||= @class_name.to_s.safe_constantize
13
+ end
14
+
15
+ # loop through each value and save in db
16
+ def save
17
+ return false unless valid?
18
+
19
+ values.each do |attributes|
20
+ create_record(attributes)
21
+ end
22
+
23
+ true
24
+ end
25
+
26
+ def values
27
+ return [] unless valid?
28
+
29
+ @values ||= [ klass.new(date).call ].flatten
30
+ end
31
+
32
+ def valid?
33
+ klass.present? && date.present?
34
+ end
35
+
36
+ private
37
+
38
+ def create_record(attributes)
39
+ finder = { day: date, key: attributes[:key] }
40
+
41
+ id = attributes.delete(:id)
42
+ type = attributes.delete(:type)
43
+
44
+ if id && type
45
+ finder[:recordable_id] = id
46
+ finder[:recordable_type] = type.to_s.classify
47
+ end
48
+
49
+ record = Record.find_or_initialize_by(finder)
50
+
51
+ record.attributes = attributes
52
+ record.save
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,29 @@
1
+ module Tally
2
+ module Calculators
3
+
4
+ def calculators
5
+ @calculators ||= []
6
+ end
7
+
8
+ def register_calculator(*class_name)
9
+ @calculators ||= []
10
+
11
+ class_name.each do |class_name|
12
+ unless @calculators.include?(class_name.to_s)
13
+ @calculators.push(class_name.to_s)
14
+ end
15
+ end
16
+
17
+ nil
18
+ end
19
+
20
+ def unregister_calculator(*class_names)
21
+ @calculators ||= []
22
+
23
+ class_names = class_names.map(&:to_s)
24
+
25
+ @calculators.delete_if { |n| class_names.include?(n) }
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ require "active_support/concern"
2
+
3
+ module Tally
4
+ module Countable
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ def increment_tally(key, by = 1)
9
+ return if new_record?
10
+
11
+ Tally.increment(key, self, by)
12
+ end
13
+
14
+ def tally_records(search_params = {})
15
+ if search_params.present?
16
+ RecordSearcher.search(search_params.merge(record: self))
17
+ else
18
+ Record.where(recordable: self)
19
+ end
20
+ end
21
+
22
+ end
23
+ end