Mercurial > libervia-backend
annotate src/bridge/bridge_constructor/base_constructor.py @ 2444:30278ea1ca7c
plugin XEP-0060: added node watching methods to bridge:
new methods psNodeWatchAdd and psNodeWatchRemove allows to set a watch for the time of the session on one node, to have a signal called when something change on this node.
This signal (psEventRaw) send raw data (raw XML), in opposition to psEvent which is there to send high level data (e.g. parsed blog data).
Those method are primarely there to let frontends manage local cache for pubsub nodes.
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 19 Nov 2017 16:51:39 +0100 |
parents | 8b37a62336c3 |
children | 0046283a285d |
rev | line source |
---|---|
2085 | 1 #!/usr/bin/env python2 |
2 #-*- coding: utf-8 -*- | |
3 | |
4 # SàT: a XMPP client | |
2414
8b37a62336c3
misc: date update (yes it's a bit late :p )
Goffi <goffi@goffi.org>
parents:
2091
diff
changeset
|
5 # Copyright (C) 2009-2017 Jérôme Poisson (goffi@goffi.org) |
2085 | 6 |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 """base constructor class""" | |
21 | |
22 from sat.bridge.bridge_constructor.constants import Const as C | |
23 from ConfigParser import NoOptionError | |
24 import sys | |
25 import os | |
26 import os.path | |
27 import re | |
28 from importlib import import_module | |
29 | |
30 | |
31 class ParseError(Exception): | |
32 #Used when the signature parsing is going wrong (invalid signature ?) | |
33 pass | |
34 | |
35 | |
36 class Constructor(object): | |
37 NAME = None # used in arguments parsing, filename will be used if not set | |
38 # following attribute are used by default generation method | |
39 # they can be set to dict of strings using python formatting syntax | |
40 # dict keys will be used to select part to replace (e.g. "signals" key will | |
41 # replace ##SIGNALS_PART## in template), while the value is the format | |
42 # keys starting with "signal" will be used for signals, while ones starting with | |
43 # "method" will be used for methods | |
44 # check D-Bus constructor for an example | |
45 CORE_FORMATS = None | |
46 CORE_TEMPLATE = None | |
47 CORE_DEST = None | |
48 FRONTEND_FORMATS = None | |
49 FRONTEND_TEMPLATE = None | |
50 FRONTEND_DEST = None | |
51 | |
2087
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
52 # set to False if your bridge need only core |
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
53 FRONTEND_ACTIVATE = True |
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
54 |
2085 | 55 def __init__(self, bridge_template, options): |
56 self.bridge_template = bridge_template | |
57 self.args = options | |
58 | |
59 @property | |
60 def constructor_dir(self): | |
61 constructor_mod = import_module(self.__module__) | |
62 return os.path.dirname(constructor_mod.__file__) | |
63 | |
64 def getValues(self, name): | |
65 """Return values of a function in a dict | |
66 @param name: Name of the function to get | |
67 @return: dict, each key has the config value or None if the value is not set""" | |
68 function = {} | |
69 for option in ['type', 'category', 'sig_in', 'sig_out', 'doc']: | |
70 try: | |
71 value = self.bridge_template.get(name, option) | |
72 except NoOptionError: | |
73 value = None | |
74 function[option] = value | |
75 return function | |
76 | |
77 def getDefault(self, name): | |
78 """Return default values of a function in a dict | |
79 @param name: Name of the function to get | |
80 @return: dict, each key is the integer param number (no key if no default value)""" | |
81 default_dict = {} | |
82 def_re = re.compile(r"param_(\d+)_default") | |
83 | |
84 for option in self.bridge_template.options(name): | |
85 match = def_re.match(option) | |
86 if match: | |
87 try: | |
88 idx = int(match.group(1)) | |
89 except ValueError: | |
90 raise ParseError("Invalid value [%s] for parameter number" % match.group(1)) | |
91 default_dict[idx] = self.bridge_template.get(name, option) | |
92 | |
93 return default_dict | |
94 | |
95 def getFlags(self, name): | |
96 """Return list of flags set for this function | |
97 | |
98 @param name: Name of the function to get | |
99 @return: List of flags (string) | |
100 """ | |
101 flags = [] | |
102 for option in self.bridge_template.options(name): | |
103 if option in C.DECLARATION_FLAGS: | |
104 flags.append(option) | |
105 return flags | |
106 | |
107 def getArgumentsDoc(self, name): | |
108 """Return documentation of arguments | |
109 @param name: Name of the function to get | |
110 @return: dict, each key is the integer param number (no key if no argument doc), value is a tuple (name, doc)""" | |
111 doc_dict = {} | |
112 option_re = re.compile(r"doc_param_(\d+)") | |
113 value_re = re.compile(r"^(\w+): (.*)$", re.MULTILINE | re.DOTALL) | |
114 for option in self.bridge_template.options(name): | |
115 if option == 'doc_return': | |
116 doc_dict['return'] = self.bridge_template.get(name, option) | |
117 continue | |
118 match = option_re.match(option) | |
119 if match: | |
120 try: | |
121 idx = int(match.group(1)) | |
122 except ValueError: | |
123 raise ParseError("Invalid value [%s] for parameter number" % match.group(1)) | |
124 value_match = value_re.match(self.bridge_template.get(name, option)) | |
125 if not value_match: | |
126 raise ParseError("Invalid value for parameter doc [%i]" % idx) | |
127 doc_dict[idx] = (value_match.group(1), value_match.group(2)) | |
128 return doc_dict | |
129 | |
130 def getDoc(self, name): | |
131 """Return documentation of the method | |
132 @param name: Name of the function to get | |
133 @return: string documentation, or None""" | |
134 if self.bridge_template.has_option(name, "doc"): | |
135 return self.bridge_template.get(name, "doc") | |
136 return None | |
137 | |
138 def argumentsParser(self, signature): | |
139 """Generator which return individual arguments signatures from a global signature""" | |
140 start = 0 | |
141 i = 0 | |
142 | |
143 while i < len(signature): | |
144 if signature[i] not in ['b', 'y', 'n', 'i', 'x', 'q', 'u', 't', 'd', 's', 'a']: | |
145 raise ParseError("Unmanaged attribute type [%c]" % signature[i]) | |
146 | |
147 if signature[i] == 'a': | |
148 i += 1 | |
149 if signature[i] != '{' and signature[i] != '(': # FIXME: must manage tuples out of arrays | |
150 i += 1 | |
151 yield signature[start:i] | |
152 start = i | |
153 continue # we have a simple type for the array | |
154 opening_car = signature[i] | |
155 assert(opening_car in ['{', '(']) | |
156 closing_car = '}' if opening_car == '{' else ')' | |
157 opening_count = 1 | |
158 while (True): # we have a dict or a list of tuples | |
159 i += 1 | |
160 if i >= len(signature): | |
161 raise ParseError("missing }") | |
162 if signature[i] == opening_car: | |
163 opening_count += 1 | |
164 if signature[i] == closing_car: | |
165 opening_count -= 1 | |
166 if opening_count == 0: | |
167 break | |
168 i += 1 | |
169 yield signature[start:i] | |
170 start = i | |
171 | |
172 def getArguments(self, signature, name=None, default=None, unicode_protect=False): | |
173 """Return arguments to user given a signature | |
174 | |
175 @param signature: signature in the short form (using s,a,i,b etc) | |
2087
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
176 @param name: dictionary of arguments name like given by getArgumentsDoc |
2085 | 177 @param default: dictionary of default values, like given by getDefault |
178 @param unicode_protect: activate unicode protection on strings (return strings as unicode(str)) | |
2091
f413bfc24458
bridge, quick_frontend: preparation for async bridge
Goffi <goffi@goffi.org>
parents:
2087
diff
changeset
|
179 @return (str): arguments that correspond to a signature (e.g.: "sss" return "arg1, arg2, arg3") |
2085 | 180 """ |
181 idx = 0 | |
182 attr_string = [] | |
183 | |
184 for arg in self.argumentsParser(signature): | |
185 attr_string.append(("unicode(%(name)s)%(default)s" if (unicode_protect and arg == 's') else "%(name)s%(default)s") % { | |
186 'name': name[idx][0] if (name and idx in name) else "arg_%i" % idx, | |
187 'default': "=" + default[idx] if (default and idx in default) else ''}) | |
188 # give arg_1, arg2, etc or name1, name2=default, etc. | |
189 #give unicode(arg_1), unicode(arg_2), etc. if unicode_protect is set and arg is a string | |
190 idx += 1 | |
191 | |
192 return ", ".join(attr_string) | |
193 | |
194 def getTemplatePath(self, template_file): | |
195 """return template path corresponding to file name | |
196 | |
197 @param template_file(str): name of template file | |
198 """ | |
199 return os.path.join(self.constructor_dir, template_file) | |
200 | |
201 def core_completion_method(self, completion, function, default, arg_doc, async_): | |
202 """override this method to extend completion""" | |
203 pass | |
204 | |
205 def core_completion_signal(self, completion, function, default, arg_doc, async_): | |
206 """override this method to extend completion""" | |
207 pass | |
208 | |
209 def frontend_completion_method(self, completion, function, default, arg_doc, async_): | |
210 """override this method to extend completion""" | |
211 pass | |
212 | |
213 def frontend_completion_signal(self, completion, function, default, arg_doc, async_): | |
214 """override this method to extend completion""" | |
215 pass | |
216 | |
217 | |
218 def generate(self, side): | |
219 """generate bridge | |
220 | |
221 call generateCoreSide or generateFrontendSide if they exists | |
222 else call generic self._generate method | |
223 """ | |
224 try: | |
225 if side == "core": | |
226 method = self.generateCoreSide | |
227 elif side == "frontend": | |
2087
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
228 if not self.FRONTEND_ACTIVATE: |
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
229 print(u"This constructor only handle core, please use core side") |
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
230 sys.exit(1) |
2085 | 231 method = self.generateFrontendSide |
232 except AttributeError: | |
233 self._generate(side) | |
234 else: | |
235 method() | |
236 | |
237 def _generate(self, side): | |
238 """generate the backend | |
239 | |
240 this is a generic method which will use formats found in self.CORE_SIGNAL_FORMAT | |
241 and self.CORE_METHOD_FORMAT (standard format method will be used) | |
242 @param side(str): core or frontend | |
243 """ | |
244 side_vars = [] | |
245 for var in ('FORMATS', 'TEMPLATE', 'DEST'): | |
246 attr = "{}_{}".format(side.upper(), var) | |
247 value = getattr(self, attr) | |
248 if value is None: | |
249 raise NotImplementedError | |
250 side_vars.append(value) | |
251 | |
252 FORMATS, TEMPLATE, DEST = side_vars | |
253 del side_vars | |
254 | |
255 parts = {part.upper():[] for part in FORMATS} | |
256 sections = self.bridge_template.sections() | |
257 sections.sort() | |
258 for section in sections: | |
259 function = self.getValues(section) | |
260 print ("Adding %s %s" % (section, function["type"])) | |
261 default = self.getDefault(section) | |
262 arg_doc = self.getArgumentsDoc(section) | |
263 async_ = "async" in self.getFlags(section) | |
264 completion = { | |
265 'sig_in': function['sig_in'] or '', | |
266 'sig_out': function['sig_out'] or '', | |
267 'category': 'plugin' if function['category'] == 'plugin' else 'core', | |
268 'name': section, | |
2087
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
269 # arguments with default values |
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
270 'args': self.getArguments(function['sig_in'], name=arg_doc, default=default), |
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
271 } |
2085 | 272 |
273 extend_method = getattr(self, "{}_completion_{}".format(side, function["type"])) | |
274 extend_method(completion, function, default, arg_doc, async_) | |
275 | |
276 for part, fmt in FORMATS.iteritems(): | |
277 if part.startswith(function["type"]): | |
278 parts[part.upper()].append(fmt.format(**completion)) | |
279 | |
280 | |
281 #at this point, signals_part, methods_part and direct_calls should be filled, | |
282 #we just have to place them in the right part of the template | |
283 bridge = [] | |
284 const_override = {env[len(C.ENV_OVERRIDE):]:v for env,v in os.environ.iteritems() if env.startswith(C.ENV_OVERRIDE)} | |
285 template_path = self.getTemplatePath(TEMPLATE) | |
286 try: | |
287 with open(template_path) as template: | |
288 for line in template: | |
289 | |
290 for part, extend_list in parts.iteritems(): | |
291 if line.startswith('##{}_PART##'.format(part)): | |
292 bridge.extend(extend_list) | |
293 break | |
294 else: | |
295 # the line is not a magic part replacement | |
296 if line.startswith('const_'): | |
297 const_name = line[len('const_'):line.find(' = ')].strip() | |
298 if const_name in const_override: | |
299 print("const {} overriden".format(const_name)) | |
300 bridge.append('const_{} = {}'.format(const_name, const_override[const_name])) | |
301 continue | |
302 bridge.append(line.replace('\n', '')) | |
303 except IOError: | |
304 print ("can't open template file [{}]".format(template_path)) | |
305 sys.exit(1) | |
306 | |
307 #now we write to final file | |
308 self.finalWrite(DEST, bridge) | |
309 | |
310 def finalWrite(self, filename, file_buf): | |
311 """Write the final generated file in [dest dir]/filename | |
312 | |
313 @param filename: name of the file to generate | |
314 @param file_buf: list of lines (stings) of the file | |
315 """ | |
316 if os.path.exists(self.args.dest_dir) and not os.path.isdir(self.args.dest_dir): | |
317 print ("The destination dir [%s] can't be created: a file with this name already exists !") | |
318 sys.exit(1) | |
319 try: | |
320 if not os.path.exists(self.args.dest_dir): | |
321 os.mkdir(self.args.dest_dir) | |
322 full_path = os.path.join(self.args.dest_dir, filename) | |
323 if os.path.exists(full_path) and not self.args.force: | |
324 print ("The destination file [%s] already exists ! Use --force to overwrite it" % full_path) | |
325 try: | |
326 with open(full_path, 'w') as dest_file: | |
327 dest_file.write('\n'.join(file_buf)) | |
328 except IOError: | |
329 print ("Can't open destination file [%s]" % full_path) | |
330 except OSError: | |
331 print("It's not possible to generate the file, check your permissions") | |
332 exit(1) |