dash-mario 0.16 → 0.17
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.
- data/CHANGELOG +11 -0
- data/README.rdoc +22 -15
- data/dash-mario.gemspec +1 -1
- data/lib/dash-fu/mario.rb +56 -5
- data/lib/dash-fu/marios/backtweets.rb +17 -25
- data/lib/dash-fu/marios/github.rb +55 -62
- data/lib/dash-fu/marios/github_issues.rb +43 -44
- data/lib/dash-fu/marios/ruby_gems.rb +28 -29
- data/test/backtweets_test.rb +53 -57
- data/test/cassettes/backtweets.yml +17 -5
- data/test/github_issues_test.rb +82 -82
- data/test/github_test.rb +95 -94
- data/test/helpers/source.rb +36 -20
- data/test/ruby_gems_test.rb +11 -11
- data/test/setup.rb +1 -1
- data/test/test.log +18 -0
- metadata +4 -4
data/CHANGELOG
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
2010-09-12 v0.17 API change to callback and HTTP access
|
2
|
+
|
3
|
+
Changed API to use callback object instead of block with too many argument
|
4
|
+
combinations.
|
5
|
+
|
6
|
+
Clarified and validating that source state can only use alphanumeric/underscore
|
7
|
+
in field names.
|
8
|
+
|
9
|
+
Added wrapper around HTTP API to make it easier to switch to asynchronous
|
10
|
+
processing later on.
|
11
|
+
|
1
12
|
2010-09-08 v0.16 To name a source, set source.name
|
2
13
|
|
3
14
|
During setup, name the source by setting source.name, not metric.name.
|
data/README.rdoc
CHANGED
@@ -41,6 +41,11 @@ For metric.columns, each item can use the following keys:
|
|
41
41
|
* id -- Column identifier (if missing, derived from column name)
|
42
42
|
* name -- Column name (required)
|
43
43
|
|
44
|
+
Note: any state information you want to store can only use field names with
|
45
|
+
alphanumeric charactercs and underscore. Periods and all other special
|
46
|
+
characters are reserved for special field names that are not stored as part of
|
47
|
+
the state.
|
48
|
+
|
44
49
|
Shortly after setup, the validate method is called with the same context. If it
|
45
50
|
raises any exception, the error is displayed to the user and the source is
|
46
51
|
discarded. Otherwise, register is called with a Webhook URL. Some Marios use
|
@@ -50,25 +55,27 @@ that to register the Webhook with another service.
|
|
50
55
|
== Updates
|
51
56
|
|
52
57
|
Periodically, the Mario will be asked to update the source by calling the update
|
53
|
-
method. This method is called with the same context and
|
54
|
-
from a webhook, then the second argument to update is a Rack::Request
|
58
|
+
method. This method is called with the same context and callbacks. If data
|
59
|
+
comes from a webhook, then the second argument to update is a Rack::Request
|
60
|
+
object.
|
55
61
|
|
56
|
-
The
|
57
|
-
|
58
|
-
|
62
|
+
The callback object passed to update can be used to update the source by calling
|
63
|
+
one of its many methods. A single update may call any combination of methods,
|
64
|
+
e.g. increasing a metric and recording an activity.
|
59
65
|
|
60
|
-
The
|
66
|
+
The callback methods exposed by the callback object are:
|
61
67
|
|
62
|
-
* set -- Columns to set (metric). Records the most recent value for this
|
63
|
-
metric.
|
64
|
-
are integers or floats.
|
65
|
-
* inc -- Columns to increment (metric). Records a change in value which may
|
66
|
-
be positive or negative. This is a hash where the keys are column ids (or
|
68
|
+
* set! -- Columns to set (metric). Records the most recent value for this
|
69
|
+
metric. The single argument is a hash where the keys are column ids (or
|
67
70
|
indexes), the values are integers or floats.
|
68
|
-
*
|
69
|
-
|
70
|
-
|
71
|
-
|
71
|
+
* inc! -- Columns to increment (metric). Records a change in value which may
|
72
|
+
be positive or negative. The single argument is a hash where the keys are
|
73
|
+
column ids (or indexes), the values are integers or floats.
|
74
|
+
* activity! -- Records a single activity. The single argument is a hash with
|
75
|
+
various values described below.
|
76
|
+
* error! -- Records a processing error. The single agrument is an error message.
|
77
|
+
|
78
|
+
An activity is specified using the following fields:
|
72
79
|
|
73
80
|
* uid -- Unique identifier (within the scope of this source). For example,
|
74
81
|
if activity is a release, this could be the version number.
|
data/dash-mario.gemspec
CHANGED
data/lib/dash-fu/mario.rb
CHANGED
@@ -99,11 +99,11 @@ module DashFu
|
|
99
99
|
end
|
100
100
|
|
101
101
|
# Called to update the source. This method will be called periodically with
|
102
|
-
# a source and a
|
103
|
-
# source, Rack::Request and a block. It can
|
104
|
-
# times with any combination of the supported named arguments for
|
105
|
-
# the source.
|
106
|
-
def update(source, request,
|
102
|
+
# a source and a callback. When triggered by a Webhook, it will be called
|
103
|
+
# with source, Rack::Request and a block. It can invoke callback methods any
|
104
|
+
# number of times with any combination of the supported named arguments for
|
105
|
+
# updating the source.
|
106
|
+
def update(source, request, callbacks)
|
107
107
|
end
|
108
108
|
|
109
109
|
# Called to unregister a Webhook (for sources that don't need it).
|
@@ -118,6 +118,57 @@ module DashFu
|
|
118
118
|
[]
|
119
119
|
end
|
120
120
|
|
121
|
+
# Use this to make HTTP request to external services. This method opens a
|
122
|
+
# new session and yields to the block. The block can them make HTTP requests
|
123
|
+
# on the remote host. The block may be called asynchronously.
|
124
|
+
def session(host, port = 80)
|
125
|
+
http = Net::HTTP.new(host, port)
|
126
|
+
http.use_ssl = true if port == 443
|
127
|
+
http.start do |http|
|
128
|
+
yield Session.new(http)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# HTTP Session.
|
133
|
+
class Session
|
134
|
+
def initialize(http) #:nodoc:
|
135
|
+
@http = http
|
136
|
+
end
|
137
|
+
|
138
|
+
# Make a GET request and yield response to the block. Response consists of
|
139
|
+
# three arguments: status code, response body and response headers. The
|
140
|
+
# block may be called asynchronoulsy.
|
141
|
+
def get(path, headers = {}, &block)
|
142
|
+
response = @http.request(get_request(path, headers))
|
143
|
+
yield response.code, response.body, {}
|
144
|
+
end
|
145
|
+
|
146
|
+
# Make a GET request and yield response to the block. If the response
|
147
|
+
# status is 200 the second argument is the response JSON object. The block
|
148
|
+
# may be called asynchronously.
|
149
|
+
def get_json(path, headers = {}, &block)
|
150
|
+
response = @http.request(get_request(path, headers))
|
151
|
+
if Net::HTTPOK === response
|
152
|
+
json = JSON.parse(response.body) rescue nil
|
153
|
+
if json
|
154
|
+
yield response.code.to_i, json, {}
|
155
|
+
else
|
156
|
+
yield 500, "Not a JSON document", {}
|
157
|
+
end
|
158
|
+
else
|
159
|
+
yield response.code.to_i, response.message, {}
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
protected
|
164
|
+
|
165
|
+
def get_request(path, headers)
|
166
|
+
request = Net::HTTP::Get.new(path)
|
167
|
+
request.basic_auth headers[:username], headers[:password] if headers[:username] && headers[:password]
|
168
|
+
request
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
121
172
|
protected
|
122
173
|
|
123
174
|
# Source identifier.
|
@@ -6,7 +6,7 @@ module DashFu::Mario
|
|
6
6
|
def setup(source, params)
|
7
7
|
url = params["url"].strip.downcase.sub(/^http(s?):\/\//, "")
|
8
8
|
source["source.name"] = "Tweets for #{url}"
|
9
|
-
source["metric.columns"] = [{ :
|
9
|
+
source["metric.columns"] = [{ id: "tweets", label: "Tweets" }]
|
10
10
|
source["metric.totals"] = true
|
11
11
|
source["url"] = url
|
12
12
|
end
|
@@ -15,43 +15,35 @@ module DashFu::Mario
|
|
15
15
|
raise "Missing URL" if source["url"].blank?
|
16
16
|
end
|
17
17
|
|
18
|
-
def update(source, request,
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
unless json
|
24
|
-
logger.error "Backtweets: #{response.code} #{response.message}"
|
25
|
-
raise "Last request didn't go as expected, trying again later"
|
26
|
-
end
|
27
|
-
|
28
|
-
if tweets = json["tweets"]
|
29
|
-
last_tweet_id = source["last_tweet_id"]
|
30
|
-
if last_tweet_id
|
18
|
+
def update(source, request, callback)
|
19
|
+
session "backtweets.com" do |http|
|
20
|
+
http.get_json "/search.json?key=#{api_key}&q=#{Rack::Utils.escape source["url"]}" do |status, json|
|
21
|
+
if status == 200 && tweets = json["tweets"]
|
22
|
+
last_tweet_id = source["last_tweet_id"]
|
31
23
|
new_ones = tweets.take_while { |tweet| tweet["tweet_id"] != last_tweet_id }.reverse
|
32
24
|
new_ones.each do |tweet|
|
33
|
-
|
34
|
-
url = "http://twitter.com/#{
|
25
|
+
screen_name, id = tweet["tweet_from_user"], tweet["tweet_id"]
|
26
|
+
url = "http://twitter.com/#{screen_name}/#{id}"
|
35
27
|
html = <<-HTML
|
36
28
|
<a href="#{url}">tweeted</a>:
|
37
29
|
<blockquote>#{tweet["tweet_text"]}</blockquote>
|
38
30
|
HTML
|
39
|
-
person = { :
|
40
|
-
|
41
|
-
|
31
|
+
person = { fullname: screen_name, identities: %W{twitter.com:#{screen_name}},
|
32
|
+
photo_url: "http://img.tweetimag.es/i/#{screen_name}_n" }
|
33
|
+
callback.activity! uid: id, url: url, html: html, tags: %w{twitter mention},
|
34
|
+
timestamp: Time.parse(tweet["tweet_created_at"]).utc, person: person
|
35
|
+
source["last_tweet_id"] = tweet["tweet_id"]
|
42
36
|
end
|
43
|
-
|
44
|
-
|
45
|
-
|
37
|
+
callback.set! tweets: json["totalresults"].to_i
|
38
|
+
else
|
39
|
+
callback.error! "Last request didn't go as expected, trying again later"
|
46
40
|
end
|
47
41
|
end
|
48
|
-
|
49
|
-
block.call :set=>{ :tweets=>json["totalresults"].to_i }
|
50
42
|
end
|
51
43
|
end
|
52
44
|
|
53
45
|
def meta(source)
|
54
|
-
[ { :
|
46
|
+
[ { text: "Search yourself", url: "http://backtweets.com/search?q=#{URI.escape source["url"]}" } ]
|
55
47
|
end
|
56
48
|
end
|
57
49
|
end
|
@@ -6,7 +6,7 @@ module DashFu::Mario
|
|
6
6
|
def setup(source, params)
|
7
7
|
repo = params["repo"].strip
|
8
8
|
source["source.name"] = "Github: #{repo}"
|
9
|
-
source["metric.columns"] = [{ :
|
9
|
+
source["metric.columns"] = [{ id: "commits", label: "Commits" }, { id: "watchers", label: "Watchers" }, { id: "forks", label: "Forks" }]
|
10
10
|
source["metric.totals"] = true
|
11
11
|
source["repo"] = repo
|
12
12
|
branch = params["branch"].strip
|
@@ -20,80 +20,73 @@ module DashFu::Mario
|
|
20
20
|
raise "Need user name and repository name, e.g. assaf/vanity" unless source["repo"][/^[\w-]+\/[\w-]+$/]
|
21
21
|
end
|
22
22
|
|
23
|
-
def update(source, request,
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
error = true
|
41
|
-
end
|
42
|
-
|
43
|
-
case response = http_request(http, source, "/api/v2/json/commits/list/:repo/:branch")
|
44
|
-
when Net::HTTPOK
|
45
|
-
commits = JSON.parse(response.body)["commits"] rescue nil
|
46
|
-
when Net::HTTPNotFound, Net::HTTPBadRequest
|
47
|
-
raise "Could not find the branch #{source["branch"]}"
|
23
|
+
def update(source, request, callback)
|
24
|
+
session "github.com", 443 do |http|
|
25
|
+
auth = { username: "#{source["username"]}/token", password: source["api_token"] }
|
26
|
+
http.get_json "/api/v2/json/repos/show/#{source["repo"]}", auth do |status, json|
|
27
|
+
case status
|
28
|
+
when 200
|
29
|
+
if repository = json["repository"]
|
30
|
+
source.update repository.slice(*%w{description url homepage})
|
31
|
+
callback.set! forks: repository["forks"], watchers: repository["watchers"]
|
32
|
+
end
|
33
|
+
when 404, 400
|
34
|
+
callback.error! "Could not find the repository #{source["repo"]}"
|
35
|
+
when 401
|
36
|
+
callback.error! "You are not authorized to access this repository, or invalid username/password"
|
37
|
+
else
|
38
|
+
callback.error! "Last request didn't go as expected, trying again later"
|
39
|
+
end
|
48
40
|
end
|
49
41
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
all.last
|
58
|
-
|
59
|
-
|
42
|
+
http.get_json "/api/v2/json/commits/list/#{source["repo"]}/#{source["branch"]}", auth do |status, json|
|
43
|
+
case status
|
44
|
+
when 200
|
45
|
+
if commits = json["commits"]
|
46
|
+
last_seen_id = source["last_commit"]["id"] if source["last_commit"]
|
47
|
+
new_ones = commits.take_while { |commit| commit["id"] != last_seen_id }
|
48
|
+
merged = new_ones.inject([]) do |all, commit|
|
49
|
+
last = all.last.last unless all.empty?
|
50
|
+
if last && last["committer"]["email"] == commit["committer"]["email"] &&
|
51
|
+
Time.parse(last["committed_date"]) - Time.parse(commit["committed_date"]) < 1.hour
|
52
|
+
all.last << commit
|
53
|
+
else
|
54
|
+
all << [commit]
|
55
|
+
end
|
56
|
+
all
|
60
57
|
end
|
61
|
-
all
|
62
|
-
end
|
63
58
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
59
|
+
merged.reverse.each do |commits|
|
60
|
+
first = commits.first
|
61
|
+
committer = first["committer"]
|
62
|
+
person = { fullname: committer["name"], identities: %W{github.com:#{committer["login"]}}, email: committer["email"] }
|
63
|
+
messages = commits.map { |commit| %{<blockquote><a href="#{commit["url"]}">#{commit["id"][0,7]}</a> #{h commit["message"].strip.split(/[\n\r]/).first[0,50]}</blockquote>} }
|
64
|
+
html = %{pushed to #{h source["branch"]} at <a href="http://github.com/#{source["repo"]}">#{h source["repo"]}</a>:\n#{messages.join("\n")}}
|
65
|
+
callback.activity! uid: first["id"], html: html, url: first["url"], tags: %w{push},
|
66
|
+
timestamp: Time.parse(first["committed_date"]).utc, person: person
|
67
|
+
end
|
68
|
+
callback.inc! commits: new_ones.count
|
69
|
+
if last_commit = commits.first
|
70
|
+
source.update "last_commit"=>last_commit.slice("id", "message", "url").
|
71
|
+
merge("timestamp"=>Time.parse(last_commit["committed_date"]).utc)
|
72
|
+
end
|
72
73
|
end
|
73
|
-
|
74
|
+
when 404, 400
|
75
|
+
callback.error! "Could not find the branch #{source["branch"]}"
|
74
76
|
else
|
75
|
-
|
77
|
+
callback.error! "Github: #{response.code} #{response.message}"
|
76
78
|
end
|
77
|
-
if last_commit = commits.first
|
78
|
-
source.update "last_commit"=>last_commit.slice("id", "message", "url").
|
79
|
-
merge("timestamp"=>Time.parse(last_commit["committed_date"]).utc)
|
80
|
-
end
|
81
|
-
else
|
82
|
-
logger.error "Github: #{response.code} #{response.message}"
|
83
|
-
error = true
|
84
79
|
end
|
85
|
-
|
86
|
-
raise "Last request didn't go as expected, trying again later" if error
|
87
80
|
end
|
88
81
|
end
|
89
82
|
|
90
83
|
def meta(source)
|
91
|
-
meta = [ { :
|
92
|
-
{ :
|
93
|
-
{ :
|
94
|
-
{ :
|
84
|
+
meta = [ { title: "Repository", text: source["repo"], url: source["url"] },
|
85
|
+
{ title: "Branch", text: source["branch"] },
|
86
|
+
{ text: source["description"] },
|
87
|
+
{ title: "Home page", url: source["homepage"] } ]
|
95
88
|
if last_commit = source["last_commit"]
|
96
|
-
meta << { :
|
89
|
+
meta << { title: "Commit", text: last_commit["message"], url: last_commit["url"] }
|
97
90
|
end
|
98
91
|
meta
|
99
92
|
end
|
@@ -6,7 +6,7 @@ module DashFu::Mario
|
|
6
6
|
def setup(source, params)
|
7
7
|
repo = params["repo"].to_s.strip
|
8
8
|
source["source.name"] = "Github Issues for #{repo}"
|
9
|
-
source["metric.columns"] = [{ :
|
9
|
+
source["metric.columns"] = [{ id: "open", label: "Open issues" }, { id: "closed", label: "Closed issues" }]
|
10
10
|
source["metric.totals"] = true
|
11
11
|
source["repo"] = repo
|
12
12
|
source["username"] = params["username"]
|
@@ -18,64 +18,63 @@ module DashFu::Mario
|
|
18
18
|
raise "Need user name and repository name, e.g. assaf/vanity" unless source["repo"][/^[\w-]+\/[\w-]+$/]
|
19
19
|
end
|
20
20
|
|
21
|
-
def update(source, request,
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
if open
|
35
|
-
update[:open] = open.count
|
36
|
-
if open_ids = source["open-ids"]
|
37
|
-
open.reject { |issue| open_ids.include?(issue["number"]) }.reverse.each do |issue|
|
38
|
-
sha = Digest::SHA1.hexdigest([source["repo"], "open", issue["number"], issue["updated_at"]].join(":"))
|
39
|
-
url = "http://github.com/#{source["repo"]}/issues#issue/#{issue["number"]}"
|
40
|
-
html = <<-HTML
|
21
|
+
def update(source, request, callback)
|
22
|
+
session "github.com", 443 do |http|
|
23
|
+
auth = { username: "#{source["username"]}/token", password: source["api_token"] }
|
24
|
+
http.get_json "/api/v2/json/issues/list/#{source["repo"]}/open" do |status, json|
|
25
|
+
case status
|
26
|
+
when 200
|
27
|
+
if open = json["issues"]
|
28
|
+
callback.set! open: open.count
|
29
|
+
open_ids = Set.new(source["open_ids"])
|
30
|
+
open.reject { |issue| open_ids.include?(issue["number"]) }.reverse.each do |issue|
|
31
|
+
sha = Digest::SHA1.hexdigest([source["repo"], "open", issue["number"], issue["updated_at"]].join(":"))
|
32
|
+
url = "http://github.com/#{source["repo"]}/issues#issue/#{issue["number"]}"
|
33
|
+
html = <<-HTML
|
41
34
|
opened <a href="#{url}">issue #{issue["number"]}</a> on #{source["repo"]}:
|
42
35
|
<blockquote>#{h issue["title"]}</blockquote>
|
43
|
-
|
44
|
-
|
45
|
-
|
36
|
+
HTML
|
37
|
+
callback.activity! uid: sha, url: url, html: html, tags: %w{issue opened},
|
38
|
+
timestamp: Time.parse(issue["created_at"]).utc
|
39
|
+
open_ids << issue["number"]
|
40
|
+
end
|
41
|
+
source["open_ids"] = open_ids.to_a
|
46
42
|
end
|
43
|
+
when 404, 400
|
44
|
+
callback.error! "Could not find the repository #{source["repo"]}"
|
45
|
+
when 401
|
46
|
+
callback.error! "You are not authorized to access this repository, or invalid username/password"
|
47
|
+
else
|
48
|
+
callback.error! "Last request didn't go as expected, trying again later"
|
47
49
|
end
|
48
|
-
source["open-ids"] = open.map { |issue| issue["number"] }
|
49
50
|
end
|
50
51
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
html = <<-HTML
|
52
|
+
http.get_json "/api/v2/json/issues/list/#{source["repo"]}/closed" do |status, json|
|
53
|
+
case status
|
54
|
+
when 200
|
55
|
+
if closed = json["issues"]
|
56
|
+
callback.set! closed: closed.count
|
57
|
+
closed_ids = Set.new(source["closed_ids"])
|
58
|
+
closed.reject { |issue| closed_ids.include?(issue["number"]) }.reverse.each do |issue|
|
59
|
+
sha = Digest::SHA1.hexdigest([source["repo"], "closed", issue["number"], issue["updated_at"]].join(":"))
|
60
|
+
url = "http://github.com/#{source["repo"]}/issues#issue/#{issue["number"]}"
|
61
|
+
html = <<-HTML
|
62
62
|
closed <a href="#{url}">issue #{issue["number"]}</a> on #{source["repo"]}:
|
63
63
|
<blockquote>#{h issue["title"]}</blockquote>
|
64
|
-
|
65
|
-
|
66
|
-
|
64
|
+
HTML
|
65
|
+
callback.activity! uid: sha, url: url, html: html, tags: %w{issue closed},
|
66
|
+
timestamp: Time.parse(issue["closed_at"]).utc
|
67
|
+
closed_ids << issue["number"]
|
68
|
+
end
|
69
|
+
source["closed_ids"] = closed_ids.to_a
|
67
70
|
end
|
68
71
|
end
|
69
|
-
source["closed-ids"] = closed.map { |issue| issue["number"] }
|
70
72
|
end
|
71
|
-
|
72
|
-
raise "Last request didn't go as expected, trying again later" if update.empty?
|
73
|
-
block.call :set=>update
|
74
73
|
end
|
75
74
|
end
|
76
75
|
|
77
76
|
def meta(source)
|
78
|
-
[ { :
|
77
|
+
[ { title: "On Github", url: "http://github.com/#{source["repo"]}/issues" } ]
|
79
78
|
end
|
80
79
|
|
81
80
|
protected
|