Add client code from bcsaller
[osm/N2VC.git] / juju / client / facade.py
1 import argparse
2 import builtins
3 import functools
4 import json
5 import keyword
6 from pathlib import Path
7 import pprint
8 import textwrap
9 from typing import Sequence, Mapping, TypeVar, Any, Union, Optional
10 import typing
11
12 from . import codegen
13
14 _marker = object()
15
16 # Map basic types to Python's typing with a callable
17 SCHEMA_TO_PYTHON = {
18 'string': str,
19 'integer': int,
20 'float': float,
21 'number': float,
22 'boolean': bool,
23 'object': Any,
24 }
25
26
27 class KindRegistry(dict):
28 def register(self, name, version, obj):
29 self[name] = {version: {
30 "object": obj,
31 }}
32
33 def lookup(self, name, version=None):
34 """If version is omitted, max version is used"""
35 versions = self.get(name)
36 if not versions:
37 return None
38 if version:
39 return versions[version]
40 return versions[max(versions)]
41
42 def getObj(self, name, version=None):
43 result = self.lookup(name, version)
44 if result:
45 obj = result["object"]
46 return obj
47 return None
48
49
50 class TypeRegistry(dict):
51 def get(self, name):
52 # Two way mapping
53 refname = Schema.referenceName(name)
54 if refname not in self:
55 result = TypeVar(refname)
56 self[refname] = result
57 self[result] = refname
58
59 return self[refname]
60
61 _types = TypeRegistry()
62 _registry = KindRegistry()
63 classes = {}
64
65 def booler(v):
66 if isinstance(v, str):
67 if v == "false":
68 return False
69 return bool(v)
70
71
72 def getRefType(ref):
73 return _types.get(ref)
74
75
76 def refType(obj):
77 return getRefType(obj["$ref"])
78
79
80 def objType(obj):
81 kind = obj.get('type')
82 if not kind:
83 raise ValueError("%s has no type" % obj)
84 result = SCHEMA_TO_PYTHON.get(kind)
85 if not result:
86 raise ValueError("%s has type %s" % (obj, kind))
87 return result
88
89
90 basic_types = [str, bool, int, float]
91
92
93 def name_to_py(name):
94 result = name.replace("-", "_")
95 result = result.lower()
96 if keyword.iskeyword(result) or result in dir(builtins):
97 result += "_"
98 return result
99
100
101 def strcast(kind, keep_builtins=False):
102 if issubclass(kind, typing.GenericMeta):
103 return str(kind)[1:]
104 if (kind in basic_types or
105 type(kind) in basic_types) and keep_builtins is False:
106 return kind.__name__
107 return kind
108
109
110 class Args(list):
111 def __init__(self, defs):
112 self.defs = defs
113 #self.append("self")
114 if defs:
115 rtypes = _registry.getObj(_types[defs])
116 if len(rtypes) == 1:
117 if not self.do_explode(rtypes[0][1]):
118 for name, rtype in rtypes:
119 self.append((name, rtype))
120 else:
121 for name, rtype in rtypes:
122 self.append((name, rtype))
123
124 def do_explode(self, kind):
125 if kind in basic_types:
126 return False
127 if not issubclass(kind, (typing.Sequence,
128 typing.Mapping)):
129 self.clear()
130 self.extend(Args(kind))
131 return True
132 return False
133
134 def PyToSchemaMapping(self):
135 m = {}
136 for n, rt in self:
137 m[name_to_py(n)] = n
138 return m
139
140 def SchemaToPyMapping(self):
141 m = {}
142 for n, tr in self:
143 m[n] = name_to_py(n)
144 return m
145
146 def _format(self, name, rtype, typed=True):
147 if typed:
148 return "{} : {}".format(
149 name_to_py(name),
150 strcast(rtype)
151 )
152 else:
153 return name_to_py(name)
154
155 def _get_arg_str(self, typed=False, joined=", "):
156 if self:
157 parts = []
158 for item in self:
159 parts.append(self._format(item[0], item[1], typed))
160 if joined:
161 return joined.join(parts)
162 return parts
163 return ''
164
165 def typed(self):
166 return self._get_arg_str(True)
167
168 def __str__(self):
169 return self._get_arg_str(False)
170
171 def get_doc(self):
172 return self._get_arg_str(True, "\n")
173
174
175 def buildTypes(schema, capture):
176 global classes
177 INDENT = " "
178 for kind in sorted((k for k in _types if not isinstance(k, str)),
179 key=lambda x: str(x)):
180 name = _types[kind]
181 args = Args(kind)
182 if name in classes:
183 continue
184 source = ["""
185 class {}(Type):
186 _toSchema = {}
187 _toPy = {}
188 def __init__(self{}{}):
189 '''
190 {}
191 '''""".format(name,
192 args.PyToSchemaMapping(),
193 args.SchemaToPyMapping(),
194 ", " if args else "",
195 args,
196 textwrap.indent(args.get_doc(), INDENT *2))
197 #pprint.pformat(schema['definitions'][name]))
198 ]
199 assignments = args._get_arg_str(False, False)
200 for assign in assignments:
201 source.append("{}self.{} = {}".format(INDENT * 2, assign, assign))
202 if not assignments:
203 source.append("{}pass".format(INDENT *2))
204 source = "\n".join(source)
205 capture.write(source)
206 capture.write("\n\n")
207 co = compile(source, __name__, "exec")
208 ns = _getns()
209 exec(co, ns)
210 cls = ns[name]
211 classes[name] = cls
212
213
214 def retspec(defs):
215 # return specs
216 # only return 1, so if there is more than one type
217 # we need to include a union
218 # In truth there is only 1 return
219 # Error or the expected Type
220 if not defs:
221 return None
222 rtypes = _registry.getObj(_types[defs])
223 if not rtypes:
224 return None
225 if len(rtypes) > 1:
226 return Union[tuple([strcast(r[1], True) for r in rtypes])]
227 return strcast(rtypes[0][1], False)
228
229
230 def return_type(defs):
231 if not defs:
232 return None
233 rtypes = _registry.getObj(_types[defs])
234 if not rtypes:
235 return None
236 if len(rtypes) > 1:
237 for n, t in rtypes:
238 if n == "Error":
239 continue
240 return t
241 return rtypes[0][1]
242
243
244 def type_anno_func(func, defs, is_result=False):
245 annos = {}
246 if not defs:
247 return func
248 rtypes = _registry.getObj(_types[defs])
249 if is_result:
250 kn = "return"
251 if not rtypes:
252 annos[kn] = None
253 elif len(rtypes) > 1:
254 annos[kn] = Union[tuple([r[1] for r in rtypes])]
255 else:
256 annos[kn] = rtypes[0][1]
257 else:
258 for name, rtype in rtypes:
259 name = name_to_py(name)
260 annos[name] = rtype
261 func.__annotations__.update(annos)
262 return func
263
264
265 def ReturnMapping(cls):
266 # Annotate the method with a return Type
267 # so the value can be cast
268 def decorator(f):
269 @functools.wraps(f)
270 def wrapper(*args, **kwargs):
271 reply = f(*args, **kwargs)
272 if cls is None or reply:
273 return reply
274 if 'Error' in reply:
275 cls = Error
276 if issubclass(cls, typing.Sequence):
277 result = []
278 for item in reply:
279 result.append(cls.from_json(item))
280 else:
281 result = cls.from_json(reply)
282
283 return result
284 return wrapper
285 return decorator
286
287
288 def makeFunc(cls, name, params, result, async=True):
289 INDENT = " "
290 args = Args(params)
291 assignments = []
292 toschema = args.PyToSchemaMapping()
293 for arg in args._get_arg_str(False, False):
294 assignments.append("{}params[\'{}\'] = {}".format(INDENT,
295 toschema[arg],
296 arg))
297 assignments = "\n".join(assignments)
298 res = retspec(result)
299 source = """
300
301 #@ReturnMapping({rettype})
302 {async}def {name}(self{argsep}{args}):
303 '''
304 {docstring}
305 Returns -> {res}
306 '''
307 # map input types to rpc msg
308 params = dict()
309 msg = dict(Type='{cls.name}', Request='{name}', Version={cls.version}, Params=params)
310 {assignments}
311 reply = {await}self.rpc(msg)
312 return self._map(reply, {name})
313
314 """
315
316 fsource = source.format(async="async " if async else "",
317 name=name,
318 argsep=", " if args else "",
319 args=args,
320 #ressep= " -> " if res else "",
321 res=res,
322 rettype=result.__name__ if result else None,
323 docstring=textwrap.indent(args.get_doc(), INDENT),
324 cls=cls,
325 assignments=assignments,
326 await="await " if async else "")
327 ns = _getns()
328 exec(fsource, ns)
329 func = ns[name]
330 return func, fsource
331
332
333 def buildMethods(cls, capture):
334 properties = cls.schema['properties']
335 for methodname in sorted(properties):
336 method, source = _buildMethod(cls, methodname)
337 setattr(cls, methodname, method)
338 capture.write(source, depth=1)
339
340
341 def _buildMethod(cls, name):
342 params = None
343 result = None
344 method = cls.schema['properties'][name]
345 if 'properties' in method:
346 prop = method['properties']
347 spec = prop.get('Params')
348 if spec:
349 params = _types.get(spec['$ref'])
350 spec = prop.get('Result')
351 if spec:
352 result = _types.get(spec['$ref'])
353 return makeFunc(cls, name, params, result)
354
355
356 def buildFacade(schema):
357 cls = type(schema.name, (Type,), dict(name=schema.name,
358 version=schema.version,
359 schema=schema))
360 source = """
361 class {name}(Type):
362 name = '{name}'
363 version = {version}
364 schema = {schema}
365 """.format(name=schema.name,
366 version=schema.version,
367 schema=textwrap.indent(pprint.pformat(schema), " "))
368 return cls, source
369
370
371 class Type:
372 def connect(self, connection):
373 self.connection = connection
374
375 async def rpc(self, msg):
376 result = await self.connection.rpc(msg)
377 return result
378
379 def _map(self, reply, method):
380 # Error, expected return or None
381 if not reply:
382 return None
383
384 if 'Error' in reply:
385 retcls = classes['Error']
386 data = reply['Error']
387 classes["Error"]
388 elif 'Response' in reply:
389 retcls = method.__return_type__
390 data = reply['Response']
391 return retcls.from_json(data)
392
393 @classmethod
394 def from_json(cls, data):
395 if isinstance(data, str):
396 data = json.loads(data)
397 return cls(**data)
398
399 def serialize(self):
400 d = {}
401 for attr, tgt in self._toSchema.items():
402 d[tgt] = getattr(self, attr)
403 return d
404
405 def to_json(self):
406 return json.dumps(self.serialize())
407
408
409 class Schema(dict):
410 def __init__(self, schema):
411 self.name = schema['Name']
412 self.version = schema['Version']
413 self.update(schema['Schema'])
414
415 @classmethod
416 def referenceName(cls, ref):
417 if ref.startswith("#/definitions/"):
418 ref = ref.rsplit("/", 1)[-1]
419 return ref
420
421 def resolveDefinition(self, ref):
422 return self['definitions'][self.referenceName(ref)]
423
424 def deref(self, prop, name):
425 if not isinstance(prop, dict):
426 raise TypeError(prop)
427 if "$ref" not in prop:
428 return prop
429
430 target = self.resolveDefinition(prop["$ref"])
431 return target
432
433 def buildDefinitions(self):
434 # here we are building the types out
435 # anything in definitions is a type
436 # but these may contain references themselves
437 # so we dfs to the bottom and build upwards
438 # when a types is already in the registry
439 defs = self.get('definitions')
440 if not defs:
441 return
442 for d, data in defs.items():
443 if d in _registry:
444 continue
445 node = self.deref(data, d)
446 kind = node.get("type")
447 if kind == "object":
448 result = self.buildObject(node, d)
449 elif kind == "array":
450 pass
451 _registry.register(d, self.version, result)
452
453 def buildObject(self, node, name=None, d=0):
454 # we don't need to build types recursively here
455 # they are all in definitions already
456 # we only want to include the type reference
457 # which we can derive from the name
458 struct = []
459 add = struct.append
460 props = node.get("properties")
461 pprops = node.get("patternProperties")
462 if props:
463 for p, prop in props.items():
464 if "$ref" in prop:
465 add((p, refType(prop)))
466 else:
467 kind = prop['type']
468 if kind == "array":
469 add((p, self.buildArray(prop, d + 1)))
470 elif kind == "object":
471 struct.extend(self.buildObject(prop, p, d + 1))
472 else:
473 add((p, objType(prop)))
474 if pprops:
475 if ".*" not in pprops:
476 raise ValueError(
477 "Cannot handle actual pattern in patterProperties %s" %
478 pprops)
479 pprop = pprops[".*"]
480 if "$ref" in pprop:
481 add((name, Mapping[str, refType(pprop)]))
482 return struct
483 ppkind = pprop["type"]
484 if ppkind == "array":
485 add((name, self.buildArray(pprop, d + 1)))
486 else:
487 add((name, Mapping[str, SCHEMA_TO_PYTHON[ppkind]]))
488 #print("{}{}".format(d * " ", struct))
489 return struct
490
491 def buildArray(self, obj, d=0):
492 # return a sequence from an array in the schema
493 if "$ref" in obj:
494 return Sequence[refType(obj)]
495 else:
496 kind = obj.get("type")
497 if kind and kind == "array":
498 items = obj['items']
499 return self.buildArray(items, d+1)
500 else:
501 return Sequence[objType(obj)]
502
503
504 def _getns():
505 ns = {'Type': Type,
506 'typing': typing,
507 'ReturnMapping': ReturnMapping
508 }
509 # Copy our types into the globals of the method
510 for facade in _registry:
511 ns[facade] = _registry.getObj(facade)
512 return ns
513
514
515
516 def generate_facacdes(options):
517 global classes
518 schemas = json.loads(Path(options.schema).read_text("utf-8"))
519 capture = codegen.CodeWriter()
520 capture.write("""
521 from libjuju.facade import Type, ReturnMapping
522 """)
523 schemas = [Schema(s) for s in schemas]
524
525 for schema in schemas:
526 schema.buildDefinitions()
527 buildTypes(schema, capture)
528
529 for schema in schemas:
530 # TODO generate class now with a metaclass that takes the schema
531 # the generated class has the right name and it in turn uses
532 # the metaclass to populate cls
533 cls, source = buildFacade(schema)
534 capture.write(source)
535 buildMethods(cls, capture)
536 classes[schema.name] = cls
537
538 return capture
539
540 def setup():
541 parser = argparse.ArgumentParser()
542 parser.add_argument("-s", "--schema", default="schemas.json")
543 parser.add_argument("-o", "--output", default="client.py")
544 options = parser.parse_args()
545 return options
546
547 def main():
548 options = setup()
549 capture = generate_facacdes(options)
550 with open(options.output, "w") as fp:
551 print(capture, file=fp)
552
553
554
555 if __name__ == '__main__':
556 main()