Index: server/lib/ipp/attributegroup.py
===================================================================
--- server/lib/ipp/attributegroup.py	(revision 94211dfae532f252aae9d59b4fd206e208b7fe83)
+++ server/lib/ipp/attributegroup.py	(revision 776a659edf6a8f91d1a6fb6f009e4abac41bf064)
@@ -58,4 +58,5 @@
     def __iter__(self):
         return (a.name for a in self.attributes)
+
     iterkeys = __iter__
 
@@ -69,5 +70,6 @@
 
     def __delitem__(self, name):
-        self.attributes = filter(lambda x: x.name != name, self.attributes)
+        self.attributes = filter(lambda x: x.name != name,
+				 self.attributes)
 
     def append(self, attribute):
Index: server/lib/server.py
===================================================================
--- server/lib/server.py	(revision 1176f8b7b194ba6ef8b67441c731da5624cb71d4)
+++ 	(revision )
@@ -1,314 +1,0 @@
-#!/usr/bin/python
-
-import logging, BaseHTTPServer
-import time
-import ipp
-import ipp.constants as const
-from ipp.constants import Operations
-
-logging.basicConfig(level=logging.DEBUG)
-
-def handler_for(operation):
-    def f(func):
-        func.ipp_operation = operation
-        return func
-    return f
-
-class IPPRequestHandler(object):
-    def _ipp_dispatch(self, request):
-        for d in dir(self):
-            if getattr(getattr(self, d), "ipp_operation", None) == request.operation_id:
-                return getattr(self, d)
-        return self.unknown_operation
-
-    def unknown_operation(self, request, response):
-        print "Received UNKNOWN OPERATION %x" % request.operation_id
-        response.operation_id = const.StatusCodes.OPERATION_NOT_SUPPORTED
-
-class PrinterRequestHandler(IPPRequestHandler):
-    def __init__(self, name):
-        self.name = name
-
-    @handler_for(Operations.GET_PRINTER_ATTRIBUTES)
-    def get_printer_attributes(self, request, response):
-        printer_attributes = ipp.AttributeGroup(const.AttributeTags.PRINTER)
-        printer_attributes[:] = \
-            ipp.Attribute("printer-uri-supported",
-                          [ipp.Value(ipp.Tags.URI,
-                                     "ipp://localhost:8000/printers/"+self.name)]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("uri-authentication-supported",
-                          [ipp.Value(ipp.Tags.KEYWORD,
-                                     "none")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("uri-security-supported",
-                          [ipp.Value(ipp.Tags.KEYWORD,
-                                     "none")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("printer-name",
-                          [ipp.Value(ipp.Tags.NAME_WITHOUT_LANGUAGE,
-                                     self.name)]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("printer-state",
-                          [ipp.Value(ipp.Tags.ENUM,
-                                     const.PrinterStates.IDLE)]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("printer-state-reasons",
-                          [ipp.Value(ipp.Tags.KEYWORD,
-                                     "none")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("ipp-versions-supported",
-                          [ipp.Value(ipp.Tags.KEYWORD,
-                                     "1.0"),
-                           ipp.Value(ipp.Tags.KEYWORD,
-                                     "1.1")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("operations-supported",
-                          [ipp.Value(ipp.Tags.ENUM,
-                                     Operations.GET_JOBS)]
-                          ) #XXX: We should query ourself for the supported operations
-        printer_attributes[:] = \
-            ipp.Attribute("charset-configured",
-                          [ipp.Value(ipp.Tags.CHARSET,
-                                     "utf-8")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("charset-supported",
-                          [ipp.Value(ipp.Tags.CHARSET,
-                                     "utf-8")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("natural-language-configured",
-                          [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
-                                     "en-us")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("generated-natural-language-supported",
-                          [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
-                                     "en-us")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("document-format-default",
-                          [ipp.Value(ipp.Tags.MIME_MEDIA_TYPE,
-                                     "application/octet-stream")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("document-format-supported",
-                          [ipp.Value(ipp.Tags.MIME_MEDIA_TYPE,
-                                     "application/octet-stream"),
-                           ipp.Value(ipp.Tags.MIME_MEDIA_TYPE,
-                                     "audio/mp3")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("printer-is-accepting-jobs",
-                          [ipp.Value(ipp.Tags.BOOLEAN,
-                                     True)]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("queued-job-count",
-                          [ipp.Value(ipp.Tags.INTEGER,
-                                     1)]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("pdl-override-supported",
-                          [ipp.Value(ipp.Tags.KEYWORD,
-                                     "not-attempted")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("printer-up-time",
-                          [ipp.Value(ipp.Tags.INTEGER,
-                                     int(time.time()))]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("compression-supported",
-                          [ipp.Value(ipp.Tags.KEYWORD,
-                                     "none")]
-                          )
-
-        response.attribute_groups.append(printer_attributes)
-        response.operation_id = const.StatusCodes.OK
-        print "get_printer_attributes called"
-
-class RootRequestHandler(IPPRequestHandler):
-    printers = [PrinterRequestHandler(name="sipbmp3")]
-
-    @handler_for(Operations.CUPS_GET_DEFAULT)
-    def cups_get_default(self, request, response):
-        print "get_default called"
-        return self.printers[0].get_printer_attributes(request, response)
-
-    @handler_for(Operations.CUPS_GET_PRINTERS)
-    def cups_get_printers(self, request, response):
-        print "get_printers called"
-        response.operation_id = const.StatusCodes.OK
-        for p in self.printers:
-            # Each printer will append a new printer attribute group.
-            p.get_printer_attributes(request, response)
-
-    @handler_for(Operations.CUPS_GET_CLASSES)
-    def cups_get_classes(self, request, response):
-        print "get_classes called"
-        response.operation_id = const.StatusCodes.OK
-        # We have no printer classes, so nothing to return.
-
-class GutenbachIPPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
-    def setup(self):
-        self.root = RootRequestHandler()
-        BaseHTTPServer.BaseHTTPRequestHandler.setup(self)
-
-    def handle_one_request(self):
-        self.raw_requestline = self.rfile.readline()
-        if not self.raw_requestline:
-            self.close_connection = 1
-            return
-        if not self.parse_request(): # An error code has been sent, just exit
-            return
-        self.handle_ipp()
-
-    def handle_ipp(self):
-        length = int(self.headers.getheader('content-length', 0))
-        request = ipp.Request(request=self.rfile,
-                              length=length)
-        print "Received request:", repr(request)
-
-        response_kwargs = {}
-        response_kwargs['version'] = request.version
-        response_kwargs['operation_id'] = const.StatusCodes.INTERNAL_ERROR
-        response_kwargs['request_id'] = request.request_id
-        response_kwargs['attribute_groups'] = [
-            ipp.AttributeGroup(const.AttributeTags.OPERATION,
-                               [ipp.Attribute('attributes-charset',
-                                              [ipp.Value(ipp.Tags.CHARSET,
-                                                         'utf-8'
-                                                         )]),
-                                ipp.Attribute('attributes-natural-language',
-                                              [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
-                                                         'en-us'
-                                                         )])
-                                ])]
-
-        response = ipp.Request(**response_kwargs)
-        handler = self.root._ipp_dispatch(request)
-        handler(request, response)
-        print "Sending response:", repr(response)
-
-        self.send_response(200, "o hai")
-        self.send_header("Content-Type", "application/ipp")
-        self.send_header("Connection", "close")
-        self.end_headers()
-        self.wfile.write(response.packed_value)
-
-    def get_jobs(self, request, response_kwargs):
-        """get-jobs response"""
-
-        job_attributes = [ipp.Attribute('job-id',
-                                       [ipp.Value(ipp.Tags.INTEGER,
-                                                 12345,
-                                                 )]),
-                          ipp.Attribute('job-name',
-                                       [ipp.Value(ipp.Tags.NAME_WITHOUT_LANGUAGE,
-                                                 'foo',
-                                                 )]),
-                          ipp.Attribute('job-originating-user-name',
-                                       [ipp.Value(ipp.Tags.NAME_WITHOUT_LANGUAGE,
-                                                 'jhamrick',
-                                                 )]),
-                          ipp.Attribute('job-k-octets',
-                                       [ipp.Value(ipp.Tags.INTEGER,
-                                                 1,
-                                                 )]),
-                          ipp.Attribute('job-state',
-                                       [ipp.Value(ipp.Tags.ENUM,
-                                                 const.JobStates.HELD,
-                                                 )]),
-                          ipp.Attribute('job-printer-uri',
-                                       [ipp.Value(ipp.Tags.URI,
-                                                 'ipp://localhost:8000/printers/foo',
-                                                 )])]
-
-
-        #req_op_attributes = request.getAttributeGroup(ipp.Tags.OPERATION_ATTRIBUTES_TAG)
-        #print req_op_attributes
-        #printer_uri = req_op_attributes[0].getAttribute('printer-uri')
-
-        op_attributes = [ipp.Attribute('attributes-charset',
-                                      [ipp.Value(ipp.Tags.CHARSET,
-                                                'utf-8',
-                                                )]),
-                         ipp.Attribute('attributes-natural-language',
-                                      [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
-                                                'en-us',
-                                                )])]
-        
-        job_attribute_group = ipp.AttributeGroup(const.AttributeTags.JOB,
-                                                 job_attributes)
-        op_attributes_group = ipp.AttributeGroup(const.AttributeTags.OPERATION,
-                                                 op_attributes)
-        response_kwargs['attribute_groups'] = [op_attributes_group,job_attribute_group]
-        response_kwargs['operation_id'] = const.StatusCodes.OK
-
-        return response_kwargs
-
-    ##### Printer Commands
-
-    def print_job(self, request):
-        pass
-
-    def validate_job(self, request):
-        pass
-
-    #def get_jobs(self, request):
-    #    pass
-
-    def print_uri(self, request):
-        pass
-
-    def create_job(self, request):
-        pass
-
-    def pause_printer(self, request):
-        pass
-
-    def resume_printer(self, request):
-        pass
-
-    def set_printer_attributes(self, request):
-        pass
-
-    ##### Job Commands
-
-    def cancel_job(self, request):
-        pass
-
-    def get_job_attributes(self, request):
-        pass
-
-    def send_document(self, request):
-        pass
-
-    def send_uri(self, request):
-        pass
-
-    def set_job_attributes(self, request):
-        pass
-
-    def cups_get_document(self, request):
-        pass
-
-    def restart_job(self, request):
-        pass
-
-    def promote_job(self, request):
-        pass
-
-if __name__ == '__main__':
-    server_address = ('', 8000)
-    httpd = BaseHTTPServer.HTTPServer(server_address, GutenbachIPPHandler)
-    httpd.serve_forever()
Index: server/lib/server/__init__.py
===================================================================
--- server/lib/server/__init__.py	(revision 776a659edf6a8f91d1a6fb6f009e4abac41bf064)
+++ server/lib/server/__init__.py	(revision 776a659edf6a8f91d1a6fb6f009e4abac41bf064)
@@ -0,0 +1,314 @@
+#!/usr/bin/python
+
+import logging, BaseHTTPServer
+import time
+import ipp
+import ipp.constants as const
+from ipp.constants import Operations
+
+logging.basicConfig(level=logging.DEBUG)
+
+def handler_for(operation):
+    def f(func):
+        func.ipp_operation = operation
+        return func
+    return f
+
+class IPPRequestHandler(object):
+    def _ipp_dispatch(self, request):
+        for d in dir(self):
+            if getattr(getattr(self, d), "ipp_operation", None) == request.operation_id:
+                return getattr(self, d)
+        return self.unknown_operation
+
+    def unknown_operation(self, request, response):
+        print "Received UNKNOWN OPERATION %x" % request.operation_id
+        response.operation_id = const.StatusCodes.OPERATION_NOT_SUPPORTED
+
+class PrinterRequestHandler(IPPRequestHandler):
+    def __init__(self, name):
+        self.name = name
+
+    @handler_for(Operations.GET_PRINTER_ATTRIBUTES)
+    def get_printer_attributes(self, request, response):
+        printer_attributes = ipp.AttributeGroup(const.AttributeTags.PRINTER)
+        printer_attributes[:] = \
+            ipp.Attribute("printer-uri-supported",
+                          [ipp.Value(ipp.Tags.URI,
+                                     "ipp://localhost:8000/printers/"+self.name)]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("uri-authentication-supported",
+                          [ipp.Value(ipp.Tags.KEYWORD,
+                                     "none")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("uri-security-supported",
+                          [ipp.Value(ipp.Tags.KEYWORD,
+                                     "none")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("printer-name",
+                          [ipp.Value(ipp.Tags.NAME_WITHOUT_LANGUAGE,
+                                     self.name)]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("printer-state",
+                          [ipp.Value(ipp.Tags.ENUM,
+                                     const.PrinterStates.IDLE)]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("printer-state-reasons",
+                          [ipp.Value(ipp.Tags.KEYWORD,
+                                     "none")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("ipp-versions-supported",
+                          [ipp.Value(ipp.Tags.KEYWORD,
+                                     "1.0"),
+                           ipp.Value(ipp.Tags.KEYWORD,
+                                     "1.1")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("operations-supported",
+                          [ipp.Value(ipp.Tags.ENUM,
+                                     Operations.GET_JOBS)]
+                          ) #XXX: We should query ourself for the supported operations
+        printer_attributes[:] = \
+            ipp.Attribute("charset-configured",
+                          [ipp.Value(ipp.Tags.CHARSET,
+                                     "utf-8")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("charset-supported",
+                          [ipp.Value(ipp.Tags.CHARSET,
+                                     "utf-8")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("natural-language-configured",
+                          [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
+                                     "en-us")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("generated-natural-language-supported",
+                          [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
+                                     "en-us")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("document-format-default",
+                          [ipp.Value(ipp.Tags.MIME_MEDIA_TYPE,
+                                     "application/octet-stream")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("document-format-supported",
+                          [ipp.Value(ipp.Tags.MIME_MEDIA_TYPE,
+                                     "application/octet-stream"),
+                           ipp.Value(ipp.Tags.MIME_MEDIA_TYPE,
+                                     "audio/mp3")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("printer-is-accepting-jobs",
+                          [ipp.Value(ipp.Tags.BOOLEAN,
+                                     True)]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("queued-job-count",
+                          [ipp.Value(ipp.Tags.INTEGER,
+                                     1)]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("pdl-override-supported",
+                          [ipp.Value(ipp.Tags.KEYWORD,
+                                     "not-attempted")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("printer-up-time",
+                          [ipp.Value(ipp.Tags.INTEGER,
+                                     int(time.time()))]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("compression-supported",
+                          [ipp.Value(ipp.Tags.KEYWORD,
+                                     "none")]
+                          )
+
+        response.attribute_groups.append(printer_attributes)
+        response.operation_id = const.StatusCodes.OK
+        print "get_printer_attributes called"
+
+class RootRequestHandler(IPPRequestHandler):
+    printers = [PrinterRequestHandler(name="sipbmp3")]
+
+    @handler_for(Operations.CUPS_GET_DEFAULT)
+    def cups_get_default(self, request, response):
+        print "get_default called"
+        return self.printers[0].get_printer_attributes(request, response)
+
+    @handler_for(Operations.CUPS_GET_PRINTERS)
+    def cups_get_printers(self, request, response):
+        print "get_printers called"
+        response.operation_id = const.StatusCodes.OK
+        for p in self.printers:
+            # Each printer will append a new printer attribute group.
+            p.get_printer_attributes(request, response)
+
+    @handler_for(Operations.CUPS_GET_CLASSES)
+    def cups_get_classes(self, request, response):
+        print "get_classes called"
+        response.operation_id = const.StatusCodes.OK
+        # We have no printer classes, so nothing to return.
+
+class GutenbachIPPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+    def setup(self):
+        self.root = RootRequestHandler()
+        BaseHTTPServer.BaseHTTPRequestHandler.setup(self)
+
+    def handle_one_request(self):
+        self.raw_requestline = self.rfile.readline()
+        if not self.raw_requestline:
+            self.close_connection = 1
+            return
+        if not self.parse_request(): # An error code has been sent, just exit
+            return
+        self.handle_ipp()
+
+    def handle_ipp(self):
+        length = int(self.headers.getheader('content-length', 0))
+        request = ipp.Request(request=self.rfile,
+                              length=length)
+        print "Received request:", repr(request)
+
+        response_kwargs = {}
+        response_kwargs['version'] = request.version
+        response_kwargs['operation_id'] = const.StatusCodes.INTERNAL_ERROR
+        response_kwargs['request_id'] = request.request_id
+        response_kwargs['attribute_groups'] = [
+            ipp.AttributeGroup(const.AttributeTags.OPERATION,
+                               [ipp.Attribute('attributes-charset',
+                                              [ipp.Value(ipp.Tags.CHARSET,
+                                                         'utf-8'
+                                                         )]),
+                                ipp.Attribute('attributes-natural-language',
+                                              [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
+                                                         'en-us'
+                                                         )])
+                                ])]
+
+        response = ipp.Request(**response_kwargs)
+        handler = self.root._ipp_dispatch(request)
+        handler(request, response)
+        print "Sending response:", repr(response)
+
+        self.send_response(200, "o hai")
+        self.send_header("Content-Type", "application/ipp")
+        self.send_header("Connection", "close")
+        self.end_headers()
+        self.wfile.write(response.packed_value)
+
+    def get_jobs(self, request, response_kwargs):
+        """get-jobs response"""
+
+        job_attributes = [ipp.Attribute('job-id',
+                                       [ipp.Value(ipp.Tags.INTEGER,
+                                                 12345,
+                                                 )]),
+                          ipp.Attribute('job-name',
+                                       [ipp.Value(ipp.Tags.NAME_WITHOUT_LANGUAGE,
+                                                 'foo',
+                                                 )]),
+                          ipp.Attribute('job-originating-user-name',
+                                       [ipp.Value(ipp.Tags.NAME_WITHOUT_LANGUAGE,
+                                                 'jhamrick',
+                                                 )]),
+                          ipp.Attribute('job-k-octets',
+                                       [ipp.Value(ipp.Tags.INTEGER,
+                                                 1,
+                                                 )]),
+                          ipp.Attribute('job-state',
+                                       [ipp.Value(ipp.Tags.ENUM,
+                                                 const.JobStates.HELD,
+                                                 )]),
+                          ipp.Attribute('job-printer-uri',
+                                       [ipp.Value(ipp.Tags.URI,
+                                                 'ipp://localhost:8000/printers/foo',
+                                                 )])]
+
+
+        #req_op_attributes = request.getAttributeGroup(ipp.Tags.OPERATION_ATTRIBUTES_TAG)
+        #print req_op_attributes
+        #printer_uri = req_op_attributes[0].getAttribute('printer-uri')
+
+        op_attributes = [ipp.Attribute('attributes-charset',
+                                      [ipp.Value(ipp.Tags.CHARSET,
+                                                'utf-8',
+                                                )]),
+                         ipp.Attribute('attributes-natural-language',
+                                      [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
+                                                'en-us',
+                                                )])]
+        
+        job_attribute_group = ipp.AttributeGroup(const.AttributeTags.JOB,
+                                                 job_attributes)
+        op_attributes_group = ipp.AttributeGroup(const.AttributeTags.OPERATION,
+                                                 op_attributes)
+        response_kwargs['attribute_groups'] = [op_attributes_group,job_attribute_group]
+        response_kwargs['operation_id'] = const.StatusCodes.OK
+
+        return response_kwargs
+
+    ##### Printer Commands
+
+    def print_job(self, request):
+        pass
+
+    def validate_job(self, request):
+        pass
+
+    #def get_jobs(self, request):
+    #    pass
+
+    def print_uri(self, request):
+        pass
+
+    def create_job(self, request):
+        pass
+
+    def pause_printer(self, request):
+        pass
+
+    def resume_printer(self, request):
+        pass
+
+    def set_printer_attributes(self, request):
+        pass
+
+    ##### Job Commands
+
+    def cancel_job(self, request):
+        pass
+
+    def get_job_attributes(self, request):
+        pass
+
+    def send_document(self, request):
+        pass
+
+    def send_uri(self, request):
+        pass
+
+    def set_job_attributes(self, request):
+        pass
+
+    def cups_get_document(self, request):
+        pass
+
+    def restart_job(self, request):
+        pass
+
+    def promote_job(self, request):
+        pass
+
+if __name__ == '__main__':
+    server_address = ('', 8000)
+    httpd = BaseHTTPServer.HTTPServer(server_address, GutenbachIPPHandler)
+    httpd.serve_forever()
Index: server/lib/server/exceptions.py
===================================================================
--- server/lib/server/exceptions.py	(revision 776a659edf6a8f91d1a6fb6f009e4abac41bf064)
+++ server/lib/server/exceptions.py	(revision 776a659edf6a8f91d1a6fb6f009e4abac41bf064)
@@ -0,0 +1,13 @@
+class InvalidJobException(Exception):
+    def __init__(self, jobid):
+	self.jobid = jobid
+
+    def __str__(self):
+	return "Job with id '%d' does not exist!" % self.jobid
+
+class InvalidPrinterStateException(Exception):
+    def __init__(self, message):
+	self.message = message
+
+    def __str__(self):
+	return self.message
Index: server/lib/server/job.py
===================================================================
--- server/lib/server/job.py	(revision 776a659edf6a8f91d1a6fb6f009e4abac41bf064)
+++ server/lib/server/job.py	(revision 776a659edf6a8f91d1a6fb6f009e4abac41bf064)
@@ -0,0 +1,81 @@
+import os
+from exceptions import InvalidJobException, \
+     InvalidPrinterStateException
+
+class Job(object):
+
+    def __init__(self, document=None):
+	"""Initialize a Gutenbach job.
+
+	This sets the status to 'initializing' and optionally sets the
+	document to print to the value of document.
+	"""
+	 
+	self._jobid = None
+	self._status = 'initializing'
+	self._document = document
+	self._printer = None
+
+    @property
+    def jobid(self):
+	return self._jobid
+
+    @jobid.setter
+    def jobid(self, val):
+	raise AttributeError("Setting jobid is illegal!")
+
+    @property
+    def document(self):
+	return self._document
+
+    @document.setter
+    def document(self, path):
+	if not os.path.exists(path):
+	    raise IOError("Document '%s' does not exist!" % path)
+	self._document = path
+
+    @property
+    def status(self):
+	return self._status
+
+    @status.setter
+    def status(self, val):
+	raise AttributeError(
+	    "Setting status directly is illegal!  " + \
+	    "Please use enqueue(), play(), or finish().")
+
+    @property
+    def printer(self):
+	return self._printer
+
+    @printer.setter
+    def printer(self, val):
+	raise AttributeError(
+	    "Setting printer directly is illegal!  " + \
+	    "Please use enqueue().")
+
+    def enqueue(self, printer):
+	if self.status != 'initializing':
+	    raise InvalidJobException(
+		"Cannot enqueue a job that has " + \
+		"already been initialized!")
+	self._printer = printer
+	self._status = 'active'
+
+    def play(self):
+	if self.status != 'active':
+	    raise InvalidJobException(
+		"Cannot play an inactive job!")
+	
+	self._status = 'playing'
+	# TODO: add external call to music player
+	self.printer.complete_job(self.jobid)
+
+    def finished(self):
+	self._status = 'finished'
+
+    def __repr__(self):
+	return str(self)
+
+    def __str__(self):
+	return "<Job %d>" % self.jobid
Index: server/lib/server/printer.py
===================================================================
--- server/lib/server/printer.py	(revision 776a659edf6a8f91d1a6fb6f009e4abac41bf064)
+++ server/lib/server/printer.py	(revision 776a659edf6a8f91d1a6fb6f009e4abac41bf064)
@@ -0,0 +1,80 @@
+import ipp
+import alsaaudio as aa
+from ipp.attribute import Attribute
+from .exceptions import InvalidJobException, \
+     InvalidPrinterStateException
+
+class Printer(object):
+
+    def __init__(self, name, card, mixer):
+
+	self.name = name
+
+	if card >= len(aa.cards()):
+	    raise aa.ALSAAudioError(
+		"Audio card at index %d does not exist!" % card)
+	elif mixer not in aa.mixers(card):
+	    raise aa.ALSAAudioError(
+		"Audio mixer '%s' does not exist!" % mixer)
+	
+	self.card = card
+	self.mixer = mixer
+
+	self.finished_jobs = []
+	self.active_jobs = []
+	self.jobs = {}
+
+	self._next_jobid = -1
+
+    @property
+    def next_jobid(self):
+	self._next_jobid += 1
+	return self._next_jobid
+
+    @next_jobid.setter
+    def next_jobid(self, val):
+	raise AttributeError("Setting next_jobid is illegal!")
+
+    def print_job(self, job):
+	jobid = self.next_jobid
+	self.active_jobs.append(jobid)
+	self.jobs[jobid] = job
+	job.jobid = jobid
+	job.enqueue(self)
+	return jobid
+
+    def complete_job(self, jobid):
+	job = self.active_jobs.pop(0)
+	if job.jobid != jobid:
+	    raise InvalidJobException(
+		"Completed job %d has unexpected job id %d!" % \
+		(job.jobid, jobid))
+	
+	self.finished_jobs.append(job)
+	job.finish()
+	return job.jobid
+
+    def start_job(self, jobid):
+	job = self.active_jobs[0]
+	if job.jobid != jobid:
+	    raise InvalidJobException(
+		"Completed job %d has unexpected job id %d!" % \
+		(job.jobid, jobid))
+
+	if job.status == 'playing':
+	    raise InvalidPrinterStateException(
+		"Next job in queue (id %d) is " + \
+		"already playing!" % jobid)
+
+	job.play()
+
+    def get_job(self, jobid):
+	if jobid not in self.jobs:
+	    raise InvalidJobException(jobid)
+	return self.jobs[jobid]
+
+    def __repr__(self):
+	return str(self)
+
+    def __str__(self):
+	return "<Printer '%s'>" % self.name
