require 'singleton' require File.join(File.dirname(__FILE__), "self") module Octopi # Dummy class, so AnonymousApi and AuthApi have somewhere to inherit from class Api include Self attr_accessor :format, :login, :token, :trace_level, :read_only end # Used for accessing the Github API anonymously class AnonymousApi < Api include HTTParty include Singleton base_uri "https://github.com/api/v2" def read_only? true end def auth_parameters { } end end class AuthApi < Api include HTTParty include Singleton base_uri "https://github.com/api/v2" def read_only? false end def auth_parameters { :login => Api.me.login, :token => Api.me.token } end end # This is the real API class. # # API requests are limited to 60 per minute. # # Sets up basic methods for accessing the API. class Api @@api = Octopi::AnonymousApi.instance @@authenticated = false include Singleton CONTENT_TYPE = { 'yaml' => ['application/x-yaml', 'text/yaml', 'text/x-yaml', 'application/yaml'], 'json' => 'application/json', 'xml' => 'application/xml', # Unexpectedly, Github returns resources such as blobs as text/html! # Thus, plain == text/html. 'plain' => ['text/plain', 'text/html'] } RETRYABLE_STATUS = [403] MAX_RETRIES = 10 # Would be nice if cattr_accessor was available, oh well. # We use this to check if we use the auth or anonymous api def self.authenticated @@authenticated end # We set this to true when the user has auth'd. def self.authenticated=(value) @@authenticated = value end # The API we're using def self.api @@api end class << self alias_method :me, :api end # set the API we're using def self.api=(value) @@api = value end def user user_data = get("/user/show/#{login}") raise "Unexpected response for user command" unless user_data and user_data['user'] User.new(user_data['user']) end def save(resource_path, data) traslate resource_path, data #still can't figure out on what format values are expected post("#{resource_path}", { :query => data }) end def find(path, result_key, resource_id, klass=nil, cache=true) result = get(path, { :id => resource_id, :cache => cache }, klass) result end def find_all(path, result_key, query, klass=nil, cache=true) result = get(path, { :query => query, :id => query, :cache => cache }, klass) result[result_key] end def get_raw(path, params, klass=nil) get(path, params, klass, 'plain') end def get(path, params = {}, klass=nil, format = :yaml) @@retries = 0 begin submit(path, params, klass, format) do |path, params, format, query| self.class.get "/#{format}#{path}", { :format => format, :query => query } end rescue RetryableAPIError => e if @@retries < MAX_RETRIES $stderr.puts e.message if e.code != 403 @@retries += 1 sleep 6 retry else raise APIError, "Github returned status #{e.code}, you may not have access to this resource." end else raise APIError, "GitHub returned status #{e.code}, despite" + " repeating the request #{MAX_RETRIES} times. Giving up." end end end def post(path, params = {}, klass=nil, format = :yaml) @@retries = 0 begin trace "POST", "/#{format}#{path}", params submit(path, params, klass, format) do |path, params, format, query| resp = self.class.post "/#{format}#{path}", { :body => params, :format => format, :query => query } resp end rescue RetryableAPIError => e if @@retries < MAX_RETRIES $stderr.puts e.message @@retries += 1 sleep 6 retry else raise APIError, "GitHub returned status #{e.code}, despite" + " repeating the request #{MAX_RETRIES} times. Giving up." end end end private def method_missing(method, *args) api.send(method, *args) end def submit(path, params = {}, klass=nil, format = :yaml, &block) # Ergh. Ugly way to do this. Find a better one! cache = params.delete(:cache) cache = true if cache.nil? cache = false params.each_pair do |k,v| if path =~ /:#{k.to_s}/ params.delete(k) path = path.gsub(":#{k.to_s}", v) end end begin key = "#{Api.api.class.to_s}:#{path}" resp = if cache APICache.get(key, :cache => 61) do yield(path, params, format, auth_parameters) end else yield(path, params, format, auth_parameters) end rescue Net::HTTPBadResponse raise RetryableAPIError end raise RetryableAPIError, resp.code.to_i if RETRYABLE_STATUS.include? resp.code.to_i # puts resp.code.inspect raise NotFound, klass || self.class if resp.code.to_i == 404 raise APIError, "GitHub returned status #{resp.code}" unless resp.code.to_i == 200 || resp.code.to_i == 201 # FIXME: This fails for showing raw Git data because that call returns # text/html as the content type. This issue has been reported. # It happens, in tests. return resp if resp.headers.empty? content_type = Array === resp.headers['content-type'] ? resp.headers['content-type'] : [resp.headers['content-type']] ctype = content_type.first.split(";").first raise FormatError, [ctype, format] unless CONTENT_TYPE[format.to_s].include?(ctype) if format == 'yaml' && resp['error'] raise APIError, resp['error'] end resp end def trace(oper, url, params) return unless trace_level par_str = " params: " + params.map { |p| "#{p[0]}=#{p[1]}" }.join(", ") if params && !params.empty? puts "#{oper}: #{url}#{par_str}" end end end