diff --git a/.gitignore b/.gitignore index 2f78cf5..731cc7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ *.pyc - +.idea/ +test_app.py +venv +__pycache__ \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..911b4a7 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +import freshbooks \ No newline at end of file diff --git a/freshbooks.py b/freshbooks.py index a680a7f..3f31a78 100755 --- a/freshbooks.py +++ b/freshbooks.py @@ -58,13 +58,14 @@ """ -import sys, os, datetime -import urllib, urllib2 +import datetime +import urllib.request import xml.dom.minidom as xml_lib + # module level constants VERSION = '0.5' # Library version -API_VERSION = '2.1' # FreshBooks API version +API_VERSION = '2.1' # FreshBooks API version SERVICE_URL = "/api/%s/xml-in" % API_VERSION # module level variables @@ -75,12 +76,13 @@ request_headers = None last_response = None + def setup(url, token, user_agent_name=None, headers={}): ''' This funtion sets the high level variables for use in the interface. ''' global account_url, account_name, auth_token, user_agent, request_headers - + account_url = url if url.find('//') == -1: account_name = url[:(url.find('freshbooks.com') - 1)] @@ -93,22 +95,26 @@ def setup(url, token, user_agent_name=None, headers={}): if not user_agent: user_agent = 'Python:%s' % account_name request_headers['User-Agent'] = user_agent - + + # these three classes are for typed exceptions class InternalError(Exception): pass - + + class AuthenticationError(Exception): pass - + + class UnknownSystemError(Exception): pass - + + class InvalidParameterError(Exception): pass -def call_api(method, elems = {}): +def call_api(method, elems={}): ''' This function calls into the FreshBooks API and returns the Response ''' @@ -146,31 +152,32 @@ def call_api(method, elems = {}): raise InvalidParameterError(msg) else: raise Exception(msg) - return last_response - + + def post(body): ''' This function actually communicates with the FreshBooks API ''' # setup HTTP basic authentication - password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() + password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() url = "" if account_url.find('//') == -1: url = "https://" url += account_url + SERVICE_URL password_mgr.add_password(None, url, auth_token, '') - handler = urllib2.HTTPBasicAuthHandler(password_mgr) - opener = urllib2.build_opener(handler) - urllib2.install_opener(opener) + handler = urllib.request.HTTPBasicAuthHandler(password_mgr) + opener = urllib.request.build_opener(handler) + urllib.request.install_opener(opener) # make the request and return the response body - request = urllib2.Request(url, body, request_headers) - response = urllib2.urlopen(request) + request = urllib.request.Request(url, body, request_headers) + response = urllib.request.urlopen(request) response_content = response.read() return response_content + class Response(object): ''' A response from FreshBooks @@ -221,7 +228,8 @@ def error_message(self): return error[0].childNodes[0].nodeValue else: return None - + + class BaseObject(object): ''' This serves as the base object for all FreshBooks objects. @@ -233,12 +241,13 @@ class BaseObject(object): # anonymous functions to do the conversions on type MAPPING_FUNCTIONS = { - 'int' : lambda val: int(val), - 'float' : lambda val: float(val), - 'bool' : lambda val: bool(int(val)) if val in ('0', '1') else val, - 'datetime' : lambda val: \ - datetime.datetime.strptime(val, - '%Y-%m-%d %H:%M:%S') if (val != '0000-00-00 00:00:00' and len(val) == 19) else datetime.datetime.strptime(val, '%Y-%m-%d') if len(val) == 10 else val + 'int': lambda val: int(val), + 'float': lambda val: float(val), + 'bool': lambda val: bool(int(val)) if val in ('0', '1') else val, + 'datetime': lambda val: datetime.datetime.strptime(val, '%Y-%m-%d %H:%M:%S') + if (val != '0000-00-00 00:00:00' + and len(val) == 19) + else datetime.datetime.strptime(val, '%Y-%m-%d') if len(val) == 10 else val } @classmethod @@ -248,6 +257,8 @@ def _new_from_xml(cls, element): object from the XML. ''' obj = cls() + + # return element # basically just go through the XML creating attributes on the # object. @@ -257,29 +268,29 @@ def _new_from_xml(cls, element): val = elem.firstChild.nodeValue # HACK: find another way to detect arrays, probably # based on a list of elements instead of a textnode + if elem.nodeName == 'lines': val = [] for item in [node for node in elem.childNodes if node.nodeType == node.ELEMENT_NODE]: c = eval(item.nodeName.capitalize()) if c: val.append(c._new_from_xml(item)) - - # if there is typing information supplied by - # the child class then use that - elif cls.TYPE_MAPPINGS.has_key(elem.nodeName): - val = \ - cls.MAPPING_FUNCTIONS[\ - cls.TYPE_MAPPINGS[elem.nodeName]](val) + else: + try: + val = cls.MAPPING_FUNCTIONS[cls.TYPE_MAPPINGS[elem.nodeName]](val) + except KeyError: + pass + setattr(obj, elem.nodeName, val) return obj @classmethod - def get(cls, object_id, element_name = None): + def get(cls, object_id, element_name=None): ''' Get a single object from the API ''' - resp = call_api('%s.get' % cls.object_name, {'%s_id' % cls.object_name : object_id}) + resp = call_api('%s.get' % cls.object_name, {'%s_id' % cls.object_name: object_id}) if resp.success: items = resp.doc.getElementsByTagName(element_name or cls.object_name) @@ -289,7 +300,7 @@ def get(cls, object_id, element_name = None): return None @classmethod - def list(cls, options = {}, element_name = None, get_all=False): + def list(cls, options={}, element_name=None, get_all=False): ''' Get a summary list of this object. If get_all is True then the paging will be checked to get all of the items. @@ -316,8 +327,7 @@ def list(cls, options = {}, element_name = None, get_all=False): resp.doc.getElementsByTagName(element_name or cls.object_name)] return result - - + def to_xml(self, doc, element_name=None): ''' Create an XML representation of the object for use @@ -327,7 +337,12 @@ def to_xml(self, doc, element_name=None): element_name = element_name or \ self.object_name.lower() root = doc.createElement(element_name) - + + def append_minidom(minidom1, minidom2): + for child in minidom2.childNodes: + minidom1.appendChild(child) + return minidom1 + # Add each member to the root element for key, value in self.__dict__.items(): if isinstance(value, list): @@ -336,18 +351,19 @@ def to_xml(self, doc, element_name=None): item_name = 'line' if key == 'lines' else key[:-1] array_item = doc.createElement(item_name) array_item.appendChild(doc.createTextNode(str(item))) - root.append(array) + # root.append(array) + root = append_minidom(root, array) elif value: elem = doc.createElement(key) elem.appendChild(doc.createTextNode(str(value))) root.appendChild(elem) - return root - - -#-----------------------------------------------# + return root + + +# -----------------------------------------------# # Client -#-----------------------------------------------# +# -----------------------------------------------# class Client(BaseObject): ''' The Client object @@ -362,254 +378,247 @@ def __init__(self): attributes for this class ''' # self.object_name = 'client' - for att in ('client_id', 'first_name', 'last_name', 'organization','email', 'username', 'password', 'work_phone', 'home_phone', 'mobile', 'fax', 'notes', 'p_street1', 'p_street2', 'p_city', 'p_state', 'p_country', 'p_code','s_street1', 's_street2', 's_city', 's_state', 's_country', 's_code', 'url'): + for att in ('client_id', 'first_name', 'last_name', 'organization','email', 'username', 'password', + 'work_phone', 'home_phone', 'mobile', 'fax', 'notes', 'p_street1', 'p_street2', 'p_city', + 'p_state', 'p_country', 'p_code','s_street1', 's_street2', 's_city', 's_state', 's_country', + 's_code', 'url'): setattr(self, att, None) -#-----------------------------------------------# +# -----------------------------------------------# # Invoice -#-----------------------------------------------# +# -----------------------------------------------# class Invoice(BaseObject): ''' The Invoice object ''' object_name = 'invoice' - TYPE_MAPPINGS = {'invoice_id' : 'int', 'client_id' : 'int', - 'po_number' : 'int', 'discount' : 'float', 'amount' : 'float', - 'date' : 'datetime', 'amount_outstanding' : 'float', - 'paid' : 'float'} + TYPE_MAPPINGS = {'invoice_id': 'int', 'client_id': 'int', 'po_number': 'int', 'discount': 'float', + 'amount': 'float', 'date': 'datetime', 'amount_outstanding': 'float', 'paid': 'float'} def __init__(self): ''' The constructor is where we initially create the attributes for this class ''' - for att in ('invoice_id', 'client_id', 'number', 'date', 'po_number', - 'terms', 'first_name', 'last_name', 'organization', 'p_street1', 'p_street2', - 'p_city','p_state', 'p_country', 'p_code', 'amount', 'amount_outstanding', 'paid', - 'lines', 'discount', 'status', 'notes', 'url'): + for att in ('invoice_id', 'client_id', 'number', 'date', 'po_number', 'terms', 'first_name', 'last_name', + 'organization', 'p_street1', 'p_street2', 'p_city','p_state', 'p_country', 'p_code', 'amount', + 'amount_outstanding', 'paid', 'lines', 'discount', 'status', 'notes', 'url'): setattr(self, att, None) self.lines = [] self.links = [] -#-----------------------------------------------# +# -----------------------------------------------# # Line--really just a part of Invoice -#-----------------------------------------------# +# -----------------------------------------------# class Line(BaseObject): - TYPE_MAPPINGS = {'unit_cost' : 'float', 'quantity' : 'float', - 'tax1_percent' : 'float', 'tax2_percent' : 'float', 'amount' : 'float'} + TYPE_MAPPINGS = {'unit_cost': 'float', 'quantity': 'float', 'tax1_percent': 'float', 'tax2_percent': 'float', + 'amount': 'float'} def __init__(self): ''' The constructor is where we initially create the attributes for this class ''' - for att in ('name', 'description', 'unit_cost', 'quantity', 'tax1_name', - 'tax2_name', 'tax1_percent', 'tax2_percent', 'amount'): + for att in ('name', 'description', 'unit_cost', 'quantity', 'tax1_name', 'tax2_name', 'tax1_percent', + 'tax2_percent', 'amount'): setattr(self, att, None) @classmethod - def get(cls, object_id, element_name = None): + def get(cls, object_id, element_name=None): ''' The Line doesn't do this ''' raise NotImplementedError("the Line doesn't support this") @classmethod - def list(cls, options = {}, element_name = None): + def list(cls, options={}, element_name=None): ''' The Line doesn't do this ''' raise NotImplementedError("the Line doesn't support this") -#-----------------------------------------------# +# -----------------------------------------------# # Item -#-----------------------------------------------# +# -----------------------------------------------# class Item(BaseObject): ''' The Item object ''' object_name = 'item' - TYPE_MAPPINGS = {'item_id' : 'int', 'unit_cost' : 'float', - 'quantity' : 'int', 'inventory' : 'int'} + TYPE_MAPPINGS = {'item_id': 'int', 'unit_cost': 'float', 'quantity': 'int', 'inventory': 'int'} def __init__(self): ''' The constructor is where we initially create the attributes for this class ''' - for att in ('item_id', 'name', 'description', 'unit_cost', - 'quantity', 'inventory'): + for att in ('item_id', 'name', 'description', 'unit_cost', 'quantity', 'inventory'): setattr(self, att, None) -#-----------------------------------------------# +# -----------------------------------------------# # Payment -#-----------------------------------------------# +# -----------------------------------------------# class Payment(BaseObject): ''' The Payment object ''' object_name = 'payment' - TYPE_MAPPINGS = {'client_id' : 'int', 'invoice_id' : 'int', - 'amount' : 'float', 'date' : 'datetime'} + TYPE_MAPPINGS = {'client_id': 'int', 'invoice_id': 'int', 'amount': 'float', 'date': 'datetime'} def __init__(self): ''' The constructor is where we initially create the attributes for this class ''' - for att in ('payment_id', 'client_id', 'invoice_id', 'date', - 'amount', 'type', 'notes'): + for att in ('payment_id', 'client_id', 'invoice_id', 'date', 'amount', 'type', 'notes'): setattr(self, att, None) -#-----------------------------------------------# +# -----------------------------------------------# # Recurring -#-----------------------------------------------# +# -----------------------------------------------# class Recurring(BaseObject): ''' The Recurring object ''' object_name = 'recurring' - TYPE_MAPPINGS = {'recurring_id' : 'int', 'client_id' : 'int', - 'po_number' : 'int', 'discount' : 'float', 'amount' : 'float', - 'occurrences' : 'int', 'date' : 'datetime'} + TYPE_MAPPINGS = {'recurring_id': 'int', 'client_id': 'int', 'po_number': 'int', 'discount': 'float', + 'amount': 'float', 'occurrences': 'int', 'date': 'datetime'} def __init__(self): ''' The constructor is where we initially create the attributes for this class ''' - for att in ('recurring_id', 'client_id', 'date', 'po_number', - 'terms', 'first_name', 'last_name', 'organization', 'p_street1', 'p_street2', 'p_city','p_state', 'p_country', 'p_code', 'amount', 'lines', 'discount', 'status', 'notes', 'occurrences', 'frequency', 'stopped', 'send_email', 'send_snail_mail'): + for att in ('recurring_id', 'client_id', 'date', 'po_number', 'terms', 'first_name', 'last_name', + 'organization', 'p_street1', 'p_street2', 'p_city','p_state', 'p_country', 'p_code', 'amount', + 'lines', 'discount', 'status', 'notes', 'occurrences', 'frequency', 'stopped', 'send_email', + 'send_snail_mail'): setattr(self, att, None) self.lines = [] -#-----------------------------------------------# +# -----------------------------------------------# # Project -#-----------------------------------------------# +# -----------------------------------------------# class Project(BaseObject): ''' The Project object ''' object_name = 'project' - TYPE_MAPPINGS = {'project_id' : 'int', 'client_id' : 'int', - 'rate' : 'float'} + TYPE_MAPPINGS = {'project_id': 'int', 'client_id': 'int', 'rate': 'float'} def __init__(self): ''' The constructor is where we initially create the attributes for this class ''' - for att in ('project_id', 'client_id', 'name', 'bill_method','rate', - 'description', 'tasks'): + for att in ('project_id', 'client_id', 'name', 'bill_method', 'rate', 'description', 'tasks'): setattr(self, att, None) self.tasks = [] -#-----------------------------------------------# +# -----------------------------------------------# # Task -#-----------------------------------------------# +# -----------------------------------------------# class Task(BaseObject): ''' The Task object ''' object_name = 'task' - TYPE_MAPPINGS = {'task_id' : 'int', 'rate' : 'float', 'billable' : 'bool'} + TYPE_MAPPINGS = {'task_id': 'int', 'rate': 'float', 'billable': 'bool'} def __init__(self): ''' The constructor is where we initially create the attributes for this class ''' - for att in ('task_id', 'name', 'billable', 'rate', - 'description'): + for att in ('task_id', 'name', 'billable', 'rate', 'description'): setattr(self, att, None) -#-----------------------------------------------# +# -----------------------------------------------# # TimeEntry -#-----------------------------------------------# +# -----------------------------------------------# class TimeEntry(BaseObject): ''' The TimeEntry object ''' object_name = 'time_entry' - TYPE_MAPPINGS = {'time_entry_id' : 'int', 'project_id' : 'int', 'task_id' : 'int', 'hours' : 'float', 'date' : 'datetime'} + TYPE_MAPPINGS = {'time_entry_id': 'int', 'project_id': 'int', 'task_id': 'int', 'staff_id': 'int', + 'hours': 'float', 'date': 'datetime'} def __init__(self): ''' The constructor is where we initially create the attributes for this class ''' - for att in ('time_entry_id', 'project_id', 'task_id', 'hours', - 'notes', 'date'): + for att in ('time_entry_id', 'project_id', 'task_id', 'staff_id', 'hours', 'notes', 'date'): setattr(self, att, None) -#-----------------------------------------------# +# -----------------------------------------------# # Estimate -#-----------------------------------------------# +# -----------------------------------------------# class Estimate(BaseObject): ''' The Estimate object ''' object_name = 'estimate' - TYPE_MAPPINGS = {'estimate_id' : 'int', 'client_id' : 'int', - 'po_number' : 'int', 'discount' : 'float', 'amount' : 'float', - 'date' : 'datetime'} + TYPE_MAPPINGS = {'estimate_id': 'int', 'client_id': 'int', 'po_number': 'int', 'discount': 'float', + 'amount': 'float', 'date': 'datetime'} def __init__(self): ''' The constructor is where we initially create the attributes for this class ''' - for att in ('estimate_id', 'client_id', 'status', 'date', 'po_number', - 'terms', 'first_name', 'last_name', 'organization', 'p_street1', 'p_street2', 'p_city','p_state', 'p_country', 'p_code', 'lines', 'discount', 'amount', 'notes'): + for att in ('estimate_id', 'client_id', 'status', 'date', 'po_number', 'terms', 'first_name', 'last_name', + 'organization', 'p_street1', 'p_street2', 'p_city','p_state', 'p_country', 'p_code', 'lines', + 'discount', 'amount', 'notes'): setattr(self, att, None) self.lines = [] -#-----------------------------------------------# +# -----------------------------------------------# # Expense -#-----------------------------------------------# +# -----------------------------------------------# class Expense(BaseObject): ''' The Expense object ''' object_name = 'expense' - TYPE_MAPPINGS = {'expense_id' : 'int', 'staff_id' : 'int', - 'client_id' : 'int', 'category_id' : 'int', 'project_id' : 'int', - 'amount' : 'float', 'date' : 'datetime'} + TYPE_MAPPINGS = {'expense_id': 'int', 'staff_id': 'int', 'client_id': 'int', 'category_id': 'int', + 'project_id': 'int', 'amount': 'float', 'date': 'datetime'} def __init__(self): ''' The constructor is where we initially create the attributes for this class ''' - for att in ('expense_id', 'staff_id', 'category_id', 'client_id', 'project_id', 'date', 'amount', 'notes', 'status'): + for att in ('expense_id', 'staff_id', 'category_id', 'client_id', 'project_id', 'date', 'amount', 'notes', + 'status'): setattr(self, att, None) -#-----------------------------------------------# +# -----------------------------------------------# # Category -#-----------------------------------------------# +# -----------------------------------------------# class Category(BaseObject): ''' The Category object ''' object_name = 'category' - TYPE_MAPPINGS = {'category_id' : 'int', 'tax1' : 'float', - 'tax2' : 'float'} + TYPE_MAPPINGS = {'category_id': 'int', 'tax1': 'float', 'tax2': 'float'} def __init__(self): ''' @@ -619,33 +628,30 @@ def __init__(self): for att in ('category_id', 'name', 'tax1', 'tax2'): setattr(self, att, None) -#-----------------------------------------------# + +# -----------------------------------------------# # Staff -#-----------------------------------------------# +# -----------------------------------------------# class Staff(BaseObject): ''' The Staff object ''' object_name = 'staff' - TYPE_MAPPINGS = {'staff_id' : 'int', 'rate' : 'float', - 'last_login' : 'datetime', - 'signup_date' : 'datetime'} + TYPE_MAPPINGS = {'staff_id': 'int', 'rate': 'float', 'last_login': 'datetime', 'signup_date': 'datetime'} def __init__(self): ''' The constructor is where we initially create the attributes for this class ''' - for att in ('staff_id', 'username', 'first_name', 'last_name', - 'email', 'business_phone', 'mobile_phone', 'rate', 'last_login', - 'number_of_logins', 'signup_date', - 'street1', 'street2', 'city', 'state', 'country', 'code'): + for att in ('staff_id', 'username', 'first_name', 'last_name', 'email', 'business_phone', 'mobile_phone', + 'rate', 'last_login', 'number_of_logins', 'signup_date', 'street1', 'street2', 'city', 'state', + 'country', 'code'): setattr(self, att, None) @classmethod - def list(cls, options = {}, get_all=False): + def list(cls, options={}, get_all=False): ''' Return a list of this object ''' return super(Staff, cls).list(options, element_name='member', get_all=get_all) -