Add logout function, reset high limits on auth
[python-simplemediawiki.debian] / simplemediawiki.py
1 # python-simplemediawiki - Extremely low-level wrapper to the MediaWiki API
2 # Copyright (C) 2010 Red Hat, Inc.
3 #
4 # This library is free software; you can redistribute it and/or modify it under
5 # the terms of the GNU Lesser General Public License as published by the Free
6 # Software Foundation; either version 2.1 of the License, or (at your option)
7 # any later version.
8 #
9 # This library is distributed in the hope that it will be useful, but WITHOUT
10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
12 # details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 """
18 simplemediawiki is an extremely low-level wrapper to the MediaWiki API. It
19 automatically handles cookies and gzip compression so that you can make basic
20 calls to the API in the easiest way possible. It also provides a few functions
21 to make day-to-day API access easier.
22
23 To use this module, instantiate a MediaWiki object, passing it the URL of
24 api.php for the wiki you want to work with. Calls go through MediaWiki.call().
25 A generic login wrapper as well as functions to determine limits and get a list
26 of namespaces are provided for your convenience.
27
28 >>> from simplemediawiki import MediaWiki
29 >>> wiki = MediaWiki('http://en.wikipedia.org/w/api.php')
30 >>> wiki.call({'action': 'query', 'prop': 'revisions', 'titles': 'Main Page'})
31 {u'query': {u'pages': {...}}}
32 """
33
34 import cookielib
35 import gzip
36 from iso8601 import iso8601
37 import json
38 from StringIO import StringIO
39 import urllib
40 import urllib2
41
42
43 class MediaWiki():
44     """
45     Class to represent a MediaWiki installation with an enabled API.
46
47     api_url: URL to api.php (usually similar to http://example.com/w/api.php)
48     """
49     _high_limits = None
50     _namespaces = None
51     _psuedo_namespaces = None
52
53     def __init__(self, api_url, cookie_file=None, user_agent=DEFAULT_UA):
54         self._api_url = api_url
55         if cookie_file:
56             self._cj = cookielib.MozillaCookieJar(cookie_file)
57             try:
58                 self._cj.load()
59             except IOError:
60                 self._cj.save()
61                 self._cj.load()
62         else:
63             self._cj = cookielib.CookieJar()
64         self._opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self._cj))
65         self._opener.addheaders = [('User-agent', user_agent)]
66
67     def _fetch_http(self, url, params):
68         request = urllib2.Request(url, urllib.urlencode(params))
69         request.add_header('Accept-encoding', 'gzip')
70         response = self._opener.open(request)
71         if isinstance(self._cj, cookielib.MozillaCookieJar):
72             self._cj.save()
73         if response.headers.get('Content-Encoding') == 'gzip':
74             compressed = StringIO(response.read())
75             gzipper = gzip.GzipFile(fileobj=compressed)
76             data = gzipper.read()
77         else:
78             data = response.read()
79         return data
80
81     def call(self, params):
82         """
83         Make a call to the wiki. Returns a dictionary that represents the JSON
84         returned by the API.
85         """
86         params['format'] = 'json'
87         return json.loads(self._fetch_http(self._api_url, params))
88
89     def normalize_api_url(self):
90         """
91         This function checks the given URL for a correct API endpoint and
92         returns that URL, while also helpfully setting this object's API URL to
93         it. If it can't magically conjure an API endpoint, it returns False.
94         """
95         data, data_json = self._normalize_api_url_tester(self._api_url)
96         if data_json:
97             return self._api_url
98         else:
99             # if there's an index.php in the URL, we might find the API
100             if 'index.php' in self._api_url:
101                 test_api_url = self._api_url.split('index.php')[0] + 'api.php'
102                 print test_api_url
103                 test_data, test_data_json = \
104                         self._normalize_api_url_tester(test_api_url)
105                 print (test_data, test_data_json)
106                 if test_data_json:
107                     self._api_url = test_api_url
108                     return self._api_url
109             return False
110
111     def _normalize_api_url_tester(self, api_url):
112         data = self._fetch_http(api_url, {'action': 'query',
113                                           'meta': 'siteinfo',
114                                           'siprop': 'general',
115                                           'format': 'json'})
116         try:
117             data_json = json.loads(data)
118             # may as well set the version
119             try:
120                 version_string = data_json['query']['general']['generator']
121                 self._mediawiki_version = version_string.split(' ', 1)[1]
122             except KeyError:
123                 pass
124             return (data, data_json)
125         except ValueError:
126             return (data, None)
127
128     def login(self, user, passwd, token=None):
129         """
130         Convenience function for logging into the wiki. It should never be
131         necessary to provide a token argument; it is part of the login process
132         since MediaWiki 1.15.3 (see MediaWiki bug 23076).
133         """
134         data = {'action': 'login',
135                 'lgname': user,
136                 'lgpassword': passwd}
137         if token:
138             data['lgtoken'] = token
139         result = self.call(data)
140         if result['login']['result'] == 'Success':
141             self._high_limits = None
142             return True
143         elif result['login']['result'] == 'NeedToken' and not token:
144             return self.login(user, passwd, result['login']['token'])
145         else:
146             return False
147
148     def logout(self):
149         """
150         Conveinence function for logging out of the wiki.
151         """
152         data = {'action': 'logout'}
153         self.call(data)
154         self._high_limits = None
155         return True
156
157     def limits(self, low, high):
158         """
159         Convenience function for determining appropriate limits in the API. If
160         the logged in user has the "apihighlimits" right, it will return the
161         high argument; otherwise it will return the low argument.
162         """
163         if self._high_limits == None:
164             result = self.call({'action': 'query',
165                                 'meta': 'userinfo',
166                                 'uiprop': 'rights'})
167             self._high_limits = 'apihighlimits' in \
168                     result['query']['userinfo']['rights']
169         if self._high_limits:
170             return high
171         else:
172             return low
173
174     def namespaces(self, psuedo=True):
175         """
176         Fetches a list of namespaces for this wiki.
177         """
178         if self._namespaces == None:
179             result = self.call({'action': 'query',
180                                 'meta': 'siteinfo',
181                                 'siprop': 'namespaces'})
182             self._namespaces = {}
183             self._psuedo_namespaces = {}
184             for nsid in result['query']['namespaces']:
185                 if int(nsid) >= 0:
186                     self._namespaces[int(nsid)] = \
187                             result['query']['namespaces'][nsid]['*']
188                 else:
189                     self._psuedo_namespaces[int(nsid)] = \
190                             result['query']['namespaces'][nsid]['*']
191         if psuedo:
192             retval = {}
193             retval.update(self._namespaces)
194             retval.update(self._psuedo_namespaces)
195             return retval
196         else:
197             return self._namespaces
198
199     @staticmethod
200     def parse_date(date):
201         """
202         Converts dates provided by the MediaWiki API into datetime.datetime
203         objects.
204         """
205         return iso8601.parse_date(date)
206
207
208 __author__ = 'Ian Weller <ian@ianweller.org>'
209 __version__ = '1.0.1'
210 DEFAULT_UA = 'python-simplemediawiki/%s ' + \
211         '+https://github.com/ianweller/python-simplemediawiki' \
212         % __version__

Benjamin Mako Hill || Want to submit a patch?