root / pkipplib / trunk / pkipplib / pkipplib.py @ 28

Revision 28, 27.9 kB (checked in by jerome, 18 years ago)

It is now possible to create a subscription.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id Revision
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3#
4# pkipplib : IPP and CUPS support for Python
5#
6# (c) 2003, 2004, 2005, 2006 Jerome Alet <alet@librelogiciel.com>
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20#
21# $Id$
22#
23#
24
25import sys
26import os
27import urllib2
28import socket
29from struct import pack, unpack
30
31IPP_VERSION = "1.1"     # default version number
32
33IPP_PORT = 631
34
35IPP_MAX_NAME = 256
36IPP_MAX_VALUES = 8
37
38IPP_TAG_ZERO = 0x00
39IPP_TAG_OPERATION = 0x01
40IPP_TAG_JOB = 0x02
41IPP_TAG_END = 0x03
42IPP_TAG_PRINTER = 0x04
43IPP_TAG_UNSUPPORTED_GROUP = 0x05
44IPP_TAG_SUBSCRIPTION = 0x06
45IPP_TAG_EVENT_NOTIFICATION = 0x07
46IPP_TAG_UNSUPPORTED_VALUE = 0x10
47IPP_TAG_DEFAULT = 0x11
48IPP_TAG_UNKNOWN = 0x12
49IPP_TAG_NOVALUE = 0x13
50IPP_TAG_NOTSETTABLE = 0x15
51IPP_TAG_DELETEATTR = 0x16
52IPP_TAG_ADMINDEFINE = 0x17
53IPP_TAG_INTEGER = 0x21
54IPP_TAG_BOOLEAN = 0x22
55IPP_TAG_ENUM = 0x23
56IPP_TAG_STRING = 0x30
57IPP_TAG_DATE = 0x31
58IPP_TAG_RESOLUTION = 0x32
59IPP_TAG_RANGE = 0x33
60IPP_TAG_BEGIN_COLLECTION = 0x34
61IPP_TAG_TEXTLANG = 0x35
62IPP_TAG_NAMELANG = 0x36
63IPP_TAG_END_COLLECTION = 0x37
64IPP_TAG_TEXT = 0x41
65IPP_TAG_NAME = 0x42
66IPP_TAG_KEYWORD = 0x44
67IPP_TAG_URI = 0x45
68IPP_TAG_URISCHEME = 0x46
69IPP_TAG_CHARSET = 0x47
70IPP_TAG_LANGUAGE = 0x48
71IPP_TAG_MIMETYPE = 0x49
72IPP_TAG_MEMBERNAME = 0x4a
73IPP_TAG_MASK = 0x7fffffff
74IPP_TAG_COPY = -0x7fffffff-1
75
76IPP_RES_PER_INCH = 3
77IPP_RES_PER_CM = 4
78
79IPP_FINISHINGS_NONE = 3
80IPP_FINISHINGS_STAPLE = 4
81IPP_FINISHINGS_PUNCH = 5
82IPP_FINISHINGS_COVER = 6
83IPP_FINISHINGS_BIND = 7
84IPP_FINISHINGS_SADDLE_STITCH = 8
85IPP_FINISHINGS_EDGE_STITCH = 9
86IPP_FINISHINGS_FOLD = 10
87IPP_FINISHINGS_TRIM = 11
88IPP_FINISHINGS_BALE = 12
89IPP_FINISHINGS_BOOKLET_MAKER = 13
90IPP_FINISHINGS_JOB_OFFSET = 14
91IPP_FINISHINGS_STAPLE_TOP_LEFT = 20
92IPP_FINISHINGS_STAPLE_BOTTOM_LEFT = 21
93IPP_FINISHINGS_STAPLE_TOP_RIGHT = 22
94IPP_FINISHINGS_STAPLE_BOTTOM_RIGHT = 23
95IPP_FINISHINGS_EDGE_STITCH_LEFT = 24
96IPP_FINISHINGS_EDGE_STITCH_TOP = 25
97IPP_FINISHINGS_EDGE_STITCH_RIGHT = 26
98IPP_FINISHINGS_EDGE_STITCH_BOTTOM = 27
99IPP_FINISHINGS_STAPLE_DUAL_LEFT = 28
100IPP_FINISHINGS_STAPLE_DUAL_TOP = 29
101IPP_FINISHINGS_STAPLE_DUAL_RIGHT = 30
102IPP_FINISHINGS_STAPLE_DUAL_BOTTOM = 31
103IPP_FINISHINGS_BIND_LEFT = 50
104IPP_FINISHINGS_BIND_TOP = 51
105IPP_FINISHINGS_BIND_RIGHT = 52
106IPP_FINISHINGS_BIND_BOTTO = 53
107
108IPP_PORTRAIT = 3
109IPP_LANDSCAPE = 4
110IPP_REVERSE_LANDSCAPE = 5
111IPP_REVERSE_PORTRAIT = 6
112
113IPP_QUALITY_DRAFT = 3
114IPP_QUALITY_NORMAL = 4
115IPP_QUALITY_HIGH = 5
116
117IPP_JOB_PENDING = 3
118IPP_JOB_HELD = 4
119IPP_JOB_PROCESSING = 5
120IPP_JOB_STOPPED = 6
121IPP_JOB_CANCELLED = 7
122IPP_JOB_ABORTED = 8
123IPP_JOB_COMPLETE = 9
124
125IPP_PRINTER_IDLE = 3
126IPP_PRINTER_PROCESSING = 4
127IPP_PRINTER_STOPPED = 5
128
129IPP_ERROR = -1
130IPP_IDLE = 0
131IPP_HEADER = 1
132IPP_ATTRIBUTE = 2
133IPP_DATA = 3
134
135IPP_PRINT_JOB = 0x0002
136IPP_PRINT_URI = 0x0003
137IPP_VALIDATE_JOB = 0x0004
138IPP_CREATE_JOB = 0x0005
139IPP_SEND_DOCUMENT = 0x0006
140IPP_SEND_URI = 0x0007
141IPP_CANCEL_JOB = 0x0008
142IPP_GET_JOB_ATTRIBUTES = 0x0009
143IPP_GET_JOBS = 0x000a
144IPP_GET_PRINTER_ATTRIBUTES = 0x000b
145IPP_HOLD_JOB = 0x000c
146IPP_RELEASE_JOB = 0x000d
147IPP_RESTART_JOB = 0x000e
148IPP_PAUSE_PRINTER = 0x0010
149IPP_RESUME_PRINTER = 0x0011
150IPP_PURGE_JOBS = 0x0012
151IPP_SET_PRINTER_ATTRIBUTES = 0x0013
152IPP_SET_JOB_ATTRIBUTES = 0x0014
153IPP_GET_PRINTER_SUPPORTED_VALUES = 0x0015
154IPP_CREATE_PRINTER_SUBSCRIPTION = 0x0016
155IPP_CREATE_JOB_SUBSCRIPTION = 0x0017
156IPP_GET_SUBSCRIPTION_ATTRIBUTES = 0x0018
157IPP_GET_SUBSCRIPTIONS = 0x0019
158IPP_RENEW_SUBSCRIPTION = 0x001a
159IPP_CANCEL_SUBSCRIPTION = 0x001b
160IPP_GET_NOTIFICATIONS = 0x001c
161IPP_SEND_NOTIFICATIONS = 0x001d
162IPP_GET_PRINT_SUPPORT_FILES = 0x0021
163IPP_ENABLE_PRINTER = 0x0022
164IPP_DISABLE_PRINTER = 0x0023
165IPP_PAUSE_PRINTER_AFTER_CURRENT_JOB = 0x0024
166IPP_HOLD_NEW_JOBS = 0x0025
167IPP_RELEASE_HELD_NEW_JOBS = 0x0026
168IPP_DEACTIVATE_PRINTER = 0x0027
169IPP_ACTIVATE_PRINTER = 0x0028
170IPP_RESTART_PRINTER = 0x0029
171IPP_SHUTDOWN_PRINTER = 0x002a
172IPP_STARTUP_PRINTER = 0x002b
173IPP_REPROCESS_JOB = 0x002c
174IPP_CANCEL_CURRENT_JOB = 0x002d
175IPP_SUSPEND_CURRENT_JOB = 0x002e
176IPP_RESUME_JOB = 0x002f
177IPP_PROMOTE_JOB = 0x0030
178IPP_SCHEDULE_JOB_AFTER = 0x0031
179IPP_PRIVATE = 0x4000
180CUPS_GET_DEFAULT = 0x4001
181CUPS_GET_PRINTERS = 0x4002
182CUPS_ADD_PRINTER = 0x4003
183CUPS_DELETE_PRINTER = 0x4004
184CUPS_GET_CLASSES = 0x4005
185CUPS_ADD_CLASS = 0x4006
186CUPS_DELETE_CLASS = 0x4007
187CUPS_ACCEPT_JOBS = 0x4008
188CUPS_REJECT_JOBS = 0x4009
189CUPS_SET_DEFAULT = 0x400a
190CUPS_GET_DEVICES = 0x400b
191CUPS_GET_PPDS = 0x400c
192CUPS_MOVE_JOB = 0x400d
193CUPS_AUTHENTICATE_JOB = 0x400e
194
195IPP_OK = 0x0000
196IPP_OK_SUBST = 0x0001
197IPP_OK_CONFLICT = 0x0002
198IPP_OK_IGNORED_SUBSCRIPTIONS = 0x0003
199IPP_OK_IGNORED_NOTIFICATIONS = 0x0004
200IPP_OK_TOO_MANY_EVENTS = 0x0005
201IPP_OK_BUT_CANCEL_SUBSCRIPTION = 0x0006
202IPP_REDIRECTION_OTHER_SITE = 0x0300
203IPP_BAD_REQUEST = 0x0400
204IPP_FORBIDDEN = 0x0401
205IPP_NOT_AUTHENTICATED = 0x0402
206IPP_NOT_AUTHORIZED = 0x0403
207IPP_NOT_POSSIBLE = 0x0404
208IPP_TIMEOUT = 0x0405
209IPP_NOT_FOUND = 0x0406
210IPP_GONE = 0x0407
211IPP_REQUEST_ENTITY = 0x0408
212IPP_REQUEST_VALUE = 0x0409
213IPP_DOCUMENT_FORMAT = 0x040a
214IPP_ATTRIBUTES = 0x040b
215IPP_URI_SCHEME = 0x040c
216IPP_CHARSET = 0x040d
217IPP_CONFLICT = 0x040e
218IPP_COMPRESSION_NOT_SUPPORTED = 0x040f
219IPP_COMPRESSION_ERROR = 0x0410
220IPP_DOCUMENT_FORMAT_ERROR = 0x0411
221IPP_DOCUMENT_ACCESS_ERROR = 0x0412
222IPP_ATTRIBUTES_NOT_SETTABLE = 0x0413
223IPP_IGNORED_ALL_SUBSCRIPTIONS = 0x0414
224IPP_TOO_MANY_SUBSCRIPTIONS = 0x0415
225IPP_IGNORED_ALL_NOTIFICATIONS = 0x0416
226IPP_PRINT_SUPPORT_FILE_NOT_FOUND = 0x0417
227
228IPP_INTERNAL_ERROR = 0x0500
229IPP_OPERATION_NOT_SUPPORTED = 0x0501
230IPP_SERVICE_UNAVAILABLE = 0x0502
231IPP_VERSION_NOT_SUPPORTED = 0x0503
232IPP_DEVICE_ERROR = 0x0504
233IPP_TEMPORARY_ERROR = 0x0505
234IPP_NOT_ACCEPTING = 0x0506
235IPP_PRINTER_BUSY = 0x0507
236IPP_ERROR_JOB_CANCELLED = 0x0508
237IPP_MULTIPLE_JOBS_NOT_SUPPORTED = 0x0509
238IPP_PRINTER_IS_DEACTIVATED = 0x50a
239 
240CUPS_PRINTER_LOCAL = 0x0000
241CUPS_PRINTER_CLASS = 0x0001
242CUPS_PRINTER_REMOTE = 0x0002
243CUPS_PRINTER_BW = 0x0004
244CUPS_PRINTER_COLOR = 0x0008
245CUPS_PRINTER_DUPLEX = 0x0010
246CUPS_PRINTER_STAPLE = 0x0020
247CUPS_PRINTER_COPIES = 0x0040
248CUPS_PRINTER_COLLATE = 0x0080
249CUPS_PRINTER_PUNCH = 0x0100
250CUPS_PRINTER_COVER = 0x0200
251CUPS_PRINTER_BIND = 0x0400
252CUPS_PRINTER_SORT = 0x0800
253CUPS_PRINTER_SMALL = 0x1000
254CUPS_PRINTER_MEDIUM = 0x2000
255CUPS_PRINTER_LARGE = 0x4000
256CUPS_PRINTER_VARIABLE = 0x8000
257CUPS_PRINTER_IMPLICIT = 0x1000
258CUPS_PRINTER_DEFAULT = 0x2000
259CUPS_PRINTER_FAX = 0x4000
260CUPS_PRINTER_REJECTING = 0x8000
261CUPS_PRINTER_DELETE = 0x1000
262CUPS_PRINTER_NOT_SHARED = 0x2000
263CUPS_PRINTER_AUTHENTICATED = 0x4000
264CUPS_PRINTER_COMMANDS = 0x8000
265CUPS_PRINTER_OPTIONS = 0xe6ff
266 
267 
268class IPPError(Exception) :
269    """An exception for IPP related stuff."""
270    def __init__(self, message = ""):
271        self.message = message
272        Exception.__init__(self, message)
273    def __repr__(self):
274        return self.message
275    __str__ = __repr__
276
277class FakeAttribute :
278    """Fakes an IPPRequest attribute to simplify usage syntax."""
279    def __init__(self, request, name) :
280        """Initializes the fake attribute."""
281        self.request = request
282        self.name = name
283       
284    def __setitem__(self, key, value) :
285        """Appends the value to the real attribute."""
286        attributeslist = getattr(self.request, "_%s_attributes" % self.name)
287        for i in range(len(attributeslist)) :
288            attribute = attributeslist[i]
289            for j in range(len(attribute)) :
290                (attrname, attrvalue) = attribute[j]
291                if attrname == key :
292                    attribute[j][1].append(value)
293                    return
294            attribute.append((key, [value]))       
295           
296    def __getitem__(self, key) :
297        """Returns an attribute's value."""
298        answer = []
299        attributeslist = getattr(self.request, "_%s_attributes" % self.name)
300        for i in range(len(attributeslist)) :
301            attribute = attributeslist[i]
302            for j in range(len(attribute)) :
303                (attrname, attrvalue) = attribute[j]
304                if attrname == key :
305                    answer.extend(attrvalue)
306        if answer :
307            return answer
308        raise KeyError, key           
309   
310class IPPRequest :
311    """A class for IPP requests."""
312    attributes_types = ("operation", "job", "printer", "unsupported", \
313                                     "subscription", "event_notification")
314    def __init__(self, data="", version=IPP_VERSION, 
315                                operation_id=None, \
316                                request_id=None, \
317                                debug=False) :
318        """Initializes an IPP Message object.
319       
320           Parameters :
321           
322             data : the complete IPP Message's content.
323             debug : a boolean value to output debug info on stderr.
324        """
325        self.debug = debug
326        self._data = data
327        self.parsed = False
328       
329        # Initializes message
330        self.setVersion(version)               
331        self.setOperationId(operation_id)
332        self.setRequestId(request_id)
333        self.data = ""
334       
335        for attrtype in self.attributes_types :
336            setattr(self, "_%s_attributes" % attrtype, [[]])
337       
338        # Initialize tags   
339        self.tags = [ None ] * 256 # by default all tags reserved
340       
341        # Delimiter tags
342        self.tags[0x01] = "operation-attributes-tag"
343        self.tags[0x02] = "job-attributes-tag"
344        self.tags[0x03] = "end-of-attributes-tag"
345        self.tags[0x04] = "printer-attributes-tag"
346        self.tags[0x05] = "unsupported-attributes-tag"
347        self.tags[0x06] = "subscription-attributes-tag"
348        self.tags[0x07] = "event_notification-attributes-tag"
349       
350        # out of band values
351        self.tags[0x10] = "unsupported"
352        self.tags[0x11] = "reserved-for-future-default"
353        self.tags[0x12] = "unknown"
354        self.tags[0x13] = "no-value"
355        self.tags[0x15] = "not-settable"
356        self.tags[0x16] = "delete-attribute"
357        self.tags[0x17] = "admin-define"
358 
359        # integer values
360        self.tags[0x20] = "generic-integer"
361        self.tags[0x21] = "integer"
362        self.tags[0x22] = "boolean"
363        self.tags[0x23] = "enum"
364       
365        # octetString
366        self.tags[0x30] = "octetString-with-an-unspecified-format"
367        self.tags[0x31] = "dateTime"
368        self.tags[0x32] = "resolution"
369        self.tags[0x33] = "rangeOfInteger"
370        self.tags[0x34] = "begCollection" # TODO : find sample files for testing
371        self.tags[0x35] = "textWithLanguage"
372        self.tags[0x36] = "nameWithLanguage"
373        self.tags[0x37] = "endCollection"
374       
375        # character strings
376        self.tags[0x40] = "generic-character-string"
377        self.tags[0x41] = "textWithoutLanguage"
378        self.tags[0x42] = "nameWithoutLanguage"
379        self.tags[0x44] = "keyword"
380        self.tags[0x45] = "uri"
381        self.tags[0x46] = "uriScheme"
382        self.tags[0x47] = "charset"
383        self.tags[0x48] = "naturalLanguage"
384        self.tags[0x49] = "mimeMediaType"
385        self.tags[0x4a] = "memberAttrName"
386       
387        # Reverse mapping to generate IPP messages
388        self.tagvalues = {}
389        for i in range(len(self.tags)) :
390            value = self.tags[i]
391            if value is not None :
392                self.tagvalues[value] = i
393                                     
394    def __getattr__(self, name) :                                 
395        """Fakes attribute access."""
396        if name in self.attributes_types :
397            return FakeAttribute(self, name)
398        else :
399            raise AttributeError, name
400           
401    def __str__(self) :       
402        """Returns the parsed IPP message in a readable form."""
403        if not self.parsed :
404            return ""
405        mybuffer = []
406        mybuffer.append("IPP version : %s.%s" % self.version)
407        mybuffer.append("IPP operation Id : 0x%04x" % self.operation_id)
408        mybuffer.append("IPP request Id : 0x%08x" % self.request_id)
409        for attrtype in self.attributes_types :
410            for attribute in getattr(self, "_%s_attributes" % attrtype) :
411                if attribute :
412                    mybuffer.append("%s attributes :" % attrtype.title())
413                for (name, value) in attribute :
414                    mybuffer.append("  %s : %s" % (name, value))
415        if self.data :           
416            mybuffer.append("IPP datas : %s" % repr(self.data))
417        return "\n".join(mybuffer)
418       
419    def logDebug(self, msg) :   
420        """Prints a debug message."""
421        if self.debug :
422            sys.stderr.write("%s\n" % msg)
423            sys.stderr.flush()
424           
425    def setVersion(self, version) :
426        """Sets the request's operation id."""
427        if version is not None :
428            try :
429                self.version = [int(p) for p in version.split(".")]
430            except AttributeError :
431                if len(version) == 2 : # 2-tuple
432                    self.version = version
433                else :   
434                    try :
435                        self.version = [int(p) for p in str(float(version)).split(".")]
436                    except :
437                        self.version = [int(p) for p in IPP_VERSION.split(".")]
438       
439    def setOperationId(self, opid) :       
440        """Sets the request's operation id."""
441        self.operation_id = opid
442       
443    def setRequestId(self, reqid) :       
444        """Sets the request's request id."""
445        self.request_id = reqid
446       
447    def dump(self) :   
448        """Generates an IPP Message.
449       
450           Returns the message as a string of text.
451        """   
452        mybuffer = []
453        if None not in (self.version, self.operation_id) :
454            mybuffer.append(chr(self.version[0]) + chr(self.version[1]))
455            mybuffer.append(pack(">H", self.operation_id))
456            mybuffer.append(pack(">I", self.request_id or 1))
457            for attrtype in self.attributes_types :
458                for attribute in getattr(self, "_%s_attributes" % attrtype) :
459                    if attribute :
460                        mybuffer.append(chr(self.tagvalues["%s-attributes-tag" % attrtype]))
461                    for (attrname, value) in attribute :
462                        nameprinted = 0
463                        for (vtype, val) in value :
464                            mybuffer.append(chr(self.tagvalues[vtype]))
465                            if not nameprinted :
466                                mybuffer.append(pack(">H", len(attrname)))
467                                mybuffer.append(attrname)
468                                nameprinted = 1
469                            else :     
470                                mybuffer.append(pack(">H", 0))
471                            if vtype in ("integer", "enum") :
472                                mybuffer.append(pack(">H", 4))
473                                mybuffer.append(pack(">I", val))
474                            elif vtype == "boolean" :
475                                mybuffer.append(pack(">H", 1))
476                                mybuffer.append(chr(val))
477                            else :   
478                                mybuffer.append(pack(">H", len(val)))
479                                mybuffer.append(val)
480            mybuffer.append(chr(self.tagvalues["end-of-attributes-tag"]))
481        mybuffer.append(self.data)   
482        return "".join(mybuffer)
483           
484    def parse(self) :
485        """Parses an IPP Request.
486       
487           NB : Only a subset of RFC2910 is implemented.
488        """
489        self._curname = None
490        self._curattributes = None
491       
492        self.setVersion((ord(self._data[0]), ord(self._data[1])))
493        self.setOperationId(unpack(">H", self._data[2:4])[0])
494        self.setRequestId(unpack(">I", self._data[4:8])[0])
495        self.position = 8
496        endofattributes = self.tagvalues["end-of-attributes-tag"]
497        maxdelimiter = self.tagvalues["event_notification-attributes-tag"]
498        nulloffset = lambda : 0
499        try :
500            tag = ord(self._data[self.position])
501            while tag != endofattributes :
502                self.position += 1
503                name = self.tags[tag]
504                if name is not None :
505                    func = getattr(self, name.replace("-", "_"), nulloffset)
506                    self.position += func()
507                    if ord(self._data[self.position]) > maxdelimiter :
508                        self.position -= 1
509                        continue
510                oldtag = tag       
511                tag = ord(self._data[self.position])
512                if tag == oldtag :
513                    self._curattributes.append([])
514        except IndexError :
515            raise IPPError, "Unexpected end of IPP message."
516           
517        self.data = self._data[self.position+1:]           
518        self.parsed = True
519       
520    def parseTag(self) :   
521        """Extracts information from an IPP tag."""
522        pos = self.position
523        tagtype = self.tags[ord(self._data[pos])]
524        pos += 1
525        posend = pos2 = pos + 2
526        namelength = unpack(">H", self._data[pos:pos2])[0]
527        if not namelength :
528            name = self._curname
529        else :   
530            posend += namelength
531            self._curname = name = self._data[pos2:posend]
532        pos2 = posend + 2
533        valuelength = unpack(">H", self._data[posend:pos2])[0]
534        posend = pos2 + valuelength
535        value = self._data[pos2:posend]
536        if tagtype in ("integer", "enum") :
537            value = unpack(">I", value)[0]
538        elif tagtype == "boolean" :   
539            value = ord(value)
540        try :   
541            (oldname, oldval) = self._curattributes[-1][-1]
542            if oldname == name :
543                oldval.append((tagtype, value))
544            else :   
545                raise IndexError
546        except IndexError :   
547            self._curattributes[-1].append((name, [(tagtype, value)]))
548        self.logDebug("%s(%s) : %s" % (name, tagtype, value))
549        return posend - self.position
550       
551    def operation_attributes_tag(self) : 
552        """Indicates that the parser enters into an operation-attributes-tag group."""
553        self._curattributes = self._operation_attributes
554        return self.parseTag()
555       
556    def job_attributes_tag(self) : 
557        """Indicates that the parser enters into a job-attributes-tag group."""
558        self._curattributes = self._job_attributes
559        return self.parseTag()
560       
561    def printer_attributes_tag(self) : 
562        """Indicates that the parser enters into a printer-attributes-tag group."""
563        self._curattributes = self._printer_attributes
564        return self.parseTag()
565       
566    def unsupported_attributes_tag(self) : 
567        """Indicates that the parser enters into an unsupported-attributes-tag group."""
568        self._curattributes = self._unsupported_attributes
569        return self.parseTag()
570       
571    def subscription_attributes_tag(self) : 
572        """Indicates that the parser enters into a subscription-attributes-tag group."""
573        self._curattributes = self._subscription_attributes
574        return self.parseTag()
575       
576    def event_notification_attributes_tag(self) : 
577        """Indicates that the parser enters into an event-notification-attributes-tag group."""
578        self._curattributes = self._event_notification_attributes
579        return self.parseTag()
580       
581           
582class CUPS :
583    """A class for a CUPS instance."""
584    def __init__(self, url=None, username=None, password=None, charset="utf-8", language="en-us", debug=False) :
585        """Initializes the CUPS instance."""
586        if url is not None :
587            self.url = url.replace("ipp://", "http://")
588            if self.url.endswith("/") :
589                self.url = self.url[:-1]
590        else :       
591            self.url = self.getDefaultURL()
592        self.username = username
593        self.password = password
594        self.charset = charset
595        self.language = language
596        self.debug = debug
597        self.lastError = None
598        self.lastErrorMessage = None
599        self.requestId = None
600       
601    def getDefaultURL(self) :   
602        """Builds a default URL."""
603        # TODO : encryption methods.
604        server = os.environ.get("CUPS_SERVER") or "localhost"
605        port = os.environ.get("IPP_PORT") or 631
606        if server.startswith("/") :
607            # it seems it's a unix domain socket.
608            # we can't handle this right now, so we use the default instead.
609            return "http://localhost:%s" % port
610        else :   
611            return "http://%s:%s" % (server, port)
612           
613    def identifierToURI(self, service, ident) :
614        """Transforms an identifier into a particular URI depending on requested service."""
615        return "%s/%s/%s" % (self.url.replace("http://", "ipp://"),
616                             service,
617                             ident)
618       
619    def nextRequestId(self) :       
620        """Increments the current request id and returns the new value."""
621        try :
622            self.requestId += 1
623        except TypeError :   
624            self.requestId = 1
625        return self.requestId
626           
627    def newRequest(self, operationid=None) :
628        """Generates a new empty request."""
629        if operationid is not None :
630            req = IPPRequest(operation_id=operationid, \
631                             request_id=self.nextRequestId(), \
632                             debug=self.debug)
633            req.operation["attributes-charset"] = ("charset", self.charset)
634            req.operation["attributes-natural-language"] = ("naturalLanguage", self.language)
635            return req
636   
637    def doRequest(self, req, url=None) :
638        """Sends a request to the CUPS server.
639           returns a new IPPRequest object, containing the parsed answer.
640        """   
641        connexion = urllib2.Request(url=url or self.url, \
642                             data=req.dump())
643        connexion.add_header("Content-Type", "application/ipp")
644        if self.username :
645            pwmanager = urllib2.HTTPPasswordMgrWithDefaultRealm()
646            pwmanager.add_password(None, \
647                                   "%s%s" % (connexion.get_host(), connexion.get_selector()), \
648                                   self.username, \
649                                   self.password or "")
650            authhandler = urllib2.HTTPBasicAuthHandler(pwmanager)                       
651            opener = urllib2.build_opener(authhandler)
652            urllib2.install_opener(opener)
653        self.lastError = None   
654        self.lastErrorMessage = None
655        try :   
656            response = urllib2.urlopen(connexion)
657        except (urllib2.URLError, urllib2.HTTPError, socket.error), error :   
658            self.lastError = error
659            self.lastErrorMessage = str(error)
660            return None
661        else :   
662            datas = response.read()
663            ippresponse = IPPRequest(datas)
664            ippresponse.parse()
665            return ippresponse
666   
667    def getPPD(self, queuename) :   
668        """Retrieves the PPD for a particular queuename."""
669        req = self.newRequest(IPP_GET_PRINTER_ATTRIBUTES)
670        req.operation["printer-uri"] = ("uri", self.identifierToURI("printers", queuename))
671        for attrib in ("printer-uri-supported", "printer-type", "member-uris") :
672            req.operation["requested-attributes"] = ("nameWithoutLanguage", attrib)
673        return self.doRequest(req)  # TODO : get the PPD from the actual print server
674       
675    def getDefault(self) :
676        """Retrieves CUPS' default printer."""
677        return self.doRequest(self.newRequest(CUPS_GET_DEFAULT))
678   
679    def getJobAttributes(self, jobid) :   
680        """Retrieves a print job's attributes."""
681        req = self.newRequest(IPP_GET_JOB_ATTRIBUTES)
682        req.operation["job-uri"] = ("uri", self.identifierToURI("jobs", jobid))
683        return self.doRequest(req)
684       
685    def getPrinters(self) :   
686        """Returns the list of print queues names."""
687        req = self.newRequest(CUPS_GET_PRINTERS)
688        req.operation["requested-attributes"] = ("keyword", "printer-name")
689        req.operation["printer-type"] = ("enum", 0)
690        req.operation["printer-type-mask"] = ("enum", CUPS_PRINTER_CLASS)
691        return [printer[1] for printer in self.doRequest(req).printer["printer-name"]]
692       
693    def getDevices(self) :   
694        """Returns a list of devices as (deviceclass, deviceinfo, devicemakeandmodel, deviceuri) tuples."""
695        answer = self.doRequest(self.newRequest(CUPS_GET_DEVICES))
696        return zip([d[1] for d in answer.printer["device-class"]], \
697                   [d[1] for d in answer.printer["device-info"]], \
698                   [d[1] for d in answer.printer["device-make-and-model"]], \
699                   [d[1] for d in answer.printer["device-uri"]])
700                   
701    def getPPDs(self) :   
702        """Returns a list of PPDs as (ppdnaturallanguage, ppdmake, ppdmakeandmodel, ppdname) tuples."""
703        answer = self.doRequest(self.newRequest(CUPS_GET_PPDS))
704        return zip([d[1] for d in answer.printer["ppd-natural-language"]], \
705                   [d[1] for d in answer.printer["ppd-make"]], \
706                   [d[1] for d in answer.printer["ppd-make-and-model"]], \
707                   [d[1] for d in answer.printer["ppd-name"]])
708                   
709    def createSubscription(self, uri, events=["all"],
710                                      userdata=None,
711                                      recipient=None,
712                                      pullmethod=None,
713                                      charset=None,
714                                      naturallanguage=None,
715                                      leaseduration=None,
716                                      timeinterval=None,
717                                      jobid=None) :
718        """Creates a printer or server subscription."""
719        req = self.newRequest(IPP_CREATE_PRINTER_SUBSCRIPTION)
720        req.operation["printer-uri"] = ("uri", uri)
721        for event in events :
722            req.subscription["notify-events"] = ("keyword", event)
723        if userdata is not None :   
724            req.subscription["notify-user-data"] = ("octetString-with-an-unspecified-format", userdata)
725        if recipient is not None :   
726            req.subscription["notify-recipient"] = ("uri", recipient)
727        if pullmethod is not None :
728            req.subscription["notify-pull-method"] = ("keyword", pullmethod)
729        if charset is not None :
730            req.subscription["notify-charset"] = ("charset", charset)
731        if naturallanguage is not None :
732            req.subscription["notify-natural-language"] = ("naturalLanguage", naturallanguage)
733        if leaseduration is not None :
734            req.subscription["notify-lease-duration"] = ("integer", leaseduration)
735        if timeinterval is not None :
736            req.subscription["notify-time-interval"] = ("integer", timeinterval)
737        if jobid is not None :
738            req.subscription["notify-job-id"] = ("integer", jobid)
739        return self.doRequest(req)
740           
741       
742if __name__ == "__main__" :           
743    if (len(sys.argv) < 2) or (sys.argv[1] == "--debug") :
744        print "usage : python pkipplib.py /var/spool/cups/c00005 [--debug] (for example)\n"
745    else :   
746        infile = open(sys.argv[1], "rb")
747        filedata = infile.read()
748        infile.close()
749       
750        msg = IPPRequest(filedata, debug=(sys.argv[-1]=="--debug"))
751        msg.parse()
752        msg2 = IPPRequest(msg.dump())
753        msg2.parse()
754        filedata2 = msg2.dump()
755       
756        if filedata == filedata2 :
757            print "Test OK : parsing original and parsing the output of the dump produce the same dump !"
758            print str(msg)
759        else :   
760            print "Test Failed !"
761            print str(msg)
762            print
763            print str(msg2)
764       
Note: See TracBrowser for help on using the browser.