5f37c27305d957a8689804304ec2dbb553d42462
3 from collections
import defaultdict
8 from pathlib
import Path
12 from typing
import Sequence
, Mapping
, TypeVar
, Any
, Union
19 JUJU_VERSION
= re
.compile('[0-9]+\.[0-9-]+[\.\-][0-9a-z]+(\.[0-9]+)?')
20 VERSION_MAP
= defaultdict(dict)
21 # Workaround for https://bugs.launchpad.net/juju/+bug/1683906
22 NAUGHTY_CLASSES
= ['ClientFacade', 'Client', 'FullStatus', 'ModelStatusInfo',
26 # Map basic types to Python's typing with a callable
37 # Friendly warning message to stick at the top of generated files.
39 # DO NOT CHANGE THIS FILE! This file is auto-generated by facade.py.
40 # Changes will be overwritten/lost when the file is regenerated.
45 # Classes and helper functions that we'll write to _client.py
47 def lookup_facade(name, version):
49 Given a facade name and version, attempt to pull that facade out
50 of the correct client<version>.py file.
54 facade = getattr(CLIENTS[str(version)], name)
56 raise ImportError("No facades found for version {}".format(version))
57 except AttributeError:
59 "No facade with name '{}' in version {}".format(name, version))
68 def from_connection(cls, connection):
70 Given a connected Connection object, return an initialized and
71 connected instance of an API Interface matching the name of
74 @param connection: initialized Connection object.
77 version = connection.facades[cls.__name__[:-6]]
79 c = lookup_facade(cls.__name__, version)
97 class KindRegistry(dict):
98 def register(self
, name
, version
, obj
):
99 self
[name
] = {version
: {
103 def lookup(self
, name
, version
=None):
104 """If version is omitted, max version is used"""
105 versions
= self
.get(name
)
109 return versions
[version
]
110 return versions
[max(versions
)]
112 def getObj(self
, name
, version
=None):
113 result
= self
.lookup(name
, version
)
115 obj
= result
["object"]
120 class TypeRegistry(dict):
123 refname
= Schema
.referenceName(name
)
124 if refname
not in self
:
125 result
= TypeVar(refname
)
126 self
[refname
] = result
127 self
[result
] = refname
131 _types
= TypeRegistry()
132 _registry
= KindRegistry()
134 factories
= codegen
.Capture()
138 if isinstance(v
, str):
145 return _types
.get(ref
)
149 return getRefType(obj
["$ref"])
153 kind
= obj
.get('type')
155 raise ValueError("%s has no type" % obj
)
156 result
= SCHEMA_TO_PYTHON
.get(kind
)
158 raise ValueError("%s has type %s" % (obj
, kind
))
162 basic_types
= [str, bool, int, float]
165 def name_to_py(name
):
166 result
= name
.replace("-", "_")
167 result
= result
.lower()
168 if keyword
.iskeyword(result
) or result
in dir(builtins
):
173 def strcast(kind
, keep_builtins
=False):
174 if issubclass(kind
, typing
.GenericMeta
):
176 if str(kind
).startswith('~'):
178 if (kind
in basic_types
or
179 type(kind
) in basic_types
) and keep_builtins
is False:
185 def __init__(self
, defs
):
188 rtypes
= _registry
.getObj(_types
[defs
])
190 if not self
.do_explode(rtypes
[0][1]):
191 for name
, rtype
in rtypes
:
192 self
.append((name
, rtype
))
194 for name
, rtype
in rtypes
:
195 self
.append((name
, rtype
))
197 def do_explode(self
, kind
):
198 if kind
in basic_types
or type(kind
) is typing
.TypeVar
:
200 if not issubclass(kind
, (typing
.Sequence
,
203 self
.extend(Args(kind
))
207 def PyToSchemaMapping(self
):
213 def SchemaToPyMapping(self
):
219 def _format(self
, name
, rtype
, typed
=True):
221 return "{} : {}".format(
226 return name_to_py(name
)
228 def _get_arg_str(self
, typed
=False, joined
=", "):
232 parts
.append(self
._format
(item
[0], item
[1], typed
))
234 return joined
.join(parts
)
242 parts
.append('{}=None'.format(name_to_py(item
[0])))
243 return ', '.join(parts
)
247 return self
._get
_arg
_str
(True)
250 return self
._get
_arg
_str
(False)
253 return self
._get
_arg
_str
(True, "\n")
256 def buildTypes(schema
, capture
):
258 for kind
in sorted((k
for k
in _types
if not isinstance(k
, str)),
259 key
=lambda x
: str(x
)):
261 if name
in capture
and not name
in NAUGHTY_CLASSES
:
264 # Write Factory class for _client.py
271 def __init__(self{}{}):
276 # pprint these to get stable ordering across regens
277 pprint
.pformat(args
.PyToSchemaMapping(), width
=999),
278 pprint
.pformat(args
.SchemaToPyMapping(), width
=999),
279 ", " if args
else "",
281 textwrap
.indent(args
.get_doc(), INDENT
* 2))
283 assignments
= args
._get
_arg
_str
(False, False)
286 source
.append("{}pass".format(INDENT
* 2))
289 arg_name
= name_to_py(arg
[0])
291 arg_type_name
= strcast(arg_type
)
292 if arg_type
in basic_types
:
293 source
.append("{}self.{} = {}".format(INDENT
* 2, arg_name
, arg_name
))
294 elif issubclass(arg_type
, typing
.Sequence
):
296 arg_type_name
.__parameters
__[0]
297 if len(arg_type_name
.__parameters
__)
300 if type(value_type
) is typing
.TypeVar
:
301 source
.append("{}self.{} = [{}.from_json(o) for o in {} or []]".format(
302 INDENT
* 2, arg_name
, strcast(value_type
), arg_name
))
304 source
.append("{}self.{} = {}".format(INDENT
* 2, arg_name
, arg_name
))
305 elif issubclass(arg_type
, typing
.Mapping
):
307 arg_type_name
.__parameters
__[1]
308 if len(arg_type_name
.__parameters
__) > 1
311 if type(value_type
) is typing
.TypeVar
:
312 source
.append("{}self.{} = {{k: {}.from_json(v) for k, v in ({} or dict()).items()}}".format(
313 INDENT
* 2, arg_name
, strcast(value_type
), arg_name
))
315 source
.append("{}self.{} = {}".format(INDENT
* 2, arg_name
, arg_name
))
316 elif type(arg_type
) is typing
.TypeVar
:
317 source
.append("{}self.{} = {}.from_json({}) if {} else None".format(
318 INDENT
* 2, arg_name
, arg_type_name
, arg_name
, arg_name
))
320 source
.append("{}self.{} = {}".format(INDENT
* 2, arg_name
, arg_name
))
322 source
= "\n".join(source
)
324 capture
[name
].write(source
)
325 capture
[name
].write("\n\n")
326 co
= compile(source
, __name__
, "exec")
335 # only return 1, so if there is more than one type
336 # we need to include a union
337 # In truth there is only 1 return
338 # Error or the expected Type
341 if defs
in basic_types
:
342 return strcast(defs
, False)
343 rtypes
= _registry
.getObj(_types
[defs
])
347 return Union
[tuple([strcast(r
[1], True) for r
in rtypes
])]
348 return strcast(rtypes
[0][1], False)
351 def return_type(defs
):
354 rtypes
= _registry
.getObj(_types
[defs
])
365 def type_anno_func(func
, defs
, is_result
=False):
369 rtypes
= _registry
.getObj(_types
[defs
])
374 elif len(rtypes
) > 1:
375 annos
[kn
] = Union
[tuple([r
[1] for r
in rtypes
])]
377 annos
[kn
] = rtypes
[0][1]
379 for name
, rtype
in rtypes
:
380 name
= name_to_py(name
)
382 func
.__annotations
__.update(annos
)
386 def ReturnMapping(cls
):
387 # Annotate the method with a return Type
388 # so the value can be cast
391 async def wrapper(*args
, **kwargs
):
393 reply
= await f(*args
, **kwargs
)
397 cls
= CLASSES
['Error']
398 if issubclass(cls
, typing
.Sequence
):
400 item_cls
= cls
.__parameters
__[0]
402 result
.append(item_cls
.from_json(item
))
405 cls = CLASSES['Error']
408 result.append(cls.from_json(item))
411 result
= cls
.from_json(reply
['response'])
418 def makeFunc(cls
, name
, params
, result
, async=True):
422 toschema
= args
.PyToSchemaMapping()
423 for arg
in args
._get
_arg
_str
(False, False):
424 assignments
.append("{}_params[\'{}\'] = {}".format(INDENT
,
427 assignments
= "\n".join(assignments
)
428 res
= retspec(result
)
431 @ReturnMapping({rettype})
432 {async}def {name}(self{argsep}{args}):
437 # map input types to rpc msg
439 msg = dict(type='{cls.name}', request='{name}', version={cls.version}, params=_params)
441 reply = {await}self.rpc(msg)
446 fsource
= source
.format(async="async " if async else "",
448 argsep
=", " if args
else "",
451 rettype
=result
.__name
__ if result
else None,
452 docstring
=textwrap
.indent(args
.get_doc(), INDENT
),
454 assignments
=assignments
,
455 await="await " if async else "")
462 def buildMethods(cls
, capture
):
463 properties
= cls
.schema
['properties']
464 for methodname
in sorted(properties
):
465 method
, source
= _buildMethod(cls
, methodname
)
466 setattr(cls
, methodname
, method
)
467 capture
["{}Facade".format(cls
.__name
__)].write(source
, depth
=1)
470 def _buildMethod(cls
, name
):
473 method
= cls
.schema
['properties'][name
]
474 if 'properties' in method
:
475 prop
= method
['properties']
476 spec
= prop
.get('Params')
478 params
= _types
.get(spec
['$ref'])
479 spec
= prop
.get('Result')
482 result
= _types
.get(spec
['$ref'])
484 result
= SCHEMA_TO_PYTHON
[spec
['type']]
485 return makeFunc(cls
, name
, params
, result
)
488 def buildFacade(schema
):
489 cls
= type(schema
.name
, (Type
,), dict(name
=schema
.name
,
490 version
=schema
.version
,
493 class {name}Facade(Type):
497 """.format(name
=schema
.name
,
498 version
=schema
.version
,
499 schema
=textwrap
.indent(pprint
.pformat(schema
), " "))
503 class TypeEncoder(json
.JSONEncoder
):
504 def default(self
, obj
):
505 if isinstance(obj
, Type
):
506 return obj
.serialize()
507 return json
.JSONEncoder
.default(self
, obj
)
511 def connect(self
, connection
):
512 self
.connection
= connection
514 async def rpc(self
, msg
):
515 result
= await self
.connection
.rpc(msg
, encoder
=TypeEncoder
)
519 def from_json(cls
, data
):
520 if isinstance(data
, cls
):
522 if isinstance(data
, str):
523 data
= json
.loads(data
)
525 for k
, v
in (data
or {}).items():
526 d
[cls
._toPy
.get(k
, k
)] = v
535 for attr
, tgt
in self
._toSchema
.items():
536 d
[tgt
] = getattr(self
, attr
)
540 return json
.dumps(self
.serialize())
544 def __init__(self
, schema
):
545 self
.name
= schema
['Name']
546 self
.version
= schema
['Version']
547 self
.update(schema
['Schema'])
550 def referenceName(cls
, ref
):
551 if ref
.startswith("#/definitions/"):
552 ref
= ref
.rsplit("/", 1)[-1]
555 def resolveDefinition(self
, ref
):
556 return self
['definitions'][self
.referenceName(ref
)]
558 def deref(self
, prop
, name
):
559 if not isinstance(prop
, dict):
560 raise TypeError(prop
)
561 if "$ref" not in prop
:
564 target
= self
.resolveDefinition(prop
["$ref"])
567 def buildDefinitions(self
):
568 # here we are building the types out
569 # anything in definitions is a type
570 # but these may contain references themselves
571 # so we dfs to the bottom and build upwards
572 # when a types is already in the registry
573 defs
= self
.get('definitions')
576 for d
, data
in defs
.items():
577 if d
in _registry
and not d
in NAUGHTY_CLASSES
:
579 node
= self
.deref(data
, d
)
580 kind
= node
.get("type")
582 result
= self
.buildObject(node
, d
)
583 elif kind
== "array":
585 _registry
.register(d
, self
.version
, result
)
586 # XXX: This makes sure that the type gets added to the global
587 # _types dict even if no other type in the schema has a ref
591 def buildObject(self
, node
, name
=None, d
=0):
592 # we don't need to build types recursively here
593 # they are all in definitions already
594 # we only want to include the type reference
595 # which we can derive from the name
598 props
= node
.get("properties")
599 pprops
= node
.get("patternProperties")
601 # Sort these so the __init__ arg list for each Type remains
602 # consistently ordered across regens of client.py
603 for p
in sorted(props
):
606 add((p
, refType(prop
)))
610 add((p
, self
.buildArray(prop
, d
+ 1)))
611 elif kind
== "object":
612 struct
.extend(self
.buildObject(prop
, p
, d
+ 1))
614 add((p
, objType(prop
)))
616 if ".*" not in pprops
:
618 "Cannot handle actual pattern in patternProperties %s" %
622 add((name
, Mapping
[str, refType(pprop
)]))
624 ppkind
= pprop
["type"]
625 if ppkind
== "array":
626 add((name
, self
.buildArray(pprop
, d
+ 1)))
628 add((name
, Mapping
[str, SCHEMA_TO_PYTHON
[ppkind
]]))
630 if not struct
and node
.get('additionalProperties', False):
631 add((name
, Mapping
[str, SCHEMA_TO_PYTHON
['object']]))
635 def buildArray(self
, obj
, d
=0):
636 # return a sequence from an array in the schema
638 return Sequence
[refType(obj
)]
640 kind
= obj
.get("type")
641 if kind
and kind
== "array":
643 return self
.buildArray(items
, d
+ 1)
645 return Sequence
[objType(obj
)]
651 'ReturnMapping': ReturnMapping
653 # Copy our types into the globals of the method
654 for facade
in _registry
:
655 ns
[facade
] = _registry
.getObj(facade
)
659 def make_factory(name
):
660 if name
in factories
:
662 factories
[name
].write("class {}(TypeFactory):\n pass\n\n".format(name
))
665 def write_facades(captures
, options
):
667 Write the Facades to the appropriate _client<version>.py
670 for version
in sorted(captures
.keys()):
671 filename
= "{}/_client{}.py".format(options
.output_dir
, version
)
672 with
open(filename
, "w") as f
:
674 f
.write("from juju.client.facade import Type, ReturnMapping\n")
675 f
.write("from juju.client._definitions import *\n\n")
677 [k
for k
in captures
[version
].keys() if "Facade" in k
]):
678 print(captures
[version
][key
], file=f
)
680 # Return the last (most recent) version for use in other routines.
684 def write_definitions(captures
, options
, version
):
686 Write auxillary (non versioned) classes to
687 _definitions.py The auxillary classes currently get
688 written redudantly into each capture object, so we can look in
689 one of them -- we just use the last one from the loop above.
692 with
open("{}/_definitions.py".format(options
.output_dir
), "w") as f
:
694 f
.write("from juju.client.facade import Type, ReturnMapping\n\n")
696 [k
for k
in captures
[version
].keys() if "Facade" not in k
]):
697 print(captures
[version
][key
], file=f
)
700 def write_client(captures
, options
):
702 Write the TypeFactory classes to _client.py, along with some
703 imports and tables so that we can look up versioned Facades.
706 with
open("{}/_client.py".format(options
.output_dir
), "w") as f
:
708 f
.write("from juju.client._definitions import *\n\n")
709 clients
= ", ".join("_client{}".format(v
) for v
in captures
)
710 f
.write("from juju.client import " + clients
+ "\n\n")
711 f
.write(CLIENT_TABLE
.format(clients
=",\n ".join(
712 ['"{}": _client{}'.format(v
, v
) for v
in captures
])))
713 f
.write(LOOKUP_FACADE
)
714 f
.write(TYPE_FACTORY
)
715 for key
in sorted([k
for k
in factories
.keys() if "Facade" in k
]):
716 print(factories
[key
], file=f
)
719 def write_version_map(options
):
721 In order to work around
722 https://bugs.launchpad.net/juju/+bug/1682925, we build a map of
723 the facades that each version supports, and write it to disk here.
726 with
open("{}/version_map.py".format(options
.output_dir
), "w") as f
:
728 f
.write("VERSION_MAP = {\n")
729 for juju_version
in sorted(VERSION_MAP
.keys()):
730 f
.write(' "{}": {{\n'.format(juju_version
))
731 for key
in VERSION_MAP
[juju_version
]:
732 f
.write(' "{}": {},\n'.format(
733 key
, VERSION_MAP
[juju_version
][key
]))
738 def generate_facades(options
):
739 captures
= defaultdict(codegen
.Capture
)
741 for p
in sorted(glob(options
.schema
)):
743 juju_version
= 'latest'
746 juju_version
= re
.search(JUJU_VERSION
, p
).group()
747 except AttributeError:
748 print("Cannot extract a juju version from {}".format(p
))
749 print("Schemas must include a juju version in the filename")
752 new_schemas
= json
.loads(Path(p
).read_text("utf-8"))
753 schemas
[juju_version
] = [Schema(s
) for s
in new_schemas
]
755 # Build all of the auxillary (unversioned) classes
756 # TODO: get rid of some of the excess trips through loops in the
758 for juju_version
in sorted(schemas
.keys()):
759 for schema
in schemas
[juju_version
]:
760 schema
.buildDefinitions()
761 buildTypes(schema
, captures
[schema
.version
])
762 VERSION_MAP
[juju_version
][schema
.name
] = schema
.version
764 # Build the Facade classes
765 for juju_version
in sorted(schemas
.keys()):
766 for schema
in schemas
[juju_version
]:
767 cls
, source
= buildFacade(schema
)
768 cls_name
= "{}Facade".format(schema
.name
)
770 captures
[schema
.version
].clear(cls_name
)
771 # Make the factory class for _client.py
772 make_factory(cls_name
)
773 # Make the actual class
774 captures
[schema
.version
][cls_name
].write(source
)
775 # Build the methods for each Facade class.
776 buildMethods(cls
, captures
[schema
.version
])
777 # Mark this Facade class as being done for this version --
778 # helps mitigate some excessive looping.
779 CLASSES
[schema
.name
] = cls
784 parser
= argparse
.ArgumentParser()
785 parser
.add_argument("-s", "--schema", default
="juju/client/schemas*")
786 parser
.add_argument("-o", "--output_dir", default
="juju/client")
787 options
= parser
.parse_args()
793 # Generate some text blobs
794 captures
= generate_facades(options
)
796 # ... and write them out
797 last_version
= write_facades(captures
, options
)
798 write_definitions(captures
, options
, last_version
)
799 write_client(captures
, options
)
800 write_version_map(options
)
802 if __name__
== '__main__':