hubba-reports 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +4 -0
- data/Manifest.txt +27 -0
- data/README.md +177 -0
- data/Rakefile +30 -0
- data/lib/hubba/reports.rb +42 -0
- data/lib/hubba/reports/folio.rb +60 -0
- data/lib/hubba/reports/reports/base.rb +36 -0
- data/lib/hubba/reports/reports/catalog.rb +44 -0
- data/lib/hubba/reports/reports/size.rb +38 -0
- data/lib/hubba/reports/reports/stars.rb +37 -0
- data/lib/hubba/reports/reports/summary.rb +39 -0
- data/lib/hubba/reports/reports/timeline.rb +53 -0
- data/lib/hubba/reports/reports/topics.rb +48 -0
- data/lib/hubba/reports/reports/traffic.rb +112 -0
- data/lib/hubba/reports/reports/traffic_pages.rb +133 -0
- data/lib/hubba/reports/reports/traffic_referrers.rb +115 -0
- data/lib/hubba/reports/reports/trending.rb +51 -0
- data/lib/hubba/reports/reports/updates.rb +60 -0
- data/lib/hubba/reports/stats.rb +230 -0
- data/lib/hubba/reports/version.rb +18 -0
- data/test/helper.rb +11 -0
- data/test/stats/j/jekyll~minima.json +25 -0
- data/test/stats/o/openblockchains~awesome-blockchains.json +27 -0
- data/test/stats/o/opendatajson~factbook.json.json +39 -0
- data/test/stats/p/poole~hyde.json +21 -0
- data/test/test_stats.rb +123 -0
- data/test/test_stats_tmp.rb +44 -0
- metadata +125 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
module Hubba
|
2
|
+
|
3
|
+
class ReportTrending < Report
|
4
|
+
|
5
|
+
def build
|
6
|
+
|
7
|
+
## note: orgs is orgs+users e.g. geraldb, yorobot etc
|
8
|
+
buf = String.new('')
|
9
|
+
buf << "# Trending"
|
10
|
+
buf << " - #{@stats.repos.size} Repos @ #{@stats.orgs.size} Orgs"
|
11
|
+
buf << "\n\n"
|
12
|
+
|
13
|
+
###
|
14
|
+
## todo:
|
15
|
+
## use calc per month (days: 30)
|
16
|
+
## per week is too optimistic (e.g. less than one star/week e.g. 0.6 or something)
|
17
|
+
|
18
|
+
repos = @stats.repos.sort do |l,r|
|
19
|
+
## note: use reverse sort (right,left) - e.g. most stars first
|
20
|
+
## r[:stars] <=> l[:stars]
|
21
|
+
|
22
|
+
## sort by created_at (use julian days)
|
23
|
+
## r[:created_at].to_date.jd <=> l[:created_at].to_date.jd
|
24
|
+
|
25
|
+
res = r.diff <=> l.diff
|
26
|
+
res = r.stats.stars <=> l.stats.stars if res == 0
|
27
|
+
res = r.stats.created.jd <=> l.stats.created.jd if res == 0
|
28
|
+
res
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
## pp repos
|
33
|
+
|
34
|
+
|
35
|
+
repos.each_with_index do |repo,i|
|
36
|
+
if repo.diff == 0
|
37
|
+
buf << "- -/- "
|
38
|
+
else
|
39
|
+
buf << "- #{repo.diff}/month "
|
40
|
+
end
|
41
|
+
|
42
|
+
buf << " ★#{repo.stats.stars} **#{repo.full_name}** (#{repo.stats.size} kb) - "
|
43
|
+
buf << "#{repo.stats.history_str}\n"
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
buf
|
48
|
+
end # method build
|
49
|
+
end # class ReportTrending
|
50
|
+
|
51
|
+
end # module Hubba
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Hubba
|
2
|
+
|
3
|
+
|
4
|
+
class ReportUpdates < Report
|
5
|
+
|
6
|
+
def build
|
7
|
+
|
8
|
+
## note: orgs is orgs+users e.g. geraldb, yorobot etc
|
9
|
+
buf = String.new('')
|
10
|
+
buf << "# Updates"
|
11
|
+
buf << " - #{@stats.repos.size} Repos @ #{@stats.orgs.size} Orgs\n"
|
12
|
+
buf << "\n\n"
|
13
|
+
|
14
|
+
repos = @stats.repos.sort do |l,r|
|
15
|
+
r.stats.committed.jd <=> l.stats.committed.jd
|
16
|
+
end
|
17
|
+
|
18
|
+
## pp repos
|
19
|
+
|
20
|
+
|
21
|
+
buf << "committed / pushed / updated / created\n\n"
|
22
|
+
|
23
|
+
today = Date.today
|
24
|
+
|
25
|
+
repos.each_with_index do |repo,i|
|
26
|
+
|
27
|
+
days_ago = today.jd - repo.stats.committed.jd
|
28
|
+
|
29
|
+
diff1 = repo.stats.committed.jd - repo.stats.pushed.jd
|
30
|
+
diff2 = repo.stats.committed.jd - repo.stats.updated.jd
|
31
|
+
diff3 = repo.stats.pushed.jd - repo.stats.updated.jd
|
32
|
+
|
33
|
+
buf << "- (#{days_ago}d) **#{repo.full_name}** ★#{repo.stats.stars} - "
|
34
|
+
buf << "#{repo.stats.committed} "
|
35
|
+
buf << "("
|
36
|
+
buf << (diff1==0 ? '=' : "#{diff1}d")
|
37
|
+
buf << "/"
|
38
|
+
buf << (diff2==0 ? '=' : "#{diff2}d")
|
39
|
+
buf << ")"
|
40
|
+
buf << " / "
|
41
|
+
buf << "#{repo.stats.pushed} "
|
42
|
+
buf << "("
|
43
|
+
buf << (diff3==0 ? '=' : "#{diff3}d")
|
44
|
+
buf << ")"
|
45
|
+
buf << " / "
|
46
|
+
buf << "#{repo.stats.updated} / "
|
47
|
+
buf << "#{repo.stats.created} - "
|
48
|
+
buf << "‹#{repo.stats.last_commit_message}›"
|
49
|
+
buf << " (#{repo.stats.size} kb)"
|
50
|
+
buf << "\n"
|
51
|
+
end
|
52
|
+
buf << "<!-- break -->\n" ## markdown hack: add a list end marker
|
53
|
+
buf << "\n\n"
|
54
|
+
|
55
|
+
|
56
|
+
buf
|
57
|
+
end # method build
|
58
|
+
end # class ReportUpdates
|
59
|
+
|
60
|
+
end # module Hubba
|
@@ -0,0 +1,230 @@
|
|
1
|
+
module Hubba
|
2
|
+
|
3
|
+
####
|
4
|
+
# keep track of repo stats over time (with history hash)
|
5
|
+
|
6
|
+
class Stats ## todo/check: rename to GithubRepoStats or RepoStats - why? why not?
|
7
|
+
|
8
|
+
## attr_reader :data - needed - why? why not?
|
9
|
+
|
10
|
+
def full_name() @data['full_name']; end
|
11
|
+
def description() @data['description'] || ''; end ## todo/check: return nil if not found - why? why not?
|
12
|
+
alias_method :descr, :description
|
13
|
+
alias_method :desc, :description
|
14
|
+
|
15
|
+
def topics() @data['topics'] || []; end ## todo/check: return nil if not found - why? why not?
|
16
|
+
|
17
|
+
|
18
|
+
## note: return datetime objects (NOT strings); if not present/available return nil/null
|
19
|
+
def created_at() @cache['created_at'] ||= parse_datetime( @data['created_at'] ); end
|
20
|
+
def updated_at() @cache['updated_at'] ||= parse_datetime( @data['updated_at'] ); end
|
21
|
+
def pushed_at() @cache['pushed_at'] ||= parse_datetime( @data['pushed_at'] ); end
|
22
|
+
|
23
|
+
## date (only) versions
|
24
|
+
def created() @cache['created'] ||= parse_date( @data['created_at'] ); end
|
25
|
+
def updated() @cache['updated'] ||= parse_date( @data['updated_at'] ); end
|
26
|
+
def pushed() @cache['pushed'] ||= parse_date( @data['pushed_at'] ); end
|
27
|
+
|
28
|
+
def size
|
29
|
+
# size of repo in kb (as reported by github api)
|
30
|
+
@data['size'] || 0 ## return 0 if not found - why? why not? (return nil - why? why not??)
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def history
|
35
|
+
@cache['history'] ||= begin
|
36
|
+
if @data['history']
|
37
|
+
build_history( @data['history'] )
|
38
|
+
else
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def stars
|
46
|
+
## return last stargazers_count entry (as number; 0 if not found)
|
47
|
+
@cache['stars'] ||= history ? history[0].stars : 0
|
48
|
+
end
|
49
|
+
|
50
|
+
###################
|
51
|
+
# traffic
|
52
|
+
def traffic() @data['traffic']; end
|
53
|
+
|
54
|
+
|
55
|
+
###########
|
56
|
+
# commits
|
57
|
+
def commits() @data['commits']; end
|
58
|
+
|
59
|
+
def last_commit ## convenience shortcut; get first/last commit (use [0]) or nil
|
60
|
+
if @data['commits'] && @data['commits'][0]
|
61
|
+
@data['commits'][0]
|
62
|
+
else
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
def committed ## last commit date (from author NOT committer)
|
69
|
+
@cache['committed'] ||= parse_date( last_commit_author_date )
|
70
|
+
end
|
71
|
+
|
72
|
+
def committed_at() ## last commit date (from author NOT committer)
|
73
|
+
@cache['committed_at'] ||= parse_datetime( last_commit_author_date )
|
74
|
+
end
|
75
|
+
|
76
|
+
def last_commit_author_date
|
77
|
+
h = last_commit
|
78
|
+
h ? h['author']['date'] : nil
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def last_commit_message ## convenience shortcut; last commit message
|
83
|
+
h = last_commit
|
84
|
+
|
85
|
+
committer_name = h['committer']['name']
|
86
|
+
author_name = h['author']['name']
|
87
|
+
message = h['message']
|
88
|
+
|
89
|
+
buf = ""
|
90
|
+
buf << message
|
91
|
+
buf << " by #{author_name}"
|
92
|
+
|
93
|
+
if committer_name != author_name
|
94
|
+
buf << " w/ #{committer_name}"
|
95
|
+
end
|
96
|
+
end # method commit_message
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
###
|
101
|
+
# helpers
|
102
|
+
def parse_datetime( str ) str ? DateTime.strptime( str, '%Y-%m-%dT%H:%M:%S') : nil; end
|
103
|
+
def parse_date( str ) str ? Date.strptime( str, '%Y-%m-%d') : nil; end
|
104
|
+
|
105
|
+
########
|
106
|
+
## build history items (structs)
|
107
|
+
|
108
|
+
class HistoryItem
|
109
|
+
|
110
|
+
attr_reader :date, :stars, :forks ## read-only attributes
|
111
|
+
attr_accessor :prev, :next ## read/write attributes (for double linked list/nodes/items)
|
112
|
+
|
113
|
+
def initialize( date:, stars:, forks: )
|
114
|
+
@date = date
|
115
|
+
@stars = stars
|
116
|
+
@forks = forks
|
117
|
+
@next = nil
|
118
|
+
end
|
119
|
+
|
120
|
+
## link items (append item at the end/tail)
|
121
|
+
def append( item )
|
122
|
+
@next = item
|
123
|
+
item.prev = self
|
124
|
+
end
|
125
|
+
|
126
|
+
def diff_days
|
127
|
+
if @next
|
128
|
+
## note: use jd=julian days for calculation
|
129
|
+
@date.jd - @next.date.jd
|
130
|
+
else
|
131
|
+
nil ## last item (tail)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def diff_stars
|
136
|
+
if @next
|
137
|
+
@stars - @next.stars
|
138
|
+
else
|
139
|
+
nil ## last item (tail)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end ## class HistoryItem
|
143
|
+
|
144
|
+
|
145
|
+
def build_history( timeseries )
|
146
|
+
items = []
|
147
|
+
|
148
|
+
keys = timeseries.keys.sort.reverse ## newest (latest) items first
|
149
|
+
keys.each do |key|
|
150
|
+
h = timeseries[ key ]
|
151
|
+
|
152
|
+
item = HistoryItem.new(
|
153
|
+
date: Date.strptime( key, '%Y-%m-%d' ),
|
154
|
+
stars: h['stargazers_count'] || 0,
|
155
|
+
forks: h['forks_count'] || 0 )
|
156
|
+
|
157
|
+
## link items
|
158
|
+
last_item = items[-1]
|
159
|
+
last_item.append( item ) if last_item ## if not nil? append (note first item has no prev item)
|
160
|
+
|
161
|
+
items << item
|
162
|
+
end
|
163
|
+
|
164
|
+
## todo/check: return [] for empty items array (items.empty?) - why?? why not??
|
165
|
+
if items.empty?
|
166
|
+
nil
|
167
|
+
else
|
168
|
+
items
|
169
|
+
end
|
170
|
+
end ## method build_history
|
171
|
+
|
172
|
+
|
173
|
+
|
174
|
+
def calc_diff_stars( samples: 3, days: 30 )
|
175
|
+
## samples: use n history item samples e.g. 3 samples
|
176
|
+
## days e.g. 7 days (per week), 30 days (per month)
|
177
|
+
|
178
|
+
if history.nil?
|
179
|
+
nil ## todo/check: return 0.0 too - why? why not?
|
180
|
+
elsif history.size == 1
|
181
|
+
## just one item; CANNOT calc diff; return zero
|
182
|
+
0.0
|
183
|
+
else
|
184
|
+
idx = [history.size, samples].min ## calc last index
|
185
|
+
last = history[idx-1]
|
186
|
+
first = history[0]
|
187
|
+
|
188
|
+
diff_days = first.date.jd - last.date.jd
|
189
|
+
diff_stars = first.stars - last.stars
|
190
|
+
|
191
|
+
## note: use factor 1000 for fixed integer division
|
192
|
+
## converts to float at the end
|
193
|
+
|
194
|
+
## todo: check for better way (convert to float upfront - why? why not?)
|
195
|
+
|
196
|
+
diff = (diff_stars * days * 1000) / diff_days
|
197
|
+
## puts "diff=#{diff}:#{diff.class.name}" ## check if it's a float
|
198
|
+
(diff.to_f/1000.0)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
def history_str ## todo/check: rename/change to format_history or fmt_history - why? why not?
|
204
|
+
## returns "pretty printed" history as string buffer
|
205
|
+
buf = ''
|
206
|
+
buf << "[#{history.size}]: "
|
207
|
+
|
208
|
+
history.each do |item|
|
209
|
+
buf << "#{item.stars}"
|
210
|
+
|
211
|
+
diff_stars = item.diff_stars
|
212
|
+
diff_days = item.diff_days
|
213
|
+
if diff_stars && diff_days ## note: last item has no diffs
|
214
|
+
if diff_stars > 0 || diff_stars < 0
|
215
|
+
if diff_stars > 0
|
216
|
+
buf << " (+#{diff_stars}"
|
217
|
+
else
|
218
|
+
buf << " (#{diff_stars}"
|
219
|
+
end
|
220
|
+
buf << " in #{diff_days}d) "
|
221
|
+
else ## diff_stars == 0
|
222
|
+
buf << " (#{diff_days}d) "
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
buf
|
227
|
+
end # method history_str
|
228
|
+
end # class Stats
|
229
|
+
|
230
|
+
end # module Hubba
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module HubbaReports
|
2
|
+
MAJOR = 0 ## todo: namespace inside version or something - why? why not??
|
3
|
+
MINOR = 1
|
4
|
+
PATCH = 0
|
5
|
+
VERSION = [MAJOR,MINOR,PATCH].join('.')
|
6
|
+
|
7
|
+
def self.version
|
8
|
+
VERSION
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.banner
|
12
|
+
"hubba-reports/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.root
|
16
|
+
"#{File.expand_path( File.dirname(File.dirname(File.dirname(File.dirname(__FILE__)))) )}"
|
17
|
+
end
|
18
|
+
end # module HubbaReports
|
data/test/helper.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
{
|
2
|
+
"full_name": "jekyll/minima",
|
3
|
+
"created_at": "2016-05-20T23:07:56Z",
|
4
|
+
"updated_at": "2018-02-11T16:13:33Z",
|
5
|
+
"pushed_at": "2018-02-07T22:14:11Z",
|
6
|
+
"size": 321,
|
7
|
+
"history": {
|
8
|
+
"2018-02-12": {
|
9
|
+
"stargazers_count": 717
|
10
|
+
}
|
11
|
+
},
|
12
|
+
"commits": [
|
13
|
+
{
|
14
|
+
"author": {
|
15
|
+
"name": "ashmaroli",
|
16
|
+
"date": "2018-02-21T19:35:59Z"
|
17
|
+
},
|
18
|
+
"committer": {
|
19
|
+
"name": "Frank Taillandier",
|
20
|
+
"date": "2018-02-21T19:35:59Z"
|
21
|
+
},
|
22
|
+
"message": "social icons should resolve baseurl properly (#201)"
|
23
|
+
}
|
24
|
+
]
|
25
|
+
}
|
@@ -0,0 +1,27 @@
|
|
1
|
+
{
|
2
|
+
"full_name": "openblockchains/awesome-blockchains",
|
3
|
+
"created_at": "2017-09-13T22:33:56Z",
|
4
|
+
"size": 1620,
|
5
|
+
"history": {
|
6
|
+
"2017-12-10": {
|
7
|
+
"stargazers_count": 1084
|
8
|
+
},
|
9
|
+
"2018-01-28": {
|
10
|
+
"stargazers_count": 1411
|
11
|
+
},
|
12
|
+
"2018-02-08": {
|
13
|
+
"stargazers_count": 1526
|
14
|
+
}
|
15
|
+
},
|
16
|
+
"commits": [
|
17
|
+
{
|
18
|
+
"committer": {
|
19
|
+
"date": "2018-02-08T09:33:28Z",
|
20
|
+
"name": "Gerald Bauer"
|
21
|
+
},
|
22
|
+
"message": "Update README.md\n\nJust a little typo cryto -> crypto."
|
23
|
+
}
|
24
|
+
],
|
25
|
+
"updated_at": "2018-02-08T19:26:35Z",
|
26
|
+
"pushed_at": "2018-02-08T09:33:29Z"
|
27
|
+
}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
{
|
2
|
+
"full_name": "opendatajson/factbook.json",
|
3
|
+
"created_at": "2014-07-12T12:43:52Z",
|
4
|
+
"history": {
|
5
|
+
"2017-02-11": {
|
6
|
+
"stargazers_count": 457
|
7
|
+
},
|
8
|
+
"2017-02-12": {
|
9
|
+
"stargazers_count": 457
|
10
|
+
},
|
11
|
+
"2017-06-18": {
|
12
|
+
"stargazers_count": 505
|
13
|
+
},
|
14
|
+
"2017-07-28": {
|
15
|
+
"stargazers_count": 512
|
16
|
+
},
|
17
|
+
"2017-12-10": {
|
18
|
+
"stargazers_count": 533
|
19
|
+
},
|
20
|
+
"2018-01-28": {
|
21
|
+
"stargazers_count": 536
|
22
|
+
},
|
23
|
+
"2018-02-08": {
|
24
|
+
"stargazers_count": 539
|
25
|
+
}
|
26
|
+
},
|
27
|
+
"commits": [
|
28
|
+
{
|
29
|
+
"committer": {
|
30
|
+
"date": "2017-03-29T17:23:29Z",
|
31
|
+
"name": "GitHub"
|
32
|
+
},
|
33
|
+
"message": "Update MONGO.md"
|
34
|
+
}
|
35
|
+
],
|
36
|
+
"size": 7355,
|
37
|
+
"updated_at": "2018-02-01T12:35:19Z",
|
38
|
+
"pushed_at": "2017-03-29T17:23:30Z"
|
39
|
+
}
|