Search code examples
pythontornado

Instantiate a Class as an Iterable


I have a Class that I'm instantiating and then passing into a Tornado Web template. Both functions return a list, but I'm missing something in making the Class itself an iterable object. I'm afraid it's something fundamental I'm doing incorrectly. I'm making REST API calls, parsing the returned XML and returning some of the data to the webapp. Here's the code:

The API calls:

class GetVMList:

    def __init__(self):
        user = 'contoso\\administrator'
        password = "apassword"
        url = "http://scspf:8090/SC2012/VMM/Microsoft.Management.Odata.svc/VirtualMachines?$filter=VMMServer%20eq%20'scvmm'"

        passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
        passman.add_password(None, url, user, password)
        # create the NTLM authentication handler
        auth_NTLM = HTTPNtlmAuthHandler.HTTPNtlmAuthHandler(passman)

        # create and install the opener
        opener = urllib2.build_opener(auth_NTLM)
        urllib2.install_opener(opener)

        # retrieve the result
        self.response = urllib2.urlopen(url)
        self.data = self.response.read()

    def name(self):
        dom = parseString(self.data)
        raw_xml = dom.getElementsByTagName('d:Name')
        clean_xml = []
        clean_data = []
        for i in raw_xml:
            clean_xml.append(i.toxml())
        for i in clean_xml:
            clean_data.append(i.replace('<d:Name>', '').replace('</d:Name>', ''))
        return clean_data

    def os(self):
        dom = parseString(self.data)
        raw_xml = dom.getElementsByTagName('d:OperatingSystem')
        clean_xml = []
        clean_data = []
        for i in raw_xml:
            clean_xml.append(i.toxml())
        for i in clean_xml:
            clean_data.append(i.replace('<d:OperatingSystem>', '').replace('</d:OperatingSystem>', ''))
        return clean_data

The instantiation:

class ListHandler(tornado.web.RequestHandler):
    def get(self):
        self.render('temp/search.html', data='')

    def post(self):
        vm_list = GetVMList()
        self.render('temp/search.html', data=vm_list)

And then the template contains this:

{% for vm in data %}
<li>{{ vm.name }} running {{ vm.os }}</li>
{% end %}

The error is: TypeError: iteration over non-sequence. I would imagine I need to use __iter__ in my Class, but I'm not sure I understand exactly how it works.


Solution

  • My advice would be the following:

    1. Create a class VM for storing information about a single VM. Its __init__ should take the information you want to store about each VM and set it as attributes on the instance. If you don't need any actual code to go along with the data about the VM, you can use a collections.namedtuple, which will save you writing an __init__() method.

    2. Write getVMs() as a generator that, given a user, password, and URL, yields a sequence of VM instances. This result can be iterated over as-is, or can easily be converted to a regular list if you need one (just pass it to list()) or used to create a dictionary that maps VM names to OSs or vice versa.

    e.g. (this code hasn't been tested):

    class VM(object):
        def __init__(self, name, os):
            self.name = name
            self.os   = os
    
    def getVMs(user, password, URL):
        passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
        passman.add_password(None, url, user, password)
        auth_NTLM = HTTPNtlmAuthHandler.HTTPNtlmAuthHandler(passman)
        urllib2.install_opener(urllib2.build_opener(auth_NTLM))
        dom = parseString(urllib2.urlopen(url).read())
        for vmnode in dom.getElementsByTagName('d:VM')   # the tag representing a VM
            name = vmnode.getElementsByTagName('d:Name')[0]  # get name of current VM
            name = name.replace('<d:Name>', '').replace('</d:Name>', '')
            os = vmnode.getElementsByTagName('d:OperatingSystem')[0]   # same for OS
            os = os.replace('<d:OperatingSystem>', '').replace('</d:OperatingSystem>', ''))
            yield VM(name, os)
    

    ... you could also give your VM objects the XML for the name and OS, or the XML for the whole VM, but this sample implementation only does the name and OS as strings.

    (There are better ways to get the contents of a DOM node without resorting to replacing the XML tags with blank strings, but I don't have time to do that right now.)

    Calling it:

    user = r"contoso\administrator"
    pass = "apassword"
    url  = ("http://scspf:8090/SC2012/VMM/Microsoft.Management.Odata.svc"
            "/VirtualMachines?$filter=VMMServer%20eq%20'scvmm'")
    
    vmlist = list(getVMs(user, pass, url))
    

    Or to just print the info for each VM without storing an intermediate list:

    for vm in getVMs(user, pass, url):
        print vm.name, vm.os
    

    Or to build a dictionary of names to VM instances (assuming a recent version of Python that has dict comprehensions):

    vmdict = {vm.name: vm for vm in getVMs(user, pass, url)}
    

    Using the generator model makes it maximally flexible for the caller. Even if that caller is you, it'll make your life easier.