source: server/lib/gutenbach/server/job.py @ 97f20dd

no-cups
Last change on this file since 97f20dd was 97f20dd, checked in by Jessica B. Hamrick <jhamrick@…>, 12 years ago

Add documentation for printer and job attributes

  • Property mode set to 100644
File size: 11.8 KB
Line 
1from . import errors
2from .player import Player
3from gutenbach.ipp import JobStates as States
4import logging
5import os
6
7# initialize logger
8logger = logging.getLogger(__name__)
9
10class GutenbachJob(object):
11
12    def __init__(self, job_id=None, creator=None, name=None,
13                 priority=None, document=None):
14        """Create an empty Gutenbach job.
15
16        Parameters
17        ----------
18        job_id : integer
19            A unique id for this job.
20        creator : string
21            The user creating the job.
22        name : string
23            The human-readable name of the job.
24        priority : integer
25            The priority of the job, used for ordering.
26        document : file object
27            A file object containing the job data.
28
29        """
30
31        self.player = None
32        self.document = None
33
34        self.id = job_id
35        self.creator = creator
36        self.name = name
37        self.priority = priority
38       
39        self._why_done = None
40
41        if document is not None:
42            self.spool(document)
43
44    def __repr__(self):
45        return str(self)
46
47    def __str__(self):
48        return "<Job %d '%s'>" % (self.id, self.name)
49
50    def __cmp__(self, other):
51        """Compares two GutenbachJobs based on their priorities.
52
53        """
54        return cmp(self.priority, other.priority)
55
56    def __del__(self):
57        if self.player:
58            if self.player.is_playing:
59                self.player.mplayer_stop()
60            if self.player.fh:
61                if self.player.fh.closed:
62                    self.player.fh.close()
63            self.player = None
64
65        self.document = None
66        self.id = None
67        self.creator = None
68        self.name = None
69        self.priority = None
70        self._why_done = None
71
72    ######################################################################
73    ###                          Properties                            ###
74    ######################################################################
75
76    @property
77    def id(self):
78        """Unique job identifier (integer).  Should be a positive
79        integer, except when unassigned, when it defaults to -1.
80       
81        """
82        return self._id
83    @id.setter
84    def id(self, val):
85        try:
86            self._id = max(int(val), -1)
87        except:
88            self._id = -1
89
90    @property
91    def priority(self):
92        """Job priority (integer).  Should be a nonzero positive
93        integer; defaults to 1 when unassigned.
94
95        """
96        return self._priority
97    @priority.setter
98    def priority(self, val):
99        try:
100            self._priority = max(int(val), 1)
101        except:
102            self._priority = 1
103
104    @property
105    def creator(self):
106        """The user who created the job (string).  Defaults to an
107        empty string.
108
109        """
110        return self._creator
111    @creator.setter
112    def creator(self, val):
113        if val is None:
114            self._creator = ""
115        else:
116            self._creator = str(val)
117
118    @property
119    def name(self):
120        """The job's human-readable name (string).  Defaults to an
121        empty string.
122
123        """
124        return self._name
125    @name.setter
126    def name(self, val):
127        if val is None:
128            self._name = ""
129        else:
130            self._name = str(val)
131
132    @property
133    def size(self):
134        """The size of the job in octets/bytes (integer).  Defaults to
135        0 if no document is specified or if there is an error reading
136        the document.
137
138        """
139        try:
140            size = os.path.getsize(self.document)
141        except:
142            size = 0
143        return size
144
145    ######################################################################
146    ###                            State                               ###
147    ######################################################################
148
149    @property
150    def is_valid(self):
151        """Whether the job is ready to be manipulated (spooled,
152        played, etc).  Note that playing the job still requires it to
153        be spooled first.
154
155        """
156
157        return self.id > 0 and \
158               self.priority > 0
159
160    @property
161    def is_ready(self):
162        """Whether the job is ready to be played; i.e., it has all the
163        necessary data to actually play the audio data.
164
165        """
166
167        return self.is_valid and \
168               self.player is not None and \
169               not self.player.is_playing and \
170               not self._why_done == "canceled" and \
171               not self._why_done == "aborted"
172
173    @property
174    def is_playing(self):
175        """Whether the job is currently playing (regardless of whether
176        it's paused).
177
178        """
179
180        return self.is_valid and \
181               self.player is not None and \
182               self.player.is_playing
183
184    @property
185    def is_paused(self):
186        """Whether the job is currently paused.
187
188        """
189
190        return self.is_valid and \
191               self.player is not None and \
192               self.player.is_paused       
193
194    @property
195    def is_done(self):
196        """Whether the job is done playing, regardless of whether it
197        completed successfully or not.
198
199        """
200
201        return (self.is_valid and \
202                self.player is not None and \
203                self.player.is_done) or \
204                (self._why_done == "canceled" or \
205                 self._why_done == "aborted")
206
207    @property
208    def is_completed(self):
209        """Whether the job completed successfully.
210
211        """
212
213        return self.is_done and self._why_done == "completed"
214
215    @property
216    def is_canceled(self):
217        """Whether the job was canceled.
218
219        """
220
221        return self.is_done and self._why_done == "canceled"
222
223    @property
224    def is_aborted(self):
225        """Whether the job was aborted.
226
227        """
228
229        return self.is_done and self._why_done == "aborted"
230
231    @property
232    def state(self):
233        """State status codes (these are equivalent to the IPP
234        job-state status codes).  State transitions are as follows:
235       
236            HELD ---> PENDING ---> PROCESSING <--> STOPPED (aka paused)
237                         ^              |---> CANCELED
238                         |              |---> ABORTED
239                         |              |---> COMPLETE ---|
240                         |--------------------------------|
241                     
242        """
243
244        if self.is_ready:
245            state = States.PENDING
246        elif self.is_playing and not self.is_paused:
247            state = States.PROCESSING
248        elif self.is_playing and self.is_paused:
249            state = States.PROCESSING_STOPPED
250        elif self.is_completed:
251            state = States.COMPLETED
252        elif self.is_canceled:
253            state = States.CANCELED
254        elif self.is_aborted:
255            state = States.ABORTED
256        else:
257            state = States.PENDING_HELD
258        return state
259
260    ######################################################################
261    ###                            Methods                             ###
262    ######################################################################
263
264    @staticmethod
265    def verify_document(document):
266        """Verifies that a document has the 'name', 'read', and
267        'close' attributes (i.e., it should be like a file object).
268
269        """
270       
271        if not hasattr(document, "name"):
272            raise errors.InvalidDocument, "no name attribute"
273        if not hasattr(document, "read"):
274            raise errors.InvalidDocument, "no read attribute"
275        if not hasattr(document, "close"):
276            raise errors.InvalidDocument, "no close attribute"
277        if not hasattr(document, "closed"):
278            raise errors.InvalidDocument, "no closed attribute"
279
280    def spool(self, document=None):
281        """Non-blocking spool.  Job must be valid (see
282        'GutenbachJob.is_valid'), and the document must be an open
283        file handler.
284
285        Raises
286        ------
287        InvalidDocument
288            If the document is not valid.
289        InvalidJobStateException
290            If the job is not valid or it is already
291            spooled/ready/finished.
292
293        """
294
295        if not self.is_valid or self.state != States.PENDING_HELD:
296            raise errors.InvalidJobStateException(self.state)
297        self.verify_document(document)
298        self.document = document.name
299        self.player = Player(document)
300        logger.debug("document for job %d is '%s'" % (self.id, self.document))
301
302    def play(self):
303        """Non-blocking play.  Job must be ready (see
304        'GutenbachJob.is_ready').
305
306        Raises
307        ------
308        InvalidJobStateException
309            If the job is not ready to be played.
310
311        """
312       
313        # make sure the job is waiting to be played and that it's
314        # valid
315        if not self.is_ready:
316            raise errors.InvalidJobStateException(self.state)
317       
318        # and set the state to processing if we're good to go
319        logger.info("playing job %s" % str(self))
320
321        def _completed():
322            logger.info("completed job %s" % str(self))
323            self._why_done = "completed"
324        self.player.callback = _completed
325        self.player.start()
326
327    def pause(self):
328        """Non-blocking pause.  Job must be playing (see
329        'GutenbachJob.is_playing').
330
331        Raises
332        ------
333        InvalidJobStateException
334            If the job is not playing.
335
336        """
337       
338        if not self.is_playing:
339            raise errors.InvalidJobStateException(self.state)
340        self.player.mplayer_pause()
341
342    def resume(self):
343        """Non-blocking resume.  Job must be paused (see
344        'GutenbachJob.is_paused').
345
346        Raises
347        ------
348        InvalidJobStateException
349            If the job is not paused.
350
351        """
352        if not self.is_paused:
353            raise errors.InvalidJobStateException(self.state)
354        self.player.mplayer_pause()
355
356    def cancel(self):
357        """Blocking cancel. The job must not have been previously
358        aborted or completed (though this method will succeed if it
359        was previously canceled).  This should be used to stop the
360        job following an external request.
361
362        Raises
363        ------
364        InvalidJobStateException
365            If the job has already finished.
366
367        """
368       
369        if self.is_playing:
370            self.player._callback = None
371            self.player.mplayer_stop()
372
373        elif self.is_done and not self._why_done == "canceled":
374            raise errors.InvalidJobStateException(self.state)
375
376        logger.info("canceled job %s" % str(self))
377        self._why_done = "canceled"
378
379    def abort(self):
380        """Blocking abort. The job must not have been previously
381        canceled or completed (though this method will succeed if it
382        was previously aborted).  This should be used to stop the job
383        following internal errors.
384
385        Raises
386        ------
387        InvalidJobStateException
388            If the job has already finished.
389
390        """
391
392        if self.is_playing:
393            self.player._callback = None
394            self.player.mplayer_stop()
395
396        elif self.is_done and not self._why_done == "aborted":
397            raise errors.InvalidJobStateException(self.state)
398
399        logger.info("aborted job %s" % str(self))
400        self._why_done = "aborted"
401
402    def restart(self):
403        """Non-blocking restart.  Job must be finished (see
404        'GutenbachJob.is_done'), and will be ready to be played (see
405        'GutenbachJob.is_ready') if this method is successful.
406
407        Raises
408        ------
409        InvalidJobStateException
410            If the job is not done.
411
412        """
413
414        if not self.is_done:
415            raise errors.InvalidJobStateException(self.state)
416
417        logger.debug("restarting job %d", self.id)
418
419        self._why_done = None
420        fh = self.player.fh
421
422        if not fh or fh.closed:
423            raise RuntimeError, "file handler is closed"
424
425        self.player = Player(fh)
Note: See TracBrowser for help on using the repository browser.