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

Benjamin Mako Hill || Want to submit a patch?