|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: LGPL-2.1-or-later |
| 4 | +# |
| 5 | +# Copyright (C) 2023 Collabora Limited |
| 6 | +# Author: Guillaume Tucker <[email protected]> |
| 7 | + |
| 8 | +"""KernelCI Command Line Tool |
| 9 | +
|
| 10 | +This executable script is the entry point for all the new KernelCI command line |
| 11 | +tools which support the new API & Pipeline design. See the documentation for |
| 12 | +more details: https://kernelci.org/docs/api/. |
| 13 | +""" |
| 14 | + |
| 15 | +import json |
| 16 | +import os |
| 17 | + |
| 18 | +import toml |
| 19 | +import click |
| 20 | + |
| 21 | +import kernelci.api |
| 22 | +import kernelci.config |
| 23 | + |
| 24 | + |
| 25 | +class Settings: |
| 26 | + |
| 27 | + def __init__(self, path=None, default_group_name='DEFAULT'): |
| 28 | + """TOML settings |
| 29 | +
|
| 30 | + `path` is the path to the TOML settings file |
| 31 | + `default_group_name` is the name of the default group |
| 32 | + """ |
| 33 | + if path is None: |
| 34 | + default_paths = [ |
| 35 | + os.getenv('KCI_SETTINGS', ''), |
| 36 | + 'kernelci.toml', |
| 37 | + os.path.expanduser('~/.config/kernelci/kernelci.toml'), |
| 38 | + '/etc/kernelci/kernelci.toml', |
| 39 | + 'kernelci.conf', |
| 40 | + os.path.expanduser('~/.config/kernelci/kernelci.conf'), |
| 41 | + '/etc/kernelci/kernelci.conf', |
| 42 | + ] |
| 43 | + for default_path in default_paths: |
| 44 | + if os.path.exists(default_path): |
| 45 | + path = default_path |
| 46 | + break |
| 47 | + self._path = path |
| 48 | + self._settings = toml.load(path) if os.path.exists(path or '') else {} |
| 49 | + self._default = self._settings.get(default_group_name, {}) |
| 50 | + self._group = {} |
| 51 | + |
| 52 | + @property |
| 53 | + def path(self): |
| 54 | + """Path to the TOML settings file""" |
| 55 | + return self._path |
| 56 | + |
| 57 | + def set_group(self, path): |
| 58 | + self._group = self.get_raw(*path) or {} |
| 59 | + |
| 60 | + def get(self, key): |
| 61 | + """Get a TOML settings value |
| 62 | +
|
| 63 | + `key` is the name of the settings key |
| 64 | + `group_path` is the name of the group where to find the key |
| 65 | + """ |
| 66 | + value = self._group.get(key) |
| 67 | + if value is None: |
| 68 | + value = self._default.get(key) |
| 69 | + return value |
| 70 | + |
| 71 | + def get_section(self, section): |
| 72 | + """Get a settings group for a particular config section |
| 73 | +
|
| 74 | + `section` is the name of the configuration section e.g. 'api' |
| 75 | + """ |
| 76 | + section_name = self.get(section) |
| 77 | + if section_name is None: |
| 78 | + raise ValueError(f"No section name specified for {section}") |
| 79 | + section_group = self._settings.get(section, {}) |
| 80 | + return section_group.get(section_name, {}) |
| 81 | + |
| 82 | + def get_from_section(self, section, key): |
| 83 | + """Get a settings value from a particular config section |
| 84 | +
|
| 85 | + `section` is the name of the configuration section e.g. 'api' |
| 86 | + `key` is the name of the settings within that group |
| 87 | + """ |
| 88 | + return self.get_section(section).get(key) |
| 89 | + |
| 90 | + def get_raw(self, *args): |
| 91 | + """Get a settings value from an arbitrary series of keys |
| 92 | +
|
| 93 | + The *args are a series of strings for the path, e.g. ('foo', 'bar', |
| 94 | + 'baz') will look for a foo.bar.baz value or baz within [foo.bar] etc. |
| 95 | + """ |
| 96 | + data = self._settings |
| 97 | + for arg in args: |
| 98 | + data = data.get(arg, {}) |
| 99 | + return data |
| 100 | + |
| 101 | + |
| 102 | +class CommandSettings: |
| 103 | + """Settings object passed to commands via the context""" |
| 104 | + |
| 105 | + class SectionFinder: |
| 106 | + """Helper class to find a section""" |
| 107 | + class Group: |
| 108 | + """Helper class to find a key within a group""" |
| 109 | + def __init__(self, group): |
| 110 | + self._group = group |
| 111 | + |
| 112 | + def __getattr__(self, key): |
| 113 | + return self._group.get(key) |
| 114 | + |
| 115 | + def __init__(self, settings): |
| 116 | + self._settings = settings |
| 117 | + |
| 118 | + def __getattr__(self, section): |
| 119 | + return self.Group(self._settings.get_section(section)) |
| 120 | + |
| 121 | + def __init__(self, settings_path): |
| 122 | + self._settings = Settings(settings_path) |
| 123 | + self._sections = self.SectionFinder(self.settings) |
| 124 | + |
| 125 | + def __getattr__(self, key): |
| 126 | + """Get a settings value for the current command group""" |
| 127 | + return self.get(key) |
| 128 | + |
| 129 | + @property |
| 130 | + def section(self): |
| 131 | + """Shortcut to get a settings section""" |
| 132 | + return self._sections |
| 133 | + |
| 134 | + @property |
| 135 | + def settings(self): |
| 136 | + """TOML Settings object""" |
| 137 | + return self._settings |
| 138 | + |
| 139 | + def set_group(self, path): |
| 140 | + """Set the group based on the current command name""" |
| 141 | + self._settings.set_group(path) |
| 142 | + |
| 143 | + def get(self, key): |
| 144 | + """Get a settings value like __getattr__()""" |
| 145 | + return self._settings.get(key) |
| 146 | + |
| 147 | + |
| 148 | +class Command(click.Command): |
| 149 | + """Wrapper command to load settings and populate default values""" |
| 150 | + |
| 151 | + def _walk_name(self, ctx): |
| 152 | + name = (ctx.info_name,) |
| 153 | + if ctx.parent: |
| 154 | + return self._walk_name(ctx.parent) + name |
| 155 | + return name |
| 156 | + |
| 157 | + def invoke(self, ctx): |
| 158 | + ctx.obj.set_group(self._walk_name(ctx)) |
| 159 | + for key, value in ctx.params.items(): |
| 160 | + if value is None: |
| 161 | + ctx.params[key] = ctx.obj.get(key) |
| 162 | + super().invoke(ctx) |
| 163 | + |
| 164 | + |
| 165 | +@click.group() |
| 166 | +@click.option('--settings', type=str, help="Path to the TOML settings file") |
| 167 | +@click.pass_context |
| 168 | +def kci(ctx, settings): |
| 169 | + """Entry point for the kci command line tool""" |
| 170 | + ctx.info_name = 'kci' # HACK because this file is called kci-click... |
| 171 | + ctx.obj = CommandSettings(settings) |
| 172 | + |
| 173 | + |
| 174 | +class Args: |
| 175 | + """Standard command line arguments""" |
| 176 | + api = click.option('--api', type=str, help="Name of the API config entry") |
| 177 | + config = click.option('--config', type=str, help="Path to the YAML config") |
| 178 | + verbose = click.option('--verbose/--no-verbose', type=bool, default=None) |
| 179 | + |
| 180 | + |
| 181 | +@kci.command(cls=Command, help="whoami with API authentication") |
| 182 | +@Args.config |
| 183 | +@Args.api |
| 184 | +@Args.verbose |
| 185 | +@click.pass_context |
| 186 | +def whoami(ctx, config, api, verbose): |
| 187 | + if verbose: |
| 188 | + click.echo(f"API {api}") |
| 189 | + click.echo(f"API {ctx.obj.api}") |
| 190 | + click.echo( |
| 191 | + len(ctx.obj.settings.get_raw('api', 'early-access', 'token')) |
| 192 | + ) |
| 193 | + click.echo(len(ctx.obj.settings.get_from_section('api', 'token'))) |
| 194 | + click.echo(len(ctx.obj.section.api.token)) |
| 195 | + |
| 196 | + configs = kernelci.config.load(config) |
| 197 | + api_config = configs['api_configs'][api] |
| 198 | + api = kernelci.api.get_api(api_config, ctx.obj.section.api.token) |
| 199 | + data = api.whoami() |
| 200 | + click.echo(json.dumps(data, indent=2)) |
| 201 | + |
| 202 | + |
| 203 | +@kci.command(cls=Command) |
| 204 | +@Args.verbose |
| 205 | +@click.option('--bingo', type=int) |
| 206 | +def hack(verbose, bingo): |
| 207 | + click.echo(f"HACK VERBOSE {verbose}") |
| 208 | + click.echo(f"HACK BINGO {bingo}") |
| 209 | + |
| 210 | + |
| 211 | +@kci.group() |
| 212 | +def foo(): |
| 213 | + click.echo("FOO command group") |
| 214 | + |
| 215 | + |
| 216 | +@foo.command(cls=Command) |
| 217 | +@click.option('--baz', type=int) |
| 218 | +@Args.verbose |
| 219 | +def bar(baz, verbose): |
| 220 | + if verbose: |
| 221 | + click.echo(f"FOO BAR BAZ: {baz}") |
| 222 | + else: |
| 223 | + click.echo(baz) |
| 224 | + |
| 225 | + |
| 226 | +if __name__ == '__main__': |
| 227 | + kci() |
0 commit comments