#!/usr/bin/python2.4
#
# Copyright 2009 Empeeric LTD. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import urllib,urllib2
import urlparse
import string
try:
   import simplejson
except ImportError:
   import json as simplejson
   
BITLY_BASE_URL = "http://api.bit.ly/"
BITLY_API_VERSION = "2.0.1"

VERBS_PARAM = { 
         'shorten':'longUrl',               
         'expand':'shortUrl', 
         'info':'shortUrl',
         'stats':'shortUrl',
         'errors':'',
}

class BitlyError(Exception):
  '''Base class for bitly errors'''
  
  @property
  def message(self):
    '''Returns the first argument used to construct this error.'''
    return self.args[0]

class Api(object):
    """ API class for bit.ly """
    def __init__(self, login, apikey):
        self.login = login
        self.apikey = apikey
        self._urllib = urllib2
        
    def shorten(self,longURLs,params={}):
        """ 
            Takes either:
            A long URL string and returns shortened URL string
            Or a list of long URL strings and returns a list of shortened URL strings.
        """
        want_result_list = True
        if not isinstance(longURLs, list):
            longURLs = [longURLs]
            want_result_list = False
        
        for index,url in enumerate(longURLs):
            if not '://' in url:
                longURLs[index] = "http://" + url
            
        request = self._getURL("shorten",longURLs,params)
        result = self._fetchUrl(request)
        json = simplejson.loads(result)
        self._CheckForError(json)
        
        results = json['results']
        res = [self._extract_short_url(results[url]) for url in longURLs]

        if want_result_list:
            return res
        else:
            return res[0]

    def _extract_short_url(self,item):
        if item['shortKeywordUrl'] == "":
            return item['shortUrl']
        else:
            return item['shortKeywordUrl']

    def expand(self,shortURL,params={}):
        """ Given a bit.ly url or hash, return long source url """
        request = self._getURL("expand",shortURL,params)
        result = self._fetchUrl(request)
        json = simplejson.loads(result)
        self._CheckForError(json)
        return json['results'][string.split(shortURL, '/')[-1]]['longUrl']

    def info(self,shortURL,params={}):
        """ 
        Given a bit.ly url or hash, 
        return information about that page, 
        such as the long source url
        """
        request = self._getURL("info",shortURL,params)
        result = self._fetchUrl(request)
        json = simplejson.loads(result)
        self._CheckForError(json)
        return json['results'][string.split(shortURL, '/')[-1]]

    def stats(self,shortURL,params={}):
        """ Given a bit.ly url or hash, return traffic and referrer data.  """
        request = self._getURL("stats",shortURL,params)
        result = self._fetchUrl(request)
        json = simplejson.loads(result)
        self._CheckForError(json)
        return Stats.NewFromJsonDict(json['results'])

    def errors(self,params={}):
        """ Get a list of bit.ly API error codes. """
        request = self._getURL("errors","",params)
        result = self._fetchUrl(request)
        json = simplejson.loads(result)
        self._CheckForError(json)
        return json['results']
        
    def setUrllib(self, urllib):
        '''Override the default urllib implementation.
    
        Args:
          urllib: an instance that supports the same API as the urllib2 module
        '''
        self._urllib = urllib
    
    def _getURL(self,verb,paramVal,more_params={}): 
        if not isinstance(paramVal, list):
            paramVal = [paramVal]
              
        params = {
                  'version':BITLY_API_VERSION,
                  'format':'json',
                  'login':self.login,
                  'apiKey':self.apikey,
            }

        params.update(more_params)
        params = params.items() 
                
        verbParam = VERBS_PARAM[verb]   
        if verbParam:
            for val in paramVal:
                params.append(( verbParam,val ))
   
        encoded_params = urllib.urlencode(params)
        return "%s%s?%s" % (BITLY_BASE_URL,verb,encoded_params)
       
    def _fetchUrl(self,url):
        '''Fetch a URL
    
        Args:
          url: The URL to retrieve
    
        Returns:
          A string containing the body of the response.
        '''
    
        # Open and return the URL 
        url_data = self._urllib.urlopen(url).read()
        return url_data    

    def _CheckForError(self, data):
        """Raises a BitlyError if bitly returns an error message.
    
        Args:
          data: A python dict created from the bitly json response
        Raises:
          BitlyError wrapping the bitly error message if one exists.
        """
        # bitly errors are relatively unlikely, so it is faster
        # to check first, rather than try and catch the exception
        if 'ERROR' in data or data['statusCode'] == 'ERROR':
            raise BitlyError, data['errorMessage']
        for key in data['results']:
            if type(data['results']) is dict and type(data['results'][key]) is dict:
                if 'statusCode' in data['results'][key] and data['results'][key]['statusCode'] == 'ERROR':
                    raise BitlyError, data['results'][key]['errorMessage'] 
       
class Stats(object):
    '''A class representing the Statistics returned by the bitly api.
    
    The Stats structure exposes the following properties:
    status.user_clicks # read only
    status.clicks # read only
    '''
    
    def __init__(self,user_clicks=None,total_clicks=None):
        self.user_clicks = user_clicks
        self.total_clicks = total_clicks
    
    @staticmethod
    def NewFromJsonDict(data):
        '''Create a new instance based on a JSON dict.
    
        Args:
          data: A JSON dict, as converted from the JSON in the bitly API
        Returns:
          A bitly.Stats instance
        '''
        return Stats(user_clicks=data.get('userClicks', None),
                      total_clicks=data.get('clicks', None))

        
if __name__ == '__main__':
    testURL1="www.yahoo.com"
    testURL2="www.cnn.com"
    a=Api(login="pythonbitly",apikey="R_06871db6b7fd31a4242709acaf1b6648")
    short=a.shorten(testURL1)    
    print "Short URL = %s" % short
    short=a.shorten(testURL1,{'history':1})    
    print "Short URL with history = %s" % short
    urlList=[testURL1,testURL2]
    shortList=a.shorten(urlList)
    print "Short URL list = %s" % shortList
    long=a.expand(short)
    print "Expanded URL = %s" % long
    info=a.info(short)
    print "Info: %s" % info
    stats=a.stats(short)
    print "User clicks %s, total clicks: %s" % (stats.user_clicks,stats.total_clicks)
    errors=a.errors()
    print "Errors: %s" % errors
    testURL3=["www.google.com"]
    short=a.shorten(testURL3) 
    print "Short url in list = %s" % short