hubba 0.3.0 → 0.6.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 +4 -4
- data/{HISTORY.md → CHANGELOG.md} +0 -0
- data/Manifest.txt +7 -5
- data/README.md +20 -20
- data/Rakefile +5 -5
- data/lib/hubba.rb +11 -16
- data/lib/hubba/client.rb +46 -20
- data/lib/hubba/config.rb +46 -0
- data/lib/hubba/github.rb +32 -51
- data/lib/hubba/stats.rb +230 -33
- data/lib/hubba/version.rb +1 -3
- data/test/helper.rb +0 -2
- data/test/stats/jekyll~minima.json +25 -0
- data/test/stats/openblockchains~awesome-blockchains.json +27 -0
- data/test/stats/opendatajson~factbook.json.json +39 -0
- data/test/stats/poole~hyde.json +21 -0
- data/test/test_config.rb +10 -2
- data/test/test_stats.rb +106 -21
- data/test/test_stats_tmp.rb +44 -0
- metadata +27 -19
- data/lib/hubba/cache.rb +0 -62
- data/test/cache/users~geraldb~orgs.json +0 -46
- data/test/cache/users~geraldb~repos.json +0 -263
- data/test/test_cache.rb +0 -46
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 63505774303ed6881c5650478eb76a1df7a3d412
|
4
|
+
data.tar.gz: 517173da9e008bd5d9ea74d2e9d8d1622edf7fdf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8a806d4ee314cc13c1605781cbc6af7192600d2c34ec9ebccc949541a800a41d93d43def28600aa0d27e7526a47afa1e319bc2184ec4cee1c917196cc080ca91
|
7
|
+
data.tar.gz: bcc42747f508c0acab55a8f652be1103afef20d9409172081bba34a530a9bbc441a2d5ec109bcd4b43e3049a3b3f74fc1c478d17e9c84a7e7ce41e7dabd11014
|
data/{HISTORY.md → CHANGELOG.md}
RENAMED
File without changes
|
data/Manifest.txt
CHANGED
@@ -1,16 +1,18 @@
|
|
1
|
-
|
1
|
+
CHANGELOG.md
|
2
2
|
Manifest.txt
|
3
3
|
README.md
|
4
4
|
Rakefile
|
5
5
|
lib/hubba.rb
|
6
|
-
lib/hubba/cache.rb
|
7
6
|
lib/hubba/client.rb
|
7
|
+
lib/hubba/config.rb
|
8
8
|
lib/hubba/github.rb
|
9
9
|
lib/hubba/stats.rb
|
10
10
|
lib/hubba/version.rb
|
11
|
-
test/cache/users~geraldb~orgs.json
|
12
|
-
test/cache/users~geraldb~repos.json
|
13
11
|
test/helper.rb
|
14
|
-
test/
|
12
|
+
test/stats/jekyll~minima.json
|
13
|
+
test/stats/openblockchains~awesome-blockchains.json
|
14
|
+
test/stats/opendatajson~factbook.json.json
|
15
|
+
test/stats/poole~hyde.json
|
15
16
|
test/test_config.rb
|
16
17
|
test/test_stats.rb
|
18
|
+
test/test_stats_tmp.rb
|
data/README.md
CHANGED
@@ -1,20 +1,20 @@
|
|
1
|
-
# hubba
|
2
|
-
|
3
|
-
hubba gem - (yet) another (lite) GitHub HTTP API client / library
|
4
|
-
|
5
|
-
* home :: [github.com/
|
6
|
-
* bugs :: [github.com/
|
7
|
-
* gem :: [rubygems.org/gems/hubba](https://rubygems.org/gems/hubba)
|
8
|
-
* rdoc :: [rubydoc.info/gems/hubba](http://rubydoc.info/gems/hubba)
|
9
|
-
|
10
|
-
|
11
|
-
## Usage
|
12
|
-
|
13
|
-
TBD
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
## License
|
18
|
-
|
19
|
-
The `hubba` scripts are dedicated to the public domain.
|
20
|
-
Use it as you please with no restrictions whatsoever.
|
1
|
+
# hubba
|
2
|
+
|
3
|
+
hubba gem - (yet) another (lite) GitHub HTTP API client / library
|
4
|
+
|
5
|
+
* home :: [github.com/rubycoco/git](https://github.com/rubycoco/git)
|
6
|
+
* bugs :: [github.com/rubycoco/git/issues](https://github.com/rubycoco/git/issues)
|
7
|
+
* gem :: [rubygems.org/gems/hubba](https://rubygems.org/gems/hubba)
|
8
|
+
* rdoc :: [rubydoc.info/gems/hubba](http://rubydoc.info/gems/hubba)
|
9
|
+
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
TBD
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
## License
|
18
|
+
|
19
|
+
The `hubba` scripts are dedicated to the public domain.
|
20
|
+
Use it as you please with no restrictions whatsoever.
|
data/Rakefile
CHANGED
@@ -5,26 +5,26 @@ Hoe.spec 'hubba' do
|
|
5
5
|
|
6
6
|
self.version = Hubba::VERSION
|
7
7
|
|
8
|
-
self.summary = 'hubba -
|
8
|
+
self.summary = 'hubba - (yet) another (lite) GitHub HTTP API client / library'
|
9
9
|
self.description = summary
|
10
10
|
|
11
|
-
self.urls =
|
11
|
+
self.urls = { home: 'https://github.com/rubycoco/git' }
|
12
12
|
|
13
13
|
self.author = 'Gerald Bauer'
|
14
14
|
self.email = 'ruby-talk@ruby-lang.org'
|
15
15
|
|
16
16
|
# switch extension to .markdown for gihub formatting
|
17
17
|
self.readme_file = 'README.md'
|
18
|
-
self.history_file = '
|
18
|
+
self.history_file = 'CHANGELOG.md'
|
19
19
|
|
20
20
|
self.extra_deps = [
|
21
|
-
['
|
21
|
+
['webclient', '>= 0.1.1']
|
22
22
|
]
|
23
23
|
|
24
24
|
self.licenses = ['Public Domain']
|
25
25
|
|
26
26
|
self.spec_extras = {
|
27
|
-
required_ruby_version: '>= 2.
|
27
|
+
required_ruby_version: '>= 2.2.2'
|
28
28
|
}
|
29
29
|
|
30
30
|
end
|
data/lib/hubba.rb
CHANGED
@@ -1,25 +1,20 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
require 'net/http'
|
4
|
-
require "net/https"
|
5
|
-
require 'uri'
|
6
|
-
|
7
|
-
require 'pp'
|
8
|
-
require 'json'
|
9
|
-
require 'yaml'
|
10
|
-
require 'time'
|
11
|
-
|
12
|
-
|
13
|
-
# 3rd party gems/libs
|
14
|
-
require 'logutils'
|
1
|
+
# 3rd party (our own)
|
2
|
+
require 'webclient'
|
15
3
|
|
16
4
|
# our own code
|
17
5
|
require 'hubba/version' # note: let version always go first
|
18
|
-
require 'hubba/
|
6
|
+
require 'hubba/config'
|
19
7
|
require 'hubba/client'
|
20
8
|
require 'hubba/github'
|
21
9
|
require 'hubba/stats'
|
22
10
|
|
23
11
|
|
12
|
+
############
|
13
|
+
# add convenience alias for alternate spelling - why? why not?
|
14
|
+
module Hubba
|
15
|
+
GitHub = Github
|
16
|
+
end
|
17
|
+
|
18
|
+
|
24
19
|
# say hello
|
25
|
-
puts Hubba.banner if defined?($
|
20
|
+
puts Hubba.banner if defined?($RUBYCOCO_DEBUG)
|
data/lib/hubba/client.rb
CHANGED
@@ -1,53 +1,79 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
1
|
module Hubba
|
4
2
|
|
5
3
|
|
6
4
|
class Client
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
13
|
|
14
14
|
## add support for basic auth - defaults to no auth (nil/nil)
|
15
|
+
## remove - deprecated (use token) - why? why not?
|
15
16
|
@user = user ## use login like Oktokit - why? why not?
|
16
17
|
@password = password
|
17
18
|
end # method initialize
|
18
19
|
|
20
|
+
|
21
|
+
|
19
22
|
def get( request_uri )
|
20
23
|
puts "GET #{request_uri}"
|
21
24
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
27
33
|
|
34
|
+
auth = []
|
28
35
|
## check if credentials (user/password) present - if yes, use basic auth
|
29
|
-
if @
|
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
|
30
42
|
puts " using basic auth - user: #{@user}, password: ***"
|
31
|
-
|
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)"
|
32
49
|
end
|
33
50
|
|
34
|
-
res =
|
51
|
+
res = Webclient.get( url,
|
52
|
+
headers: headers,
|
53
|
+
auth: auth )
|
35
54
|
|
36
55
|
# Get specific header
|
37
56
|
# response["content-type"]
|
38
57
|
# => "text/html; charset=UTF-8"
|
39
58
|
|
40
59
|
# Iterate all response headers.
|
41
|
-
|
42
|
-
|
60
|
+
puts "HTTP HEADERS:"
|
61
|
+
res.headers.each do |key, value|
|
62
|
+
puts " #{key}: >#{value}<"
|
43
63
|
end
|
64
|
+
puts
|
65
|
+
|
44
66
|
# => "location => http://www.google.com/"
|
45
67
|
# => "content-type => text/html; charset=UTF-8"
|
46
68
|
# ...
|
47
69
|
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
51
77
|
end # methdo get
|
52
78
|
|
53
79
|
end ## class Client
|
data/lib/hubba/config.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Hubba
|
2
|
+
|
3
|
+
class Configuration
|
4
|
+
def data_dir() @data_dir || './data'; end
|
5
|
+
def data_dir=( value ) @data_dir = value; end
|
6
|
+
|
7
|
+
# try default setup via ENV variables
|
8
|
+
def token() @token || ENV[ 'HUBBA_TOKEN' ]; end
|
9
|
+
def token=( value ) @token = value; end
|
10
|
+
|
11
|
+
# todo/check: use HUBBA_LOGIN - why? why not?
|
12
|
+
def user() @user || ENV[ 'HUBBA_USER' ]; end
|
13
|
+
def password() @password || ENV[ 'HUBBA_PASSWORD' ]; end
|
14
|
+
def user=( value ) @user = value; end
|
15
|
+
def password=( value ) @password = value; end
|
16
|
+
|
17
|
+
end # class Configuration
|
18
|
+
|
19
|
+
|
20
|
+
## lets you use
|
21
|
+
## Hubba.configure do |config|
|
22
|
+
## config.token = 'secret'
|
23
|
+
## -or-
|
24
|
+
## config.user = 'testdada'
|
25
|
+
## config.password = 'secret'
|
26
|
+
## end
|
27
|
+
##
|
28
|
+
## move configure block to GitHub class - why? why not?
|
29
|
+
## e.g. GitHub.configure do |config|
|
30
|
+
## ...
|
31
|
+
## end
|
32
|
+
|
33
|
+
|
34
|
+
def self.configuration
|
35
|
+
@configuration ||= Configuration.new
|
36
|
+
end
|
37
|
+
class << self
|
38
|
+
alias_method :config, :configuration
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def self.configure
|
43
|
+
yield( configuration )
|
44
|
+
end
|
45
|
+
|
46
|
+
end # module Hubba
|
data/lib/hubba/github.rb
CHANGED
@@ -1,38 +1,5 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
1
|
module Hubba
|
4
2
|
|
5
|
-
class Configuration
|
6
|
-
attr_accessor :user
|
7
|
-
attr_accessor :password
|
8
|
-
|
9
|
-
def initialize
|
10
|
-
# try default setup via ENV variables
|
11
|
-
@user = ENV[ 'HUBBA_USER' ] ## use HUBBA_LOGIN - why? why not?
|
12
|
-
@password = ENV[ 'HUBBA_PASSWORD' ]
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
## lets you use
|
17
|
-
## Hubba.configure do |config|
|
18
|
-
## config.user = 'testdada'
|
19
|
-
## config.password = 'secret'
|
20
|
-
## end
|
21
|
-
##
|
22
|
-
## move configure block to GitHub class - why? why not?
|
23
|
-
## e.g. GitHub.configure do |config|
|
24
|
-
## ...
|
25
|
-
## end
|
26
|
-
|
27
|
-
|
28
|
-
def self.configuration
|
29
|
-
@configuration ||= Configuration.new
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.configure
|
33
|
-
yield( configuration )
|
34
|
-
end
|
35
|
-
|
36
3
|
|
37
4
|
class Resource
|
38
5
|
attr_reader :data
|
@@ -53,25 +20,25 @@ class Orgs < Resource
|
|
53
20
|
## sort by name
|
54
21
|
data.map { |item| item['login'] }.sort
|
55
22
|
end
|
23
|
+
alias_method :names, :logins ## add name alias - why? why not?
|
56
24
|
end
|
57
25
|
|
58
26
|
|
59
27
|
|
60
28
|
class Github
|
61
29
|
|
62
|
-
def initialize
|
63
|
-
@
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
68
40
|
end
|
69
41
|
|
70
|
-
def offline!() @offline = true; end ## switch to offline - todo: find a "better" way - why? why not?
|
71
|
-
def online!() @offline = false; end
|
72
|
-
def offline?() @offline == true; end
|
73
|
-
def online?() @offline == false; end
|
74
|
-
|
75
42
|
|
76
43
|
def user( name )
|
77
44
|
Resource.new( get "/users/#{name}" )
|
@@ -95,6 +62,7 @@ def user_orgs( name )
|
|
95
62
|
end
|
96
63
|
|
97
64
|
|
65
|
+
|
98
66
|
def org( name )
|
99
67
|
Resource.new( get "/orgs/#{name}" )
|
100
68
|
end
|
@@ -114,15 +82,28 @@ def repo_commits( full_name )
|
|
114
82
|
end
|
115
83
|
|
116
84
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
85
|
+
|
86
|
+
####
|
87
|
+
# more
|
88
|
+
def update( obj )
|
89
|
+
if obj.is_a?( Stats )
|
90
|
+
full_name = obj.full_name
|
91
|
+
puts "[update 1/2] fetching repo >#{full_name}<..."
|
92
|
+
repo = repo( full_name )
|
93
|
+
puts "[update 2/2] fetching repo >#{full_name}< commits ..."
|
94
|
+
commits = repo_commits( full_name )
|
95
|
+
|
96
|
+
obj.update( repo, commits )
|
121
97
|
else
|
122
|
-
|
98
|
+
raise ArgumentError, "unknown source object passed in - expected Hubba::Stats; got #{obj.class.name}"
|
123
99
|
end
|
124
100
|
end
|
125
101
|
|
126
|
-
end # class Github
|
127
102
|
|
128
|
-
|
103
|
+
private
|
104
|
+
def get( request_uri )
|
105
|
+
@client.get( request_uri )
|
106
|
+
end
|
107
|
+
|
108
|
+
end # class Github
|
109
|
+
end # module Hubba
|
data/lib/hubba/stats.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
1
|
module Hubba
|
4
2
|
|
5
3
|
####
|
@@ -12,47 +10,225 @@ module Hubba
|
|
12
10
|
def initialize( full_name )
|
13
11
|
@data = {}
|
14
12
|
@data['full_name'] = full_name # e.g. poole/hyde etc.
|
13
|
+
|
14
|
+
@cache = {}
|
15
15
|
end
|
16
16
|
|
17
|
-
def full_name() @data['full_name']; end
|
18
17
|
|
19
|
-
def
|
20
|
-
|
21
|
-
def pushed_at() @data['pushed_at']; end
|
18
|
+
def full_name() @data['full_name']; end
|
19
|
+
|
22
20
|
|
23
|
-
|
21
|
+
## note: return datetime objects (NOT strings); if not present/available return nil/null
|
22
|
+
def created_at() @cache['created_at'] ||= parse_datetime( @data['created_at'] ); end
|
23
|
+
def updated_at() @cache['updated_at'] ||= parse_datetime( @data['updated_at'] ); end
|
24
|
+
def pushed_at() @cache['pushed_at'] ||= parse_datetime( @data['pushed_at'] ); end
|
24
25
|
|
26
|
+
## date (only) versions
|
27
|
+
def created() @cache['created'] ||= parse_date( @data['created_at'] ); end
|
28
|
+
def updated() @cache['updated'] ||= parse_date( @data['updated_at'] ); end
|
29
|
+
def pushed() @cache['pushed'] ||= parse_date( @data['pushed_at'] ); end
|
25
30
|
|
26
31
|
def size
|
27
32
|
# size of repo in kb (as reported by github api)
|
28
33
|
@data['size'] || 0 ## return 0 if not found - why? why not? (return nil - why? why not??)
|
29
34
|
end
|
30
35
|
|
36
|
+
|
37
|
+
def history
|
38
|
+
@cache['history'] ||= begin
|
39
|
+
if @data['history']
|
40
|
+
build_history( @data['history'] )
|
41
|
+
else
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
31
48
|
def stars
|
32
49
|
## return last stargazers_count entry (as number; 0 if not found)
|
33
|
-
|
50
|
+
@cache['stars'] ||= history ? history[0].stars : 0
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def commits() @data['commits']; end
|
55
|
+
|
56
|
+
def last_commit ## convenience shortcut; get first/last commit (use [0]) or nil
|
57
|
+
if @data['commits'] && @data['commits'][0]
|
58
|
+
@data['commits'][0]
|
59
|
+
else
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def committed ## last commit date (from author NOT committer)
|
66
|
+
@cache['committed'] ||= parse_date( last_commit_author_date )
|
67
|
+
end
|
68
|
+
|
69
|
+
def committed_at() ## last commit date (from author NOT committer)
|
70
|
+
@cache['committed_at'] ||= parse_datetime( last_commit_author_date )
|
71
|
+
end
|
72
|
+
|
73
|
+
def last_commit_author_date
|
74
|
+
h = last_commit
|
75
|
+
h ? h['author']['date'] : nil
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def last_commit_message ## convenience shortcut; last commit message
|
80
|
+
h = last_commit
|
81
|
+
|
82
|
+
committer_name = h['committer']['name']
|
83
|
+
author_name = h['author']['name']
|
84
|
+
message = h['message']
|
85
|
+
|
86
|
+
buf = ""
|
87
|
+
buf << message
|
88
|
+
buf << " by #{author_name}"
|
89
|
+
|
90
|
+
if committer_name != author_name
|
91
|
+
buf << " w/ #{committer_name}"
|
92
|
+
end
|
93
|
+
end # method commit_message
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
###
|
98
|
+
# helpers
|
99
|
+
def parse_datetime( str ) str ? DateTime.strptime( str, '%Y-%m-%dT%H:%M:%S') : nil; end
|
100
|
+
def parse_date( str ) str ? Date.strptime( str, '%Y-%m-%d') : nil; end
|
101
|
+
|
102
|
+
########
|
103
|
+
## build history items (structs)
|
104
|
+
|
105
|
+
class HistoryItem
|
106
|
+
|
107
|
+
attr_reader :date, :stars ## read-only attributes
|
108
|
+
attr_accessor :prev, :next ## read/write attributes (for double linked list/nodes/items)
|
109
|
+
|
110
|
+
def initialize( date:, stars: )
|
111
|
+
@date = date
|
112
|
+
@stars = stars
|
113
|
+
@next = nil
|
114
|
+
end
|
115
|
+
|
116
|
+
## link items (append item at the end/tail)
|
117
|
+
def append( item )
|
118
|
+
@next = item
|
119
|
+
item.prev = self
|
120
|
+
end
|
121
|
+
|
122
|
+
def diff_days
|
123
|
+
if @next
|
124
|
+
## note: use jd=julian days for calculation
|
125
|
+
@date.jd - @next.date.jd
|
126
|
+
else
|
127
|
+
nil ## last item (tail)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def diff_stars
|
132
|
+
if @next
|
133
|
+
@stars - @next.stars
|
134
|
+
else
|
135
|
+
nil ## last item (tail)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end ## class HistoryItem
|
139
|
+
|
140
|
+
|
141
|
+
def build_history( timeseries )
|
142
|
+
items = []
|
143
|
+
|
144
|
+
keys = timeseries.keys.sort.reverse ## newest (latest) items first
|
145
|
+
keys.each do |key|
|
146
|
+
h = timeseries[ key ]
|
147
|
+
|
148
|
+
item = HistoryItem.new(
|
149
|
+
date: Date.strptime( key, '%Y-%m-%d' ),
|
150
|
+
stars: h['stargazers_count'] || 0 )
|
151
|
+
|
152
|
+
## link items
|
153
|
+
last_item = items[-1]
|
154
|
+
last_item.append( item ) if last_item ## if not nil? append (note first item has no prev item)
|
155
|
+
|
156
|
+
items << item
|
157
|
+
end
|
158
|
+
|
159
|
+
## todo/check: return [] for empty items array (items.empty?) - why?? why not??
|
160
|
+
if items.empty?
|
161
|
+
nil
|
162
|
+
else
|
163
|
+
items
|
164
|
+
end
|
165
|
+
end ## method build_history
|
34
166
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
167
|
+
|
168
|
+
|
169
|
+
def calc_diff_stars( samples: 3, days: 30 )
|
170
|
+
## samples: use n history item samples e.g. 3 samples
|
171
|
+
## days e.g. 7 days (per week), 30 days (per month)
|
172
|
+
|
173
|
+
if history.nil?
|
174
|
+
nil ## todo/check: return 0.0 too - why? why not?
|
175
|
+
elsif history.size == 1
|
176
|
+
## just one item; CANNOT calc diff; return zero
|
177
|
+
0.0
|
178
|
+
else
|
179
|
+
idx = [history.size, samples].min ## calc last index
|
180
|
+
last = history[idx-1]
|
181
|
+
first = history[0]
|
182
|
+
|
183
|
+
diff_days = first.date.jd - last.date.jd
|
184
|
+
diff_stars = first.stars - last.stars
|
185
|
+
|
186
|
+
## note: use factor 1000 for fixed integer division
|
187
|
+
## converts to float at the end
|
188
|
+
|
189
|
+
## todo: check for better way (convert to float upfront - why? why not?)
|
190
|
+
|
191
|
+
diff = (diff_stars * days * 1000) / diff_days
|
192
|
+
puts "diff=#{diff}:#{diff.class.name}" ## check if it's a float
|
193
|
+
(diff.to_f/1000.0)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
def history_str ## todo/check: rename/change to format_history or fmt_history - why? why not?
|
199
|
+
## returns "pretty printed" history as string buffer
|
200
|
+
buf = ''
|
201
|
+
buf << "[#{history.size}]: "
|
202
|
+
|
203
|
+
history.each do |item|
|
204
|
+
buf << "#{item.stars}"
|
205
|
+
|
206
|
+
diff_stars = item.diff_stars
|
207
|
+
diff_days = item.diff_days
|
208
|
+
if diff_stars && diff_days ## note: last item has no diffs
|
209
|
+
if diff_stars > 0 || diff_stars < 0
|
210
|
+
if diff_stars > 0
|
211
|
+
buf << " (+#{diff_stars}"
|
212
|
+
else
|
213
|
+
buf << " (#{diff_stars}"
|
214
|
+
end
|
215
|
+
buf << " in #{diff_days}d) "
|
216
|
+
else ## diff_stars == 0
|
217
|
+
buf << " (#{diff_days}d) "
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
buf
|
222
|
+
end # method history_str
|
47
223
|
|
48
224
|
|
49
225
|
|
50
226
|
###############################
|
51
227
|
## fetch / read / write methods
|
52
228
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
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 )
|
56
232
|
|
57
233
|
## e.g. 2015-05-11T20:21:43Z
|
58
234
|
## puts Time.iso8601( repo.data['created_at'] )
|
@@ -77,7 +253,6 @@ module Hubba
|
|
77
253
|
|
78
254
|
##########################
|
79
255
|
## also check / keep track of (latest) commit
|
80
|
-
commits = gh.repo_commits( full_name )
|
81
256
|
puts "last commit/update:"
|
82
257
|
## pp commits
|
83
258
|
commit = {
|
@@ -85,36 +260,58 @@ module Hubba
|
|
85
260
|
'date' => commits.data[0]['commit']['committer']['date'],
|
86
261
|
'name' => commits.data[0]['commit']['committer']['name']
|
87
262
|
},
|
263
|
+
'author' => {
|
264
|
+
'date' => commits.data[0]['commit']['author']['date'],
|
265
|
+
'name' => commits.data[0]['commit']['author']['name']
|
266
|
+
},
|
88
267
|
'message' => commits.data[0]['commit']['message']
|
89
268
|
}
|
90
269
|
|
91
270
|
## for now store only the latest commit (e.g. a single commit in an array)
|
92
|
-
@data[ 'commits'] = [commit]
|
271
|
+
@data[ 'commits' ] = [commit]
|
93
272
|
|
94
273
|
pp @data
|
274
|
+
|
275
|
+
## reset (invalidate) cached values from data hash
|
276
|
+
## use after reading or fetching
|
277
|
+
@cache = {}
|
278
|
+
|
279
|
+
self ## return self for (easy chaining)
|
95
280
|
end
|
96
281
|
|
97
282
|
|
98
283
|
|
99
|
-
def write
|
284
|
+
def write
|
100
285
|
basename = full_name.gsub( '/', '~' ) ## e.g. poole/hyde become poole~hyde
|
101
|
-
|
286
|
+
data_dir = Hubba.config.data_dir
|
287
|
+
puts "writing stats to #{basename} (#{data_dir})..."
|
288
|
+
|
289
|
+
## todo/fix: add FileUtils.makepath_r or such!!!
|
102
290
|
File.open( "#{data_dir}/#{basename}.json", 'w:utf-8' ) do |f|
|
103
291
|
f.write JSON.pretty_generate( data )
|
104
292
|
end
|
293
|
+
self ## return self for (easy chaining)
|
105
294
|
end
|
106
295
|
|
107
|
-
|
296
|
+
|
297
|
+
def read
|
108
298
|
## note: skip reading if file not present
|
109
299
|
basename = full_name.gsub( '/', '~' ) ## e.g. poole/hyde become poole~hyde
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
300
|
+
data_dir = Hubba.config.data_dir
|
301
|
+
path = "#{data_dir}/#{basename}.json"
|
302
|
+
|
303
|
+
if File.exist?( path )
|
304
|
+
puts "reading stats from #{basename} (#{data_dir})..."
|
305
|
+
json = File.open( path, 'r:utf-8' ) { |f| f.read }
|
114
306
|
@data = JSON.parse( json )
|
307
|
+
|
308
|
+
## reset (invalidate) cached values from data hash
|
309
|
+
## use after reading or fetching
|
310
|
+
@cache = {}
|
115
311
|
else
|
116
|
-
puts "skipping reading
|
312
|
+
puts "!! WARN: - skipping reading stats from #{basename} -- file not found"
|
117
313
|
end
|
314
|
+
self ## return self for (easy chaining)
|
118
315
|
end
|
119
316
|
|
120
317
|
end # class Stats
|