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

Subscribers

People subscribed via source and target branches