]> git.decadent.org.uk Git - dak.git/blob - tools/queue_rss.py
Add a missing print in report_multiple_source().
[dak.git] / tools / queue_rss.py
1 #!/usr/bin/python
2 # Generate two rss feeds for a directory with .changes file
3
4 # License: GPL v2 or later
5 # Author: Filippo Giunchedi <filippo@debian.org>
6 # Version: 0.5
7
8 import cgi
9 import os
10 import os.path
11 import cPickle
12 import re
13 import sys
14 import time
15 from optparse import OptionParser
16 from datetime import datetime
17
18 import PyRSS2Gen
19
20 try:
21     # starting with squeeze
22     from debian.deb822 import Changes
23 except:
24     # up to lenny
25     from debian_bundle.deb822 import Changes
26
27 inrss_filename = "NEW_in.rss"
28 outrss_filename = "NEW_out.rss"
29 db_filename = "status.db"
30
31 parser = OptionParser()
32 parser.set_defaults(queuedir="queue", outdir="out", datadir="status",
33                     logdir="log", max_entries="30")
34
35 parser.add_option("-q", "--queuedir", dest="queuedir",
36         help="The queue dir (%default)")
37 parser.add_option("-o", "--outdir", dest="outdir",
38         help="The output directory (%default)")
39 parser.add_option("-d", "--datadir", dest="datadir",
40         help="The data dir (%default)")
41 parser.add_option("-l", "--logdir", dest="logdir",
42         help="The ACCEPT/REJECT dak log dir (%default)")
43 parser.add_option("-m", "--max-entries", dest="max_entries", type="int",
44         help="Max number of entries to keep (%default)")
45
46 class Status:
47     def __init__(self):
48         self.feed_in = PyRSS2Gen.RSS2(
49                        title = "Packages entering NEW",
50                        link = "http://ftp-master.debian.org/new.html",
51                        description = "Debian packages entering the NEW queue" )
52
53         self.feed_out = PyRSS2Gen.RSS2(
54                        title = "Packages leaving NEW",
55                        link = "http://ftp-master.debian.org/new.html",
56                        description = "Debian packages leaving the NEW queue" )
57
58         self.queue = {}
59
60 def purge_old_items(feed, max):
61     """ Purge RSSItem from feed, no more than max. """
62     if feed.items is None or len(feed.items) == 0:
63         return False
64
65     feed.items = feed.items[:max]
66     return True
67
68 def parse_changes(fname):
69     """ Parse a .changes file named fname.
70
71     Return {fname: parsed} """
72
73     m = Changes(open(fname))
74
75     wanted_fields = set(['Source', 'Version', 'Architecture', 'Distribution',
76                          'Date', 'Maintainer', 'Description', 'Changes'])
77
78     if not set(m.keys()).issuperset(wanted_fields):
79         return None
80
81     return {os.path.basename(fname): m}
82
83 def parse_queuedir(dir):
84     """ Parse dir for .changes files.
85
86     Return a dictionary {filename: parsed_file}"""
87
88     if not os.path.exists(dir):
89         return None
90
91     res = {}
92     for fname in os.listdir(dir):
93         if not fname.endswith(".changes"):
94             continue
95
96         parsed = parse_changes(os.path.join(dir, fname))
97         if parsed:
98             res.update(parsed)
99
100     return res
101
102 def parse_leave_reason(fname):
103     """ Parse a dak log file fname for ACCEPT/REJECT reason from process-new.
104
105     Return a dictionary {filename: reason}"""
106
107     reason_re = re.compile(".+\|process-new\|.+\|NEW (ACCEPT|REJECT): (\S+)")
108
109     try:
110         f = open(fname)
111     except IOError, e:
112         sys.stderr.write("Can't open %s: %s\n" % (fname, e))
113         return {}
114
115     res = {}
116     for l in f.readlines():
117         m = reason_re.search(l)
118         if m:
119             res[m.group(2)] = m.group(1)
120
121     f.close()
122     return res
123
124 def add_rss_item(status, msg, direction):
125     if direction == "in":
126         feed = status.feed_in
127         title = "%s %s entered NEW" % (msg['Source'], msg['Version'])
128         pubdate = msg['Date']
129     elif direction == "out":
130         feed = status.feed_out
131         if msg.has_key('Leave-Reason'):
132             title = "%s %s left NEW (%s)" % (msg['Source'], msg['Version'],
133                                              msg['Leave-Reason'])
134         else:
135             title = "%s %s left NEW" % (msg['Source'], msg['Version'])
136
137
138         pubdate = datetime.utcnow()
139     else:
140         return False
141
142     description = "<pre>Description: %s\nChanges: %s\n</pre>" % \
143             (cgi.escape(msg['Description']),
144              cgi.escape(msg['Changes']))
145
146     link = "http://ftp-master.debian.org/new/%s_%s.html" % \
147             (msg['Source'], msg['Version'])
148
149     feed.items.insert(0,
150         PyRSS2Gen.RSSItem(
151             title,
152             pubDate = pubdate,
153             description = description,
154             author = cgi.escape(msg['Maintainer']),
155             link = link,
156             guid = link
157         )
158     )
159
160 def update_feeds(curqueue, status, settings):
161     # inrss -> append all items in curqueue not in status.queue
162     # outrss -> append all items in status.queue not in curqueue
163
164     leave_reason = None
165     # logfile from dak's process-new
166     reason_log = os.path.join(settings.logdir, time.strftime("%Y-%m"))
167
168     for (name, parsed) in curqueue.items():
169         if not status.queue.has_key(name):
170             # new package
171             add_rss_item(status, parsed, "in")
172
173     for (name, parsed) in status.queue.items():
174         if not curqueue.has_key(name):
175             # removed package, try to find out why
176             if leave_reason is None:
177                 leave_reason = parse_leave_reason(reason_log)
178             if leave_reason and leave_reason.has_key(name):
179                 parsed['Leave-Reason'] = leave_reason[name]
180             add_rss_item(status, parsed, "out")
181
182
183
184 if __name__ == "__main__":
185
186     (settings, args) = parser.parse_args()
187
188     if not os.path.exists(settings.outdir):
189         sys.stderr.write("Outdir '%s' does not exists\n" % settings.outdir)
190         parser.print_help()
191         sys.exit(1)
192
193     if not os.path.exists(settings.datadir):
194         sys.stderr.write("Datadir '%s' does not exists\n" % settings.datadir)
195         parser.print_help()
196         sys.exit(1)
197
198     status_db = os.path.join(settings.datadir, db_filename)
199
200     try:
201         status = cPickle.load(open(status_db))
202     except IOError:
203         status = Status()
204
205     current_queue = parse_queuedir(settings.queuedir)
206     if not current_queue:
207         sys.stderr.write("Unable to scan queuedir '%s'\n" % settings.queuedir)
208         parser.print_help()
209         sys.exit(1)
210
211     update_feeds(current_queue, status, settings)
212
213     purge_old_items(status.feed_in, settings.max_entries)
214     purge_old_items(status.feed_out, settings.max_entries)
215
216     feed_in_file = os.path.join(settings.outdir, inrss_filename)
217     feed_out_file = os.path.join(settings.outdir, outrss_filename)
218
219     try:
220         status.feed_in.write_xml(file(feed_in_file, "w+"), "utf-8")
221         status.feed_out.write_xml(file(feed_out_file, "w+"), "utf-8")
222     except IOError, why:
223         sys.stderr.write("Unable to write feeds: %s\n", why)
224         sys.exit(1)
225
226     status.queue = current_queue
227
228     try:
229         cPickle.dump(status, open(status_db, "w+"))
230     except IOError, why:
231         sys.stderr.write("Unable to save status: %s\n", why)
232         sys.exit(1)
233
234 # vim:et:ts=4