diff options
Diffstat (limited to 'game/python-extra/mwclient/page.py')
-rw-r--r-- | game/python-extra/mwclient/page.py | 541 |
1 files changed, 541 insertions, 0 deletions
diff --git a/game/python-extra/mwclient/page.py b/game/python-extra/mwclient/page.py new file mode 100644 index 0000000..d83896c --- /dev/null +++ b/game/python-extra/mwclient/page.py @@ -0,0 +1,541 @@ +import six +from six import text_type +import time +from mwclient.util import parse_timestamp +import mwclient.listing +import mwclient.errors + + +class Page(object): + + def __init__(self, site, name, info=None, extra_properties=None): + if type(name) is type(self): + self.__dict__.update(name.__dict__) + return + self.site = site + self.name = name + self._textcache = {} + + if not info: + if extra_properties: + prop = 'info|' + '|'.join(six.iterkeys(extra_properties)) + extra_props = [] + for extra_prop in six.itervalues(extra_properties): + extra_props.extend(extra_prop) + else: + prop = 'info' + extra_props = () + + if type(name) is int: + info = self.site.get('query', prop=prop, pageids=name, + inprop='protection', *extra_props) + else: + info = self.site.get('query', prop=prop, titles=name, + inprop='protection', *extra_props) + info = six.next(six.itervalues(info['query']['pages'])) + self._info = info + + if 'invalid' in info: + raise mwclient.errors.InvalidPageTitle(info.get('invalidreason')) + + self.namespace = info.get('ns', 0) + self.name = info.get('title', u'') + if self.namespace: + self.page_title = self.strip_namespace(self.name) + else: + self.page_title = self.name + + self.base_title = self.page_title.split('/')[0] + self.base_name = self.name.split('/')[0] + + self.touched = parse_timestamp(info.get('touched')) + self.revision = info.get('lastrevid', 0) + self.exists = 'missing' not in info + self.length = info.get('length') + self.protection = { + i['type']: (i['level'], i['expiry']) + for i in info.get('protection', ()) + if i + } + self.redirect = 'redirect' in info + self.pageid = info.get('pageid', None) + self.contentmodel = info.get('contentmodel', None) + self.pagelanguage = info.get('pagelanguage', None) + self.restrictiontypes = info.get('restrictiontypes', None) + + self.last_rev_time = None + self.edit_time = None + + def redirects_to(self): + """ Get the redirect target page, or None if the page is not a redirect.""" + info = self.site.get('query', prop='pageprops', titles=self.name, redirects='') + if 'redirects' in info['query']: + for page in info['query']['redirects']: + if page['from'] == self.name: + return Page(self.site, page['to']) + return None + else: + return None + + def resolve_redirect(self): + """ Get the redirect target page, or the current page if its not a redirect.""" + target_page = self.redirects_to() + if target_page is None: + return self + else: + return target_page + + def __repr__(self): + return "<Page object '%s' for %s>" % (self.name.encode('utf-8'), self.site) + + def __unicode__(self): + return self.name + + @staticmethod + def strip_namespace(title): + if title[0] == ':': + title = title[1:] + return title[title.find(':') + 1:] + + @staticmethod + def normalize_title(title): + # TODO: Make site dependent + title = title.strip() + if title[0] == ':': + title = title[1:] + title = title[0].upper() + title[1:] + title = title.replace(' ', '_') + return title + + def can(self, action): + """Check if the current user has the right to carry out some action + with the current page. + + Example: + >>> page.can('edit') + True + + """ + level = self.protection.get(action, (action,))[0] + if level == 'sysop': + level = 'editprotected' + + return level in self.site.rights + + def get_token(self, type, force=False): + return self.site.get_token(type, force, title=self.name) + + def text(self, section=None, expandtemplates=False, cache=True, slot='main'): + """Get the current wikitext of the page, or of a specific section. + + If the page does not exist, an empty string is returned. By + default, results will be cached and if you call text() again + with the same section and expandtemplates the result will come + from the cache. The cache is stored on the instance, so it + lives as long as the instance does. + + Args: + section (int): Section number, to only get text from a single section. + expandtemplates (bool): Expand templates (default: `False`) + cache (bool): Use in-memory caching (default: `True`) + """ + + if not self.can('read'): + raise mwclient.errors.InsufficientPermission(self) + if not self.exists: + return u'' + if section is not None: + section = text_type(section) + + key = hash((section, expandtemplates)) + if cache and key in self._textcache: + return self._textcache[key] + + revs = self.revisions(prop='content|timestamp', limit=1, section=section, + slots=slot) + try: + rev = next(revs) + if 'slots' in rev: + text = rev['slots'][slot]['*'] + else: + text = rev['*'] + self.last_rev_time = rev['timestamp'] + except StopIteration: + text = u'' + self.last_rev_time = None + if not expandtemplates: + self.edit_time = time.gmtime() + else: + # The 'rvexpandtemplates' option was removed in MediaWiki 1.32, so we have to + # make an extra API call, see https://github.com/mwclient/mwclient/issues/214 + text = self.site.expandtemplates(text) + + if cache: + self._textcache[key] = text + return text + + def save(self, *args, **kwargs): + """Alias for edit, for maintaining backwards compatibility.""" + return self.edit(*args, **kwargs) + + def edit(self, text, summary=u'', minor=False, bot=True, section=None, **kwargs): + """Update the text of a section or the whole page by performing an edit operation. + """ + return self._edit(summary, minor, bot, section, text=text, **kwargs) + + def append(self, text, summary=u'', minor=False, bot=True, section=None, + **kwargs): + """Append text to a section or the whole page by performing an edit operation. + """ + return self._edit(summary, minor, bot, section, appendtext=text, **kwargs) + + def prepend(self, text, summary=u'', minor=False, bot=True, section=None, + **kwargs): + """Prepend text to a section or the whole page by performing an edit operation. + """ + return self._edit(summary, minor, bot, section, prependtext=text, **kwargs) + + def _edit(self, summary, minor, bot, section, **kwargs): + if not self.site.logged_in and self.site.force_login: + raise mwclient.errors.AssertUserFailedError() + if self.site.blocked: + raise mwclient.errors.UserBlocked(self.site.blocked) + if not self.can('edit'): + raise mwclient.errors.ProtectedPageError(self) + + if not self.site.writeapi: + raise mwclient.errors.NoWriteApi(self) + + data = {} + if minor: + data['minor'] = '1' + if not minor: + data['notminor'] = '1' + if self.last_rev_time: + data['basetimestamp'] = time.strftime('%Y%m%d%H%M%S', self.last_rev_time) + if self.edit_time: + data['starttimestamp'] = time.strftime('%Y%m%d%H%M%S', self.edit_time) + if bot: + data['bot'] = '1' + if section is not None: + data['section'] = section + + data.update(kwargs) + + if self.site.force_login: + data['assert'] = 'user' + + def do_edit(): + result = self.site.post('edit', title=self.name, summary=summary, + token=self.get_token('edit'), + **data) + if result['edit'].get('result').lower() == 'failure': + raise mwclient.errors.EditError(self, result['edit']) + return result + + try: + result = do_edit() + except mwclient.errors.APIError as e: + if e.code == 'badtoken': + # Retry, but only once to avoid an infinite loop + self.get_token('edit', force=True) + try: + result = do_edit() + except mwclient.errors.APIError as e: + self.handle_edit_error(e, summary) + else: + self.handle_edit_error(e, summary) + + # 'newtimestamp' is not included if no change was made + if 'newtimestamp' in result['edit'].keys(): + self.last_rev_time = parse_timestamp(result['edit'].get('newtimestamp')) + + # Workaround for https://phabricator.wikimedia.org/T211233 + for cookie in self.site.connection.cookies: + if 'PostEditRevision' in cookie.name: + self.site.connection.cookies.clear(cookie.domain, cookie.path, + cookie.name) + + # clear the page text cache + self._textcache = {} + return result['edit'] + + def handle_edit_error(self, e, summary): + if e.code == 'editconflict': + raise mwclient.errors.EditError(self, summary, e.info) + elif e.code in {'protectedtitle', 'cantcreate', 'cantcreate-anon', + 'noimageredirect-anon', 'noimageredirect', 'noedit-anon', + 'noedit', 'protectedpage', 'cascadeprotected', + 'customcssjsprotected', + 'protectednamespace-interface', 'protectednamespace'}: + raise mwclient.errors.ProtectedPageError(self, e.code, e.info) + elif e.code == 'assertuserfailed': + raise mwclient.errors.AssertUserFailedError() + else: + raise e + + def touch(self): + """Perform a "null edit" on the page to update the wiki's cached data of it. + This is useful in contrast to purge when needing to update stored data on a wiki, + for example Semantic MediaWiki properties or Cargo table values, since purge + only forces update of a page's displayed values and not its store. + """ + if not self.exists: + return + self.append('') + + def move(self, new_title, reason='', move_talk=True, no_redirect=False): + """Move (rename) page to new_title. + + If user account is an administrator, specify no_redirect as True to not + leave a redirect. + + If user does not have permission to move page, an InsufficientPermission + exception is raised. + + """ + if not self.can('move'): + raise mwclient.errors.InsufficientPermission(self) + + if not self.site.writeapi: + raise mwclient.errors.NoWriteApi(self) + + data = {} + if move_talk: + data['movetalk'] = '1' + if no_redirect: + data['noredirect'] = '1' + result = self.site.post('move', ('from', self.name), to=new_title, + token=self.get_token('move'), reason=reason, **data) + return result['move'] + + def delete(self, reason='', watch=False, unwatch=False, oldimage=False): + """Delete page. + + If user does not have permission to delete page, an InsufficientPermission + exception is raised. + + """ + if not self.can('delete'): + raise mwclient.errors.InsufficientPermission(self) + + if not self.site.writeapi: + raise mwclient.errors.NoWriteApi(self) + + data = {} + if watch: + data['watch'] = '1' + if unwatch: + data['unwatch'] = '1' + if oldimage: + data['oldimage'] = oldimage + result = self.site.post('delete', title=self.name, + token=self.get_token('delete'), + reason=reason, **data) + return result['delete'] + + def purge(self): + """Purge server-side cache of page. This will re-render templates and other + dynamic content. + + """ + self.site.post('purge', titles=self.name) + + # def watch: requires 1.14 + + # Properties + def backlinks(self, namespace=None, filterredir='all', redirect=False, + limit=None, generator=True): + """List pages that link to the current page, similar to Special:Whatlinkshere. + + API doc: https://www.mediawiki.org/wiki/API:Backlinks + + """ + prefix = mwclient.listing.List.get_prefix('bl', generator) + kwargs = dict(mwclient.listing.List.generate_kwargs( + prefix, namespace=namespace, filterredir=filterredir, + )) + if redirect: + kwargs['%sredirect' % prefix] = '1' + kwargs[prefix + 'title'] = self.name + + return mwclient.listing.List.get_list(generator)( + self.site, 'backlinks', 'bl', limit=limit, return_values='title', + **kwargs + ) + + def categories(self, generator=True, show=None): + """List categories used on the current page. + + API doc: https://www.mediawiki.org/wiki/API:Categories + + Args: + generator (bool): Return generator (Default: True) + show (str): Set to 'hidden' to only return hidden categories + or '!hidden' to only return non-hidden ones. + + Returns: + mwclient.listings.PagePropertyGenerator + """ + prefix = mwclient.listing.List.get_prefix('cl', generator) + kwargs = dict(mwclient.listing.List.generate_kwargs( + prefix, show=show + )) + + if generator: + return mwclient.listing.PagePropertyGenerator( + self, 'categories', 'cl', **kwargs + ) + else: + # TODO: return sortkey if wanted + return mwclient.listing.PageProperty( + self, 'categories', 'cl', return_values='title', **kwargs + ) + + def embeddedin(self, namespace=None, filterredir='all', limit=None, generator=True): + """List pages that transclude the current page. + + API doc: https://www.mediawiki.org/wiki/API:Embeddedin + + Args: + namespace (int): Restricts search to a given namespace (Default: None) + filterredir (str): How to filter redirects, either 'all' (default), + 'redirects' or 'nonredirects'. + limit (int): Maximum amount of pages to return per request + generator (bool): Return generator (Default: True) + + Returns: + mwclient.listings.List: Page iterator + """ + prefix = mwclient.listing.List.get_prefix('ei', generator) + kwargs = dict(mwclient.listing.List.generate_kwargs(prefix, namespace=namespace, + filterredir=filterredir)) + kwargs[prefix + 'title'] = self.name + + return mwclient.listing.List.get_list(generator)( + self.site, 'embeddedin', 'ei', limit=limit, return_values='title', + **kwargs + ) + + def extlinks(self): + """List external links from the current page. + + API doc: https://www.mediawiki.org/wiki/API:Extlinks + + """ + return mwclient.listing.PageProperty(self, 'extlinks', 'el', return_values='*') + + def images(self, generator=True): + """List files/images embedded in the current page. + + API doc: https://www.mediawiki.org/wiki/API:Images + + """ + if generator: + return mwclient.listing.PagePropertyGenerator(self, 'images', '') + else: + return mwclient.listing.PageProperty(self, 'images', '', + return_values='title') + + def iwlinks(self): + """List interwiki links from the current page. + + API doc: https://www.mediawiki.org/wiki/API:Iwlinks + + """ + return mwclient.listing.PageProperty(self, 'iwlinks', 'iw', + return_values=('prefix', '*')) + + def langlinks(self, **kwargs): + """List interlanguage links from the current page. + + API doc: https://www.mediawiki.org/wiki/API:Langlinks + + """ + return mwclient.listing.PageProperty(self, 'langlinks', 'll', + return_values=('lang', '*'), + **kwargs) + + def links(self, namespace=None, generator=True, redirects=False): + """List links to other pages from the current page. + + API doc: https://www.mediawiki.org/wiki/API:Links + + """ + prefix = mwclient.listing.List.get_prefix('pl', generator) + kwargs = dict(mwclient.listing.List.generate_kwargs(prefix, namespace=namespace)) + + if redirects: + kwargs['redirects'] = '1' + if generator: + return mwclient.listing.PagePropertyGenerator(self, 'links', 'pl', **kwargs) + else: + return mwclient.listing.PageProperty(self, 'links', 'pl', + return_values='title', **kwargs) + + def revisions(self, startid=None, endid=None, start=None, end=None, + dir='older', user=None, excludeuser=None, limit=50, + prop='ids|timestamp|flags|comment|user', + expandtemplates=False, section=None, + diffto=None, slots=None, uselang=None): + """List revisions of the current page. + + API doc: https://www.mediawiki.org/wiki/API:Revisions + + Args: + startid (int): Revision ID to start listing from. + endid (int): Revision ID to stop listing at. + start (str): Timestamp to start listing from. + end (str): Timestamp to end listing at. + dir (str): Direction to list in: 'older' (default) or 'newer'. + user (str): Only list revisions made by this user. + excludeuser (str): Exclude revisions made by this user. + limit (int): The maximum number of revisions to return per request. + prop (str): Which properties to get for each revision, + default: 'ids|timestamp|flags|comment|user' + expandtemplates (bool): Expand templates in rvprop=content output + section (int): Section number. If rvprop=content is set, only the contents + of this section will be retrieved. + diffto (str): Revision ID to diff each revision to. Use "prev", "next" and + "cur" for the previous, next and current revision respectively. + slots (str): The content slot (Mediawiki >= 1.32) to retrieve content from. + uselang (str): Language to use for parsed edit comments and other localized + messages. + + Returns: + mwclient.listings.List: Revision iterator + """ + kwargs = dict(mwclient.listing.List.generate_kwargs( + 'rv', startid=startid, endid=endid, start=start, end=end, user=user, + excludeuser=excludeuser, diffto=diffto, slots=slots + )) + + if self.site.version[:2] < (1, 32) and 'rvslots' in kwargs: + # https://github.com/mwclient/mwclient/issues/199 + del kwargs['rvslots'] + + kwargs['rvdir'] = dir + kwargs['rvprop'] = prop + kwargs['uselang'] = uselang + if expandtemplates: + kwargs['rvexpandtemplates'] = '1' + if section is not None: + kwargs['rvsection'] = section + + return mwclient.listing.RevisionsIterator(self, 'revisions', 'rv', limit=limit, + **kwargs) + + def templates(self, namespace=None, generator=True): + """List templates used on the current page. + + API doc: https://www.mediawiki.org/wiki/API:Templates + + """ + prefix = mwclient.listing.List.get_prefix('tl', generator) + kwargs = dict(mwclient.listing.List.generate_kwargs(prefix, namespace=namespace)) + if generator: + return mwclient.listing.PagePropertyGenerator(self, 'templates', prefix, + **kwargs) + else: + return mwclient.listing.PageProperty(self, 'templates', prefix, + return_values='title', **kwargs) |