meshagent-cli 0.21.0__py3-none-any.whl → 0.23.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.
meshagent/cli/services.py CHANGED
@@ -9,7 +9,7 @@ from aiohttp import ClientResponseError
9
9
  import pathlib
10
10
  from meshagent.cli import async_typer
11
11
  from meshagent.api.services import well_known_service_path
12
- from meshagent.api.specs.service import ServiceSpec
12
+ from meshagent.api.specs.service import ServiceSpec, ServiceTemplateSpec
13
13
  from meshagent.api.keys import parse_api_key
14
14
 
15
15
  import asyncio
@@ -35,6 +35,7 @@ from meshagent.api import (
35
35
  )
36
36
  from meshagent.cli.common_options import OutputFormatOption
37
37
 
38
+ from pydantic import RootModel
38
39
  from pydantic_yaml import parse_yaml_raw_as
39
40
 
40
41
 
@@ -44,6 +45,32 @@ from meshagent.cli.call import _make_call
44
45
  app = async_typer.AsyncTyper(help="Manage services for your project")
45
46
 
46
47
 
48
+ class ServiceTemplateValues(RootModel[dict[str, str]]):
49
+ pass
50
+
51
+
52
+ def _load_template_values(
53
+ values_file: Optional[str],
54
+ values: Optional[list[str]] = None,
55
+ ) -> dict[str, str]:
56
+ template_values: dict[str, str] = {}
57
+
58
+ if values_file is not None:
59
+ with open(str(pathlib.Path(values_file).expanduser().resolve()), "rb") as f:
60
+ template_values = parse_yaml_raw_as(ServiceTemplateValues, f.read()).root
61
+
62
+ if values:
63
+ for item in values:
64
+ if "=" not in item:
65
+ raise typer.BadParameter("Template values must be key=value")
66
+ key, value = item.split("=", 1)
67
+ if not key:
68
+ raise typer.BadParameter("Template values must include a key")
69
+ template_values[key] = value
70
+
71
+ return template_values
72
+
73
+
47
74
  @app.async_command("create")
