80c337913107cb95506232b135ffe47969ced19f
[wikipedia-api-cdsw] / mwclient / page.py
1 import client
2 import errors
3 import listing
4
5 import urllib
6 import urlparse
7 import time
8 import warnings
9
10
11 class Page(object):
12
13     def __init__(self, site, name, info=None, extra_properties={}):
14         if type(name) is type(self):
15             return self.__dict__.update(name.__dict__)
16         self.site = site
17         self.name = name
18         self.section = None
19
20         if not info:
21             if extra_properties:
22                 prop = 'info|' + '|'.join(extra_properties.iterkeys())
23                 extra_props = []
24                 [extra_props.extend(extra_prop) for extra_prop in extra_properties.itervalues()]
25             else:
26                 prop = 'info'
27                 extra_props = ()
28
29             if type(name) is int:
30                 info = self.site.api('query', prop=prop, pageids=name,
31                                      inprop='protection', *extra_props)
32             else:
33                 info = self.site.api('query', prop=prop, titles=name,
34                                      inprop='protection', *extra_props)
35             info = info['query']['pages'].itervalues().next()
36         self._info = info
37
38         self.namespace = info.get('ns', 0)
39         self.name = info.get('title', u'')
40         if self.namespace:
41             self.page_title = self.strip_namespace(self.name)
42         else:
43             self.page_title = self.name
44
45         self.touched = client.parse_timestamp(info.get('touched', '0000-00-00T00:00:00Z'))
46         self.revision = info.get('lastrevid', 0)
47         self.exists = 'missing' not in info
48         self.length = info.get('length')
49         self.protection = dict([(i['type'], (i['level'], i['expiry'])) for i in info.get('protection', ()) if i])
50         self.redirect = 'redirect' in info
51
52         self.last_rev_time = None
53         self.edit_time = None
54
55     def redirects_to(self):
56         """ Returns the redirect target page, or None if the page is not a redirect page."""
57         info = self.site.api('query', prop='pageprops', titles=self.name, redirects='')['query']
58         if 'redirects' in info:
59             for page in info['redirects']:
60                 if page['from'] == self.name:
61                     return Page(self.site, page['to'])
62             return None
63         else:
64             return None
65
66     def resolve_redirect(self):
67         """ Returns the redirect target page, or the current page if it's not a redirect page."""
68         target_page = self.redirects_to()
69         if target_page is None:
70             return self
71         else:
72             return target_page
73
74     def __repr__(self):
75         return "<Page object '%s' for %s>" % (self.name.encode('utf-8'), self.site)
76
77     def __unicode__(self):
78         return self.name
79
80     @staticmethod
81     def strip_namespace(title):
82         if title[0] == ':':
83             title = title[1:]
84         return title[title.find(':') + 1:]
85
86     @staticmethod
87     def normalize_title(title):
88         # TODO: Make site dependent
89         title = title.strip()
90         if title[0] == ':':
91             title = title[1:]
92         title = title[0].upper() + title[1:]
93         title = title.replace(' ', '_')
94         return title
95
96     def can(self, action):
97         level = self.protection.get(action, (action, ))[0]
98         if level == 'sysop':
99             level = 'editprotected'
100
101         return level in self.site.rights
102
103     def get_token(self, type, force=False):
104         return self.site.get_token(type, force, title=self.name)
105
106     def get_expanded(self):
107         """Deprecated. Use page.text(expandtemplates=True) instead"""
108         warnings.warn("page.get_expanded() was deprecated in mwclient 0.7.0, use page.text(expandtemplates=True) instead.",
109                       category=DeprecationWarning, stacklevel=2)
110
111         return self.text(expandtemplates=True)
112
113     def edit(self, *args, **kwargs):
114         """Deprecated. Use page.text() instead"""
115         warnings.warn("page.edit() was deprecated in mwclient 0.7.0, please use page.text() instead.",
116                       category=DeprecationWarning, stacklevel=2)
117         return self.text(*args, **kwargs)
118
119     def text(self, section=None, expandtemplates=False):
120         """
121         Returns the current wikitext of the page, or of a specific section.
122         If the page does not exist, an empty string is returned.
123
124         :Arguments:
125           - `section` : numbered section or `None` to get the whole page (default: `None`)
126           - `expandtemplates` : set to `True` to expand templates (default: `False`)
127         """
128
129         if not self.can('read'):
130             raise errors.InsufficientPermission(self)
131         if not self.exists:
132             return u''
133         if section is not None:
134             section = str(section)
135
136         revs = self.revisions(prop='content|timestamp', limit=1, section=section, expandtemplates=expandtemplates)
137         try:
138             rev = revs.next()
139             text = rev['*']
140             self.section = section
141             self.last_rev_time = rev['timestamp']
142         except StopIteration:
143             text = u''
144             self.section = None
145             self.last_rev_time = None
146         if not expandtemplates:
147             self.edit_time = time.gmtime()
148         return text
149
150     def save(self, text, summary=u'', minor=False, bot=True, section=None, **kwargs):
151         """
152         Update the text of a section or the whole page by performing an edit operation.
153         """
154         if not self.site.logged_in and self.site.force_login:
155             # Should we really check for this?
156             raise errors.LoginError(self.site, 'By default, mwclient protects you from accidentally ' +
157                                     'editing without being logged in. If you actually want to edit without '
158                                     'logging in, you can set force_login on the Site object to False.')
159         if self.site.blocked:
160             raise errors.UserBlocked(self.site.blocked)
161         if not self.can('edit'):
162             raise errors.ProtectedPageError(self)
163
164         if not section:
165             section = self.section
166
167         if not self.site.writeapi:
168             raise errors.NoWriteApi(self)
169
170         data = {}
171         if minor:
172             data['minor'] = '1'
173         if not minor:
174             data['notminor'] = '1'
175         if self.last_rev_time:
176             data['basetimestamp'] = time.strftime('%Y%m%d%H%M%S', self.last_rev_time)
177         if self.edit_time:
178             data['starttimestamp'] = time.strftime('%Y%m%d%H%M%S', self.edit_time)
179         if bot:
180             data['bot'] = '1'
181         if section:
182             data['section'] = section
183
184         data.update(kwargs)
185
186         def do_edit():
187             result = self.site.api('edit', title=self.name, text=text,
188                                    summary=summary, token=self.get_token('edit'),
189                                    **data)
190             if result['edit'].get('result').lower() == 'failure':
191                 raise errors.EditError(self, result['edit'])
192             return result
193         try:
194             result = do_edit()
195         except errors.APIError, e:
196             if e.code == 'badtoken':
197                 # Retry, but only once to avoid an infinite loop
198                 self.get_token('edit', force=True)
199                 try:
200                     result = do_edit()
201                 except errors.APIError, e:
202                     self.handle_edit_error(e, summary)
203             else:
204                 self.handle_edit_error(e, summary)
205
206         # 'newtimestamp' is not included if no change was made
207         if 'newtimestamp' in result['edit'].keys():
208             self.last_rev_time = client.parse_timestamp(result['edit'].get('newtimestamp'))
209         return result['edit']
210
211     def handle_edit_error(self, e, summary):
212         if e.code == 'editconflict':
213             raise errors.EditError(self, summary, e.info)
214         elif e.code in ('protectedtitle', 'cantcreate', 'cantcreate-anon', 'noimageredirect-anon',
215                         'noimageredirect', 'noedit-anon', 'noedit'):
216             raise errors.ProtectedPageError(self, e.code, e.info)
217         else:
218             raise
219
220     def move(self, new_title, reason='', move_talk=True, no_redirect=False):
221         """Move (rename) page to new_title.
222
223         If user account is an administrator, specify no_direct as True to not
224         leave a redirect.
225
226         If user does not have permission to move page, an InsufficientPermission
227         exception is raised.
228
229         """
230         if not self.can('move'):
231             raise errors.InsufficientPermission(self)
232
233         if not self.site.writeapi:
234             raise errors.NoWriteApi(self)
235
236         data = {}
237         if move_talk:
238             data['movetalk'] = '1'
239         if no_redirect:
240             data['noredirect'] = '1'
241         result = self.site.api('move', ('from', self.name), to=new_title,
242                                token=self.get_token('move'), reason=reason, **data)
243         return result['move']
244
245     def delete(self, reason='', watch=False, unwatch=False, oldimage=False):
246         """Delete page.
247
248         If user does not have permission to delete page, an InsufficientPermission
249         exception is raised.
250
251         """
252         if not self.can('delete'):
253             raise errors.InsufficientPermission(self)
254
255         if not self.site.writeapi:
256             raise errors.NoWriteApi(self)
257
258         data = {}
259         if watch:
260             data['watch'] = '1'
261         if unwatch:
262             data['unwatch'] = '1'
263         if oldimage:
264             data['oldimage'] = oldimage
265         result = self.site.api('delete', title=self.name,
266                                token=self.get_token('delete'),
267                                reason=reason, **data)
268         return result['delete']
269
270     def purge(self):
271         """Purge server-side cache of page. This will re-render templates and other
272         dynamic content.
273
274         """
275         self.site.raw_index('purge', title=self.name)
276
277     # def watch: requires 1.14
278
279     # Properties
280     def backlinks(self, namespace=None, filterredir='all', redirect=False, limit=None, generator=True):
281         prefix = listing.List.get_prefix('bl', generator)
282         kwargs = dict(listing.List.generate_kwargs(prefix,
283                                                    namespace=namespace, filterredir=filterredir))
284         if redirect:
285             kwargs['%sredirect' % prefix] = '1'
286         kwargs[prefix + 'title'] = self.name
287
288         return listing.List.get_list(generator)(self.site, 'backlinks', 'bl', limit=limit, return_values='title', **kwargs)
289
290     def categories(self, generator=True):
291         if generator:
292             return listing.PagePropertyGenerator(self, 'categories', 'cl')
293         else:
294             # TODO: return sortkey if wanted
295             return listing.PageProperty(self, 'categories', 'cl', return_values='title')
296
297     def embeddedin(self, namespace=None, filterredir='all', redirect=False, limit=None, generator=True):
298         prefix = listing.List.get_prefix('ei', generator)
299         kwargs = dict(listing.List.generate_kwargs(prefix,
300                                                    namespace=namespace, filterredir=filterredir))
301         if redirect:
302             kwargs['%sredirect' % prefix] = '1'
303         kwargs[prefix + 'title'] = self.name
304
305         return listing.List.get_list(generator)(self.site, 'embeddedin', 'ei', limit=limit, return_values='title', **kwargs)
306
307     def extlinks(self):
308         return listing.PageProperty(self, 'extlinks', 'el', return_values='*')
309
310     def images(self, generator=True):
311         if generator:
312             return listing.PagePropertyGenerator(self, 'images', '')
313         else:
314             return listing.PageProperty(self, 'images', '', return_values='title')
315
316     def iwlinks(self):
317         return listing.PageProperty(self, 'iwlinks', 'iw', return_values=('prefix', '*'))
318
319     def langlinks(self, **kwargs):
320         return listing.PageProperty(self, 'langlinks', 'll', return_values=('lang', '*'), **kwargs)
321
322     def links(self, namespace=None, generator=True, redirects=False):
323         kwargs = dict(listing.List.generate_kwargs('pl', namespace=namespace))
324         if redirects:
325             kwargs['redirects'] = '1'
326         if generator:
327             return listing.PagePropertyGenerator(self, 'links', 'pl', **kwargs)
328         else:
329             return listing.PageProperty(self, 'links', 'pl', return_values='title', **kwargs)
330
331     def revisions(self, startid=None, endid=None, start=None, end=None,
332                   dir='older', user=None, excludeuser=None, limit=50,
333                   prop='ids|timestamp|flags|comment|user', expandtemplates=False, section=None):
334         kwargs = dict(listing.List.generate_kwargs('rv', startid=startid, endid=endid,
335                                                    start=start, end=end, user=user, excludeuser=excludeuser))
336         kwargs['rvdir'] = dir
337         kwargs['rvprop'] = prop
338         if expandtemplates:
339             kwargs['rvexpandtemplates'] = '1'
340         if section is not None:
341             kwargs['rvsection'] = section
342
343         return listing.RevisionsIterator(self, 'revisions', 'rv', limit=limit, **kwargs)
344
345     def templates(self, namespace=None, generator=True):
346         kwargs = dict(listing.List.generate_kwargs('tl', namespace=namespace))
347         if generator:
348             return listing.PagePropertyGenerator(self, 'templates', 'tl')
349         else:
350             return listing.PageProperty(self, 'templates', 'tl', return_values='title')
351
352
353 class Image(Page):
354
355     def __init__(self, site, name, info=None):
356         Page.__init__(self, site, name, info,
357                       extra_properties={'imageinfo':
358                                         (('iiprop', 'timestamp|user|comment|url|size|sha1|metadata|archivename'), )
359                                         })
360         self.imagerepository = self._info.get('imagerepository', '')
361         self.imageinfo = self._info.get('imageinfo', ({}, ))[0]
362
363     def imagehistory(self):
364         return listing.PageProperty(self, 'imageinfo', 'ii',
365                                     iiprop='timestamp|user|comment|url|size|sha1|metadata|archivename')
366
367     def imageusage(self, namespace=None, filterredir='all', redirect=False,
368                    limit=None, generator=True):
369         prefix = listing.List.get_prefix('iu', generator)
370         kwargs = dict(listing.List.generate_kwargs(prefix, title=self.name,
371                                                    namespace=namespace, filterredir=filterredir))
372         if redirect:
373             kwargs['%sredirect' % prefix] = '1'
374         return listing.List.get_list(generator)(self.site, 'imageusage', 'iu',
375                                                 limit=limit, return_values='title', **kwargs)
376
377     def duplicatefiles(self, limit=None):
378         return listing.PageProperty(self, 'duplicatefiles', 'df',
379                                     dflimit=limit)
380
381     def download(self):
382         url = self.imageinfo['url']
383         if not url.startswith('http://'):
384             url = 'http://' + self.site.host + url
385         url = urlparse.urlparse(url)
386         # TODO: query string
387         return self.site.connection.get(url[1], url[2])
388
389     def __repr__(self):
390         return "<Image object '%s' for %s>" % (self.name.encode('utf-8'), self.site)

Benjamin Mako Hill || Want to submit a patch?