relationalai 1.0.0a3__py3-none-any.whl → 1.0.0a5__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 (118) hide show
  1. relationalai/config/config.py +47 -21
  2. relationalai/config/connections/__init__.py +5 -2
  3. relationalai/config/connections/duckdb.py +2 -2
  4. relationalai/config/connections/local.py +31 -0
  5. relationalai/config/connections/snowflake.py +0 -1
  6. relationalai/config/external/raiconfig_converter.py +235 -0
  7. relationalai/config/external/raiconfig_models.py +202 -0
  8. relationalai/config/external/utils.py +31 -0
  9. relationalai/config/shims.py +1 -0
  10. relationalai/semantics/__init__.py +10 -8
  11. relationalai/semantics/backends/sql/sql_compiler.py +1 -4
  12. relationalai/semantics/experimental/__init__.py +0 -0
  13. relationalai/semantics/experimental/builder.py +295 -0
  14. relationalai/semantics/experimental/builtins.py +154 -0
  15. relationalai/semantics/frontend/base.py +67 -42
  16. relationalai/semantics/frontend/core.py +34 -6
  17. relationalai/semantics/frontend/front_compiler.py +209 -37
  18. relationalai/semantics/frontend/pprint.py +6 -2
  19. relationalai/semantics/metamodel/__init__.py +7 -0
  20. relationalai/semantics/metamodel/metamodel.py +2 -0
  21. relationalai/semantics/metamodel/metamodel_analyzer.py +58 -16
  22. relationalai/semantics/metamodel/pprint.py +6 -1
  23. relationalai/semantics/metamodel/rewriter.py +11 -7
  24. relationalai/semantics/metamodel/typer.py +116 -41
  25. relationalai/semantics/reasoners/__init__.py +11 -0
  26. relationalai/semantics/reasoners/graph/__init__.py +35 -0
  27. relationalai/semantics/reasoners/graph/core.py +9028 -0
  28. relationalai/semantics/std/__init__.py +30 -10
  29. relationalai/semantics/std/aggregates.py +641 -12
  30. relationalai/semantics/std/common.py +146 -13
  31. relationalai/semantics/std/constraints.py +71 -1
  32. relationalai/semantics/std/datetime.py +904 -21
  33. relationalai/semantics/std/decimals.py +143 -2
  34. relationalai/semantics/std/floats.py +57 -4
  35. relationalai/semantics/std/integers.py +98 -4
  36. relationalai/semantics/std/math.py +857 -35
  37. relationalai/semantics/std/numbers.py +216 -20
  38. relationalai/semantics/std/re.py +213 -5
  39. relationalai/semantics/std/strings.py +437 -44
  40. relationalai/shims/executor.py +60 -52
  41. relationalai/shims/fixtures.py +85 -0
  42. relationalai/shims/helpers.py +26 -2
  43. relationalai/shims/hoister.py +28 -9
  44. relationalai/shims/mm2v0.py +204 -173
  45. relationalai/tools/cli/cli.py +192 -10
  46. relationalai/tools/cli/components/progress_reader.py +1 -1
  47. relationalai/tools/cli/docs.py +394 -0
  48. relationalai/tools/debugger.py +11 -4
  49. relationalai/tools/qb_debugger.py +435 -0
  50. relationalai/tools/typer_debugger.py +1 -2
  51. relationalai/util/dataclasses.py +3 -5
  52. relationalai/util/docutils.py +1 -2
  53. relationalai/util/error.py +2 -5
  54. relationalai/util/python.py +23 -0
  55. relationalai/util/runtime.py +1 -2
  56. relationalai/util/schema.py +2 -4
  57. relationalai/util/structures.py +4 -2
  58. relationalai/util/tracing.py +8 -2
  59. {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/METADATA +8 -5
  60. {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/RECORD +118 -95
  61. {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/WHEEL +1 -1
  62. v0/relationalai/__init__.py +1 -1
  63. v0/relationalai/clients/client.py +52 -18
  64. v0/relationalai/clients/exec_txn_poller.py +122 -0
  65. v0/relationalai/clients/local.py +23 -8
  66. v0/relationalai/clients/resources/azure/azure.py +36 -11
  67. v0/relationalai/clients/resources/snowflake/__init__.py +4 -4
  68. v0/relationalai/clients/resources/snowflake/cli_resources.py +12 -1
  69. v0/relationalai/clients/resources/snowflake/direct_access_resources.py +124 -100
  70. v0/relationalai/clients/resources/snowflake/engine_service.py +381 -0
  71. v0/relationalai/clients/resources/snowflake/engine_state_handlers.py +35 -29
  72. v0/relationalai/clients/resources/snowflake/error_handlers.py +43 -2
  73. v0/relationalai/clients/resources/snowflake/snowflake.py +277 -179
  74. v0/relationalai/clients/resources/snowflake/use_index_poller.py +8 -0
  75. v0/relationalai/clients/types.py +5 -0
  76. v0/relationalai/errors.py +19 -1
  77. v0/relationalai/semantics/lqp/algorithms.py +173 -0
  78. v0/relationalai/semantics/lqp/builtins.py +199 -2
  79. v0/relationalai/semantics/lqp/executor.py +68 -37
  80. v0/relationalai/semantics/lqp/ir.py +28 -2
  81. v0/relationalai/semantics/lqp/model2lqp.py +215 -45
  82. v0/relationalai/semantics/lqp/passes.py +13 -658
  83. v0/relationalai/semantics/lqp/rewrite/__init__.py +12 -0
  84. v0/relationalai/semantics/lqp/rewrite/algorithm.py +385 -0
  85. v0/relationalai/semantics/lqp/rewrite/constants_to_vars.py +70 -0
  86. v0/relationalai/semantics/lqp/rewrite/deduplicate_vars.py +104 -0
  87. v0/relationalai/semantics/lqp/rewrite/eliminate_data.py +108 -0
  88. v0/relationalai/semantics/lqp/rewrite/extract_keys.py +25 -3
  89. v0/relationalai/semantics/lqp/rewrite/period_math.py +77 -0
  90. v0/relationalai/semantics/lqp/rewrite/quantify_vars.py +65 -31
  91. v0/relationalai/semantics/lqp/rewrite/unify_definitions.py +317 -0
  92. v0/relationalai/semantics/lqp/utils.py +11 -1
  93. v0/relationalai/semantics/lqp/validators.py +14 -1
  94. v0/relationalai/semantics/metamodel/builtins.py +2 -1
  95. v0/relationalai/semantics/metamodel/compiler.py +2 -1
  96. v0/relationalai/semantics/metamodel/dependency.py +12 -3
  97. v0/relationalai/semantics/metamodel/executor.py +11 -1
  98. v0/relationalai/semantics/metamodel/factory.py +2 -2
  99. v0/relationalai/semantics/metamodel/helpers.py +7 -0
  100. v0/relationalai/semantics/metamodel/ir.py +3 -2
  101. v0/relationalai/semantics/metamodel/rewrite/dnf_union_splitter.py +30 -20
  102. v0/relationalai/semantics/metamodel/rewrite/flatten.py +50 -13
  103. v0/relationalai/semantics/metamodel/rewrite/format_outputs.py +9 -3
  104. v0/relationalai/semantics/metamodel/typer/checker.py +6 -4
  105. v0/relationalai/semantics/metamodel/typer/typer.py +4 -3
  106. v0/relationalai/semantics/metamodel/visitor.py +4 -3
  107. v0/relationalai/semantics/reasoners/optimization/solvers_dev.py +1 -1
  108. v0/relationalai/semantics/reasoners/optimization/solvers_pb.py +336 -86
  109. v0/relationalai/semantics/rel/compiler.py +2 -1
  110. v0/relationalai/semantics/rel/executor.py +3 -2
  111. v0/relationalai/semantics/tests/lqp/__init__.py +0 -0
  112. v0/relationalai/semantics/tests/lqp/algorithms.py +345 -0
  113. v0/relationalai/tools/cli.py +339 -186
  114. v0/relationalai/tools/cli_controls.py +216 -67
  115. v0/relationalai/tools/cli_helpers.py +410 -6
  116. v0/relationalai/util/format.py +5 -2
  117. {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/entry_points.txt +0 -0
  118. {relationalai-1.0.0a3.dist-info → relationalai-1.0.0a5.dist-info}/top_level.txt +0 -0
@@ -30,10 +30,9 @@ from ..tools import debugger as deb, qb_debugger as qb_deb
30
30
  from ..clients import config
31
31
  from v0.relationalai.tools.constants import RAI_APP_NAME
32
32
  from v0.relationalai.clients.resources.snowflake.cli_resources import CLIResources
33
+ from v0.relationalai.clients.resources.snowflake import EngineType
33
34
  from v0.relationalai.tools.cli_helpers import (
34
35
  EMPTY_STRING_REGEX,
35
- ENGINE_NAME_ERROR,
36
- ENGINE_NAME_REGEX,
37
36
  PASSCODE_REGEX,
38
37
  UUID,
39
38
  RichGroup,
@@ -42,14 +41,25 @@ from v0.relationalai.tools.cli_helpers import (
42
41
  ensure_config,
43
42
  exit_with_divider,
44
43
  exit_with_error,
44
+ exit_with_handled_exception,
45
45
  filter_profiles_by_platform,
46
46
  format_row,
47
47
  get_config, get_resource_provider,
48
48
  is_latest_cli_version,
49
49
  issue_top_level_profile_warning,
50
50
  latest_version,
51
+ select_engine_interactive,
52
+ select_engine_with_state_filter,
53
+ ensure_engine_type_for_snowflake,
54
+ build_engine_operation_messages,
55
+ prompt_and_validate_engine_name,
56
+ validate_auto_suspend_mins,
57
+ get_engine_type_for_creation,
58
+ get_and_validate_engine_size,
59
+ create_engine_with_spinner,
51
60
  show_dictionary_table,
52
61
  show_engines,
62
+ show_engine_details,
53
63
  show_imports,
54
64
  show_transactions,
55
65
  supports_platform,
@@ -113,6 +123,50 @@ def cli(profile):
113
123
  rich.print(f"[yellow]RelationalAI version ({latest_ver}) is the latest. Please consider upgrading.[/yellow]")
114
124
  GlobalProfile.set(profile)
115
125
 
126
+
127
+ #--------------------------------------------------
128
+ # Engines helpers
129
+ #--------------------------------------------------
130
+
131
+ def _exit_engine_requires_type(name: str, available_types: list[str], cmd: str) -> None:
132
+ """Exit with a consistent 'available types' hint and example command."""
133
+ types_display = ", ".join(available_types) if available_types else "<unknown>"
134
+ example_type = available_types[0] if available_types else "<ENGINE TYPE>"
135
+ exit_with_error(
136
+ f"[yellow]Engine '{name}' has no LOGIC type engine. Available types: {types_display}. "
137
+ f"Please re-run with [cyan]--type[/cyan]. Example: \n\n"
138
+ f"[green]rai {cmd} --name {name} --type {example_type}[/green]"
139
+ )
140
+
141
+
142
+ def _get_engine_types_for_name(provider: ResourcesBase, name: str) -> list[str] | None:
143
+ """Return available engine types for a given name, or None if name not found.
144
+
145
+ Errors are handled via exit_with_handled_exception, since this is used for user-facing
146
+ CLI diagnostics.
147
+ """
148
+ try:
149
+ engines_with_name = provider.list_engines(name=name)
150
+ except Exception as e:
151
+ exit_with_handled_exception("Error fetching engines", e)
152
+ raise Exception("unreachable")
153
+ if not engines_with_name:
154
+ return None
155
+ return sorted({(e.get("type") or "").upper() for e in engines_with_name if e.get("type")})
156
+
157
+
158
+ def _require_type_if_no_logic(provider: ResourcesBase, name: str, cmd: str) -> str:
159
+ """Return LOGIC if present, otherwise exit with a helpful types message."""
160
+ available_types = _get_engine_types_for_name(provider, name)
161
+ if not available_types:
162
+ exit_with_error(f"[yellow]No engine found with name '{name}'.")
163
+ raise Exception("unreachable")
164
+ has_logic = any(t == EngineType.LOGIC for t in available_types)
165
+ if has_logic:
166
+ return EngineType.LOGIC
167
+ _exit_engine_requires_type(name, available_types, cmd)
168
+ raise Exception("unreachable")
169
+
116
170
  #--------------------------------------------------
117
171
  # Init
118
172
  #--------------------------------------------------
@@ -716,7 +770,7 @@ def config_check(all_profiles:bool=False):
716
770
  engine_name = cfg.get("engine")
717
771
  assert isinstance(engine_name, str), f"Engine name must be a string, not {type(engine_name)}"
718
772
  # This essentially checks if the profile is valid since we are connecting to get the engine
719
- engine = provider.get_engine(engine_name)
773
+ engine = provider.get_engine(engine_name, EngineType.LOGIC)
720
774
  if not engine or (engine and not provider.is_valid_engine_state(engine.get("state"))):
721
775
  provider.auto_create_engine_async(engine_name)
722
776
  except Exception as e:
@@ -823,16 +877,20 @@ def debugger(host, port, old, qb, profile):
823
877
 
824
878
  @cli.command(name="engines:list", help="List all engines")
825
879
  @click.option("--state", help="Filter by engine state")
826
- def engines_list(state:str|None=None):
880
+ @click.option("--name", help="Filter by engine name (case-insensitive partial match)")
881
+ @click.option("--type", help="Filter by engine type")
882
+ @click.option("--size", help="Filter by engine size")
883
+ @click.option("--created-by", help="Filter by creator (case-insensitive partial match)")
884
+ def engines_list(state:str|None=None, name:str|None=None, type:str|None=None, size:str|None=None, created_by:str|None=None):
827
885
  divider(flush=True)
828
886
  ensure_config()
829
887
  rich.print("Note: [cyan]Engine names are case sensitive")
830
888
  rich.print("")
831
889
  with Spinner("Fetching engines"):
832
890
  try:
833
- engines = get_resource_provider().list_engines(state)
891
+ engines = get_resource_provider().list_engines(state=state, name=name, type=type, size=size, created_by=created_by)
834
892
  except Exception as e:
835
- return exit_with_error(f"\n\n[yellow]Error fetching engines: {e}")
893
+ return exit_with_handled_exception("Error fetching engines", e)
836
894
 
837
895
  if len(engines):
838
896
  show_engines(engines)
@@ -842,31 +900,64 @@ def engines_list(state:str|None=None):
842
900
 
843
901
  @cli.command(name="engines:get", help="Get engine details")
844
902
  @click.option("--name", help="Name of the engine")
845
- def engines_get(name:str|None=None):
903
+ @click.option("--type", help="Type of the engine")
904
+ def engines_get(name:str|None=None, type:str|None=None):
846
905
  divider(flush=True)
847
906
  ensure_config()
907
+ provider = get_resource_provider()
908
+
909
+ # Default to LOGIC for backwards compatibility when --type is not provided but --name is.
910
+ # If LOGIC doesn't exist but other types do, we'll show a targeted hint after probing.
911
+ if name and type is None:
912
+ type = EngineType.LOGIC
848
913
 
849
914
  rich.print("Note: [cyan]Engine names are case sensitive")
850
915
  rich.print("")
851
916
 
852
- if not name:
853
- name = controls.text("Engine name:", validator=ENGINE_NAME_REGEX.match, invalid_message=ENGINE_NAME_ERROR)
854
- rich.print("")
917
+ engine = None
918
+ if not name or (not type or not EngineType.is_valid(type)):
919
+ if type and not EngineType.is_valid(type):
920
+ rich.print(f"[yellow]Invalid engine type '{type}'.")
855
921
 
856
- with Spinner("Fetching engine"):
857
922
  try:
858
- engine = get_resource_provider().get_engine(name)
923
+ engines_list = provider.list_engines(name=name if name else None, type=type if type else None)
859
924
  except Exception as e:
860
- return exit_with_error(f"\n\n[yellow]Error fetching engine: {e}")
925
+ return exit_with_handled_exception("Error fetching engines", e)
926
+ result = select_engine_interactive(
927
+ provider,
928
+ "Select an engine:",
929
+ engine_name=name,
930
+ engines=engines_list,
931
+ )
932
+ if result is None:
933
+ return
934
+ name, type = result
935
+
936
+ for eng in engines_list:
937
+ if eng.get("name", "").upper() == name.upper() and (type is None or eng.get("type", "").upper() == type.upper()):
938
+ engine = eng
939
+ break
940
+
941
+ if engine is None:
942
+ with Spinner("Fetching engine"):
943
+ try:
944
+ engine_type = type or EngineType.LOGIC
945
+ engine = provider.get_engine(name, engine_type)
946
+ except Exception as e:
947
+ return exit_with_handled_exception("Error fetching engine", e)
861
948
 
862
949
  if engine:
863
- table = Table(show_header=True, border_style="dim", header_style="bold", box=rich_box.SIMPLE_HEAD)
864
- table.add_column("Name")
865
- table.add_column("Size")
866
- table.add_column("State")
867
- table.add_row(engine.get("name"), engine.get("size"), engine.get("state"))
868
- rich.print(table)
950
+ show_engine_details(cast(dict[str, Any], engine))
869
951
  else:
952
+ # If the user didn't specify --type, try to detect whether the engine exists
953
+ # under a non-LOGIC type and provide a helpful hint.
954
+ if name and type == EngineType.LOGIC:
955
+ try:
956
+ available_types = _get_engine_types_for_name(provider, name)
957
+ except Exception:
958
+ available_types = None
959
+ if available_types and EngineType.LOGIC not in available_types:
960
+ _exit_engine_requires_type(name, available_types, "engines:get")
870
961
  exit_with_error(f'[yellow]Engine "{name}" not found')
871
962
  divider()
872
963
 
@@ -874,33 +965,18 @@ def engines_get(name:str|None=None):
874
965
  # Engine create
875
966
  #--------------------------------------------------
876
967
 
877
- def create_engine_flow(cfg:config.Config, name=None, size=None, auto_suspend_mins=None):
968
+ def create_engine_flow(cfg:config.Config, name=None, engine_type=None, size=None, auto_suspend_mins=None):
969
+ """Main flow for creating an engine interactively or programmatically."""
878
970
  provider = get_resource_provider(None, cfg)
879
- engine = None
880
- is_engine_present = False
881
- is_engine_suspended = False
882
971
  is_interactive = name is None or size is None
883
- auto_suspend_mins = cfg.get("auto_suspend_mins", None) if auto_suspend_mins is None else auto_suspend_mins
884
972
  if is_interactive:
885
973
  rich.print("Note: [cyan]Engine names are case sensitive")
886
974
  rich.print("")
887
975
 
888
- if not name:
889
- name = controls.prompt(
890
- "Engine name:",
891
- name,
892
- validator=ENGINE_NAME_REGEX.match,
893
- invalid_message=ENGINE_NAME_ERROR,
894
- newline=True
895
- )
976
+ auto_suspend_mins = cfg.get("auto_suspend_mins", None) if auto_suspend_mins is None else auto_suspend_mins
977
+ auto_suspend_mins = validate_auto_suspend_mins(auto_suspend_mins)
896
978
 
897
- if auto_suspend_mins is not None:
898
- error_msg = f"[yellow]Error: auto_suspend_mins must be an integer instead of {type(auto_suspend_mins)}"
899
- try:
900
- auto_suspend_mins = int(auto_suspend_mins)
901
- except ValueError:
902
- exit_with_error(error_msg)
903
- assert isinstance(auto_suspend_mins, int), error_msg
979
+ name = prompt_and_validate_engine_name(name)
904
980
 
905
981
  is_name_valid, msg = validate_engine_name(name)
906
982
  if not is_name_valid:
@@ -912,66 +988,49 @@ def create_engine_flow(cfg:config.Config, name=None, size=None, auto_suspend_min
912
988
  else:
913
989
  exit_with_divider(1)
914
990
 
915
- with Spinner(f"Validating engine '{name}' name", "Engine name validated", "Error:"):
916
- try:
917
- engine = provider.get_engine(name)
918
- is_engine_present = engine and engine is not None
919
- is_engine_suspended = engine and engine.get("state", None) == "SUSPENDED"
920
- except Exception as e:
921
- if "already exists" in f"{e}":
922
- is_engine_present = True
923
- elif "Not Found" in f"{e}":
924
- is_engine_present = False
925
- else:
926
- raise e
927
- if is_engine_suspended:
928
- rich.print("")
929
- with Spinner(f"Resuming engine '{name}'", f"Engine '{name}' resumed", "Error:"):
930
- provider.resume_engine(name)
931
- return name
932
- if is_engine_present:
933
- rich.print("")
934
- if is_interactive:
935
- rich.print(f"Engine '{name}' already exists")
936
- rich.print("")
937
- return create_engine_flow(cfg)
938
- else:
939
- exit_with_error(f"[yellow]Engine '{name}' already exists")
991
+ # Backwards-compatible behavior:
992
+ # - If --type is omitted, default to LOGIC (script-friendly; avoids interactive prompts).
993
+ # Interactive behavior:
994
+ # - Only prompt for engine type when the user didn't provide --name (fully interactive flow).
995
+ if name is None and engine_type is None:
996
+ engine_type = ""
997
+ engine_type = get_engine_type_for_creation(provider, cfg, engine_type)
940
998
 
941
- # If no size is provided via the params and no size is set in the config,
942
- if not size and not cfg.get("engine_size", None):
943
- rich.print("")
944
- cloud_provider = provider.get_cloud_provider()
945
- choices = provider.get_engine_sizes(cloud_provider)
946
- size = controls.fuzzy(f"Engine size ({cloud_provider.upper()}):", choices=choices)
947
- # If a size is provided via the params or the config, validate it
948
- elif size or cfg.get("engine_size", None):
949
- # If size is None but config has engine_size, use config value
950
- if size is None:
951
- size = cfg.get("engine_size", None)
952
- valid_sizes = provider.get_engine_sizes()
953
- if not isinstance(size, str) or size not in valid_sizes:
954
- exit_with_error(f"\nInvalid engine size [yellow]{size}[/yellow] provided. Please check your config.\n\nValid sizes: [green]{valid_sizes}[/green]")
955
- assert isinstance(size, str), "engine_size must be a string"
999
+ # Simple existence check using the new get_engine API
1000
+ try:
1001
+ existing = provider.get_engine(name, engine_type or EngineType.LOGIC)
1002
+ if existing:
1003
+ engine_type_label = EngineType.get_label(engine_type or EngineType.LOGIC)
1004
+ exit_with_error(f"[yellow]Engine '{name}' with type '{engine_type_label} ({engine_type})' already exists.")
1005
+ except Exception:
1006
+ # If get_engine fails, proceed to creation path; real errors will be surfaced by create_engine
1007
+ pass
956
1008
 
957
- rich.print("")
1009
+ size = get_and_validate_engine_size(provider, cfg, size, engine_type)
1010
+
1011
+ if is_interactive:
1012
+ rich.print("")
958
1013
 
959
- with Spinner(
960
- f"Creating '{name}' engine with size {size}... (this may take several minutes)",
961
- f"Engine '{name}' created!",
962
- failed_message="Error:"
963
- ):
964
- provider.create_engine(name, size, auto_suspend_mins)
1014
+ create_engine_with_spinner(provider, name, size, engine_type, auto_suspend_mins)
965
1015
  return name
966
1016
 
967
1017
  @cli.command(name="engines:create", help="Create a new engine")
968
1018
  @click.option("--name", help="Name of the engine")
1019
+ @click.option("--type", help="Type of the engine")
969
1020
  @click.option("--size", help="Size of the engine")
970
- @click.option("--auto_suspend_mins", help="Suspend the engine after this many minutes of inactivity", default=None)
971
- def engines_create(name, size, auto_suspend_mins):
1021
+ @click.option(
1022
+ "--auto-suspend-mins",
1023
+ "--auto_suspend_mins",
1024
+ help="Suspend the engine after this many minutes of inactivity",
1025
+ default=None,
1026
+ )
1027
+ def engines_create(name, type, size, auto_suspend_mins):
972
1028
  divider(flush=True)
973
1029
  cfg = ensure_config()
974
- create_engine_flow(cfg, name, size, auto_suspend_mins)
1030
+ try:
1031
+ create_engine_flow(cfg, name, type, size, auto_suspend_mins)
1032
+ except Exception as e:
1033
+ return exit_with_handled_exception("Error creating engine", e)
975
1034
  divider()
976
1035
 
977
1036
  #--------------------------------------------------
@@ -980,40 +1039,62 @@ def engines_create(name, size, auto_suspend_mins):
980
1039
 
981
1040
  @cli.command(name="engines:delete", help="Delete an engine")
982
1041
  @click.option("--name", help="Name of the engine")
983
- @click.option("--force", help="Force delete the engine", is_flag=True)
984
- def engines_delete(name, force=False):
1042
+ @click.option("--type", help="Type of the engine")
1043
+ def engines_delete(name, type):
985
1044
  divider(flush=True)
986
1045
  ensure_config()
987
1046
  provider = get_resource_provider()
988
- if not name:
989
- name = controls.fuzzy_with_refetch(
990
- "Select an engine:",
991
- "engines",
992
- lambda: [engine["name"] for engine in provider.list_engines()],
993
- )
994
- if not name or isinstance(name, Exception):
995
- return
996
- else:
997
- try:
998
- engine = provider.get_engine(name)
999
- if not engine:
1000
- exit_with_error(f"[yellow]Engine '{name}' not found")
1001
- except Exception as e:
1002
- exit_with_error(f"[yellow]Error fetching engine: {e}")
1047
+ try:
1048
+ _engines_delete(provider, name, type)
1049
+ except Exception as e:
1050
+ return exit_with_handled_exception("Error deleting engine", e)
1051
+ divider()
1052
+
1053
+ def _engines_delete(provider: ResourcesBase, name, type) -> None:
1054
+ # If --type is omitted but --name is provided:
1055
+ # - prefer LOGIC for backwards compatibility if that engine exists
1056
+ # - otherwise, require explicit --type to avoid accidentally deleting the wrong engine type
1057
+ if name and type is None:
1058
+ # We only auto-select LOGIC; otherwise require explicit --type (avoid accidental deletes).
1059
+ type = _require_type_if_no_logic(provider, name, "engines:delete")
1003
1060
 
1004
- del_err = None
1005
- with Spinner(f"Deleting '{name}' engine", f"Engine '{name}' deleted!", "Error:"):
1061
+ # Select engine if name or type missing
1062
+ if not name or not type:
1006
1063
  try:
1007
- provider.delete_engine(name, force)
1064
+ result = select_engine_interactive(provider, "Select an engine to delete:", engine_name=name)
1008
1065
  except Exception as e:
1009
- if "SETUP_CDC" in f"{e}":
1010
- del_err = Exception("[yellow]Imports are setup to utilize this engine.\nUse '[cyan]rai engines:delete --force[/cyan]' to force delete engines.")
1011
- else:
1012
- del_err = e
1013
- raise del_err
1014
- if isinstance(del_err, Exception):
1015
- exit_with_divider(1)
1016
- divider()
1066
+ return exit_with_handled_exception("Error fetching engines", e)
1067
+ if result is None:
1068
+ if name:
1069
+ exit_with_error(f"[yellow]No engine found with name '{name}'.")
1070
+ return
1071
+ name, type = result
1072
+
1073
+ engine_type = ensure_engine_type_for_snowflake(
1074
+ provider,
1075
+ name,
1076
+ type,
1077
+ f"[yellow]Engine type is required for engine '{name}'. Please specify --type or select from the list.",
1078
+ )
1079
+
1080
+ operation_msg, success_msg = build_engine_operation_messages(provider, name, engine_type, "Deleting", "Deleted")
1081
+
1082
+ try:
1083
+ with Spinner(operation_msg, success_msg):
1084
+ provider.delete_engine(name, engine_type)
1085
+ except Exception as e:
1086
+ error_str = str(e).lower()
1087
+ if "setup_cdc" in str(e):
1088
+ exc = Exception(
1089
+ "Imports are setup to utilize this engine.\n"
1090
+ "Use 'rai engines:delete --force' to force delete engines."
1091
+ )
1092
+ elif "engine not found" in error_str or ("not found" in error_str and "engine" in error_str):
1093
+ engine_type_label = EngineType.get_label(engine_type) if EngineType.is_valid(engine_type) else engine_type
1094
+ exc = Exception(f"Engine '{name}' with type '{engine_type_label} ({engine_type})' not found.")
1095
+ else:
1096
+ exc = e
1097
+ exit_with_handled_exception("Error deleting engine", exc)
1017
1098
 
1018
1099
  #--------------------------------------------------
1019
1100
  # Engine resume
@@ -1021,35 +1102,67 @@ def engines_delete(name, force=False):
1021
1102
 
1022
1103
  @cli.command(name="engines:resume", help="Resume an engine")
1023
1104
  @click.option("--name", help="Name of the engine")
1024
- def engines_resume(name):
1105
+ @click.option("--type", help="Type of the engine")
1106
+ def engines_resume(name, type):
1025
1107
  divider(flush=True)
1026
1108
  ensure_config()
1027
1109
  provider = get_resource_provider()
1028
- if not name:
1029
- name = controls.fuzzy_with_refetch(
1110
+ try:
1111
+ _engines_resume(provider, name, type)
1112
+ except Exception as e:
1113
+ return exit_with_handled_exception("Error resuming engine", e)
1114
+ divider()
1115
+
1116
+ def _engines_resume(provider: ResourcesBase, name, type) -> None:
1117
+ type_was_omitted = type is None
1118
+ if name and type is None:
1119
+ type = EngineType.LOGIC
1120
+
1121
+ # Validate type early if provided
1122
+ if type and not EngineType.is_valid(type):
1123
+ exit_with_error(f"[yellow]Invalid engine type '{type}'. Valid types: LOGIC, SOLVER, ML")
1124
+
1125
+ try:
1126
+ result = select_engine_with_state_filter(
1127
+ provider,
1128
+ name,
1129
+ type,
1130
+ "SUSPENDED",
1131
+ "Select a suspended engine to resume:",
1030
1132
  "Select a suspended engine to resume:",
1031
- "engines",
1032
- lambda: [engine["name"] for engine in provider.list_engines('SUSPENDED')],
1133
+ "[yellow]No suspended engines found",
1134
+ f"[yellow]No suspended engines found with name '{name}'" if name else "[yellow]No suspended engines found",
1033
1135
  )
1034
- if not name or isinstance(name, Exception):
1035
- return
1036
- else:
1037
- try:
1038
- engine = provider.get_engine(name)
1039
- if not engine:
1040
- exit_with_error(f"[yellow]Engine '{name}' not found")
1041
- if engine and engine.get("state") != "SUSPENDED":
1042
- exit_with_error(f"[yellow]Engine '{name}' not in 'SUSPENDED' state")
1043
- except Exception as e:
1044
- exit_with_error(f"[yellow]Error fetching engine: {e}")
1136
+ except Exception as e:
1137
+ return exit_with_handled_exception("Error fetching engines", e)
1138
+ if result is None:
1139
+ return
1140
+ name, type = result
1045
1141
 
1046
- with Spinner(f"Resuming '{name}' engine", f"Engine '{name}' resumed", "Error:"):
1047
- try:
1048
- provider.resume_engine(name)
1049
- except Exception as e:
1050
- rich.print(f"[yellow]Error resuming engine: {e}")
1051
- exit_with_divider(1)
1052
- divider()
1142
+ engine_type = ensure_engine_type_for_snowflake(
1143
+ provider,
1144
+ name,
1145
+ type,
1146
+ f"[yellow]Engine type is required for engine '{name}'. Please specify --type or select from the list.",
1147
+ )
1148
+
1149
+ operation_msg, success_msg = build_engine_operation_messages(provider, name, engine_type, "Resuming", "Resumed")
1150
+ try:
1151
+ with Spinner(operation_msg, success_msg):
1152
+ provider.resume_engine(name, engine_type)
1153
+ except Exception as e:
1154
+ error_str = str(e).lower()
1155
+ if "engine not found" in error_str or ("not found" in error_str and "engine" in error_str):
1156
+ # If the user omitted --type and we defaulted to LOGIC, try to hint at other types.
1157
+ if type_was_omitted and engine_type == EngineType.LOGIC:
1158
+ available_types = _get_engine_types_for_name(provider, name)
1159
+ if available_types and EngineType.LOGIC not in available_types:
1160
+ _exit_engine_requires_type(name, available_types, "engines:resume")
1161
+ engine_type_label = EngineType.get_label(engine_type) if EngineType.is_valid(engine_type) else engine_type
1162
+ exc = Exception(f"Engine '{name}' with type '{engine_type_label} ({engine_type})' not found.")
1163
+ else:
1164
+ exc = e
1165
+ exit_with_handled_exception("Error resuming engine", exc)
1053
1166
 
1054
1167
  #--------------------------------------------------
1055
1168
  # Engine suspend
@@ -1057,35 +1170,66 @@ def engines_resume(name):
1057
1170
 
1058
1171
  @cli.command(name="engines:suspend", help="Suspend an engine")
1059
1172
  @click.option("--name", help="Name of the engine")
1060
- def engines_suspend(name):
1173
+ @click.option("--type", help="Type of the engine")
1174
+ def engines_suspend(name, type):
1061
1175
  divider(flush=True)
1062
1176
  ensure_config()
1063
1177
  provider = get_resource_provider()
1064
- if not name:
1065
- name = controls.fuzzy_with_refetch(
1066
- "Select an active engine to suspend:",
1067
- "engines",
1068
- lambda: [engine["name"] for engine in provider.list_engines('READY')],
1178
+ try:
1179
+ _engines_suspend(provider, name, type)
1180
+ except Exception as e:
1181
+ return exit_with_handled_exception("Error suspending engine", e)
1182
+ divider()
1183
+
1184
+ def _engines_suspend(provider: ResourcesBase, name, type) -> None:
1185
+ type_was_omitted = type is None
1186
+ if name and type is None:
1187
+ type = EngineType.LOGIC
1188
+
1189
+ if type and not EngineType.is_valid(type):
1190
+ exit_with_error(f"[yellow]Invalid engine type '{type}'. Valid types: LOGIC, SOLVER, ML")
1191
+
1192
+ try:
1193
+ result = select_engine_with_state_filter(
1194
+ provider,
1195
+ name,
1196
+ type,
1197
+ "READY",
1198
+ "Select a ready engine to suspend:",
1199
+ "Select a ready engine to suspend:",
1200
+ "[yellow]No ready engines found",
1201
+ f"[yellow]No ready engines found with name '{name}'" if name else "[yellow]No ready engines found",
1069
1202
  )
1070
- if not name or isinstance(name, Exception):
1071
- return
1072
- else:
1073
- try:
1074
- engine = provider.get_engine(name)
1075
- if not engine:
1076
- exit_with_error(f"[yellow]Engine '{name}' not found")
1077
- if engine and engine.get("state") != "READY":
1078
- exit_with_error(f"[yellow]Engine '{name}' must be in 'READY' state to be suspended")
1079
- except Exception as e:
1080
- exit_with_error(f"[yellow]Error fetching engine: {e}")
1203
+ except Exception as e:
1204
+ return exit_with_handled_exception("Error fetching engines", e)
1205
+ if result is None:
1206
+ return
1207
+ name, type = result
1081
1208
 
1082
- with Spinner(f"Suspending '{name}' engine", f"Engine '{name}' suspended", "Error:"):
1083
- try:
1084
- provider.suspend_engine(name)
1085
- except Exception as e:
1086
- rich.print(f"[yellow]Error suspending engine: {e}")
1087
- exit_with_divider(1)
1088
- divider()
1209
+ engine_type = ensure_engine_type_for_snowflake(
1210
+ provider,
1211
+ name,
1212
+ type,
1213
+ f"[yellow]Engine type is required for engine '{name}'. Please specify --type or select from the list.",
1214
+ )
1215
+
1216
+ operation_msg, success_msg = build_engine_operation_messages(provider, name, engine_type, "Suspending", "Suspended")
1217
+ try:
1218
+ with Spinner(operation_msg, success_msg):
1219
+ provider.suspend_engine(name, engine_type)
1220
+ except Exception as e:
1221
+ error_str = str(e).lower()
1222
+ if "engine not found" in error_str or ("not found" in error_str and "engine" in error_str):
1223
+ # If the user omitted --type and we defaulted to LOGIC, try to hint at other types.
1224
+ if type_was_omitted and engine_type == EngineType.LOGIC:
1225
+ available_types = _get_engine_types_for_name(provider, name)
1226
+ if available_types and EngineType.LOGIC not in available_types:
1227
+ _exit_engine_requires_type(name, available_types, "engines:suspend")
1228
+ engine_type_label = EngineType.get_label(engine_type) if EngineType.is_valid(engine_type) else engine_type
1229
+ exc = Exception(f"Engine '{name}' with type '{engine_type_label} ({engine_type})' not found.")
1230
+ else:
1231
+ exc = e
1232
+ exit_with_handled_exception("Error suspending engine", exc)
1089
1233
 
1090
1234
  #--------------------------------------------------
1091
1235
  # Engine alter engine pool
@@ -1105,7 +1249,10 @@ def engines_alter_pool(size:str|None=None, min:int|None=None, max:int|None=None)
1105
1249
 
1106
1250
  # Ask for engine size if not provided
1107
1251
  if not size:
1108
- valid_sizes = provider.get_engine_sizes()
1252
+ try:
1253
+ valid_sizes = provider.get_engine_sizes()
1254
+ except Exception as e:
1255
+ return exit_with_handled_exception("Error fetching engine sizes", e)
1109
1256
  size = controls.fuzzy(
1110
1257
  "Select engine size:",
1111
1258
  choices=valid_sizes,
@@ -1113,7 +1260,10 @@ def engines_alter_pool(size:str|None=None, min:int|None=None, max:int|None=None)
1113
1260
  )
1114
1261
 
1115
1262
  # Validate engine size
1116
- valid_sizes = provider.get_engine_sizes()
1263
+ try:
1264
+ valid_sizes = provider.get_engine_sizes()
1265
+ except Exception as e:
1266
+ return exit_with_handled_exception("Error fetching engine sizes", e)
1117
1267
  if size not in valid_sizes:
1118
1268
  exit_with_error(f"Invalid engine size '{size}'. Valid sizes: {valid_sizes}")
1119
1269
 
@@ -1148,9 +1298,12 @@ def engines_alter_pool(size:str|None=None, min:int|None=None, max:int|None=None)
1148
1298
  rich.print()
1149
1299
 
1150
1300
  # Call the API method
1151
- with Spinner("Altering engine pool", "Engine pool altered", "Error:"):
1152
- # Type cast to ensure type checker recognizes the method
1153
- cast(ResourcesBase, provider).alter_engine_pool(size, min, max)
1301
+ try:
1302
+ with Spinner("Altering engine pool", "Engine pool altered"):
1303
+ # Type cast to ensure type checker recognizes the method
1304
+ cast(ResourcesBase, provider).alter_engine_pool(size, min, max)
1305
+ except Exception as e:
1306
+ return exit_with_handled_exception("Error altering engine pool", e)
1154
1307
  divider()
1155
1308
 
1156
1309
  #--------------------------------------------------
@@ -1319,7 +1472,7 @@ def parse_source(provider: ResourcesBase, raw: str) -> ImportSource:
1319
1472
 
1320
1473
  @supports_platform("snowflake")
1321
1474
  @cli.command(name="imports:setup", help="Modify and view imports setup")
1322
- @click.option("--engine_size", help="Engine size")
1475
+ @click.option("--engine-size", "--engine_size", help="Engine size")
1323
1476
  @click.option("--resume", help="Resume imports", is_flag=True)
1324
1477
  @click.option("--suspend", help="Suspend imports", is_flag=True)
1325
1478
  def imports_setup(engine_size:str|None=None, resume:bool=False, suspend:bool=False):
@@ -1380,7 +1533,7 @@ def imports_setup(engine_size:str|None=None, resume:bool=False, suspend:bool=Fal
1380
1533
  lambda k, v: {k: str(v), "style": "red"} if k == "enabled" and not v else format_row(k, v)
1381
1534
  )
1382
1535
  except Exception as e:
1383
- exit_with_error(f"\n\n[yellow]Error fetching imports setup: {e}")
1536
+ exit_with_handled_exception("Error fetching imports setup", e)
1384
1537
  divider()
1385
1538
 
1386
1539
 
@@ -1496,7 +1649,7 @@ def imports_wait(source: List[str], model: str):
1496
1649
  try:
1497
1650
  models = [model["name"] for model in provider.list_graphs()]
1498
1651
  except Exception as e:
1499
- return exit_with_error(f"\n\n[yellow]Error fetching models: {e}")
1652
+ return exit_with_handled_exception("Error fetching models", e)
1500
1653
  if not models:
1501
1654
  return exit_with_error("[yellow]No models found")
1502
1655
  rich.print()
@@ -1592,7 +1745,7 @@ def imports_stream(
1592
1745
  try:
1593
1746
  models = ["[CREATE MODEL]"] + [model["name"] for model in provider.list_graphs()]
1594
1747
  except Exception as e:
1595
- return exit_with_error(f"\n\n[yellow]Error fetching models: {e}")
1748
+ return exit_with_handled_exception("Error fetching models", e)
1596
1749
 
1597
1750
  rich.print()
1598
1751
  model = controls.fuzzy("Select a model:", models)
@@ -1614,7 +1767,7 @@ def imports_stream(
1614
1767
  else:
1615
1768
  sources = [parse_source(provider, source_) for source_ in source]
1616
1769
  except Exception as e:
1617
- return exit_with_error(f"[yellow]Error: {e}")
1770
+ return exit_with_handled_exception("Error", e)
1618
1771
 
1619
1772
  for import_source in sources:
1620
1773
  try:
@@ -1640,7 +1793,7 @@ def imports_stream(
1640
1793
  exit_with_error("\n\n[yellow]Stream engine not found. Please use '[cyan]rai imports:setup[/cyan]' to set up imports.")
1641
1794
  else:
1642
1795
  rich.print()
1643
- exit_with_error(f"\n[yellow]Error creating stream: {e}")
1796
+ exit_with_handled_exception("Error creating stream", e)
1644
1797
  wait = not no_wait
1645
1798
  if wait:
1646
1799
  poll_imports(provider, [source.name for source in sources], model, no_wait_notice=True)
@@ -1673,7 +1826,7 @@ def imports_snapshot(source:str|None, model:str|None, name:str|None, type:str|No
1673
1826
  try:
1674
1827
  models = [model["name"] for model in provider.list_graphs()]
1675
1828
  except Exception as e:
1676
- return exit_with_error(f"\n\n[yellow]Error fetching models: {e}")
1829
+ return exit_with_handled_exception("Error fetching models", e)
1677
1830
  if len(models) == 0:
1678
1831
  exit_with_error("[yellow]No models found")
1679
1832
  rich.print()
@@ -1695,7 +1848,7 @@ def imports_snapshot(source:str|None, model:str|None, name:str|None, type:str|No
1695
1848
  exit_with_error("\n[yellow]Error creating snapshot, aborting.")
1696
1849
 
1697
1850
  except Exception as e:
1698
- exit_with_error(f"\n\n[yellow]Error creating snapshot: {e}")
1851
+ exit_with_handled_exception("Error creating snapshot", e)
1699
1852
  divider()
1700
1853
 
1701
1854
  #--------------------------------------------------
@@ -1715,7 +1868,7 @@ def imports_delete(object, model, force):
1715
1868
  try:
1716
1869
  models = [model["name"] for model in provider.list_graphs()]
1717
1870
  except Exception as e:
1718
- return exit_with_error(f"\n\n[yellow]Error fetching models: {e}")
1871
+ return exit_with_handled_exception("Error fetching models", e)
1719
1872
  if len(models) == 0:
1720
1873
  rich.print()
1721
1874
  exit_with_error("[yellow]No models found")
@@ -1727,7 +1880,7 @@ def imports_delete(object, model, force):
1727
1880
  try:
1728
1881
  imports = provider.list_imports(model=model)
1729
1882
  except Exception as e:
1730
- return exit_with_error(f"\n\n[yellow]Error fetching imports: {e}")
1883
+ return exit_with_handled_exception("Error fetching imports", e)
1731
1884
 
1732
1885
  if not imports and not force:
1733
1886
  rich.print()
@@ -1751,7 +1904,7 @@ def imports_delete(object, model, force):
1751
1904
  try:
1752
1905
  provider.delete_import(object, model, force)
1753
1906
  except Exception as e:
1754
- exit_with_error(f"\n\n[yellow]Error deleting import: {e}")
1907
+ exit_with_handled_exception("Error deleting import", e)
1755
1908
  divider()
1756
1909
 
1757
1910
  #--------------------------------------------------
@@ -1771,7 +1924,7 @@ def exports_list(model):
1771
1924
  try:
1772
1925
  models = [model["name"] for model in provider.list_graphs()]
1773
1926
  except Exception as e:
1774
- return exit_with_error(f"\n\n[yellow]Error fetching models: {e}")
1927
+ return exit_with_handled_exception("Error fetching models", e)
1775
1928
  if len(models) == 0:
1776
1929
  return exit_with_error("[yellow]No models found")
1777
1930
  rich.print()
@@ -1782,7 +1935,7 @@ def exports_list(model):
1782
1935
  try:
1783
1936
  exports = provider.list_exports(model, "")
1784
1937
  except Exception as e:
1785
- return exit_with_error(f"\n\n[yellow]Error fetching exports: {e}")
1938
+ return exit_with_handled_exception("Error fetching exports", e)
1786
1939
 
1787
1940
  rich.print()
1788
1941
  if len(exports):
@@ -1813,7 +1966,7 @@ def exports_delete(export, model):
1813
1966
  try:
1814
1967
  models = [model["name"] for model in provider.list_graphs()]
1815
1968
  except Exception as e:
1816
- return exit_with_error(f"\n\n[yellow]Error fetching models: {e}")
1969
+ return exit_with_handled_exception("Error fetching models", e)
1817
1970
  if len(models) == 0:
1818
1971
  exit_with_error("[yellow]No models found")
1819
1972
  rich.print()
@@ -1849,7 +2002,7 @@ def transactions_get(id):
1849
2002
  try:
1850
2003
  transaction = provider.get_transaction(id)
1851
2004
  except Exception as e:
1852
- exit_with_error(f"\n\n[yellow]Error fetching transaction: {e}")
2005
+ exit_with_handled_exception("Error fetching transaction", e)
1853
2006
  rich.print()
1854
2007
  if transaction:
1855
2008
  show_dictionary_table(transaction, format_row)
@@ -1881,7 +2034,7 @@ def transactions_list(id, state, engine, limit, all_users):
1881
2034
  )
1882
2035
  except Exception as e:
1883
2036
  rich.print()
1884
- return exit_with_error(f"\n\n[yellow]Error fetching transactions: {e}\n")
2037
+ return exit_with_handled_exception("Error fetching transactions", e)
1885
2038
 
1886
2039
  if len(transactions) == 0:
1887
2040
  rich.print()
@@ -1912,7 +2065,7 @@ def transactions_cancel(id, all_users):
1912
2065
  created_by=cfg.get("user", None),
1913
2066
  )
1914
2067
  except Exception as e:
1915
- return exit_with_error(f"\n\n[yellow]Error fetching transactions: {e}")
2068
+ return exit_with_handled_exception("Error fetching transactions", e)
1916
2069
 
1917
2070
  if not transactions:
1918
2071
  exit_with_error("\n[yellow]No active transactions found")