dash-mario 0.16 → 0.17

Sign up to get free protection for your applications and to get access to all the features.
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.
@@ -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 a block. If data comes
54
- from a webhook, then the second argument to update is a Rack::Request object.
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 block passed to update can be used to update the source by yielding to it
57
- with a hash of named arguments. A single update may yield any number of times
58
- with any combination of arguments.
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 named arguments are:
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. This is a hash where the keys are column ids (or indexes), the values
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
- * timestamp -- The Time (if missing, uses the current system time).
69
- * activity -- Activity to show in the stream. See below.
70
-
71
- Activity can specify the following attributes:
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.
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "dash-mario"
3
- spec.version = "0.16"
3
+ spec.version = "0.17"
4
4
  spec.author = "Assaf Arkin"
5
5
  spec.email = "assaf@labnotes.org"
6
6
  spec.homepage = "http://dash-fu.com"
@@ -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 block. When triggered by a Webhook, it will be called with
103
- # source, Rack::Request and a block. It can yield to the block any number of
104
- # times with any combination of the supported named arguments for updating
105
- # the source.
106
- def update(source, request, &block)
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"] = [{ :id=>"tweets", :label=>"Tweets" }]
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, &block)
19
- Net::HTTP.start "backtweets.com" do |http|
20
- get = Net::HTTP::Get.new("/search.json?key=#{api_key}&q=#{Rack::Utils.escape source["url"]}")
21
- response = http.request(get)
22
- json = JSON.parse(response.body) rescue nil if Net::HTTPOK === response
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
- handle, id = tweet["tweet_from_user"], tweet["tweet_id"]
34
- url = "http://twitter.com/#{handle}/#{id}"
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 = { :fullname=>handle, :identities=>%W{twitter.com:#{handle}}, :photo_url=>tweet["tweet_profile_image_url"] }
40
- block.call :activity=>{ :uid=>id, :url=>url, :html=>html, :tags=>%w{twitter mention},
41
- :timestamp=>Time.parse(tweet["tweet_created_at"]).utc, :person=>person }
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
- end
44
- if recent = tweets.first
45
- source["last_tweet_id"] = recent["tweet_id"]
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
- [ { :text=>"Search yourself", :url=>"http://backtweets.com/search?q=#{URI.escape source["url"]}" } ]
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"] = [{ :id=>"commits", :label=>"Commits" }, { :id=>"watchers", :label=>"Watchers" }, { :id=>"forks", :label=>"Forks" }]
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, &block)
24
- http = Net::HTTP.new("github.com", 443)
25
- http.use_ssl = true
26
- http.start do
27
- case response = http_request(http, source, "/api/v2/json/repos/show/:repo")
28
- when Net::HTTPOK
29
- json = JSON.parse(response.body)["repository"] rescue nil
30
- when Net::HTTPNotFound, Net::HTTPBadRequest
31
- raise "Could not find the repository #{source["repo"]}"
32
- when Net::HTTPUnauthorized
33
- raise "You are not authorized to access this repository, or invalid username/password"
34
- end
35
- if json
36
- source.update json.slice(*%w{description url homepage})
37
- block.call :set=>{ :forks=>json["forks"], :watchers=>json["watchers"] }
38
- else
39
- logger.error "Github: #{response.code} #{response.message}"
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
- if commits
51
- last_seen_id = source["last_commit"]["id"] if source["last_commit"]
52
- if last_seen_id
53
- new_ones = commits.take_while { |commit| commit["id"] != last_seen_id }
54
- merged = new_ones.inject([]) do |all, commit|
55
- last = all.last.last unless all.empty?
56
- if last && last["committer"]["email"] == commit["committer"]["email"]
57
- all.last << commit
58
- else
59
- all << [commit]
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
- merged.reverse.each do |commits|
65
- first = commits.first
66
- committer = first["committer"]
67
- person = { :fullname=>committer["name"], :identities=>%W{github.com:#{committer["login"]}}, :email=>committer["email"] }
68
- 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>} }
69
- html = %{pushed to #{h source["branch"]} at <a href="http://github.com/#{source["repo"]}">#{h source["repo"]}</a>:\n#{messages.join("\n")}}
70
- block.call :activity=>{ :uid=>first["id"], :html=>html, :url=>first["url"], :tags=>%w{push},
71
- :timestamp=>Time.parse(first["committed_date"]).utc, :person=>person }
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
- block.call :inc=>{ :commits=>new_ones.count }
74
+ when 404, 400
75
+ callback.error! "Could not find the branch #{source["branch"]}"
74
76
  else
75
- block.call :set=>{ :commits=>commits.count }
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 = [ { :title=>"Repository", :text=>source["repo"], :url=>source["url"] },
92
- { :title=>"Branch", :text=>source["branch"] },
93
- { :text=>source["description"] },
94
- { :title=>"Home page", :url=>source["homepage"] } ]
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 << { :title=>"Commit", :text=>last_commit["message"], :url=>last_commit["url"] }
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"] = [{ :id=>"open", :label=>"Open issues" }, { :id=>"closed", :label=>"Closed issues" }]
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, &block)
22
- http = Net::HTTP.new("github.com", 443)
23
- http.use_ssl = true
24
- http.start do
25
- update = {}
26
- case response = http_request(http, source, "open")
27
- when Net::HTTPOK
28
- open = JSON.parse(response.body)["issues"] rescue nil
29
- when Net::HTTPNotFound, Net::HTTPBadRequest
30
- raise "Could not find the repository #{source["repo"]}"
31
- when Net::HTTPUnauthorized
32
- raise "You are not authorized to access this repository, or invalid username/password"
33
- end
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
- HTML
44
- block.call :activity=>{ :uid=>sha, :url=>url, :html=>html, :tags=>%w{issue opened},
45
- :timestamp=>Time.parse(issue["created_at"]).utc }
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
- case response = http_request(http, source, "closed")
52
- when Net::HTTPOK
53
- closed = JSON.parse(response.body)["issues"] rescue nil
54
- end
55
- if closed
56
- update[:closed] = closed.count
57
- if closed_ids = 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
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
- HTML
65
- block.call :activity=>{ :uid=>sha, :url=>url, :html=>html, :tags=>%w{issue closed},
66
- :timestamp=>Time.parse(issue["closed_at"]).utc }
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
- [ { :title=>"On Github", :url=>"http://github.com/#{source["repo"]}/issues" } ]
77
+ [ { title: "On Github", url: "http://github.com/#{source["repo"]}/issues" } ]
79
78
  end
80
79
 
81
80
  protected