root / pykota / trunk / pykota / dumper.py @ 3260

Revision 3260, 15.0 kB (checked in by jerome, 16 years ago)

Changed license to GNU GPL v3 or later.
Changed Python source encoding from ISO-8859-15 to UTF-8 (only ASCII
was used anyway).

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
RevLine 
[3260]1# -*- coding: UTF-8 -*-
[2016]2#
[3260]3# PyKota : Print Quotas for CUPS
[2016]4#
[3133]5# (c) 2003, 2004, 2005, 2006, 2007 Jerome Alet <alet@librelogiciel.com>
[3260]6# This program is free software: you can redistribute it and/or modify
[2016]7# it under the terms of the GNU General Public License as published by
[3260]8# the Free Software Foundation, either version 3 of the License, or
[2016]9# (at your option) any later version.
[3260]10#
[2016]11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
[3260]17# along with this program.  If not, see <http://www.gnu.org/licenses/>.
[2016]18#
19# $Id$
20#
[2034]21#
[2016]22
[3184]23"""This module handles all the data dumping facilities for PyKota."""
24
[2016]25import sys
26import os
27import pwd
[2264]28from xml.sax import saxutils
29
[2016]30from mx import DateTime
31
32try :
33    import jaxml
34except ImportError :   
35    sys.stderr.write("The jaxml Python module is not installed. XML output is disabled.\n")
36    sys.stderr.write("Download jaxml from http://www.librelogiciel.com/software/ or from your Debian archive of choice\n")
37    hasJAXML = 0
38else :   
39    hasJAXML = 1
40
41from pykota import version
[2512]42from pykota.tool import PyKotaTool, PyKotaToolError, PyKotaCommandLineError, N_
[2016]43
44class DumPyKota(PyKotaTool) :       
45    """A class for dumpykota."""
46    validdatatypes = { "history" : N_("History"),
47                       "users" : N_("Users"),
48                       "groups" : N_("Groups"),
49                       "printers" : N_("Printers"),
50                       "upquotas" : N_("Users Print Quotas"),
51                       "gpquotas" : N_("Users Groups Print Quotas"),
52                       "payments" : N_("History of Payments"),
53                       "pmembers" : N_("Printers Groups Membership"), 
54                       "umembers" : N_("Users Groups Membership"),
[2342]55                       "billingcodes" : N_("Billing Codes"),
[2600]56                       "all": N_("All"),
[2016]57                     }
58    validformats = { "csv" : N_("Comma Separated Values"),
59                     "ssv" : N_("Semicolon Separated Values"),
60                     "tsv" : N_("Tabulation Separated Values"),
61                     "xml" : N_("eXtensible Markup Language"),
62                     "cups" : N_("CUPS' page_log"),
63                   } 
64    validfilterkeys = [ "username",
65                        "groupname",
66                        "printername",
67                        "pgroupname",
[2218]68                        "hostname",
69                        "billingcode",
[3093]70                        "jobid", 
[2266]71                        "start",
72                        "end",
[2016]73                      ]
[2032]74    def main(self, arguments, options, restricted=1) :
[2016]75        """Print Quota Data Dumper."""
[2032]76        if restricted and not self.config.isAdmin :
[2512]77            raise PyKotaCommandLineError, "%s : %s" % (pwd.getpwuid(os.geteuid())[0], _("You're not allowed to use this command."))
[2016]78           
79        datatype = options["data"]
80        if datatype not in self.validdatatypes.keys() :
[2512]81            raise PyKotaCommandLineError, _("Invalid modifier [%s] for --data command line option, see help.") % datatype
[2016]82                   
[3165]83        orderby = options["orderby"]             
84        if orderby :
85            fields = [f.strip() for f in orderby.split(",")]
86            orderby = []
87            for field in fields :
88                if field.isalpha() \
89                   or ((field[0] in ("+", "-")) and field[1:].isalpha()) :
90                    orderby.append(field)
91                else :   
92                    self.printInfo("Skipping invalid ordering statement '%(field)s'" % locals(), "error") 
93        else :
94            orderby = []
95           
[2600]96        extractonly = {}
97        if datatype == "all" :           
98            if (options["format"] != "xml") or options["sum"] or arguments :
99                self.printInfo(_("Dumping all PyKota's datas forces format to XML, and disables --sum and filters."), "warn")
100            options["format"] = "xml"
101            options["sum"] = None
102        else :   
103            for filterexp in arguments :
104                if filterexp.strip() :
105                    try :
106                        (filterkey, filtervalue) = [part.strip() for part in filterexp.split("=")]
[3030]107                        filterkey = filterkey.lower()
[2600]108                        if filterkey not in self.validfilterkeys :
109                            raise ValueError               
110                    except ValueError :   
111                        raise PyKotaCommandLineError, _("Invalid filter value [%s], see help.") % filterexp
112                    else :   
113                        extractonly.update({ filterkey : filtervalue })
114           
[2016]115        format = options["format"]
[2567]116        if (format not in self.validformats.keys()) \
117           or ((format == "cups") \
118              and ((datatype != "history") or options["sum"])) :
[2512]119            raise PyKotaCommandLineError, _("Invalid modifier [%s] for --format command line option, see help.") % format
[2016]120           
121        if (format == "xml") and not hasJAXML :
122            raise PyKotaToolError, _("XML output is disabled because the jaxml module is not available.")
123           
[3142]124        if datatype not in ("payments", "history") : 
125            if options["sum"] : 
126                raise PyKotaCommandLineError, _("Invalid data type [%s] for --sum command line option, see help.") % datatype
127            if extractonly.has_key("start") or extractonly.has_key("end") :   
128                self.printInfo(_("Invalid filter for the %(datatype)s data type.") % locals(), "warn")
129                try :
130                    del extractonly["start"]
131                except KeyError :   
132                    pass
133                try :
134                    del extractonly["end"]
135                except KeyError :   
136                    pass
[2285]137           
[2600]138        retcode = 0
139        nbentries = 0   
140        mustclose = 0   
141        if options["output"].strip() == "-" :   
142            self.outfile = sys.stdout
143        else :   
144            self.outfile = open(options["output"], "w")
145            mustclose = 1
146           
147        if datatype == "all" :   
148            # NB : order does matter to allow easier or faster restore
149            allentries = []
150            datatypes = [ "printers", "pmembers", "users", "groups", \
151                          "billingcodes", "umembers", "upquotas", \
152                          "gpquotas", "payments", "history" ]
[2602]153            neededdatatypes = datatypes[:]             
[2600]154            for datatype in datatypes :
[3166]155                entries = getattr(self.storage, "extract%s" % datatype.title())(extractonly) # We don't care about ordering here
[2601]156                if entries :
157                    nbentries += len(entries)
158                    allentries.append(entries)
[2602]159                else :   
160                    neededdatatypes.remove(datatype)
161            retcode = self.dumpXml(allentries, neededdatatypes)
[2600]162        else :   
[3165]163            entries = getattr(self.storage, "extract%s" % datatype.title())(extractonly, orderby)
[2600]164            if entries :
165                nbentries = len(entries)
166                retcode = getattr(self, "dump%s" % format.title())([self.summarizeDatas(entries, datatype, extractonly, options["sum"])], [datatype])
[2016]167               
[2600]168        if mustclose :
169            self.outfile.close()
170            if not nbentries : 
171                os.remove(options["output"])
[2016]172           
[2600]173        return retcode
[2016]174       
[2295]175    def summarizeDatas(self, entries, datatype, extractonly, sum=0) :   
[2285]176        """Transforms the datas into a summarized view (with totals).
177       
178           If sum is false, returns the entries unchanged.
179        """   
180        if not sum :
181            return entries
182        else :   
[2289]183            headers = entries[0]
184            nbheaders = len(headers)
185            fieldnumber = {}
[2293]186            fieldname = {}
[2289]187            for i in range(nbheaders) :
[2295]188                fieldnumber[headers[i]] = i
189           
[2289]190            if datatype == "payments" :
[2293]191                totalize = [ ("amount", float) ]
[2295]192                keys = [ "username" ]
193            else : # elif datatype == "history"
194                totalize = [ ("jobsize", int), 
195                             ("jobprice", float),
196                             ("jobsizebytes", int),
[2664]197                             ("precomputedjobsize", int),
198                             ("precomputedjobprice", float),
[2295]199                           ]
200                keys = [ k for k in ("username", "printername", "hostname", "billingcode") if k in extractonly.keys() ]
[2293]201               
[2295]202            newentries = [ headers ]
203            sortedentries = entries[1:]
204            if keys :
205                # If we have several keys, we can sort only on the first one, because they
206                # will vary the same way.
207                sortedentries.sort(lambda x, y, fnum=fieldnumber[keys[0]] : cmp(x[fnum], y[fnum]))
208            totals = {}
209            for (k, t) in totalize :
210                totals[k] = { "convert" : t, "value" : 0.0 }
211            prevkeys = {}
212            for k in keys :
213                prevkeys[k] = sortedentries[0][fieldnumber[k]]
214            for entry in sortedentries :
215                curval = '-'.join([str(entry[fieldnumber[k]]) for k in keys])
216                prevval = '-'.join([str(prevkeys[k]) for k in keys])
217                if curval != prevval :
218                    summary = [ "*" ] * nbheaders
219                    for k in keys :
220                        summary[fieldnumber[k]] = prevkeys[k]
221                    for k in totals.keys() :   
222                        summary[fieldnumber[k]] = totals[k]["convert"](totals[k]["value"])
223                    newentries.append(summary)
224                    for k in totals.keys() :   
225                        totals[k]["value"] = totals[k]["convert"](entry[fieldnumber[k]])
226                else :   
227                    for k in totals.keys() :   
[2664]228                        totals[k]["value"] += totals[k]["convert"](entry[fieldnumber[k]] or 0.0)
[2295]229                for k in keys :
230                    prevkeys[k] = entry[fieldnumber[k]]
231            summary = [ "*" ] * nbheaders
232            for k in keys :
233                summary[fieldnumber[k]] = prevkeys[k]
234            for k in totals.keys() :   
235                summary[fieldnumber[k]] = totals[k]["convert"](totals[k]["value"])
236            newentries.append(summary)
[2289]237            return newentries
[2285]238           
[2600]239    def dumpWithSeparator(self, separator, allentries) :   
[2016]240        """Dumps datas with a separator."""
[2600]241        for entries in allentries :
242            for entry in entries :
243                line = []
244                for value in entry :
245                    if type(value).__name__ in ("str", "NoneType") :
246                        line.append('"%s"' % str(value).replace(separator, "\\%s" % separator).replace('"', '\\"'))
247                    else :   
248                        line.append(str(value))
249                try :
250                    self.outfile.write("%s\n" % separator.join(line))
251                except IOError, msg :   
252                    sys.stderr.write("%s : %s\n" % (_("PyKota data dumper failed : I/O error"), msg))
253                    return -1
[2016]254        return 0       
255       
[2600]256    def dumpCsv(self, allentries, dummy) :   
[2016]257        """Dumps datas with a comma as the separator."""
[2600]258        return self.dumpWithSeparator(",", allentries)
[2016]259                           
[2600]260    def dumpSsv(self, allentries, dummy) :   
[2016]261        """Dumps datas with a comma as the separator."""
[2600]262        return self.dumpWithSeparator(";", allentries)
[2016]263                           
[2600]264    def dumpTsv(self, allentries, dummy) :   
[2016]265        """Dumps datas with a comma as the separator."""
[2600]266        return self.dumpWithSeparator("\t", allentries)
[2016]267       
[2600]268    def dumpCups(self, allentries, dummy) :   
[2016]269        """Dumps history datas as CUPS' page_log format."""
[3121]270        months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]
[2600]271        entries = allentries[0]
[2016]272        fieldnames = entries[0]
273        fields = {}
274        for i in range(len(fieldnames)) :
275            fields[fieldnames[i]] = i
276        sortindex = fields["jobdate"]   
277        entries = entries[1:]
[2830]278        entries.sort(lambda m, n, si=sortindex : cmp(m[si], n[si]))
[2219]279        for entry in entries :   
[2016]280            printername = entry[fields["printername"]]
281            username = entry[fields["username"]]
282            jobid = entry[fields["jobid"]]
[3050]283            jobdate = DateTime.ISO.ParseDateTime(str(entry[fields["jobdate"]])[:19])
[2016]284            gmtoffset = jobdate.gmtoffset()
[3121]285            #jobdate = "%s %+03i00" % (jobdate.strftime("%d/%b/%Y:%H:%M:%S"), gmtoffset.hour)
286            jobdate = "%02i/%s/%04i:%02i:%02i:%02i %+03i%02i" % (jobdate.day,
287                                                                 months[jobdate.month - 1],
288                                                                 jobdate.year,
289                                                                 jobdate.hour,
290                                                                 jobdate.minute,
291                                                                 jobdate.second,
292                                                                 gmtoffset.hour,
293                                                                 gmtoffset.minute)
[2016]294            jobsize = entry[fields["jobsize"]] or 0
295            copies = entry[fields["copies"]] or 1
296            hostname = entry[fields["hostname"]] or ""
[2217]297            billingcode = entry[fields["billingcode"]] or "-"
[2163]298            for pagenum in range(1, jobsize+1) :
[2217]299                self.outfile.write("%s %s %s [%s] %s %s %s %s\n" % (printername, username, jobid, jobdate, pagenum, copies, billingcode, hostname))
[2600]300        return 0       
[2016]301       
[2600]302    def dumpXml(self, allentries, datatypes) :
[2016]303        """Dumps datas as XML."""
304        x = jaxml.XML_document(encoding="UTF-8")
305        x.pykota(version=version.__version__, author=version.__author__)
[2600]306        for (entries, datatype) in zip(allentries, datatypes) :
[2016]307            x._push()
[2600]308            x.dump(storage=self.config.getStorageBackend()["storagebackend"], type=datatype)
309            headers = entries[0]
310            for entry in entries[1:] :
311                x._push()
312                x.entry()
313                for (header, value) in zip(headers, entry) :
314                    strvalue = str(value)
315                    typval = type(value).__name__
316                    if header in ("filename", "title", "options", "billingcode") \
317                              and (typval == "str") :
318                        try :
[3055]319                            strvalue = unicode(strvalue, self.charset).encode("UTF-8")
[2600]320                        except UnicodeError :   
321                            pass
322                        strvalue = saxutils.escape(strvalue, { "'" : "&apos;", \
323                                                               '"' : "&quot;" })
324                    x.attribute(strvalue, type=typval, name=header)
325                x._pop()   
[2016]326            x._pop()   
327        x._output(self.outfile)
[2600]328        return 0       
Note: See TracBrowser for help on using the browser.