Skip to content

Commit 1561319

Browse files
committed
[RFC] kci-click: add kci PoC using Click
Add a kci-click proof-of-concept replacement for kci using the Click package. It comes with a sample kernelci-click.toml settings file to illustrate how the values can be combined with the command line arguments. Some sample commands: $ KCI_SETTINGS=kernelci-click.toml ./kci-click foo bar --verbose FOO command group FOO BAR BAZ: 789 $ ./kci-click whoami --no-verbose { "id": "64ef04e7391d44b7fa620d13", "active": true, "profile": { "username": "admin", "hashed_password": "<hashed-password>", "groups": [ { "id": "6499aa9da02fef8143c1feb0", "name": "admin" } ], "email": "[email protected]" } } Signed-off-by: Guillaume Tucker <[email protected]>
1 parent 77c16d9 commit 1561319

File tree

2 files changed

+245
-0
lines changed

2 files changed

+245
-0
lines changed

kci-click

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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()

kernelci-click.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[DEFAULT]
2+
verbose = true
3+
api = 'early-access'
4+
storage = 'staging-azure'
5+
6+
[kci.hack]
7+
bingo = 1234
8+
9+
[api]
10+
early-access.token = '1234abcde'
11+
12+
# Alternative:
13+
# [api.early-access]
14+
# token = '1234abcd'
15+
16+
[kci.foo.bar]
17+
baz = 789
18+
verbose = false

0 commit comments

Comments
 (0)