hubba-reports 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/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
|
+
}
|