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

Benjamin Mako Hill || Want to submit a patch?