ossert 0.1.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 +16 -0
- data/.rspec +2 -0
- data/.rubocop_todo.yml +44 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +16 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +199 -0
- data/Rakefile +12 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/config/classifiers.yml +153 -0
- data/config/descriptions.yml +45 -0
- data/config/sidekiq.rb +15 -0
- data/config/stats.yml +198 -0
- data/config/translations.yml +44 -0
- data/db/backups/.keep +0 -0
- data/db/migrate/001_create_projects.rb +22 -0
- data/db/migrate/002_create_exceptions.rb +14 -0
- data/db/migrate/003_add_meta_to_projects.rb +14 -0
- data/db/migrate/004_add_timestamps_to_projects.rb +12 -0
- data/db/migrate/005_create_classifiers.rb +19 -0
- data/lib/ossert/classifiers/decision_tree.rb +112 -0
- data/lib/ossert/classifiers/growing/check.rb +172 -0
- data/lib/ossert/classifiers/growing/classifier.rb +175 -0
- data/lib/ossert/classifiers/growing.rb +163 -0
- data/lib/ossert/classifiers.rb +14 -0
- data/lib/ossert/config.rb +24 -0
- data/lib/ossert/fetch/bestgems.rb +98 -0
- data/lib/ossert/fetch/github.rb +536 -0
- data/lib/ossert/fetch/rubygems.rb +80 -0
- data/lib/ossert/fetch.rb +142 -0
- data/lib/ossert/presenters/project.rb +202 -0
- data/lib/ossert/presenters/project_v2.rb +117 -0
- data/lib/ossert/presenters.rb +8 -0
- data/lib/ossert/project.rb +144 -0
- data/lib/ossert/quarters_store.rb +164 -0
- data/lib/ossert/rake_tasks.rb +6 -0
- data/lib/ossert/reference.rb +87 -0
- data/lib/ossert/repositories.rb +138 -0
- data/lib/ossert/saveable.rb +153 -0
- data/lib/ossert/stats/agility_quarter.rb +62 -0
- data/lib/ossert/stats/agility_total.rb +71 -0
- data/lib/ossert/stats/base.rb +113 -0
- data/lib/ossert/stats/community_quarter.rb +28 -0
- data/lib/ossert/stats/community_total.rb +24 -0
- data/lib/ossert/stats.rb +32 -0
- data/lib/ossert/tasks/database.rake +179 -0
- data/lib/ossert/tasks/ossert.rake +52 -0
- data/lib/ossert/version.rb +4 -0
- data/lib/ossert/workers/fetch.rb +21 -0
- data/lib/ossert/workers/fetch_bestgems_page.rb +32 -0
- data/lib/ossert/workers/refresh_fetch.rb +22 -0
- data/lib/ossert/workers/sync_rubygems.rb +0 -0
- data/lib/ossert/workers.rb +11 -0
- data/lib/ossert.rb +63 -0
- data/ossert.gemspec +47 -0
- metadata +396 -0
data/lib/ossert/fetch.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'benchmark'
|
5
|
+
|
6
|
+
require 'ossert/fetch/github'
|
7
|
+
require 'ossert/fetch/rubygems'
|
8
|
+
require 'ossert/fetch/bestgems'
|
9
|
+
|
10
|
+
# TODO: Add logging
|
11
|
+
|
12
|
+
module Ossert
|
13
|
+
# Public: Various classes and methods for fetching data from different sources.
|
14
|
+
# Such as GitHub, Rubygems, Bestgems. Also provides simple functionality for
|
15
|
+
# fetching HTTP API.
|
16
|
+
module Fetch
|
17
|
+
ALL_FETCHERS = [Rubygems, Bestgems, GitHub].freeze
|
18
|
+
|
19
|
+
# Public: Fetch data for project using all fetchers by default process method
|
20
|
+
#
|
21
|
+
# project - The Ossert::Project instance to fill using fetchers
|
22
|
+
#
|
23
|
+
# Examples
|
24
|
+
#
|
25
|
+
# project = Ossert::Project.new('ramaze')
|
26
|
+
# Ossert::Fetch.all(project)
|
27
|
+
# project.dump
|
28
|
+
#
|
29
|
+
# Returns nothing.
|
30
|
+
def all(project)
|
31
|
+
ALL_FETCHERS.each do |fetcher|
|
32
|
+
puts "======> with #{fetcher}..."
|
33
|
+
time = Benchmark.realtime do
|
34
|
+
fetcher.new(project).process
|
35
|
+
end
|
36
|
+
puts "<====== Finished in #{time.round(3)} sec."
|
37
|
+
sleep(1)
|
38
|
+
end
|
39
|
+
nil
|
40
|
+
rescue => e
|
41
|
+
puts "Fetching Failed for '#{name}' with error: #{e.inspect}"
|
42
|
+
puts e.backtrace
|
43
|
+
end
|
44
|
+
module_function :all
|
45
|
+
|
46
|
+
# Public: Fetch data for project using given fetchers by process method
|
47
|
+
#
|
48
|
+
# fetchers - The Array or one of Ossert::Fetch::GitHub, Ossert::Fetch::Bestgems, Ossert::Fetch::Rubygems to
|
49
|
+
# use for processing
|
50
|
+
# project - The Ossert::Project instance to fill using fetchers
|
51
|
+
# process - The Symbol method name used for processing by fetchers (default: :process)
|
52
|
+
#
|
53
|
+
# Examples
|
54
|
+
#
|
55
|
+
# project = Ossert::Project.new('ramaze')
|
56
|
+
# Ossert::Fetch.only(Ossert::Fetch::Rubygems, project, :process_meta)
|
57
|
+
# project.dump_attribute :meta_data
|
58
|
+
#
|
59
|
+
# Returns nothing.
|
60
|
+
def only(fetchers, project, process = :process)
|
61
|
+
fetchers = Array.wrap(fetchers)
|
62
|
+
puts "Fetching project '#{project.name}'..."
|
63
|
+
(ALL_FETCHERS & fetchers).each do |fetcher|
|
64
|
+
puts "======> with #{fetcher}..."
|
65
|
+
time = Benchmark.realtime do
|
66
|
+
fetcher.new(project).send(process)
|
67
|
+
end
|
68
|
+
puts "<====== Finished in #{time.round(3)} sec."
|
69
|
+
sleep(1)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
module_function :only
|
73
|
+
|
74
|
+
# Public: Simple client for fetching HTTP API
|
75
|
+
#
|
76
|
+
# Examples
|
77
|
+
#
|
78
|
+
# client = SimpleClient.new("http://bestgems.org/api/v1/")
|
79
|
+
# client.get("gems/#{project.rubygems_alias}/total_downloads.json")
|
80
|
+
# # => Some JSON from api
|
81
|
+
class SimpleClient
|
82
|
+
attr_reader :api_endpoint, :type
|
83
|
+
|
84
|
+
# Public: Instantiate client for fetching API for given api_endpoint and response type
|
85
|
+
#
|
86
|
+
# path - The String describes path of endpoint to access the data
|
87
|
+
# type - The String describes type of response data, e.g. 'json'
|
88
|
+
#
|
89
|
+
# Examples
|
90
|
+
#
|
91
|
+
# client = SimpleClient.new("http://bestgems.org/api/v1/")
|
92
|
+
# client.get("gems/#{project.rubygems_alias}/total_downloads.json")
|
93
|
+
# # => Some JSON from api
|
94
|
+
#
|
95
|
+
# Returns nothing.
|
96
|
+
def initialize(api_endpoint, type = nil)
|
97
|
+
raise ArgumentError if !api_endpoint.start_with?('http') || !api_endpoint.end_with?('/')
|
98
|
+
@api_endpoint = api_endpoint
|
99
|
+
@type = type || 'json'
|
100
|
+
end
|
101
|
+
|
102
|
+
class NotFound < StandardError; end
|
103
|
+
class UnexpectedResponseError < StandardError; end
|
104
|
+
|
105
|
+
# Public: Get data via HTTP GET for given API path
|
106
|
+
#
|
107
|
+
# path - The String describes path of endpoint to access the data
|
108
|
+
#
|
109
|
+
# Examples
|
110
|
+
#
|
111
|
+
# client = SimpleClient.new("http://bestgems.org/api/v1/")
|
112
|
+
# client.get("gems/#{project.rubygems_alias}/total_downloads.json")
|
113
|
+
# # => Some JSON from api
|
114
|
+
#
|
115
|
+
# Returns nothing.
|
116
|
+
def get(path)
|
117
|
+
raise ArgumentError unless path.end_with? type
|
118
|
+
response = agent.get("#{api_endpoint}#{path}")
|
119
|
+
case response.status
|
120
|
+
when 404
|
121
|
+
raise NotFound
|
122
|
+
when 200
|
123
|
+
JSON.parse(response.body)
|
124
|
+
else
|
125
|
+
raise UnexpectedResponseError
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
# Internal: Initialize Faraday agent for processing requests
|
132
|
+
#
|
133
|
+
# Returns Faraday::Connection instance.
|
134
|
+
def agent
|
135
|
+
@agent ||= ::Faraday.new do |http|
|
136
|
+
http.request :url_encoded
|
137
|
+
http.adapter :net_http
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'ossert/presenters/project_v2'
|
3
|
+
|
4
|
+
module Ossert
|
5
|
+
module Presenters
|
6
|
+
class Project
|
7
|
+
include Ossert::Presenters::ProjectV2
|
8
|
+
|
9
|
+
attr_reader :project, :decorator
|
10
|
+
|
11
|
+
def initialize(project)
|
12
|
+
@project = project
|
13
|
+
@reference = Ossert::Classifiers::Growing.current.reference_values_per_grade
|
14
|
+
@decorator = Decorator.new(@reference)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.with_presenter(project)
|
18
|
+
presenter = new(project)
|
19
|
+
presenter.prepare!
|
20
|
+
yield(presenter)
|
21
|
+
presenter.cleanup_references!
|
22
|
+
end
|
23
|
+
|
24
|
+
class Decorator
|
25
|
+
def initialize(reference)
|
26
|
+
@reference = reference
|
27
|
+
end
|
28
|
+
|
29
|
+
# value, Float !
|
30
|
+
def with_reference(text, value, metric, type)
|
31
|
+
return (text.to_i.positive? ? "+#{text}" : text).to_s if type == :delta
|
32
|
+
metric_by_ref = @reference[type][metric.to_s]
|
33
|
+
reference = Project::CLASSES.inject('NaN') do |acc, ref|
|
34
|
+
metric_by_ref[ref][:range].cover?(value) ? ref : acc
|
35
|
+
end
|
36
|
+
|
37
|
+
{ text: "#{text} #{Project::KLASS_2_GRADE[reference]}",
|
38
|
+
mark: Project::KLASS_2_GRADE[reference].downcase }
|
39
|
+
rescue => e
|
40
|
+
puts "NO DATA FOR METRIC: '#{metric}'"
|
41
|
+
raise e
|
42
|
+
end
|
43
|
+
|
44
|
+
TOO_LONG_AGO = 20.years.ago
|
45
|
+
METRICS_DECORATIONS = {
|
46
|
+
/(percent|divergence)/ => ->(value) { "#{value.ceil}%" },
|
47
|
+
/(date|changed)/ => (lambda do |value|
|
48
|
+
date = Time.at(value)
|
49
|
+
return 'N/A' if date < TOO_LONG_AGO
|
50
|
+
date.strftime('%d/%m/%y')
|
51
|
+
end),
|
52
|
+
/processed_in/ => (lambda do |value|
|
53
|
+
return 'N/A' if value >= Ossert::Stats::PER_YEAR_TOO_LONG || value.zero?
|
54
|
+
case value
|
55
|
+
when 1
|
56
|
+
"~#{value.ceil} day"
|
57
|
+
when 2..30
|
58
|
+
"~#{value.ceil} days"
|
59
|
+
when 31..61
|
60
|
+
"~#{(value / 31).to_i} month"
|
61
|
+
else
|
62
|
+
"~#{(value / 31).to_i} months"
|
63
|
+
end
|
64
|
+
end),
|
65
|
+
/period/ => (lambda do |value|
|
66
|
+
if (years = value.to_i / 365).positive?
|
67
|
+
"#{years}+ years"
|
68
|
+
'Less than a year'
|
69
|
+
else
|
70
|
+
end
|
71
|
+
end),
|
72
|
+
/downloads/ => ->(value) { value.ceil.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse }
|
73
|
+
}.freeze
|
74
|
+
|
75
|
+
def value(metric, value)
|
76
|
+
value = value.to_f
|
77
|
+
METRICS_DECORATIONS.each { |check, decorator| return decorator.call(value) if metric =~ check }
|
78
|
+
value.to_i
|
79
|
+
end
|
80
|
+
|
81
|
+
def metric(metric, value, type)
|
82
|
+
with_reference(value(metric, value), value.to_f, metric, type)
|
83
|
+
end
|
84
|
+
|
85
|
+
def quarter_with_diff(project, time, section)
|
86
|
+
section_type = "#{section}_quarter".to_sym
|
87
|
+
quarter_data = project.send(section).quarters[quarter_start(time)].metrics_to_hash
|
88
|
+
diff = quarter_values(project.send(section).quarters[quarter_start(time) - 1.day].metrics_to_hash)
|
89
|
+
|
90
|
+
quarter_data.each_with_object({}) do |(metric, value), result|
|
91
|
+
decorated_metric = metric(metric, value, section_type)
|
92
|
+
result[Ossert.t(metric)] = <<-TEXT
|
93
|
+
#{decorated_metric[:text]} <> #{metric(metric, value.to_i - diff[metric], :delta)}
|
94
|
+
TEXT
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def metrics(metrics_data, section_type)
|
99
|
+
metrics_data.each_with_object({}) do |(metric, value), result|
|
100
|
+
result[metric] = metric(metric, value.to_i, section_type)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def quarter_values(quarter_data)
|
105
|
+
quarter_data.each_with_object({}) do |(metric, value), res|
|
106
|
+
res[metric] = value.to_i
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def quarter_start(time)
|
113
|
+
Time.at(time).to_date.to_time(:utc).beginning_of_quarter
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def agility_quarter(time)
|
118
|
+
decorator.quarter_with_diff(@project, time, :agility)
|
119
|
+
end
|
120
|
+
|
121
|
+
def community_quarter(time)
|
122
|
+
decorator.quarter_with_diff(@project, time, :community)
|
123
|
+
end
|
124
|
+
|
125
|
+
def agility_quarter_values(time)
|
126
|
+
decorator.quarter_values @project.agility.quarters[time].metrics_to_hash
|
127
|
+
end
|
128
|
+
|
129
|
+
def community_quarter_values(time)
|
130
|
+
decorator.quarter_values @project.community.quarters[time].metrics_to_hash
|
131
|
+
end
|
132
|
+
|
133
|
+
def agility_total
|
134
|
+
@agility_total ||= decorator.metrics @project.agility.total.metrics_to_hash, :agility_total
|
135
|
+
end
|
136
|
+
|
137
|
+
def community_total
|
138
|
+
@community_total ||= decorator.metrics @project.community.total.metrics_to_hash, :community_total
|
139
|
+
end
|
140
|
+
|
141
|
+
def community_last_year
|
142
|
+
@community_last_year ||= decorator.metrics @project.community.quarters.last_year_as_hash, :community_year
|
143
|
+
end
|
144
|
+
|
145
|
+
def agility_last_year
|
146
|
+
@agility_last_year ||= decorator.metrics @project.agility.quarters.last_year_as_hash, :agility_year
|
147
|
+
end
|
148
|
+
|
149
|
+
def metric_preview(metric)
|
150
|
+
preview = {}
|
151
|
+
return(preview) if (section = Ossert::Stats.guess_section_by_metric(metric)) == :not_found
|
152
|
+
|
153
|
+
preview[:tooltip] = MultiJson.dump(tooltip_data(metric))
|
154
|
+
preview[:translation] = Ossert.t(metric)
|
155
|
+
|
156
|
+
preview.merge!(last_year_section(metric, section))
|
157
|
+
preview.merge!(total_section(metric, section))
|
158
|
+
preview
|
159
|
+
end
|
160
|
+
|
161
|
+
def last_year_section(metric, section)
|
162
|
+
section_last_year = public_send("#{section}_last_year")[metric]
|
163
|
+
{
|
164
|
+
last_year_mark: section_last_year.try(:[], :mark),
|
165
|
+
last_year_val: section_last_year.try(:[], :text) || 'N/A'
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
def total_section(metric, section)
|
170
|
+
section_total = public_send("#{section}_total")[metric]
|
171
|
+
{
|
172
|
+
total_mark: section_total.try(:[], :mark),
|
173
|
+
total_val: section_total.try(:[], :text) || 'N/A'
|
174
|
+
}
|
175
|
+
end
|
176
|
+
|
177
|
+
def prepare!
|
178
|
+
agility_total
|
179
|
+
agility_last_year
|
180
|
+
community_total
|
181
|
+
community_last_year
|
182
|
+
|
183
|
+
lookback = 5
|
184
|
+
check_results = (lookback - 1).downto(0).map do |last_year_offset|
|
185
|
+
Ossert::Classifiers::Growing.current.check(@project, last_year_offset)
|
186
|
+
end
|
187
|
+
|
188
|
+
@grade = check_results.last(2).first.map { |k, v| [k, v[:mark].downcase] }.to_h
|
189
|
+
@fast_preview_graph = fast_preview_graph_data(lookback, check_results)
|
190
|
+
end
|
191
|
+
attr_reader :grade, :fast_preview_graph
|
192
|
+
|
193
|
+
def cleanup_references!
|
194
|
+
@reference = nil
|
195
|
+
@project = nil
|
196
|
+
@fast_preview_graph_data = nil
|
197
|
+
@grade = nil
|
198
|
+
@decorator = nil
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Ossert
|
3
|
+
module Presenters
|
4
|
+
module ProjectV2
|
5
|
+
CLASSES = %w(
|
6
|
+
ClassE
|
7
|
+
ClassD
|
8
|
+
ClassC
|
9
|
+
ClassB
|
10
|
+
ClassA
|
11
|
+
).freeze
|
12
|
+
KLASS_2_GRADE = {
|
13
|
+
'ClassA' => 'A',
|
14
|
+
'ClassB' => 'B',
|
15
|
+
'ClassC' => 'C',
|
16
|
+
'ClassD' => 'D',
|
17
|
+
'ClassE' => 'E'
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
def preview_reference_values_for(metric, section) # maybe automatically find section?
|
21
|
+
metric_by_grades = @reference[section][metric.to_s]
|
22
|
+
grades = CLASSES.reverse
|
23
|
+
sign = reversed_metrics.include?(metric) ? '<' : '>'
|
24
|
+
|
25
|
+
grades.each_with_object({}) do |klass, preview|
|
26
|
+
preview[KLASS_2_GRADE[klass]] = "#{sign} #{metric_by_grades[klass][:threshold].to_i}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
REFERENCES_STUB = {
|
31
|
+
'ClassA' => { threshold: '0', range: [] },
|
32
|
+
'ClassB' => { threshold: '0', range: [] },
|
33
|
+
'ClassC' => { threshold: '0', range: [] },
|
34
|
+
'ClassD' => { threshold: '0', range: [] },
|
35
|
+
'ClassE' => { threshold: '0', range: [] }
|
36
|
+
}.freeze
|
37
|
+
|
38
|
+
# Tooltip data:
|
39
|
+
# {
|
40
|
+
# title: '',
|
41
|
+
# description: '',
|
42
|
+
# ranks: [
|
43
|
+
# {"type":"a","year":100,"total":300},
|
44
|
+
# {"type":"b","year":80,"total":240},
|
45
|
+
# {"type":"c","year":60,"total":120},
|
46
|
+
# {"type":"d","year":40,"total":100},
|
47
|
+
# {"type":"e","year":20,"total":80}
|
48
|
+
# ]
|
49
|
+
# }
|
50
|
+
def tooltip_data(metric)
|
51
|
+
classes = CLASSES.reverse
|
52
|
+
section = Ossert::Stats.guess_section_by_metric(metric)
|
53
|
+
ranks = classes.inject([]) do |preview, klass|
|
54
|
+
base = { type: KLASS_2_GRADE[klass].downcase, year: ' N/A ', total: ' N/A ' }
|
55
|
+
preview << [:year, :total].each_with_object(base) do |section_type, result|
|
56
|
+
next unless (metric_data = metric_tooltip_data(metric, section, section_type, klass)).present?
|
57
|
+
result[section_type] = metric_data
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
{ title: Ossert.t(metric), description: Ossert.descr(metric), ranks: ranks }
|
62
|
+
end
|
63
|
+
|
64
|
+
def metric_tooltip_data(metric, section, section_type, klass)
|
65
|
+
return if section == :not_found # this should not happen
|
66
|
+
reference_section = [section, section_type].join('_')
|
67
|
+
return unless (metric_by_grades = @reference[reference_section.to_sym][metric.to_s])
|
68
|
+
|
69
|
+
[
|
70
|
+
reversed_metrics.include?(metric) ? '< ' : '> ',
|
71
|
+
decorator.value(metric, metric_by_grades[klass][:threshold])
|
72
|
+
].join(' ')
|
73
|
+
end
|
74
|
+
|
75
|
+
def reversed_metrics
|
76
|
+
@reversed_metrics ||= Ossert::Classifiers::Growing.config['reversed']
|
77
|
+
end
|
78
|
+
|
79
|
+
# Fast preview graph
|
80
|
+
# [
|
81
|
+
# {"title":"Jan - Mar 2016","type":"a","values":[10,20]},
|
82
|
+
# {"title":"Apr - Jun 2016","type":"b","values":[20,25]},
|
83
|
+
# {"title":"Jul - Sep 2016","type":"c","values":[25,35]},
|
84
|
+
# {"title":"Oct - Dec 2016","type":"d","values":[35,50]},
|
85
|
+
# {"title":"Next year","type":"e","values":[50,10]}
|
86
|
+
# ]
|
87
|
+
def fast_preview_graph_data(lookback = 5, check_results = nil)
|
88
|
+
graph_data = { popularity: [], maintenance: [], maturity: [] } # replace with config
|
89
|
+
|
90
|
+
check_results.each_with_index do |check_result, index|
|
91
|
+
check_result.each do |check, results|
|
92
|
+
sum_up_checks(graph_data, check, results, index, lookback - index)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
graph_data.map { |k, v| [k, MultiJson.dump(v)] }.to_h
|
96
|
+
end
|
97
|
+
|
98
|
+
def sum_up_checks(graph_data, check, results, index, offset)
|
99
|
+
gain = results[:gain]
|
100
|
+
graph_data[check] << {
|
101
|
+
title: last_quarters_bounds(offset),
|
102
|
+
type: results[:mark].downcase,
|
103
|
+
values: [gain, gain]
|
104
|
+
}
|
105
|
+
|
106
|
+
graph_data[check][index - 1][:values][1] = gain if index.positive?
|
107
|
+
end
|
108
|
+
|
109
|
+
def last_quarters_bounds(last_year_offset)
|
110
|
+
date = Time.current.utc - ((last_year_offset - 1) * 3).months
|
111
|
+
|
112
|
+
[date.beginning_of_quarter.strftime('%b'),
|
113
|
+
date.end_of_quarter.strftime('%b %Y')].join(' - ')
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Ossert
|
3
|
+
class Project
|
4
|
+
include Ossert::Saveable
|
5
|
+
|
6
|
+
attr_accessor :name, :github_alias, :rubygems_alias,
|
7
|
+
:community, :agility, :reference,
|
8
|
+
:meta, :created_at, :updated_at
|
9
|
+
|
10
|
+
META_STUB = {
|
11
|
+
homepage_url: nil,
|
12
|
+
docs_url: nil,
|
13
|
+
wiki_url: nil,
|
14
|
+
source_url: nil,
|
15
|
+
issue_tracker_url: nil,
|
16
|
+
mailing_list_url: nil,
|
17
|
+
authors: nil,
|
18
|
+
top_10_contributors: [],
|
19
|
+
description: nil,
|
20
|
+
current_version: nil,
|
21
|
+
rubygems_url: nil,
|
22
|
+
github_url: nil
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def fetch_all(name, reference = Ossert::Saveable::UNUSED_REFERENCE)
|
27
|
+
project = find_by_name(name, reference)
|
28
|
+
|
29
|
+
Ossert::Fetch.all project
|
30
|
+
project.prepare_time_bounds!
|
31
|
+
project.dump
|
32
|
+
end
|
33
|
+
|
34
|
+
def projects_by_reference
|
35
|
+
load_referenced.group_by(&:reference)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def grade_by_growing_classifier
|
40
|
+
raise unless Classifiers::Growing.current.ready?
|
41
|
+
Classifiers::Growing.current.grade(self)
|
42
|
+
end
|
43
|
+
alias grade_by_classifier grade_by_growing_classifier
|
44
|
+
|
45
|
+
def analyze_by_decisision_tree
|
46
|
+
raise unless Classifiers::DecisionTree.current.ready?
|
47
|
+
Classifiers::DecisionTree.current.check(self)
|
48
|
+
end
|
49
|
+
|
50
|
+
def initialize(name, github_alias = nil, rubygems_alias = nil, reference = nil)
|
51
|
+
@name = name.dup
|
52
|
+
@github_alias = github_alias
|
53
|
+
@rubygems_alias = (rubygems_alias || name).dup
|
54
|
+
@reference = reference.dup
|
55
|
+
|
56
|
+
@agility = Agility.new
|
57
|
+
@community = Community.new
|
58
|
+
@meta = META_STUB.dup
|
59
|
+
end
|
60
|
+
|
61
|
+
def assign_data(meta:, agility:, community:, created_at:, updated_at:)
|
62
|
+
@agility = agility
|
63
|
+
@community = community
|
64
|
+
@meta = meta
|
65
|
+
@created_at = created_at
|
66
|
+
@updated_at = updated_at
|
67
|
+
end
|
68
|
+
|
69
|
+
def decorated
|
70
|
+
@decorated ||= Ossert::Presenters::Project.new(self)
|
71
|
+
end
|
72
|
+
|
73
|
+
TIME_BOUNDS_CONFIG = {
|
74
|
+
base_value: {
|
75
|
+
start: nil,
|
76
|
+
end: nil
|
77
|
+
},
|
78
|
+
aggregation: {
|
79
|
+
start: :min,
|
80
|
+
end: :max
|
81
|
+
},
|
82
|
+
extended: {
|
83
|
+
start: nil,
|
84
|
+
end: nil
|
85
|
+
}
|
86
|
+
}.freeze
|
87
|
+
|
88
|
+
def prepare_time_bounds!(extended_start: nil, extended_end: nil)
|
89
|
+
config = TIME_BOUNDS_CONFIG.dup
|
90
|
+
config[:base_value][:start] = Time.now.utc
|
91
|
+
config[:base_value][:end] = 20.years.ago
|
92
|
+
config[:extended][:start] = (extended_start || Time.now.utc).to_datetime
|
93
|
+
config[:extended][:end] = (extended_end || 20.years.ago).to_datetime
|
94
|
+
|
95
|
+
agility.quarters.fullfill! && community.quarters.fullfill!
|
96
|
+
|
97
|
+
[:start, :end].map { |time_bound| time_bound_values(time_bound, config).to_date }
|
98
|
+
end
|
99
|
+
|
100
|
+
def time_bound_values(time_bound, config)
|
101
|
+
[
|
102
|
+
config[:base_value][time_bound], config[:extended][time_bound],
|
103
|
+
agility.quarters.send("#{time_bound}_date"), community.quarters.send("#{time_bound}_date")
|
104
|
+
].send(config[:aggregation][time_bound])
|
105
|
+
end
|
106
|
+
|
107
|
+
def meta_to_json
|
108
|
+
MultiJson.dump(meta)
|
109
|
+
end
|
110
|
+
|
111
|
+
class BaseStore
|
112
|
+
attr_accessor :quarters, :total, :total_prediction, :quarter_prediction
|
113
|
+
|
114
|
+
def initialize(quarters: nil, total: nil)
|
115
|
+
@quarters = quarters || QuartersStore.new(self.class.quarter_stats_klass_name)
|
116
|
+
@total = total || ::Kernel.const_get(self.class.total_stats_klass_name).new
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class Agility < BaseStore
|
121
|
+
class << self
|
122
|
+
def quarter_stats_klass_name
|
123
|
+
'Ossert::Stats::AgilityQuarter'
|
124
|
+
end
|
125
|
+
|
126
|
+
def total_stats_klass_name
|
127
|
+
'Ossert::Stats::AgilityTotal'
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class Community < BaseStore
|
133
|
+
class << self
|
134
|
+
def quarter_stats_klass_name
|
135
|
+
'Ossert::Stats::CommunityQuarter'
|
136
|
+
end
|
137
|
+
|
138
|
+
def total_stats_klass_name
|
139
|
+
'Ossert::Stats::CommunityTotal'
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|