Commit 34e2801d authored by Xin Huang's avatar Xin Huang Committed by Albert Astals Cid

Albert says: bunch of files i forgot to git add, sorry ^_^

parent 05eb1123
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.
.. 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.
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)
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.
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
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())
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
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
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
"""
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
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
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
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
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
# -*- 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)