48
75
  async def service_create(
49
76
  *,
@@ -173,6 +200,213 @@ async def service_update(
173
200
  await client.close()
174
201
 
175
202
 
203
+ @app.async_command("validate")
204
+ async def service_validate(
205
+ *,
206
+ file: Annotated[
207
+ str,
208
+ typer.Option("--file", "-f", help="File path to a service definition"),
209
+ ],
210
+ ):
211
+ """Validate a service spec from a YAML file."""
212
+ try:
213
+ with open(str(pathlib.Path(file).expanduser().resolve()), "rb") as f:
214
+ spec = parse_yaml_raw_as(ServiceSpec, f.read())
215
+ except Exception as exc:
216
+ print(f"[red]Invalid service spec: {exc}[/red]")
217
+ raise typer.Exit(code=1)
218
+
219
+ print(f"[green]Service spec is valid:[/] {spec.metadata.name}")
220
+
221
+
222
+ @app.async_command("create-template")
223
+ async def service_create_template(
224
+ *,
225
+ project_id: ProjectIdOption,
226
+ file: Annotated[
227
+ str,
228
+ typer.Option("--file", "-f", help="File path to a service template"),
229
+ ],
230
+ values: Annotated[
231
+ Optional[str],
232
+ typer.Option("--values-file", help="File path to template values"),
233
+ ] = None,
234
+ value: Annotated[
235
+ Optional[list[str]],
236
+ typer.Option(
237
+ "--value",
238
+ "-v",
239
+ help="Template value override (key=value)",
240
+ ),
241
+ ] = None,
242
+ room: Annotated[
243
+ Optional[str],
244
+ typer.Option("--room", help="The name of a room to create the service for"),
245
+ ] = None,
246
+ ):
247
+ """Create a service from a ServiceTemplate spec."""
248
+ client = await get_client()
249
+ try:
250
+ project_id = await resolve_project_id(project_id)
251
+
252
+ with open(str(pathlib.Path(file).expanduser().resolve()), "rb") as f:
253
+ template = f.read()
254
+
255
+ template_values = _load_template_values(values, value)
256
+
257
+ try:
258
+ if room is None:
259
+ service = await client.create_service_from_template(
260
+ project_id=project_id, template=template, values=template_values
261
+ )
262
+ else:
263
+ service = await client.create_room_service_from_template(
264
+ project_id=project_id,
265
+ template=template,
266
+ values=template_values,
267
+ room_name=room,
268
+ )
269
+ except ClientResponseError as exc:
270
+ if exc.status == 409:
271
+ print(
272
+ f"[red]Service name already in use: {template.metadata.name}[/red]"
273
+ )
274
+ raise typer.Exit(code=1)
275
+ raise
276
+ else:
277
+ service_id = service.id or ""
278
+ print(f"[green]Created service:[/] {service_id}")
279
+
280
+ finally:
281
+ await client.close()
282
+
283
+
284
+ @app.async_command("update-template")
285
+ async def service_update_template(
286
+ *,
287
+ project_id: ProjectIdOption,
288
+ id: Optional[str] = None,
289
+ file: Annotated[
290
+ str,
291
+ typer.Option("--file", "-f", help="File path to a service template"),
292
+ ],
293
+ values: Annotated[
294
+ Optional[str],
295
+ typer.Option("--values-file", help="File path to template values"),
296
+ ] = None,
297
+ value: Annotated[
298
+ Optional[list[str]],
299
+ typer.Option(
300
+ "--value",
301
+ "-v",
302
+ help="Template value override (key=value)",
303
+ ),
304
+ ] = None,
305
+ create: Annotated[
306
+ Optional[bool],
307
+ typer.Option(
308
+ help="create the service if it does not exist",
309
+ ),
310
+ ] = False,
311
+ room: Annotated[
312
+ Optional[str],
313
+ typer.Option("--room", help="The name of a room to update the service for"),
314
+ ] = None,
315
+ ):
316
+ """Update a service using a ServiceTemplate spec."""
317
+ client = await get_client()
318
+ try:
319
+ project_id = await resolve_project_id(project_id)
320
+
321
+ with open(str(pathlib.Path(file).expanduser().resolve()), "rb") as f:
322
+ template = f.read()
323
+
324
+ template_values = _load_template_values(values, value)
325
+
326
+ try:
327
+ if id is None:
328
+ if room is None:
329
+ services = await client.list_services(project_id=project_id)
330
+ else:
331
+ services = await client.list_room_services(
332
+ project_id=project_id, room_name=room
333
+ )
334
+
335
+ for s in services:
336
+ if s.metadata.name == template.metadata.name:
337
+ id = s.id
338
+
339
+ if id is None and not create:
340
+ print("[red]pass a service id or specify --create[/red]")
341
+ raise typer.Exit(code=1)
342
+
343
+ if id is None:
344
+ if room is None:
345
+ service = await client.create_service_from_template(
346
+ project_id=project_id,
347
+ template=template,
348
+ values=template_values,
349
+ )
350
+ else:
351
+ service = await client.create_room_service_from_template(
352
+ project_id=project_id,
353
+ template=template,
354
+ values=template_values,
355
+ room_name=room,
356
+ )
357
+ id = service.id
358
+ else:
359
+ if room is None:
360
+ service = await client.update_service_from_template(
361
+ project_id=project_id,
362
+ service_id=id,
363
+ template=template,
364
+ values=template_values,
365
+ )
366
+ else:
367
+ service = await client.update_room_service_from_template(
368
+ project_id=project_id,
369
+ service_id=id,
370
+ template=template,
371
+ values=template_values,
372
+ room_name=room,
373
+ )
374
+ if service.id is not None:
375
+ id = service.id
376
+
377
+ except ClientResponseError as exc:
378
+ if exc.status == 409:
379
+ print(
380
+ f"[red]Service name already in use: {template.metadata.name}[/red]"
381
+ )
382
+ raise typer.Exit(code=1)
383
+ raise
384
+ else:
385
+ print(f"[green]Updated service:[/] {id}")
386
+
387
+ finally:
388
+ await client.close()
389
+
390
+
391
+ @app.async_command("validate-template")
392
+ async def service_validate_template(
393
+ *,
394
+ file: Annotated[
395
+ str,
396
+ typer.Option("--file", "-f", help="File path to a service template"),
397
+ ],
398
+ ):
399
+ """Validate a service template from a YAML file."""
400
+ try:
401
+ with open(str(pathlib.Path(file).expanduser().resolve()), "rb") as f:
402
+ template = parse_yaml_raw_as(ServiceTemplateSpec, f.read())
403
+ except Exception as exc:
404
+ print(f"[red]Invalid service template: {exc}[/red]")
405
+ raise typer.Exit(code=1)
406
+
407
+ print(f"[green]Service template is valid:[/] {template.metadata.name}")
408
+
409
+
176
410
  @app.async_command("run")
177
411
  async def service_run(
178
412
  *,
@@ -291,7 +525,7 @@ async def service_run(
291
525
 
292
526
  sys.stdout.write("\n")
293
527
 
294
- for p in spec.ports:
528
+ for p in spec.ports or []:
295
529
  print(f"[bold green]Connecting port {p.num}...[/bold green]")
296
530
 
297
531
  for endpoint in p.endpoints:
@@ -370,9 +604,7 @@ async def service_list(
370
604
  )
371
605
 
372
606
  if o == "json":
373
- print(
374
- {"services": [svc.model_dump(mode="json") for svc in services]}
375
- ).model_dump_json(indent=2)
607
+ print({"services": [svc.model_dump(mode="json") for svc in services]})
376
608
  else:
377
609
  print_json_table(
378
610
  [
@@ -398,21 +630,12 @@ async def service_delete(
398
630
  *,
399
631
  project_id: ProjectIdOption,
400
632
  service_id: Annotated[str, typer.Argument(help="ID of the service to delete")],
401
- room: Annotated[
402
- Optional[str],
403
- typer.Option("--room", help="The name of a room to delete the service for"),
404
- ] = None,
405
633
  ):
406
634
  """Delete a service."""
407
635
  client = await get_client()
408
636
  try:
409
637
  project_id = await resolve_project_id(project_id)
410
- if room is None:
411
- await client.delete_service(project_id=project_id, service_id=service_id)
412
- else:
413
- await client.delete_service(
414
- project_id=project_id, service_id=service_id, room_name=room
415
- )
638
+ await client.delete_service(project_id=project_id, service_id=service_id)
416
639
  print(f"[green]Service {service_id} deleted.[/]")
417
640
  finally:
418
641
  await client.close()