1"""
2    SoftLayer.CLI.environment
3    ~~~~~~~~~~~~~~~~~~~~~~~~~
4    Abstracts everything related to the user's environment when running the CLI
5
6    :license: MIT, see LICENSE for more details.
7"""
8import importlib
9
10import click
11import pkg_resources
12
13import SoftLayer
14from SoftLayer.CLI import formatting
15from SoftLayer.CLI import routes
16
17# pylint: disable=too-many-instance-attributes, invalid-name, no-self-use
18
19# Calling pkg_resources.iter_entry_points shows a false-positive
20# pylint: disable=no-member
21
22
23class Environment(object):
24    """Provides access to the current CLI environment."""
25
26    def __init__(self):
27        # {'path:to:command': ModuleLoader()}
28        # {'vs:list': ModuleLoader()}
29        self.commands = {}
30        self.aliases = {}
31
32        self.vars = {}
33
34        self.client = None
35        self.format = 'table'
36        self.skip_confirmations = False
37        self.config_file = None
38
39        self._modules_loaded = False
40
41    def out(self, output, newline=True):
42        """Outputs a string to the console (stdout)."""
43        click.echo(output, nl=newline)
44
45    def err(self, output, newline=True):
46        """Outputs an error string to the console (stderr)."""
47        click.echo(output, nl=newline, err=True)
48
49    def fmt(self, output, fmt=None):
50        """Format output based on current the environment format."""
51        if fmt is None:
52            fmt = self.format
53        return formatting.format_output(output, fmt)
54
55    def format_output_is_json(self):
56        """Return True if format output is json or jsonraw"""
57        return 'json' in self.format
58
59    def fout(self, output, newline=True):
60        """Format the input and output to the console (stdout)."""
61        if output is not None:
62            try:
63                self.out(self.fmt(output), newline=newline)
64            except UnicodeEncodeError:
65                # If we hit an undecodeable entry, just try outputting as json.
66                self.out(self.fmt(output, 'json'), newline=newline)
67
68    def input(self, prompt, default=None, show_default=True):
69        """Provide a command prompt."""
70        return click.prompt(prompt, default=default, show_default=show_default)
71
72    def getpass(self, prompt, default=None):
73        """Provide a password prompt."""
74        password = click.prompt(prompt, hide_input=True, default=default)
75
76        # https://github.com/softlayer/softlayer-python/issues/1436
77        # click.prompt uses python's getpass() in the background
78        # https://github.com/python/cpython/blob/3.9/Lib/getpass.py#L97
79        # In windows, shift+insert actually inputs the below 2 characters
80        # If we detect those 2 characters, need to manually read from the clipbaord instead
81        # https://stackoverflow.com/questions/101128/how-do-i-read-text-from-the-clipboard
82        if password == 'àR':
83            # tkinter is a built in python gui, but it has clipboard reading functions.
84            # pylint: disable=import-outside-toplevel
85            from tkinter import Tk
86            tk_manager = Tk()
87            password = tk_manager.clipboard_get()
88            # keep the window from showing
89            tk_manager.withdraw()
90        return password
91
92    # Command loading methods
93    def list_commands(self, *path):
94        """Command listing."""
95        path_str = ':'.join(path)
96
97        commands = []
98        for command in self.commands:
99
100            # Filter based on prefix and the segment length
101            if all([command.startswith(path_str),
102                    len(path) == command.count(":")]):
103
104                # offset is used to exclude the path that the caller requested.
105                offset = len(path_str) + 1 if path_str else 0
106                commands.append(command[offset:])
107
108        return sorted(commands)
109
110    def get_command(self, *path):
111        """Return command at the given path or raise error."""
112        path_str = ':'.join(path)
113
114        if path_str in self.commands:
115            return self.commands[path_str].load()
116
117        return None
118
119    def resolve_alias(self, path_str):
120        """Returns the actual command name. Uses the alias mapping."""
121        if path_str in self.aliases:
122            return self.aliases[path_str]
123        return path_str
124
125    def load(self):
126        """Loads all modules."""
127        if self._modules_loaded is True:
128            return
129
130        self.load_modules_from_python(routes.ALL_ROUTES)
131        self.aliases.update(routes.ALL_ALIASES)
132        self._load_modules_from_entry_points('softlayer.cli')
133
134        self._modules_loaded = True
135
136    def load_modules_from_python(self, route_list):
137        """Load modules from the native python source."""
138        for name, modpath in route_list:
139            if ':' in modpath:
140                path, attr = modpath.split(':', 1)
141            else:
142                path, attr = modpath, None
143            self.commands[name] = ModuleLoader(path, attr=attr)
144
145    def _load_modules_from_entry_points(self, entry_point_group):
146        """Load modules from the entry_points (slower).
147
148        Entry points can be used to add new commands to the CLI.
149
150        Usage:
151
152            entry_points={'softlayer.cli': ['new-cmd = mymodule.new_cmd.cli']}
153
154        """
155        for obj in pkg_resources.iter_entry_points(group=entry_point_group,
156                                                   name=None):
157            self.commands[obj.name] = obj
158
159    def ensure_client(self, config_file=None, is_demo=False, proxy=None):
160        """Create a new SLAPI client to the environment.
161
162        This will be a no-op if there is already a client in this environment.
163        """
164        if self.client is not None:
165            return
166
167        # Environment can be passed in explicitly. This is used for testing
168        if is_demo:
169            client = SoftLayer.BaseClient(
170                transport=SoftLayer.FixtureTransport(),
171                auth=None,
172            )
173        else:
174            # Create SL Client
175            client = SoftLayer.create_client_from_env(
176                proxy=proxy,
177                config_file=config_file,
178            )
179        self.client = client
180
181
182class ModuleLoader(object):
183    """Module loader that acts a little like an EntryPoint object."""
184
185    def __init__(self, import_path, attr=None):
186        self.import_path = import_path
187        self.attr = attr
188
189    def load(self):
190        """load and return the module/attribute."""
191        module = importlib.import_module(self.import_path)
192        if self.attr:
193            return getattr(module, self.attr)
194        return module
195
196
197pass_env = click.make_pass_decorator(Environment, ensure=True)
198