aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Huang <[email protected]>2017-03-24 00:49:02 +0100
committerAlbert Astals Cid <[email protected]>2017-03-24 00:49:02 +0100
commit34e2801df9c1e478c9f1204ea1bc0361329ed828 (patch)
treeb4e47aecdfc8e1dcd4e91a3eacc206bd726d7137
parent05eb11235d426acdea99637efd2472de730b23cd (diff)
Albert says: bunch of files i forgot to git add, sorry ^_^
-rw-r--r--plugins/wiktionary/mwclient/LICENSE.md22
-rw-r--r--plugins/wiktionary/mwclient/README.rst142
-rw-r--r--plugins/wiktionary/mwclient/image.py66
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/LICENSE15
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/README.rst60
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/__init__.py22
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/__init__.py8
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/douban.py18
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/facebook.py33
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/fitbit.py26
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/linkedin.py24
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/mailchimp.py22
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/slack.py37
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/weibo.py17
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/oauth1_auth.py95
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/oauth1_session.py378
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/oauth2_auth.py36
-rw-r--r--plugins/wiktionary/mwclient/requests_oauthlib/oauth2_session.py376
-rw-r--r--plugins/wiktionary/mwclient/six.LICENSE18
-rw-r--r--plugins/wiktionary/mwclient/six.README18
-rw-r--r--plugins/wiktionary/mwclient/six.py868
-rw-r--r--plugins/wiktionary/mwclient/sleep.py49
-rw-r--r--plugins/wiktionary/mwclient/util.py7
23 files changed, 2357 insertions, 0 deletions
diff --git a/plugins/wiktionary/mwclient/LICENSE.md b/plugins/wiktionary/mwclient/LICENSE.md
new file mode 100644
index 0000000..9359f20
--- /dev/null
+++ b/plugins/wiktionary/mwclient/LICENSE.md
@@ -0,0 +1,22 @@
+Copyright (c) 2006-2013 Bryan Tong Minh
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/plugins/wiktionary/mwclient/README.rst b/plugins/wiktionary/mwclient/README.rst
new file mode 100644
index 0000000..39b0d3f
--- /dev/null
+++ b/plugins/wiktionary/mwclient/README.rst
@@ -0,0 +1,142 @@
+
+.. figure:: docs/source/logo.png
+ :alt: Logo
+ :align: center
+
+mwclient
+========
+
+.. image:: https://img.shields.io/travis/mwclient/mwclient.svg
+ :target: https://travis-ci.org/mwclient/mwclient
+ :alt: Build status
+
+.. image:: https://img.shields.io/coveralls/mwclient/mwclient.svg
+ :target: https://coveralls.io/r/mwclient/mwclient
+ :alt: Test coverage
+
+.. image:: https://landscape.io/github/mwclient/mwclient/master/landscape.svg?style=flat
+ :target: https://landscape.io/github/mwclient/mwclient/master
+ :alt: Code health
+
+.. image:: https://img.shields.io/pypi/v/mwclient.svg
+ :target: https://pypi.python.org/pypi/mwclient
+ :alt: Latest version
+
+.. image:: https://img.shields.io/pypi/dw/mwclient.svg
+ :target: https://pypi.python.org/pypi/mwclient
+ :alt: Downloads
+
+.. image:: https://img.shields.io/github/license/mwclient/mwclient.svg
+ :target: http://opensource.org/licenses/MIT
+ :alt: MIT license
+
+.. image:: https://readthedocs.org/projects/mwclient/badge/?version=master
+ :target: http://mwclient.readthedocs.io/en/latest/
+ :alt: Documentation status
+
+.. image:: http://isitmaintained.com/badge/resolution/tldr-pages/tldr.svg
+ :target: http://isitmaintained.com/project/tldr-pages/tldr
+ :alt: Issue statistics
+
+mwclient is a lightweight Python client library to the `MediaWiki API <https://mediawiki.org/wiki/API>`_
+which provides access to most API functionality.
+It works with Python 2.7, 3.3 and above, and supports MediaWiki 1.16 and above.
+For functions not available in the current MediaWiki, a ``MediaWikiVersionError`` is raised.
+
+The current stable `version 0.8.4 <https://github.com/mwclient/mwclient/archive/v0.8.4.zip>`_
+was released on 23 February 2017, and is `available through PyPI <https://pypi.python.org/pypi/mwclient>`_:
+
+.. code-block:: console
+
+ $ pip install mwclient
+
+The current `development version <https://github.com/mwclient/mwclient>`_
+can be installed from GitHub:
+
+.. code-block:: console
+
+ $ pip install git+git://github.com/mwclient/mwclient.git
+
+Please see the
+`changelog document <https://github.com/mwclient/mwclient/blob/master/CHANGELOG.md>`_
+for a list of changes.
+
+Getting started
+---------------
+
+See the `user guide <http://mwclient.readthedocs.io/en/latest/user/index.html>`_
+to get started using mwclient.
+
+For more information, see the
+`REFERENCE.md <https://github.com/mwclient/mwclient/blob/master/REFERENCE.md>`_ file
+and the `documentation on the wiki <https://github.com/mwclient/mwclient/wiki>`_.
+
+
+Contributing
+--------------------
+
+mwclient ships with a test suite based on `pytest <https://pytest.org>`_.
+Only a small part of mwclient is currently tested,
+but hopefully coverage will improve in the future.
+
+The easiest way to run tests is:
+
+.. code-block:: console
+
+ $ python setup.py test
+
+This will make an in-place build and download test dependencies locally
+if needed. To make tests run faster, you can use pip to do an
+`"editable" install <https://pip.readthedocs.org/en/latest/reference/pip_install.html#editable-installs>`_:
+
+.. code-block:: console
+
+ $ pip install pytest pytest-pep8 responses
+ $ pip install -e .
+ $ py.test
+
+To run tests with different Python versions in isolated virtualenvs, you
+can use `Tox <https://testrun.org/tox/latest/>`_:
+
+.. code-block:: console
+
+ $ pip install tox
+ $ tox
+
+*Documentation* consists of both a manually compiled user guide (under ``docs/user``)
+and a reference guide generated from the docstrings,
+using Sphinx autodoc with the napoleon extension.
+Documentation is built automatically on `ReadTheDocs`_ after each commit.
+To build documentation locally for testing, do:
+
+.. code-block:: console
+
+ $ cd docs
+ $ make html
+
+When writing docstrings, try to adhere to the `Google style`_.
+
+.. _Google style: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html
+.. _ReadTheDocs: https://mwclient.readthedocs.io/
+
+Implementation notes
+--------------------
+
+Most properties and generators accept the same parameters as the API,
+without their two-letter prefix. Exceptions to this rule:
+
+* ``Image.imageinfo`` is the imageinfo of the latest image.
+ Earlier versions can be fetched using ``imagehistory()``
+* ``Site.all*``: parameter ``[ap]from`` renamed to ``start``
+* ``categorymembers`` is implemented as ``Category.members``
+* ``deletedrevs`` is ``deletedrevisions``
+* ``usercontribs`` is ``usercontributions``
+* First parameters of ``search`` and ``usercontributions`` are ``search`` and ``user``
+ respectively
+
+Properties and generators are implemented as Python generators.
+Their limit parameter is only an indication of the number of items in one chunk.
+It is not the total limit.
+Doing ``list(generator(limit = limit))`` will return ALL items of generator,
+and not be limited by the limit value.
+Default chunk size is generally the maximum chunk size.
diff --git a/plugins/wiktionary/mwclient/image.py b/plugins/wiktionary/mwclient/image.py
new file mode 100644
index 0000000..828ff4c
--- /dev/null
+++ b/plugins/wiktionary/mwclient/image.py
@@ -0,0 +1,66 @@
+import mwclient.listing
+import mwclient.page
+
+
+class Image(mwclient.page.Page):
+
+ def __init__(self, site, name, info=None):
+ super(Image, self).__init__(site, name, info,
+ extra_properties={'imageinfo': (('iiprop', 'timestamp|user|comment|url|size|sha1|metadata|archivename'), )})
+ self.imagerepository = self._info.get('imagerepository', '')
+ self.imageinfo = self._info.get('imageinfo', ({}, ))[0]
+
+ def imagehistory(self):
+ """
+ Get file revision info for the given file.
+
+ API doc: https://www.mediawiki.org/wiki/API:Imageinfo
+ """
+ return mwclient.listing.PageProperty(self, 'imageinfo', 'ii',
+ iiprop='timestamp|user|comment|url|size|sha1|metadata|archivename')
+
+ def imageusage(self, namespace=None, filterredir='all', redirect=False,
+ limit=None, generator=True):
+ """
+ List pages that use the given file.
+
+ API doc: https://www.mediawiki.org/wiki/API:Imageusage
+ """
+ prefix = mwclient.listing.List.get_prefix('iu', generator)
+ kwargs = dict(mwclient.listing.List.generate_kwargs(prefix, title=self.name, namespace=namespace, filterredir=filterredir))
+ if redirect:
+ kwargs['%sredirect' % prefix] = '1'
+ return mwclient.listing.List.get_list(generator)(self.site, 'imageusage', 'iu', limit=limit, return_values='title', **kwargs)
+
+ def duplicatefiles(self, limit=None):
+ """
+ List duplicates of the current file.
+
+ API doc: https://www.mediawiki.org/wiki/API:Duplicatefiles
+ """
+ return mwclient.listing.PageProperty(self, 'duplicatefiles', 'df', dflimit=limit)
+
+ def download(self, destination=None):
+ """
+ Download the file. If `destination` is given, the file will be written
+ directly to the stream. Otherwise the file content will be stored in memory
+ and returned (with the risk of running out of memory for large files).
+
+ Recommended usage:
+
+ >>> with open(filename, 'wb') as fd:
+ ... image.download(fd)
+
+ Args:
+ destination (file object): Destination file
+ """
+ url = self.imageinfo['url']
+ if destination is not None:
+ res = self.site.connection.get(url, stream=True)
+ for chunk in res.iter_content(1024):
+ destination.write(chunk)
+ else:
+ return self.site.connection.get(url).content
+
+ def __repr__(self):
+ return "<Image object '%s' for %s>" % (self.name.encode('utf-8'), self.site)
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/LICENSE b/plugins/wiktionary/mwclient/requests_oauthlib/LICENSE
new file mode 100644
index 0000000..de09f40
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2014 Kenneth Reitz.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/README.rst b/plugins/wiktionary/mwclient/requests_oauthlib/README.rst
new file mode 100644
index 0000000..4c718f9
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/README.rst
@@ -0,0 +1,60 @@
+Requests-OAuthlib |build-status| |coverage-status| |docs|
+=========================================================
+
+This project provides first-class OAuth library support for `Requests <http://python-requests.org>`_.
+
+The OAuth 1 workflow
+--------------------
+
+OAuth 1 can seem overly complicated and it sure has its quirks. Luckily,
+requests_oauthlib hides most of these and let you focus at the task at hand.
+
+Accessing protected resources using requests_oauthlib is as simple as:
+
+.. code-block:: pycon
+
+ >>> from requests_oauthlib import OAuth1Session
+ >>> twitter = OAuth1Session('client_key',
+ client_secret='client_secret',
+ resource_owner_key='resource_owner_key',
+ resource_owner_secret='resource_owner_secret')
+ >>> url = 'https://api.twitter.com/1/account/settings.json'
+ >>> r = twitter.get(url)
+
+Before accessing resources you will need to obtain a few credentials from your
+provider (i.e. Twitter) and authorization from the user for whom you wish to
+retrieve resources for. You can read all about this in the full
+`OAuth 1 workflow guide on RTD <http://requests-oauthlib.readthedocs.org/en/latest/oauth1_workflow.html>`_.
+
+The OAuth 2 workflow
+--------------------
+
+OAuth 2 is generally simpler than OAuth 1 but comes in more flavours. The most
+common being the Authorization Code Grant, also known as the WebApplication
+flow.
+
+Fetching a protected resource after obtaining an access token can be extremely
+simple. However, before accessing resources you will need to obtain a few
+credentials from your provider (i.e. Google) and authorization from the user
+for whom you wish to retrieve resources for. You can read all about this in the
+full `OAuth 2 workflow guide on RTD <http://requests-oauthlib.readthedocs.org/en/latest/oauth2_workflow.html>`_.
+
+Installation
+-------------
+
+To install requests and requests_oauthlib you can use pip:
+
+.. code-block:: bash
+
+ $ pip install requests requests_oauthlib
+
+.. |build-status| image:: https://travis-ci.org/requests/requests-oauthlib.svg?branch=master
+ :target: https://travis-ci.org/requests/requests-oauthlib
+.. |coverage-status| image:: https://img.shields.io/coveralls/requests/requests-oauthlib.svg
+ :target: https://coveralls.io/r/requests/requests-oauthlib
+.. |docs| image:: https://readthedocs.org/projects/requests-oauthlib/badge/?version=latest
+ :alt: Documentation Status
+ :scale: 100%
+ :target: https://readthedocs.org/projects/requests-oauthlib/
+
+Version: 0.8.0
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/__init__.py b/plugins/wiktionary/mwclient/requests_oauthlib/__init__.py
new file mode 100644
index 0000000..56560ba
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/__init__.py
@@ -0,0 +1,22 @@
+from .oauth1_auth import OAuth1
+from .oauth1_session import OAuth1Session
+from .oauth2_auth import OAuth2
+from .oauth2_session import OAuth2Session, TokenUpdated
+
+__version__ = '0.8.0'
+
+import requests
+if requests.__version__ < '2.0.0':
+ msg = ('You are using requests version %s, which is older than '
+ 'requests-oauthlib expects, please upgrade to 2.0.0 or later.')
+ raise Warning(msg % requests.__version__)
+
+import logging
+try: # Python 2.7+
+ from logging import NullHandler
+except ImportError:
+ class NullHandler(logging.Handler):
+ def emit(self, record):
+ pass
+
+logging.getLogger('requests_oauthlib').addHandler(NullHandler())
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/__init__.py b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/__init__.py
new file mode 100644
index 0000000..65e49c1
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/__init__.py
@@ -0,0 +1,8 @@
+from __future__ import absolute_import
+
+from .facebook import facebook_compliance_fix
+from .fitbit import fitbit_compliance_fix
+from .linkedin import linkedin_compliance_fix
+from .slack import slack_compliance_fix
+from .mailchimp import mailchimp_compliance_fix
+from .weibo import weibo_compliance_fix
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/douban.py b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/douban.py
new file mode 100644
index 0000000..2e45b3b
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/douban.py
@@ -0,0 +1,18 @@
+import json
+
+from oauthlib.common import to_unicode
+
+
+def douban_compliance_fix(session):
+
+ def fix_token_type(r):
+ token = json.loads(r.text)
+ token.setdefault('token_type', 'Bearer')
+ fixed_token = json.dumps(token)
+ r._content = to_unicode(fixed_token).encode('utf-8')
+ return r
+
+ session._client_default_token_placement = 'query'
+ session.register_compliance_hook('access_token_response', fix_token_type)
+
+ return session
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/facebook.py b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/facebook.py
new file mode 100644
index 0000000..07181c3
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/facebook.py
@@ -0,0 +1,33 @@
+from json import dumps
+try:
+ from urlparse import parse_qsl
+except ImportError:
+ from urllib.parse import parse_qsl
+
+from oauthlib.common import to_unicode
+
+
+def facebook_compliance_fix(session):
+
+ def _compliance_fix(r):
+ # if Facebook claims to be sending us json, let's trust them.
+ if 'application/json' in r.headers.get('content-type', {}):
+ return r
+
+ # Facebook returns a content-type of text/plain when sending their
+ # x-www-form-urlencoded responses, along with a 200. If not, let's
+ # assume we're getting JSON and bail on the fix.
+ if 'text/plain' in r.headers.get('content-type', {}) and r.status_code == 200:
+ token = dict(parse_qsl(r.text, keep_blank_values=True))
+ else:
+ return r
+
+ expires = token.get('expires')
+ if expires is not None:
+ token['expires_in'] = expires
+ token['token_type'] = 'Bearer'
+ r._content = to_unicode(dumps(token)).encode('UTF-8')
+ return r
+
+ session.register_compliance_hook('access_token_response', _compliance_fix)
+ return session
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/fitbit.py b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/fitbit.py
new file mode 100644
index 0000000..cec533b
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/fitbit.py
@@ -0,0 +1,26 @@
+"""
+The Fitbit API breaks from the OAuth2 RFC standard by returning an "errors"
+object list, rather than a single "error" string. This puts hooks in place so
+that oauthlib can process an error in the results from access token and refresh
+token responses. This is necessary to prevent getting the generic red herring
+MissingTokenError.
+"""
+
+from json import loads, dumps
+
+from oauthlib.common import to_unicode
+
+
+def fitbit_compliance_fix(session):
+
+ def _missing_error(r):
+ token = loads(r.text)
+ if 'errors' in token:
+ # Set the error to the first one we have
+ token['error'] = token['errors'][0]['errorType']
+ r._content = to_unicode(dumps(token)).encode('UTF-8')
+ return r
+
+ session.register_compliance_hook('access_token_response', _missing_error)
+ session.register_compliance_hook('refresh_token_response', _missing_error)
+ return session
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/linkedin.py b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/linkedin.py
new file mode 100644
index 0000000..e697ced
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/linkedin.py
@@ -0,0 +1,24 @@
+from json import loads, dumps
+
+from oauthlib.common import add_params_to_uri, to_unicode
+
+
+def linkedin_compliance_fix(session):
+
+ def _missing_token_type(r):
+ token = loads(r.text)
+ token['token_type'] = 'Bearer'
+ r._content = to_unicode(dumps(token)).encode('UTF-8')
+ return r
+
+ def _non_compliant_param_name(url, headers, data):
+ token = [('oauth2_access_token', session.access_token)]
+ url = add_params_to_uri(url, token)
+ return url, headers, data
+
+ session._client.default_token_placement = 'query'
+ session.register_compliance_hook('access_token_response',
+ _missing_token_type)
+ session.register_compliance_hook('protected_request',
+ _non_compliant_param_name)
+ return session
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/mailchimp.py b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/mailchimp.py
new file mode 100644
index 0000000..ee9bc94
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/mailchimp.py
@@ -0,0 +1,22 @@
+import json
+
+from oauthlib.common import to_unicode
+
+def mailchimp_compliance_fix(session):
+ def _null_scope(r):
+ token = json.loads(r.text)
+ if 'scope' in token and token['scope'] is None:
+ token.pop('scope')
+ r._content = to_unicode(json.dumps(token)).encode('utf-8')
+ return r
+
+ def _non_zero_expiration(r):
+ token = json.loads(r.text)
+ if 'expires_in' in token and token['expires_in'] == 0:
+ token['expires_in'] = 3600
+ r._content = to_unicode(json.dumps(token)).encode('utf-8')
+ return r
+
+ session.register_compliance_hook('access_token_response', _null_scope)
+ session.register_compliance_hook('access_token_response', _non_zero_expiration)
+ return session
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/slack.py b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/slack.py
new file mode 100644
index 0000000..ab2d938
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/slack.py
@@ -0,0 +1,37 @@
+try:
+ from urlparse import urlparse, parse_qs
+except ImportError:
+ from urllib.parse import urlparse, parse_qs
+
+from oauthlib.common import add_params_to_uri
+
+
+def slack_compliance_fix(session):
+ def _non_compliant_param_name(url, headers, data):
+ # If the user has already specified the token, either in the URL
+ # or in a data dictionary, then there's nothing to do.
+ # If the specified token is different from ``session.access_token``,
+ # we assume the user intends to override the access token.
+ url_query = dict(parse_qs(urlparse(url).query))
+ token = url_query.get("token")
+ if not token and isinstance(data, dict):
+ token = data.get("token")
+
+ if token:
+ # Nothing to do, just return.
+ return url, headers, data
+
+ if not data:
+ data = {"token": session.access_token}
+ elif isinstance(data, dict):
+ data["token"] = session.access_token
+ else:
+ # ``data`` is something other than a dict: maybe a stream,
+ # maybe a file object, maybe something else. We can't easily
+ # modify it, so we'll set the token by modifying the URL instead.
+ token = [('token', session.access_token)]
+ url = add_params_to_uri(url, token)
+ return url, headers, data
+
+ session.register_compliance_hook('protected_request', _non_compliant_param_name)
+ return session
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/weibo.py b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/weibo.py
new file mode 100644
index 0000000..28aca32
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/compliance_fixes/weibo.py
@@ -0,0 +1,17 @@
+from json import loads, dumps
+
+from oauthlib.common import to_unicode
+
+
+def weibo_compliance_fix(session):
+
+ def _missing_token_type(r):
+ token = loads(r.text)
+ token['token_type'] = 'Bearer'
+ r._content = to_unicode(dumps(token)).encode('UTF-8')
+ return r
+
+ session._client.default_token_placement = 'query'
+ session.register_compliance_hook('access_token_response',
+ _missing_token_type)
+ return session
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/oauth1_auth.py b/plugins/wiktionary/mwclient/requests_oauthlib/oauth1_auth.py
new file mode 100644
index 0000000..4626384
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/oauth1_auth.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import logging
+
+from oauthlib.common import extract_params
+from oauthlib.oauth1 import Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER
+from oauthlib.oauth1 import SIGNATURE_TYPE_BODY
+from requests.compat import is_py3
+from requests.utils import to_native_string
+from requests.auth import AuthBase
+
+CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
+CONTENT_TYPE_MULTI_PART = 'multipart/form-data'
+
+if is_py3:
+ unicode = str
+
+log = logging.getLogger(__name__)
+
+# OBS!: Correct signing of requests are conditional on invoking OAuth1
+# as the last step of preparing a request, or at least having the
+# content-type set properly.
+class OAuth1(AuthBase):
+ """Signs the request using OAuth 1 (RFC5849)"""
+
+ client_class = Client
+
+ def __init__(self, client_key,
+ client_secret=None,
+ resource_owner_key=None,
+ resource_owner_secret=None,
+ callback_uri=None,
+ signature_method=SIGNATURE_HMAC,
+ signature_type=SIGNATURE_TYPE_AUTH_HEADER,
+ rsa_key=None, verifier=None,
+ decoding='utf-8',
+ client_class=None,
+ force_include_body=False,
+ **kwargs):
+
+ try:
+ signature_type = signature_type.upper()
+ except AttributeError:
+ pass
+
+ client_class = client_class or self.client_class
+
+ self.force_include_body = force_include_body
+
+ self.client = client_class(client_key, client_secret, resource_owner_key,
+ resource_owner_secret, callback_uri, signature_method,
+ signature_type, rsa_key, verifier, decoding=decoding, **kwargs)
+
+ def __call__(self, r):
+ """Add OAuth parameters to the request.
+
+ Parameters may be included from the body if the content-type is
+ urlencoded, if no content type is set a guess is made.
+ """
+ # Overwriting url is safe here as request will not modify it past
+ # this point.
+ log.debug('Signing request %s using client %s', r, self.client)
+
+ content_type = r.headers.get('Content-Type', '')
+ if (not content_type and extract_params(r.body)
+ or self.client.signature_type == SIGNATURE_TYPE_BODY):
+ content_type = CONTENT_TYPE_FORM_URLENCODED
+ if not isinstance(content_type, unicode):
+ content_type = content_type.decode('utf-8')
+
+ is_form_encoded = (CONTENT_TYPE_FORM_URLENCODED in content_type)
+
+ log.debug('Including body in call to sign: %s',
+ is_form_encoded or self.force_include_body)
+
+ if is_form_encoded:
+ r.headers['Content-Type'] = CONTENT_TYPE_FORM_URLENCODED
+ r.url, headers, r.body = self.client.sign(
+ unicode(r.url), unicode(r.method), r.body or '', r.headers)
+ elif self.force_include_body:
+ # To allow custom clients to work on non form encoded bodies.
+ r.url, headers, r.body = self.client.sign(
+ unicode(r.url), unicode(r.method), r.body or '', r.headers)
+ else:
+ # Omit body data in the signing of non form-encoded requests
+ r.url, headers, _ = self.client.sign(
+ unicode(r.url), unicode(r.method), None, r.headers)
+
+ r.prepare_headers(headers)
+ r.url = to_native_string(r.url)
+ log.debug('Updated url: %s', r.url)
+ log.debug('Updated headers: %s', headers)
+ log.debug('Updated body: %r', r.body)
+ return r
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/oauth1_session.py b/plugins/wiktionary/mwclient/requests_oauthlib/oauth1_session.py
new file mode 100644
index 0000000..61e4c41
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/oauth1_session.py
@@ -0,0 +1,378 @@
+from __future__ import unicode_literals
+
+try:
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import urlparse
+
+import logging
+
+from oauthlib.common import add_params_to_uri
+from oauthlib.common import urldecode as _urldecode
+from oauthlib.oauth1 import (
+ SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_TYPE_AUTH_HEADER
+)
+import requests
+
+from . import OAuth1
+
+import sys
+if sys.version > "3":
+ unicode = str
+
+
+log = logging.getLogger(__name__)
+
+
+def urldecode(body):
+ """Parse query or json to python dictionary"""
+ try:
+ return _urldecode(body)
+ except:
+ import json
+ return json.loads(body)
+
+
+class TokenRequestDenied(ValueError):
+
+ def __init__(self, message, response):
+ super(TokenRequestDenied, self).__init__(message)
+ self.response = response
+
+ @property
+ def status_code(self):
+ """For backwards-compatibility purposes"""
+ return self.response.status_code
+
+
+class TokenMissing(ValueError):
+ def __init__(self, message, response):
+ super(TokenMissing, self).__init__(message)
+ self.response = response
+
+
+class VerifierMissing(ValueError):
+ pass
+
+
+class OAuth1Session(requests.Session):
+ """Request signing and convenience methods for the oauth dance.
+
+ What is the difference between OAuth1Session and OAuth1?
+
+ OAuth1Session actually uses OAuth1 internally and its purpose is to assist
+ in the OAuth workflow through convenience methods to prepare authorization
+ URLs and parse the various token and redirection responses. It also provide
+ rudimentary validation of responses.
+
+ An example of the OAuth workflow using a basic CLI app and Twitter.
+
+ >>> # Credentials obtained during the registration.
+ >>> client_key = 'client key'
+ >>> client_secret = 'secret'
+ >>> callback_uri = 'https://127.0.0.1/callback'
+ >>>
+ >>> # Endpoints found in the OAuth provider API documentation
+ >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
+ >>> authorization_url = 'https://api.twitter.com/oauth/authorize'
+ >>> access_token_url = 'https://api.twitter.com/oauth/access_token'
+ >>>
+ >>> oauth_session = OAuth1Session(client_key,client_secret=client_secret, callback_uri=callback_uri)
+ >>>
+ >>> # First step, fetch the request token.
+ >>> oauth_session.fetch_request_token(request_token_url)
+ {
+ 'oauth_token': 'kjerht2309u',
+ 'oauth_token_secret': 'lsdajfh923874',
+ }
+ >>>
+ >>> # Second step. Follow this link and authorize
+ >>> oauth_session.authorization_url(authorization_url)
+ 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&oauth_callback=https%3A%2F%2F127.0.0.1%2Fcallback'
+ >>>
+ >>> # Third step. Fetch the access token
+ >>> redirect_response = raw_input('Paste the full redirect URL here.')
+ >>> oauth_session.parse_authorization_response(redirect_response)
+ {
+ 'oauth_token: 'kjerht2309u',
+ 'oauth_token_secret: 'lsdajfh923874',
+ 'oauth_verifier: 'w34o8967345',
+ }
+ >>> oauth_session.fetch_access_token(access_token_url)
+ {
+ 'oauth_token': 'sdf0o9823sjdfsdf',
+ 'oauth_token_secret': '2kjshdfp92i34asdasd',
+ }
+ >>> # Done. You can now make OAuth requests.
+ >>> status_url = 'http://api.twitter.com/1/statuses/update.json'
+ >>> new_status = {'status': 'hello world!'}
+ >>> oauth_session.post(status_url, data=new_status)
+ <Response [200]>
+ """
+
+ def __init__(self, client_key,
+ client_secret=None,
+ resource_owner_key=None,
+ resource_owner_secret=None,
+ callback_uri=None,
+ signature_method=SIGNATURE_HMAC,
+ signature_type=SIGNATURE_TYPE_AUTH_HEADER,
+ rsa_key=None,
+ verifier=None,
+ client_class=None,
+ force_include_body=False,
+ **kwargs):
+ """Construct the OAuth 1 session.
+
+ :param client_key: A client specific identifier.
+ :param client_secret: A client specific secret used to create HMAC and
+ plaintext signatures.
+ :param resource_owner_key: A resource owner key, also referred to as
+ request token or access token depending on
+ when in the workflow it is used.
+ :param resource_owner_secret: A resource owner secret obtained with
+ either a request or access token. Often
+ referred to as token secret.
+ :param callback_uri: The URL the user is redirect back to after
+ authorization.
+ :param signature_method: Signature methods determine how the OAuth
+ signature is created. The three options are
+ oauthlib.oauth1.SIGNATURE_HMAC (default),
+ oauthlib.oauth1.SIGNATURE_RSA and
+ oauthlib.oauth1.SIGNATURE_PLAIN.
+ :param signature_type: Signature type decides where the OAuth
+ parameters are added. Either in the
+ Authorization header (default) or to the URL
+ query parameters or the request body. Defined as
+ oauthlib.oauth1.SIGNATURE_TYPE_AUTH_HEADER,
+ oauthlib.oauth1.SIGNATURE_TYPE_QUERY and
+ oauthlib.oauth1.SIGNATURE_TYPE_BODY
+ respectively.
+ :param rsa_key: The private RSA key as a string. Can only be used with
+ signature_method=oauthlib.oauth1.SIGNATURE_RSA.
+ :param verifier: A verifier string to prove authorization was granted.
+ :param client_class: A subclass of `oauthlib.oauth1.Client` to use with
+ `requests_oauthlib.OAuth1` instead of the default
+ :param force_include_body: Always include the request body in the
+ signature creation.
+ :param **kwargs: Additional keyword arguments passed to `OAuth1`
+ """
+ super(OAuth1Session, self).__init__()
+ self._client = OAuth1(client_key,
+ client_secret=client_secret,
+ resource_owner_key=resource_owner_key,
+ resource_owner_secret=resource_owner_secret,
+ callback_uri=callback_uri,
+ signature_method=signature_method,
+ signature_type=signature_type,
+ rsa_key=rsa_key,
+ verifier=verifier,
+ client_class=client_class,
+ force_include_body=force_include_body,
+ **kwargs)
+ self.auth = self._client
+
+ @property
+ def authorized(self):
+ """Boolean that indicates whether this session has an OAuth token
+ or not. If `self.authorized` is True, you can reasonably expect
+ OAuth-protected requests to the resource to succeed. If
+ `self.authorized` is False, you need the user to go through the OAuth
+ authentication dance before OAuth-protected requests to the resource
+ will succeed.
+ """
+ if self._client.client.signature_method == SIGNATURE_RSA:
+ # RSA only uses resource_owner_key
+ return bool(self._client.client.resource_owner_key)
+ else:
+ # other methods of authentication use all three pieces
+ return (
+ bool(self._client.client.client_secret) and
+ bool(self._client.client.resource_owner_key) and
+ bool(self._client.client.resource_owner_secret)
+ )
+
+ def authorization_url(self, url, request_token=None, **kwargs):
+ """Create an authorization URL by appending request_token and optional
+ kwargs to url.
+
+ This is the second step in the OAuth 1 workflow. The user should be
+ redirected to this authorization URL, grant access to you, and then
+ be redirected back to you. The redirection back can either be specified
+ during client registration or by supplying a callback URI per request.
+
+ :param url: The authorization endpoint URL.
+ :param request_token: The previously obtained request token.
+ :param kwargs: Optional parameters to append to the URL.
+ :returns: The authorization URL with new parameters embedded.
+
+ An example using a registered default callback URI.
+
+ >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
+ >>> authorization_url = 'https://api.twitter.com/oauth/authorize'
+ >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
+ >>> oauth_session.fetch_request_token(request_token_url)
+ {
+ 'oauth_token': 'sdf0o9823sjdfsdf',
+ 'oauth_token_secret': '2kjshdfp92i34asdasd',
+ }
+ >>> oauth_session.authorization_url(authorization_url)
+ 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf'
+ >>> oauth_session.authorization_url(authorization_url, foo='bar')
+ 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&foo=bar'
+
+ An example using an explicit callback URI.
+
+ >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
+ >>> authorization_url = 'https://api.twitter.com/oauth/authorize'
+ >>> oauth_session = OAuth1Session('client-key', client_secret='secret', callback_uri='https://127.0.0.1/callback')
+ >>> oauth_session.fetch_request_token(request_token_url)
+ {
+ 'oauth_token': 'sdf0o9823sjdfsdf',
+ 'oauth_token_secret': '2kjshdfp92i34asdasd',
+ }
+ >>> oauth_session.authorization_url(authorization_url)
+ 'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&oauth_callback=https%3A%2F%2F127.0.0.1%2Fcallback'
+ """
+ kwargs['oauth_token'] = request_token or self._client.client.resource_owner_key
+ log.debug('Adding parameters %s to url %s', kwargs, url)
+ return add_params_to_uri(url, kwargs.items())
+
+ def fetch_request_token(self, url, realm=None, **request_kwargs):
+ """Fetch a request token.
+
+ This is the first step in the OAuth 1 workflow. A request token is
+ obtained by making a signed post request to url. The token is then
+ parsed from the application/x-www-form-urlencoded response and ready
+ to be used to construct an authorization url.
+
+ :param url: The request token endpoint URL.
+ :param realm: A list of realms to request access to.
+ :param \*\*request_kwargs: Optional arguments passed to ''post''
+ function in ''requests.Session''
+ :returns: The response in dict format.
+
+ Note that a previously set callback_uri will be reset for your
+ convenience, or else signature creation will be incorrect on
+ consecutive requests.
+
+ >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
+ >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
+ >>> oauth_session.fetch_request_token(request_token_url)
+ {
+ 'oauth_token': 'sdf0o9823sjdfsdf',
+ 'oauth_token_secret': '2kjshdfp92i34asdasd',
+ }
+ """
+ self._client.client.realm = ' '.join(realm) if realm else None
+ token = self._fetch_token(url, **request_kwargs)
+ log.debug('Resetting callback_uri and realm (not needed in next phase).')
+ self._client.client.callback_uri = None
+ self._client.client.realm = None
+ return token
+
+ def fetch_access_token(self, url, verifier=None, **request_kwargs):
+ """Fetch an access token.
+
+ This is the final step in the OAuth 1 workflow. An access token is
+ obtained using all previously obtained credentials, including the
+ verifier from the authorization step.
+
+ Note that a previously set verifier will be reset for your
+ convenience, or else signature creation will be incorrect on
+ consecutive requests.
+
+ >>> access_token_url = 'https://api.twitter.com/oauth/access_token'
+ >>> redirect_response = 'https://127.0.0.1/callback?oauth_token=kjerht2309uf&oauth_token_secret=lsdajfh923874&oauth_verifier=w34o8967345'
+ >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
+ >>> oauth_session.parse_authorization_response(redirect_response)
+ {
+ 'oauth_token: 'kjerht2309u',
+ 'oauth_token_secret: 'lsdajfh923874',
+ 'oauth_verifier: 'w34o8967345',
+ }
+ >>> oauth_session.fetch_access_token(access_token_url)
+ {
+ 'oauth_token': 'sdf0o9823sjdfsdf',
+ 'oauth_token_secret': '2kjshdfp92i34asdasd',
+ }
+ """
+ if verifier:
+ self._client.client.verifier = verifier
+ if not getattr(self._client.client, 'verifier', None):
+ raise VerifierMissing('No client verifier has been set.')
+ token = self._fetch_token(url, **request_kwargs)
+ log.debug('Resetting verifier attribute, should not be used anymore.')
+ self._client.client.verifier = None
+ return token
+
+ def parse_authorization_response(self, url):
+ """Extract parameters from the post authorization redirect response URL.
+
+ :param url: The full URL that resulted from the user being redirected
+ back from the OAuth provider to you, the client.
+ :returns: A dict of parameters extracted from the URL.
+
+ >>> redirect_response = 'https://127.0.0.1/callback?oauth_token=kjerht2309uf&oauth_token_secret=lsdajfh923874&oauth_verifier=w34o8967345'
+ >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
+ >>> oauth_session.parse_authorization_response(redirect_response)
+ {
+ 'oauth_token: 'kjerht2309u',
+ 'oauth_token_secret: 'lsdajfh923874',
+ 'oauth_verifier: 'w34o8967345',
+ }
+ """
+ log.debug('Parsing token from query part of url %s', url)
+ token = dict(urldecode(urlparse(url).query))
+ log.debug('Updating internal client token attribute.')
+ self._populate_attributes(token)
+ return token
+
+ def _populate_attributes(self, token):
+ if 'oauth_token' in token:
+ self._client.client.resource_owner_key = token['oauth_token']
+ else:
+ raise TokenMissing(
+ 'Response does not contain a token: {resp}'.format(resp=token),
+ token,
+ )
+ if 'oauth_token_secret' in token:
+ self._client.client.resource_owner_secret = (
+ token['oauth_token_secret'])
+ if 'oauth_verifier' in token:
+ self._client.client.verifier = token['oauth_verifier']
+
+ def _fetch_token(self, url, **request_kwargs):
+ log.debug('Fetching token from %s using client %s', url, self._client.client)
+ r = self.post(url, **request_kwargs)
+
+ if r.status_code >= 400:
+ error = "Token request failed with code %s, response was '%s'."
+ raise TokenRequestDenied(error % (r.status_code, r.text), r)
+
+ log.debug('Decoding token from response "%s"', r.text)
+ try:
+ token = dict(urldecode(r.text.strip()))
+ except ValueError as e:
+ error = ("Unable to decode token from token response. "
+ "This is commonly caused by an unsuccessful request where"
+ " a non urlencoded error message is returned. "
+ "The decoding error was %s""" % e)
+ raise ValueError(error)
+
+ log.debug('Obtained token %s', token)
+ log.debug('Updating internal client attributes from token data.')
+ self._populate_attributes(token)
+ return token
+
+ def rebuild_auth(self, prepared_request, response):
+ """
+ When being redirected we should always strip Authorization
+ header, since nonce may not be reused as per OAuth spec.
+ """
+ if 'Authorization' in prepared_request.headers:
+ # If we get redirected to a new host, we should strip out
+ # any authentication headers.
+ prepared_request.headers.pop('Authorization', True)
+ prepared_request.prepare_auth(self.auth)
+ return
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/oauth2_auth.py b/plugins/wiktionary/mwclient/requests_oauthlib/oauth2_auth.py
new file mode 100644
index 0000000..0ce58cc
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/oauth2_auth.py
@@ -0,0 +1,36 @@
+from __future__ import unicode_literals
+from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError
+from oauthlib.oauth2 import is_secure_transport
+from requests.auth import AuthBase
+
+
+class OAuth2(AuthBase):
+ """Adds proof of authorization (OAuth2 token) to the request."""
+
+ def __init__(self, client_id=None, client=None, token=None):
+ """Construct a new OAuth 2 authorization object.
+
+ :param client_id: Client id obtained during registration
+ :param client: :class:`oauthlib.oauth2.Client` to be used. Default is
+ WebApplicationClient which is useful for any
+ hosted application but not mobile or desktop.
+ :param token: Token dictionary, must include access_token
+ and token_type.
+ """
+ self._client = client or WebApplicationClient(client_id, token=token)
+ if token:
+ for k, v in token.items():
+ setattr(self._client, k, v)
+
+ def __call__(self, r):
+ """Append an OAuth 2 token to the request.
+
+ Note that currently HTTPS is required for all requests. There may be
+ a token type that allows for plain HTTP in the future and then this
+ should be updated to allow plain HTTP on a white list basis.
+ """
+ if not is_secure_transport(r.url):
+ raise InsecureTransportError()
+ r.url, r.headers, r.body = self._client.add_token(r.url,
+ http_method=r.method, body=r.body, headers=r.headers)
+ return r
diff --git a/plugins/wiktionary/mwclient/requests_oauthlib/oauth2_session.py b/plugins/wiktionary/mwclient/requests_oauthlib/oauth2_session.py
new file mode 100644
index 0000000..5c46c2d
--- /dev/null
+++ b/plugins/wiktionary/mwclient/requests_oauthlib/oauth2_session.py
@@ -0,0 +1,376 @@
+from __future__ import unicode_literals
+
+import logging
+
+from oauthlib.common import generate_token, urldecode
+from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError
+from oauthlib.oauth2 import TokenExpiredError, is_secure_transport
+import requests
+
+log = logging.getLogger(__name__)
+
+
+class TokenUpdated(Warning):
+ def __init__(self, token):
+ super(TokenUpdated, self).__init__()
+ self.token = token
+
+
+class OAuth2Session(requests.Session):
+ """Versatile OAuth 2 extension to :class:`requests.Session`.
+
+ Supports any grant type adhering to :class:`oauthlib.oauth2.Client` spec
+ including the four core OAuth 2 grants.
+
+ Can be used to create authorization urls, fetch tokens and access protected
+ resources using the :class:`requests.Session` interface you are used to.
+
+ - :class:`oauthlib.oauth2.WebApplicationClient` (default): Authorization Code Grant
+ - :class:`oauthlib.oauth2.MobileApplicationClient`: Implicit Grant
+ - :class:`oauthlib.oauth2.LegacyApplicationClient`: Password Credentials Grant
+ - :class:`oauthlib.oauth2.BackendApplicationClient`: Client Credentials Grant
+
+ Note that the only time you will be using Implicit Grant from python is if
+ you are driving a user agent able to obtain URL fragments.
+ """
+
+ def __init__(self, client_id=None, client=None, auto_refresh_url=None,
+ auto_refresh_kwargs=None, scope=None, redirect_uri=None, token=None,
+ state=None, token_updater=None, **kwargs):
+ """Construct a new OAuth 2 client session.
+
+ :param client_id: Client id obtained during registration
+ :param client: :class:`oauthlib.oauth2.Client` to be used. Default is
+ WebApplicationClient which is useful for any
+ hosted application but not mobile or desktop.
+ :param scope: List of scopes you wish to request access to
+ :param redirect_uri: Redirect URI you registered as callback
+ :param token: Token dictionary, must include access_token
+ and token_type.
+ :param state: State string used to prevent CSRF. This will be given
+ when creating the authorization url and must be supplied
+ when parsing the authorization response.
+ Can be either a string or a no argument callable.
+ :auto_refresh_url: Refresh token endpoint URL, must be HTTPS. Supply
+ this if you wish the client to automatically refresh
+ your access tokens.
+ :auto_refresh_kwargs: Extra arguments to pass to the refresh token
+ endpoint.
+ :token_updater: Method with one argument, token, to be used to update
+ your token databse on automatic token refresh. If not
+ set a TokenUpdated warning will be raised when a token
+ has been refreshed. This warning will carry the token
+ in its token argument.
+ :param kwargs: Arguments to pass to the Session constructor.
+ """
+ super(OAuth2Session, self).__init__(**kwargs)
+ self._client = client or WebApplicationClient(client_id, token=token)
+ self.token = token or {}
+ self.scope = scope
+ self.redirect_uri = redirect_uri
+ self.state = state or generate_token
+ self._state = state
+ self.auto_refresh_url = auto_refresh_url
+ self.auto_refresh_kwargs = auto_refresh_kwargs or {}
+ self.token_updater = token_updater
+
+ # Allow customizations for non compliant providers through various
+ # hooks to adjust requests and responses.
+ self.compliance_hook = {
+ 'access_token_response': set([]),
+ 'refresh_token_response': set([]),
+ 'protected_request': set([]),
+ }
+
+ def new_state(self):
+ """Generates a state string to be used in authorizations."""
+ try:
+ self._state = self.state()
+ log.debug('Generated new state %s.', self._state)
+ except TypeError:
+ self._state = self.state
+ log.debug('Re-using previously supplied state %s.', self._state)
+ return self._state
+
+ @property
+ def client_id(self):
+ return getattr(self._client, "client_id", None)
+
+ @client_id.setter
+ def client_id(self, value):
+ self._client.client_id = value
+
+ @client_id.deleter
+ def client_id(self):
+ del self._client.client_id
+
+ @property
+ def token(self):
+ return getattr(self._client, "token", None)
+
+ @token.setter
+ def token(self, value):
+ self._client.token = value
+ self._client._populate_attributes(value)
+
+ @property
+ def access_token(self):
+ return getattr(self._client, "access_token", None)
+
+ @access_token.setter
+ def access_token(self, value):
+ self._client.access_token = value
+
+ @access_token.deleter
+ def access_token(self):
+ del self._client.access_token
+
+ @property
+ def authorized(self):
+ """Boolean that indicates whether this session has an OAuth token
+ or not. If `self.authorized` is True, you can reasonably expect
+ OAuth-protected requests to the resource to succeed. If
+ `self.authorized` is False, you need the user to go through the OAuth
+ authentication dance before OAuth-protected requests to the resource
+ will succeed.
+ """
+ return bool(self.access_token)
+
+ def authorization_url(self, url, state=None, **kwargs):
+ """Form an authorization URL.
+
+ :param url: Authorization endpoint url, must be HTTPS.
+ :param state: An optional state string for CSRF protection. If not
+ given it will be generated for you.
+ :param kwargs: Extra parameters to include.
+ :return: authorization_url, state
+ """
+ state = state or self.new_state()
+ return self._client.prepare_request_uri(url,
+ redirect_uri=self.redirect_uri,
+ scope=self.scope,
+ state=state,
+ **kwargs), state
+
+ def fetch_token(self, token_url, code=None, authorization_response=None,
+ body='', auth=None, username=None, password=None, method='POST',
+ timeout=None, headers=None, verify=True, proxies=None, **kwargs):
+ """Generic method for fetching an access token from the token endpoint.
+
+ If you are using the MobileApplicationClient you will want to use
+ token_from_fragment instead of fetch_token.
+
+ :param token_url: Token endpoint URL, must use HTTPS.
+ :param code: Authorization code (used by WebApplicationClients).
+ :param authorization_response: Authorization response URL, the callback
+ URL of the request back to you. Used by
+ WebApplicationClients instead of code.
+ :param body: Optional application/x-www-form-urlencoded body to add the
+ include in the token request. Prefer kwargs over body.
+ :param auth: An auth tuple or method as accepted by requests.
+ :param username: Username used by LegacyApplicationClients.
+ :param password: Password used by LegacyApplicationClients.
+ :param method: The HTTP method used to make the request. Defaults
+ to POST, but may also be GET. Other methods should
+ be added as needed.
+ :param headers: Dict to default request headers with.
+ :param timeout: Timeout of the request in seconds.
+ :param verify: Verify SSL certificate.
+ :param kwargs: Extra parameters to include in the token request.
+ :return: A token dict
+ """
+ if not is_secure_transport(token_url):
+ raise InsecureTransportError()
+
+ if not code and authorization_response:
+ self._client.parse_request_uri_response(authorization_response,
+ state=self._state)
+ code = self._client.code
+ elif not code and isinstance(self._client, WebApplicationClient):
+ code = self._client.code
+ if not code:
+ raise ValueError('Please supply either code or '
+ 'authorization_response parameters.')
+
+
+ body = self._client.prepare_request_body(code=code, body=body,
+ redirect_uri=self.redirect_uri, username=username,
+ password=password, **kwargs)
+
+ client_id = kwargs.get('client_id', '')
+ if auth is None:
+ if client_id:
+ log.debug('Encoding client_id "%s" with client_secret as Basic auth credentials.', client_id)
+ client_secret = kwargs.get('client_secret', '')
+ client_secret = client_secret if client_secret is not None else ''
+ auth = requests.auth.HTTPBasicAuth(client_id, client_secret)
+ elif username:
+ if password is None:
+ raise ValueError('Username was supplied, but not password.')
+ log.debug('Encoding username, password as Basic auth credentials.')
+ auth = requests.auth.HTTPBasicAuth(username, password)
+
+ headers = headers or {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
+ }
+ self.token = {}
+ if method.upper() == 'POST':
+ r = self.post(token_url, data=dict(urldecode(body)),
+ timeout=timeout, headers=headers, auth=auth,
+ verify=verify, proxies=proxies)
+ log.debug('Prepared fetch token request body %s', body)
+ elif method.upper() == 'GET':
+ # if method is not 'POST', switch body to querystring and GET
+ r = self.get(token_url, params=dict(urldecode(body)),
+ timeout=timeout, headers=headers, auth=auth,
+ verify=verify, proxies=proxies)
+ log.debug('Prepared fetch token request querystring %s', body)
+ else:
+ raise ValueError('The method kwarg must be POST or GET.')
+
+ log.debug('Request to fetch token completed with status %s.',
+ r.status_code)
+ log.debug('Request headers were %s', r.request.headers)
+ log.debug('Request body was %s', r.request.body)
+ log.debug('Response headers were %s and content %s.',
+ r.headers, r.text)
+ log.debug('Invoking %d token response hooks.',
+ len(self.compliance_hook['access_token_response']))
+ for hook in self.compliance_hook['access_token_response']:
+ log.debug('Invoking hook %s.', hook)
+ r = hook(r)
+
+ self._client.parse_request_body_response(r.text, scope=self.scope)
+ self.token = self._client.token
+ log.debug('Obtained token %s.', self.token)
+ return self.token
+
+ def token_from_fragment(self, authorization_response):
+ """Parse token from the URI fragment, used by MobileApplicationClients.
+
+ :param authorization_response: The full URL of the redirect back to you
+ :return: A token dict
+ """
+ self._client.parse_request_uri_response(authorization_response,
+ state=self._state)
+ self.token = self._client.token
+ return self.token
+
+ def refresh_token(self, token_url, refresh_token=None, body='', auth=None,
+ timeout=None, headers=None, verify=True, proxies=None, **kwargs):
+ """Fetch a new access token using a refresh token.
+
+ :param token_url: The token endpoint, must be HTTPS.
+ :param refresh_token: The refresh_token to use.
+ :param body: Optional application/x-www-form-urlencoded body to add the
+ include in the token request. Prefer kwargs over body.
+ :param auth: An auth tuple or method as accepted by requests.
+ :param timeout: Timeout of the request in seconds.
+ :param verify: Verify SSL certificate.
+ :param kwargs: Extra parameters to include in the token request.
+ :return: A token dict
+ """
+ if not token_url:
+ raise ValueError('No token endpoint set for auto_refresh.')
+
+ if not is_secure_transport(token_url):
+ raise InsecureTransportError()
+
+ refresh_token = refresh_token or self.token.get('refresh_token')
+
+ log.debug('Adding auto refresh key word arguments %s.',
+ self.auto_refresh_kwargs)
+ kwargs.update(self.auto_refresh_kwargs)
+ body = self._client.prepare_refresh_body(body=body,
+ refresh_token=refresh_token, scope=self.scope, **kwargs)
+ log.debug('Prepared refresh token request body %s', body)
+
+ if headers is None:
+ headers = {
+ 'Accept': 'application/json',
+ 'Content-Type': (
+ 'application/x-www-form-urlencoded;charset=UTF-8'
+ ),
+ }
+
+ r = self.post(token_url, data=dict(urldecode(body)), auth=auth,
+ timeout=timeout, headers=headers, verify=verify, withhold_token=True, proxies=proxies)
+ log.debug('Request to refresh token completed with status %s.',
+ r.status_code)
+ log.debug('Response headers were %s and content %s.',
+ r.headers, r.text)
+ log.debug('Invoking %d token response hooks.',
+ len(self.compliance_hook['refresh_token_response']))
+ for hook in self.compliance_hook['refresh_token_response']:
+ log.debug('Invoking hook %s.', hook)
+ r = hook(r)
+
+ self.token = self._client.parse_request_body_response(r.text, scope=self.scope)
+ if not 'refresh_token' in self.token:
+ log.debug('No new refresh token given. Re-using old.')
+ self.token['refresh_token'] = refresh_token
+ return self.token
+
+ def request(self, method, url, data=None, headers=None, withhold_token=False,
+ client_id=None, client_secret=None, **kwargs):
+ """Intercept all requests and add the OAuth 2 token if present."""
+ if not is_secure_transport(url):
+ raise InsecureTransportError()
+ if self.token and not withhold_token:
+ log.debug('Invoking %d protected resource request hooks.',
+ len(self.compliance_hook['protected_request']))
+ for hook in self.compliance_hook['protected_request']:
+ log.debug('Invoking hook %s.', hook)
+ url, headers, data = hook(url, headers, data)
+
+ log.debug('Adding token %s to request.', self.token)
+ try:
+ url, headers, data = self._client.add_token(url,
+ http_method=method, body=data, headers=headers)
+ # Attempt to retrieve and save new access token if expired
+ except TokenExpiredError:
+ if self.auto_refresh_url:
+ log.debug('Auto refresh is set, attempting to refresh at %s.',
+ self.auto_refresh_url)
+
+ # We mustn't pass auth twice.
+ auth = kwargs.pop('auth', None)
+ if client_id and client_secret and (auth is None):
+ log.debug('Encoding client_id "%s" with client_secret as Basic auth credentials.', client_id)
+ auth = requests.auth.HTTPBasicAuth(client_id, client_secret)
+ token = self.refresh_token(
+ self.auto_refresh_url, auth=auth, **kwargs
+ )
+ if self.token_updater:
+ log.debug('Updating token to %s using %s.',
+ token, self.token_updater)
+ self.token_updater(token)
+ url, headers, data = self._client.add_token(url,
+ http_method=method, body=data, headers=headers)
+ else:
+ raise TokenUpdated(token)
+ else:
+ raise
+
+ log.debug('Requesting url %s using method %s.', url, method)
+ log.debug('Supplying headers %s and data %s', headers, data)
+ log.debug('Passing through key word arguments %s.', kwargs)
+ return super(OAuth2Session, self).request(method, url,
+ headers=headers, data=data, **kwargs)
+
+ def register_compliance_hook(self, hook_type, hook):
+ """Register a hook for request/response tweaking.
+
+ Available hooks are:
+ access_token_response invoked before token parsing.
+ refresh_token_response invoked before refresh token parsing.
+ protected_request invoked before making a request.
+
+ If you find a new hook is needed please send a GitHub PR request
+ or open an issue.
+ """
+ if hook_type not in self.compliance_hook:
+ raise ValueError('Hook type %s is not in %s.',
+ hook_type, self.compliance_hook)
+ self.compliance_hook[hook_type].add(hook)
diff --git a/plugins/wiktionary/mwclient/six.LICENSE b/plugins/wiktionary/mwclient/six.LICENSE
new file mode 100644
index 0000000..e558f9d
--- /dev/null
+++ b/plugins/wiktionary/mwclient/six.LICENSE
@@ -0,0 +1,18 @@
+Copyright (c) 2010-2015 Benjamin Peterson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/plugins/wiktionary/mwclient/six.README b/plugins/wiktionary/mwclient/six.README
new file mode 100644
index 0000000..422457f
--- /dev/null
+++ b/plugins/wiktionary/mwclient/six.README
@@ -0,0 +1,18 @@
+Six is a Python 2 and 3 compatibility library. It provides utility functions
+for smoothing over the differences between the Python versions with the goal of
+writing Python code that is compatible on both Python versions. See the
+documentation for more information on what is provided.
+
+Six supports every Python version since 2.6. It is contained in only one Python
+file, so it can be easily copied into your project. (The copyright and license
+notice must be retained.)
+
+Online documentation is at https://pythonhosted.org/six/.
+
+Bugs can be reported to https://bitbucket.org/gutworth/six. The code can also
+be found there.
+
+For questions about six or porting in general, email the python-porting mailing
+list: https://mail.python.org/mailman/listinfo/python-porting
+
+Version: 1.10.0
diff --git a/plugins/wiktionary/mwclient/six.py b/plugins/wiktionary/mwclient/six.py
new file mode 100644
index 0000000..190c023
--- /dev/null
+++ b/plugins/wiktionary/mwclient/six.py
@@ -0,0 +1,868 @@
+"""Utilities for writing code that runs on Python 2 and 3"""
+
+# Copyright (c) 2010-2015 Benjamin Peterson
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+from __future__ import absolute_import
+
+import functools
+import itertools
+import operator
+import sys
+import types
+
+__author__ = "Benjamin Peterson <[email protected]>"
+__version__ = "1.10.0"
+
+
+# Useful for very coarse version differentiation.
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+PY34 = sys.version_info[0:2] >= (3, 4)
+
+if PY3:
+ string_types = str,
+ integer_types = int,
+ class_types = type,
+ text_type = str
+ binary_type = bytes
+
+ MAXSIZE = sys.maxsize
+else:
+ string_types = basestring,
+ integer_types = (int, long)
+ class_types = (type, types.ClassType)
+ text_type = unicode
+ binary_type = str
+
+ if sys.platform.startswith("java"):
+ # Jython always uses 32 bits.
+ MAXSIZE = int((1 << 31) - 1)
+ else:
+ # It's possible to have sizeof(long) != sizeof(Py_ssize_t).
+ class X(object):
+
+ def __len__(self):
+ return 1 << 31
+ try:
+ len(X())
+ except OverflowError:
+ # 32-bit
+ MAXSIZE = int((1 << 31) - 1)
+ else:
+ # 64-bit
+ MAXSIZE = int((1 << 63) - 1)
+ del X
+
+
+def _add_doc(func, doc):
+ """Add documentation to a function."""
+ func.__doc__ = doc
+
+
+def _import_module(name):
+ """Import module, returning the module after the last dot."""
+ __import__(name)
+ return sys.modules[name]
+
+
+class _LazyDescr(object):
+
+ def __init__(self, name):
+ self.name = name
+
+ def __get__(self, obj, tp):
+ result = self._resolve()
+ setattr(obj, self.name, result) # Invokes __set__.
+ try:
+ # This is a bit ugly, but it avoids running this again by
+ # removing this descriptor.
+ delattr(obj.__class__, self.name)
+ except AttributeError:
+ pass
+ return result
+
+
+class MovedModule(_LazyDescr):
+
+ def __init__(self, name, old, new=None):
+ super(MovedModule, self).__init__(name)
+ if PY3:
+ if new is None:
+ new = name
+ self.mod = new
+ else:
+ self.mod = old
+
+ def _resolve(self):
+ return _import_module(self.mod)
+
+ def __getattr__(self, attr):
+ _module = self._resolve()
+ value = getattr(_module, attr)
+ setattr(self, attr, value)
+ return value
+
+
+class _LazyModule(types.ModuleType):
+
+ def __init__(self, name):
+ super(_LazyModule, self).__init__(name)
+ self.__doc__ = self.__class__.__doc__
+
+ def __dir__(self):
+ attrs = ["__doc__", "__name__"]
+ attrs += [attr.name for attr in self._moved_attributes]
+ return attrs
+
+ # Subclasses should override this
+ _moved_attributes = []
+
+
+class MovedAttribute(_LazyDescr):
+
+ def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
+ super(MovedAttribute, self).__init__(name)
+ if PY3:
+ if new_mod is None:
+ new_mod = name
+ self.mod = new_mod
+ if new_attr is None:
+ if old_attr is None:
+ new_attr = name
+ else:
+ new_attr = old_attr
+ self.attr = new_attr
+ else:
+ self.mod = old_mod
+ if old_attr is None:
+ old_attr = name
+ self.attr = old_attr
+
+ def _resolve(self):
+ module = _import_module(self.mod)
+ return getattr(module, self.attr)
+
+
+class _SixMetaPathImporter(object):
+
+ """
+ A meta path importer to import six.moves and its submodules.
+
+ This class implements a PEP302 finder and loader. It should be compatible
+ with Python 2.5 and all existing versions of Python3
+ """
+
+ def __init__(self, six_module_name):
+ self.name = six_module_name
+ self.known_modules = {}
+
+ def _add_module(self, mod, *fullnames):
+ for fullname in fullnames:
+ self.known_modules[self.name + "." + fullname] = mod
+
+ def _get_module(self, fullname):
+ return self.known_modules[self.name + "." + fullname]
+
+ def find_module(self, fullname, path=None):
+ if fullname in self.known_modules:
+ return self
+ return None
+
+ def __get_module(self, fullname):
+ try:
+ return self.known_modules[fullname]
+ except KeyError:
+ raise ImportError("This loader does not know module " + fullname)
+
+ def load_module(self, fullname):
+ try:
+ # in case of a reload
+ return sys.modules[fullname]
+ except KeyError:
+ pass
+ mod = self.__get_module(fullname)
+ if isinstance(mod, MovedModule):
+ mod = mod._resolve()
+ else:
+ mod.__loader__ = self
+ sys.modules[fullname] = mod
+ return mod
+
+ def is_package(self, fullname):
+ """
+ Return true, if the named module is a package.
+
+ We need this method to get correct spec objects with
+ Python 3.4 (see PEP451)
+ """
+ return hasattr(self.__get_module(fullname), "__path__")
+
+ def get_code(self, fullname):
+ """Return None
+
+ Required, if is_package is implemented"""
+ self.__get_module(fullname) # eventually raises ImportError
+ return None
+ get_source = get_code # same as get_code
+
+_importer = _SixMetaPathImporter(__name__)
+
+
+class _MovedItems(_LazyModule):
+
+ """Lazy loading of moved objects"""
+ __path__ = [] # mark as package
+
+
+_moved_attributes = [
+ MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
+ MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
+ MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
+ MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
+ MovedAttribute("intern", "__builtin__", "sys"),
+ MovedAttribute("map", "itertools", "builtins", "imap", "map"),
+ MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"),
+ MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"),
+ MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
+ MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"),
+ MovedAttribute("reduce", "__builtin__", "functools"),
+ MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
+ MovedAttribute("StringIO", "StringIO", "io"),
+ MovedAttribute("UserDict", "UserDict", "collections"),
+ MovedAttribute("UserList", "UserList", "collections"),
+ MovedAttribute("UserString", "UserString", "collections"),
+ MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
+ MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
+ MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
+ MovedModule("builtins", "__builtin__"),
+ MovedModule("configparser", "ConfigParser"),
+ MovedModule("copyreg", "copy_reg"),
+ MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
+ MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"),
+ MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
+ MovedModule("http_cookies", "Cookie", "http.cookies"),
+ MovedModule("html_entities", "htmlentitydefs", "html.entities"),
+ MovedModule("html_parser", "HTMLParser", "html.parser"),
+ MovedModule("http_client", "httplib", "http.client"),
+ MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
+ MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
+ MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
+ MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
+ MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
+ MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
+ MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
+ MovedModule("cPickle", "cPickle", "pickle"),
+ MovedModule("queue", "Queue"),
+ MovedModule("reprlib", "repr"),
+ MovedModule("socketserver", "SocketServer"),
+ MovedModule("_thread", "thread", "_thread"),
+ MovedModule("tkinter", "Tkinter"),
+ MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
+ MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
+ MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
+ MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
+ MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
+ MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
+ MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
+ MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
+ MovedModule("tkinter_colorchooser", "tkColorChooser",
+ "tkinter.colorchooser"),
+ MovedModule("tkinter_commondialog", "tkCommonDialog",
+ "tkinter.commondialog"),
+ MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
+ MovedModule("tkinter_font", "tkFont", "tkinter.font"),
+ MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
+ MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
+ "tkinter.simpledialog"),
+ MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
+ MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
+ MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
+ MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
+ MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
+ MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
+]
+# Add windows specific modules.
+if sys.platform == "win32":
+ _moved_attributes += [
+ MovedModule("winreg", "_winreg"),
+ ]
+
+for attr in _moved_attributes:
+ setattr(_MovedItems, attr.name, attr)
+ if isinstance(attr, MovedModule):
+ _importer._add_module(attr, "moves." + attr.name)
+del attr
+
+_MovedItems._moved_attributes = _moved_attributes
+
+moves = _MovedItems(__name__ + ".moves")
+_importer._add_module(moves, "moves")
+
+
+class Module_six_moves_urllib_parse(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_parse"""
+
+
+_urllib_parse_moved_attributes = [
+ MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
+ MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
+ MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
+ MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
+ MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
+ MovedAttribute("urljoin", "urlparse", "urllib.parse"),
+ MovedAttribute("urlparse", "urlparse", "urllib.parse"),
+ MovedAttribute("urlsplit", "urlparse", "urllib.parse"),
+ MovedAttribute("urlunparse", "urlparse", "urllib.parse"),
+ MovedAttribute("urlunsplit", "urlparse", "urllib.parse"),
+ MovedAttribute("quote", "urllib", "urllib.parse"),
+ MovedAttribute("quote_plus", "urllib", "urllib.parse"),
+ MovedAttribute("unquote", "urllib", "urllib.parse"),
+ MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
+ MovedAttribute("urlencode", "urllib", "urllib.parse"),
+ MovedAttribute("splitquery", "urllib", "urllib.parse"),
+ MovedAttribute("splittag", "urllib", "urllib.parse"),
+ MovedAttribute("splituser", "urllib", "urllib.parse"),
+ MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_params", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_query", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
+]
+for attr in _urllib_parse_moved_attributes:
+ setattr(Module_six_moves_urllib_parse, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
+ "moves.urllib_parse", "moves.urllib.parse")
+
+
+class Module_six_moves_urllib_error(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_error"""
+
+
+_urllib_error_moved_attributes = [
+ MovedAttribute("URLError", "urllib2", "urllib.error"),
+ MovedAttribute("HTTPError", "urllib2", "urllib.error"),
+ MovedAttribute("ContentTooShortError", "urllib", "urllib.error"),
+]
+for attr in _urllib_error_moved_attributes:
+ setattr(Module_six_moves_urllib_error, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
+ "moves.urllib_error", "moves.urllib.error")
+
+
+class Module_six_moves_urllib_request(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_request"""
+
+
+_urllib_request_moved_attributes = [
+ MovedAttribute("urlopen", "urllib2", "urllib.request"),
+ MovedAttribute("install_opener", "urllib2", "urllib.request"),
+ MovedAttribute("build_opener", "urllib2", "urllib.request"),
+ MovedAttribute("pathname2url", "urllib", "urllib.request"),
+ MovedAttribute("url2pathname", "urllib", "urllib.request"),
+ MovedAttribute("getproxies", "urllib", "urllib.request"),
+ MovedAttribute("Request", "urllib2", "urllib.request"),
+ MovedAttribute("OpenerDirector", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
+ MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
+ MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"),
+ MovedAttribute("FileHandler", "urllib2", "urllib.request"),
+ MovedAttribute("FTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("UnknownHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"),
+ MovedAttribute("urlretrieve", "urllib", "urllib.request"),
+ MovedAttribute("urlcleanup", "urllib", "urllib.request"),
+ MovedAttribute("URLopener", "urllib", "urllib.request"),
+ MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
+ MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
+]
+for attr in _urllib_request_moved_attributes:
+ setattr(Module_six_moves_urllib_request, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
+ "moves.urllib_request", "moves.urllib.request")
+
+
+class Module_six_moves_urllib_response(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_response"""
+
+
+_urllib_response_moved_attributes = [
+ MovedAttribute("addbase", "urllib", "urllib.response"),
+ MovedAttribute("addclosehook", "urllib", "urllib.response"),
+ MovedAttribute("addinfo", "urllib", "urllib.response"),
+ MovedAttribute("addinfourl", "urllib", "urllib.response"),
+]
+for attr in _urllib_response_moved_attributes:
+ setattr(Module_six_moves_urllib_response, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
+ "moves.urllib_response", "moves.urllib.response")
+
+
+class Module_six_moves_urllib_robotparser(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_robotparser"""
+
+
+_urllib_robotparser_moved_attributes = [
+ MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"),
+]
+for attr in _urllib_robotparser_moved_attributes:
+ setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
+ "moves.urllib_robotparser", "moves.urllib.robotparser")
+
+
+class Module_six_moves_urllib(types.ModuleType):
+
+ """Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
+ __path__ = [] # mark as package
+ parse = _importer._get_module("moves.urllib_parse")
+ error = _importer._get_module("moves.urllib_error")
+ request = _importer._get_module("moves.urllib_request")
+ response = _importer._get_module("moves.urllib_response")
+ robotparser = _importer._get_module("moves.urllib_robotparser")
+
+ def __dir__(self):
+ return ['parse', 'error', 'request', 'response', 'robotparser']
+
+_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
+ "moves.urllib")
+
+
+def add_move(move):
+ """Add an item to six.moves."""
+ setattr(_MovedItems, move.name, move)
+
+
+def remove_move(name):
+ """Remove item from six.moves."""
+ try:
+ delattr(_MovedItems, name)
+ except AttributeError:
+ try:
+ del moves.__dict__[name]
+ except KeyError:
+ raise AttributeError("no such move, %r" % (name,))
+
+
+if PY3:
+ _meth_func = "__func__"
+ _meth_self = "__self__"
+
+ _func_closure = "__closure__"
+ _func_code = "__code__"
+ _func_defaults = "__defaults__"
+ _func_globals = "__globals__"
+else:
+ _meth_func = "im_func"
+ _meth_self = "im_self"
+
+ _func_closure = "func_closure"
+ _func_code = "func_code"
+ _func_defaults = "func_defaults"
+ _func_globals = "func_globals"
+
+
+try:
+ advance_iterator = next
+except NameError:
+ def advance_iterator(it):
+ return it.next()
+next = advance_iterator
+
+
+try:
+ callable = callable
+except NameError:
+ def callable(obj):
+ return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
+
+
+if PY3:
+ def get_unbound_function(unbound):
+ return unbound
+
+ create_bound_method = types.MethodType
+
+ def create_unbound_method(func, cls):
+ return func
+
+ Iterator = object
+else:
+ def get_unbound_function(unbound):
+ return unbound.im_func
+
+ def create_bound_method(func, obj):
+ return types.MethodType(func, obj, obj.__class__)
+
+ def create_unbound_method(func, cls):
+ return types.MethodType(func, None, cls)
+
+ class Iterator(object):
+
+ def next(self):
+ return type(self).__next__(self)
+
+ callable = callable
+_add_doc(get_unbound_function,
+ """Get the function out of a possibly unbound function""")
+
+
+get_method_function = operator.attrgetter(_meth_func)
+get_method_self = operator.attrgetter(_meth_self)
+get_function_closure = operator.attrgetter(_func_closure)
+get_function_code = operator.attrgetter(_func_code)
+get_function_defaults = operator.attrgetter(_func_defaults)
+get_function_globals = operator.attrgetter(_func_globals)
+
+
+if PY3:
+ def iterkeys(d, **kw):
+ return iter(d.keys(**kw))
+
+ def itervalues(d, **kw):
+ return iter(d.values(**kw))
+
+ def iteritems(d, **kw):
+ return iter(d.items(**kw))
+
+ def iterlists(d, **kw):
+ return iter(d.lists(**kw))
+
+ viewkeys = operator.methodcaller("keys")
+
+ viewvalues = operator.methodcaller("values")
+
+ viewitems = operator.methodcaller("items")
+else:
+ def iterkeys(d, **kw):
+ return d.iterkeys(**kw)
+
+ def itervalues(d, **kw):
+ return d.itervalues(**kw)
+
+ def iteritems(d, **kw):
+ return d.iteritems(**kw)
+
+ def iterlists(d, **kw):
+ return d.iterlists(**kw)
+
+ viewkeys = operator.methodcaller("viewkeys")
+
+ viewvalues = operator.methodcaller("viewvalues")
+
+ viewitems = operator.methodcaller("viewitems")
+
+_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
+_add_doc(itervalues, "Return an iterator over the values of a dictionary.")
+_add_doc(iteritems,
+ "Return an iterator over the (key, value) pairs of a dictionary.")
+_add_doc(iterlists,
+ "Return an iterator over the (key, [values]) pairs of a dictionary.")
+
+
+if PY3:
+ def b(s):
+ return s.encode("latin-1")
+
+ def u(s):
+ return s
+ unichr = chr
+ import struct
+ int2byte = struct.Struct(">B").pack
+ del struct
+ byte2int = operator.itemgetter(0)
+ indexbytes = operator.getitem
+ iterbytes = iter
+ import io
+ StringIO = io.StringIO
+ BytesIO = io.BytesIO
+ _assertCountEqual = "assertCountEqual"
+ if sys.version_info[1] <= 1:
+ _assertRaisesRegex = "assertRaisesRegexp"
+ _assertRegex = "assertRegexpMatches"
+ else:
+ _assertRaisesRegex = "assertRaisesRegex"
+ _assertRegex = "assertRegex"
+else:
+ def b(s):
+ return s
+ # Workaround for standalone backslash
+
+ def u(s):
+ return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
+ unichr = unichr
+ int2byte = chr
+
+ def byte2int(bs):
+ return ord(bs[0])
+
+ def indexbytes(buf, i):
+ return ord(buf[i])
+ iterbytes = functools.partial(itertools.imap, ord)
+ import StringIO
+ StringIO = BytesIO = StringIO.StringIO
+ _assertCountEqual = "assertItemsEqual"
+ _assertRaisesRegex = "assertRaisesRegexp"
+ _assertRegex = "assertRegexpMatches"
+_add_doc(b, """Byte literal""")
+_add_doc(u, """Text literal""")
+
+
+def assertCountEqual(self, *args, **kwargs):
+ return getattr(self, _assertCountEqual)(*args, **kwargs)
+
+
+def assertRaisesRegex(self, *args, **kwargs):
+ return getattr(self, _assertRaisesRegex)(*args, **kwargs)
+
+
+def assertRegex(self, *args, **kwargs):
+ return getattr(self, _assertRegex)(*args, **kwargs)
+
+
+if PY3:
+ exec_ = getattr(moves.builtins, "exec")
+
+ def reraise(tp, value, tb=None):
+ if value is None:
+ value = tp()
+ if value.__traceback__ is not tb:
+ raise value.with_traceback(tb)
+ raise value
+
+else:
+ def exec_(_code_, _globs_=None, _locs_=None):
+ """Execute code in a namespace."""
+ if _globs_ is None:
+ frame = sys._getframe(1)
+ _globs_ = frame.f_globals
+ if _locs_ is None:
+ _locs_ = frame.f_locals
+ del frame
+ elif _locs_ is None:
+ _locs_ = _globs_
+ exec("""exec _code_ in _globs_, _locs_""")
+
+ exec_("""def reraise(tp, value, tb=None):
+ raise tp, value, tb
+""")
+
+
+if sys.version_info[:2] == (3, 2):
+ exec_("""def raise_from(value, from_value):
+ if from_value is None:
+ raise value
+ raise value from from_value
+""")
+elif sys.version_info[:2] > (3, 2):
+ exec_("""def raise_from(value, from_value):
+ raise value from from_value
+""")
+else:
+ def raise_from(value, from_value):
+ raise value
+
+
+print_ = getattr(moves.builtins, "print", None)
+if print_ is None:
+ def print_(*args, **kwargs):
+ """The new-style print function for Python 2.4 and 2.5."""
+ fp = kwargs.pop("file", sys.stdout)
+ if fp is None:
+ return
+
+ def write(data):
+ if not isinstance(data, basestring):
+ data = str(data)
+ # If the file has an encoding, encode unicode with it.
+ if (isinstance(fp, file) and
+ isinstance(data, unicode) and
+ fp.encoding is not None):
+ errors = getattr(fp, "errors", None)
+ if errors is None:
+ errors = "strict"
+ data = data.encode(fp.encoding, errors)
+ fp.write(data)
+ want_unicode = False
+ sep = kwargs.pop("sep", None)
+ if sep is not None:
+ if isinstance(sep, unicode):
+ want_unicode = True
+ elif not isinstance(sep, str):
+ raise TypeError("sep must be None or a string")
+ end = kwargs.pop("end", None)
+ if end is not None:
+ if isinstance(end, unicode):
+ want_unicode = True
+ elif not isinstance(end, str):
+ raise TypeError("end must be None or a string")
+ if kwargs:
+ raise TypeError("invalid keyword arguments to print()")
+ if not want_unicode:
+ for arg in args:
+ if isinstance(arg, unicode):
+ want_unicode = True
+ break
+ if want_unicode:
+ newline = unicode("\n")
+ space = unicode(" ")
+ else:
+ newline = "\n"
+ space = " "
+ if sep is None:
+ sep = space
+ if end is None:
+ end = newline
+ for i, arg in enumerate(args):
+ if i:
+ write(sep)
+ write(arg)
+ write(end)
+if sys.version_info[:2] < (3, 3):
+ _print = print_
+
+ def print_(*args, **kwargs):
+ fp = kwargs.get("file", sys.stdout)
+ flush = kwargs.pop("flush", False)
+ _print(*args, **kwargs)
+ if flush and fp is not None:
+ fp.flush()
+
+_add_doc(reraise, """Reraise an exception.""")
+
+if sys.version_info[0:2] < (3, 4):
+ def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
+ updated=functools.WRAPPER_UPDATES):
+ def wrapper(f):
+ f = functools.wraps(wrapped, assigned, updated)(f)
+ f.__wrapped__ = wrapped
+ return f
+ return wrapper
+else:
+ wraps = functools.wraps
+
+
+def with_metaclass(meta, *bases):
+ """Create a base class with a metaclass."""
+ # This requires a bit of explanation: the basic idea is to make a dummy
+ # metaclass for one level of class instantiation that replaces itself with
+ # the actual metaclass.
+ class metaclass(meta):
+
+ def __new__(cls, name, this_bases, d):
+ return meta(name, bases, d)
+ return type.__new__(metaclass, 'temporary_class', (), {})
+
+
+def add_metaclass(metaclass):
+ """Class decorator for creating a class with a metaclass."""
+ def wrapper(cls):
+ orig_vars = cls.__dict__.copy()
+ slots = orig_vars.get('__slots__')
+ if slots is not None:
+ if isinstance(slots, str):
+ slots = [slots]
+ for slots_var in slots:
+ orig_vars.pop(slots_var)
+ orig_vars.pop('__dict__', None)
+ orig_vars.pop('__weakref__', None)
+ return metaclass(cls.__name__, cls.__bases__, orig_vars)
+ return wrapper
+
+
+def python_2_unicode_compatible(klass):
+ """
+ A decorator that defines __unicode__ and __str__ methods under Python 2.
+ Under Python 3 it does nothing.
+
+ To support Python 2 and 3 with a single code base, define a __str__ method
+ returning text and apply this decorator to the class.
+ """
+ if PY2:
+ if '__str__' not in klass.__dict__:
+ raise ValueError("@python_2_unicode_compatible cannot be applied "
+ "to %s because it doesn't define __str__()." %
+ klass.__name__)
+ klass.__unicode__ = klass.__str__
+ klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
+ return klass
+
+
+# Complete the moves implementation.
+# This code is at the end of this module to speed up module loading.
+# Turn this module into a package.
+__path__ = [] # required for PEP 302 and PEP 451
+__package__ = __name__ # see PEP 366 @ReservedAssignment
+if globals().get("__spec__") is not None:
+ __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable
+# Remove other six meta path importers, since they cause problems. This can
+# happen if six is removed from sys.modules and then reloaded. (Setuptools does
+# this for some reason.)
+if sys.meta_path:
+ for i, importer in enumerate(sys.meta_path):
+ # Here's some real nastiness: Another "instance" of the six module might
+ # be floating around. Therefore, we can't use isinstance() to check for
+ # the six meta path importer, since the other six instance will have
+ # inserted an importer with different class.
+ if (type(importer).__name__ == "_SixMetaPathImporter" and
+ importer.name == __name__):
+ del sys.meta_path[i]
+ break
+ del i, importer
+# Finally, add the importer to the meta path import hook.
+sys.meta_path.append(_importer)
diff --git a/plugins/wiktionary/mwclient/sleep.py b/plugins/wiktionary/mwclient/sleep.py
new file mode 100644
index 0000000..c8bf9db
--- /dev/null
+++ b/plugins/wiktionary/mwclient/sleep.py
@@ -0,0 +1,49 @@
+import time
+import logging
+from mwclient.errors import MaximumRetriesExceeded
+
+log = logging.getLogger(__name__)
+
+
+class Sleepers(object):
+
+ def __init__(self, max_retries, retry_timeout, callback=lambda *x: None):
+ self.max_retries = max_retries
+ self.retry_timeout = retry_timeout
+ self.callback = callback
+
+ def make(self, args=None):
+ return Sleeper(args, self.max_retries, self.retry_timeout, self.callback)
+
+
+class Sleeper(object):
+ """
+ For any given operation, a `Sleeper` object keeps count of the number of
+ retries. For each retry, the sleep time increases until the max number of
+ retries is reached and a `MaximumRetriesExceeded` is raised. The sleeper
+ object should be discarded once the operation is successful.
+ """
+
+ def __init__(self, args, max_retries, retry_timeout, callback):
+ self.args = args
+ self.retries = 0
+ self.max_retries = max_retries
+ self.retry_timeout = retry_timeout
+ self.callback = callback
+
+ def sleep(self, min_time=0):
+ """
+ Sleep a minimum of `min_time` seconds.
+ The actual sleeping time will increase with the number of retries.
+ """
+ self.retries += 1
+ if self.retries > self.max_retries:
+ raise MaximumRetriesExceeded(self, self.args)
+
+ self.callback(self, self.retries, self.args)
+
+ timeout = self.retry_timeout * (self.retries - 1)
+ if timeout < min_time:
+ timeout = min_time
+ log.debug('Sleeping for %d seconds', timeout)
+ time.sleep(timeout)
diff --git a/plugins/wiktionary/mwclient/util.py b/plugins/wiktionary/mwclient/util.py
new file mode 100644
index 0000000..bcd7b60
--- /dev/null
+++ b/plugins/wiktionary/mwclient/util.py
@@ -0,0 +1,7 @@
+import time
+
+
+def parse_timestamp(t):
+ if t is None or t == '0000-00-00T00:00:00Z':
+ return (0, 0, 0, 0, 0, 0, 0, 0, 0)
+ return time.strptime(t, '%Y-%m-%dT%H:%M:%SZ')