#!/usr/bin/python

"""
	Official Python library for interfacing with the myGengo API.
	It's released under a BSD-style license and totally, freely available. Check it out on Github
	if you find any issues!

	Questions, comments? ryan@mygengo.com
"""

__author__ = 'Ryan McGrath <ryan@mygengo.com>'
__version__ = '1.2.1'

import httplib2, mimetypes, mimetools, re, hmac

from pprint import pprint
from hashlib import sha1
from urllib import urlencode, quote
from time import time
from operator import itemgetter

# mockdb is a file with a dictionary of every API endpoint for myGengo.
from mockdb import api_urls, apihash

# There are some special setups (like, oh, a Django application) where
# simplejson exists behind the scenes anyway. Past Python 2.6, this should
# never really cause any problems to begin with.
try:
	# Python 2.6 and up
	import json
except ImportError:
	try:
		# Python 2.6 and below (2.4/2.5, 2.3 is not guranteed to work with this library to begin with)
		import simplejson as json
	except ImportError:
		try:
			# This case gets rarer by the day, but if we need to, we can pull it from Django provided it's there.
			from django.utils import simplejson as json
		except:
			# Seriously wtf is wrong with you if you get this Exception.
			raise Exception("mygengo requires the simplejson library (or Python 2.6+) to work. http://www.undefined.org/python/")

class MyGengoError(Exception):
	"""
		Generic error class, catch-all for most MyGengo issues.
		Special cases are handled by APILimit and AuthError.

		Note: You need to explicitly import them into your code, e.g:

		from mygengo import MyGengoError, MyGengoAuthError
	"""
	def __init__(self, msg, error_code=None):
		self.msg = msg
		if error_code == 1000:
			# Auth errors tend to be the most requested for their own
			# Exception instances, so give it to the masses, yo.
			raise MyGengoAuthError(msg)
	
	def __str__(self):
		return repr(self.msg)

class MyGengoAuthError(MyGengoError):
	"""
		Raised when you try to access a protected resource and it fails due to some issue with
		your authentication.
	"""
	def __init__(self, msg):
		self.msg = msg
	
	def __str__(self):
		return repr(self.msg)

