clarity-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,174 @@
1
+ from clarity_cli.outputs import CliOutput, CliTable
2
+ from clarity_cli.input import CliInputs
3
+ from clarity_cli.helpers import (is_logged_in, get_tests, trigger_test_execution, write_new_profile_to_config,
4
+ get_profile_configuration, get_clarity_access_token, get_devices, format_device_state, get_params)
5
+ from clarity_cli.defs import TableColumn, ThemeColors, CONFIG_FILE, ProfileConfig, InputTypes
6
+ from clarity_cli.exceptions import StopCommand
7
+
8
+
9
+ class CliCommands():
10
+ def __init__(self):
11
+ self.out = CliOutput()
12
+ self.input = CliInputs()
13
+
14
+ def _get_current_profile_and_config(self, profile=None, override_config_path=None):
15
+ config, profile_name = self.out.run_sync_function("Validate profile...",
16
+ get_profile_configuration, profile=profile, override_config_path=override_config_path)
17
+ return config, profile_name
18
+
19
+ def execute(self, ctx, test_id=None, profile=None, override_config_path=None, project=None, workspace=None, agent_id=None, parameters_file=None):
20
+ """Execute a test from the available tests"""
21
+ self.out.setup_context(ctx)
22
+ config, profile_name = self._get_current_profile_and_config(profile, override_config_path)
23
+ web_token = is_logged_in(profile_name)
24
+
25
+ project = project or config.project
26
+ workspace = workspace or config.workspace
27
+ agent_id = agent_id or config.agent
28
+
29
+ if not project:
30
+ self.out.vprint("project wasn't provided, asking user")
31
+ project = self.input.ask_for_text_input("Project id")
32
+ if not workspace:
33
+ self.out.vprint("workspace id wasn't provided, asking user")
34
+ workspace = self.input.ask_for_text_input("Workspace id")
35
+
36
+ self.out.main_message("Test Execution")
37
+
38
+ # Display available tests in a table
39
+ test_execution_columns = [TableColumn("ID", ThemeColors.CYAN),
40
+ TableColumn("Name", ThemeColors.GREEN),
41
+ TableColumn("Version", ThemeColors.RED),
42
+ TableColumn("Description", ThemeColors.YELLOW)]
43
+ tests_table = CliTable("Available Tests", test_execution_columns)
44
+ available_tests, params = self.out.run_sync_function("Getting Tests...", get_tests, token=web_token, workspace_id=workspace, domain=config.domain)
45
+
46
+ selected_test = None
47
+
48
+ # If test ID is provided directly, use it
49
+ if test_id:
50
+ selected_test = next((test for test in available_tests if test["test_id"] == test_id), None)
51
+ if not selected_test:
52
+ raise StopCommand(f"Error: Test with ID '{test_id}' not found")
53
+
54
+ self.input.ask_for_confirmation(f"Execute test '{selected_test['test_name']}' ({test_id})?")
55
+
56
+ # If no test ID provided, use interactive selection
57
+ else:
58
+ tests_table.add_data(available_tests,
59
+ headers_mapping={"ID": "test_id", "Name": "test_name", "Version": "test_version", "Description": "description"})
60
+ tests_table.print_table()
61
+ # Create choices for the test selection
62
+ choices = [f"{test['test_id']} - {test['test_name']}, {test['test_version']}" for test in available_tests]
63
+ selected = self.input.ask_for_input_from_list("Select a test to execute", choices)
64
+
65
+ # Get the selected test ID
66
+ selected_test_id = selected.split(' - ')[0]
67
+ selected_test = next((test for test in available_tests if test["test_id"] == selected_test_id), None)
68
+
69
+ if not selected_test:
70
+ raise StopCommand("Error: Could not find the selected test")
71
+
72
+ if selected_test.get('iot_device_required'):
73
+ if not agent_id:
74
+ self.out.vprint("agent id wasn't provided, asking user")
75
+ devices_column = [TableColumn("Name", ThemeColors.CYAN),
76
+ TableColumn("ID"),
77
+ TableColumn("Version"),
78
+ TableColumn("Status")]
79
+ available_devices = self.out.run_sync_function("Getting devices...", get_devices, token=web_token, workspace_id=workspace, domain=config.domain)
80
+ devices_by_name = {}
81
+ for device in available_devices:
82
+ format_device_state(device)
83
+ devices_by_name[device['device_name']] = device
84
+ devices_table = CliTable("Available Devices", devices_column)
85
+ devices_table.add_data(available_devices,
86
+ headers_mapping={"ID": "device_id", "Name": "device_name", "Version": "iot_client_version", "Status": "state"})
87
+ devices_table.print_table()
88
+ agent_name = self.input.ask_for_input_from_list("Please select an agent id", devices_by_name.keys())
89
+ agent_id = devices_by_name.get(agent_name)
90
+ if not agent_id:
91
+ raise StopCommand("Agent was not provided, please ")
92
+
93
+ self.out.main_message("Configure flow parameters")
94
+ self.out.vprint(f"test flow param config:\n{params[selected_test_id].get('properties', {})}")
95
+ if parameters_file:
96
+ self.out.vprint(f"Loading parameters file: {parameters_file}")
97
+ params_from_config = get_params(parameters_file)
98
+
99
+ for param_name, param in params[selected_test_id].get('properties', {}).items():
100
+ if param.get('type') in list(InputTypes.__members__):
101
+ input_value = self.input.ask_for_text_input(param_name, default=param.get('default'))
102
+ if params_from_config.get(param_name):
103
+ self.out.vprint(f"Got param: {param_name} from file, value: {params_from_config[param_name]}")
104
+ param['default'] = params_from_config[param_name]
105
+ else:
106
+ self.out.vprint(f"Got param: {param_name} user: { InputTypes[param['type']].value(input_value)}")
107
+ param['default'] = InputTypes[param['type']].value(input_value)
108
+ if not params[selected_test_id]:
109
+ self.out.warning("Flow variables weren't found")
110
+ # Execute the test (in a real app, this would trigger an actual test)
111
+ web_token = is_logged_in(profile_name)
112
+ execution_url = self.out.run_sync_function(f"Executing: {selected_test['test_name']}...",
113
+ function=trigger_test_execution, domain=config.domain, token=web_token, workspace=workspace,
114
+ project_id=project, agent_id=agent_id, test_id=selected_test_id, params=params[selected_test_id])
115
+
116
+ self.out.ok(f"Successfully executed test: {CliOutput.bold(selected_test['test_name'])}")
117
+ self.out.print(f"[dim]Results will be available in the dashboard: {execution_url}[/dim]")
118
+
119
+ def write_profile_to_config(self, ctx, profile_name=None, client_id=None, client_secret=None, token_endpoint=None,
120
+ scope=None, project=None, workspace=None, agent_id=None, domain=None, default=None):
121
+ self.out.setup_context(ctx)
122
+ self.out.main_message("New Profile")
123
+ if not profile_name:
124
+ self.out.vprint("profile name wasn't provided, asking user")
125
+ profile_name = self.input.ask_for_text_input("Profile name")
126
+ if not client_id:
127
+ self.out.vprint("client_id wasn't provided, asking user")
128
+ client_id = self.input.ask_for_text_input("Client id")
129
+ if not client_secret:
130
+ self.out.vprint("client_secret wasn't provided, asking user")
131
+ client_secret = self.input.ask_for_password_input("Client secret")
132
+ if not token_endpoint:
133
+ self.out.vprint("token endpoint wasn't provided, asking user")
134
+ token_endpoint = self.input.ask_for_password_input("Token endpoint")
135
+ if not scope:
136
+ self.out.vprint("scope wasn't provided, asking user")
137
+ scope = self.input.ask_for_password_input("Scope")
138
+ if not domain:
139
+ self.out.vprint("domain wasn't provided, asking user")
140
+ domain = self.input.ask_for_text_input("Clarity domain")
141
+ if not project:
142
+ self.out.vprint("project wasn't provided, asking user")
143
+ project = self.input.ask_for_text_input("Project id", optional=True)
144
+ if not workspace:
145
+ self.out.vprint("workspace provided, asking user")
146
+ workspace = self.input.ask_for_text_input("Workspace id", optional=True)
147
+ if not agent_id:
148
+ self.out.vprint("agent_id wasn't provided, asking user")
149
+ agent_id = self.input.ask_for_text_input("Agent id", optional=True)
150
+ if default is None:
151
+ default = self.input.ask_for_confirmation("Set this profile as default?", default=False)
152
+ if profile_name and client_id and client_secret and token_endpoint and scope and domain:
153
+ columns = [TableColumn("Property", ThemeColors.CYAN), TableColumn("Value", ThemeColors.GREEN)]
154
+ table = CliTable("New Profile", columns)
155
+ table.add_data([("Profile name", profile_name), ("Client id", client_id), ("Client secret", client_secret), ("Clarity domain", domain),
156
+ ("Default Project id", project), ("Default workspace id", workspace), ("Default agent id", agent_id), ("Is default", str(default))])
157
+ table.print_table()
158
+ self.input.ask_for_confirmation("Setup the new profile?", default=True, hard_stop=True)
159
+ else:
160
+ raise StopCommand("Missing property")
161
+
162
+ config = ProfileConfig(client_id=client_id, client_secret=client_secret, domain=domain, token_endpoint=token_endpoint, scope=scope,
163
+ project=project, workspace=workspace, agent_id=agent_id)
164
+ self.out.run_sync_function(f"Configuring new profile {profile_name}...",
165
+ write_new_profile_to_config, profile_name=profile_name, default=default, config=config)
166
+ self.out.ok(f"New profile {CliOutput.bold(profile_name)} successfully configured")
167
+ self.out.vprint(f"All profiles can be found at: {CONFIG_FILE}")
168
+
169
+ def login_using_config_file(self, ctx, profile=None, override_config_path=None):
170
+ self.out.setup_context(ctx)
171
+ config, profile = self._get_current_profile_and_config(profile, override_config_path)
172
+ self.out.run_sync_function("Logging in", get_clarity_access_token, profile=profile, client_id=config.client_id,
173
+ client_secret=config.client_secret, token_endpoint=config.token_endpoint, scope=config.scope)
174
+ self.out.ok("Successfully logged in!")
clarity_cli/defs.py ADDED
@@ -0,0 +1,47 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+ from typing import Optional
4
+ from enum import Enum
5
+ from pydantic import BaseModel
6
+
7
+
8
+ # Define the base directory for storing configuration
9
+ CONFIG_DIR = Path.home() / '.clarity'
10
+ CONFIG_FILE = CONFIG_DIR / 'config.json'
11
+ TOKEN_FILE = CONFIG_DIR / 'token.json'
12
+
13
+ EXECUTION_VIEW_PATH = 'testing/executions/view?executionId={execution_id}&projectId={project_id}'
14
+ DEVICES_LIST = 'inv-api/devices/status'
15
+ TEST_LIST = 'inv-api/testing/catalog'
16
+
17
+
18
+ class ThemeColors(Enum):
19
+ GREEN = "green"
20
+ YELLOW = "yellow"
21
+ BLUE = "blue"
22
+ CYAN = "cyan"
23
+ WHITE = "white"
24
+ RED = "red"
25
+
26
+
27
+ @dataclass
28
+ class TableColumn:
29
+ name: str
30
+ color: Optional[ThemeColors] = ThemeColors.WHITE
31
+
32
+
33
+ class ProfileConfig(BaseModel):
34
+ client_id: str
35
+ client_secret: str
36
+ token_endpoint: str
37
+ scope: str
38
+ project: Optional[str] = ""
39
+ workspace: Optional[str] = ""
40
+ agent: Optional[str] = ""
41
+ domain: str
42
+
43
+
44
+ class InputTypes(Enum):
45
+ integer = int
46
+ string = str
47
+ boolean = bool
@@ -0,0 +1,14 @@
1
+ from clarity_cli.outputs import CliOutput
2
+
3
+ out = CliOutput()
4
+
5
+
6
+ class StopCommand(Exception):
7
+ def __init__(self, *args):
8
+ out.error(*args)
9
+ exit(0)
10
+
11
+
12
+ class UnAuthenticated(Exception):
13
+ def __init__(self, *args):
14
+ raise StopCommand(f"You need to login first. Run {CliOutput.bold('clarity login')}")
clarity_cli/helpers.py ADDED
@@ -0,0 +1,217 @@
1
+ import json
2
+ import jwt
3
+ import datetime
4
+ import base64
5
+ import requests
6
+ from clarity_cli.outputs import CliOutput
7
+ from clarity_cli.defs import CONFIG_DIR, TOKEN_FILE, CONFIG_FILE, ProfileConfig, EXECUTION_VIEW_PATH, TEST_LIST, DEVICES_LIST
8
+ from clarity_cli.exceptions import StopCommand, UnAuthenticated
9
+ out = CliOutput()
10
+
11
+
12
+ def ensure_config_dir():
13
+ """Ensure the config directory exists"""
14
+ if not CONFIG_DIR.exists():
15
+ CONFIG_DIR.mkdir(parents=True)
16
+ out.ok("Created configuration directory at ~/.clarity")
17
+
18
+
19
+ def is_logged_in(profile):
20
+ """Check if the user is logged in by verifying the token"""
21
+ if not TOKEN_FILE.exists():
22
+ raise UnAuthenticated()
23
+ out.vprint("Token file found")
24
+ try:
25
+ with open(TOKEN_FILE, 'r') as f:
26
+ token_data = json.load(f)
27
+
28
+ # Verify token hasn't expired
29
+ token = token_data.get(profile)
30
+ if not token:
31
+ raise UnAuthenticated()
32
+ out.vprint("Token was loaded successfully")
33
+ # Decode the token without verification to check expiration
34
+ decoded = jwt.decode(token, options={"verify_signature": False})
35
+ out.vprint("Token was parsed successfully")
36
+ exp_timestamp = decoded.get('exp', datetime.datetime.now(datetime.timezone.utc))
37
+
38
+ # Check if token has expired
39
+ if datetime.datetime.fromtimestamp(exp_timestamp, tz=datetime.timezone.utc) < datetime.datetime.now(datetime.timezone.utc):
40
+ out.warning(
41
+ "Your session has expired. Please login again.")
42
+ raise UnAuthenticated()
43
+
44
+ return token
45
+
46
+ except Exception as e:
47
+ out.error(f"Error checking login status: {str(e)}")
48
+ raise UnAuthenticated()
49
+
50
+
51
+ def format_device_state(device):
52
+ if device['state'] == 'connected':
53
+ device['state'] = f"[green]{device['state']}[/green]"
54
+ if device['state'] == 'disconnected':
55
+ device['state'] = f"[red]{device['state']}[/red]"
56
+ else:
57
+ device['state'] = f"[yellow]{device['state']}[/yellow]"
58
+
59
+
60
+ def get_devices(token, workspace_id, domain):
61
+ headers = {"Authorization": f"Bearer {token}", "workspace": workspace_id}
62
+ url = f"{domain}/{DEVICES_LIST}"
63
+ response = requests.get(url, headers=headers)
64
+ if response.status_code == 200:
65
+ res = response.json()
66
+ devices = res['device_list']
67
+ return devices
68
+ else:
69
+ out.vprint("Error getting devices list")
70
+ out.vprint(response.text)
71
+ raise StopCommand("Couldn't get devices list")
72
+
73
+
74
+ def get_tests(token, workspace_id, domain):
75
+ headers = {"Authorization": f"Bearer {token}", "workspace": workspace_id}
76
+ url = f"{domain}/{TEST_LIST}"
77
+ response = requests.get(url, headers=headers)
78
+ # Check if the request was successful
79
+ param_res = {}
80
+ if response.status_code == 200:
81
+ res = response.json()
82
+ for test in res:
83
+ param_res[test['test_id']] = test['params_schema']
84
+ return res, param_res
85
+ else:
86
+ out.vprint("Error getting tests list")
87
+ out.vprint(response.text)
88
+ raise StopCommand("Couldn't get Test list")
89
+
90
+
91
+ def trigger_test_execution(domain, token, workspace, project_id, agent_id, test_id, params):
92
+ headers = {"Authorization": f"Bearer {token}", "workspace": workspace}
93
+ url = f"{domain}/inv-api/testing/executions/handler"
94
+ data = {
95
+ "project_id": project_id,
96
+ "agent_id": agent_id,
97
+ "test_plan_config": [{
98
+ "params_config": {},
99
+ "test_id": test_id,
100
+ "test_config": {'variables_config': params}
101
+ }
102
+ ],
103
+ }
104
+ response = requests.post(url, headers=headers, json=data)
105
+ # Check if the request was successful
106
+ if response.status_code == 202:
107
+ res = response.json()
108
+ out.ok("Execution started")
109
+ out.vprint(f"execution id: {res['execution_id']}, project id: {res['project_id']}")
110
+ # return URL to track the execution
111
+ return f"{domain}/{EXECUTION_VIEW_PATH.format(execution_id=res['execution_id'], project_id=res['project_id'])}"
112
+ else:
113
+ out.vprint("Status code:", response.status_code)
114
+ raise StopCommand(f"Failed to start execution, {response.text}")
115
+
116
+
117
+ def get_clarity_access_token(profile, client_id, client_secret, token_endpoint, scope):
118
+ # The client credentials grant requires HTTP Basic Authentication,
119
+ # where the username is the client ID and the password is the client secret
120
+ try:
121
+ client_auth = base64.b64encode(f"{client_id}:{client_secret}".encode("utf-8")).decode("utf-8")
122
+ headers = {
123
+ "Content-Type": "application/x-www-form-urlencoded",
124
+ "Authorization": f"Basic {client_auth}",
125
+ }
126
+
127
+ # The grant_type must be client_credentials
128
+ data = {
129
+ "grant_type": "client_credentials",
130
+ "scope": scope,
131
+ }
132
+ response = requests.post(token_endpoint, headers=headers, data=data)
133
+
134
+ if response.status_code != 200:
135
+ out.verbose(response.text)
136
+ raise StopCommand("Failed to get access token")
137
+
138
+ response_data = response.json()
139
+ out.vprint(f"Got token: {response_data['access_token']}")
140
+ write_token(response_data["access_token"], profile)
141
+ except Exception as e:
142
+ out.vprint(f"Login error - {e}")
143
+ raise StopCommand("Unable to login")
144
+
145
+
146
+ def read_config(config_override=None):
147
+ config_to_use = config_override or CONFIG_FILE
148
+ try:
149
+ out.vprint(f"Reading config file: {config_to_use}")
150
+ with open(config_to_use, "r") as f:
151
+ return json.load(f)
152
+ except Exception:
153
+ return {}
154
+
155
+
156
+ def read_token():
157
+ try:
158
+ with open(TOKEN_FILE, "r") as f:
159
+ return json.load(f)
160
+ except Exception:
161
+ return {}
162
+
163
+
164
+ def get_params(params_file):
165
+ try:
166
+ with open(params_file, "r") as f:
167
+ return json.load(f)
168
+ except Exception:
169
+ return {}
170
+
171
+
172
+ def write_new_profile_to_config(profile_name: str, default: bool, config: ProfileConfig):
173
+ try:
174
+ cur_config = read_config()
175
+ new_profile = {
176
+ profile_name: config.model_dump()
177
+ }
178
+ full_config = {**cur_config, **new_profile}
179
+ full_config['default'] = profile_name if default else full_config.get('default', "")
180
+ with open(CONFIG_FILE, "w") as f:
181
+ json.dump(full_config, f)
182
+ except Exception as e:
183
+ StopCommand(f"An error occurred while writing new profile to config file - {e}")
184
+
185
+
186
+ def write_token(token, profile):
187
+ tokens = read_token()
188
+ tokens[profile] = token
189
+ try:
190
+ out.vprint(f"writing token to {TOKEN_FILE}, for profile: {profile}")
191
+ with open(TOKEN_FILE, 'w') as f:
192
+ json.dump(tokens, f)
193
+ except Exception as e:
194
+ StopCommand(f"Unable to write tmp token to: {TOKEN_FILE} - {e}")
195
+
196
+
197
+ def get_profile_configuration(profile=None, override_config_path=None):
198
+ current_configuration = read_config(override_config_path)
199
+ if not profile:
200
+ out.warning("Profile was not provided, using default profile")
201
+ profile = current_configuration.get('default')
202
+ if profile:
203
+ out.ok(f"Default profile found, using {profile} profile")
204
+ else:
205
+ raise StopCommand("Default profile wasn't configured, please specify a profile")
206
+ try:
207
+ profile_config = ProfileConfig(**current_configuration.get(profile, {}))
208
+ if not profile_config:
209
+ out.error(f"profile {CliOutput.bold(profile)} was not found")
210
+ out.warning("use "+{CliOutput.bold('\'clarity profile-setup\'')} + "command to set it up")
211
+ except Exception as e:
212
+ # is_valid_config = all([True if key in profile_config else False for key in ['client_id', 'client_secret', 'token_endpoint', 'scope']])
213
+ # if not is_valid_config:
214
+ out.vprint(f"Error parsing config - {e}")
215
+ out.warning("use "+{CliOutput.bold('\'clarity profile-setup\'')} + "command to set it up")
216
+ raise StopCommand("Profile configuration is not valid, please reconfigure or use another profile")
217
+ return profile_config, profile
clarity_cli/input.py ADDED
@@ -0,0 +1,47 @@
1
+ import inquirer
2
+ from inquirer import themes
3
+ from clarity_cli.exceptions import StopCommand
4
+ from clarity_cli.outputs import CliOutput
5
+
6
+ out = CliOutput()
7
+
8
+
9
+ class CliInputs():
10
+
11
+ def ask_for_confirmation(self, confirmation_message, default=True, hard_stop=False):
12
+ questions = [
13
+ inquirer.Confirm('confirm', message=f"{confirmation_message}", default=default)]
14
+
15
+ answers = inquirer.prompt(questions, theme=themes.GreenPassion())
16
+ if not answers:
17
+ out.vprint("Command was canceled")
18
+ raise StopCommand()
19
+ if hard_stop and not answers['confirm']:
20
+ out.vprint("Command was not confirmed")
21
+ raise StopCommand()
22
+ return answers['confirm']
23
+
24
+ def ask_for_input_from_list(self, message, options: list):
25
+ questions = [inquirer.List('list_input', message=message, choices=options)]
26
+ answers = inquirer.prompt(questions, theme=themes.GreenPassion())
27
+ if not answers or not answers['list_input']:
28
+ out.warning("Command was canceled")
29
+ raise StopCommand()
30
+ return answers.get('list_input')
31
+
32
+ def ask_for_text_input(self, message, default="", optional=False):
33
+ final_message = f"{message} [Optional]" if optional else message
34
+ questions = [inquirer.Text('text_input', message=final_message, default=default)]
35
+ answers = inquirer.prompt(questions, theme=themes.Default())
36
+ if not answers or (optional and not answers['text_input']):
37
+ out.warning("Command was canceled")
38
+ raise StopCommand()
39
+ return answers.get('text_input')
40
+
41
+ def ask_for_password_input(self, message):
42
+ questions = [inquirer.Password('pass_input', message=message)]
43
+ answers = inquirer.prompt(questions, theme=themes.Default())
44
+ if not answers or not answers['pass_input']:
45
+ out.warning("Command was canceled")
46
+ raise StopCommand()
47
+ return answers.get('pass_input')
clarity_cli/main.py ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Clarity CLI - Interactive CLI for test management and execution
4
+
5
+ This CLI tool provides functionality for:
6
+ - Login (generates and stores JWT tokens)
7
+ - Executing tests (displays and allows selection from available tests)
8
+ """
9
+
10
+ # import json
11
+ # import datetime
12
+ # import uuid
13
+ import click
14
+ # import inquirer
15
+ # from inquirer import themes
16
+ # from rich.panel import Panel
17
+ # from rich.table import Table
18
+ # import jwt
19
+ from clarity_cli.helpers import ensure_config_dir
20
+ from clarity_cli.commands import CliCommands
21
+
22
+
23
+ commands = CliCommands()
24
+
25
+
26
+ @click.group()
27
+ @click.option('--verbose', '-v', is_flag=True, help='Verbose mode')
28
+ @click.pass_context
29
+ def cli(ctx, verbose=False):
30
+ """Clarity CLI - Interactive test management and execution tool"""
31
+ ctx.ensure_object(dict)
32
+ ctx.obj['VERBOSE'] = verbose
33
+ ensure_config_dir()
34
+
35
+
36
+ @cli.command()
37
+ @click.option('--test-id', '-t', help='Specify a test ID to execute directly')
38
+ @click.option('--profile', '-p', help='Specify a name for the new profile')
39
+ @click.option('--override-config-path', '-c', help='Path to config.json, default config stored in ~/.config.json')
40
+ @click.option('--project', '-proj', help='Specify the default project id for clarity operations')
41
+ @click.option('--workspace', '-w', help='Specify the default workspace id for clarity operations')
42
+ @click.option('--agent-id', '-a', help='Specify the default agent id for executions')
43
+ @click.option('--parameters-file', help="Path to a json file that includes all the flow variables values for execution, if flow var doesn't exist using the default value")
44
+ @click.pass_context
45
+ def execute(ctx, test_id=None, profile=None, override_config_path=None, project=None, workspace=None, agent_id=None, parameters_file=None):
46
+ commands.execute(ctx, test_id, profile, override_config_path, project, workspace, agent_id, parameters_file)
47
+
48
+
49
+ @cli.command()
50
+ @click.option('--profile', '-p', help='Specify a name for the new profile')
51
+ @click.option('--client-id', '-id', help='Specify the client ID you got from cyclarity portal')
52
+ @click.option('--client-secret', '-cs', help='Specify the client secret you got from cyclarity portal')
53
+ @click.option('--token-endpoint', '-e', help='Specify the token endpoint you got from cyclarity portal')
54
+ @click.option('--scope', '-s', help='Specify the scope you got from cyclarity portal')
55
+ @click.option('--project', '-proj', help='Specify the default project id for clarity operations')
56
+ @click.option('--workspace', '-w', help='Specify the default workspace id for clarity operations')
57
+ @click.option('--agent_id', '-a', help='Specify the default agent id for executions')
58
+ @click.option('--default', '-d', help='Use this profile as default profile')
59
+ @click.pass_context
60
+ def profile_setup(ctx, profile=None, client_id=None, client_secret=None, token_endpoint=None, scope=None, project=None, workspace=None, agent_id=None, default=None):
61
+ commands.write_profile_to_config(ctx, profile, client_id, client_secret, token_endpoint, scope, project, workspace, agent_id, default)
62
+
63
+
64
+ @cli.command()
65
+ @click.option('--profile', '-p', help='Profile for login, if not provided use default profile')
66
+ @click.option('--override-config-path', '-c', help='Path to config.json, default config stored in ~/.config.json')
67
+ @click.pass_context
68
+ def login(ctx, profile=None, override_config_path=None):
69
+ commands.login_using_config_file(ctx, profile, override_config_path)
70
+
71
+
72
+ if __name__ == '__main__':
73
+ cli()
clarity_cli/outputs.py ADDED
@@ -0,0 +1,67 @@
1
+ from rich.console import Console
2
+ from rich.panel import Panel
3
+ from rich.table import Table
4
+ from clarity_cli.defs import TableColumn
5
+
6
+ verbose = True # or False, depending on your use case
7
+
8
+
9
+ class CliTable():
10
+ def __init__(self, table_name, columns: list[TableColumn]):
11
+ self.out = CliOutput()
12
+ self.table = Table(title=table_name)
13
+ self.columns = columns
14
+ for column in columns:
15
+ self.table.add_column(column.name, style=column.color.value)
16
+
17
+ def add_data(self, data: list[dict[str, str]], headers_mapping: dict[str, str]):
18
+ for row in data:
19
+ self.table.add_row(*[row.get(headers_mapping[col.header], "") for col in self.table.columns])
20
+
21
+ def print_table(self):
22
+ self.out.print(self.table)
23
+
24
+
25
+ class CliOutput():
26
+ _instance = None
27
+ console: Console = None
28
+
29
+ def __new__(cls, *args, **kwargs):
30
+ if cls._instance is None:
31
+ cls._instance = super(CliOutput, cls).__new__(cls)
32
+ cls._instance.verbose = False # Default value
33
+ cls._instance.console = Console()
34
+ return cls._instance
35
+
36
+ def setup_context(self, ctx):
37
+ self.verbose = ctx.obj.get('VERBOSE')
38
+
39
+ def ok(self, message=None):
40
+ if message:
41
+ self.print(f"[green]{message}[/green]")
42
+
43
+ def error(self, message=None):
44
+ if message:
45
+ self.print(f"[red][bold]{message}[bold][/red]")
46
+
47
+ def warning(self, message=None):
48
+ if message:
49
+ self.print(f"[yellow]{message}[/yellow]")
50
+
51
+ def main_message(self, message):
52
+ self.print(Panel.fit(message, style="blue"))
53
+
54
+ def print(self, message):
55
+ self.console.print(message)
56
+
57
+ def vprint(self, message):
58
+ if self.verbose:
59
+ self.print(f"[dim]{message}[/dim]")
60
+
61
+ @staticmethod
62
+ def bold(message):
63
+ return f"[bold]{message}[/bold]"
64
+
65
+ def run_sync_function(self, message, function, **args):
66
+ with self.console.status(f"[bold green]{message}\n"):
67
+ return function(**args)
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.3
2
+ Name: clarity-cli
3
+ Version: 0.1.0
4
+ Summary: Interactive CLI for test management and execution
5
+ License: MIT
6
+ Author: Clarity Team
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Dist: click (>=8.1.3,<9.0.0)
15
+ Requires-Dist: inquirer (>=3.1.3,<4.0.0)
16
+ Requires-Dist: pydantic (>=2.11.4,<3.0.0)
17
+ Requires-Dist: pyjwt (>=2.6.0,<3.0.0)
18
+ Requires-Dist: requests (>=2.32.3,<3.0.0)
19
+ Requires-Dist: rich (>=13.3.5,<14.0.0)
20
+ Description-Content-Type: text/markdown
21
+
22
+
@@ -0,0 +1,12 @@
1
+ clarity_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ clarity_cli/commands.py,sha256=SF_S2LqL_NO4kTlpzgbR7PmPGSlVDN78Su9zLJk0xkk,10824
3
+ clarity_cli/defs.py,sha256=qtZqH5ZaY1uLBw9Ok3JHPVgTH--rwicpGGfzdOeU7Xg,1038
4
+ clarity_cli/exceptions.py,sha256=WCZDMf3WsvC0bpyEQJ--ag1nQbB7HeqR0RY90oKN_jM,325
5
+ clarity_cli/helpers.py,sha256=L_B_4ckXnyrGEE7bXd3atkD9jWXcZ8yJbXZtzbLAEEI,8101
6
+ clarity_cli/input.py,sha256=k_gknkG9YjiF20oV86Nt6_7Pw1_82rrLXLZIvdSbHQk,1974
7
+ clarity_cli/main.py,sha256=x_hsp3Y__kNY-MsjH4diD-4IItEJeRS5Rr4S4zdSr6c,3448
8
+ clarity_cli/outputs.py,sha256=le52pwNQI1bW3e9X7M6iN8GleKEMdRlGj7EqMNsUKs0,2023
9
+ clarity_cli-0.1.0.dist-info/METADATA,sha256=6ue_CAF6R3SRGjD_uUdyl5o692ARdGsxmAXIzl6NgpY,762
10
+ clarity_cli-0.1.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
11
+ clarity_cli-0.1.0.dist-info/entry_points.txt,sha256=aDG82xIqh9xMP0O9nRcQan9ifvufwFZFNbNVJA7PlVU,48
12
+ clarity_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ clarity=clarity_cli.main:cli
3
+