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

Revision 3165, 14.9 kB (checked in by jerome, 17 years ago)

Added --orderby command line switch to dumpykota.
Doesn't work yet with the LDAP backend, since sorting will have
to be done by PyKota's code instead of relying on the database
backend itself to do the dirty work.

  • 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)
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.