anyscale 0.26.21__py3-none-any.whl → 0.26.23__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.
Files changed (32) hide show
  1. anyscale/_private/anyscale_client/anyscale_client.py +103 -43
  2. anyscale/_private/anyscale_client/common.py +38 -8
  3. anyscale/_private/anyscale_client/fake_anyscale_client.py +98 -27
  4. anyscale/_private/docgen/models.md +2 -2
  5. anyscale/_private/models/model_base.py +95 -0
  6. anyscale/_private/workload/workload_sdk.py +3 -1
  7. anyscale/aggregated_instance_usage/models.py +4 -4
  8. anyscale/client/README.md +1 -0
  9. anyscale/client/openapi_client/api/default_api.py +122 -1
  10. anyscale/client/openapi_client/models/baseimagesenum.py +68 -1
  11. anyscale/client/openapi_client/models/supportedbaseimagesenum.py +68 -1
  12. anyscale/commands/command_examples.py +4 -0
  13. anyscale/commands/list_util.py +107 -0
  14. anyscale/commands/service_commands.py +267 -31
  15. anyscale/commands/util.py +5 -4
  16. anyscale/controllers/service_controller.py +7 -86
  17. anyscale/sdk/anyscale_client/models/baseimagesenum.py +68 -1
  18. anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +68 -1
  19. anyscale/service/__init__.py +53 -3
  20. anyscale/service/_private/service_sdk.py +177 -41
  21. anyscale/service/commands.py +78 -1
  22. anyscale/service/models.py +65 -0
  23. anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
  24. anyscale/util.py +35 -1
  25. anyscale/version.py +1 -1
  26. {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/METADATA +1 -1
  27. {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/RECORD +32 -31
  28. {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/LICENSE +0 -0
  29. {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/NOTICE +0 -0
  30. {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/WHEEL +0 -0
  31. {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/entry_points.txt +0 -0
  32. {anyscale-0.26.21.dist-info → anyscale-0.26.23.dist-info}/top_level.txt +0 -0
@@ -2,15 +2,18 @@ from io import StringIO
2
2
  from json import dumps as json_dumps
3
3
  import pathlib
4
4
  import sys
5
- from typing import Any, Dict, Optional, Tuple
5
+ from typing import Any, Dict, List, Optional, Tuple
6
6
 
7
7
  import click
8
+ from rich.console import Console
9
+ from rich.table import Table
8
10
  from typing_extensions import Literal
9
11
  import yaml
10
12
 
11
13
  from anyscale._private.models.image_uri import ImageURI
12
14
  from anyscale.cli_logger import BlockLogger
13
15
  from anyscale.commands import command_examples
16
+ from anyscale.commands.list_util import display_list
14
17
  from anyscale.commands.util import (
15
18
  AnyscaleCommand,
16
19
  convert_kv_strings_to_dict,
@@ -19,8 +22,21 @@ from anyscale.commands.util import (
19
22
  )
20
23
  from anyscale.controllers.service_controller import ServiceController
21
24
  import anyscale.service
22
- from anyscale.service import ServiceConfig, ServiceLogMode, ServiceState, ServiceStatus
23
- from anyscale.util import validate_non_negative_arg
25
+ from anyscale.service.models import (
26
+ ServiceConfig,
27
+ ServiceLogMode,
28
+ ServiceSortField,
29
+ ServiceState,
30
+ ServiceStatus,
31
+ ServiceVersionStatus,
32
+ SortOrder,
33
+ )
34
+ from anyscale.util import (
35
+ AnyscaleJSONEncoder,
36
+ get_endpoint,
37
+ validate_non_negative_arg,
38
+ validate_service_state_filter,
39
+ )
24
40
 
25
41
 
26
42
  log = BlockLogger() # CLI Logger
@@ -389,12 +405,14 @@ def status(
389
405
  status_dict.get("primary_version", {}).pop("config", None)
390
406
  status_dict.get("canary_version", {}).pop("config", None)
391
407
 
408
+ console = Console()
392
409
  if json:
393
- print(json_dumps(status_dict, indent=4, sort_keys=False))
410
+ json_str = json_dumps(status_dict, indent=2, cls=AnyscaleJSONEncoder)
411
+ console.print_json(json=json_str)
394
412
  else:
395
413
  stream = StringIO()
396
414
  yaml.dump(status_dict, stream, sort_keys=False)
397
- print(stream.getvalue(), end="")
415
+ console.print(stream.getvalue(), end="")
398
416
 
399
417
 
400
418
  @service_cli.command(
@@ -624,51 +642,270 @@ def rollout( # noqa: PLR0913
624
642
  )
625
643
 
626
644
 
627
- # TODO(mowen): Add cloud support for this when we refactor to new SDK method
628
- @service_cli.command(name="list", help="Display information about services.")
629
- @click.option(
630
- "--name", "-n", required=False, default=None, help="Filter by service name."
645
+ MAX_PAGE_SIZE = 50
646
+ NON_INTERACTIVE_DEFAULT_MAX_ITEMS = 10
647
+
648
+
649
+ def validate_page_size(ctx, param, value):
650
+ value = validate_non_negative_arg(ctx, param, value)
651
+ if value is not None and value > MAX_PAGE_SIZE:
652
+ raise click.BadParameter(f"must be less than or equal to {MAX_PAGE_SIZE}.")
653
+ return value
654
+
655
+
656
+ def validate_max_items(ctx, param, value):
657
+ if value is None:
658
+ return None
659
+ return validate_non_negative_arg(ctx, param, value)
660
+
661
+
662
+ def _parse_sort_option(sort: Optional[str],) -> Tuple[Optional[str], SortOrder]:
663
+ """
664
+ Given a raw sort string (e.g. "-created_at"), return
665
+ (canonical_field_name, SortOrder).
666
+ """
667
+ if not sort:
668
+ return None, SortOrder.ASC
669
+
670
+ # build case-insensitive map of allowed fields
671
+ allowed = {f.value.lower(): f.value for f in ServiceSortField.__members__.values()}
672
+
673
+ # detect leading '-' for descending
674
+ if sort.startswith("-"):
675
+ raw = sort[1:]
676
+ order = SortOrder.DESC
677
+ else:
678
+ raw = sort
679
+ order = SortOrder.ASC
680
+
681
+ key = raw.lower()
682
+ if key not in allowed:
683
+ allowed_names = ", ".join(sorted(allowed.values()))
684
+ raise click.BadParameter(
685
+ f"Invalid sort field '{raw}'. Allowed fields: {allowed_names}"
686
+ )
687
+
688
+ return allowed[key], order
689
+
690
+
691
+ def _create_service_list_table(show_header: bool) -> Table:
692
+ table = Table(show_header=show_header, expand=True)
693
+ # NAME and ID: larger ratios, can wrap but never truncate
694
+ table.add_column(
695
+ "NAME", no_wrap=False, overflow="fold", ratio=3, min_width=15,
696
+ )
697
+ table.add_column(
698
+ "ID", no_wrap=False, overflow="fold", ratio=2, min_width=12,
699
+ )
700
+ # all other columns will wrap as needed
701
+ for heading in (
702
+ "CURRENT STATE",
703
+ "CREATOR",
704
+ "PROJECT",
705
+ "LAST DEPLOYED AT",
706
+ ):
707
+ table.add_column(
708
+ heading, no_wrap=False, overflow="fold", ratio=1, min_width=8,
709
+ )
710
+
711
+ return table
712
+
713
+
714
+ def _format_service_output_data(svc: ServiceStatus) -> Dict[str, str]:
715
+ last_deployed_at = ""
716
+ if isinstance(svc.primary_version, ServiceVersionStatus):
717
+ last_deployed_at = svc.primary_version.created_at.strftime("%Y-%m-%d %H:%M:%S")
718
+
719
+ return {
720
+ "name": svc.name,
721
+ "id": svc.id,
722
+ "current_state": str(svc.state),
723
+ "creator": str(svc.creator or ""),
724
+ "project": str(svc.project or ""),
725
+ "last_deployed_at": last_deployed_at,
726
+ }
727
+
728
+
729
+ @service_cli.command(
730
+ name="list", help="List services.", cls=AnyscaleCommand,
631
731
  )
732
+ @click.option("--service-id", "--id", help="ID of the service to display.")
733
+ @click.option("--name", "-n", help="Name of the service to display.")
632
734
  @click.option(
633
- "--service-id", "--id", required=False, default=None, help="Filter by service id."
735
+ "--cloud",
736
+ type=str,
737
+ help="The Anyscale Cloud of this workload; defaults to your org/workspace cloud.",
634
738
  )
635
739
  @click.option(
636
- "--project-id", required=False, default=None, help="Filter by project id."
740
+ "--project",
741
+ type=str,
742
+ help="Named project to use; defaults to your org/workspace project.",
637
743
  )
638
744
  @click.option(
639
745
  "--created-by-me",
746
+ is_flag=True,
747
+ default=False,
640
748
  help="List services created by me only.",
749
+ )
750
+ @click.option(
751
+ "--state",
752
+ "-s",
753
+ "state_filter",
754
+ multiple=True,
755
+ callback=validate_service_state_filter,
756
+ help=(
757
+ "Filter by service state (repeatable). "
758
+ f"Allowed: {', '.join(s.value for s in ServiceState)}"
759
+ ),
760
+ )
761
+ @click.option(
762
+ "--include-archived",
641
763
  is_flag=True,
642
764
  default=False,
765
+ help="Include archived services.",
643
766
  )
644
767
  @click.option(
645
768
  "--max-items",
646
- required=False,
647
- default=10,
648
769
  type=int,
649
- help="Max items to show in list.",
650
- callback=validate_non_negative_arg,
770
+ callback=validate_max_items,
771
+ help="Max total items (only with --no-interactive).",
651
772
  )
652
- def list( # noqa: A001
653
- name: Optional[str],
773
+ @click.option(
774
+ "--page-size",
775
+ type=int,
776
+ default=10,
777
+ show_default=True,
778
+ callback=validate_page_size,
779
+ help=f"Items per page (max {MAX_PAGE_SIZE}).",
780
+ )
781
+ @click.option(
782
+ "--interactive/--no-interactive",
783
+ default=True,
784
+ show_default=True,
785
+ help="Use interactive paging.",
786
+ )
787
+ @click.option(
788
+ "--sort",
789
+ help=(
790
+ "Sort by FIELD (prefix with '-' for desc). "
791
+ f"Allowed: {', '.join(f.value for f in ServiceSortField.__members__.values())}"
792
+ ),
793
+ )
794
+ @click.option(
795
+ "-v",
796
+ "--verbose",
797
+ is_flag=True,
798
+ default=False,
799
+ help="Include full config in JSON output.",
800
+ )
801
+ @click.option(
802
+ "-j",
803
+ "--json",
804
+ "json_output",
805
+ is_flag=True,
806
+ default=False,
807
+ help="Emit structured JSON to stdout.",
808
+ )
809
+ def list( # noqa: PLR0913, A001
654
810
  service_id: Optional[str],
655
- project_id: Optional[str],
811
+ name: Optional[str],
656
812
  created_by_me: bool,
657
- max_items: int,
813
+ cloud: Optional[str],
814
+ project: Optional[str],
815
+ state_filter: List[str],
816
+ include_archived: bool,
817
+ max_items: Optional[int],
818
+ page_size: int,
819
+ sort: Optional[str],
820
+ json_output: bool,
821
+ interactive: bool,
822
+ verbose: bool,
658
823
  ):
659
- """List services based on the provided filters.
660
-
661
- This returns both v1 and v2 services.
662
- """
663
- service_controller = ServiceController()
664
- service_controller.list(
665
- name=name,
666
- service_id=service_id,
667
- project_id=project_id,
668
- created_by_me=created_by_me,
669
- max_items=max_items,
824
+ """List services based on the provided filters."""
825
+ if max_items is not None and interactive:
826
+ raise click.UsageError("--max-items only allowed with --no-interactive")
827
+
828
+ # parse sort
829
+ sort_field, sort_order = _parse_sort_option(sort)
830
+
831
+ # normalize max_items
832
+ effective_max = max_items
833
+ if not interactive and effective_max is None:
834
+ stderr = Console(stderr=True)
835
+ stderr.print(
836
+ f"Defaulting to {NON_INTERACTIVE_DEFAULT_MAX_ITEMS} items in batch mode; "
837
+ "use --max-items to override."
838
+ )
839
+ effective_max = NON_INTERACTIVE_DEFAULT_MAX_ITEMS
840
+
841
+ console = Console()
842
+ stderr = Console(stderr=True)
843
+
844
+ # diagnostics
845
+ stderr.print("[bold]Listing services with:[/]")
846
+ stderr.print(f"• name = {name or '<any>'}")
847
+ stderr.print(f"• states = {', '.join(state_filter) or '<all>'}")
848
+ stderr.print(f"• created_by_me = {created_by_me}")
849
+ stderr.print(f"• include_archived= {include_archived}")
850
+ stderr.print(f"• sort = {sort or '<none>'}")
851
+ stderr.print(f"• mode = {'interactive' if interactive else 'batch'}")
852
+ stderr.print(f"• per-page limit = {page_size}")
853
+ stderr.print(f"• max-items total = {effective_max or 'all'}")
854
+ stderr.print(f"\nView your Services in the UI at {get_endpoint('/services')}\n")
855
+
856
+ creator_id = (
857
+ ServiceController().get_authenticated_user_id() if created_by_me else None
670
858
  )
671
859
 
860
+ # choose formatter
861
+ if json_output:
862
+
863
+ def json_formatter(svc: ServiceStatus) -> Dict[str, Any]:
864
+ data = svc.to_dict()
865
+ if not verbose:
866
+ data.get("primary_version", {}).pop("config", None)
867
+ data.get("canary_version", {}).pop("config", None)
868
+ return data
869
+
870
+ formatter = json_formatter
871
+ else:
872
+ formatter = _format_service_output_data
873
+
874
+ total = 0
875
+ try:
876
+ iterator = anyscale.service.list(
877
+ service_id=service_id,
878
+ name=name,
879
+ state_filter=state_filter,
880
+ creator_id=creator_id,
881
+ cloud=cloud,
882
+ project=project,
883
+ include_archived=include_archived,
884
+ max_items=None if interactive else effective_max,
885
+ page_size=page_size,
886
+ sort_field=sort_field,
887
+ sort_order=sort_order,
888
+ )
889
+ total = display_list(
890
+ iterator=iter(iterator),
891
+ item_formatter=formatter,
892
+ table_creator=_create_service_list_table,
893
+ json_output=json_output,
894
+ page_size=page_size,
895
+ interactive=interactive,
896
+ max_items=effective_max,
897
+ console=console,
898
+ )
899
+
900
+ if not json_output:
901
+ if total > 0:
902
+ stderr.print(f"\nFetched {total} services.")
903
+ else:
904
+ stderr.print("\nNo services found.")
905
+ except Exception as e: # noqa: BLE001
906
+ log.error(f"Failed to list services: {e}")
907
+ sys.exit(1)
908
+
672
909
 
673
910
  # TODO(mowen): Add cloud support for this when we refactor to new SDK method
674
911
  @service_cli.command(name="rollback", help="Roll back a service.")
@@ -708,7 +945,6 @@ def rollback(
708
945
  service_controller.rollback(service_id, max_surge_percent)
709
946
 
710
947
 
711
- # TODO(mowen): Add cloud support for this when we refactor to new SDK method
712
948
  @service_cli.command(name="terminate", help="Terminate a service.")
713
949
  @click.option(
714
950
  "--service-id", "--id", required=False, help="ID of service.",
anyscale/commands/util.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from copy import deepcopy
2
- from typing import Dict, Tuple
2
+ from typing import Dict, Tuple, TypeVar
3
3
 
4
4
  import click
5
5
 
@@ -136,9 +136,10 @@ def convert_kv_strings_to_dict(strings: Tuple[str]) -> Dict[str, str]:
136
136
  return ret_dict
137
137
 
138
138
 
139
- def override_env_vars(
140
- config: WorkloadConfig, overrides: Dict[str, str]
141
- ) -> WorkloadConfig:
139
+ T = TypeVar("T", bound=WorkloadConfig)
140
+
141
+
142
+ def override_env_vars(config: T, overrides: Dict[str, str]) -> T:
142
143
  """Returns a new copy of the WorkloadConfig with env vars overridden.
143
144
 
144
145
  This is a per-key override, so keys already in the config that are not specified in
@@ -2,7 +2,6 @@ import os
2
2
  from typing import Any, Dict, List, Optional
3
3
 
4
4
  import click
5
- import tabulate
6
5
  import yaml
7
6
 
8
7
  from anyscale import AnyscaleSDK
@@ -14,9 +13,6 @@ from anyscale.client.openapi_client.api.default_api import DefaultApi as Interna
14
13
  from anyscale.client.openapi_client.models.decorated_list_service_api_model import (
15
14
  DecoratedListServiceAPIModel,
16
15
  )
17
- from anyscale.client.openapi_client.models.decorated_production_service_v2_api_model import (
18
- DecoratedProductionServiceV2APIModel,
19
- )
20
16
  from anyscale.controllers.base_controller import BaseController
21
17
  from anyscale.models.service_model import ServiceConfig
22
18
  from anyscale.project_utils import infer_project_id
@@ -81,6 +77,12 @@ class ServiceController(BaseController):
81
77
  max_surge_percent=config.max_surge_percent,
82
78
  )
83
79
 
80
+ def get_authenticated_user_id(self) -> str:
81
+ user_info_response = (
82
+ self.internal_api_client.get_user_info_api_v2_userinfo_get()
83
+ )
84
+ return user_info_response.result.id
85
+
84
86
  def _get_services_by_name(
85
87
  self,
86
88
  *,
@@ -93,12 +95,7 @@ class ServiceController(BaseController):
93
95
 
94
96
  Note that "name" is an *exact match* (different from the REST API semantics).
95
97
  """
96
- creator_id = None
97
- if created_by_me:
98
- user_info_response = (
99
- self.internal_api_client.get_user_info_api_v2_userinfo_get()
100
- )
101
- creator_id = user_info_response.result.id
98
+ creator_id = self.get_authenticated_user_id() if created_by_me else None
102
99
 
103
100
  paging_token = None
104
101
  services_list: List[DecoratedListServiceAPIModel] = []
@@ -364,75 +361,6 @@ class ServiceController(BaseController):
364
361
  log=self.log,
365
362
  )
366
363
 
367
- def list(
368
- self,
369
- *,
370
- name: Optional[str] = None,
371
- service_id: Optional[str] = None,
372
- project_id: Optional[str] = None,
373
- created_by_me: bool = False,
374
- max_items: int = 10,
375
- ) -> None:
376
- self._list_via_service_list_endpoint(
377
- name=name,
378
- service_id=service_id,
379
- project_id=project_id,
380
- created_by_me=created_by_me,
381
- max_items=max_items,
382
- )
383
-
384
- def _list_via_service_list_endpoint(
385
- self,
386
- *,
387
- name: Optional[str] = None,
388
- service_id: Optional[str] = None,
389
- project_id: Optional[str] = None,
390
- created_by_me: bool = False,
391
- max_items: int,
392
- ):
393
- services = []
394
- if service_id:
395
- service_v2_result: DecoratedProductionServiceV2APIModel = (
396
- self.internal_api_client.get_service_api_v2_services_v2_service_id_get(
397
- service_id
398
- ).result
399
- )
400
- services.append(
401
- [
402
- service_v2_result.name,
403
- service_v2_result.id,
404
- service_v2_result.current_state,
405
- # TODO: change to email once https://github.com/anyscale/product/pull/18189 is merged
406
- service_v2_result.creator_id,
407
- ]
408
- )
409
-
410
- else:
411
- services_data = self._get_services_by_name(
412
- name=name,
413
- project_id=project_id,
414
- created_by_me=created_by_me,
415
- max_items=max_items,
416
- )
417
-
418
- services = [
419
- [
420
- service.name,
421
- service.id,
422
- service.current_state,
423
- service.creator.email,
424
- ]
425
- for service in services_data
426
- ]
427
-
428
- table = tabulate.tabulate(
429
- services,
430
- headers=["NAME", "ID", "CURRENT STATE", "CREATOR"],
431
- tablefmt="plain",
432
- )
433
- self.log.info(f'View your Services in the UI at {get_endpoint("/services")}')
434
- self.log.info(f"Services:\n{table}")
435
-
436
364
  def rollback(self, service_id: str, max_surge_percent: Optional[int] = None):
437
365
  service: ServiceModel = self.sdk.rollback_service(
438
366
  service_id,
@@ -445,10 +373,3 @@ class ServiceController(BaseController):
445
373
  self.log.info(
446
374
  f'View the service in the UI at {get_endpoint(f"/services/{service.id}")}'
447
375
  )
448
-
449
- def terminate(self, service_id: str):
450
- service: ServiceModel = self.sdk.terminate_service(service_id).result
451
- self.log.info(f"Service {service.id} terminate initiated.")
452
- self.log.info(
453
- f'View the service in the UI at {get_endpoint(f"/services/{service.id}")}'
454
- )