def make( cls, value: Optional[Union[str, Tuple[str, ...]]] = None, expires_at: Optional[datetime] = None, ) -> "Resolution": """ Makes a `Resolution` to describe a value and the duration to cache it. Args: value: Environment variable value, or `None` to not set. expires_at: Time to cache the value until. `None` to not cache. Returns: Resolution. """ store: Dict[str, Any] = {} if value is not None: if isinstance(value, str): store.update({"values": (value, )}) else: store.update({"values": value}) if expires_at: store.update({"expires_at": expires_at.isoformat()}) # Don't log the store; it contains confidential information. get_logger().debug('"Resolution.make" created a new store.') return Resolution(store=store)
def resolve(state: Optional[BaseState] = None) -> Dict[str, str]: """ Gathers values for all the required environment variables. Reads from the cache where possible, and invokes plugins where needed. Returns: Environment variable names and values. """ logger = get_logger() this_state = state or State() environs: Dict[str, str] = {**environ} for variable in this_state.get_variables(): if resolution := fresh_resolution(variable.resolution): logger.debug("%s has a fresh cache.", variable.names) else: logger.info("Resolving %s...", bold(variable.names)) support = ResolutionSupport( logger=get_logger(name=variable.plugin.id), confidential_prompt=confidential_prompt, ) plugin = get_plugin(variable.plugin) try: resolution = plugin.resolve(support=support) except CannotResolveError as ex: raise CannotResolveError( f'"{variable.plugin.id}" failed: {ex}') # If the resolution has a cache expiry date then cache it. # Otherwise don't cache it, and remove any previously-set cache. if resolution.expires_at: this_state.resolution_cache.update( names=variable.names, resolution=resolution, ) else: this_state.resolution_cache.remove(names=variable.names) if resolution.values: logger.debug("Enumerating resolved values: %s", resolution.values) variable_count = len(variable.names) resolution_count = len(resolution.values) if variable_count != resolution_count: raise IncorrectResolutionCountError( plugin_id=variable.plugin.id, variable_count=variable_count, resolution_count=resolution_count, ) for index, name in enumerate(variable.names): value = resolution.values[index] logger.debug("Adding resolved variable: %s=%s", name, value) environs.update({name: value})
def get_plugin(config: PluginConfiguration) -> PluginBase: """ Gets a plugin instance to handle the given configuration. Raises `NoPluginError` if there isn't a plugin available to handle this configuration. Raises `MultiplePluginsError` if there are multple plugins available to handle this configuration. """ plugins = [ e for e in iter_entry_points("wev.plugins") if e.name == config.id ] if len(plugins) == 0: raise NoPluginError(plugin_id=config.id) if len(plugins) > 1: raise MultiplePluginsError(plugin_id=config.id, count=len(plugins)) logger = get_logger() logger.debug("Instantiating plugin with: %s", config) plugin = cast(PluginBase, plugins[0].load().Plugin(config)) logger.debug("Instantiated plugin: %s", plugin) return plugin
def explain(logger: Optional[Logger] = None, state: Optional[BaseState] = None) -> None: """ Logs an explanation of what an execution will do. Args: logger: Logger. One will be created if not specified. """ return _explain(logger=logger or get_logger(), state=state or State())
def __init__(self, store: Dict[str, Any]) -> None: self.logger = get_logger() # Don't log the store; it probably contains confidential information. self.logger.debug("Initialising new Resolution.") if not isinstance(store, dict): raise ValueError( 'Resolution "store" is not a dictionary: %s', type(store), ) self.store = store
def __init__(self, args: Optional[List[str]] = None) -> None: self.logger = get_logger() wev = Style.BRIGHT + "wev" + Style.RESET_ALL with_env_vars = (Style.BRIGHT + "w" + Style.RESET_ALL + "ith " + Style.BRIGHT + "e" + Style.RESET_ALL + "nvironment " + Style.BRIGHT + "v" + Style.RESET_ALL + "ariables") description = f"{wev} runs shell commands {with_env_vars}." epilog = ( "examples:\n" + " wev --explain\n" + " wev --version\n" + " wev pipenv install\n" + " wev --log-level DEBUG pipenv install\n" + "\n" + "Built with ❤️ by Cariad Eccleston: https://github.com/cariad/wev") self.arg_parser = ArgumentParser( "wev", description=description, epilog=epilog, formatter_class=RawDescriptionHelpFormatter, ) self.arg_parser.add_argument( "command", nargs="...", help=f"shell command and arguments to run {with_env_vars}.", ) self.arg_parser.add_argument( "--explain", action="store_true", help=f"explain what {wev} will do", ) self.arg_parser.add_argument( "--log-level", default="INFO", help="log level (default is INFO)", metavar="LEVEL", ) self.arg_parser.add_argument( "--version", action="store_true", help="print the version", ) self.args = self.arg_parser.parse_args(args) set_level(self.args.log_level.upper())
def execute(command: List[str]) -> int: """ Executes the command with environment variables. Args: command: Shell command. Returns: Shell return code. """ logger = get_logger() variables = resolve() logger.debug("Starting %s...", command[0]) return run(command, env=variables).returncode
def __init__(self, names: Tuple[str, ...], store: Dict[str, Any]) -> None: self.logger = get_logger() # Don't log the values; they're probably confidential. self.logger.debug('Variable: name="%s"', names) if "resolution" in store and not isinstance(store["resolution"], dict): raise ValueError( '"resolution" is not a dictionary: %s', type(store["resolution"]), ) self.names = names self.store = store
def __init__( self, context: str, logger: Optional[Logger] = None, path: Optional[Path] = None, ) -> None: self.logger = logger or get_logger() self.context = context self.path = path or Path.home().absolute().joinpath(".wevcache") self.resolutions: Dict[Tuple[str, ...], Dict[str, Any]] = {} self.logger.debug( 'ResolutionCache: context="%s" path="%s"', self.context, self.path, )
def _explain(logger: Logger, state: BaseState) -> None: """ Logs an explanation of what an execution will do. Args: logger: Logger. """ logger.info( "%s (%s) execution plan generated at %s:", bold("wev"), get_version(), bold(get_now()), ) list_index_padding = 2 list_index_suffix = "." logger.info("") for index, variable in enumerate(state.get_variables()): logger.info( "%s%s %s will be resolved by the %s plugin.", str(index + 1).rjust(list_index_padding), list_index_suffix, bold(displayable(variable.names)), bold(variable.plugin.id), ) margin = " " if variable.resolution: logger.info("") logger.info("%s%s", margin, dim(variable.resolution.explain_cache)) if not variable.should_read_from_cache: logger.info("") plugin = get_plugin(variable.plugin) for line in plugin.explain(logger=get_logger(name=variable.plugin.id)): logger.info("%s%s", margin, dim(line)) logger.info("")
def __init__(self) -> None: self.logger = get_logger() self.logger.debug("Creating a new state.")
def logger() -> Logger: basicConfig() logger = get_logger() logger.setLevel(level="DEBUG") return logger
def resolution_support() -> ResolutionSupport: return ResolutionSupport( logger=get_logger(), confidential_prompt=confidential_prompt, )