Merge lp:~marco-gallotta/ibid/contest into lp:~ibid-core/ibid/old-trunk-1.6

Proposed by Stefano Rivera
Status: Merged
Approved by: Jonathan Hitchcock
Approved revision: not available
Merged at revision: 860
Proposed branch: lp:~marco-gallotta/ibid/contest
Merge into: lp:~ibid-core/ibid/old-trunk-1.6
Diff against target: 385 lines (+338/-4)
3 files modified
ibid/plugins/codecontest.py (+331/-0)
ibid/utils/__init__.py (+4/-4)
scripts/ibid-plugin (+3/-0)
To merge this branch: bzr merge lp:~marco-gallotta/ibid/contest
Reviewer Review Type Date Requested Status
Keegan Carruthers-Smith Approve
Max Rabkin Approve
Stefano Rivera Approve
Review via email: mp+17658@code.launchpad.net

This proposal supersedes a proposal from 2010-01-15.

To post a comment you must log in.
Revision history for this message
marcog (marco-gallotta) wrote : Posted in a previous version of this proposal

First iteration of the contest plugin, with USACO scriping pretty much done.

Revision history for this message
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal

Run pyflakes. Otherwise you are simply asking me to do it for you:

ibid/plugins/contest.py:1: 'codecs' imported but unused
ibid/plugins/contest.py:4: 'ElementTree' imported but unused
ibid/plugins/contest.py:8: 'ibid' imported but unused
ibid/plugins/contest.py:10: 'eagerload' imported but unused
ibid/plugins/contest.py:70: local variable 'etree' is assigned to but never used

The name "contest" is too board. It should be called programming_contest, if anything.
Also, it's really esoteric, so it should have autoload = False on all the Processors.

> priority = -20
why? Put a comment in explaining please.

> urlencode({u'NAME': user, u'PASSWORD': password})
URLencode doesn't encode to utf-8, so utf-8 encode the parameters yourself, and give it bytestrings.

Applies to all your urlencodes()

> if font.text and font.text.find('Please try again') != -1:
More pythonic:
if font.text and u'Please try again' in font.text:

This crops up again in: division = [b.text for b in etree.getiterator(u'b') if b.text and b.text.find(usaco_user) != -1][0]

> class UsacoException(Exception):
> pass

I believe that if you are creating your own exception to carry an exception message it should do so and not rely on Exception, as Python 3's Exception doesn't have a message. We don't support Python 3 yet, but it's good practice.
> .filter(and_(
> Identity.identity == user,
> Identity.source == event.source)) \

You can save a line by doing:
 .filter(Identity.identity == user) \
 .filter(Identity.source == event.source) \

> usaco_account = [attr.value for attr in account.attributes if attr.name == 'usaco_account']

If you are going to do that, you do want to eager load attributes.

> __redacted__
I'd prefer [redacted]. It looks nicer :)

> def setup(self):
> pass

Be aware that if lp:~stefanor/ibid/ibid-plugin-507489 gets in first, it'll clash.

review: Needs Fixing
Revision history for this message
marcog (marco-gallotta) wrote : Posted in a previous version of this proposal

> The name "contest" is too board. It should be called programming_contest, if
> anything.

We still need to agree on a name. I agree that contest is too general. I think programming_contest is too long (20 characters and the longest current name is 12); if we do go with this, I think we should drop the _ to follow the style of other plugins (gameservers, buildbot). Vhata suggested progcomp, but as he said that's ugly. There's also the possibility that maths contests are added: what then?

> Run pyflakes. Otherwise you are simply asking me to do it for you:
>
> ibid/plugins/contest.py:1: 'codecs' imported but unused
> ibid/plugins/contest.py:4: 'ElementTree' imported but unused
> ibid/plugins/contest.py:8: 'ibid' imported but unused
> ibid/plugins/contest.py:10: 'eagerload' imported but unused
> ibid/plugins/contest.py:70: local variable 'etree' is assigned to but never
> used
>
> Also, it's really esoteric, so it should have autoload = False on all the
> Processors.
>
> > priority = -20
> why? Put a comment in explaining please.
>
> > urlencode({u'NAME': user, u'PASSWORD': password})
> URLencode doesn't encode to utf-8, so utf-8 encode the parameters yourself,
> and give it bytestrings.
>
> Applies to all your urlencodes()
>
> > if font.text and font.text.find('Please try again') != -1:
> More pythonic:
> if font.text and u'Please try again' in font.text:
>
> This crops up again in: division = [b.text for b in etree.getiterator(u'b') if
> b.text and b.text.find(usaco_user) != -1][0]
>
> > class UsacoException(Exception):
> > pass
>
> I believe that if you are creating your own exception to carry an exception
> message it should do so and not rely on Exception, as Python 3's Exception
> doesn't have a message. We don't support Python 3 yet, but it's good practice.
> > .filter(and_(
> > Identity.identity == user,
> > Identity.source == event.source)) \
>
> You can save a line by doing:
> .filter(Identity.identity == user) \
> .filter(Identity.source == event.source) \
>
> > usaco_account = [attr.value for attr in account.attributes if attr.name ==
> 'usaco_account']
>
> If you are going to do that, you do want to eager load attributes.
>
> > __redacted__
> I'd prefer [redacted]. It looks nicer :)

