root / pykota / trunk / bin / edpykota @ 1246

Revision 1207, 26.0 kB (checked in by jalet, 21 years ago)

Old help message deletedd

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# PyKota Print Quota Editor
5#
6# PyKota - Print Quotas for CUPS and LPRng
7#
8# (c) 2003 Jerome Alet <alet@librelogiciel.com>
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
22#
23# $Id$
24#
25# $Log$
26# Revision 1.63  2003/11/24 16:50:58  jalet
27# Old help message deletedd
28#
29# Revision 1.62  2003/11/12 23:28:38  jalet
30# More work on new backend. This commit may be unstable.
31#
32# Revision 1.61  2003/11/12 13:06:35  jalet
33# Bug fix wrt no user/group name command line argument to edpykota
34#
35# Revision 1.60  2003/10/09 21:25:24  jalet
36# Multiple printer names or wildcards can be passed on the command line
37# separated with commas.
38# Beta phase.
39#
40# Revision 1.59  2003/10/07 09:07:27  jalet
41# Character encoding added to please latest version of Python
42#
43# Revision 1.58  2003/10/03 12:27:01  jalet
44# Several optimizations, especially with LDAP backend
45#
46# Revision 1.57  2003/08/20 16:01:19  jalet
47# Comment added.
48#
49# Revision 1.56  2003/07/29 20:55:17  jalet
50# 1.14 is out !
51#
52# Revision 1.55  2003/07/28 09:11:12  jalet
53# PyKota now tries to add its attributes intelligently in existing LDAP
54# directories.
55#
56# Revision 1.54  2003/07/21 06:32:42  jalet
57# Prevents email messages to be sent at modification/creation time for
58# a user/group quota
59#
60# Revision 1.53  2003/07/09 06:03:41  jalet
61# Fixed typo when using edpykota --prototype
62#
63# Revision 1.52  2003/07/07 12:11:13  jalet
64# Small fix
65#
66# Revision 1.51  2003/07/07 11:55:50  jalet
67# Small fix
68#
69# Revision 1.50  2003/07/05 12:33:53  jalet
70# More on previous fix.
71#
72# Revision 1.49  2003/07/05 12:32:07  jalet
73# Ensure that the user don't pass more than two prices for a printer.
74#
75# Revision 1.48  2003/06/25 19:52:30  jalet
76# Should be ready for testing :-)
77#
78# Revision 1.47  2003/06/25 14:10:01  jalet
79# Hey, it may work (edpykota --reset excepted) !
80#
81# Revision 1.46  2003/06/16 11:59:09  jalet
82# More work on LDAP
83#
84# Revision 1.45  2003/06/11 19:32:00  jalet
85# Severe bug wrt account balance setting should be corrected.
86#
87# Revision 1.44  2003/04/29 22:03:38  jalet
88# Better error handling.
89#
90# Revision 1.43  2003/04/23 22:13:56  jalet
91# Preliminary support for LPRng added BUT STILL UNTESTED.
92#
93# Revision 1.42  2003/04/17 13:38:47  jalet
94# Docstring corrected for better manual page
95#
96# Revision 1.41  2003/04/16 12:35:49  jalet
97# Groups quota work now !
98#
99# Revision 1.40  2003/04/16 08:22:09  jalet
100# More strict error detection.
101# Minor code rewrite to avoid some repetitive tests.
102#
103# Revision 1.39  2003/04/16 08:01:53  jalet
104# edpykota --charge command line option works now.
105#
106# Revision 1.38  2003/04/15 22:02:43  jalet
107# More complete docstring
108#
109# Revision 1.37  2003/04/15 21:58:33  jalet
110# edpykota now accepts a --delete option.
111# Preparation to allow edpykota to accept much more command line options
112# (WARNING : docstring is OK, but code isn't !)
113#
114# Revision 1.36  2003/04/15 13:55:28  jalet
115# Options --limitby and --balance added to edpykota
116#
117# Revision 1.35  2003/04/15 13:06:39  jalet
118# Allow to add a printer without any user
119#
120# Revision 1.34  2003/04/11 16:51:11  jalet
121# Bug fix for edpykota --add with users who already had a quota on the printer.
122#
123# Revision 1.33  2003/04/10 21:47:20  jalet
124# Job history added. Upgrade script neutralized for now !
125#
126# Revision 1.32  2003/04/08 21:31:39  jalet
127# (anything or 0) = anything !!! Go back to school Jerome !
128#
129# Revision 1.31  2003/04/08 21:13:44  jalet
130# Prepare --groups option to work.
131#
132# Revision 1.30  2003/04/08 21:10:18  jalet
133# Checks --groups option presence instead of --users because --users is the default.
134#
135# Revision 1.29  2003/04/05 09:28:56  jalet
136# Unnecessary message was logged
137#
138# Revision 1.28  2003/03/29 13:45:26  jalet
139# GPL paragraphs were incorrectly (from memory) copied into the sources.
140# Two README files were added.
141# Upgrade script for PostgreSQL pre 1.01 schema was added.
142#
143# Revision 1.27  2003/03/10 00:23:04  jalet
144# Bad english
145#
146# Revision 1.26  2003/03/10 00:11:27  jalet
147# Cleaner example.
148#
149# Revision 1.25  2003/03/09 23:56:21  jalet
150# Option noquota added to do accounting only.
151#
152# Revision 1.24  2003/02/27 23:48:41  jalet
153# Correctly maps PyKota's log levels to syslog log levels
154#
155# Revision 1.23  2003/02/27 22:55:20  jalet
156# WARN log priority doesn't exist.
157#
158# Revision 1.22  2003/02/27 09:37:02  jalet
159# Wildcards seem to work now
160#
161# Revision 1.21  2003/02/27 09:04:46  jalet
162# user and group names can be passed as wildcards if the --add option
163# is not set. The default is to act on all users or groups.
164#
165# Revision 1.20  2003/02/10 12:07:30  jalet
166# Now repykota should output the recorded total page number for each printer too.
167#
168# Revision 1.19  2003/02/09 13:40:29  jalet
169# typo
170#
171# Revision 1.18  2003/02/09 12:56:53  jalet
172# Internationalization begins...
173#
174# Revision 1.17  2003/02/08 22:47:23  jalet
175# Option --reset can now be used without having to use soft and hard limits
176# on the command line.
177#
178# Revision 1.16  2003/02/08 22:39:46  jalet
179# --reset command line option added
180#
181# Revision 1.15  2003/02/08 22:20:01  jalet
182# Clarification on why we don't check with /etc/passwd to see if the user
183# name is valid or not.
184#
185# Revision 1.14  2003/02/08 22:18:15  jalet
186# Now checks user and group names for validity before adding them
187#
188# Revision 1.13  2003/02/08 22:09:02  jalet
189# Only printer was added the first time.
190#
191# Revision 1.12  2003/02/08 21:44:49  jalet
192# Python 2.1 string module doesn't define ascii_letters
193#
194# Revision 1.11  2003/02/08 09:42:44  jalet
195# Better handle wrong or bad command line arguments
196#
197# Revision 1.10  2003/02/08 09:39:20  jalet
198# typos
199#
200# Revision 1.9  2003/02/08 09:38:06  jalet
201# Badly placed test
202#
203# Revision 1.8  2003/02/07 22:53:57  jalet
204# Checks if printer name is valid before adding it
205#
206# Revision 1.7  2003/02/07 22:17:58  jalet
207# Incomplete test
208#
209# Revision 1.6  2003/02/07 22:13:13  jalet
210# Perhaps edpykota is now able to add printers !!! Oh, stupid me !
211#
212# Revision 1.5  2003/02/06 14:49:04  jalet
213# edpykota should be ok now
214#
215# Revision 1.4  2003/02/06 14:28:59  jalet
216# edpykota should be ok, minus some typos
217#
218# Revision 1.3  2003/02/06 10:47:21  jalet
219# Documentation string and command line options didn't match.
220#
221# Revision 1.2  2003/02/06 10:39:23  jalet
222# Preliminary edpykota work.
223#
224# Revision 1.1  2003/02/05 21:41:09  jalet
225# Skeletons added for all command line tools
226#
227#
228#
229
230import sys
231
232from pykota import version
233from pykota.tool import PyKotaTool, PyKotaToolError
234from pykota.config import PyKotaConfigError
235from pykota.storage import PyKotaStorageError
236
237__doc__ = """edpykota v%s (C) 2003 C@LL - Conseil Internet & Logiciels Libres
238A Print Quota editor for PyKota.
239
240command line usage :
241
242  edpykota [options] user1 user2 ... userN
243 
244  edpykota [options] group1 group2 ... groupN
245
246options :
247
248  -v | --version       Prints edpykota's version number then exits.
249  -h | --help          Prints this message then exits.
250 
251  -a | --add           Adds users and/or printers if they don't
252                       exist on the Quota Storage Server.
253                       
254  -d | --delete        Deletes users/groups from the quota storage.
255                       Printers are never deleted.
256                       
257  -c | --charge p[,j]  Sets the price per page and per job to charge
258                       for a particular printer. Job price is optional.
259                       If both are to be set, separate them with a comma.
260                       Floating point values are allowed.
261 
262  -i | --ingroups g1[,g2...]  Puts the users into each of the groups
263                              listed, separated by commas. The groups
264                              must already exist in the Quota Storage.
265 
266  -u | --users         Edit users print quotas, this is the default.
267 
268  -P | --printer p     Edit quotas on printer p only. Actually p can
269                       use wildcards characters to select only
270                       some printers. The default value is *, meaning
271                       all printers.
272                       You can specify several names or wildcards,
273                       by separating them with commas.
274 
275  -g | --groups        Edit groups print quotas instead of users.
276                         
277  -p | --prototype u|g Uses user u or group g as a prototype to set
278                       print quotas
279                       
280  -n | --noquota       Doesn't set a quota but only does accounting.
281 
282  -r | --reset         Resets the printed page counter for the user
283                       or group to zero. The life time page counter
284                       is kept unchanged.
285                       
286  -l | --limitby l     Choose if the user/group is limited in printing                     
287                       by its account balance or by its page quota.
288                       The default value is 'quota'. Allowed values
289                       are 'quota' and 'balance'.
290                       
291  -b | --balance b     Sets the user's account balance to b.                     
292                       Account balance may be increase or decreased
293                       if b is prefixed with + or -.
294                       WARNING : when decreasing account balance,
295                       the total paid so far by the user is decreased
296                       too.
297                       Groups don't have a real balance, but the
298                       sum of their users' account balance.
299                       
300  -S | --softlimit sl  Sets the quota soft limit to sl pages.                       
301 
302  -H | --hardlimit hl  Sets the quota hard limit to hl pages.
303 
304  user1 through userN and group1 through groupN can use wildcards
305  if the --add option is not set.
306 
307examples :                             
308
309  $ edpykota -p jerome john paul george ringo
310 
311  This will set print quotas for the users john, paul, george and ringo
312  to the same values than user jerome. User jerome must exist.
313 
314  $ edpykota --printer lp -S 50 -H 60 jerome
315 
316  This will set jerome's print quota on the lp printer to a soft limit
317  of 50 pages, and a hard limit of 60 pages. If either user jerome or
318  printer lp doesn't exist on the Quota Storage Server then nothing is done.
319
320  $ edpykota --add --printer lp --ingroups coders,it -S 50 -H 60 jerome
321 
322  Same as above, but if either user jerome or printer lp doesn't exist
323  on the Quota Storage Server they are automatically added. Also
324  user jerome is put into the groups "coders" and "it" which must
325  already exist in the Quota Storage.
326           
327  $ edpykota -g -S 500 -H 550 financial support           
328 
329  This will set print quota soft limit to 500 pages and hard limit
330  to 550 pages for groups financial and support on all printers.
331 
332  $ edpykota --reset jerome "jo*"
333 
334  This will reset jerome's page counter to zero on all printers, as
335  well as every user whose name begins with 'jo'.
336  Their life time page counter on each printer will be kept unchanged.
337 
338  $ edpykota --printer hpcolor --noquota jerome
339 
340  This will tell PyKota to not limit jerome when printing on the
341  hpcolor printer. All his jobs will be allowed on this printer, but
342  accounting of the pages he prints will still be kept.
343  Print Quotas for jerome on other printers are unchanged.
344 
345  $ edpykota --limitby balance jerome
346 
347  This will tell PyKota to limit jerome by his account's balance
348  when printing.
349 
350  $ edpykota --balance +10.0 jerome
351 
352  This will increase jerome's account balance by 10.0 (in your
353  own currency). You can decrease the account balance with a
354  dash prefix, and set it to a fixed amount with no prefix.
355 
356  $ edpykota --delete jerome rachel
357 
358  This will completely delete jerome and rachel from the Quota Storage
359  database. All their quotas and jobs will be deleted too.
360 
361  $ edpykota --printer lp --charge 0.1
362 
363  This will set the page price for printer lp to 0.1. Job price
364  will not be changed.
365
366This program is free software; you can redistribute it and/or modify
367it under the terms of the GNU General Public License as published by
368the Free Software Foundation; either version 2 of the License, or
369(at your option) any later version.
370
371This program is distributed in the hope that it will be useful,
372but WITHOUT ANY WARRANTY; without even the implied warranty of
373MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
374GNU General Public License for more details.
375
376You should have received a copy of the GNU General Public License
377along with this program; if not, write to the Free Software
378Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
379
380Please e-mail bugs to: %s""" % (version.__version__, version.__author__)
381       
382class EdPyKota(PyKotaTool) :       
383    """A class for edpykota."""
384    def main(self, names, options) :
385        """Edit user or group quotas."""
386       
387        suffix = (options["groups"] and "Group") or "User"       
388       
389        softlimit = hardlimit = None   
390        if not options["noquota"] :
391            if options["softlimit"] :
392                try :
393                    softlimit = int(options["softlimit"].strip())
394                except ValueError :   
395                    raise PyKotaToolError, _("Invalid softlimit value %s.") % options["softlimit"]
396            if options["hardlimit"] :
397                try :
398                    hardlimit = int(options["hardlimit"].strip())
399                except ValueError :   
400                    raise PyKotaToolError, _("Invalid hardlimit value %s.") % options["hardlimit"]
401            if (softlimit is not None) and (hardlimit is not None) and (hardlimit < softlimit) :       
402                # error, exchange them
403                self.logger.log_message(_("Hard limit %i is less than soft limit %i, values will be exchanged.") % (hardlimit, softlimit))
404                (softlimit, hardlimit) = (hardlimit, softlimit)
405           
406        balance = options["balance"]
407        if balance :
408            balance = balance.strip()
409            try :
410                balancevalue = float(balance)
411            except ValueError :   
412                raise PyKotaToolError, _("Invalid balance value %s" % options["balance"])
413           
414        if options["charge"] :
415            try :
416                charges = [float(part) for part in options["charge"].split(',', 1)]
417            except ValueError :   
418                raise PyKotaToolError, _("Invalid charge amount value %s" % options["charge"])
419            else :   
420                if len(charges) > 2 :
421                    charges = charges[:2]
422                if len(charges) != 2 :
423                    charges = [charges[0], None]
424                   
425        limitby = options["limitby"]
426        if limitby :
427            limitby = limitby.strip().lower()
428        if limitby and (limitby not in ('quota', 'balance')) :   
429            raise PyKotaToolError, _("Invalid limitby value %s" % options["limitby"])
430           
431        if options["ingroups"] :   
432            groupnames = [gname.strip() for gname in options["ingroups"].split(',')]
433        else :   
434            groupnames = []
435           
436        if options["prototype"] :   
437            protoentry = getattr(self.storage, "get%s" % suffix)(options["prototype"])
438           
439        printeradded = 0
440        printers = self.storage.getMatchingPrinters(options["printer"])
441        if not printers :
442            pname = options["printer"]
443            if options["add"] and pname :
444                if self.isValidName(pname) :
445                    printers = [ self.storage.addPrinter(pname) ]
446                    if printers[0].Exists :
447                        printeradded = 1
448                    else :   
449                        raise PyKotaToolError, _("Impossible to add printer %s") % pname
450                else :   
451                    raise PyKotaToolError, _("Invalid printer name %s") % pname
452            else :
453                raise PyKotaToolError, _("There's no printer matching %s") % pname
454        if not names :   
455            if options["add"] :
456                if not printeradded :
457                    raise PyKotaToolError, _("You have to pass user or group names on the command line")
458                else :   
459                    names = getattr(self.storage, "getAll%ssNames" % suffix)()
460            else :   
461                names = [ "*" ] # all users
462               
463        changed = {} # tracks changes made at the user/group level
464        for printer in printers :
465            if options["charge"] :
466                (perpage, perjob) = charges
467                printer.setPrices(perpage, perjob)   
468               
469            if options["prototype"] :
470                if protoentry.Exists :
471                    protoquota = getattr(self.storage, "get%sPQuota" % suffix)(protoentry, printer)
472                    if not protoquota.Exists :
473                        self.logger.log_message(_("Prototype %s not found in Quota Storage for printer %s.") % (protoentry.Name, printer.Name))
474                        continue    # skip this printer
475                    else :   
476                        (softlimit, hardlimit) = (protoquota.SoftLimit, protoquota.HardLimit)
477                else :       
478                    self.logger.log_message(_("Prototype object %s not found in Quota Storage.") % protoentry.Name)
479                   
480            if not options["noquota"] :   
481                if hardlimit is None :   
482                    hardlimit = softlimit
483                    if hardlimit is not None :
484                        self.logger.log_message(_("Undefined hard limit set to soft limit (%s) on printer %s.") % (str(hardlimit), printer.Name))
485                if softlimit is None :   
486                    softlimit = hardlimit
487                    if softlimit is not None :
488                        self.logger.log_message(_("Undefined soft limit set to hard limit (%s) on printer %s.") % (str(softlimit), printer.Name))
489                       
490            if options["add"] :   
491                allentries = []   
492                for name in names :
493                    entry = getattr(self.storage, "get%s" % suffix)(name)
494                    entrypquota = getattr(self.storage, "get%sPQuota" % suffix)(entry, printer)
495                    allentries.append((entry, entrypquota))
496            else :   
497                allentries = getattr(self.storage, "getPrinter%ssAndQuotas" % suffix)(printer, names)
498               
499            for (entry, entrypquota) in allentries :
500                if not changed.has_key(entry.Name) :
501                    changed[entry.Name] = {}
502                    if not options["groups"] :
503                        changed[entry.Name]["ingroups"] = []
504                       
505                if not entry.Exists :       
506                    # not found
507                    if options["add"] :
508                        # In case we want to add something, it is crucial
509                        # that we DON'T check with the system accounts files
510                        # like /etc/passwd because users may be defined
511                        # only remotely
512                        if self.isValidName(entry.Name) :
513                            entry = getattr(self.storage, "add%s" % suffix)(entry)
514                        else :   
515                            if options["groups"] :
516                                self.logger.log_message(_("Invalid group name %s") % entry.Name)
517                            else :   
518                                self.logger.log_message(_("Invalid user name %s") % entry.Name)
519                               
520                if entry.Exists and (not entrypquota.Exists) :
521                    # not found
522                    if options["add"] :
523                        entrypquota = getattr(self.storage, "add%sPQuota" % suffix)(entry, printer)
524                       
525                if not entrypquota.Exists :     
526                    self.logger.log_message(_("Quota not found for object %s on printer %s.") % (entry.Name, printer.Name))
527                else :   
528                    if options["delete"] :
529                        entry.delete()
530                    else :
531                        if options["noquota"] or options["prototype"] or ((softlimit is not None) and (hardlimit is not None)) :
532                            entrypquota.setLimits(softlimit, hardlimit)
533                        if limitby :
534                            if changed[entry.Name].get("limitby") is None :
535                                entry.setLimitBy(limitby)
536                                changed[entry.Name]["limitby"] = limitby
537                       
538                        if not options["groups"] :
539                            if options["reset"] :
540                                entrypquota.reset()
541                               
542                            if balance :
543                                if changed[entry.Name].get("balance") is None :
544                                    if balance.startswith("+") or balance.startswith("-") :
545                                        newbalance = float(entry.AccountBalance or 0.0) + balancevalue
546                                        newlifetimepaid = float(entry.LifeTimePaid or 0.0) + balancevalue
547                                        entry.setAccountBalance(newbalance, newlifetimepaid)
548                                    else :
549                                        diff = balancevalue - float(entry.AccountBalance or 0.0)
550                                        newlifetimepaid = float(entry.LifeTimePaid or 0.0) + diff
551                                        entry.setAccountBalance(balancevalue, newlifetimepaid)
552                                    changed[entry.Name]["balance"] = balance
553                                   
554                            for groupname in groupnames :       
555                                # not executed if option --ingroups is not used
556                                if groupname not in changed[entry.Name]["ingroups"] :
557                                    group = self.storage.getGroup(groupname)
558                                    if group.Exists :
559                                        self.storage.addUserToGroup(entry, group)
560                                        changed[entry.Name]["ingroups"].append(groupname)
561                                    else :
562                                        self.logger.log_message(_("Group %s not found in the PyKota Storage.") % groupname)
563                                       
564                        # This line disabled to prevent sending of unwanted email                 
565                        # messages if quota is reached at creation/modification time.
566                        # The check will be done at print time anyway.
567                        # getattr(self, "warn%sPQuota" % suffix)(entrypquota)   
568                     
569if __name__ == "__main__" : 
570    retcode = 0
571    try :
572        defaults = { \
573                     "printer" : "*", \
574                   }
575        short_options = "vhdc:l:b:i:naugrp:P:S:H:"
576        long_options = ["help", "version", "charge=", "delete", "limitby=", "balance=", "ingroups=", "noquota", "add", "users", "groups", "reset", "prototype=", "printer=", "softlimit=", "hardlimit="]
577       
578        # Initializes the command line tool
579        editor = EdPyKota(doc=__doc__)
580       
581        # parse and checks the command line
582        (options, args) = editor.parseCommandline(sys.argv[1:], short_options, long_options)
583       
584        # sets long options
585        options["help"] = options["h"] or options["help"]
586        options["version"] = options["v"] or options["version"]
587        options["add"] = options["a"] or options["add"]
588        options["users"] = options["u"] or options["users"]
589        options["groups"] = options["g"] or options["groups"]
590        options["prototype"] = options["p"] or options["prototype"]
591        options["printer"] = options["P"] or options["printer"] or defaults["printer"]
592        options["softlimit"] = options["S"] or options["softlimit"]
593        options["hardlimit"] = options["H"] or options["hardlimit"] 
594        options["reset"] = options["r"] or options["reset"] 
595        options["noquota"] = options["n"] or options["noquota"]
596        options["limitby"] = options["l"] or options["limitby"]
597        options["balance"] = options["b"] or options["balance"] 
598        options["delete"] = options["d"] or options["delete"] 
599        options["ingroups"] = options["i"] or options["ingroups"]
600        options["charge"] = options["c"] or options["charge"]
601       
602        if options["help"] :
603            editor.display_usage_and_quit()
604        elif options["version"] :
605            editor.display_version_and_quit()
606        elif options["users"] and options["groups"] :   
607            raise PyKotaToolError, _("incompatible options, see help.")
608        elif (options["add"] or options["prototype"]) and options["delete"] :   
609            raise PyKotaToolError, _("incompatible options, see help.")
610        elif (options["softlimit"] or options["hardlimit"]) and options["prototype"] :   
611            raise PyKotaToolError, _("incompatible options, see help.")
612        elif options["noquota"] and (options["prototype"] or options["hardlimit"] or options["softlimit"]) :
613            raise PyKotaToolError, _("incompatible options, see help.")
614        elif options["groups"] and (options["balance"] or options["ingroups"]) :
615            raise PyKotaToolError, _("incompatible options, see help.")
616        else :
617            retcode = editor.main(args, options)
618    except (PyKotaToolError, PyKotaConfigError, PyKotaStorageError), msg :           
619        sys.stderr.write("%s\n" % msg)
620        sys.stderr.flush()
621        retcode = -1
622
623    try :
624        editor.storage.close()
625    except (TypeError, NameError, AttributeError) :   
626        pass
627       
628    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.