summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xviwiki300
1 files changed, 300 insertions, 0 deletions
diff --git a/viwiki b/viwiki
new file mode 100755
index 0000000..5779654
--- /dev/null
+++ b/viwiki
@@ -0,0 +1,300 @@
1#!/usr/bin/env python2.7
2"""
3Wiki API and Editor
4by Mike Crute (mcrute@gmail.com)
5
6This module contains a full API for MoinMoin as well as a shim to download
7pages, present them in vim and upload the changes. Great care has been taken to
8ensure that this script relies only on the python standard library and that it
9is compatible with both Python 2.7+ and Python 3.0+
10
11This module is usually never on the pythonpath for a system but can be loaded
12into other python scripts for use by copying this function into their code.
13
14def get_wiki(path):
15 import imp, os.path
16 return imp.load_module('wiki', open(os.path.expanduser(path)), path,
17 [s for s in imp.get_suffixes() if s[0] == '.py'][0])
18
19To get a programatic handle to the API create a class like this which will
20parse ~/.wikivimrc and use the login info from the [wiki_name] key.
21
22>>> wiki = get_wiki('~/bin/viwiki')
23>>> api = wiki.WikiAPI(*wiki.get_credentials('wiki_name'))
24
25The API supports attachment handling but a separate program called wikiattach
26is used for managing attachments from the command line.
27"""
28
29import re
30import os
31import imp
32import sys
33import stat
34import os.path
35import tempfile
36import subprocess
37from codecs import open
38from optparse import OptionParser
39
40try:
41 from cStringIO import StringIO as BytesIO
42 from ConfigParser import SafeConfigParser
43 from xmlrpclib import ServerProxy, MultiCall, Fault, Binary
44except ImportError:
45 from io import BytesIO
46 from configparser import SafeConfigParser
47 from xmlrpc.client import ServerProxy, MultiCall, Fault, Binary
48
49
50def get_wiki_config(wiki_name=None):
51 """Parses wiki config file and provides config dictionary
52
53 Parses the ~/.wikivimrc file and returns a dictionary of config values for
54 the named wiki. If no wiki_name is passed the function will use the first
55 wiki defined in the file.
56 """
57 config = SafeConfigParser()
58 config.read(os.path.expanduser("~/.wikivimrc"))
59
60 section = wiki_name if wiki_name else config.sections()[0]
61
62 return dict(
63 (key, config.get(section, key))
64 for key in config.options(section))
65
66
67def get_wiki_api(wiki_name=None):
68 """Parses wiki config file and provides wiki API class
69
70 Parses the config file and looks for an `api` key in the wiki config. If
71 that is specified it must contain a string of wiki class name, colon, path
72 to wiki class file per the example below. That class will be imported and
73 returned, otherwise the default MoinMoin wiki API is returned.
74 """
75 config = get_wiki_config(wiki_name)
76 api = config.get("api", "WikiAPI").split(":")
77
78 if len(api) == 2:
79 module = imp.load_module(
80 'wiki_api', open(os.path.expanduser(api[1])), api[1],
81 [s for s in imp.get_suffixes() if s[0] == '.py'][0])
82 return getattr(module, api[0])
83 else:
84 return globals()[api[0]]
85
86
87def get_credentials(wiki_name=None):
88 """Parses wiki config file and provides credentials tuple
89
90 Parses the ~/.wikivimrc file and returns a credentials tuple suitable for
91 unpacking directly into to the WikiAPI constructor.
92
93 If no wiki_name is passed the function will use the first wiki defined in
94 the file.
95
96 The format of the file is:
97 [wikiname]
98 host = https://example.com/
99 username = foobear
100 password = secret
101 """
102 config = get_wiki_config(wiki_name)
103 return (config["host"], config.get("username"), config.get("password"))
104
105
106class WikiAPI(object):
107 """Low Level Wiki API
108
109 This class wraps the low level XMLRPC wiki interface and exposes higher
110 level methods that do specific tasks in a little bit more python friendly
111 way. This assumes a private wiki and requires credentials.
112 """
113
114 DEFAULT_FORMAT = "moin"
115
116 def __init__(self, host, username, password):
117 self.wiki = ServerProxy("{0}?action=xmlrpc2".format(host))
118 self.token = self.wiki.getAuthToken(username, password)
119
120 def __call__(self, method, *args):
121 proxy = MultiCall(self.wiki)
122 proxy.applyAuthToken(self.token)
123 getattr(proxy, method)(*args)
124 results = tuple(proxy())
125
126 if results[0] != "SUCCESS":
127 raise Exception("Authentication failed!")
128
129 return results[1]
130
131 def page_format(self, page, contents):
132 """Determine file format of a page
133 """
134 format = re.findall("#format (.*)", contents)
135 format = format[0] if len(format) else self.DEFAULT_FORMAT
136 return self.DEFAULT_FORMAT if format == "wiki" else format
137
138 def search(self, term):
139 # http://hg.moinmo.in/moin/1.9/file/tip/MoinMoin/xmlrpc/__init__.py#l683
140 return [page[0] for page in
141 self("searchPagesEx", term, "text", 0, False, 0, False)]
142
143 def get_page(self, page_name):
144 try:
145 return self("getPage", page_name).decode("utf-8")
146 except Fault as error:
147 if error.faultString == "No such page was found.":
148 return ""
149
150 def put_page(self, page_name, contents):
151 try:
152 return self("putPage", page_name, contents.encode("utf-8"))
153 except Fault as error:
154 print(error)
155 return False
156
157 def list_attachments(self, page_name):
158 return self("listAttachments", page_name)
159
160 def get_attachment(self, page_name, attachment):
161 return BytesIO(self("getAttachment", page_name, attachment).data)
162
163 def put_attachment(self, page_name, name, contents):
164 data = Binary(contents.read())
165 self("putAttachment", page_name, name, data)
166
167 def delete_attachment(self, page_name, attachment):
168 self("deleteAttachment", page_name, attachment)
169
170
171class WikiEditor(object):
172 """Wiki Page Command Line Editor
173
174 This class handles editing and updating wiki pages based on arguments
175 passed on the command line. It requires a connected WikiAPI instance to do
176 its dirty work.
177 """
178
179 EDITOR_OPTIONS = {
180 "vim": "vim -c ':set wrap spell ft={format}' {filename}",
181 }
182
183 def __init__(self, wiki_api, editor=None):
184 self.api = wiki_api
185 self.editor = editor if editor else os.environ.get("EDITOR", "vim")
186 self.editor = self.EDITOR_OPTIONS.get(self.editor, self.editor)
187
188 @staticmethod
189 def _get_tempfile():
190 _, filename = tempfile.mkstemp()
191 return filename, open(filename, "w", "utf-8")
192
193 @staticmethod
194 def _file_last_updated(filename):
195 return os.stat(filename).st_mtime
196
197 def download_page(self, page):
198 page_contents = self.api.get_page(page)
199
200 filename, open_file = self._get_tempfile()
201 format = self.api.page_format(page, page_contents)
202
203 with open_file as fh:
204 fh.write(page_contents)
205
206 return filename, format
207
208 def upload_page(self, page, filename, timestamp):
209 updated_last = self._file_last_updated(filename)
210
211 if timestamp == updated_last:
212 print("Nothing changed")
213 return
214
215 with open(filename, "r", "utf-8") as fh:
216 contents = fh.read()
217
218 try:
219 if not self.api.put_page(page, contents):
220 raise Exception("failed to write page")
221 except:
222 print("Failed to save page.")
223 print("Contents exist in {!r}".format(filename))
224 return
225 finally:
226 del contents
227
228 os.unlink(filename)
229
230 def edit_page(self, page):
231 filename, format = self.download_page(page)
232 updated_first = self._file_last_updated(filename)
233
234 subprocess.call(
235 self.editor.format(format=format, filename=filename),
236 shell=True)
237
238 self.upload_page(page, filename, updated_first)
239
240
241class NonInteractiveEditor(WikiEditor):
242 """Non-Interactive Wiki Page Editor
243
244 This class handles editing and updating wiki pages based on arguments
245 passed on the command line. It requires a connected WikiAPI instance to do
246 its dirty work.
247 """
248
249 @staticmethod
250 def get_input(arg=None):
251 # cat foo.txt | viwiki PageName
252 # viwiki PageName < foo.txt
253 if (stat.S_IFMT(os.fstat(sys.stdin.fileno()).st_mode)
254 in (stat.S_IFREG, stat.S_IFIFO)):
255 if hasattr(sys.stdin, 'buffer'):
256 return sys.stdin.buffer
257 else:
258 return sys.stdin
259
260 if not arg:
261 return None
262
263 # viwiki PageName <(cat foo.txt)
264 # viwiki PageName /path/to/foo.txt
265 if os.path.exists(arg):
266 return open(arg, 'rb')
267
268 def edit_page(self, args):
269 input = self.get_input(args[1] if len(args) == 2 else None)
270
271 if input:
272 self.api.put_page(args[0], input.read().decode("utf-8"))
273 else:
274 sys.stdout.write(self.api.get_page(args[0]).encode("utf-8"))
275 sys.stdout.flush()
276
277
278def main(default_wiki=None):
279 parser = OptionParser()
280 parser.add_option("-n", "--non-interactive", dest="interactive",
281 help="Don't start vim", action="store_false", default=True)
282 parser.add_option("-w", "--wiki", dest="wiki", default=default_wiki,
283 help="Wiki config to use")
284 options, args = parser.parse_args()
285
286 api_class = get_wiki_api(options.wiki)
287 api = api_class(*get_credentials(options.wiki))
288
289 if len(args) == 0:
290 print("usage: viwiki <page name>")
291 return 1
292
293 if options.interactive:
294 WikiEditor(api).edit_page(args[0])
295 else:
296 NonInteractiveEditor(api).edit_page(args)
297
298
299if __name__ == "__main__":
300 main()