Done

> > def setup(self):
> > pass
>
> Be aware that if lp:~stefanor/ibid/ibid-plugin-507489 gets in first, it'll
> clash.

Hopefully someone notices :)

Revision history for this message
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal

ibid/plugins/contest.py:7: 'and_' imported but unused

And the name needs to be sorted. But otherwise I approve

review: Approve
Revision history for this message
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal

Can you add a copyright header to the top (see any other file in current trunk)

review: Needs Fixing
Revision history for this message
marcog (marco-gallotta) wrote : Posted in a previous version of this proposal

> Can you add a copyright header to the top (see any other file in current
> trunk)

Done

Revision history for this message
Stefano Rivera (stefanor) wrote :

It still needs a better name

review: Approve
Revision history for this message
Max Rabkin (max-rabkin) wrote :

It would be nice if "usaco results for rabkin1" worked using the USACO username, if the name is not a linked one. Even nicer, if real names worked too.

Perhaps it would be a good idea to forbid linking accounts in public, to discourage people from telling others their password.

When I tried to link my account on mibid, I waited a while and then got "That didn't go down very well. Burp."; So I haven't managed to test that successfully (either because of a bug in the plugin, or mibid's setup - a proxy issue?).

A few responses are missing full stops. Meta-comment: should the bot automatically add full stops to responses that don't end with terminating punctuation?

Revision history for this message
Max Rabkin (max-rabkin) wrote :

> When I tried to link my account on mibid, I waited a while and then got "That didn't go down very well. Burp."; So I haven't managed to test that successfully (either because of a bug in the plugin, or mibid's setup - a proxy issue?).

Tested on Ibido. For consistency with results, maybe make "for <user>"
optional in section/division queries, too, though it won't often be
used.

Perhaps the admin should be able to add anybody without their password
-- useful if the bot is a private one for a coaches channel.

If I ask for results for a competition I didn't participate in, I get
no response. I see no need to have a multiline response if I only
competed in one division: "taejo (rabkin1 on USACO) scored 86 in the
GOLD division" or "Gold: taejo (rabkin1 on USACO) scored 86" are both
better, IMO.

lp:~marco-gallotta/ibid/contest updated
863. By marcog

Search for usaco results also by name and usaco account when not linked

864. By marcog

Add timeout for fetching USACO results and suggest that the site might be down

865. By marcog

Forbid giving passwords in public

Revision history for this message
marcog (marco-gallotta) wrote :

> It would be nice if "usaco results for rabkin1" worked using the USACO
> username, if the name is not a linked one. Even nicer, if real names worked
> too.

Done

> Perhaps it would be a good idea to forbid linking accounts in public, to
> discourage people from telling others their password.

Done

> When I tried to link my account on mibid, I waited a while and then got "That
> didn't go down very well. Burp."; So I haven't managed to test that
> successfully (either because of a bug in the plugin, or mibid's setup - a
> proxy issue?).

That was a connection issue on my PC. I have put in something to try catch this, but for now it's untested.

> A few responses are missing full stops. Meta-comment: should the bot
> automatically add full stops to responses that don't end with terminating
> punctuation?

I did a very quick skim of a few plugins and it seems most responses don't end with a full stop. So leaving that as is for now.

> Tested on Ibido. For consistency with results, maybe make "for <user>"
> optional in section/division queries, too, though it won't often be
> used.

You mean you want it then to list for all users?

> Perhaps the admin should be able to add anybody without their password
> -- useful if the bot is a private one for a coaches channel.
>
> If I ask for results for a competition I didn't participate in, I get
> no response. I see no need to have a multiline response if I only
> competed in one division: "taejo (rabkin1 on USACO) scored 86 in the
> GOLD division" or "Gold: taejo (rabkin1 on USACO) scored 86" are both
> better, IMO.

