X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FN2VC.git;a=blobdiff_plain;f=modules%2Flibjuju%2Fjuju%2Fclient%2Foverrides.py;fp=modules%2Flibjuju%2Fjuju%2Fclient%2Foverrides.py;h=49ab931b1a19ad30ab512ccd80b713182fa840db;hp=0000000000000000000000000000000000000000;hb=e2051cca7dac12aa09f6ed33555dcc4548c4b52b;hpb=9d18c22a0dc9e295adda50601fc5e2f45d2c9b8a diff --git a/modules/libjuju/juju/client/overrides.py b/modules/libjuju/juju/client/overrides.py new file mode 100644 index 0000000..49ab931 --- /dev/null +++ b/modules/libjuju/juju/client/overrides.py @@ -0,0 +1,365 @@ +import re +from collections import namedtuple + +from . import _client, _definitions +from .facade import ReturnMapping, Type, TypeEncoder + +__all__ = [ + 'Delta', + 'Number', + 'Binary', + 'ConfigValue', + 'Resource', +] + +__patches__ = [ + 'ResourcesFacade', + 'AllWatcherFacade', + 'ActionFacade', +] + + +class Delta(Type): + """A single websocket delta. + + :ivar entity: The entity name, e.g. 'unit', 'application' + :vartype entity: str + + :ivar type: The delta type, e.g. 'add', 'change', 'remove' + :vartype type: str + + :ivar data: The raw delta data + :vartype data: dict + + NOTE: The 'data' variable above is being incorrectly cross-linked by a + Sphinx bug: https://github.com/sphinx-doc/sphinx/issues/2549 + + """ + _toSchema = {'deltas': 'deltas'} + _toPy = {'deltas': 'deltas'} + + def __init__(self, deltas=None): + """ + :param deltas: [str, str, object] + + """ + self.deltas = deltas + + Change = namedtuple('Change', 'entity type data') + change = Change(*self.deltas) + + self.entity = change.entity + self.type = change.type + self.data = change.data + + @classmethod + def from_json(cls, data): + return cls(deltas=data) + + +class ResourcesFacade(Type): + """Patch parts of ResourcesFacade to make it work. + """ + + @ReturnMapping(_client.AddPendingResourcesResult) + async def AddPendingResources(self, application_tag, charm_url, resources): + """Fix the calling signature of AddPendingResources. + + The ResourcesFacade doesn't conform to the standard facade pattern in + the Juju source, which leads to the schemagened code not matching up + properly with the actual calling convention in the API. There is work + planned to fix this in Juju, but we have to work around it for now. + + application_tag : str + charm_url : str + resources : typing.Sequence<+T_co>[~CharmResource]<~CharmResource> + Returns -> typing.Union[_ForwardRef('ErrorResult'), + typing.Sequence<+T_co>[str]] + """ + # map input types to rpc msg + _params = dict() + msg = dict(type='Resources', + request='AddPendingResources', + version=1, + params=_params) + _params['tag'] = application_tag + _params['url'] = charm_url + _params['resources'] = resources + reply = await self.rpc(msg) + return reply + + +class AllWatcherFacade(Type): + """ + Patch rpc method of allwatcher to add in 'id' stuff. + + """ + async def rpc(self, msg): + if not hasattr(self, 'Id'): + client = _client.ClientFacade.from_connection(self.connection) + + result = await client.WatchAll() + self.Id = result.watcher_id + + msg['Id'] = self.Id + result = await self.connection.rpc(msg, encoder=TypeEncoder) + return result + + +class ActionFacade(Type): + + class _FindTagsResults(Type): + _toSchema = {'matches': 'matches'} + _toPy = {'matches': 'matches'} + + def __init__(self, matches=None, **unknown_fields): + ''' + FindTagsResults wraps the mapping between the requested prefix and the + matching tags for each requested prefix. + + Matches map[string][]Entity `json:"matches"` + ''' + self.matches = {} + matches = matches or {} + for prefix, tags in matches.items(): + self.matches[prefix] = [_definitions.Entity.from_json(r) + for r in tags] + + @ReturnMapping(_FindTagsResults) + async def FindActionTagsByPrefix(self, prefixes): + ''' + prefixes : typing.Sequence[str] + Returns -> typing.Sequence[~Entity] + ''' + # map input types to rpc msg + _params = dict() + msg = dict(type='Action', + request='FindActionTagsByPrefix', + version=2, + params=_params) + _params['prefixes'] = prefixes + reply = await self.rpc(msg) + return reply + + +class Number(_definitions.Number): + """ + This type represents a semver string. + + Because it is not standard JSON, the typical from_json parsing fails and + the parsing must be handled specially. + + See https://github.com/juju/version for more info. + """ + numberPat = re.compile(r'^(\d{1,9})\.(\d{1,9})(?:\.|-([a-z]+))(\d{1,9})(\.\d{1,9})?$') # noqa + + def __init__(self, major=None, minor=None, patch=None, tag=None, + build=None, **unknown_fields): + ''' + major : int + minor : int + patch : int + tag : str + build : int + ''' + self.major = int(major or '0') + self.minor = int(minor or '0') + self.patch = int(patch or '0') + self.tag = tag or '' + self.build = int(build or '0') + + def __repr__(self): + return ''.format( + self.major, self.minor, self.patch, self.tag, self.build) + + def __str__(self): + return self.serialize() + + @property + def _cmp(self): + return (self.major, self.minor, self.tag, self.patch, self.build) + + def __eq__(self, other): + return isinstance(other, type(self)) and self._cmp == other._cmp + + def __lt__(self, other): + return self._cmp < other._cmp + + def __le__(self, other): + return self._cmp <= other._cmp + + def __gt__(self, other): + return self._cmp > other._cmp + + def __ge__(self, other): + return self._cmp >= other._cmp + + @classmethod + def from_json(cls, data): + parsed = None + if isinstance(data, cls): + return data + elif data is None: + return cls() + elif isinstance(data, dict): + parsed = data + elif isinstance(data, str): + match = cls.numberPat.match(data) + if match: + parsed = { + 'major': match.group(1), + 'minor': match.group(2), + 'tag': match.group(3), + 'patch': match.group(4), + 'build': (match.group(5)[1:] if match.group(5) + else 0), + } + if not parsed: + raise TypeError('Unable to parse Number version string: ' + '{}'.format(data)) + d = {} + for k, v in parsed.items(): + d[cls._toPy.get(k, k)] = v + + return cls(**d) + + def serialize(self): + s = "" + if not self.tag: + s = "{}.{}.{}".format(self.major, self.minor, self.patch) + else: + s = "{}.{}-{}{}".format(self.major, self.minor, self.tag, + self.patch) + if self.build: + s = "{}.{}".format(s, self.build) + return s + + def to_json(self): + return self.serialize() + + +class Binary(_definitions.Binary): + """ + This type represents a semver string with additional series and arch info. + + Because it is not standard JSON, the typical from_json parsing fails and + the parsing must be handled specially. + + See https://github.com/juju/version for more info. + """ + binaryPat = re.compile(r'^(\d{1,9})\.(\d{1,9})(?:\.|-([a-z]+))(\d{1,9})(\.\d{1,9})?-([^-]+)-([^-]+)$') # noqa + + def __init__(self, number=None, series=None, arch=None, **unknown_fields): + ''' + number : Number + series : str + arch : str + ''' + self.number = Number.from_json(number) + self.series = series + self.arch = arch + + def __repr__(self): + return ''.format( + self.number, self.series, self.arch) + + def __str__(self): + return self.serialize() + + def __eq__(self, other): + return ( + isinstance(other, type(self)) and + other.number == self.number and + other.series == self.series and + other.arch == self.arch) + + @classmethod + def from_json(cls, data): + parsed = None + if isinstance(data, cls): + return data + elif data is None: + return cls() + elif isinstance(data, dict): + parsed = data + elif isinstance(data, str): + match = cls.binaryPat.match(data) + if match: + parsed = { + 'number': { + 'major': match.group(1), + 'minor': match.group(2), + 'tag': match.group(3), + 'patch': match.group(4), + 'build': (match.group(5)[1:] if match.group(5) + else 0), + }, + 'series': match.group(6), + 'arch': match.group(7), + } + if parsed is None: + raise TypeError('Unable to parse Binary version string: ' + '{}'.format(data)) + d = {} + for k, v in parsed.items(): + d[cls._toPy.get(k, k)] = v + + return cls(**d) + + def serialize(self): + return "{}-{}-{}".format(self.number.serialize(), + self.series, self.arch) + + def to_json(self): + return self.serialize() + + +class ConfigValue(_definitions.ConfigValue): + def __repr__(self): + return '<{} source={} value={}>'.format(type(self).__name__, + repr(self.source), + repr(self.value)) + + +class Resource(Type): + _toSchema = {'application': 'application', + 'charmresource': 'CharmResource', + 'id_': 'id', + 'pending_id': 'pending-id', + 'timestamp': 'timestamp', + 'username': 'username', + 'name': 'name', + 'origin': 'origin'} + _toPy = {'CharmResource': 'charmresource', + 'application': 'application', + 'id': 'id_', + 'pending-id': 'pending_id', + 'timestamp': 'timestamp', + 'username': 'username', + 'name': 'name', + 'origin': 'origin'} + + def __init__(self, charmresource=None, application=None, id_=None, + pending_id=None, timestamp=None, username=None, name=None, + origin=None, **unknown_fields): + ''' + charmresource : CharmResource + application : str + id_ : str + pending_id : str + timestamp : str + username : str + name: str + origin : str + ''' + if charmresource: + self.charmresource = _client.CharmResource.from_json(charmresource) + else: + self.charmresource = None + self.application = application + self.id_ = id_ + self.pending_id = pending_id + self.timestamp = timestamp + self.username = username + self.name = name + self.origin = origin