Permalink
Browse files

Added a small library that converts XML to Python dictionaries, flesh…

…ed out the _parse_xml and _parse_field functions which cast the strings returned by Lighthouse into python literals (datetimes, integers, boolean).
  • Loading branch information...
1 parent 5e45529 commit 6bf4f82ffdde839ad2ff49e72b17b34b01b2952a Clinton Ecker committed Feb 1, 2009
Showing with 321 additions and 29 deletions.
  1. +120 −29 lighthouse.py
  2. +201 −0 xmltodict.py
View
@@ -3,48 +3,139 @@
# Python Lighthouse API
#
# Lighthouse simple hosted issue tracking, bug tracking, and project
-# management software.
+# management software.
#
# They also have an XML-based API for working with your projects
#
+# http://lighthouseapp.com
+# http://lighthouseapp.com/api
+#
# Created by Clinton Ecker on 2009-01-31.
# Copyright 2009 Clint Ecker. All rights reserved.
#
+import urllib
+import os.path
+import pprint
+from xmltodict import xmltodict
+from dateutil.parser import *
+from dateutil.tz import *
+
class Lighthouse(object):
- """The main lighthouse object for managing the connection"""
- def __init__(self, arg):
- super(Lighthouse, self).__init__()
- self.arg = arg
+ """The main lighthouse object for managing the connection"""
+
+ def __init__(self, token=None, url=None):
+ super(Lighthouse, self).__init__()
+ self.token = token
+ self.url = url
+
+ def _datetime(self, data):
+ return parse(data)
+
+ def _integer(self, data):
+ if data:
+ return int(data,10)
+ else:
+ return None
+
+ def _boolean(self, data):
+ if data == 'true' or data == '1' or data == 1:
+ return True
+ else:
+ return False
+
+ def _string(self, data):
+ return data
+
+ def _nil(self):
+ return None
+
+ def _get_data(self, path):
+ if self.token != None and self.url != None:
+ endpoint = os.path.join(self.url, path)
+ rh = urllib.urlopen(endpoint)
+ data = rh.read()
+ return self._parse_xml(data)
+ else:
+ print "Error: Please set token and url properly"
+
+ def _parse_xml(self, xmldata):
+ return xmltodict(xmldata)
+
+ def _parse_field(self, field):
+ field_type = None
+ field_name = None
+ field_attributes = None
+ converter = None
+
+ attributes = field.get('attributes', {})
+ field_value = field.get('cdata', None)
+ field_name = field.get('name', None)
+
+ if attributes:
+ field_type = attributes.get('type', None)
+ if field_type:
+ converter = getattr(self,'_'+field_type)
+ field_value = converter(field_value)
+
+ return (field_name, field_value, field_type)
+
+ @property
+ def projects(self):
+ path = 'projects.xml'
+ project_list = self._get_data(path)
+ #pprint.pprint(project_list['children'][0]['children'])
+ projects = []
+ for project in project_list['children']:
+ p_obj = Project()
+ for field in project['children']:
+ field_name, field_value, field_type = self._parse_field(field)
+ print "%s: %s" % (field_name, field_value)
+
class Ticket(object):
- """Tickets are individual issues or bugs"""
- def __init__(self, arg):
- super(Ticket, self).__init__()
- self.arg = arg
+ """Tickets are individual issues or bugs"""
+ def __init__(self, arg):
+ super(Ticket, self).__init__()
+ self.arg = arg
class Project(object):
- """Projects contain milestones, tickets, messages, and changesets"""
- def __init__(self, arg):
- super(Project, self).__init__()
- self.arg = arg
+ """Projects contain milestones, tickets, messages, and changesets"""
+ def __init__(self, archived=None, created_at=None,
+ description=None, p_id=None, name=None, open_tickets_count=None,
+ permalink=None, public=None, updated_at=None, open_states=None,
+ closed_states=None, open_states_list=None, closed_states_list=None):
+
+ super(Project, self).__init__()
+
+ self.archived = archived
+ self.created_at = created_at
+ self.description = description
+ self.id = p_id
+ self.name = name
+ self.permalink = permalink
+ self.public = public
+ self.updated_at = updated_at
+ self.open_states = open_states
+ self.closed_states = closed_states
+ self.open_states_list = open_states_list
+ self.closed_states_list = closed_states_list
+ self.open_tickets_count = open_tickets_count
class Milestone(object):
- """Milestones reference tickets"""
- def __init__(self, arg):
- super(Milestone, self).__init__()
- self.arg = arg
-
+ """Milestones reference tickets"""
+ def __init__(self, arg):
+ super(Milestone, self).__init__()
+ self.arg = arg
+
class Message(object):
- """Messages are notes"""
- def __init__(self, arg):
- super(Message, self).__init__()
- self.arg = arg
-
+ """Messages are notes"""
+ def __init__(self, arg):
+ super(Message, self).__init__()
+ self.arg = arg
+
class User(object):
- """A user"""
- def __init__(self, arg):
- super(User, self).__init__()
- self.arg = arg
-
-
+ """A user"""
+ def __init__(self, arg):
+ super(User, self).__init__()
+ self.arg = arg
View
@@ -0,0 +1,201 @@
+""" xmltodict(): convert xml into tree of Python dicts.
+
+This was copied and modified from John Bair's recipe at aspn.activestate.com:
+ http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/149368
+"""
+import os
+import string
+from xml.parsers import expat
+
+# Python seems to need to compile code with \n linesep:
+code_linesep = "\n"
+
+class Xml2Obj:
+ """XML to Object"""
+ def __init__(self):
+ self.root = None
+ self.nodeStack = []
+ self.attsToSkip = []
+ self._inCode = False
+ self._mthdName = ""
+ self._mthdCode = ""
+ self._codeDict = None
+
+
+ def StartElement(self, name, attributes):
+ """SAX start element even handler"""
+ if name == "code":
+ # This is code for the parent element
+ self._inCode = True
+ parent = self.nodeStack[-1]
+ if not parent.has_key("code"):
+ parent["code"] = {}
+ self._codeDict = parent["code"]
+
+ else:
+ if self._inCode:
+ self._mthdName = name.encode()
+ else:
+ element = {"name": name.encode()}
+ if len(attributes) > 0:
+ for att in self.attsToSkip:
+ if attributes.has_key(att):
+ del attributes[att]
+ element["attributes"] = attributes
+
+ # Push element onto the stack and make it a child of parent
+ if len(self.nodeStack) > 0:
+ parent = self.nodeStack[-1]
+ if not parent.has_key("children"):
+ parent["children"] = []
+ parent["children"].append(element)
+ else:
+ self.root = element
+ self.nodeStack.append(element)
+
+
+ def EndElement(self, name):
+ """SAX end element event handler"""
+ if self._inCode:
+ if name == "code":
+ self._inCode = False
+ self._codeDict = None
+ else:
+ # End of an individual method
+ self._codeDict[self._mthdName] = self._mthdCode
+ self._mthdName = ""
+ self._mthdCode = ""
+ else:
+ self.nodeStack = self.nodeStack[:-1]
+
+
+ def CharacterData(self, data):
+ """SAX character data event handler"""
+ if data.strip():
+ data = data.replace("&lt;", "<")
+ data = data.encode()
+ if self._inCode:
+ if self._mthdCode:
+ self._mthdCode += "%s%s" % (code_linesep, data)
+ else:
+ self._mthdCode = data
+ else:
+ element = self.nodeStack[-1]
+ if not element.has_key("cdata"):
+ element["cdata"] = ""
+ element["cdata"] += data
+
+
+ def Parse(self, xml):
+ # Create a SAX parser
+ Parser = expat.ParserCreate()
+ # SAX event handlers
+ Parser.StartElementHandler = self.StartElement
+ Parser.EndElementHandler = self.EndElement
+ Parser.CharacterDataHandler = self.CharacterData
+ # Parse the XML File
+ ParserStatus = Parser.Parse(xml, 1)
+ return self.root
+
+
+ def ParseFromFile(self, filename):
+ return self.Parse(open(filename,"r").read())
+
+
+def xmltodict(xml, attsToSkip=[]):
+ """Given an xml string or file, return a Python dictionary."""
+ parser = Xml2Obj()
+ parser.attsToSkip = attsToSkip
+ if os.linesep not in xml and os.path.exists(xml):
+ # argument was a file
+ return parser.ParseFromFile(xml)
+ else:
+ # argument must have been raw xml:
+ return parser.Parse(xml)
+
+
+def dicttoxml(dct, level=0, header=None, linesep=None):
+ """Given a Python dictionary, return an xml string.
+
+ The dictionary must be in the format returned by dicttoxml(), with keys
+ on "attributes", "code", "cdata", "name", and "children".
+
+ Send your own XML header, otherwise a default one will be used.
+
+ The linesep argument is a dictionary, with keys on levels, allowing the
+ developer to add extra whitespace depending on the level.
+ """
+ def escQuote(val, noEscape):
+ """Add surrounding quotes to the string, and escape
+ any illegal XML characters.
+ """
+ if not isinstance(val, basestring):
+ val = str(val)
+ qt = '"'
+ slsh = "\\"
+ val = val.replace("<", "&lt;").replace(">", "&gt;").replace(slsh, slsh+slsh)
+ if not noEscape:
+ # First escape internal ampersands:
+ val = val.replace("&", "&amp;")
+ # Escape any internal quotes
+ val = val.replace('"', '&quot;').replace("'", "&apos;")
+ return "%s%s%s" % (qt, val, qt)
+
+ att = ""
+ ret = ""
+
+ if dct.has_key("attributes"):
+ for key, val in dct["attributes"].items():
+ # Some keys are already handled.
+ noEscape = key in ("sizerInfo",)
+ val = escQuote(val, noEscape)
+ att += " %s=%s" % (key, val)
+
+ ret += "%s<%s%s" % ("\t" * level, dct["name"], att)
+
+ if (not dct.has_key("cdata") and not dct.has_key("children")
+ and not dct.has_key("code")):
+ ret += " />%s" % os.linesep
+ else:
+ ret += ">"
+ if dct.has_key("cdata"):
+ ret += "%s" % dct["cdata"].replace("<", "&lt;")
+
+ if dct.has_key("code"):
+ if len(dct["code"].keys()):
+ ret += "%s%s<code>%s" % (os.linesep, "\t" * (level+1), os.linesep)
+ methodTab = "\t" * (level+2)
+ for mthd, cd in dct["code"].items():
+ # Convert \n's in the code to os.linesep:
+ cd = os.linesep.join(cd.splitlines())
+
+ # Make sure that the code ends with a linefeed
+ if not cd.endswith(os.linesep):
+ cd += os.linesep
+
+ ret += "%s<%s><![CDATA[%s%s]]>%s%s</%s>%s" % (methodTab,
+ mthd, os.linesep, cd, os.linesep,
+ methodTab, mthd, os.linesep)
+ ret += "%s</code>%s" % ("\t" * (level+1), os.linesep)
+
+
+ if dct.has_key("children") and len(dct["children"]) > 0:
+ ret += os.linesep
+ for child in dct["children"]:
+ ret += dicttoxml(child, level+1, linesep=linesep)
+ indnt = ""
+ if ret.endswith(os.linesep):
+ # Indent the closing tag
+ indnt = ("\t" * level)
+ ret += "%s</%s>%s" % (indnt, dct["name"], os.linesep)
+
+ if linesep:
+ ret += linesep.get(level, "")
+
+ if level == 0:
+ if header is None:
+ header = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>%s' \
+ % os.linesep
+ ret = header + ret
+
+ return ret

0 comments on commit 6bf4f82

Please sign in to comment.