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

Revision 3166, 15.0 kB (checked in by jerome, 17 years ago)

Added a comment.
Updated dumpykota's manual page.

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