-
Notifications
You must be signed in to change notification settings - Fork 99
CharmHub - Origin #463
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CharmHub - Origin #463
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| from functools import partial | ||
|
|
||
| import asyncio | ||
| import theblues.charmstore | ||
| import theblues.errors | ||
|
|
||
|
|
||
| class CharmStore: | ||
| """ | ||
| Async wrapper around theblues.charmstore.CharmStore | ||
| """ | ||
| def __init__(self, loop, cs_timeout=20): | ||
| self.loop = loop | ||
| self._cs = theblues.charmstore.CharmStore(timeout=cs_timeout) | ||
|
|
||
| def __getattr__(self, name): | ||
| """ | ||
| Wrap method calls in coroutines that use run_in_executor to make them | ||
| async. | ||
| """ | ||
| attr = getattr(self._cs, name) | ||
| if not callable(attr): | ||
| wrapper = partial(getattr, self._cs, name) | ||
| setattr(self, name, wrapper) | ||
| else: | ||
| async def coro(*args, **kwargs): | ||
| method = partial(attr, *args, **kwargs) | ||
| for attempt in range(1, 4): | ||
| try: | ||
| return await self.loop.run_in_executor(None, method) | ||
| except theblues.errors.ServerError: | ||
| if attempt == 3: | ||
| raise | ||
| await asyncio.sleep(1, loop=self.loop) | ||
| setattr(self, name, coro) | ||
| wrapper = coro | ||
| return wrapper |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| from enum import Enum | ||
| from .errors import JujuError | ||
|
|
||
|
|
||
| class Source(Enum): | ||
| """Source defines a origin source. Providing a hint to the controller about | ||
| what the charm identity is from the URL and origin source. | ||
|
|
||
| """ | ||
| LOCAL = "local" | ||
| CHARM_STORE = "charm-store" | ||
| CHARM_HUB = "charm-hub" | ||
|
|
||
| def __str__(self): | ||
| return self.value | ||
|
|
||
|
|
||
| class Origin: | ||
| def __init__(self, source, channel, platform): | ||
| self.source = source | ||
| self.channel = channel | ||
| self.platform = platform | ||
|
|
||
| def __str__(self): | ||
| return "origin using source {} for channel {} and platform {}".format(str(self.source), self.channel, self.platform) | ||
|
|
||
|
|
||
| class Risk(Enum): | ||
| STABLE = "stable" | ||
| CANDIDATE = "candidate" | ||
| BETA = "beta" | ||
| EDGE = "edge" | ||
|
|
||
| def __str__(self): | ||
| return self.value | ||
|
|
||
| @staticmethod | ||
| def valid(potential): | ||
| for risk in [Risk.STABLE, Risk.CANDIDATE, Risk.BETA, Risk.EDGE]: | ||
| if str(risk) == potential: | ||
| return True | ||
| return False | ||
|
|
||
|
|
||
| class Channel: | ||
| """Channel identifies and describes completely a store channel. | ||
|
|
||
| A channel consists of, and is subdivided by, tracks, risk-levels and | ||
| - Tracks enable snap developers to publish multiple supported releases of | ||
| their application under the same snap name. | ||
| - Risk-levels represent a progressive potential trade-off between stability | ||
| and new features. | ||
|
|
||
| The complete channel name can be structured as three distinct parts separated | ||
| by slashes: | ||
|
|
||
| <track>/<risk> | ||
|
|
||
| """ | ||
| def __init__(self, track=None, risk=None): | ||
| if not Risk.valid(risk): | ||
| raise JujuError("unexpected risk {}".format(risk)) | ||
|
|
||
| self.track = track or "" | ||
| self.risk = risk | ||
|
|
||
| @staticmethod | ||
| def parse(s): | ||
| """parse a channel from a given string. | ||
| Parse does not take into account branches. | ||
|
|
||
| """ | ||
| if not s: | ||
| raise JujuError("channel cannot be empty") | ||
|
|
||
| p = s.split("/") | ||
|
|
||
| risk = None | ||
| track = None | ||
| if len(p) == 1: | ||
| if Risk.valid(p[0]): | ||
| risk = p[0] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aha. Now I understand why you don't raise the error in the risk. Disregard my above comment. It would be a lot messier to do a try here -- the validator expects to get invalid risks without complaining. |
||
| else: | ||
| track = p[0] | ||
| risk = str(Risk.STABLE) | ||
| elif len(p) == 2: | ||
| track = p[0] | ||
| risk = p[1] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No harm in validating the risk here, as long as you have your validator. :-) |
||
| else: | ||
| raise JujuError("channel is malformed and has too many components {}".format(s)) | ||
|
|
||
| if risk is not None and not Risk.valid(risk): | ||
| raise JujuError("risk in channel {} is not valid".format(s)) | ||
| if track is not None and track == "": | ||
| raise JujuError("track in channel {} is not valid".format(s)) | ||
|
|
||
| return Channel(track, risk) | ||
|
|
||
| def normalize(self): | ||
| track = self.track if self.track != "latest" else "" | ||
| risk = self.risk if self.risk != "" else "" | ||
| return Channel(track, risk) | ||
|
|
||
| def __eq__(self, other): | ||
| if isinstance(other, self.__class__): | ||
| return self.track == other.track and self.risk == other.risk | ||
| return False | ||
|
|
||
| def __str__(self): | ||
| path = self.risk | ||
| if self.track != "": | ||
| path = "{}/{}".format(self.track, path) | ||
| return path | ||
|
|
||
|
|
||
| class Platform: | ||
| """ParsePlatform parses a string representing a store platform. | ||
| Serialized version of platform can be expected to conform to the following: | ||
|
|
||
| 1. Architecture is mandatory. | ||
| 2. OS is optional and can be dropped. Series is mandatory if OS wants | ||
| to be displayed. | ||
| 3. Series is also optional. | ||
|
|
||
| To indicate something is missing `unknown` can be used in place. | ||
|
|
||
| Examples: | ||
|
|
||
| 1. `<arch>/<os>/<series>` | ||
| 2. `<arch>` | ||
| 3. `<arch>/<series>` | ||
| 4. `<arch>/unknown/<series>` | ||
|
|
||
| """ | ||
| def __init__(self, arch, series=None, os=None): | ||
| self.arch = arch | ||
| self.series = series | ||
| self.os = os | ||
|
|
||
| @staticmethod | ||
| def parse(s): | ||
| if not s: | ||
| raise JujuError("platform cannot be empty") | ||
|
|
||
| p = s.split("/") | ||
|
|
||
| arch = None | ||
| os = None | ||
| series = None | ||
| if len(p) == 1: | ||
| arch = p[0] | ||
| elif len(p) == 2: | ||
| arch = p[0] | ||
| series = p[1] | ||
| elif len(p) == 3: | ||
| arch = p[0] | ||
| os = p[1] | ||
| series = p[2] | ||
| else: | ||
| raise JujuError("platform is malformed and has too many components {}".format(s)) | ||
|
|
||
| if not arch: | ||
| raise JujuError("architecture in platform {} is not valid".format(s)) | ||
|
SimonRichardson marked this conversation as resolved.
|
||
| if os is not None and os == "": | ||
| raise JujuError("os in platform {} is not valid".format(s)) | ||
| if series is not None and series == "": | ||
| raise JujuError("series in platform {} is not valid".format(s)) | ||
|
|
||
| return Platform(arch, series, os) | ||
|
|
||
| def normalize(self): | ||
| os = self.os if self.os is not None or self.os != "unknown" else None | ||
| series = self.series | ||
| if series is None or series == "unknown": | ||
| os = None | ||
| series = None | ||
|
|
||
| return Platform(self.arch, series, os) | ||
|
|
||
| def __eq__(self, other): | ||
| if isinstance(other, self.__class__): | ||
| return self.arch == other.arch and self.os == other.os and self.series == other.series | ||
| return False | ||
|
|
||
| def __str__(self): | ||
| path = self.arch | ||
| if self.os is not None and self.os != "": | ||
| path = "{}/{}".format(path, self.os) | ||
| if self.series is not None and self.series != "": | ||
| path = "{}/{}".format(path, self.series) | ||
| return path | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that it's probably be more conventional here to raise the error in the validation method. You're going to raise it anyway, and this leaves room to write an @valid_risk wrapper for functions.
None of that is absolutely necessary. But there is a more compact way to do things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would only work for
kwargsthough right, as there is no way to know viaargs? So in order to use the wrapper you'd have to ensure that?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could do it with args, which are always in the same order.
Regardless, this is the way it is for other reasons, so I'm good with it :-)