modAL 1.6.2.dev9__py3-none-any.whl → 1.6.2.dev10__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.
modal/_workspace.py CHANGED
@@ -8,7 +8,7 @@ from typing import Optional
8
8
 
9
9
  from google.protobuf.empty_pb2 import Empty
10
10
 
11
- from modal.exception import DeprecationError, InvalidError
11
+ from modal.exception import DeprecationError, InvalidError, NotFoundError
12
12
  from modal.types import ServiceUserInfo
13
13
  from modal_proto import api_pb2
14
14
 
@@ -629,8 +629,7 @@ class _WorkspaceServiceUserManager:
629
629
  service_user_name: The name of the Service User.
630
630
  environment_name: The name of the Environment to which the Service User's new role will
631
631
  apply.
632
- role: The access level to grant this Service User. Must be one of the strings `"no-access"`,
633
- `"viewer"`, or `"contributor"`.
632
+ role: The access level to grant this Service User.
634
633
 
635
634
  Examples:
636
635
 
@@ -644,6 +643,9 @@ class _WorkspaceServiceUserManager:
644
643
  ```
645
644
  """
646
645
 
646
+ if not role:
647
+ raise InvalidError("Invalid argument: `role` must not be empty.")
648
+
647
649
  if not self._workspace._is_hydrated:
648
650
  await self._workspace.hydrate()
649
651
 
@@ -663,8 +665,6 @@ class _WorkspaceServiceUserManager:
663
665
  async def info(self, name: str) -> ServiceUserInfo:
664
666
  """Get information about a specific Service User.
665
667
 
666
- To call this function, you are required to have Manager permissions in the Workspace.
667
-
668
668
  Args:
669
669
  name: The name of the Service User to introspect.
670
670
 
@@ -691,8 +691,6 @@ class _WorkspaceServiceUserManager:
691
691
  async def list(self) -> list[ServiceUserInfo]:
692
692
  """List all Service Users in the current workspace.
693
693
 
694
- To call this function, you are required to have Manager permissions in the Workspace.
695
-
696
694
  Returns:
697
695
  A list of `ServiceUserInfo` dataclasses.
698
696
 
@@ -712,13 +710,12 @@ class _WorkspaceServiceUserManager:
712
710
 
713
711
  return [ServiceUserInfo._from_proto(su) for su in resp.service_users]
714
712
 
715
- async def delete(self, name: str) -> None:
713
+ async def delete(self, name: str, *, allow_missing: bool = False) -> None:
716
714
  """Permanently delete a Service User.
717
715
 
718
- To call this function, you are required to have Manager permissions in the Workspace.
719
-
720
716
  Args:
721
717
  name: The name of the Service User to delete.
718
+ allow_missing: If `True`, do not throw an exception if the Service User does not exist.
722
719
 
723
720
  Examples:
724
721
 
@@ -731,4 +728,10 @@ class _WorkspaceServiceUserManager:
731
728
  if not self._workspace._is_hydrated:
732
729
  await self._workspace.hydrate()
733
730
 
734
- await self._workspace.client._stub.ServiceUserDelete(api_pb2.ServiceUserDeleteRequest(service_user_name=name))
731
+ try:
732
+ await self._workspace.client._stub.ServiceUserDelete(
733
+ api_pb2.ServiceUserDeleteRequest(service_user_name=name)
734
+ )
735
+ except NotFoundError:
736
+ if not allow_missing:
737
+ raise
modal/cli/workspace.py CHANGED
@@ -1,15 +1,21 @@
1
1
  # Copyright Modal Labs 2025
2
2
  import json as json_mod
3
+ from datetime import timezone
3
4
 
4
5
  import click
5
6
  import rich
6
7
  import rich.box
7
8
  import rich.table
9
+ from rich.box import SIMPLE, SIMPLE_HEAD
10
+ from rich.text import Text
8
11
 
9
- from modal._utils.time_utils import timestamp_to_localized_str
12
+ from modal._utils.async_utils import synchronizer
13
+ from modal._utils.time_utils import parse_duration, timestamp_to_localized_str
14
+ from modal._workspace import _Workspace
10
15
  from modal.cli.utils import confirm_or_suggest_yes, display_table, yes_option
11
16
  from modal.output import OutputManager
12
17
  from modal.workspace import Workspace
18
+ from modal_proto import api_pb2
13
19
 
14
20
  from ._help import ModalGroup
15
21
 
@@ -267,3 +273,361 @@ def settings_set(setting: str, value: str):
267
273
  except Exception as e:
268
274
  rich.print(f"[red]✗[/red] {setting}: {e}")
269
275
  raise SystemExit(1)
276
+
277
+
278
+ SERVICE_USERS_HELP_TEXT = """Manage Service Users. Must be workspace manager or owner.
279
+
280
+ Service users are programmatic accounts that allow automated systems to interact with Modal. They're
281
+ ideal for CI/CD pipelines, automated deployments, and other workflows that need to authenticate.
282
+
283
+ Service User credentials are typically made available to a process as environment variables:
284
+
285
+ ```
286
+ export MODAL_TOKEN_ID=your-token-id
287
+ export MODAL_TOKEN_SECRET=your-token-secret
288
+ ```
289
+
290
+ A newly created Service User has no permissions by default. In order to interact with a specific
291
+ Environment, Service Users must be granted explicit permissions.
292
+
293
+ See https://modal.com/docs/guide/service-users for more information.
294
+ """
295
+
296
+ service_users_cli = ModalGroup(name="service-users", help=SERVICE_USERS_HELP_TEXT)
297
+ workspace_cli.add_command(service_users_cli)
298
+
299
+
300
+ @service_users_cli.command("create", no_args_is_help=True)
301
+ @click.argument("name")
302
+ @click.option(
303
+ "--expires-in",
304
+ "expires_in_str",
305
+ default=None,
306
+ help="Expiration time. Accepts integers (interpreted as seconds), as well as values suffixed by "
307
+ "`s`, `m`, `h`, or `d`.",
308
+ )
309
+ @click.option("--json", "json", is_flag=True, default=False, help="Output as JSON.")
310
+ @synchronizer.create_blocking
311
+ async def service_users_create(name: str, expires_in_str: str | None, json: bool):
312
+ """Create a Service User in this workspace.
313
+
314
+ In order to run this command, you must have at least Manager level permissions in the Workspace.
315
+
316
+ By default, Service Users have no permissions. To explicitly grant the Service User permissions
317
+ to interact with a given environment, use [`modal workspace service-users set-role`](https://modal.com/docs/cli/latest/workspace#modal-workspace-service-users-set-role)
318
+ or [`modal environment roles update`](https://modal.com/docs/cli/latest/environment#modal-environment-roles-update).
319
+
320
+ Optionally, you can also specify an expiration window by passing `--expires-in`. After the specified
321
+ amount of time elapses, this Service User will be deleted, and any credentials associated with
322
+ this Service User will stop working. The option expects a positive integer value, optionally
323
+ suffixed by the units `s`, `m`, `h`, or `d`, meaning seconds, minutes, hours, and days respectively.
324
+ If no suffix is provided, the value is interpreted as seconds. If `--expires-in` is not provided,
325
+ the newly created Service User will never expire.
326
+
327
+ Examples:
328
+
329
+ ```bash
330
+ # Create a Service User named `test-1`, which never expires:
331
+ modal workspace service-users create test-1
332
+
333
+ # Two ways of creating a Service User which is valid for an hour:
334
+ modal workspace service-users create test-2 --expires-in 3600
335
+ modal workspace service-users create test-3 --expires-in 1h
336
+
337
+ # Create a Service User which is valid for 1 year, outputting JSON:
338
+ modal workspace service-users create test-4 --expires-in 365d --json
339
+ ```
340
+ """
341
+
342
+ expires_in: int | None
343
+ if expires_in_str is None:
344
+ expires_in = None
345
+ else:
346
+ try:
347
+ expires_in = int(expires_in_str)
348
+ except ValueError:
349
+ try:
350
+ expires_in = int(parse_duration(expires_in_str).total_seconds())
351
+ except ValueError as e:
352
+ raise click.UsageError(str(e)) from e
353
+
354
+ if expires_in is not None and expires_in <= 0:
355
+ raise click.UsageError("Value passed to `--expires-in` must be a positive integer.")
356
+
357
+ if expires_in is None:
358
+ req = api_pb2.ServiceUserCreateRequest(name=name)
359
+ else:
360
+ req = api_pb2.ServiceUserCreateRequest(name=name, expires_in_seconds=expires_in)
361
+
362
+ ws = _Workspace.from_context()
363
+ await ws.hydrate()
364
+
365
+ resp = await ws.client._stub.ServiceUserCreate(req)
366
+
367
+ output_manager = OutputManager.get()
368
+
369
+ if json:
370
+ output_manager.print_json(
371
+ json_mod.dumps(
372
+ {
373
+ "token_id": resp.token_id,
374
+ "token_secret": resp.token_secret,
375
+ "expires_at": timestamp_to_localized_str(resp.expires_at.ToDatetime(timezone.utc).timestamp())
376
+ if expires_in
377
+ else None,
378
+ }
379
+ )
380
+ )
381
+ return
382
+
383
+ output_manager.print(f"[green]✓[/green] Successfully created Service User `{name}`, token details below:")
384
+
385
+ table = rich.table.Table("", pad_edge=False, box=None)
386
+ table.add_row("Token ID:", resp.token_id)
387
+ table.add_row("Token Secret:", resp.token_secret)
388
+
389
+ if expires_in:
390
+ table.add_row(
391
+ "Expires At:", timestamp_to_localized_str(resp.expires_at.ToDatetime(timezone.utc).timestamp(), isotz=False)
392
+ )
393
+
394
+ output_manager.print(table)
395
+ output_manager.print("")
396
+
397
+ output_manager.print("[yellow]:warning:[/yellow] This is the only time you will be able to see the Token Secret.")
398
+
399
+
400
+ @service_users_cli.command("list")
401
+ @click.option("--json", is_flag=True, default=False, help="Output as JSON.")
402
+ def service_users_list(json: bool):
403
+ """List all Service Users in this workspace.
404
+
405
+ In order to run this command, you must have at least Manager level permissions in the Workspace.
406
+
407
+ Examples:
408
+
409
+ ```bash
410
+ # List all Service Users with a human-readable output format
411
+ modal workspace service-users list
412
+
413
+ # List all Service Users and return JSON output
414
+ modal workspace service-users list --json
415
+ ```
416
+ """
417
+
418
+ ws = Workspace.from_context()
419
+ service_users = ws.service_users.list()
420
+
421
+ output_manager = OutputManager.get()
422
+
423
+ if json:
424
+ output_manager.print_json(
425
+ json_mod.dumps(
426
+ [
427
+ {
428
+ "name": su.name,
429
+ "service_user_id": su.service_user_id,
430
+ "token_id": su.token_id,
431
+ "created_by": su.created_by,
432
+ "created_at": timestamp_to_localized_str(su.created_at.timestamp()),
433
+ "last_used_at": timestamp_to_localized_str(su.last_used_at.timestamp())
434
+ if su.last_used_at is not None
435
+ else None,
436
+ "expires_at": timestamp_to_localized_str(su.expires_at.timestamp())
437
+ if su.expires_at is not None
438
+ else None,
439
+ "environment_roles": su.environment_roles,
440
+ }
441
+ for su in service_users
442
+ ]
443
+ )
444
+ )
445
+ return
446
+
447
+ table = rich.table.Table(box=SIMPLE_HEAD)
448
+ table.add_column("Name")
449
+ table.add_column("Token ID")
450
+ table.add_column("Created By")
451
+ table.add_column("Expires")
452
+
453
+ for su in service_users:
454
+ table.add_row(
455
+ su.name,
456
+ su.token_id,
457
+ su.created_by,
458
+ timestamp_to_localized_str(su.expires_at.timestamp(), isotz=False) if su.expires_at is not None else "-",
459
+ )
460
+
461
+ output_manager.print(table)
462
+
463
+
464
+ @service_users_cli.command("delete", no_args_is_help=True)
465
+ @click.argument("name")
466
+ @click.option("--allow-missing", is_flag=True, default=False, help="Don't error if the Volume doesn't exist.")
467
+ @yes_option
468
+ def service_users_delete(name: str, *, allow_missing: bool, yes: bool):
469
+ """Delete a Service User in this Workspace.
470
+
471
+ Note that this action is irreversible. Any existing processes / scripts that use the credentials
472
+ associated with the target Service User will stop being able to authenticate with Modal.
473
+
474
+ ```bash
475
+ # Delete the Service User named `test-service-user`
476
+ modal workspace service-users delete test-service-user
477
+ ```
478
+ """
479
+ if not yes:
480
+ confirm_or_suggest_yes(
481
+ f"Are you sure you want to irrevocably delete the Service User '{name}' and its associated credentials?"
482
+ )
483
+
484
+ ws = Workspace.from_context()
485
+ ws.service_users.delete(name, allow_missing=allow_missing)
486
+
487
+ output_manager = OutputManager.get()
488
+ output_manager.print(f"[green]✓[/green] Deleted Service User `{name}`.")
489
+
490
+
491
+ @service_users_cli.command("info", no_args_is_help=True)
492
+ @click.argument("name")
493
+ @click.option("--json", is_flag=True, default=False, help="Output as JSON.")
494
+ def service_users_info(name: str, json: bool):
495
+ """Inspect a Service User in this workspace.
496
+
497
+ Examples:
498
+
499
+ ```bash
500
+ # Inspect the Service User named `test-service-user`
501
+ modal workspace service-users info test-service-user
502
+
503
+ # Inspect the Service User named `test-service-user-2`, outputting JSON
504
+ modal workspace service-users info test-service-user-2 --json
505
+ ```
506
+ """
507
+
508
+ ws = Workspace.from_context()
509
+ su = ws.service_users.info(name)
510
+
511
+ output_manager = OutputManager.get()
512
+ if json:
513
+ output_manager.print_json(
514
+ json_mod.dumps(
515
+ {
516
+ "name": su.name,
517
+ "service_user_id": su.service_user_id,
518
+ "token_id": su.token_id,
519
+ "created_by": su.created_by,
520
+ "created_at": timestamp_to_localized_str(su.created_at.timestamp()),
521
+ "last_used_at": timestamp_to_localized_str(su.last_used_at.timestamp())
522
+ if su.last_used_at is not None
523
+ else None,
524
+ "expires_at": timestamp_to_localized_str(su.expires_at.timestamp())
525
+ if su.expires_at is not None
526
+ else None,
527
+ "environment_roles": su.environment_roles,
528
+ }
529
+ )
530
+ )
531
+ return
532
+
533
+ rows: list[tuple[str, str]] = []
534
+
535
+ rows.append(("Name:", su.name))
536
+ rows.append(("Service User ID:", su.service_user_id))
537
+ rows.append(("Token ID:", su.token_id))
538
+
539
+ environment_roles: list[tuple[str, str]] = []
540
+ for env_name, role in su.environment_roles.items():
541
+ if role == "no-access":
542
+ continue
543
+
544
+ environment_roles.append((env_name, role))
545
+
546
+ rows.append(("Created By:", su.created_by))
547
+ rows.append(("Created At:", str(timestamp_to_localized_str(su.created_at.timestamp(), isotz=False))))
548
+ rows.append(
549
+ (
550
+ "Last Used:",
551
+ str(timestamp_to_localized_str(su.last_used_at.timestamp(), isotz=False))
552
+ if su.last_used_at is not None
553
+ else "-",
554
+ )
555
+ )
556
+ rows.append(
557
+ (
558
+ "Expires:",
559
+ str(timestamp_to_localized_str(su.expires_at.timestamp(), isotz=False))
560
+ if su.expires_at is not None
561
+ else "-",
562
+ )
563
+ )
564
+
565
+ if len(environment_roles) > 0:
566
+ rows.append(("", ""))
567
+ rows.append(("Environment Access:", ""))
568
+ for env_name, r in environment_roles:
569
+ rows.append((f" {env_name}", r.capitalize()))
570
+
571
+ if len(environment_roles) == 0:
572
+ output_manager.print("")
573
+ output_manager.print(" [yellow]:warning:[/yellow] This Service User does not have access to any Environments.")
574
+
575
+ table = rich.table.Table(box=SIMPLE, show_header=False)
576
+ for row in rows:
577
+ left, right = row
578
+ table.add_row(left, Text(right, justify="right"))
579
+
580
+ output_manager.print(table)
581
+
582
+
583
+ @service_users_cli.command("set-role", no_args_is_help=True)
584
+ @click.argument("service_user_name")
585
+ @click.argument("environment_name")
586
+ @click.argument("role")
587
+ def service_users_set_role(
588
+ service_user_name: str,
589
+ environment_name: str,
590
+ role: str,
591
+ ):
592
+ """Update a Service User's role in a specific Environment.
593
+
594
+ Examples:
595
+
596
+ ```bash
597
+ # Give Contributor access in Environment `main` to Service User `test-service-user`
598
+ modal workspace service-users set-role test-service-user main contributor
599
+
600
+ # Revoke access to Environment `prod` from Service User `test-service-user`
601
+ modal workspace service-users set-role test-service-user prod no-access
602
+ ```
603
+ """
604
+
605
+ ws = Workspace.from_context()
606
+ info = ws.service_users.info(service_user_name)
607
+
608
+ existing_role = info.environment_roles.get(environment_name) or "no-access"
609
+
610
+ ws.service_users.set_role(service_user_name, environment_name, role=role)
611
+
612
+ output_manager = OutputManager.get()
613
+ if role == existing_role:
614
+ if role == "no-access":
615
+ output_manager.print(
616
+ f"Service User [bold]{service_user_name}[/bold] does not have access to "
617
+ f"Environment [bold]{environment_name}[/bold] already. Role was not updated."
618
+ )
619
+ else:
620
+ output_manager.print(
621
+ f"Service User [bold]{service_user_name}[/bold] is already a {role.capitalize()} in "
622
+ f"Environment [bold]{environment_name}[/bold]. Role was not updated."
623
+ )
624
+ elif role == "no-access":
625
+ output_manager.print(
626
+ f"[green]✓[/green] Revoked Service User [bold]{service_user_name}[/bold]'s access to "
627
+ f"Environment [bold]{environment_name}[/bold]."
628
+ )
629
+ else:
630
+ output_manager.print(
631
+ f"[green]✓[/green] Updated Service User [bold]{service_user_name}[/bold]'s role in "
632
+ f"Environment [bold]{environment_name}[/bold] to {role.capitalize()}."
633
+ )
modal/client.pyi CHANGED
@@ -80,7 +80,7 @@ class _Client:
80
80
  server_url: str,
81
81
  client_type: int,
82
82
  credentials: typing.Optional[tuple[str, str]],
83
- version: str = "1.6.2.dev9",
83
+ version: str = "1.6.2.dev10",
84
84
  *,
85
85
  oauth_credentials: typing.Optional[_OAuthCredentials] = None,
86
86
  ):
@@ -290,7 +290,7 @@ class Client:
290
290
  server_url: str,
291
291
  client_type: int,
292
292
  credentials: typing.Optional[tuple[str, str]],
293
- version: str = "1.6.2.dev9",
293
+ version: str = "1.6.2.dev10",
294
294
  *,
295
295
  oauth_credentials: typing.Optional[_OAuthCredentials] = None,
296
296
  ):
modal/functions.pyi CHANGED
@@ -619,7 +619,7 @@ class Function(
619
619
 
620
620
  _call_generator: ___call_generator_spec
621
621
 
622
- class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
622
+ class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
623
623
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
624
624
  """Calls the function remotely, executing it with the given arguments and returning the execution's result.
625
625
 
@@ -644,7 +644,7 @@ class Function(
644
644
  """
645
645
  ...
646
646
 
647
- remote: __remote_spec[modal._functions.ReturnType, modal._functions.P]
647
+ remote: __remote_spec[modal._functions.P, modal._functions.ReturnType]
648
648
 
649
649
  class __remote_gen_spec(typing_extensions.Protocol):
650
650
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -694,7 +694,7 @@ class Function(
694
694
  """
695
695
  ...
696
696
 
697
- class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
697
+ class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
698
698
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
699
699
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
700
700
 
@@ -727,7 +727,7 @@ class Function(
727
727
  """
728
728
  ...
729
729
 
730
- _experimental_spawn: ___experimental_spawn_spec[modal._functions.ReturnType, modal._functions.P]
730
+ _experimental_spawn: ___experimental_spawn_spec[modal._functions.P, modal._functions.ReturnType]
731
731
 
732
732
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER]):
733
733
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> None: ...
@@ -735,7 +735,7 @@ class Function(
735
735
 
736
736
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P]
737
737
 
738
- class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
738
+ class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
739
739
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
740
740
  """Calls the function with the given arguments, without waiting for the results.
741
741
 
@@ -768,7 +768,7 @@ class Function(
768
768
  """
769
769
  ...
770
770
 
771
- spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P]
771
+ spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType]
772
772
 
773
773
  @property
774
774
  def _raw_f_(self) -> collections.abc.Callable[..., typing.Any]: ...
modal/workspace.pyi CHANGED
@@ -807,8 +807,7 @@ class WorkspaceServiceUserManager:
807
807
  service_user_name: The name of the Service User.
808
808
  environment_name: The name of the Environment to which the Service User's new role will
809
809
  apply.
810
- role: The access level to grant this Service User. Must be one of the strings `"no-access"`,
811
- `"viewer"`, or `"contributor"`.
810
+ role: The access level to grant this Service User.
812
811
 
813
812
  Examples:
814
813
 
@@ -832,8 +831,7 @@ class WorkspaceServiceUserManager:
832
831
  service_user_name: The name of the Service User.
833
832
  environment_name: The name of the Environment to which the Service User's new role will
834
833
  apply.
835
- role: The access level to grant this Service User. Must be one of the strings `"no-access"`,
836
- `"viewer"`, or `"contributor"`.
834
+ role: The access level to grant this Service User.
837
835
 
838
836
  Examples:
839
837
 
@@ -854,8 +852,6 @@ class WorkspaceServiceUserManager:
854
852
  def __call__(self, /, name: str) -> modal.types.ServiceUserInfo:
855
853
  """Get information about a specific Service User.
856
854
 
857
- To call this function, you are required to have Manager permissions in the Workspace.
858
-
859
855
  Args:
860
856
  name: The name of the Service User to introspect.
861
857
 
@@ -874,8 +870,6 @@ class WorkspaceServiceUserManager:
874
870
  async def aio(self, /, name: str) -> modal.types.ServiceUserInfo:
875
871
  """Get information about a specific Service User.
876
872
 
877
- To call this function, you are required to have Manager permissions in the Workspace.
878
-
879
873
  Args:
880
874
  name: The name of the Service User to introspect.
881
875
 
@@ -897,8 +891,6 @@ class WorkspaceServiceUserManager:
897
891
  def __call__(self, /) -> list[modal.types.ServiceUserInfo]:
898
892
  """List all Service Users in the current workspace.
899
893
 
900
- To call this function, you are required to have Manager permissions in the Workspace.
901
-
902
894
  Returns:
903
895
  A list of `ServiceUserInfo` dataclasses.
904
896
 
@@ -915,8 +907,6 @@ class WorkspaceServiceUserManager:
915
907
  async def aio(self, /) -> list[modal.types.ServiceUserInfo]:
916
908
  """List all Service Users in the current workspace.
917
909
 
918
- To call this function, you are required to have Manager permissions in the Workspace.
919
-
920
910
  Returns:
921
911
  A list of `ServiceUserInfo` dataclasses.
922
912
 
@@ -933,13 +923,12 @@ class WorkspaceServiceUserManager:
933
923
  list: __list_spec
934
924
 
935
925
  class __delete_spec(typing_extensions.Protocol):
936
- def __call__(self, /, name: str) -> None:
926
+ def __call__(self, /, name: str, *, allow_missing: bool = False) -> None:
937
927
  """Permanently delete a Service User.
938
928
 
939
- To call this function, you are required to have Manager permissions in the Workspace.
940
-
941
929
  Args:
942
930
  name: The name of the Service User to delete.
931
+ allow_missing: If `True`, do not throw an exception if the Service User does not exist.
943
932
 
944
933
  Examples:
945
934
 
@@ -950,13 +939,12 @@ class WorkspaceServiceUserManager:
950
939
  """
951
940
  ...
952
941
 
953
- async def aio(self, /, name: str) -> None:
942
+ async def aio(self, /, name: str, *, allow_missing: bool = False) -> None:
954
943
  """Permanently delete a Service User.
955
944
 
956
- To call this function, you are required to have Manager permissions in the Workspace.
957
-
958
945
  Args:
959
946
  name: The name of the Service User to delete.
947
+ allow_missing: If `True`, do not throw an exception if the Service User does not exist.
960
948
 
961
949
  Examples:
962
950
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.6.2.dev9
3
+ Version: 1.6.2.dev10
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License-Expression: Apache-2.0
@@ -32,14 +32,14 @@ modal/_tunnel.py,sha256=4M1qLFXvJLV2s_vr5vHXDtGdZr0OUNmXNj1C3lyF4qE,7360
32
32
  modal/_tunnel.pyi,sha256=WVP0dkoN__mM73ZQ-r3nHKCXvYlRMmEWW8iw2xrSY6M,14698
33
33
  modal/_type_manager.py,sha256=DWjgmjYJuOagw2erin506UUbG2H5UzZCFEekS-7hmfA,9087
34
34
  modal/_watcher.py,sha256=Ot1My6fhTp5v0NXR7-ESV8kNQqvpgozrIcvLBtf5r-U,3564
35
- modal/_workspace.py,sha256=zZm5eo2JAyj6MYkZD61jl58ne1fp6vok7acmZ1CVl3A,27825
35
+ modal/_workspace.py,sha256=HgV--qq7SDc4v5fcogoUdypPTA9MXh2w998J5iQ1Zao,27830
36
36
  modal/app.py,sha256=tAILXi3rk-d2KgdLKGMsB7awShiPBcPfEfiPLn1fGQY,74703
37
37
  modal/app.pyi,sha256=TjcJhoWwZTK2eQ_HCIChU62xSdLi6SvHVQYksy-eZ7w,88565
38
38
  modal/billing.py,sha256=E1-P_P8bJFLp-bTaggYd4BaaViImPB5UdtXiY8VeAZQ,390
39
39
  modal/billing.pyi,sha256=4m7VLYNS8IiJ2I3CJx0awvrOM6BILWRVLfdeqtAYr8M,7189
40
40
  modal/call_graph.py,sha256=XGALIDn1bLchWo_goyX23bcPjzCZcfqKUo8INIfDapo,1775
41
41
  modal/client.py,sha256=cstrL0wpXCZmcvRs7Sy3QRw9Vz8zkZy8NNk0tPe38sk,22852
42
- modal/client.pyi,sha256=MB-HfDpp1QFox_pWh2XDH70EZQlFwGP4jRPYeFZP-hk,23488
42
+ modal/client.pyi,sha256=FIs-LOX3P2SlgWbBi0iRAAeqRM-7pRxdSXrdFK2jai0,23490
43
43
  modal/cloud_bucket_mount.py,sha256=ZOg69dhIFcJcwim4JP-6s1tqwOYAfgS9806jRpspifw,7511
44
44
  modal/cloud_bucket_mount.pyi,sha256=HoPe_NG5jYdVfmTNY-ZJ3-C8Eo_FzJovl9orJRjkGbE,10252
45
45
  modal/cls.py,sha256=qoGJD590GnHHLvVhkn-BMl4ATMrw2Fg_V4evy5ZQbJQ,41784
@@ -56,7 +56,7 @@ modal/environments.pyi,sha256=drmg6yVLjpPhGJ4byOrdBEp2KRLburPrKT-FYtgNPoM,23628
56
56
  modal/exception.py,sha256=L-dUKust6feZzn5qhTVusSAr0IBa4Wa3qIA225IT_lY,12334
57
57
  modal/file_pattern_matcher.py,sha256=CpaNNj7oBlOSewX-AAUZCF8KDxH0_DmgwzfpVGbPYJ4,8138
58
58
  modal/functions.py,sha256=EZRcHFfnoYbvawUJnAHF5nIvoo9GX2Djy7mUmOCXmXc,258
59
- modal/functions.pyi,sha256=Ts7fwfZP4JSuq6rkHdeaa3kGjDItYfip5jKoD_fUhZo,56516
59
+ modal/functions.pyi,sha256=SuolW78OayX4pRq8rsIZr7SXUH6Qsf_sPagsB-YCAqE,56516
60
60
  modal/image.py,sha256=c0lUSAoU-Ip3g5DGZ0uhdCzoWSQWh1g0i-7Rxo-Sz-I,160
61
61
  modal/image.pyi,sha256=R5rdY4P30IEm-F7A2S9CdXa6QYNk-pQwsEun9BKTyLw,61094
62
62
  modal/io_streams.py,sha256=iWwhGCcTwhNtIo2t2f-qVV3FBEK1DIi40Tm_Ai01eYA,32992
@@ -96,7 +96,7 @@ modal/types.py,sha256=LuOQoaDt8T-1i2XOfXzy3Ym1yxHEZ08HTszhGddvCvI,37474
96
96
  modal/volume.py,sha256=wbTVEriIv3djjeUAuUWkLABgG4ppbNMmXLJeV0bev3Q,71117
97
97
  modal/volume.pyi,sha256=Rp2gw_zh8HAAHRbdDkF782tttQfyv3uzSriO-zpMXDU,67350
98
98
  modal/workspace.py,sha256=83AGrCXEN1-QhC6XfQusgWO50GOIJX_c1QKsF52wjdA,1157
99
- modal/workspace.pyi,sha256=S7QwyBm0wzQgR6lToTvSAKD2AdSLo0e8UbBS8TPlckU,35519
99
+ modal/workspace.pyi,sha256=IfyKAkNmnFi-lMVBDuykPu2UTi7kg7495kmZ4w7yiYY,35009
100
100
  modal/_output/__init__.py,sha256=xtGN6SPv2zvByrJ32JPao0yJKpxyCIZ-2JMTScrisuQ,28
101
101
  modal/_output/manager.py,sha256=DqEguniI8uWMFUZFX4U4gsCFpio1zQeWjJhJC_GZhi4,18286
102
102
  modal/_output/pty.py,sha256=ngkKVpU8zDOxg94LiWYmdwXavd305HQDXzjWGrHQBIM,10408
@@ -189,7 +189,7 @@ modal/cli/skills.py,sha256=oniDafrmu_ARYTlButnlXbKE9XlEU1-EFz-eO4tdnFY,11954
189
189
  modal/cli/token.py,sha256=TOuauMzSe7PV1bMnmGQO7oJaAsYY03C1GY6Nww8ac8Y,4905
190
190
  modal/cli/utils.py,sha256=1tkDLqtVqYvj4xLoRaOuO_cl8WJxPpHQnIdTg5S2tjA,11670
191
191
  modal/cli/volume.py,sha256=nM5QKRjX-d6lQO4FPNDFbIZhuWfZ2O_RW8gtWWwgs4Q,11281
192
- modal/cli/workspace.py,sha256=wt11BRti_FMnWnztTrnPoXUAtLhFVDH194aImXyWCms,8980
192
+ modal/cli/workspace.py,sha256=JHIX9XyYYyrgq8GgjexSGmqwO6dSyPL5irYhecYRVZI,22323
193
193
  modal/experimental/__init__.py,sha256=iHj9XLSZ_J0ciUalr96hJ57GbOsi2v87u5QZ6mPiHtg,13205
194
194
  modal/experimental/_flash_container_entry.py,sha256=u0ciXgUain0tosulHFVh_Dp7vi51uHWWiThdQbj0whs,2318
195
195
  modal/experimental/flash.py,sha256=ZZJIlJ9lLv03CQr6muRUXQB-5tZII_zX7qiLqRuJWGM,13120
@@ -207,7 +207,7 @@ modal/sandbox/_task_command_router_client.py,sha256=qhFXy8eyGIxwxRFZkTsxJVxsAc8c
207
207
  modal/sandbox/sandbox.py,sha256=4ekghND9qUGf5wtIQH4mVa6PHGjHgY0UN5RKryOM05k,683
208
208
  modal/sandbox/sandbox.pyi,sha256=HYxopAUYMfMEVMWgzeb4qPATPFzLkTCnqpSl9lI-3ac,150321
209
209
  modal/skills/modal/SKILL.md,sha256=Bm-0BPTYppJoeQzA-N-Re8L6-nKnV6gspJXMl4cD0P0,5490
210
- modal-1.6.2.dev9.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
210
+ modal-1.6.2.dev10.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
211
211
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
212
212
  modal_docs/gen_cli_docs.py,sha256=9-b8vl2EsLWD9zJqpCK8IUi5h44Bjn3i3rSdedZV4bA,6415
213
213
  modal_docs/gen_cli_docs_main.py,sha256=UPGr7O1DYTIRLwfINe2rbdh89J3cPVTU1OWL-SjxnOQ,198
@@ -231,10 +231,10 @@ modal_proto/task_command_router_pb2.py,sha256=ysC0d8p3YxwyOpP3B-qDlvE81Yo4ntQiwi
231
231
  modal_proto/task_command_router_pb2.pyi,sha256=FDcd5t3ZCmQHUjefOzg07iglrgKYcBrzFvdKIWsEMHI,50628
232
232
  modal_proto/task_command_router_pb2_grpc.py,sha256=8iGUGwEVbqOsZWPJ7OsQ9OKUGaSCf9Dr2tvb04Ww6d4,44620
233
233
  modal_proto/task_command_router_pb2_grpc.pyi,sha256=26Fq9RQo09B9KLHsgdpRuw-I6Z4-2jrXLQkuDzB-moI,15097
234
- modal_version/__init__.py,sha256=BO-DM7wsEgysII6-KveacoC7VbNzkiPdH-MGsOiTomQ,120
234
+ modal_version/__init__.py,sha256=2lqb4ysefgTqscFMp9LWCNfAU0rKY3oVgW2wnWTy-pw,121
235
235
  modal_version/__main__.py,sha256=nLUbubxKx7GXNzJIkTB7M7_u8080LyLM0IS0rzFkC_c,119
236
- modal-1.6.2.dev9.dist-info/METADATA,sha256=m8MgmkSOJS_QswrCMRdd_b-t8D2lh7Ws830RJPwVup0,2512
237
- modal-1.6.2.dev9.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
238
- modal-1.6.2.dev9.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
239
- modal-1.6.2.dev9.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
240
- modal-1.6.2.dev9.dist-info/RECORD,,
236
+ modal-1.6.2.dev10.dist-info/METADATA,sha256=9gYqJTJseHyFzcr4xOZ3H3E-6BriZbJuQKCaBfxANZ8,2513
237
+ modal-1.6.2.dev10.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
238
+ modal-1.6.2.dev10.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
239
+ modal-1.6.2.dev10.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
240
+ modal-1.6.2.dev10.dist-info/RECORD,,
modal_version/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2026
2
2
  """Supplies the current version of the modal client library."""
3
3
 
4
- __version__ = "1.6.2.dev9"
4
+ __version__ = "1.6.2.dev10"