hubba 0.6.1 → 0.6.2

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 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