Commit f4cd1f8d0b3fc45801ceeec3769ad79c95fb74a7
1 parent
e0bbbfaf
Exists in
dev
Add of an unofficial version of plantuml which is fixed (the error which was occ…
…uring is commented in the file)
Showing
1 changed file
with
234 additions
and
0 deletions
Show diff stats
... | ... | @@ -0,0 +1,234 @@ |
1 | +#!/usr/bin/env python | |
2 | + | |
3 | +from __future__ import print_function | |
4 | + | |
5 | +import base64 | |
6 | +import string | |
7 | +from argparse import ArgumentParser | |
8 | +from io import open | |
9 | +from os import environ, path, makedirs | |
10 | +from zlib import compress | |
11 | + | |
12 | +import httplib2 | |
13 | +import six | |
14 | +from six.moves.urllib.parse import urlencode | |
15 | + | |
16 | +if six.PY2: | |
17 | + from string import maketrans | |
18 | +else: | |
19 | + maketrans = bytes.maketrans | |
20 | + | |
21 | +__version__ = 0, 3, 0 | |
22 | +__version_string__ = '.'.join(str(x) for x in __version__) | |
23 | + | |
24 | +__author__ = 'Doug Napoleone, Samuel Marks, Eric Frederich' | |
25 | +__email__ = 'doug.napoleone+plantuml@gmail.com' | |
26 | + | |
27 | + | |
28 | +plantuml_alphabet = string.digits + string.ascii_uppercase + string.ascii_lowercase + '-_' | |
29 | +base64_alphabet = string.ascii_uppercase + string.ascii_lowercase + string.digits + '+/' | |
30 | +b64_to_plantuml = maketrans(base64_alphabet.encode('utf-8'), plantuml_alphabet.encode('utf-8')) | |
31 | + | |
32 | + | |
33 | +class PlantUMLError(Exception): | |
34 | + """ | |
35 | + Error in processing. | |
36 | + """ | |
37 | + pass | |
38 | + | |
39 | + | |
40 | +class PlantUMLConnectionError(PlantUMLError): | |
41 | + """ | |
42 | + Error connecting or talking to PlantUML Server. | |
43 | + """ | |
44 | + pass | |
45 | + | |
46 | + | |
47 | +class PlantUMLHTTPError(PlantUMLConnectionError): | |
48 | + """ | |
49 | + Request to PlantUML server returned HTTP Error. | |
50 | + """ | |
51 | + | |
52 | + def __init__(self, response, content, *args, **kwdargs): | |
53 | + # Solving issue with message following the solution on the PR : https://github.com/dougn/python-plantuml/pull/9 | |
54 | + #super(PlantUMLConnectionError, self).__init__(*args, **kwdargs) | |
55 | + self.response = response | |
56 | + self.content = content | |
57 | + #if not self.message: | |
58 | + if not getattr(self,'message',None): | |
59 | + self.message = "%d: %s" % ( | |
60 | + self.response.status, self.response.reason) | |
61 | + super(PlantUMLConnectionError, self).__init__(*args, **kwdargs) | |
62 | + | |
63 | + | |
64 | +def deflate_and_encode(plantuml_text): | |
65 | + """zlib compress the plantuml text and encode it for the plantuml server. | |
66 | + """ | |
67 | + zlibbed_str = compress(plantuml_text.encode('utf-8')) | |
68 | + compressed_string = zlibbed_str[2:-4] | |
69 | + return base64.b64encode(compressed_string).translate(b64_to_plantuml).decode('utf-8') | |
70 | + | |
71 | + | |
72 | +class PlantUML(object): | |
73 | + """Connection to a PlantUML server with optional authentication. | |
74 | + | |
75 | + All parameters are optional. | |
76 | + | |
77 | + :param str url: URL to the PlantUML server image CGI. defaults to | |
78 | + http://www.plantuml.com/plantuml/img/ | |
79 | + :param dict basic_auth: This is if the plantuml server requires basic HTTP | |
80 | + authentication. Dictionary containing two keys, 'username' | |
81 | + and 'password', set to appropriate values for basic HTTP | |
82 | + authentication. | |
83 | + :param dict form_auth: This is for plantuml server requires a cookie based | |
84 | + webform login authentication. Dictionary containing two | |
85 | + primary keys, 'url' and 'body'. The 'url' should point to | |
86 | + the login URL for the server, and the 'body' should be a | |
87 | + dictionary set to the form elements required for login. | |
88 | + The key 'method' will default to 'POST'. The key 'headers' | |
89 | + defaults to | |
90 | + {'Content-type':'application/x-www-form-urlencoded'}. | |
91 | + Example: form_auth={'url': 'http://example.com/login/', | |
92 | + 'body': { 'username': 'me', 'password': 'secret'} | |
93 | + :param dict http_opts: Extra options to be passed off to the | |
94 | + httplib2.Http() constructor. | |
95 | + :param dict request_opts: Extra options to be passed off to the | |
96 | + httplib2.Http().request() call. | |
97 | + | |
98 | + """ | |
99 | + | |
100 | + def __init__(self, url, basic_auth={}, form_auth={}, | |
101 | + http_opts={}, request_opts={}): | |
102 | + self.HttpLib2Error = httplib2.HttpLib2Error | |
103 | + self.url = url | |
104 | + self.request_opts = request_opts | |
105 | + self.auth_type = 'basic_auth' if basic_auth else ( | |
106 | + 'form_auth' if form_auth else None) | |
107 | + self.auth = basic_auth if basic_auth else ( | |
108 | + form_auth if form_auth else None) | |
109 | + | |
110 | + # Proxify | |
111 | + try: | |
112 | + from urlparse import urlparse | |
113 | + import socks | |
114 | + | |
115 | + proxy_uri = urlparse(environ.get('HTTPS_PROXY', environ.get('HTTP_PROXY'))) | |
116 | + if proxy_uri: | |
117 | + proxy = {'proxy_info': httplib2.ProxyInfo(socks.PROXY_TYPE_HTTP, | |
118 | + proxy_uri.hostname, proxy_uri.port)} | |
119 | + http_opts.update(proxy) | |
120 | + self.request_opts.update(proxy) | |
121 | + except ImportError: | |
122 | + pass | |
123 | + | |
124 | + self.http = httplib2.Http(**http_opts) | |
125 | + | |
126 | + if self.auth_type == 'basic_auth': | |
127 | + self.http.add_credentials( | |
128 | + self.auth['username'], self.auth['password']) | |
129 | + elif self.auth_type == 'form_auth': | |
130 | + if 'url' not in self.auth: | |
131 | + raise PlantUMLError( | |
132 | + "The form_auth option 'url' must be provided and point to " | |
133 | + "the login url.") | |
134 | + if 'body' not in self.auth: | |
135 | + raise PlantUMLError( | |
136 | + "The form_auth option 'body' must be provided and include " | |
137 | + "a dictionary with the form elements required to log in. " | |
138 | + "Example: form_auth={'url': 'http://example.com/login/', " | |
139 | + "'body': { 'username': 'me', 'password': 'secret'}") | |
140 | + login_url = self.auth['url'] | |
141 | + body = self.auth['body'] | |
142 | + method = self.auth.get('method', 'POST') | |
143 | + headers = self.auth.get( | |
144 | + 'headers', {'Content-type': 'application/x-www-form-urlencoded'}) | |
145 | + try: | |
146 | + response, content = self.http.request( | |
147 | + login_url, method, headers=headers, | |
148 | + body=urlencode(body)) | |
149 | + except self.HttpLib2Error as e: | |
150 | + raise PlantUMLConnectionError(e) | |
151 | + if response.status != 200: | |
152 | + raise PlantUMLHTTPError(response, content) | |
153 | + self.request_opts['Cookie'] = response['set-cookie'] | |
154 | + | |
155 | + def get_url(self, plantuml_text): | |
156 | + """Return the server URL for the image. | |
157 | + You can use this URL in an IMG HTML tag. | |
158 | + | |
159 | + :param str plantuml_text: The plantuml markup to render | |
160 | + :returns: the plantuml server image URL | |
161 | + """ | |
162 | + return self.url + deflate_and_encode(plantuml_text) | |
163 | + | |
164 | + def processes(self, plantuml_text): | |
165 | + """Processes the plantuml text into the raw PNG image data. | |
166 | + | |
167 | + :param str plantuml_text: The plantuml markup to render | |
168 | + :returns: the raw image data | |
169 | + """ | |
170 | + url = self.get_url(plantuml_text) | |
171 | + try: | |
172 | + response, content = self.http.request(url, **self.request_opts) | |
173 | + except self.HttpLib2Error as e: | |
174 | + raise PlantUMLConnectionError(e) | |
175 | + if response.status != 200: | |
176 | + raise PlantUMLHTTPError(response, content) | |
177 | + return content | |
178 | + | |
179 | + def processes_file(self, filename, outfile=None, errorfile=None, directory=''): | |
180 | + """Take a filename of a file containing plantuml text and processes | |
181 | + it into a .png image. | |
182 | + | |
183 | + :param str filename: Text file containing plantuml markup | |
184 | + :param str outfile: Filename to write the output image to. If not | |
185 | + supplied, then it will be the input filename with the | |
186 | + file extension replaced with '.png'. | |
187 | + :param str errorfile: Filename to write server html error page | |
188 | + to. If this is not supplined, then it will be the | |
189 | + input ``filename`` with the extension replaced with | |
190 | + '_error.html'. | |
191 | + :returns: ``True`` if the image write succedded, ``False`` if there was | |
192 | + an error written to ``errorfile``. | |
193 | + """ | |
194 | + if outfile is None: | |
195 | + outfile = path.splitext(filename)[0] + '.png' | |
196 | + if errorfile is None: | |
197 | + errorfile = path.splitext(filename)[0] + '_error.html' | |
198 | + if directory and not path.exists(directory): | |
199 | + makedirs(directory) | |
200 | + data = open(filename).read() | |
201 | + try: | |
202 | + content = self.processes(data) | |
203 | + except PlantUMLHTTPError as e: | |
204 | + # added wb when oppening file (in official version it will open the file with usual format (not bytes) | |
205 | + err = open(path.join(directory, errorfile), 'wb') | |
206 | + err.write(e.content) | |
207 | + err.close() | |
208 | + return False | |
209 | + out = open(path.join(directory, outfile), 'wb') | |
210 | + out.write(content) | |
211 | + out.close() | |
212 | + return True | |
213 | + | |
214 | + | |
215 | +def _build_parser(): | |
216 | + parser = ArgumentParser(description='Generate images from plantuml defined files using plantuml server') | |
217 | + parser.add_argument('files', metavar='filename', nargs='+', | |
218 | + help='file(s) to generate images from') | |
219 | + parser.add_argument('-o', '--out', default='', | |
220 | + help='directory to put the files into') | |
221 | + parser.add_argument('-s', '--server', default='http://www.plantuml.com/plantuml/img/', | |
222 | + help='server to generate from, defaults to "http://www.plantuml.com/plantuml/img/"') | |
223 | + return parser | |
224 | + | |
225 | + | |
226 | +def main(): | |
227 | + args = _build_parser().parse_args() | |
228 | + pl = PlantUML(args.server) | |
229 | + print(list(map(lambda filename: {'filename': filename, | |
230 | + 'gen_success': pl.processes_file(filename, directory=args.out)}, args.files))) | |
231 | + | |
232 | + | |
233 | +if __name__ == '__main__': | |
234 | + main() | ... | ... |