ddollar-octopi 0.0.13

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.
@@ -0,0 +1,236 @@
1
+ require 'rubygems'
2
+ require 'httparty'
3
+ require 'yaml'
4
+ require 'pp'
5
+
6
+ module Octopi
7
+ def authenticated(*args, &block)
8
+ opts = args.last.is_a?(Hash) ? args.last : {}
9
+ config = read_gitconfig
10
+ login = config["github"]["user"]
11
+ token = config["github"]["token"]
12
+
13
+ api = AuthApi.new(login, token)
14
+ api.trace_level = opts[:trace]
15
+
16
+ puts "=> Trace on: #{api.trace_level}" if api.trace_level
17
+
18
+ yield api
19
+ end
20
+
21
+ def authenticated_with(*args, &block)
22
+ opts = args.last.is_a?(Hash) ? args.last : {}
23
+ if opts[:config]
24
+ config = File.open(opts[:config]) { |yf| YAML::load(yf) }
25
+ raise "Missing config #{opts[:config]}" unless config
26
+
27
+ login = config["login"]
28
+ token = config["token"]
29
+ trace = config["trace"]
30
+ else
31
+ login, token = *args
32
+ end
33
+
34
+ puts "=> Trace on: #{trace}" if trace
35
+
36
+ api = AuthApi.new(login, token)
37
+ api.trace_level = trace if trace
38
+ yield api
39
+ end
40
+
41
+ def read_gitconfig
42
+ config = {}
43
+ group = nil
44
+ File.foreach("#{ENV['HOME']}/.gitconfig") do |line|
45
+ line.strip!
46
+ if line[0] != ?# and line =~ /\S/
47
+ if line =~ /^\[(.*)\]$/
48
+ group = $1
49
+ else
50
+ key, value = line.split("=")
51
+ value ||= ''
52
+ (config[group]||={})[key.strip] = value.strip
53
+ end
54
+ end
55
+ end
56
+ config
57
+ end
58
+
59
+ class Api
60
+ CONTENT_TYPE = {
61
+ 'yaml' => 'application/x-yaml',
62
+ 'json' => 'application/json',
63
+ 'xml' => 'application/sml'
64
+ }
65
+ RETRYABLE_STATUS = [403]
66
+ MAX_RETRIES = 10
67
+
68
+ attr_accessor :format, :login, :token, :trace_level, :read_only
69
+
70
+ def initialize(login = nil, token = nil, format = "yaml")
71
+ @format = format
72
+ @read_only = true
73
+
74
+ if login
75
+ @login = login
76
+ @token = token
77
+ @read_only = false
78
+ self.class.default_params :login => login, :token => token
79
+ end
80
+ end
81
+
82
+ def read_only?
83
+ read_only
84
+ end
85
+
86
+ {:keys => 'public_keys', :emails => 'emails'}.each_pair do |action, key|
87
+ define_method("#{action}") do
88
+ get("/user/#{action}")[key]
89
+ end
90
+ end
91
+
92
+ def user
93
+ user_data = get("/user/show/#{login}")
94
+ raise "Unexpected response for user command" unless user_data and user_data['user']
95
+ User.new(self, user_data['user'])
96
+ end
97
+
98
+ def open_issue(user, repo, params)
99
+ Issue.open(user, repo, params, self)
100
+ end
101
+
102
+ def repository(name)
103
+ repo = Repository.find(user, name, self)
104
+ repo.api = self
105
+ repo
106
+ end
107
+ alias_method :repo, :repository
108
+
109
+ def commits(repo,opts={})
110
+ branch = opts[:branch] || "master"
111
+ commits = Commit.find_all(repo, branch, self)
112
+ end
113
+
114
+ def save(resource_path, data)
115
+ traslate resource_path, data
116
+ #still can't figure out on what format values are expected
117
+ post("#{resource_path}", { :query => data })
118
+ end
119
+
120
+ def find(path, result_key, resource_id)
121
+ get(path, { :id => resource_id })
122
+ end
123
+
124
+ def find_all(path, result_key, query)
125
+ get(path, { :query => query, :id => query })[result_key]
126
+ end
127
+
128
+ def get_raw(path, params)
129
+ get(path, params, 'plain')
130
+ end
131
+
132
+ def get(path, params = {}, format = "yaml")
133
+ @@retries = 0
134
+ begin
135
+ trace "GET [#{format}]", "/#{format}#{path}", params
136
+ submit(path, params, format) do |path, params, format|
137
+ self.class.get "/#{format}#{path}"
138
+ end
139
+ rescue RetryableAPIError => e
140
+ if @@retries < MAX_RETRIES
141
+ $stderr.puts e.message
142
+ @@retries += 1
143
+ sleep 6
144
+ retry
145
+ else
146
+ raise APIError, "GitHub returned status #{e.code}, despite" +
147
+ " repeating the request #{MAX_RETRIES} times. Giving up."
148
+ end
149
+ end
150
+ end
151
+
152
+ def post(path, params = {}, format = "yaml")
153
+ @@retries = 0
154
+ begin
155
+ trace "POST", "/#{format}#{path}", params
156
+ submit(path, params, format) do |path, params, format|
157
+ resp = self.class.post "/#{format}#{path}", :query => params
158
+ resp
159
+ end
160
+ rescue RetryableAPIError => e
161
+ if @@retries < MAX_RETRIES
162
+ $stderr.puts e.message
163
+ @@retries += 1
164
+ sleep 6
165
+ retry
166
+ else
167
+ raise APIError, "GitHub returned status #{e.code}, despite" +
168
+ " repeating the request #{MAX_RETRIES} times. Giving up."
169
+ end
170
+ end
171
+ end
172
+
173
+ private
174
+ def submit(path, params = {}, format = "yaml", &block)
175
+ params.each_pair do |k,v|
176
+ if path =~ /:#{k.to_s}/
177
+ params.delete(k)
178
+ path = path.gsub(":#{k.to_s}", v)
179
+ end
180
+ end
181
+ query = login ? { :login => login, :token => token } : {}
182
+ query.merge!(params)
183
+
184
+ begin
185
+ resp = yield(path, query.merge(params), format)
186
+ rescue Net::HTTPBadResponse
187
+ raise RetryableAPIError
188
+ end
189
+
190
+ if @trace_level
191
+ case @trace_level
192
+ when "curl"
193
+ query_trace = []
194
+ query.each_pair { |k,v| query_trace << "-F '#{k}=#{v}'" }
195
+ puts "===== [curl version]"
196
+ puts "curl #{query_trace.join(" ")} #{self.class.base_uri}/#{format}#{path}"
197
+ puts "===================="
198
+ end
199
+ end
200
+ raise RetryableAPIError, resp.code.to_i if RETRYABLE_STATUS.include? resp.code.to_i
201
+ raise APIError,
202
+ "GitHub returned status #{resp.code}" unless resp.code.to_i == 200
203
+ # FIXME: This fails for showing raw Git data because that call returns
204
+ # text/html as the content type. This issue has been reported.
205
+ ctype = resp.headers['content-type'].first
206
+ raise FormatError, [ctype, format] unless
207
+ ctype.match(/^#{CONTENT_TYPE[format]};/)
208
+ if format == 'yaml' && resp['error']
209
+ raise APIError, resp['error'].first['error']
210
+ end
211
+ resp
212
+ end
213
+
214
+ def trace(oper, url, params)
215
+ return unless trace_level
216
+ par_str = " params: " + params.map { |p| "#{p[0]}=#{p[1]}" }.join(", ") if params and !params.empty?
217
+ puts "#{oper}: #{url}#{par_str}"
218
+ end
219
+ end
220
+
221
+ class AuthApi < Api
222
+ include HTTParty
223
+ base_uri "https://github.com/api/v2"
224
+ end
225
+
226
+ class AnonymousApi < Api
227
+ include HTTParty
228
+ base_uri "http://github.com/api/v2"
229
+ end
230
+
231
+ ANONYMOUS_API = AnonymousApi.new
232
+
233
+ %w{error base resource user tag repository issue file_object blob key commit branch}.
234
+ each{|f| require "#{File.dirname(__FILE__)}/octopi/#{f}"}
235
+
236
+ end
@@ -0,0 +1,111 @@
1
+ class String
2
+ def camel_case
3
+ self.gsub(/(^|_)(.)/) { $2.upcase }
4
+ end
5
+ end
6
+ module Octopi
7
+ class Base
8
+ VALID = {
9
+ :repo => {
10
+ # FIXME: API currently chokes on repository names containing periods,
11
+ # but presumably this will be fixed.
12
+ :pat => /^[A-Za-z0-9_\.-]+$/,
13
+ :msg => "%s is an invalid repository name"},
14
+ :user => {
15
+ :pat => /^[A-Za-z0-9_\.-]+$/,
16
+ :msg => "%s is an invalid username"},
17
+ :file => {
18
+ :pat => /^[^ \/]+$/,
19
+ :msg => "%s is an invalid filename"},
20
+ :sha => {
21
+ :pat => /^[a-f0-9]{40}$/,
22
+ :msg => "%s is an invalid SHA hash"},
23
+ :state => {
24
+ # FIXME: Any way to access Issue::STATES from here?
25
+ :pat => /^(open|closed)$/,
26
+ :msg => "%s is an invalid state; should be 'open' or 'closed'."
27
+ }
28
+ }
29
+
30
+ attr_accessor :api
31
+
32
+ def initialize(api, hash)
33
+ @api = api
34
+ @keys = []
35
+
36
+ raise "Missing data for #{@resource}" unless hash
37
+
38
+ hash.each_pair do |k,v|
39
+ @keys << k
40
+ next if k =~ /\./
41
+ instance_variable_set("@#{k}", v)
42
+
43
+ method = (TrueClass === v || FalseClass === v) ? "#{k}?" : k
44
+
45
+ self.class.send :define_method, "#{method}=" do |v|
46
+ instance_variable_set("@#{k}", v)
47
+ end
48
+
49
+ self.class.send :define_method, method do
50
+ instance_variable_get("@#{k}")
51
+ end
52
+ end
53
+ end
54
+
55
+ def property(p, v)
56
+ path = "#{self.class.path_for(:resource)}/#{p}"
57
+ @api.find(path, self.class.resource_name(:singular), v)
58
+ end
59
+
60
+ def save
61
+ hash = {}
62
+ @keys.each { |k| hash[k] = send(k) }
63
+ @api.save(self.path_for(:resource), hash)
64
+ end
65
+
66
+ private
67
+ def self.extract_user_repository(*args)
68
+ opts = args.last.is_a?(Hash) ? args.pop : {}
69
+ if opts.empty?
70
+ if args.length > 1
71
+ repo, user = *args
72
+ else
73
+ repo = args.pop
74
+ end
75
+ else
76
+ opts[:repo] = opts[:repository] if opts[:repository]
77
+ repo = args.pop || opts[:repo]
78
+ user = opts[:user]
79
+ end
80
+
81
+ user = repo.owner if repo.is_a? Repository
82
+
83
+ if repo.is_a?(String) and !user
84
+ raise "Need user argument when repository is identified by name"
85
+ end
86
+ ret = extract_names(user, repo)
87
+ ret << opts
88
+ ret
89
+ end
90
+
91
+ def self.extract_names(*args)
92
+ args.map do |v|
93
+ v = v.name if v.is_a? Repository
94
+ v = v.login if v.is_a? User
95
+ v
96
+ end
97
+ end
98
+
99
+ def self.validate_args(spec)
100
+ m = caller[0].match(/\/([a-z0-9_]+)\.rb:\d+:in `([a-z_0-9]+)'/)
101
+ meth = m ? "#{m[1].camel_case}.#{m[2]}" : 'method'
102
+ raise ArgumentError, "Invalid spec" unless
103
+ spec.values.all? { |s| VALID.key? s }
104
+ errors = spec.reject{|arg, spec| arg.nil?}.
105
+ reject{|arg, spec| arg.to_s.match(VALID[spec][:pat])}.
106
+ map {|arg, spec| "Invalid argument '%s' for %s (%s)" %
107
+ [arg, meth, VALID[spec][:msg] % arg]}
108
+ raise ArgumentError, "\n" + errors.join("\n") unless errors.empty?
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,21 @@
1
+ module Octopi
2
+ class Blob < Base
3
+ include Resource
4
+ set_resource_name "blob"
5
+
6
+ resource_path "/blob/show/:id"
7
+
8
+ def self.find(user, repo, sha, path=nil)
9
+ user = user.login if user.is_a? User
10
+ repo = repo.name if repo.is_a? Repository
11
+ self.class.validate_args(sha => :sha, user => :user, path => :file)
12
+ if path
13
+ super [user,repo,sha,path]
14
+ else
15
+ blob = ANONYMOUS_API.get_raw(path_for(:resource),
16
+ {:id => [user,repo,sha].join('/')})
17
+ new(ANONYMOUS_API, {:text => blob})
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ module Octopi
2
+ class Branch < Base
3
+ include Resource
4
+ set_resource_name "branch", "branches"
5
+
6
+ resource_path "/repos/show/:id"
7
+
8
+ def self.find(user, repo, api=ANONYMOUS_API)
9
+ user = user.login if user.is_a? User
10
+ repo = repo.name if repo.is_a? Repository
11
+ self.validate_args(user => :user, repo => :repo)
12
+ api = ANONYMOUS_API if repo.is_a?(Repository) && !repo.private
13
+ find_plural([user,repo,'branches'], :resource, api){
14
+ |i| {:name => i.first, :hash => i.last }
15
+ }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,65 @@
1
+ module Octopi
2
+ class Commit < Base
3
+ include Resource
4
+ find_path "/commits/list/:query"
5
+ resource_path "/commits/show/:id"
6
+
7
+ attr_accessor :repository
8
+
9
+ # Finds all commits for a given Repository's branch
10
+ #
11
+ # You can provide the user and repo parameters as
12
+ # String or as User and Repository objects. When repo
13
+ # is provided as a Repository object, user is superfluous.
14
+ #
15
+ # If no branch is given, "master" is assumed.
16
+ #
17
+ # Sample usage:
18
+ #
19
+ # find_all(repo, :branch => "develop") # repo must be an object
20
+ # find_all("octopi", :user => "fcoury") # user must be provided
21
+ # find_all(:user => "fcoury", :repo => "octopi") # branch defaults to master
22
+ #
23
+ def self.find_all(*args)
24
+ api = args.last.is_a?(Api) ? args.pop : ANONYMOUS_API
25
+ repo = args.first
26
+ user ||= repo.owner if repo.is_a? Repository
27
+ user, repo_name, opts = extract_user_repository(*args)
28
+ self.validate_args(user => :user, repo_name => :repo)
29
+ branch = opts[:branch] || "master"
30
+ api = ANONYMOUS_API if repo.is_a?(Repository) && !repo.private
31
+ commits = super user, repo_name, branch, api
32
+ commits.each { |c| c.repository = repo } if repo.is_a? Repository
33
+ commits
34
+ end
35
+
36
+ # TODO: Make find use hashes like find_all
37
+ def self.find(*args)
38
+ if args.last.is_a?(Commit)
39
+ commit = args.pop
40
+ super "#{commit.repo_identifier}"
41
+ else
42
+ user, name, sha = *args
43
+ user = user.login if user.is_a? User
44
+ name = repo.name if name.is_a? Repository
45
+ self.validate_args(user => :user, name => :repo, sha => :sha)
46
+ super [user, name, sha]
47
+ end
48
+ end
49
+
50
+ def details
51
+ self.class.find(self)
52
+ end
53
+
54
+ def repo_identifier
55
+ url_parts = url.split('/')
56
+ if @repository
57
+ parts = [@repository.owner, @repository.name, url_parts[6]]
58
+ else
59
+ parts = [url_parts[3], url_parts[4], url_parts[6]]
60
+ end
61
+
62
+ parts.join('/')
63
+ end
64
+ end
65
+ end