class MyGengo(object):
	def __init__(self, public_key = None, private_key = None, sandbox = False, api_version = 1, headers = None, debug = False):
		"""MyGengo(public_key = None, private_key = None, sandbox = False, headers = None)

			Instantiates an instance of MyGengo.
			
			Parameters:
				public_key - Your 'public' key for myGengo. Retrieve this from your account information if you want to do authenticated calls.
				private_key - Your 'private' key for myGengo. Retrieve this from your account information if you want to do authenticated calls.
				sandbox - Whether to use the myGengo sandbox or not. Check with myGengo for the differences with this as it may change.
				headers - User agent header, dictionary style ala {'User-Agent': 'Bert'}
				debug - a flag (True/False) which will cause things to properly blow the hell up on exceptions. Useful for debugging. ;P
		"""
		self.api_url = api_urls['sandbox'] if sandbox is True else api_urls['base']
		self.api_version = api_version
		self.public_key = public_key
		self.private_key = private_key
		# If there's headers, set them, otherwise be an embarassing parent for their own good.
		self.headers = headers
		if self.headers is None:
			self.headers = {'User-agent': 'myGengo Python Library v%s' % __version__}
		# No matter whether we get some supplied or use the generic, tell it we want JSON. ;P
		self.headers['Accept'] = 'application/json'
		self.debug = debug
		self.client = httplib2.Http()
	
	def __getattr__(self, api_call):
		"""
			The most magically awesome block of code you'll ever see.

			Rather than list out 9 million methods for this API, we just keep a table (see above) of
			every API endpoint and their corresponding function id for this library. This pretty much gives
			unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is
			going to be your bottleneck... well, don't use Python. ;P

			For those who don't get what's going on here, Python classes have this great feature known as __getattr__().
			It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist,
			we can take over and find the API method in our table. We then return a function that downloads and parses
			what we're looking for, based on the key/values passed in.

			I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing".

			Note: I'm largely borrowing this technique from another API library/wrapper I've written in the past (Twython).
			If you happen to read both sources and find the same text... well, that's why. ;)
		"""
		def get(self, **kwargs):
			# Grab the (hopefully) existing method 'definition' to fire off from our api hash table.
			fn = apihash[api_call]
			
			# Do a check here for specific job sets - we need to support posting multiple jobs
			# at once, so see if there's an dictionary of jobs passed in, pop it out, let things go on as normal,
			# then pick this chain back up below...
			job = kwargs.pop('job', None)
			jobs = kwargs.pop('jobs', None)
			comment = kwargs.pop('comment', None)
			action = kwargs.pop('action', None)
			
			# Set up a true base URL, abstracting away the need to care about the sandbox mode
			# or API versioning at this stage.
			base_url = self.api_url.replace('{{version}}', 'v%d' % self.api_version)
			
			# Go through and replace any mustaches that are in our API url with their appropriate key/value pairs...
			# NOTE: We pop() here because we don't want the extra data included and messing up our hash down the road.
			base = re.sub(
				'\{\{(?P<m>[a-zA-Z]+)\}\}',
				lambda m: "%s" % kwargs.pop(m.group(1), 'no_argument_specified'), # In case of debugging needs
				base_url + fn['url']
			)
			
			# Build up a proper 'authenticated' url...
			#
			# Note: for further information on what's going on here, it's best to familiarize yourself
			# with the myGengo authentication API. (http://mygengo.com/services/api/dev-docs/authentication)
			query_params = dict([k, quote(v.encode('utf-8'))] for k, v in kwargs.items())
			if self.public_key is not None:
				query_params['api_key'] = self.public_key
			query_params['ts'] = str(int(time()))
			
			# Encoding jobs becomes a bit different than any other method call, so we catch them and do a little
			# JSON-dumping action. Catching them also allows us to provide some sense of portability between the various
			# job-posting methods in that they can all safely rely on passing dictionaries around. Huzzah!
			if fn['method'] == 'POST' or fn['method'] == 'PUT':
				if job is not None:
					query_params['data'] = json.dumps({'job': job}, separators = (',', ':'))
				elif jobs is not None:
					query_params['data'] = json.dumps(jobs, separators = (',', ':'))
				elif comment is not None:
					query_params['data'] = json.dumps(comment, separators = (',', ':'))
				elif action is not None:
					query_params['data'] = json.dumps(action, separators = (',', ':'))
				
				query_json = json.dumps(query_params, separators = (',', ':'), sort_keys = True).replace('/', '\\/')
				query_hmac = hmac.new(self.private_key, query_json, sha1)
				query_params['api_sig'] = query_hmac.hexdigest()
				query_data = urlencode(query_params)
				
				# Httplib2 doesn't assume this for POST requests, but in the case of the myGengo API it's a bit of a hidden necessity.
				headers = self.headers
				headers['Content-Type'] = 'application/x-www-form-urlencoded'
				
				if self.debug is True:
					print query_data
				
				resp, content = self.client.request(base, fn['method'], headers = headers, body = query_data)
			else:
				query_string = urlencode(sorted(query_params.items(), key = itemgetter(0)))
				if self.private_key is not None:
					query_hmac = hmac.new(self.private_key, query_string, sha1)
					query_params['api_sig'] = query_hmac.hexdigest()
					query_string = urlencode(query_params)
				
				if self.debug is True:
					print query_string
				
				resp, content = self.client.request(base + '?%s' % query_string, fn['method'], headers = self.headers)
			results = json.loads(content)
			
			# See if we got any weird or odd errors back that we can cleanly raise on or something...
			if 'opstat' in results and results['opstat'] != 'ok':
				raise MyGengoError(results['err']['msg'], results['err']['code'])
			
			# If not, screw it, return the junks!
			return results
			
		if api_call in apihash:
			return get.__get__(self)
		else:
			raise AttributeError
	
	@staticmethod
	def unicode2utf8(text):
		try:
			if isinstance(text, unicode):
				text = text.encode('utf-8')
		except:
			pass
		return text
