Merge lp:~marco-gallotta/ibid/contest into lp:~ibid-core/ibid/old-trunk-1.6
- contest
- Merge into old-trunk-1.6
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 | ||||
Related bugs: |
|
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.
Commit message
Description of the change
marcog (marco-gallotta) wrote : Posted in a previous version of this proposal | # |
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/
ibid/plugins/
ibid/plugins/
ibid/plugins/
ibid/plugins/
The name "contest" is too board. It should be called programming_
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.
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.getiterat
> class UsacoException(
> 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(
.filter(
> 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.
marcog (marco-gallotta) wrote : Posted in a previous version of this proposal | # |
> The name "contest" is too board. It should be called programming_
> 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/
> ibid/plugins/
> ibid/plugins/
> ibid/plugins/
> ibid/plugins/
> 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.
> 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.getiterat
> b.text and b.text.
>
> > class UsacoException(
> > 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(
> .filter(
>
> > 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 :)
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal | # |
ibid/plugins/
And the name needs to be sorted. But otherwise I approve
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)
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
Stefano Rivera (stefanor) wrote : | # |
It still needs a better name
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?
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.
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.
- 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
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?
Max Rabkin (max-rabkin) : | # |
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.
Preview Diff
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 |
First iteration of the contest plugin, with USACO scriping pretty much done.