6 from pathlib
import Path
9 from typing
import Sequence
, Mapping
, TypeVar
, Any
, Union
, Optional
16 # Map basic types to Python's typing with a callable
27 class KindRegistry(dict):
28 def register(self
, name
, version
, obj
):
29 self
[name
] = {version
: {
33 def lookup(self
, name
, version
=None):
34 """If version is omitted, max version is used"""
35 versions
= self
.get(name
)
39 return versions
[version
]
40 return versions
[max(versions
)]
42 def getObj(self
, name
, version
=None):
43 result
= self
.lookup(name
, version
)
45 obj
= result
["object"]
50 class TypeRegistry(dict):
53 refname
= Schema
.referenceName(name
)
54 if refname
not in self
:
55 result
= TypeVar(refname
)
56 self
[refname
] = result
57 self
[result
] = refname
61 _types
= TypeRegistry()
62 _registry
= KindRegistry()
66 if isinstance(v
, str):
73 return _types
.get(ref
)
77 return getRefType(obj
["$ref"])
81 kind
= obj
.get('type')
83 raise ValueError("%s has no type" % obj
)
84 result
= SCHEMA_TO_PYTHON
.get(kind
)
86 raise ValueError("%s has type %s" % (obj
, kind
))
90 basic_types
= [str, bool, int, float]
94 result
= name
.replace("-", "_")
95 result
= result
.lower()
96 if keyword
.iskeyword(result
) or result
in dir(builtins
):
101 def strcast(kind
, keep_builtins
=False):
102 if issubclass(kind
, typing
.GenericMeta
):
104 if (kind
in basic_types
or
105 type(kind
) in basic_types
) and keep_builtins
is False:
111 def __init__(self
, defs
):
115 rtypes
= _registry
.getObj(_types
[defs
])
117 if not self
.do_explode(rtypes
[0][1]):
118 for name
, rtype
in rtypes
:
119 self
.append((name
, rtype
))
121 for name
, rtype
in rtypes
:
122 self
.append((name
, rtype
))
124 def do_explode(self
, kind
):
125 if kind
in basic_types
:
127 if not issubclass(kind
, (typing
.Sequence
,
130 self
.extend(Args(kind
))
134 def PyToSchemaMapping(self
):
140 def SchemaToPyMapping(self
):
146 def _format(self
, name
, rtype
, typed
=True):
148 return "{} : {}".format(
153 return name_to_py(name
)
155 def _get_arg_str(self
, typed
=False, joined
=", "):
159 parts
.append(self
._format
(item
[0], item
[1], typed
))
161 return joined
.join(parts
)
166 return self
._get
_arg
_str
(True)
169 return self
._get
_arg
_str
(False)
172 return self
._get
_arg
_str
(True, "\n")
175 def buildTypes(schema
, capture
):
178 for kind
in sorted((k
for k
in _types
if not isinstance(k
, str)),
179 key
=lambda x
: str(x
)):
188 def __init__(self{}{}):
192 args
.PyToSchemaMapping(),
193 args
.SchemaToPyMapping(),
194 ", " if args
else "",
196 textwrap
.indent(args
.get_doc(), INDENT
*2))
197 #pprint.pformat(schema['definitions'][name]))
199 assignments
= args
._get
_arg
_str
(False, False)
200 for assign
in assignments
:
201 source
.append("{}self.{} = {}".format(INDENT
* 2, assign
, assign
))
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")
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
222 rtypes
= _registry
.getObj(_types
[defs
])
226 return Union
[tuple([strcast(r
[1], True) for r
in rtypes
])]
227 return strcast(rtypes
[0][1], False)
230 def return_type(defs
):
233 rtypes
= _registry
.getObj(_types
[defs
])
244 def type_anno_func(func
, defs
, is_result
=False):
248 rtypes
= _registry
.getObj(_types
[defs
])
253 elif len(rtypes
) > 1:
254 annos
[kn
] = Union
[tuple([r
[1] for r
in rtypes
])]
256 annos
[kn
] = rtypes
[0][1]
258 for name
, rtype
in rtypes
:
259 name
= name_to_py(name
)
261 func
.__annotations
__.update(annos
)
265 def ReturnMapping(cls
):
266 # Annotate the method with a return Type
267 # so the value can be cast
270 def wrapper(*args
, **kwargs
):
271 reply
= f(*args
, **kwargs
)
272 if cls
is None or reply
:
276 if issubclass(cls
, typing
.Sequence
):
279 result
.append(cls
.from_json(item
))
281 result
= cls
.from_json(reply
)
288 def makeFunc(cls
, name
, params
, result
, async=True):
292 toschema
= args
.PyToSchemaMapping()
293 for arg
in args
._get
_arg
_str
(False, False):
294 assignments
.append("{}params[\'{}\'] = {}".format(INDENT
,
297 assignments
= "\n".join(assignments
)
298 res
= retspec(result
)
301 #@ReturnMapping({rettype})
302 {async}def {name}(self{argsep}{args}):
307 # map input types to rpc msg
309 msg = dict(Type='{cls.name}', Request='{name}', Version={cls.version}, Params=params)
311 reply = {await}self.rpc(msg)
312 return self._map(reply, {name})
316 fsource
= source
.format(async="async " if async else "",
318 argsep
=", " if args
else "",
320 #ressep= " -> " if res else "",
322 rettype
=result
.__name
__ if result
else None,
323 docstring
=textwrap
.indent(args
.get_doc(), INDENT
),
325 assignments
=assignments
,
326 await="await " if async else "")
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)
341 def _buildMethod(cls
, name
):
344 method
= cls
.schema
['properties'][name
]
345 if 'properties' in method
:
346 prop
= method
['properties']
347 spec
= prop
.get('Params')
349 params
= _types
.get(spec
['$ref'])
350 spec
= prop
.get('Result')
352 result
= _types
.get(spec
['$ref'])
353 return makeFunc(cls
, name
, params
, result
)
356 def buildFacade(schema
):
357 cls
= type(schema
.name
, (Type
,), dict(name
=schema
.name
,
358 version
=schema
.version
,
365 """.format(name
=schema
.name
,
366 version
=schema
.version
,
367 schema
=textwrap
.indent(pprint
.pformat(schema
), " "))
372 def connect(self
, connection
):
373 self
.connection
= connection
375 async def rpc(self
, msg
):
376 result
= await self
.connection
.rpc(msg
)
379 def _map(self
, reply
, method
):
380 # Error, expected return or None
385 retcls
= classes
['Error']
386 data
= reply
['Error']
388 elif 'Response' in reply
:
389 retcls
= method
.__return
_type
__
390 data
= reply
['Response']
391 return retcls
.from_json(data
)
394 def from_json(cls
, data
):
395 if isinstance(data
, str):
396 data
= json
.loads(data
)
401 for attr
, tgt
in self
._toSchema
.items():
402 d
[tgt
] = getattr(self
, attr
)
406 return json
.dumps(self
.serialize())
410 def __init__(self
, schema
):
411 self
.name
= schema
['Name']
412 self
.version
= schema
['Version']
413 self
.update(schema
['Schema'])
416 def referenceName(cls
, ref
):
417 if ref
.startswith("#/definitions/"):
418 ref
= ref
.rsplit("/", 1)[-1]
421 def resolveDefinition(self
, ref
):
422 return self
['definitions'][self
.referenceName(ref
)]
424 def deref(self
, prop
, name
):
425 if not isinstance(prop
, dict):
426 raise TypeError(prop
)
427 if "$ref" not in prop
:
430 target
= self
.resolveDefinition(prop
["$ref"])
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')
442 for d
, data
in defs
.items():
445 node
= self
.deref(data
, d
)
446 kind
= node
.get("type")
448 result
= self
.buildObject(node
, d
)
449 elif kind
== "array":
451 _registry
.register(d
, self
.version
, result
)
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
460 props
= node
.get("properties")
461 pprops
= node
.get("patternProperties")
463 for p
, prop
in props
.items():
465 add((p
, refType(prop
)))
469 add((p
, self
.buildArray(prop
, d
+ 1)))
470 elif kind
== "object":
471 struct
.extend(self
.buildObject(prop
, p
, d
+ 1))
473 add((p
, objType(prop
)))
475 if ".*" not in pprops
:
477 "Cannot handle actual pattern in patterProperties %s" %
481 add((name
, Mapping
[str, refType(pprop
)]))
483 ppkind
= pprop
["type"]
484 if ppkind
== "array":
485 add((name
, self
.buildArray(pprop
, d
+ 1)))
487 add((name
, Mapping
[str, SCHEMA_TO_PYTHON
[ppkind
]]))
488 #print("{}{}".format(d * " ", struct))
491 def buildArray(self
, obj
, d
=0):
492 # return a sequence from an array in the schema
494 return Sequence
[refType(obj
)]
496 kind
= obj
.get("type")
497 if kind
and kind
== "array":
499 return self
.buildArray(items
, d
+1)
501 return Sequence
[objType(obj
)]
507 'ReturnMapping': ReturnMapping
509 # Copy our types into the globals of the method
510 for facade
in _registry
:
511 ns
[facade
] = _registry
.getObj(facade
)
516 def generate_facacdes(options
):
518 schemas
= json
.loads(Path(options
.schema
).read_text("utf-8"))
519 capture
= codegen
.CodeWriter()
521 from juju.client.facade import Type, ReturnMapping
523 schemas
= [Schema(s
) for s
in schemas
]
525 for schema
in schemas
:
526 schema
.buildDefinitions()
527 buildTypes(schema
, capture
)
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
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()
549 capture
= generate_facacdes(options
)
550 with
open(options
.output
, "w") as fp
:
551 print(capture
, file=fp
)
555 if __name__
== '__main__':