change version to 1.0.2-2
[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 import iso8601
37 import json
38 from StringIO import StringIO
39 import urllib
40 import urllib2
41
42 __author__ = 'Ian Weller <ian@ianweller.org>'
43 __version__ = '1.0.2'
44 DEFAULT_UA = ('python-simplemediawiki/%s '
45               '+https://github.com/ianweller/python-simplemediawiki') \
46         % __version__
47
48
49 class MediaWiki():
50     """
51     Class to represent a MediaWiki installation with an enabled API.
52
53     api_url: URL to api.php (usually similar to http://example.com/w/api.php)
54     """
55     _high_limits = None
56     _namespaces = None
57     _psuedo_namespaces = None
58
59     def __init__(self, api_url, cookie_file=None, user_agent=DEFAULT_UA):
60         self._api_url = api_url
61         if cookie_file:
62             self._cj = cookielib.MozillaCookieJar(cookie_file)
63             try:
64                 self._cj.load()
65             except IOError:
66                 self._cj.save()
67                 self._cj.load()
68         else:
69             self._cj = cookielib.CookieJar()
70         self._opener = urllib2.build_opener(
71                 urllib2.HTTPCookieProcessor(self._cj)
72         )
73         self._opener.addheaders = [('User-agent', user_agent)]
74
75     def _fetch_http(self, url, params):
76         """
77         Standard HTTP request handler for this class with gzip and cookie
78         support.
79         """
80         request = urllib2.Request(url, urllib.urlencode(params))
81         request.add_header('Accept-encoding', 'gzip')
82         response = self._opener.open(request)
83         if isinstance(self._cj, cookielib.MozillaCookieJar):
84             self._cj.save()
85         if response.headers.get('Content-Encoding') == 'gzip':
86             compressed = StringIO(response.read())
87             gzipper = gzip.GzipFile(fileobj=compressed)
88             data = gzipper.read()
89         else:
90             data = response.read()
91         return data
92
93     def call(self, params):
94         """
95         Make a call to the wiki. Returns a dictionary that represents the JSON
96         returned by the API.
97         """
98         params['format'] = 'json'
99         return json.loads(self._fetch_http(self._api_url, params))
100
101     def normalize_api_url(self):
102         """
103         This function checks the given URL for a correct API endpoint and
104         returns that URL, while also helpfully setting this object's API URL to
105         it. If it can't magically conjure an API endpoint, it returns False.
106         """
107         def tester(self, api_url):
108             """
109             Attempts to fetch general information about the MediaWiki instance
110             in order to test whether the given URL will return JSON.
111             """
112             data = self._fetch_http(api_url, {'action': 'query',
113                                               'meta': 'siteinfo'})
114             try:
115                 data_json = json.loads(data)
116                 return (data, data_json)
117             except ValueError:
118                 return (data, None)
119
120         data, data_json = tester(self, self._api_url)
121         if data_json:
122             return self._api_url
123         else:
124             # if there's an index.php in the URL, we might find the API
125             if 'index.php' in self._api_url:
126                 test_api_url = self._api_url.split('index.php')[0] + 'api.php'
127                 print test_api_url
128                 test_data, test_data_json = tester(self, test_api_url)
129                 print (test_data, test_data_json)
130                 if test_data_json:
131                     self._api_url = test_api_url
132                     return self._api_url
133             return False
134
135
136     def login(self, user, passwd, token=None):
137         """
138         Convenience function for logging into the wiki. It should never be
139         necessary to provide a token argument; it is part of the login process
140         since MediaWiki 1.15.3 (see MediaWiki bug 23076).
141         """
142         data = {'action': 'login',
143                 'lgname': user,
144                 'lgpassword': passwd}
145         if token:
146             data['lgtoken'] = token
147         result = self.call(data)
148         if result['login']['result'] == 'Success':
149             self._high_limits = None
150             return True
151         elif result['login']['result'] == 'NeedToken' and not token:
152             return self.login(user, passwd, result['login']['token'])
153         else:
154             return False
155
156     def logout(self):
157         """
158         Conveinence function for logging out of the wiki.
159         """
160         data = {'action': 'logout'}
161         self.call(data)
162         self._high_limits = None
163         return True
164
165     def limits(self, low, high):
166         """
167         Convenience function for determining appropriate limits in the API. If
168         the logged in user has the "apihighlimits" right, it will return the
169         high argument; otherwise it will return the low argument.
170         """
171         if self._high_limits == None:
172             result = self.call({'action': 'query',
173                                 'meta': 'userinfo',
174                                 'uiprop': 'rights'})
175             self._high_limits = 'apihighlimits' in \
176                     result['query']['userinfo']['rights']
177         if self._high_limits:
178             return high
179         else:
180             return low
181
182     def namespaces(self, psuedo=True):
183         """
184         Fetches a list of namespaces for this wiki.
185         """
186         if self._namespaces == None:
187             result = self.call({'action': 'query',
188                                 'meta': 'siteinfo',
189                                 'siprop': 'namespaces'})
190             self._namespaces = {}
191             self._psuedo_namespaces = {}
192             for nsid in result['query']['namespaces']:
193                 if int(nsid) >= 0:
194                     self._namespaces[int(nsid)] = \
195                             result['query']['namespaces'][nsid]['*']
196                 else:
197                     self._psuedo_namespaces[int(nsid)] = \
198                             result['query']['namespaces'][nsid]['*']
199         if psuedo:
200             retval = {}
201             retval.update(self._namespaces)
202             retval.update(self._psuedo_namespaces)
203             return retval
204         else:
205             return self._namespaces
206
207     @staticmethod
208     def parse_date(date):
209         """
210         Converts dates provided by the MediaWiki API into datetime.datetime
211         objects.
212         """
213         return iso8601.parse_date(date)

Benjamin Mako Hill || Want to submit a patch?