Ok, will add these to my TODO list.

lp:~marco-gallotta/ibid/contest updated
866. By marcog

Allow admins to link usaco accounts without a usaco password

867. By marcog

Tell the user when usaco site appears dead

868. By marcog

Change from looking up Identities to Accounts table to find users not on the current source

869. By marcog

Improve the 'you need an account' msg

870. By marcog

Put division inline if only one person is listed in usaco results

Revision history for this message
marcog (marco-gallotta) wrote :

> > Tested on Ibido. For consistency with results, maybe make "for <user>"
> > optional in section/division queries, too, though it won't often be
> > used.
>
> You mean you want it then to list for all users?

Not doing, as per discussion.

> > Perhaps the admin should be able to add anybody without their password
> > -- useful if the bot is a private one for a coaches channel.
> >
> > If I ask for results for a competition I didn't participate in, I get
> > no response. I see no need to have a multiline response if I only
> > competed in one division: "taejo (rabkin1 on USACO) scored 86 in the
> > GOLD division" or "Gold: taejo (rabkin1 on USACO) scored 86" are both
> > better, IMO.
>
> Ok, will add these to my TODO list.

Done.

So everything so far has been addressed. Anything more?

Revision history for this message
Max Rabkin (max-rabkin) :
review: Approve
lp:~marco-gallotta/ibid/contest updated
871. By marcog

Correct the test for 'not a teacher account'

Revision history for this message
Keegan Carruthers-Smith (keegan-csmith) wrote :

I vote for "codecontest" as the plugin name. After marcog commits the fix I just suggested in IRC I approve.

review: Approve
lp:~marco-gallotta/ibid/contest updated
872. By marcog

Rename contest->codecontest

873. By marcog

