videosdkagent-cli 0.0.1__py2.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.
@@ -0,0 +1,58 @@
1
+ import os
2
+ import aiohttp
3
+ from videosdk_cli.utils.config_manager import get_config_value
4
+ from videosdk_cli.utils.apis.error import AuthenticationError, APIRequestError
5
+
6
+ class VideoSDKAsyncClient:
7
+ def __init__(self):
8
+ self.auth_token = get_config_value("VIDEOSDK_AUTH_TOKEN") or ""
9
+ self.base_url = os.environ.get(
10
+ "API_BASE_URL", "https://api.videosdk.live"
11
+ )
12
+
13
+ self.headers = {
14
+ "Authorization": self.auth_token,
15
+ "Content-Type": "application/json",
16
+ }
17
+
18
+ async def _request(
19
+ self,
20
+ method: str,
21
+ endpoint: str,
22
+ *,
23
+ session: aiohttp.ClientSession | None = None,
24
+ **kwargs,
25
+ ):
26
+ """
27
+ Common aiohttp request handler
28
+ """
29
+ url = endpoint if endpoint.startswith("http") else f"{self.base_url}{endpoint}"
30
+ close_session = False
31
+
32
+ if session is None:
33
+ session = aiohttp.ClientSession(headers=self.headers)
34
+ close_session = True
35
+
36
+ try:
37
+ async with session.request(method, url, **kwargs) as resp:
38
+ # 🔐 Common auth handling
39
+ if resp.status in (401, 403):
40
+ raise AuthenticationError(
41
+ "Authentication failed. Please check your VIDEOSDK_AUTH_TOKEN."
42
+ )
43
+
44
+ if resp.status >= 400:
45
+ text = await resp.text()
46
+ raise APIRequestError(
47
+ f"Request failed [{resp.status}]: {text}"
48
+ )
49
+
50
+ # auto-detect JSON vs text
51
+ content_type = resp.headers.get("Content-Type", "")
52
+ if "application/json" in content_type:
53
+ return await resp.json()
54
+ return await resp.text()
55
+
56
+ finally:
57
+ if close_session:
58
+ await session.close()
@@ -0,0 +1,49 @@
1
+ import aiohttp
2
+
3
+
4
+ class AuthAPIClient:
5
+ """
6
+ Client for VideoSDK CLI authentication flow.
7
+ Responsible ONLY for talking to your backend auth APIs.
8
+ """
9
+
10
+ def __init__(self, base_url: str = "http://localhost:8000"):
11
+ self.base_url = base_url.rstrip("/")
12
+
13
+ async def start_auth(self) -> dict:
14
+ """
15
+ Starts the auth flow.
16
+
17
+ Expected response:
18
+ {
19
+ "verify_url": "...",
20
+ "request_id": "...",
21
+ "expires_in": 600
22
+ }
23
+ """
24
+ async with aiohttp.ClientSession() as session:
25
+ async with session.post(f"{self.base_url}/v2/cli/request/start") as resp:
26
+ resp.raise_for_status()
27
+ return await resp.json()
28
+
29
+ async def check_status(self, requestId: str) -> dict:
30
+ """
31
+ Polls auth status.
32
+
33
+ Expected approved response:
34
+ {
35
+ "status": "approved",
36
+ "project_token": "VIDEOSDK_AUTH_TOKEN"
37
+ }
38
+
39
+ Other responses:
40
+ { "status": "pending" }
41
+ { "status": "expired" }
42
+ """
43
+ async with aiohttp.ClientSession() as session:
44
+ async with session.get(
45
+ f"{self.base_url}/v2/cli/request/status",
46
+ params={"requestId": requestId},
47
+ ) as resp:
48
+ resp.raise_for_status()
49
+ return await resp.json()
@@ -0,0 +1,66 @@
1
+ import json
2
+ import click
3
+ from pathlib import Path
4
+
5
+ CONFIG_DIR = Path(click.get_app_dir("videosdk_config"))
6
+ CONFIG_FILE = CONFIG_DIR / "config.json"
7
+
8
+
9
+
10
+ def load_config():
11
+ """
12
+ Load the configuration from the standard user config directory.
13
+ For Windows : C:/Users/User_name/AppData/Roaming/videosdk_config/config.json
14
+ For mac/linux : ~/.config/videosdk_config/config.json
15
+
16
+ Returns:
17
+ dict or None: Configuration dictionary, or None if file is empty or missing.
18
+ """
19
+ if not CONFIG_FILE.is_file():
20
+ return None
21
+
22
+ try:
23
+ with open(CONFIG_FILE, "r") as f:
24
+ content = f.read().strip()
25
+ if not content:
26
+ return None
27
+ return json.loads(content)
28
+
29
+ except json.JSONDecodeError:
30
+ print(f"[WARNING] Config file at {CONFIG_FILE} is invalid. A new one will be created on next save.")
31
+ return None
32
+
33
+ except Exception as e:
34
+ print(f"[ERROR] An unexpected error occurred while loading the config file: {e}")
35
+ return None
36
+
37
+
38
+
39
+
40
+ def save_config(config: dict):
41
+ """Save the configuration to the standard user config directory."""
42
+ try:
43
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
44
+ with open(CONFIG_FILE, "w") as f:
45
+ json.dump(config, f, indent=2)
46
+
47
+ except Exception as e:
48
+ print(f"[ERROR] Unable to save config to {CONFIG_FILE}: {e}")
49
+
50
+
51
+
52
+ def get_config_value(key: str, default=None):
53
+ """A helper function to safely get a single value from the config."""
54
+ config = load_config()
55
+
56
+ if not config:
57
+ return default
58
+ return config.get(key, default)
59
+
60
+
61
+
62
+ def set_config_value(key: str, value):
63
+ """A helper function to safely set a single value in the config."""
64
+ config = load_config() or {}
65
+ config[key] = value
66
+ save_config(config)
@@ -0,0 +1,3 @@
1
+ class AuthenticationError(Exception):
2
+ """Custom exception for API authentication failures (e.g., 401 Unauthorized)."""
3
+ pass
@@ -0,0 +1,381 @@
1
+ from genericpath import exists
2
+ import click
3
+ from typing import Optional
4
+ from pathlib import Path
5
+ from dotenv import dotenv_values
6
+ import os
7
+ import re
8
+ from videosdk_cli.utils.apis.deployment_client import DeploymentClient
9
+ from videosdk_cli.utils.videosdk_yaml_helper import update_agent_config
10
+ from videosdk_cli.utils.videosdk_yaml_helper import AgentConfig
11
+ from rich.console import Console
12
+ from InquirerPy import inquirer
13
+ from rich.table import Table
14
+ from videosdk_cli.utils.ui.kv_blocks import print_kv_blocks
15
+ console = Console()
16
+
17
+ async def init_agent(app_dir: Path, name: Optional[str] = None):
18
+ client = DeploymentClient()
19
+
20
+ # Call backend
21
+ response = await client.agent_init(name)
22
+
23
+ agent_id = response.get("agentId")
24
+ agent_name = response.get("name") or "agent-test"
25
+
26
+ if not agent_id or not agent_name:
27
+ raise RuntimeError("Invalid response from agent_init API")
28
+
29
+ # Build AgentConfig dataclass
30
+ agent_config = AgentConfig(
31
+ id=agent_id,
32
+ name=agent_name,
33
+ )
34
+
35
+ # Update videosdk.yaml
36
+ update_agent_config(
37
+ app_dir=app_dir,
38
+ agent_config=agent_config,
39
+ )
40
+
41
+ return response
42
+
43
+
44
+ async def list_deployment(deployment_id: Optional[str] = None,head: Optional[int] = None,tail: Optional[int] = None):
45
+ client = DeploymentClient()
46
+ try:
47
+ response = await client.agent_list(deployment_id,head,tail)
48
+ deployments = response.get("deployments", [])
49
+
50
+ if not deployments:
51
+ console.print("[bold red]No deployments found.[/bold red]")
52
+ return
53
+
54
+ table = Table(title="Deployments", show_lines=True)
55
+
56
+ table.add_column("Name", style="cyan", no_wrap=True)
57
+ table.add_column("Region", style="green")
58
+ table.add_column("Profile", style="magenta")
59
+ table.add_column("Deployment ID", style="yellow",no_wrap=True)
60
+ table.add_column("Created At", style="white")
61
+
62
+
63
+ for d in deployments:
64
+ created = d["createdAt"].replace("T", " ").replace("Z", "")
65
+
66
+ table.add_row(
67
+ d["name"],
68
+ d["region"],
69
+ d["profile"],
70
+ d["deploymentId"],
71
+ created
72
+ )
73
+
74
+ console.print(table)
75
+ return response
76
+ except Exception as e:
77
+ raise e
78
+
79
+ async def list_agents_manager(agent_id: Optional[str] = None,head: Optional[int] = None,tail: Optional[int] = None):
80
+ client = DeploymentClient()
81
+ try:
82
+ print(agent_id)
83
+ response = await client.agent_list(agent_id,head,tail)
84
+ deployments = response.get("deployments", [])
85
+
86
+ if not deployments:
87
+ console.print("[bold red]No deployments found.[/bold red]")
88
+ return
89
+
90
+ table = Table(title="Deployments", show_lines=True)
91
+
92
+ table.add_column("Name", style="cyan", no_wrap=True)
93
+ table.add_column("Region", style="green")
94
+ table.add_column("Profile", style="magenta")
95
+ table.add_column("Deployment ID", style="yellow",no_wrap=True)
96
+ table.add_column("Created At", style="white")
97
+
98
+
99
+ for d in deployments:
100
+ created = d["createdAt"].replace("T", " ").replace("Z", "")
101
+
102
+ table.add_row(
103
+ d["name"],
104
+ d["region"],
105
+ d["profile"],
106
+ d["deploymentId"],
107
+ created
108
+ )
109
+
110
+ console.print(table)
111
+ return response
112
+ except Exception as e:
113
+ raise e
114
+
115
+ async def describe_agent_manager(agent_id:str,deployment_id:Optional[str] = None):
116
+ client = DeploymentClient()
117
+ try:
118
+ response = await client.agent_describe(agent_id,deployment_id)
119
+ data_to_print = {}
120
+ if response:
121
+ data_to_print = {
122
+ "Agent ID": response.get("agentId",'N/A'),
123
+ "Name": response.get("name",'N/A'),
124
+ "Region": response.get("region",'N/A'),
125
+ "Profile": response.get("profile",'N/A'),
126
+ "Deployment ID": response.get("deploymentId",'N/A'),
127
+ "Created At": response.get("createdAt",'N/A').replace("T", " ").replace("Z", ""),
128
+ "minReplica": response.get("minReplica",'N/A'),
129
+ "maxReplica": response.get("maxReplica",'N/A'),
130
+ "imageCR": response.get("image",{}).get("imageCR",'N/A'),
131
+ "imageUrl": response.get("image",{}).get("imageUrl",'N/A'),
132
+ "Active": response.get("active",'N/A'),
133
+ }
134
+
135
+ print_kv_blocks([data_to_print])
136
+
137
+ except Exception as e:
138
+ raise e
139
+
140
+ def parse_env_file(env_file: Path) -> dict:
141
+ if not env_file.exists():
142
+ raise click.ClickException("Env file does not exist")
143
+
144
+ env_vars = dotenv_values(env_file)
145
+
146
+ if not env_vars:
147
+ raise click.ClickException("Env file is empty or invalid")
148
+
149
+
150
+ # Convert OrderedDict -> dict
151
+ return dict(env_vars)
152
+
153
+ async def secret_set_manager(name:str,file:Optional[str] = None):
154
+ client = DeploymentClient()
155
+ try:
156
+ console.print(f"Secret Name: [green]{name}[/green]")
157
+
158
+ # 🔒 Validate secret name
159
+ if not re.fullmatch(r"[a-z0-9_-]+", name):
160
+ console.print(
161
+ "❌ Invalid secret name.\n"
162
+ "Rules:\n"
163
+ " • must be lowercase\n"
164
+ " • no spaces\n"
165
+ " • allowed characters: a-z, 0-9, -, _",
166
+ )
167
+ raise click.Abort()
168
+
169
+ secrets = {}
170
+
171
+ if file:
172
+ console.print(f"File: [green]{file}[/green]")
173
+ secrets = parse_env_file(Path(os.getcwd()) / file)
174
+
175
+ else:
176
+ while True:
177
+ key = click.prompt("Enter key", type=str)
178
+ if not re.fullmatch(r"^[A-Za-z0-9_-]+$", key):
179
+ console.print(
180
+ "❌ Invalid key.\n"
181
+ "Rules:\n"
182
+ " • must be lowercase\n"
183
+ " • no spaces\n"
184
+ " • allowed characters: a-z, 0-9, -, _",
185
+ )
186
+ continue
187
+
188
+ value = click.prompt("Enter value", type=str, hide_input=True)
189
+
190
+ secrets[key] = value
191
+
192
+ choice = inquirer.select(
193
+ message="Add another secret?",
194
+ choices=["Yes", "No"],
195
+ default="No",
196
+ ).execute()
197
+
198
+ if choice == "No":
199
+ break
200
+
201
+ if not secrets:
202
+ console.print("No secrets provided. Aborting.")
203
+ return
204
+
205
+ console.print("\nSecrets to be saved:")
206
+ for k in secrets:
207
+ console.print(f"- {k}: ******")
208
+
209
+ action = inquirer.select(
210
+ message="Confirm action",
211
+ choices=["Save secrets", "Cancel"],
212
+ default="Save secrets",
213
+ ).execute()
214
+
215
+ if action == "Cancel":
216
+ console.print("[yellow]Cancelled. No secrets were saved.[/yellow]")
217
+ return
218
+
219
+ console.print("\nSaving secrets...")
220
+ response = await client.secret_set(name, secrets)
221
+ console.print("Secrets saved successfully.")
222
+ except Exception as e:
223
+ raise e
224
+
225
+
226
+ async def list_secret_manager():
227
+ client = DeploymentClient()
228
+ try:
229
+ console.print("[bold blue]Listing secrets...[/bold blue]")
230
+ response = await client.secret_list()
231
+ data = response.get("data", [])
232
+
233
+ if not data:
234
+ console.print("[bold red]No secrets found.[/bold red]")
235
+ return
236
+
237
+ table = Table(title="Secrets", show_lines=True)
238
+
239
+ table.add_column("name", style="cyan", no_wrap=True)
240
+ table.add_column("secretId", style="green")
241
+ table.add_column("type", style="magenta")
242
+
243
+ for d in data:
244
+ table.add_row(
245
+ d.get('name','N/A'),
246
+ d.get('secretId','N/A'),
247
+ d.get('type','N/A'),
248
+ )
249
+
250
+ console.print(table)
251
+ console.print("Secrets listed successfully.")
252
+ except Exception as e:
253
+ raise e
254
+
255
+ async def remove_secret_set(name: str):
256
+ client = DeploymentClient()
257
+ try:
258
+ console.print("[bold blue]Removing secret...[/bold blue]")
259
+ response = await client.secret_remove(name)
260
+ console.print("Secret removed successfully.")
261
+ except Exception as e:
262
+ raise e
263
+
264
+ async def describe_secret_set(name: str):
265
+ client = DeploymentClient()
266
+ try:
267
+ console.print("[bold blue]Getting secret...[/bold blue]")
268
+ response = await client.secret_get(name)
269
+ data_to_print = {}
270
+ data=response.get("data", {})
271
+ if data:
272
+ data_to_print = {
273
+ "Name": data.get("name",'N/A'),
274
+ "Secret ID": data.get("secretId",'N/A'),
275
+ "type": data.get("type",'N/A'),
276
+ }
277
+ print_kv_blocks([data_to_print])
278
+ # 2️⃣ Keys table
279
+ keys = data.get("keys", {})
280
+
281
+ if not keys:
282
+ console.print("\n[dim]No keys found[/dim]")
283
+ return
284
+
285
+ table = Table(title="Secret Keys", show_header=True, header_style="bold")
286
+
287
+ table.add_column("Key", style="cyan", no_wrap=True)
288
+ table.add_column("Value", style="green")
289
+
290
+ for k in keys:
291
+ table.add_row(
292
+ k,
293
+ keys.get(k,'N/A')
294
+ )
295
+
296
+ console.print(table)
297
+ except Exception as e:
298
+ raise e
299
+
300
+ async def add_secret_set(name: str):
301
+ client = DeploymentClient()
302
+ try:
303
+ console.print("[bold blue]Adding secret...[/bold blue]")
304
+ secrets = {}
305
+
306
+ while True:
307
+ key = click.prompt("Enter key", type=str)
308
+ value = click.prompt("Enter value", type=str, hide_input=True)
309
+
310
+ secrets[key] = value
311
+
312
+ choice = inquirer.select(
313
+ message="Add another secret?",
314
+ choices=["Yes", "No"],
315
+ default="No",
316
+ ).execute()
317
+
318
+ if choice == "No":
319
+ break
320
+
321
+ if not secrets:
322
+ console.print("No secrets provided. Aborting.")
323
+ return
324
+
325
+ console.print("\nSecrets to be saved:")
326
+ for k in secrets:
327
+ console.print(f"- {k}: ******")
328
+
329
+ action = inquirer.select(
330
+ message="Confirm action",
331
+ choices=["Save secrets", "Cancel"],
332
+ default="Save secrets",
333
+ ).execute()
334
+
335
+ if action == "Cancel":
336
+ console.print("Cancelled. No secrets were saved.", fg="yellow")
337
+ return
338
+
339
+ response = await client.secret_add_key(name, secrets)
340
+
341
+ console.print("Secret added successfully.")
342
+ except Exception as e:
343
+ raise e
344
+
345
+ async def remove_secret_set_key(name: str):
346
+ client = DeploymentClient()
347
+ try:
348
+ console.print("[bold blue]Removing secret...[/bold blue]")
349
+ keys=[]
350
+ while True:
351
+ key = click.prompt("Enter key", type=str)
352
+ keys.append(key)
353
+
354
+ choice = inquirer.select(
355
+ message="Remove another key?",
356
+ choices=["Yes", "No"],
357
+ default="No",
358
+ ).execute()
359
+
360
+ if choice == "No":
361
+ break
362
+ if not keys:
363
+ console.print("No keys provided.")
364
+ return
365
+ response = await client.secret_remove_key(name,keys)
366
+ console.print("Secret removed successfully.")
367
+ except Exception as e:
368
+ raise e
369
+
370
+ async def image_pull_secret_manager(name: str):
371
+ client = DeploymentClient()
372
+ try:
373
+ console.print("[bold blue]Setting image pull secret...[/bold blue]")
374
+ console.print(f"Secret Name: [green]{name}[/green]")
375
+ server =click.prompt("Enter server name", type=str)
376
+ username =click.prompt("Enter username", type=str)
377
+ password =click.prompt("Enter password", type=str,hide_input=True)
378
+ response = await client.secret_set(name,{"serverName":server,"USERNAME":username,"PASSWORD":password},type="IMAGE_PULL")
379
+ console.print("Image pull secret set successfully.")
380
+ except Exception as e:
381
+ raise e
@@ -0,0 +1,87 @@
1
+ import yaml
2
+ import click
3
+ import os
4
+ from pathlib import Path
5
+
6
+ def load_project_config(search_path=None):
7
+ """
8
+ Load the project configuration from videosdk.yaml or videosdk.yml.
9
+
10
+ Args:
11
+ search_path (Path, optional): Directory to search in. Defaults to current working directory.
12
+
13
+ Returns:
14
+ dict or None: Configuration dictionary if found, None otherwise.
15
+ """
16
+ if search_path is None:
17
+ search_path = Path(os.getcwd())
18
+ else:
19
+ search_path = Path(search_path)
20
+
21
+ config_path = search_path / "videosdk.yaml"
22
+ if not config_path.exists():
23
+ config_path = search_path / "videosdk.yml"
24
+
25
+ if not config_path.exists():
26
+ return None
27
+
28
+ try:
29
+ with open(config_path, 'r') as f:
30
+ return yaml.safe_load(f)
31
+ except Exception as e:
32
+ # We perform a lenient load here. Validation errors should generally be handled
33
+ # by the caller or result in None/empty dict depending on strictness required.
34
+ # But per requirements we usually just want the config if valid.
35
+ # For now, if it fails to parse, we might return None or let the error bubble?
36
+ # The prompt said "if not there in yaml we will throw error" only if required config is missing.
37
+ # So if the file exists but is invalid, that's trickier.
38
+ # Let's print a warning and return None for now to be safe,
39
+ # preventing a crash if the file is just empty or garbage.
40
+ click.echo(f"[WARNING] Failed to load config from {config_path}: {e}", err=True)
41
+ return None
42
+
43
+ def get_config_option(cli_value, config_keys, default=None, required=True, fail_message=None):
44
+ """
45
+ Get a configuration option resolving priority: CLI > videosdk.yaml > Error (if required) > Default.
46
+
47
+ Args:
48
+ cli_value: The value provided via CLI (e.g. from click.option).
49
+ config_keys (list[str]): List of keys to navigate the config dict (e.g. ['agent', 'image']).
50
+ default: Default value if not found.
51
+ required (bool): If True, raise UsageError if value is not found in CLI or config.
52
+ fail_message (str, optional): Custom error message for UsageError.
53
+
54
+ Returns:
55
+ The resolved value.
56
+ """
57
+ # 1. Check CLI value
58
+ if cli_value is not None:
59
+ return cli_value
60
+
61
+ # 2. Check Project Config
62
+ config = load_project_config()
63
+ if config:
64
+ value = config
65
+ for key in config_keys:
66
+ if isinstance(value, dict):
67
+ value = value.get(key)
68
+ else:
69
+ value = None
70
+ break
71
+
72
+ if value is not None:
73
+ return value
74
+
75
+ # 3. Check Default
76
+ if default is not None:
77
+ return default
78
+
79
+ # 4. Fail if required/no default
80
+ if required:
81
+ if fail_message:
82
+ raise click.UsageError(fail_message)
83
+ else:
84
+ key_path = ".".join(config_keys)
85
+ raise click.UsageError(f"Missing required configuration: '{key_path}'. Please provide it via CLI or videosdk.yaml.")
86
+
87
+ return None