hubba 0.6.1 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 122527cdc3cddb68e2c54a6e51fcfb77c2c91c0c
4
- data.tar.gz: 6bb52baff0536f55ad969885fc8500b031457e44
3
+ metadata.gz: 0c7c4d9d7af183756855559bc03b8910bdcb1ace
4
+ data.tar.gz: 4cfd725ee9c1a3138370cbcb61e1bd900d689012
5
5
  SHA512:
6
- metadata.gz: df4e036a205be30371ec9de85e8085717235922fcc21e22f9f516cc40481a2a8cf6a9b0edd0a37cb24759059a08b5ea74957f41f89139bf9e1a860ed20029e3b
7
- data.tar.gz: 698fc0d240aa6c6f8b125d1ae004beacf5ae97c220eb42d4fd542a49a656ece101e3a9fd9e740eaefb63e7f03c132458872f4ea5949cd30e2eceb9ccc3bb9e54
6
+ metadata.gz: a6cb53226a5a73777bc9ad86d82a22a39ea56710514233d5530b489d9fcce1bd9cc67b65f6d41f488d3ee4a266e0be5cbcbeef9231cef9007427928d60785214
7
+ data.tar.gz: 38395c69b0e45a174a1629029803f41b3e5d1ca7dba27d893dfcebd70f59ed4f17e60e6ec1715c331b61d8e49ef727de564aba14a03b0dfe75a6c15850e3bdc8
data/Manifest.txt CHANGED
@@ -3,12 +3,14 @@ Manifest.txt
3
3
  README.md
4
4
  Rakefile
5
5
  lib/hubba.rb
6
- lib/hubba/client.rb
7
6
  lib/hubba/config.rb
7
+ lib/hubba/folio.rb
8
8
  lib/hubba/github.rb
9
+ lib/hubba/hubba.rb
9
10
  lib/hubba/reports.rb
10
- lib/hubba/reposet.rb
11
11
  lib/hubba/stats.rb
12
+ lib/hubba/update.rb
13
+ lib/hubba/update_traffic.rb
12
14
  lib/hubba/version.rb
13
15
  test/helper.rb
14
16
  test/stats/jekyll~minima.json
data/README.md CHANGED
@@ -10,8 +10,255 @@ hubba gem - (yet) another (lite) GitHub HTTP API client / library
10
10
 
11
11
  ## Usage
12
12
 
13
- TBD
14
13
 