Small tidy-up

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'ibid/plugins/codecontest.py'
2--- ibid/plugins/codecontest.py 1970-01-01 00:00:00 +0000
3+++ ibid/plugins/codecontest.py 2010-01-25 18:33:14 +0000
4@@ -0,0 +1,331 @@
5+# Copyright (c) 2010, Marco Gallotta
6+# Released under terms of the MIT/X/Expat Licence. See COPYING for details.
7+
8+import re
9+from urllib2 import HTTPError, URLError
10+
11+from urllib import urlencode
12+
13+from ibid.config import Option
14+from ibid.db import eagerload
15+from ibid.db.models import Account, Attribute, Identity
16+from ibid.plugins import Processor, match, auth_responses
17+from ibid.utils import cacheable_download
18+from ibid.utils.html import get_html_parse_tree
19+
20+help = {u'usaco': u'Query USACO sections, divisions and more. Since this info is private, users are required to provide their USACO password when linking their USACO account to their ibid account and only linked accounts can be queried. Your password is used only to confirm that the account is yours and is discarded immediately.'}
21+
22+class UsacoException(Exception):
23+ def __init__(self, msg):
24+ self.msg = msg
25+
26+ def __unicode__(self):
27+ return unicode(self.msg)
28+
29+class Usaco(Processor):
30+ """usaco <section|division> for <user>
31+ usaco <contest> results [for <name|user>]
32+ (i am|<user> is) <usaco_username> on usaco [password <usaco_password>]"""
33+
34+ admin_user = Option('admin_user', 'Admin user on USACO', None)
35+ admin_password = Option('admin_password', 'Admin password on USACO', None)
36+
37+ feature = 'usaco'
38+ # Clashes with identity, so lower our priority since if we match, then
39+ # this is the better match
40+ priority = -20
41+ autoload = False
42+
43+ def _login(self, user, password):
44+ params = urlencode({'NAME': user.encode('utf-8'), 'PASSWORD': password.encode('utf-8')})
45+ try:
46+ etree = get_html_parse_tree(u'http://ace.delos.com/usacogate', data=params, treetype=u'etree')
47+ except URLError:
48+ raise UsacoException(u'Sorry, USACO (or my connection?) is down')
49+ for font in etree.getiterator(u'font'):
50+ if font.text and u'Please try again' in font.text:
51+ return None
52+ return etree
53+
54+ def _check_login(self, user, password):
55+ return self._login(user, password) is not None
56+
57+ def _get_section(self, monitor_url, usaco_user, user):
58+ try:
59+ etree = get_html_parse_tree(monitor_url, treetype=u'etree')
60+ except URLError:
61+ raise UsacoException(u'Sorry, USACO (or my connection?) is down')
62+ usaco_user = usaco_user.lower()
63+ header = True
64+ for tr in etree.getiterator(u'tr'):
65+ if header:
66+ header = False
67+ continue
68+ tds = [t.text for t in tr.getiterator(u'td')]
69+ section = u'is on section %s' % tds[5]
70+ if tds[5] == u'DONE':
71+ section = u'has completed USACO training'
72+ if tds[0] and tds[0].lower() == usaco_user:
73+ return u'%(user)s (%(usaco_user)s on USACO) %(section)s and last logged in %(days)s ago' % {
74+ 'user': user,
75+ 'usaco_user': usaco_user,
76+ 'days': tds[3],
77+ 'section': section,
78+ }
79+
80+ return None
81+
82+ def _add_user(self, monitor_url, user):
83+ matches = re.search(r'a=(.+)&', monitor_url)
84+ auth = matches.group(1)
85+ params = urlencode({'STUDENTID': user.encode('utf-8'), 'ADD': 'ADD STUDENT',
86+ 'a': auth.encode('utf-8'), 'monitor': '1'})
87+ try:
88+ etree = get_html_parse_tree(monitor_url, treetype=u'etree', data=params)
89+ except URLError:
90+ raise UsacoException(u'Sorry, USACO (or my connection?) is down')
91+ for font in etree.getiterator(u'font'):
92+ if font.text and u'No STATUS file for' in font.text:
93+ raise UsacoException(u'Sorry, user %s not found' % user)
94+
95+ def _get_monitor_url(self):
96+ if self.admin_user is None or self.admin_password is None:
97+ raise UsacoException(u'Sorry, you need to configure a USACO admin account')
98+ return
99+ try:
100+ etree = self._login(self.admin_user, self.admin_password)
101+ except URLError:
102+ raise UsacoException(u'Sorry, USACO (or my connection?) is down')
103+ if etree is None:
104+ raise UsacoException(u'Sorry, the configured USACO admin account is invalid')
105+
106+ urls = [a.get(u'href') for a in etree.getiterator(u'a')]
107+ monitor_url = [url for url in urls if u'monitor' in url]
108+ if len(monitor_url) == 0:
109+ raise UsacoException(u'USACO admin account does not have teacher status')
110+
111+ return monitor_url[0]
112+
113+ def _get_usaco_user(self, event, user):
114+ account = event.session.query(Account) \
115+ .options(eagerload('attributes')) \
116+ .filter(Account.username == user) \
117+ .first()
118+ if account is None:
119+ account = event.session.query(Account) \
120+ .options(eagerload('attributes')) \
121+ .join('identities') \
122+ .filter(Identity.identity == user) \
123+ .filter(Identity.source == event.source) \
124+ .first()
125+ if account is None:
126+ raise UsacoException(u'Sorry, %s has not been linked to a USACO account yet' % user)
127+
128+ usaco_account = [attr.value for attr in account.attributes if attr.name == 'usaco_account']
129+ if len(usaco_account) == 0:
130+ raise UsacoException(u'Sorry, %s has not been linked to a USACO account yet' % user)
131+ return usaco_account[0]
132+
133+ def _get_usaco_users(self, event):
134+ accounts = event.session.query(Account) \
135+ .join(['attributes']) \
136+ .add_entity(Attribute) \
137+ .filter(Attribute.name == u'usaco_account') \
138+ .all()
139+
140+ users = {}
141+ for a in accounts:
142+ users[a[1].value] = a[0].username
143+ return users
144+
145+ @match(r'^usaco\s+section\s+(?:for\s+)?(.+)$')
146+ def get_section(self, event, user):
147+ try:
148+ usaco_user = self._get_usaco_user(event, user)
149+ monitor_url = self._get_monitor_url()
150+ section = self._get_section(monitor_url, usaco_user, user)
151+ except UsacoException, e:
152+ event.addresponse(e)
153+ return
154+
155+ if section:
156+ event.addresponse(section)
157+ return
158+
159+ try:
160+ self._add_user(monitor_url, user)
161+ event.addresponse(self._get_section(monitor_url, usaco_user, user))
162+ except UsacoException, e:
163+ event.addresponse(e)
164+ return
165+
166+ @match(r'^usaco\s+division\s+(?:for\s+)?(.+)$')
167+ def get_division(self, event, user):
168+ try:
169+ usaco_user = self._get_usaco_user(event, user)
170+ except UsacoException, e:
171+ event.addresponse(e)
172+ return
173+
174+ params = urlencode({'id': usaco_user.encode('utf-8'), 'search': 'SEARCH'})
175+ try:
176+ etree = get_html_parse_tree(u'http://ace.delos.com/showdiv', data=params, treetype=u'etree')
177+ except URLError:
178+ event.addresponse(u'Sorry, USACO (or my connection?) is down')
179+ division = [b.text for b in etree.getiterator(u'b') if b.text and usaco_user in b.text][0]
180+ if division.find(u'would compete') != -1:
181+ event.addresponse(u'%(user)s (%(usaco_user)s on USACO) has not competed in a USACO before',
182+ {u'user': user, u'usaco_user': usaco_user})
183+ matches = re.search(r'(\w+) Division', division)
184+ division = matches.group(1).lower()
185+ event.addresponse(u'%(user)s (%(usaco_user)s on USACO) is in the %(division)s division',
186+ {u'user': user, u'usaco_user': usaco_user, u'division': division})
187+
188+ def _redact(self, event, term):
189+ for type in ['raw', 'deaddressed', 'clean', 'stripped']:
190+ event['message'][type] = re.sub(r'(.*)(%s)' % re.escape(term), r'\1[redacted]', event['message'][type])
191+
192+ @match(r'^(\S+)\s+(?:is|am)\s+(\S+)\s+on\s+usaco(?:\s+password\s+(\S+))?$')
193+ def usaco_account(self, event, user, usaco_user, password):
194+ if password:
195+ self._redact(event, password)
196+
197+ if event.public and password:
198+ event.addresponse(u"Giving your password in public is bad! Please tell me that again in a private message.")
199+ return
200+
201+ if not event.account:
202+ event.addresponse(u'Sorry, you need to create an account with me first (type "usage accounts" to see how)')
203+ return
204+ admin = auth_responses(event, u'usacoadmin')
205+ if user.lower() == 'i':
206+ if password is None and not admin:
207+ event.addresponse(u'Sorry, I need your USACO password to verify your account')
208+ if password and not self._check_login(user, password):
209+ event.addresponse(u'Sorry, that account is invalid')
210+ return
211+ account = event.session.query(Account).get(event.account)
212+ else:
213+ if not admin:
214+ event.addresponse(event.complain)
215+ return
216+ account = event.session.query(Account).filter_by(username=user).first()
217+ if account is None:
218+ event.addresponse(u"I don't know who %s is", user)
219+ return
220+
221+ try:
222+ monitor_url = self._get_monitor_url()
223+ except UsacoException, e:
224+ event.addresponse(e)
225+ return
226+
227+ try:
228+ self._add_user(monitor_url, usaco_user)
229+ except UsacoException, e:
230+ event.addresponse(e)
231+ return
232+
233+ usaco_account = [attr for attr in account.attributes if attr.name == u'usaco_account']
234+ if usaco_account:
235+ usaco_account[0].value = usaco_user
236+ else:
237+ account.attributes.append(Attribute('usaco_account', usaco_user))
238+ event.session.save_or_update(account)
239+ event.session.commit()
240+
241+ event.addresponse(u'Done')
242+
243+ @match(r'^usaco\s+(\S+)\s+results(?:\s+for\s+(.+))?$')
244+ def usaco_results(self, event, contest, user):
245+ if user is not None:
246+ try:
247+ usaco_user = self._get_usaco_user(event, user)
248+ except UsacoException, e:
249+ if 'down' in e.msg:
250+ event.addresponse(e)
251+ return
252+ usaco_user = user
253+
254+ url = u'http://ace.delos.com/%sresults' % contest.upper()
255+ try:
256+ filename = cacheable_download(url, u'usaco/results_%s' % contest.upper(), timeout=30)
257+ except HTTPError:
258+ event.addresponse(u"Sorry, the results for %s aren't released yet", contest)
259+ except URLError:
260+ event.addresponse(u"Sorry, I couldn't fetch the USACO results. Maybe USACO is down?")
261+
262+ if user is not None:
263+ users = {usaco_user: user.lower()}
264+ else:
265+ try:
266+ users = self._get_usaco_users(event)
267+ print users
268+ except UsacoException, e:
269+ event.addresponse(e)
270+ return
271+
272+ text = open(filename, 'r').read().decode('ISO-8859-2')
273+ divisions = [u'gold', u'silver', u'bronze']
274+ results = [[], [], []]
275+ division = None
276+ count = 0
277+ for line in text.splitlines():
278+ for index, d in enumerate(divisions):
279+ if d in line.lower():
280+ division = index
281+ # Example results line:
282+ # 2010 POL Jakub Pachocki meret1 ***** ***** 270 ***** ***** * 396 ***** ***** ** 324 1000
283+ matches = re.match(r'^\s*(\d{4})\s+([A-Z]{3})\s+(.+?)\s+(\S+\d)\s+([\*xts\.e0-9 ]+?)\s+(\d+)\s*$', line)
284+ if matches:
285+ year = matches.group(1)
286+ country = matches.group(2)
287+ name = matches.group(3)
288+ usaco_user = matches.group(4)
289+ scores = matches.group(5)
290+ total = matches.group(6)
291+ match = False
292+ if usaco_user.lower() in users.keys():
293+ match = True
294+ elif user is not None and name.lower() == user.lower():
295+ match = True
296+ users[usaco_user] = user
297+ if match:
298+ results[division].append((year, country, name, usaco_user, scores, total))
299+ count += 1
300+
301+ response = []
302+ for i, division in enumerate(divisions):
303+ if results[i] and count > 1:
304+ response.append(u'%s division results:' % division.title())
305+ for result in results[i]:
306+ user_string = users[result[3]]
307+ if users[result[3]] != result[3]:
308+ user_string = u'%(user)s (%(usaco_user)s on USACO)' % {
309+ u'user': users[result[3]],
310+ u'usaco_user': result[3],
311+ }
312+ if count <= 1:
313+ division_string = u' in the %s division' % division.title()
314+ else:
315+ division_string = u''
316+ response.append(u'%(user)s scored %(total)s%(division)s (%(scores)s)' % {
317+ u'user': user_string,
318+ u'total': result[5],
319+ u'scores': result[4],
320+ u'division': division_string
321+ })
322+
323+ if count == 0:
324+ if user is not None:
325+ event.addresponse(u'%(user)s did not compete in %(contest)s', {
326+ u'user': user,
327+ u'contest': contest,
328+ })
329+ else:
330+ event.addresponse(u"Sorry, I don't know anyone that entered %s", contest)
331+ return
332+
333+ event.addresponse(u'\n'.join(response), conflate=False)
334+
335+# vi: set et sta sw=4 ts=4:
336
337=== modified file 'ibid/utils/__init__.py'
338--- ibid/utils/__init__.py 2010-01-24 18:38:02 +0000
339+++ ibid/utils/__init__.py 2010-01-25 18:33:15 +0000
340@@ -44,20 +44,20 @@
341 return text
342
343 downloads_in_progress = defaultdict(Lock)
344-def cacheable_download(url, cachefile, headers={}):
345+def cacheable_download(url, cachefile, headers={}, timeout=60):
346 """Download url to cachefile if it's modified since cachefile.
347 Specify cachefile in the form pluginname/cachefile.
348 Returns complete path to downloaded file."""
349
350 downloads_in_progress[cachefile].acquire()
351 try:
352- f = _cacheable_download(url, cachefile, headers)
353+ f = _cacheable_download(url, cachefile, headers, timeout)
354 finally:
355 downloads_in_progress[cachefile].release()
356
357 return f
358
359-def _cacheable_download(url, cachefile, headers={}):
360+def _cacheable_download(url, cachefile, headers={}, timeout=60):
361 # We do allow absolute paths, for people who know what they are doing,
362 # but the common use case should be pluginname/cachefile.
363 if cachefile[0] not in (os.sep, os.altsep):
364@@ -93,7 +93,7 @@
365 req.add_header("If-Modified-Since", modified)
366
367 try:
368- connection = urllib2.urlopen(req)
369+ connection = urllib2.urlopen(req, timeout=timeout)
370 except urllib2.HTTPError, e:
371 if e.code == 304 and exists:
372 return cachefile
373
374=== modified file 'scripts/ibid-plugin'
375--- scripts/ibid-plugin 2010-01-18 22:13:23 +0000
376+++ scripts/ibid-plugin 2010-01-25 18:33:15 +0000
377@@ -65,6 +65,9 @@
378 permissions = []
379 supports = ('action', 'multiline', 'notice')
380
381+ def setup(self):
382+ pass
383+
384 def logging_name(self, name):
385 return name
386

Subscribers

People subscribed via source and target branches