14
+ ### Step 0: Secrets, Secrets, Secrets - Your Authentication Token
15
+
16
+ Note: Set your GitHub env credentials (personal access token preferred) e.g.
17
+
18
+ ```
19
+ set HUBBA_TOKEN=abcdef0123456789
20
+ # - or -
21
+ set HUBBA_USER=you
22
+ set HUBBA_PASSWORD=topsecret
23
+ ```
24
+
25
+
26
+ ### Step 1: Get a list of all your repos
27
+
28
+ Use the GitHub API to get a list of all your repos:
29
+
30
+ ``` ruby
31
+ require 'hubba'
32
+
33
+ h = Hubba.reposet( 'geraldb' )
34
+ pp h
35
+
36
+ File.open( './repos.yml', 'w' ) { |f| f.write( h.to_yaml ) }
37
+ ```
38
+
39
+ resulting in:
40
+
41
+ ``` yaml
42
+ geraldb (11):
43
+ - austria
44
+ - catalog
45
+ - chelitas
46
+ - geraldb.github.io
47
+ - logos
48
+ - sandbox
49
+ - talks
50
+ - web-proxy-win
51
+ - webcomponents
52
+ - webpub-reader
53
+ - wine.db.tools
54
+
55
+ openfootball (41):
56
+ - africa-cup
57
+ - austria
58
+ - club-world-cup
59
+ - clubs
60
+ - confed-cup
61
+ - copa-america
62
+ - copa-libertadores
63
+ - copa-sudamericana
64
+ - deutschland
65
+ # ...
66
+ ```
67
+
68
+
69
+ Note: If you have more than one account (e.g. an extra robot account or such)
70
+ you can add them along e.g.
71
+
72
+
73
+ ``` ruby
74
+ h = Hubba.reposet( 'geraldb', 'yorobot' )
75
+ pp h
76
+ ```
77
+
78
+
79
+ Note: By default all your repos from organizations get auto-included -
80
+ use the `orgs: false` option to turn off auto-inclusion.
81
+
82
+ Note: By default all (personal) repos, that is, repos in your primary (first)
83
+ account that are forks get auto-excluded.
84
+
85
+
86
+
87
+ ### Step 2: Update repo statistics (daily / weekly / monthly)
88
+
89
+
90
+ Use `update_stats` to
91
+ to get the latest commit, star count and more for all your repos
92
+ listed in `./repos.yml` via the GitHub API:
93
+
94
+ ``` ruby
95
+ Hubba.update_stats( './repos.yml' )
96
+ ```
97
+
98
+ Note: By default the datafiles (one per repo)
99
+ get stored in the `./data` directory.
100
+
101
+
102
+
103
+
104
+ ### Step 3: Generate some statistics / reports
105
+
106
+
107
+ Hubba has four built-in reports (for now):
108
+
109
+ - `ReportSummary` - A-Z list of your repos by orgs with stars and size in kb
110
+ - `ReportStars` - your repos ranked by stars
111
+ - `ReportTimeline` - your repos in reverse chronological order by creation
112
+ - `ReportUpdates` - your repos in reverse chronological order by last commit
113
+
114
+
115
+ If you only generate a single report, use:
116
+
117
+ ``` ruby
118
+ report = Hubba::ReportSummary.new( './repos.yml' )
119
+ report.save( './SUMMARY.md' )
120
+ ```
121
+
122
+
123
+ If you generate more reports, (re)use the in-memory statistics cache / object:
124
+
125
+ ``` ruby
126
+ stats = Hubba.stats( './repos.yml' )
127
+
128
+ report = Hubba::ReportSummary.new( stats )
129
+ report.save( './SUMMARY.md' )
130
+
131
+ report = Hubba::ReportStars.new( stats )
132
+ report.save( './STARS.md' )
133
+
134
+ report = Hubba::ReportTimeline.new( stats )
135
+ report.save( './TIMELINE.md' )
136
+
137
+ report = Hubba::ReportUpdates.new( stats )
138
+ report.save( './UPDATES.md' )
139
+ ```
140
+
141
+
142
+ ### Report Examples
143
+
144
+ #### Report Example - Summary
145
+
146
+ A-Z list of your repos by orgs with stars and size in kb.
147
+ Results in:
148
+
149
+ ---
150
+
151
+ > 593 repos @ 83 orgs
152
+ >
153
+ > ### geraldb _(11)_
154
+ >
155
+ > **austria** ★1 (552 kb) · **catalog** ★3 (156 kb) · **chelitas** ★1 (168 kb) · **geraldb.github.io** ★1 (520 kb) · **logos** ★1 (363 kb) · **sandbox** ★2 (529 kb) · **talks** ★200 (16203 kb) · **web-proxy-win** ★8 (152 kb) · **webcomponents** ★1 (164 kb) · **webpub-reader** ★3 (11 kb) · **wine.db.tools** ★1 (252 kb)
156
+ >
157
+ > ...
158
+
159
+ ---
160
+
161
+ (Live Example - [`SUMMARY.md`](https://github.com/yorobot/backup/blob/master/SUMMARY.md))
162
+
163
+
164
+ #### Report Example - Stars
165
+
166
+ Your repos ranked by stars. Results in:
167
+
168
+ ---
169
+
170
+ > 593 repos @ 83 orgs
171
+ >
172
+ > 1. ★2936 **openblockchains/awesome-blockchains** (2514 kb)
173
+ > 2. ★851 **planetjekyll/awesome-jekyll-plugins** (148 kb)
174
+ > 3. ★604 **factbook/factbook.json** (7355 kb)
175
+ > 4. ★593 **openfootball/football.json** (2135 kb)
176
+ > 5. ★570 **openmundi/world.db** (1088 kb)
177
+ > 6. ★552 **openblockchains/programming-blockchains** (552 kb)
178
+ > 7. ★547 **mundimark/awesome-markdown** (83 kb)
179
+ > 8. ★532 **planetjekyll/awesome-jekyll** (110 kb)
180
+ > 9. ★489 **cryptocopycats/awesome-cryptokitties** (4154 kb)
181
+ > 10. ★445 **openfootball/world-cup** (638 kb)
182
+ >
183
+ > ...
184
+
185
+ ---
186
+
187
+ (Live Example: [`STARS.md`](https://github.com/yorobot/backup/blob/master/STARS.md))
188
+
189
+
190
+ #### Report Example - Timeline
191
+
192
+ Your repos in reverse chronological order by creation.
193
+ Results in:
194
+
195
+ ---
196
+
197
+ > 593 repos @ 83 orgs
198
+ >
199
+ > ## 2020
200
+ >
201
+ > ### 9
202
+ >
203
+ > - 2020-09-18 ★1 **yorobot/workflow** (83 kb)
204
+ >
205
+ > ### 6
206
+ >
207
+ > - 2020-06-27 ★2 **yorobot/sport.db.more** (80 kb)
208
+ > - 2020-06-24 ★1 **yorobot/stage** (554 kb)
209
+ > - 2020-06-11 ★1 **yorobot/cache.csv** (336 kb)
210
+ >
211
+ > ...
212
+
213
+ ---
214
+
215
+ (Live Example: [`TIMELINE.md`](https://github.com/yorobot/backup/blob/master/TIMELINE.md))
216
+
217
+
218
+
219
+ #### Report Example - Updates
220
+
221
+ Your repos in reverse chronological order by last commit. Results in:
222
+
223
+ ---
224
+
225
+ > 593 repos @ 83 orgs
226
+ >
227
+ > committed / pushed / updated / created
228
+ >
229
+ > - (1d) **yorobot/backup** ★4 - 2020-10-08 (=/=) / 2020-10-08 (=) / 2020-10-08 / 2015-04-04 - ‹› (1595 kb)
230
+ > - (1d) **yorobot/logs** ★1 - 2020-10-08 (=/=) / 2020-10-08 (=) / 2020-10-08 / 2016-09-13 - ‹› (172 kb)
231
+ > - (1d) **rubycoco/git** ★9 - 2020-10-08 (=/=) / 2020-10-08 (=) / 2020-10-08 / 2015-11-16 - ‹› (88 kb)
232
+ > - (1d) **openfootball/football.json** ★593 - 2020-10-08 (=/=) / 2020-10-08 (=) / 2020-10-08 / 2015-09-17 - ‹› (2135 kb)
233
+ > - (2d) **yorobot/workflow** ★1 - 2020-10-07 (=/=) / 2020-10-07 (=) / 2020-10-07 / 2020-09-18 - ‹› (83 kb)
234
+ > - (2d) **rubycoco/webclient** ★5 - 2020-10-07 (=/=) / 2020-10-07 (=) / 2020-10-07 / 2012-06-02 - ‹› (39 kb)
235
+ > - (3d) **footballcsv/belgium** ★1 - 2020-10-06 (=/=) / 2020-10-06 (=) / 2020-10-06 / 2014-07-25 - ‹› (314 kb)
236
+ > - (3d) **footballcsv/england** ★105 - 2020-10-06 (=/=) / 2020-10-06 (=) / 2020-10-06 / 2014-07-23 - ‹› (8666 kb)
237
+ > - (3d) **footballcsv/austria** ★1 - 2020-10-06 (=/=) / 2020-10-06 (=) / 2020-10-06 / 2018-07-16 - ‹› (91 kb)
238
+ > - (3d) **footballcsv/espana** ★15 - 2020-10-06 (=/=) / 2020-10-06 (=) / 2020-10-06 / 2014-07-23 - ‹› (1107 kb)
239
+ > - (3d) **footballcsv/deutschland** ★5 - 2020-10-06 (=/=) / 2020-10-06 (=) / 2020-10-06 / 2014-07-25 - ‹› (1343 kb)
240
+ >
241
+ > ...
242
+
243
+ ---
244
+
245
+ (Live Example: [`UPDATES.md`](https://github.com/yorobot/backup/blob/master/UPDATES.md))
246
+
247
+
248
+
249
+ That's all for now.
250
+
251
+
252
+
253
+ ## Installation
254
+
255
+ Use
256
+
257
+ gem install hubba
258
+
259
+ or add the gem to your Gemfile
260
+
261
+ gem 'hubba'
15
262
 
16
263
 
17
264
  ## License
@@ -0,0 +1,60 @@
1
+ module Hubba
2
+
3
+ class Folio # todo/check: use a different name e.g (Port)Folio, Cache, Summary, (Data)Base, Census, Catalog, Collection, Index, Register or such???
4
+ class Repo ## (nested) class
5
+
6
+ attr_reader :owner,
7
+ :name
8
+
9
+ def initialize( owner, name )
10
+ @owner = owner ## rename to login, username - why? why not?
11
+ @name = name ## rename to reponame ??
12
+ end
13
+
14
+ def full_name() "#{owner}/#{name}"; end
15
+
16
+ def stats
17
+ ## note: load stats on demand only (first access) for now - why? why not?
18
+ @stats ||= begin
19
+ stats = Stats.new( full_name )
20
+ stats.read
21
+ stats
22
+ end
23
+ end
24
+
25
+ def diff
26
+ @diff ||= stats.calc_diff_stars( samples: 3, days: 30 )
27
+ end
28
+ end # (nested) class Repo
29
+
30
+
31
+ attr_reader :orgs, :repos
32
+
33
+ def initialize( h )
34
+ @orgs = [] # orgs and users -todo/check: use better name - logins or owners? why? why not?
35
+ @repos = []
36
+ add( h )
37
+
38
+ puts "#{@repos.size} repos @ #{@orgs.size} orgs"
39
+ end
40
+
41
+ #############
42
+ ## private helpes
43
+ def add( h ) ## add repos.yml set
44
+ h.each do |org_with_counter, names|
45
+ ## remove optional number from key e.g.
46
+ ## mrhydescripts (3) => mrhydescripts
47
+ ## footballjs (4) => footballjs
48
+ ## etc.
49
+ org = org_with_counter.sub( /\([0-9]+\)/, '' ).strip
50
+ repos = []
51
+ names.each do |name|
52
+ repo = Repo.new( org, name )
53
+ repos << repo
54
+ end
55
+ @orgs << [org, repos]
56
+ @repos += repos
57
+ end
58
+ end
59
+ end # class Folio
60
+ end # module Hubba
data/lib/hubba/github.rb CHANGED
@@ -1,45 +1,59 @@
1
1
  module Hubba
2
2
 
3
3
 
4
- class Resource
5
- attr_reader :data
6
- def initialize( data )
7
- @data = data
4
+ class Github
5
+ BASE_URL = 'https://api.github.com'
6
+
7
+ ###############
8
+ ## (nested) classes for "wrapped" response (parsed json body)
9
+ class Resource
10
+ attr_reader :data
11
+ def initialize( data )
12
+ @data = data
13
+ end
8
14
  end
9
- end
10
15
 
11
- class Repos < Resource
12
- def names
13
- ## sort by name
14
- data.map { |item| item['name'] }.sort
16
+ class Repos < Resource
17
+ def names
18
+ ## sort by name
19
+ data.map { |item| item['name'] }.sort
20
+ end
15
21
  end
16
- end
17
22
 
18
- class Orgs < Resource
19
- def logins
20
- ## sort by name
21
- data.map { |item| item['login'] }.sort
23
+ class Orgs < Resource
24
+ def logins
25
+ ## sort by name
26
+ data.map { |item| item['login'] }.sort
27
+ end
28
+ alias_method :names, :logins ## add name alias - why? why not?
22
29
  end
23
- alias_method :names, :logins ## add name alias - why? why not?
24
- end
25
-
26
30
 
27
31
 
28
- class Github
29
32
 
30
- def initialize
31
- @client = if Hubba.configuration.token
32
- Client.new( token: Hubba.configuration.token )
33
- elsif Hubba.configuration.user &&
34
- Hubba.configuration.password
35
- Client.new( user: Hubba.configuration.user,
36
- password: Hubba.configuration.password )
37
- else
38
- Client.new
39
- end
33
+ def initialize( token: nil,
34
+ user: nil,
35
+ password: nil )
36
+ @token = nil
37
+ @user = nil
38
+ @password = nil
39
+
40
+ if token ## 1a) give preference to passed in token
41
+ @token = token
42
+ elsif user && password ## 1b) or passed in user/password credentials
43
+ @user = user
44
+ @password = password
45
+ elsif Hubba.config.token ## 2a) followed by configured (or env) token
46
+ @token = Hubba.config.token
47
+ elsif Hubba.config.user && Hubba.config.password ## 2b)
48
+ @user = Hubba.config.user
49
+ @password = Hubba.config.password
50
+ else ## 3)
51
+ ## no token or credentials passed in or configured
52
+ end
40
53
  end
41
54
 
42
55
 
56
+
43
57
  def user( name )
44
58
  Resource.new( get "/users/#{name}" )
45
59
  end
@@ -77,61 +91,120 @@ def repo( full_name ) ## full_name (handle) e.g. henrythemes/jekyll-starter-th
77
91
  Resource.new( get "/repos/#{full_name}" )
78
92
  end
79
93
 
94
+ def repo_languages( full_name )
95
+ Resource.new( get "/repos/#{full_name}/languages" )
96
+ end
97
+
98
+ def repo_topics( full_name )
99
+ ## note: requires "api preview" accept headers (overwrites default v3+json)
100
+ ## e.g. application/vnd.github.mercy-preview+json
101
+ Resource.new( get( "/repos/#{full_name}/topics", preview: 'mercy' ) )
102
+ end
103
+
104
+
80
105
  def repo_commits( full_name )
81
106
  Resource.new( get "/repos/#{full_name}/commits" )
82
107
  end
83
108
 
84
109
 
110
+ def repo_traffic_clones( full_name )
111
+ # Get repository clones
112
+ # Get the total number of clones and breakdown per day or week
113
+ # for the last 14 days.
114
+ # Timestamps are aligned to UTC midnight of the beginning of the day or week.
115
+ # Week begins on Monday.
116
+ Resource.new( get "/repos/#{full_name}/traffic/clones" )
117
+ end
85
118
 
86
- ####
87
- # more
88
- def update( obj )
89
- if obj.is_a?( Stats )
90
- stats = obj
91
- full_name = stats.full_name
92
- puts "[update 1/2] fetching repo >#{full_name}<..."
93
- repo = repo( full_name )
94
- puts "[update 2/2] fetching repo >#{full_name}< commits ..."
95
- commits = repo_commits( full_name )
96
-
97
- stats.update( repo, commits )
98
- else
99
- raise ArgumentError, "unknown source object passed in - expected Hubba::Stats; got #{obj.class.name}"
100
- end
119
+ def repo_traffic_views( full_name )
120
+ # Get page views
121
+ # Get the total number of views and breakdown per day or week
122
+ # for the last 14 days.
123
+ # Timestamps are aligned to UTC midnight of the beginning of the day or week.
124
+ # Week begins on Monday.
125
+ Resource.new( get "/repos/#{full_name}/traffic/views" )
101
126
  end
102
127
 
103
128
 
104
- def update_stats( h ) ## todo/fix: change to Reposet - why? why not???
105
- h.each do |org_with_counter,names|
129
+ def repo_traffic_popular_paths( full_name )
130
+ # Get top referral paths
131
+ # Get the top 10 popular contents over the last 14 days.
132
+ Resource.new( get "/repos/#{full_name}/traffic/popular/paths" )
133
+ end
106
134
 
107
- ## remove optional number from key e.g.
108
- ## mrhydescripts (3) => mrhydescripts
109
- ## footballjs (4) => footballjs
110
- ## etc.
135
+ def repo_traffic_popular_referrers( full_name )
136
+ # Get top referral sources
137
+ # Get the top 10 referrers over the last 14 days.
138
+ Resource.new( get "/repos/#{full_name}/traffic/popular/referrers" )
139
+ end
111
140
 
112
- org = org_with_counter.sub( /\([0-9]+\)/, '' ).strip
113
141
 
114
- ## puts " -- #{key_with_counter} [#{key}] --"
115
142
 
116
- names.each do |name|
117
- full_name = "#{org}/#{name}"
118
143
 
119
- ## puts " fetching stats #{count+1}/#{repo_count} - >#{full_name}<..."
120
- stats = Stats.new( full_name )
121
- stats.read
144
+ private
145
+ def get( request_uri, preview: nil )
146
+
147
+ puts "GET #{request_uri}"
148
+
149
+ ## note: request_uri ALWAYS starts with leading /, thus use + for now!!!
150
+ # e.g. /users/geraldb
151
+ # /users/geraldb/repos
152
+ url = BASE_URL + request_uri
153
+
154
+
155
+ headers = {}
156
+ ## add default headers if nothing (custom) set / passed-in
157
+ headers['User-Agent'] = "ruby/hubba v#{VERSION}" ## required by GitHub API
158
+ headers['Accept'] = if preview # e.g. mercy or ???
159
+ "application/vnd.github.#{preview}-preview+json"
160
+ else
161
+ 'application/vnd.github.v3+json' ## default - recommend by GitHub API
162
+ end
163
+
164
+ auth = []
165
+ ## check if credentials (user/password) present - if yes, use basic auth
166
+ if @token
167
+ puts " using (personal access) token - starting with: #{@token[0..6]}**********"
168
+ headers['Authorization'] = "token #{@token}"
169
+ ## token works like:
170
+ ## curl -H 'Authorization: token my_access_token' https://api.github.com/user/repos
171
+ elsif @user && @password
172
+ puts " using basic auth - user: #{@user}, password: ***"
173
+ ## use credential auth "tuple" (that is, array with two string items) for now
174
+ ## or use Webclient::HttpBasicAuth or something - why? why not?
175
+ auth = [@user, @password]
176
+ # req.basic_auth( @user, @password )
177
+ else
178
+ puts " using no credentials (no token, no user/password)"
179
+ end
122
180
 
123
- update( stats ) ## fetch & update stats
181
+ res = Webclient.get( url,
182
+ headers: headers,
183
+ auth: auth )
124
184
 
125
- stats.write
126
- end
127
- end
128
- end
185
+ # Get specific header
186
+ # response["content-type"]
187
+ # => "text/html; charset=UTF-8"
129
188
 
189
+ # Iterate all response headers.
190
+ # puts "HTTP HEADERS:"
191
+ # res.headers.each do |key, value|
192
+ # puts " #{key}: >#{value}<"
193
+ # end
194
+ # puts
130
195
 
131
- private
132
- def get( request_uri )
133
- @client.get( request_uri )
134
- end
196
+ # => "location => http://www.google.com/"
197
+ # => "content-type => text/html; charset=UTF-8"
198
+ # ...
199
+
200
+ if res.status.ok?
201
+ res.json
202
+ else
203
+ puts "!! HTTP ERROR: #{res.status.code} #{res.status.message}:"
204
+ pp res.raw
205
+ exit 1
206
+ end
207
+ end # method get
135
208
 
136
209
  end # class Github
137
210
  end # module Hubba
@@ -1,100 +1,13 @@
1
1
  module Hubba
2
2
 
3
3
 
4
- def self.update_stats( host_or_path='./repos.yml' ) ## move to reposet e.g. Reposet#update_status!!!!
5
- h = if hash_or_path.is_a?( String ) ## assume it is a file path!!!
6
- path = hash_or_path
7
- YAML.load_file( path )
8
- else
9
- hash_or_path # assume its a hash / reposet already!!!
10
- end
11
-
12
- gh = Github.new
13
- gh.update_stats( h )
14
- end
15
-
16
-
17
- def self.stats( hash_or_path='./repos.yml' ) ## use read_stats or such - why? why not?
18
- h = if hash_or_path.is_a?( String ) ## assume it is a file path!!!
19
- path = hash_or_path
20
- YAML.load_file( path )
21
- else
22
- hash_or_path # assume its a hash / reposet already!!!
23
- end
24
-
25
- Summary.new( h ) ## wrap in "easy-access" facade / wrapper
26
- end
27
-
28
-
29
-
30
- class Summary # todo/check: use a different name e.g (Data)Base, Census, Catalog, Collection, Index, Register or such???
31
-
32
- class Repo ## (nested) class
33
-
34
- attr_reader :owner,
35
- :name
36
-
37
- def initialize( owner, name )
38
- @owner = owner ## rename to login, username - why? why not?
39
- @name = name ## rename to reponame ??
40
- end
41
-
42
- def full_name() "#{owner}/#{name}"; end
43
-
44
- def stats
45
- ## note: load stats on demand only (first access) for now - why? why not?
46
- @stats ||= begin
47
- stats = Stats.new( full_name )
48
- stats.read
49
- stats
50
- end
51
- end
52
-
53
- def diff
54
- @diff ||= stats.calc_diff_stars( samples: 3, days: 30 )
55
- end
56
- end # (nested) class Repo
57
-
58
-
59
- attr_reader :orgs, :repos
60
-
61
- def initialize( hash )
62
- @orgs = [] # orgs and users -todo/check: use better name - logins or owners? why? why not?
63
- @repos = []
64
- add( hash )
65
-
66
- puts "#{@repos.size} repos @ #{@orgs.size} orgs"
67
- end
68
-
69
- #############
70
- ## private helpes
71
- def add( hash ) ## add repos.yml set
72
- hash.each do |org_with_counter, names|
73
- ## remove optional number from key e.g.
74
- ## mrhydescripts (3) => mrhydescripts
75
- ## footballjs (4) => footballjs
76
- ## etc.
77
- org = org_with_counter.sub( /\([0-9]+\)/, '' ).strip
78
- repos = []
79
- names.each do |name|
80
- repo = Repo.new( org, name )
81
- repos << repo
82
- end
83
- @orgs << [org, repos]
84
- @repos += repos
85
- end
86
- end
87
- end # class Summary
88
-
89
-
90
-
91
4
  ## orgs - include repos form org(anizations) too
92
5
  ## cache - save json response to cache_dir - change to/use debug/tmp_dir? - why? why not?
93
6
  def self.reposet( *users, orgs: true,
94
7
  cache: false )
95
8
  # users = [users] if users.is_a?( String ) ### wrap in array if single user
96
9
 
97
- gh = Hubba::Github.new
10
+ gh = Github.new
98
11
 
99
12
  forks = []
100
13
 
@@ -166,5 +79,19 @@ def self.reposet( *users, orgs: true,
166
79
 
167
80
  h
168
81
  end ## method reposet
82
+
83
+
84
+ def self.stats( hash_or_path='./repos.yml' ) ## use read_stats or such - why? why not?
85
+ h = if hash_or_path.is_a?( String ) ## assume it is a file path!!!
86
+ path = hash_or_path
87
+ YAML.load_file( path )
88
+ else
89
+ hash_or_path # assume its a hash / reposet already!!!
90
+ end
91
+
92
+ Folio.new( h ) ## wrap in "easy-access" facade / wrapper
93
+ end ## method stats
94
+
95
+
169
96
  end # module Hubba
170
97
 
data/lib/hubba/stats.rb CHANGED
@@ -223,12 +223,105 @@ module Hubba
223
223
 
224
224
 
225
225
 
226
- ###############################
227
- ## fetch / read / write methods
226
+ ##################
227
+ ## update
228
+
229
+ def update_traffic( clones: nil,
230
+ views: nil,
231
+ paths: nil,
232
+ referrers: nil )
233
+
234
+ traffic = @data[ 'traffic' ] ||= {}
235
+
236
+ summary = traffic['summary'] ||= {}
237
+ history = traffic['history'] ||= {}
238
+
239
+
240
+ if views
241
+ raise ArgumentError, "Github::Resource expected; got #{views.class.name}" unless views.is_a?( Github::Resource )
242
+ =begin
243
+ {"count"=>1526,
244
+ "uniques"=>287,
245
+ "views"=>
246
+ [{"timestamp"=>"2020-09-27T00:00:00Z", "count"=>52, "uniques"=>13},
247
+ {"timestamp"=>"2020-09-28T00:00:00Z", "count"=>108, "uniques"=>28},
248
+ ...
249
+ ]}>
250
+ =end
251
+
252
+ ## keep lastest (summary) record of last two weeks (14 days)
253
+ summary['views'] = { 'count' => views.data['count'],
254
+ 'uniques' => views.data['uniques'] }
255
+
256
+ ## update history / day-by-day items / timeline
257
+ views.data['views'].each do |view|
258
+ # e.g. "2020-09-27T00:00:00Z"
259
+ timestamp = DateTime.strptime( view['timestamp'], '%Y-%m-%dT%H:%M:%S%z' )
260
+
261
+ item = history[ timestamp.strftime( '%Y-%m-%d' ) ] ||= {} ## e.g. 2016-09-27
262
+ ## note: merge "in-place"
263
+ item.merge!( { 'views' => { 'count' => view['count'],
264
+ 'uniques' => view['uniques'] }} )
265
+ end
266
+ end
267
+
268
+ if clones
269
+ raise ArgumentError, "Github::Resource expected; got #{clones.class.name}" unless clones.is_a?( Github::Resource )
270
+ =begin
271
+ {"count"=>51,
272
+ "uniques"=>17,
273
+ "clones"=>
274
+ [{"timestamp"=>"2020-09-26T00:00:00Z", "count"=>1, "uniques"=>1},
275
+ {"timestamp"=>"2020-09-27T00:00:00Z", "count"=>2, "uniques"=>1},
276
+ ...
277
+ ]}
278
+ =end
279
+
280
+ ## keep lastest (summary) record of last two weeks (14 days)
281
+ summary['clones'] = { 'count' => clones.data['count'],
282
+ 'uniques' => clones.data['uniques'] }
283
+
284
+ ## update history / day-by-day items / timeline
285
+ clones.data['clones'].each do |clone|
286
+ # e.g. "2020-09-27T00:00:00Z"
287
+ timestamp = DateTime.strptime( clone['timestamp'], '%Y-%m-%dT%H:%M:%S%z' )
288
+
289
+ item = history[ timestamp.strftime( '%Y-%m-%d' ) ] ||= {} ## e.g. 2016-09-27
290
+ ## note: merge "in-place"
291
+ item.merge!( { 'clones' => { 'count' => clone['count'],
292
+ 'uniques' => clone['uniques'] }} )
293
+ end
294
+ end
228
295
 
229
- def update( repo, commits ) ## update stats / fetch data from github via api
230
- raise ArgumentError, "Hubba::Resource expected; got #{repo.class.name}" unless repo.is_a?( Resource )
231
- raise ArgumentError, "Hubba::Resource expected; got #{commits.class.name}" unless commits.is_a?( Resource )
296
+ if paths
297
+ raise ArgumentError, "Github::Resource expected; got #{paths.class.name}" unless paths.is_a?( Github::Resource )
298
+ =begin
299
+ [{"path"=>"/openfootball/england",
300
+ "title"=>
301
+ "openfootball/england: Free open public domain football data for England (and ...",
302
+ "count"=>394,
303
+ "uniques"=>227},
304
+ =end
305
+ summary['paths'] = paths.data
306
+ end
307
+
308
+ if referrers
309
+ raise ArgumentError, "Github::Resource expected; got #{referrers.class.name}" unless referrers.is_a?( Github::Resource )
310
+ =begin
311
+ [{"referrer"=>"github.com", "count"=>327, "uniques"=>198},
312
+ {"referrer"=>"openfootball.github.io", "count"=>71, "uniques"=>54},
313
+ {"referrer"=>"Google", "count"=>5, "uniques"=>5},
314
+ {"referrer"=>"reddit.com", "count"=>4, "uniques"=>4}]
315
+ =end
316
+ summary['referrers'] = referrers.data
317
+ end
318
+ end # method update_traffic
319
+
320
+
321
+ def update( repo,
322
+ commits: nil,
323
+ topics: nil ) ## update stats / fetch data from github via api
324
+ raise ArgumentError, "Github::Resource expected; got #{repo.class.name}" unless repo.is_a?( Github::Resource )
232
325
 
233
326
  ## e.g. 2015-05-11T20:21:43Z
234
327
  ## puts Time.iso8601( repo.data['created_at'] )
@@ -236,42 +329,82 @@ module Hubba
236
329
  @data['updated_at'] = repo.data['updated_at']
237
330
  @data['pushed_at'] = repo.data['pushed_at']
238
331
 
239
- @data['size'] = repo.data['size'] # size in kb (kilobyte)
332
+ @data['size'] = repo.data['size'] # note: size in kb (kilobyte)
333
+
334
+ @data['description'] = repo.data['description']
335
+ @data['language'] = repo.data['language'] ## note: might be nil!!!
240
336
 
337
+
338
+
339
+ ########################################
340
+ #### history / by date record
241
341
  rec = {}
242
342
 
243
- puts "stargazers_count"
244
- puts repo.data['stargazers_count']
245
343
  rec['stargazers_count'] = repo.data['stargazers_count']
344
+ rec['forks_count'] = repo.data['forks_count']
345
+
246
346
 
247
347
  today = Date.today.strftime( '%Y-%m-%d' ) ## e.g. 2016-09-27
248
348
  puts "add record #{today} to history..."
249
349
  pp rec # check if stargazers_count is a number (NOT a string)
250
350
 
251
- @data[ 'history' ] ||= {}
252
- @data[ 'history' ][ today ] = rec
351
+ history = @data[ 'history' ] ||= {}
352
+ item = history[ today ] ||= {}
353
+ ## note: merge "in-place" (overwrite with new - but keep other key/value pairs if any e.g. pageviews, clones, etc.)
354
+ item.merge!( rec )
355
+
356
+
253
357
 
254
358
  ##########################
255
359
  ## also check / keep track of (latest) commit
256
- puts "last commit/update:"
257
- ## pp commits
258
- commit = {
259
- 'committer' => {
260
- 'date' => commits.data[0]['commit']['committer']['date'],
261
- 'name' => commits.data[0]['commit']['committer']['name']
262
- },
263
- 'author' => {
264
- 'date' => commits.data[0]['commit']['author']['date'],
265
- 'name' => commits.data[0]['commit']['author']['name']
266
- },
267
- 'message' => commits.data[0]['commit']['message']
268
- }
269
-
270
- ## for now store only the latest commit (e.g. a single commit in an array)
271
- @data[ 'commits' ] = [commit]
360
+ if commits
361
+ raise ArgumentError, "Github::Resource expected; got #{commits.class.name}" unless commits.is_a?( Github::Resource )
362
+
363
+ puts "update - last commit:"
364
+ ## pp commits
365
+ commit = {
366
+ 'committer' => {
367
+ 'date' => commits.data[0]['commit']['committer']['date'],
368
+ 'name' => commits.data[0]['commit']['committer']['name']
369
+ },
370
+ 'author' => {
371
+ 'date' => commits.data[0]['commit']['author']['date'],
372
+ 'name' => commits.data[0]['commit']['author']['name']
373
+ },
374
+ 'message' => commits.data[0]['commit']['message']
375
+ }
376
+
377
+ ## for now store only the latest commit (e.g. a single commit in an array)
378
+ @data[ 'commits' ] = [commit]
379
+ end
380
+
381
+ if topics
382
+ raise ArgumentError, "Github::Resource expected; got #{topics.class.name}" unless topics.is_a?( Github::Resource )
383
+
384
+ puts "update - topics:"
385
+ ## e.g.
386
+ # {"names"=>
387
+ # ["opendata",
388
+ # "football",
389
+ # "seriea",
390
+ # "italia",
391
+ # "italy",
392
+ # "juve",
393
+ # "inter",
394
+ # "napoli",
395
+ # "roma",
396
+ # "sqlite"]}
397
+ #
398
+ # {"names"=>[]}
399
+
400
+ @data[ 'topics' ] = topics.data['names']
401
+ end
402
+
272
403
 
273
404
  pp @data
274
405
 
406
+
407
+
275
408
  ## reset (invalidate) cached values from data hash
276
409
  ## use after reading or fetching
277
410
  @cache = {}
@@ -280,6 +413,8 @@ module Hubba
280
413
  end
281
414
 
282
415
 
416
+ ########################################
417
+ ## read / write methods / helpers
283
418
 
284
419
  def write
285
420
  basename = full_name.gsub( '/', '~' ) ## e.g. poole/hyde become poole~hyde
@@ -0,0 +1,44 @@
1
+ module Hubba
2
+
3
+ def self.update_stats( hash_or_path='./repos.yml' ) ## move to reposet e.g. Reposet#update_status!!!!
4
+ h = if hash_or_path.is_a?( String ) ## assume it is a file path!!!
5
+ path = hash_or_path
6
+ YAML.load_file( path )
7
+ else
8
+ hash_or_path # assume its a hash / reposet already!!!
9
+ end
10
+
11
+ gh = Github.new
12
+
13
+ h.each do |org_with_counter,names|
14
+
15
+ ## remove optional number from key e.g.
16
+ ## mrhydescripts (3) => mrhydescripts
17
+ ## footballjs (4) => footballjs
18
+ ## etc.
19
+ org = org_with_counter.sub( /\([0-9]+\)/, '' ).strip
20
+
21
+ ## puts " -- #{key_with_counter} [#{key}] --"
22
+
23
+ names.each do |name|
24
+ full_name = "#{org}/#{name}"
25
+
26
+ ## puts " fetching stats #{count+1}/#{repo_count} - >#{full_name}<..."
27
+ stats = Stats.new( full_name )
28
+ stats.read
29
+
30
+ puts "update >#{full_name}< [1/3] - fetching repo..."
31
+ repo = gh.repo( full_name )
32
+ puts "update >#{full_name}< [2/3] - fetching repo commits ..."
33
+ commits = gh.repo_commits( full_name )
34
+ puts "update >#{full_name}< [3/3] - fetching repo topics ..."
35
+ topics = gh.repo_topics( full_name )
36
+
37
+ stats.update( repo,
38
+ commits: commits,
39
+ topics: topics )
40
+ stats.write
41
+ end
42
+ end
43
+ end
44
+ end # module Hubba
@@ -0,0 +1,52 @@
1
+ module Hubba
2
+
3
+
4
+ ###
5
+ ## note: keep update traffic separate from update (basic) stats
6
+ ## traffic stats require (personal access) token with push access!!
7
+
8
+ def self.update_traffic( hash_or_path='./repos.yml' ) ## move to reposet e.g. Reposet#update_status!!!!
9
+ h = if hash_or_path.is_a?( String ) ## assume it is a file path!!!
10
+ path = hash_or_path
11
+ YAML.load_file( path )
12
+ else
13
+ hash_or_path # assume its a hash / reposet already!!!
14
+ end
15
+
16
+ gh = Github.new
17
+
18
+ h.each do |org_with_counter,names|
19
+
20
+ ## remove optional number from key e.g.
21
+ ## mrhydescripts (3) => mrhydescripts
22
+ ## footballjs (4) => footballjs
23
+ ## etc.
24
+ org = org_with_counter.sub( /\([0-9]+\)/, '' ).strip
25
+
26
+ ## puts " -- #{key_with_counter} [#{key}] --"
27
+
28
+ names.each do |name|
29
+ full_name = "#{org}/#{name}"
30
+
31
+ ## puts " fetching stats #{count+1}/#{repo_count} - >#{full_name}<..."
32
+ stats = Stats.new( full_name )
33
+ stats.read
34
+
35
+ puts "update >#{full_name}< [1/4] - fetching repo traffic clones..."
36
+ clones = gh.repo_traffic_clones( full_name )
37
+ puts "update >#{full_name}< [2/4] - fetching repo traffic views..."
38
+ views = gh.repo_traffic_views( full_name )
39
+ puts "update >#{full_name}< [3/4] - fetching repo traffic popular paths..."
40
+ paths = gh.repo_traffic_popular_paths( full_name )
41
+ puts "update >#{full_name}< [4/4] - fetching repo traffic popular referrers..."
42
+ referrers = gh.repo_traffic_popular_referrers( full_name )
43
+
44
+ stats.update_traffic( clones: clones,
45
+ views: views,
46
+ paths: paths,
47
+ referrers: referrers )
48
+ stats.write
49
+ end
50
+ end
51
+ end
52
+ end # module Hubba
data/lib/hubba/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  module Hubba
2
2
  MAJOR = 0 ## todo: namespace inside version or something - why? why not??
3
3
  MINOR = 6
4
- PATCH = 1
4
+ PATCH = 2
5
5
  VERSION = [MAJOR,MINOR,PATCH].join('.')
6
6
 
7
7
  def self.version
data/lib/hubba.rb CHANGED
@@ -22,12 +22,14 @@ end
22
22
  # our own code
23
23
  require 'hubba/version' # note: let version always go first
24
24
  require 'hubba/config'
25
- require 'hubba/client'
26
25
  require 'hubba/github'
27
26
  require 'hubba/stats'
28
27
 
29
28
  ## "higher level" porcelain services / helpers for easy (re)use
30
- require 'hubba/reposet'
29
+ require 'hubba/folio' ## "access layer" for reports
30
+ require 'hubba/hubba'
31
+ require 'hubba/update'
32
+ require 'hubba/update_traffic'
31
33
 
32
34
  require 'hubba/reports'
33
35
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hubba
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gerald Bauer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-09 00:00:00.000000000 Z
11
+ date: 2020-10-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: webclient
@@ -72,12 +72,14 @@ files:
72
72
  - README.md
73
73
  - Rakefile
74
74
  - lib/hubba.rb
75
- - lib/hubba/client.rb
76
75
  - lib/hubba/config.rb
76
+ - lib/hubba/folio.rb
77
77
  - lib/hubba/github.rb
78
+ - lib/hubba/hubba.rb
78
79
  - lib/hubba/reports.rb
79
- - lib/hubba/reposet.rb
80
80
  - lib/hubba/stats.rb
81
+ - lib/hubba/update.rb
82
+ - lib/hubba/update_traffic.rb
81
83
  - lib/hubba/version.rb
82
84
  - test/helper.rb
83
85
  - test/stats/jekyll~minima.json
data/lib/hubba/client.rb DELETED
@@ -1,82 +0,0 @@
1
- module Hubba
2
-
3
-
4
- class Client
5
-
6
- BASE_URL = 'https://api.github.com'
7
-
8
-
9
- def initialize( token: nil,
10
- user: nil, password: nil )
11
- ## add support for (personal access) token
12
- @token = token
13
-
14
- ## add support for basic auth - defaults to no auth (nil/nil)
15
- ## remove - deprecated (use token) - why? why not?
16
- @user = user ## use login like Oktokit - why? why not?
17
- @password = password
18
- end # method initialize
19
-
20
-
21
-
22
- def get( request_uri )
23
- puts "GET #{request_uri}"
24
-
25
- ## note: request_uri ALWAYS starts with leading /, thus use + for now!!!
26
- # e.g. /users/geraldb
27
- # /users/geraldb/repos
28
- url = BASE_URL + request_uri
29
-
30
- headers = {}
31
- headers['User-Agent'] = 'ruby/hubba' ## required by GitHub API
32
- headers['Accept'] = 'application/vnd.github.v3+json' ## recommend by GitHub API
33
-
34
- auth = []
35
- ## check if credentials (user/password) present - if yes, use basic auth
36
- if @token
37
- puts " using (personal access) token - starting with: #{@token[0..6]}**********"
38
- headers['Authorization'] = "token #{@token}"
39
- ## token works like:
40
- ## curl -H 'Authorization: token my_access_token' https://api.github.com/user/repos
41
- elsif @user && @password
42
- puts " using basic auth - user: #{@user}, password: ***"
43
- ## use credential auth "tuple" (that is, array with two string items) for now
44
- ## or use Webclient::HttpBasicAuth or something - why? why not?
45
- auth = [@user, @password]
46
- # req.basic_auth( @user, @password )
47
- else
48
- puts " using no credentials (no token, no user/password)"
49
- end
50
-
51
- res = Webclient.get( url,
52
- headers: headers,
53
- auth: auth )
54
-
55
- # Get specific header
56
- # response["content-type"]
57
- # => "text/html; charset=UTF-8"
58
-
59
- # Iterate all response headers.
60
- # puts "HTTP HEADERS:"
61
- # res.headers.each do |key, value|
62
- # puts " #{key}: >#{value}<"
63
- # end
64
- # puts
65
-
66
- # => "location => http://www.google.com/"
67
- # => "content-type => text/html; charset=UTF-8"
68
- # ...
69
-
70
- if res.status.ok?
71
- res.json
72
- else
73
- puts "!! HTTP ERROR: #{res.status.code} #{res.status.message}:"
74
- pp res.raw
75
- exit 1
76
- end
77
- end # methdo get
78
-
79
- end ## class Client
80
-
81
-
82
- end # module Hubba