anyscale 0.26.50__py3-none-any.whl → 0.26.52__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 (48) hide show
  1. anyscale/_private/anyscale_client/README.md +1 -1
  2. anyscale/_private/anyscale_client/anyscale_client.py +178 -46
  3. anyscale/_private/anyscale_client/common.py +61 -2
  4. anyscale/_private/anyscale_client/fake_anyscale_client.py +145 -8
  5. anyscale/_private/docgen/__main__.py +34 -23
  6. anyscale/_private/docgen/generator.py +15 -18
  7. anyscale/_private/docgen/models.md +4 -2
  8. anyscale/_private/workload/workload_sdk.py +103 -8
  9. anyscale/client/README.md +5 -0
  10. anyscale/client/openapi_client/__init__.py +1 -0
  11. anyscale/client/openapi_client/api/default_api.py +538 -0
  12. anyscale/client/openapi_client/models/__init__.py +1 -0
  13. anyscale/client/openapi_client/models/baseimagesenum.py +83 -1
  14. anyscale/client/openapi_client/models/cloud_resource.py +59 -3
  15. anyscale/client/openapi_client/models/cloud_resource_gcp.py +59 -3
  16. anyscale/client/openapi_client/models/clouddeployment_response.py +121 -0
  17. anyscale/client/openapi_client/models/create_cloud_resource.py +59 -3
  18. anyscale/client/openapi_client/models/create_cloud_resource_gcp.py +59 -3
  19. anyscale/client/openapi_client/models/object_storage.py +2 -2
  20. anyscale/client/openapi_client/models/ray_runtime_env_config.py +57 -1
  21. anyscale/client/openapi_client/models/supportedbaseimagesenum.py +80 -1
  22. anyscale/cloud/models.py +1 -1
  23. anyscale/commands/cloud_commands.py +73 -70
  24. anyscale/commands/command_examples.py +28 -40
  25. anyscale/commands/project_commands.py +377 -106
  26. anyscale/commands/workspace_commands_v2.py +62 -29
  27. anyscale/controllers/cloud_controller.py +91 -91
  28. anyscale/job/_private/job_sdk.py +38 -20
  29. anyscale/project/__init__.py +101 -1
  30. anyscale/project/_private/project_sdk.py +90 -2
  31. anyscale/project/commands.py +188 -1
  32. anyscale/project/models.py +198 -2
  33. anyscale/sdk/anyscale_client/models/baseimagesenum.py +83 -1
  34. anyscale/sdk/anyscale_client/models/ray_runtime_env_config.py +57 -1
  35. anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +80 -1
  36. anyscale/service/_private/service_sdk.py +2 -1
  37. anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
  38. anyscale/util.py +3 -0
  39. anyscale/utils/cloud_utils.py +20 -0
  40. anyscale/utils/runtime_env.py +3 -1
  41. anyscale/version.py +1 -1
  42. {anyscale-0.26.50.dist-info → anyscale-0.26.52.dist-info}/METADATA +1 -1
  43. {anyscale-0.26.50.dist-info → anyscale-0.26.52.dist-info}/RECORD +48 -47
  44. {anyscale-0.26.50.dist-info → anyscale-0.26.52.dist-info}/WHEEL +0 -0
  45. {anyscale-0.26.50.dist-info → anyscale-0.26.52.dist-info}/entry_points.txt +0 -0
  46. {anyscale-0.26.50.dist-info → anyscale-0.26.52.dist-info}/licenses/LICENSE +0 -0
  47. {anyscale-0.26.50.dist-info → anyscale-0.26.52.dist-info}/licenses/NOTICE +0 -0
  48. {anyscale-0.26.50.dist-info → anyscale-0.26.52.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,99 @@
1
- from typing import Optional
1
+ from io import StringIO
2
+ from json import dumps as json_dumps
3
+ import sys
4
+ from typing import Dict, Optional, Tuple
2
5
 
3
6
  import click
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+ import yaml
4
10
 
5
11
  import anyscale
6
12
  from anyscale.cli_logger import BlockLogger
7
13
  from anyscale.commands import command_examples
8
- from anyscale.commands.util import AnyscaleCommand, LegacyAnyscaleCommand, NotRequiredIf
14
+ from anyscale.commands.list_util import display_list
15
+ from anyscale.commands.util import AnyscaleCommand, NotRequiredIf
9
16
  from anyscale.controllers.project_controller import ProjectController
10
17
  from anyscale.project.models import (
11
18
  CreateProjectCollaborator,
12
19
  CreateProjectCollaborators,
20
+ Project,
21
+ ProjectMinimal,
22
+ ProjectSortField,
23
+ ProjectSortOrder,
13
24
  )
14
25
  from anyscale.project_utils import validate_project_name
15
- from anyscale.util import validate_non_negative_arg
26
+ from anyscale.util import (
27
+ AnyscaleJSONEncoder,
28
+ get_endpoint,
29
+ )
16
30
 
17
31
 
18
32
  log = BlockLogger()
19
33
 
34
+ MAX_PAGE_SIZE = 50
35
+ NON_INTERACTIVE_DEFAULT_MAX_ITEMS = 10
36
+
37
+
38
+ def _create_project_list_table(show_header: bool) -> Table:
39
+ table = Table(show_header=show_header, expand=True)
40
+ # NAME and ID: larger ratios, can wrap but never truncate
41
+ table.add_column(
42
+ "NAME", no_wrap=False, overflow="fold", ratio=3, min_width=15,
43
+ )
44
+ table.add_column(
45
+ "ID", no_wrap=False, overflow="fold", ratio=2, min_width=12,
46
+ )
47
+ # all other columns will wrap as needed
48
+ for heading in (
49
+ "DESCRIPTION",
50
+ "CREATED AT",
51
+ "CREATOR",
52
+ "PARENT CLOUD ID",
53
+ ):
54
+ table.add_column(
55
+ heading, no_wrap=False, overflow="fold", ratio=1, min_width=8,
56
+ )
57
+ return table
58
+
59
+
60
+ def _format_project_output_data(project: Project) -> Dict[str, str]:
61
+ return {
62
+ "name": project.name,
63
+ "id": project.id,
64
+ "description": project.description,
65
+ "created_at": project.created_at,
66
+ "creator": str(project.creator_id or ""),
67
+ "parent_cloud_id": str(project.parent_cloud_id or ""),
68
+ }
69
+
70
+
71
+ def _parse_sort_option(
72
+ sort: Optional[str],
73
+ ) -> Tuple[Optional[ProjectSortField], ProjectSortOrder]:
74
+ if not sort:
75
+ return None, ProjectSortOrder.ASC
76
+
77
+ # build case-insensitive map of allowed fields
78
+ allowed = {f.value.lower(): f.value for f in ProjectSortField.__members__.values()}
79
+
80
+ # detect leading '-' for descending
81
+ if sort.startswith("-"):
82
+ raw = sort[1:]
83
+ order = ProjectSortOrder.DESC
84
+ else:
85
+ raw = sort
86
+ order = ProjectSortOrder.ASC
87
+
88
+ key = raw.lower()
89
+ if key not in allowed:
90
+ allowed_names = ", ".join(sorted(allowed.values()))
91
+ raise click.BadParameter(
92
+ f"Invalid sort field '{raw}'. Allowed fields: {allowed_names}"
93
+ )
94
+
95
+ return ProjectSortField(allowed[key]), order
96
+
20
97
 
21
98
  @click.group(
22
99
  "project",
@@ -28,49 +105,316 @@ def project_cli() -> None:
28
105
 
29
106
 
30
107
  @project_cli.command(
31
- name="list",
32
- short_help="List projects for which you have access.",
33
- help="List projects for which you have access. By default, only projects created by you are listed.",
34
- cls=LegacyAnyscaleCommand,
35
- is_limited_support=True,
36
- legacy_prefix="anyscale project",
108
+ name="get",
109
+ help="Get details of a project.",
110
+ cls=AnyscaleCommand,
111
+ example=command_examples.PROJECT_GET_EXAMPLE,
37
112
  )
38
113
  @click.option(
39
- "--name", "-n", help="List information for a particular project.", type=str
114
+ "--id", "-i", type=str, required=True, help="ID of the project.",
40
115
  )
41
- @click.option("--json", help="Format output as JSON.", is_flag=True)
42
116
  @click.option(
43
- "--any-creator",
44
- "-a",
45
- help="[Deprecated] List projects created by any user.",
117
+ "--json",
118
+ "-j",
46
119
  is_flag=True,
47
- default=None,
48
- hidden=True,
120
+ default=False,
121
+ help="Output the details in a structured JSON format.",
122
+ )
123
+ def get(id: str, json: bool = False): # noqa: A002
124
+ try:
125
+ project: Project = anyscale.project.get(project_id=id)
126
+ except ValueError as e:
127
+ log.error(f"Error getting project details: {e}")
128
+ sys.exit(1)
129
+
130
+ console = Console()
131
+ if json:
132
+ json_str = json_dumps(project.to_dict(), indent=2, cls=AnyscaleJSONEncoder)
133
+ console.print_json(json=json_str)
134
+ else:
135
+ stream = StringIO()
136
+ yaml.dump(project.to_dict(), stream, sort_keys=False)
137
+ console.print(stream.getvalue(), end="")
138
+
139
+
140
+ @project_cli.command(
141
+ name="list",
142
+ help="List all projects with optional filters.",
143
+ cls=AnyscaleCommand,
144
+ example=command_examples.PROJECT_LIST_EXAMPLE,
49
145
  )
50
- @click.option("--created-by-me", help="List projects created by me only.", is_flag=True)
51
146
  @click.option(
52
- "--max-items",
53
- required=False,
54
- default=20,
147
+ "--name", "-n", type=str, help="A string to filter projects by name.",
148
+ )
149
+ @click.option(
150
+ "--creator", "-u", type=str, help="The ID of a creator to filter projects.",
151
+ )
152
+ @click.option(
153
+ "--cloud", "-c", type=str, help="The ID of a parent cloud to filter projects.",
154
+ )
155
+ @click.option(
156
+ "--include-defaults/--exclude-defaults",
157
+ default=True,
158
+ show_default=True,
159
+ help="Whether to include default projects.",
160
+ )
161
+ @click.option(
162
+ "--max-items", type=int, help="The maximum number of projects to return.",
163
+ )
164
+ @click.option(
165
+ "--page-size",
55
166
  type=int,
56
- help="Max items to show in list.",
57
- callback=validate_non_negative_arg,
167
+ default=10,
168
+ show_default=True,
169
+ help="The number of projects to return per page.",
170
+ )
171
+ @click.option(
172
+ "--sort",
173
+ help=(
174
+ "Sort by FIELD (prefix with '-' for desc). "
175
+ f"Allowed: {', '.join(ProjectSortField.__members__.values())}"
176
+ ),
177
+ )
178
+ @click.option(
179
+ "--interactive/--no-interactive",
180
+ default=True,
181
+ show_default=True,
182
+ help="Use interactive paging.",
183
+ )
184
+ @click.option(
185
+ "--json",
186
+ "-j",
187
+ is_flag=True,
188
+ default=False,
189
+ help="Output the list in a structured JSON format.",
58
190
  )
59
191
  def list( # noqa: A001
192
+ *,
193
+ name: Optional[str] = None,
194
+ creator: Optional[str] = None,
195
+ cloud: Optional[str] = None,
196
+ include_defaults: bool = True,
197
+ max_items: Optional[int] = None,
198
+ page_size: Optional[int] = None,
199
+ sort: Optional[str] = None,
200
+ interactive: bool = True,
201
+ json: bool = False,
202
+ ):
203
+
204
+ if max_items is not None and interactive:
205
+ raise click.UsageError("--max-items only allowed with --no-interactive")
206
+
207
+ sort_field, sort_order = _parse_sort_option(sort)
208
+
209
+ # normalize max_items
210
+ effective_max = max_items
211
+ if not interactive and effective_max is None:
212
+ stderr = Console(stderr=True)
213
+ stderr.print(
214
+ f"Defaulting to {NON_INTERACTIVE_DEFAULT_MAX_ITEMS} items in batch mode; "
215
+ "use --max-items to override."
216
+ )
217
+ effective_max = NON_INTERACTIVE_DEFAULT_MAX_ITEMS
218
+
219
+ console = Console()
220
+ stderr = Console(stderr=True)
221
+
222
+ # diagnostics
223
+ stderr.print("[bold]Listing projects with:[/]")
224
+ stderr.print(f"• name_contains = {name or '<any>'}")
225
+ stderr.print(f"• creator_id = {creator or '<any>'}")
226
+ stderr.print(f"• parent_cloud_id = {cloud or '<any>'}")
227
+ stderr.print(f"• include defaults = {include_defaults}")
228
+ stderr.print(f"• sort-field = {sort_field or '<none>'}")
229
+ stderr.print(f"• sort-order = {sort_order or '<none>'}")
230
+ stderr.print(f"• mode = {'interactive' if interactive else 'batch'}")
231
+ stderr.print(f"• per-page limit = {page_size}")
232
+ stderr.print(f"• max-items total = {effective_max or 'all'}")
233
+ stderr.print(f"\nView your Projects in the UI at {get_endpoint('/projects')}\n")
234
+
235
+ # choose formatter
236
+ if json:
237
+
238
+ def formatter(project):
239
+ return ProjectMinimal.from_dict(project.to_dict()).to_dict()
240
+
241
+ else:
242
+ formatter = _format_project_output_data
243
+
244
+ total = 0
245
+ try:
246
+ iterator = anyscale.project.list(
247
+ name_contains=name,
248
+ creator_id=creator,
249
+ parent_cloud_id=cloud,
250
+ include_defaults=include_defaults,
251
+ max_items=effective_max,
252
+ page_size=page_size,
253
+ sort_field=sort_field,
254
+ sort_order=sort_order,
255
+ )
256
+ total = display_list(
257
+ iterator=iter(iterator),
258
+ item_formatter=formatter,
259
+ table_creator=_create_project_list_table,
260
+ json_output=json,
261
+ page_size=page_size or MAX_PAGE_SIZE,
262
+ interactive=interactive,
263
+ max_items=effective_max,
264
+ console=console,
265
+ )
266
+
267
+ if not json:
268
+ if total > 0:
269
+ stderr.print(f"\nFetched {total} projects.")
270
+ else:
271
+ stderr.print("\nNo projects found.")
272
+ except Exception as e: # noqa: BLE001
273
+ log.error(f"Failed to list projects: {e}")
274
+ sys.exit(1)
275
+
276
+
277
+ @project_cli.command(
278
+ name="create",
279
+ help="Create a new project.",
280
+ cls=AnyscaleCommand,
281
+ example=command_examples.PROJECT_CREATE_EXAMPLE,
282
+ )
283
+ @click.option(
284
+ "--name", "-n", type=str, required=True, help="Name of the project.",
285
+ )
286
+ @click.option(
287
+ "--cloud", "-c", type=str, required=True, help="Parent cloud ID for the project.",
288
+ )
289
+ @click.option(
290
+ "--description", "-d", type=str, help="Description of the project.",
291
+ )
292
+ @click.option(
293
+ "--initial-cluster-config",
294
+ "-f",
295
+ type=str,
296
+ help="Initial cluster config for the project.",
297
+ )
298
+ def create(
60
299
  name: str,
61
- json: bool,
62
- created_by_me: bool,
63
- any_creator: Optional[bool],
64
- max_items: int,
300
+ cloud: str,
301
+ *,
302
+ description: Optional[str] = None,
303
+ initial_cluster_config: Optional[str] = None,
65
304
  ) -> None:
66
- if any_creator is not None:
67
- log.warning(
68
- "`--any-creator` and `-a` flags have been deprecated. "
69
- "`anyscale project list` now shows projects created by any user by default. "
70
- "If you would like to show projects created by you only, you can pass the --created-by-me flag"
305
+ try:
306
+ project_id: str = anyscale.project.create(
307
+ name,
308
+ cloud,
309
+ description=description or "",
310
+ initial_cluster_config=initial_cluster_config,
71
311
  )
72
- project_controller = ProjectController()
73
- project_controller.list(name, json, created_by_me, max_items)
312
+ except ValueError as e:
313
+ log.error(f"Error creating project: {e}")
314
+ sys.exit(1)
315
+
316
+ log.info(f"Created project '{name}' with ID: {project_id}")
317
+
318
+
319
+ @project_cli.command(
320
+ name="delete",
321
+ help="Delete a project.",
322
+ cls=AnyscaleCommand,
323
+ example=command_examples.PROJECT_DELETE_EXAMPLE,
324
+ )
325
+ @click.option(
326
+ "--id", "-i", type=str, required=True, help="ID of the project to delete.",
327
+ )
328
+ def delete(id: str): # noqa: A002
329
+ try:
330
+ anyscale.project.delete(id)
331
+ except ValueError as e:
332
+ log.error(f"Error deleting project: {e}")
333
+ sys.exit(1)
334
+
335
+ log.info(f"Deleted project '{id}'")
336
+
337
+
338
+ @project_cli.command(
339
+ name="get-default",
340
+ help="Get the default project for a cloud.",
341
+ cls=AnyscaleCommand,
342
+ example=command_examples.PROJECT_GET_DEFAULT_EXAMPLE,
343
+ )
344
+ @click.option(
345
+ "--cloud", "-c", type=str, required=True, help="Parent cloud ID for the project.",
346
+ )
347
+ @click.option(
348
+ "--json",
349
+ "-j",
350
+ is_flag=True,
351
+ default=False,
352
+ help="Output the project in a structured JSON format.",
353
+ )
354
+ def get_default(cloud: str, json: bool = False):
355
+ try:
356
+ project: Project = anyscale.project.get_default(cloud)
357
+ except ValueError as e:
358
+ log.error(f"Error getting default project for cloud '{cloud}': {e}")
359
+ sys.exit(1)
360
+
361
+ console = Console()
362
+ if json:
363
+ json_str = json_dumps(project.to_dict(), indent=2, cls=AnyscaleJSONEncoder)
364
+ console.print_json(json=json_str)
365
+ else:
366
+ stream = StringIO()
367
+ yaml.dump(project.to_dict(), stream, sort_keys=False)
368
+ console.print(stream.getvalue(), end="")
369
+
370
+
371
+ @project_cli.command(
372
+ name="add-collaborators",
373
+ help="Add collaborators to the project.",
374
+ cls=AnyscaleCommand,
375
+ example=command_examples.PROJECT_ADD_COLLABORATORS_EXAMPLE,
376
+ )
377
+ @click.option(
378
+ "--cloud",
379
+ "-c",
380
+ help="Name of the cloud that the project belongs to.",
381
+ required=True,
382
+ )
383
+ @click.option(
384
+ "--project",
385
+ "-p",
386
+ help="Name of the project to add collaborators to.",
387
+ required=True,
388
+ )
389
+ @click.option(
390
+ "--users-file",
391
+ help="Path to a YAML file containing a list of users to add to the project.",
392
+ required=True,
393
+ )
394
+ def add_collaborators(cloud: str, project: str, users_file: str) -> None:
395
+ collaborators = CreateProjectCollaborators.from_yaml(users_file)
396
+
397
+ try:
398
+ anyscale.project.add_collaborators(
399
+ cloud=cloud,
400
+ project=project,
401
+ collaborators=[
402
+ CreateProjectCollaborator(**collaborator)
403
+ for collaborator in collaborators.collaborators
404
+ ],
405
+ )
406
+ except ValueError as e:
407
+ log.error(f"Error adding collaborators to project: {e}")
408
+ return
409
+
410
+ log.info(
411
+ f"Successfully added {len(collaborators.collaborators)} collaborators to project {project}."
412
+ )
413
+
414
+
415
+ # ================================================
416
+ # LEGACY CODE
417
+ # ================================================
74
418
 
75
419
 
76
420
  def _validate_project_name(ctx, param, value) -> str: # noqa: ARG001
@@ -178,76 +522,3 @@ def init(project_id: Optional[str], name: Optional[str],) -> None:
178
522
 
179
523
  project_controller = ProjectController()
180
524
  project_controller.init(project_id, name, None, None)
181
-
182
-
183
- @project_cli.command(
184
- name="create",
185
- help="Create a new project.",
186
- cls=LegacyAnyscaleCommand,
187
- is_limited_support=True,
188
- legacy_prefix="anyscale project",
189
- )
190
- @click.option(
191
- "--name",
192
- "-n",
193
- help="Project name.",
194
- callback=_validate_project_name,
195
- prompt=True,
196
- default=_default_project_name(),
197
- )
198
- @click.option(
199
- "--parent-cloud-id",
200
- required=False,
201
- default=None,
202
- help=(
203
- "Cloud id that this project is associated with. This argument "
204
- "is only relevant if cloud isolation is enabled."
205
- ),
206
- )
207
- def create(name: str, parent_cloud_id: str) -> None:
208
- project_controller = ProjectController()
209
- project_controller.create(name, parent_cloud_id)
210
-
211
-
212
- @project_cli.command(
213
- name="add-collaborators",
214
- help="Add collaborators to the project.",
215
- cls=AnyscaleCommand,
216
- example=command_examples.PROJECT_ADD_COLLABORATORS_EXAMPLE,
217
- )
218
- @click.option(
219
- "--cloud",
220
- "-c",
221
- help="Name of the cloud that the project belongs to.",
222
- required=True,
223
- )
224
- @click.option(
225
- "--project",
226
- "-p",
227
- help="Name of the project to add collaborators to.",
228
- required=True,
229
- )
230
- @click.option(
231
- "--users-file",
232
- help="Path to a YAML file containing a list of users to add to the project.",
233
- required=True,
234
- )
235
- def add_collaborators(cloud: str, project: str, users_file: str,) -> None:
236
- collaborators = CreateProjectCollaborators.from_yaml(users_file)
237
-
238
- try:
239
- anyscale.project.add_collaborators(
240
- cloud=cloud,
241
- project=project,
242
- collaborators=[
243
- CreateProjectCollaborator(**collaborator)
244
- for collaborator in collaborators.collaborators
245
- ],
246
- )
247
- except ValueError as e:
248
- log.error(f"Error adding collaborators to project: {e}")
249
- return
250
-
251
- log.info(
252
- f"Successfully added {len(collaborators.collaborators)} collaborators to project {project}."
253
- )
@@ -34,7 +34,7 @@ log = BlockLogger() # CLI Logger
34
34
 
35
35
  # Constants for SSH configuration
36
36
  HTTPS_PORT = "443"
37
- SSH_TIMEOUT_SECONDS = 45
37
+ SSH_TEST_TIMEOUT_SECONDS = 8
38
38
  WSS_PATH = "/sshws"
39
39
  PREFERRED_AUTH_METHOD = "PreferredAuthentications=publickey"
40
40
 
@@ -319,15 +319,10 @@ def _parse_user_args(user_args: List[str]) -> Tuple[List[str], List[str]]:
319
319
  return user_options, user_command
320
320
 
321
321
 
322
- def _try_https_connection(
323
- workspace_obj: Workspace,
324
- workspace_private_sdk,
325
- host_name: str,
326
- config_file: str,
327
- ctx_args: List[str],
328
- shell_command: str,
322
+ def _test_https_connectivity(
323
+ workspace_obj: Workspace, workspace_private_sdk, host_name: str, config_file: str,
329
324
  ) -> bool:
330
- """Attempt HTTPS SSH connection. Returns True if successful."""
325
+ """Test HTTPS SSH connectivity with a quick command. Returns True if available."""
331
326
  try:
332
327
  cluster = workspace_private_sdk.client.get_workspace_cluster(workspace_obj.id)
333
328
  if not cluster:
@@ -337,35 +332,62 @@ def _try_https_connection(
337
332
  workspace_obj, workspace_private_sdk, host_name, config_file
338
333
  )
339
334
 
340
- ssh_cmd = _build_ssh_command(ssh_config, ctx_args, shell_command)
335
+ # Build a test command using the same logic as the actual connection
336
+ # but with a simple echo command that exits immediately
337
+ test_args = [
338
+ "-o",
339
+ "ConnectTimeout=5",
340
+ "-o",
341
+ "BatchMode=yes",
342
+ "echo",
343
+ "connectivity_test",
344
+ ]
345
+ test_cmd = _build_ssh_command(ssh_config, test_args, "")
341
346
 
342
- # Try HTTPS connection with a timeout
347
+ # Run test with a timeout
343
348
  result = subprocess.run(
344
- ssh_cmd,
349
+ test_cmd,
345
350
  check=False,
346
- timeout=SSH_TIMEOUT_SECONDS,
347
- stderr=subprocess.DEVNULL,
351
+ timeout=SSH_TEST_TIMEOUT_SECONDS,
352
+ capture_output=True,
353
+ text=True,
348
354
  )
349
355
 
350
- return result.returncode == 0
356
+ # Check if we got the expected output
357
+ if result.returncode == 0 and "connectivity_test" in result.stdout:
358
+ return True
359
+
360
+ # Connection failed - no need to show error code to user
361
+ return False
351
362
 
352
363
  except subprocess.TimeoutExpired:
353
- print(
354
- "HTTPS connection timed out or failed (SSH proxy may not be available), "
355
- "falling back to Legacy SSH connection..."
356
- )
364
+ # Silent failure - the main code will show a user-friendly message
357
365
  return False
358
366
  except OSError:
359
- print(
360
- "HTTPS connection timed out or failed (SSH proxy may not be available), "
361
- "falling back to Legacy SSH connection..."
362
- )
367
+ # Silent failure
363
368
  return False
364
369
  except (click.ClickException, ValueError, AttributeError, KeyError, TypeError):
365
- print("HTTPS setup failed, falling back to Legacy SSH connection...")
370
+ # Silent failure
366
371
  return False
367
372
 
368
373
 
374
+ def _execute_https_ssh(
375
+ workspace_obj: Workspace,
376
+ workspace_private_sdk,
377
+ host_name: str,
378
+ config_file: str,
379
+ ctx_args: List[str],
380
+ shell_command: str,
381
+ ) -> None:
382
+ """Execute HTTPS SSH connection without timeout."""
383
+ ssh_config = _setup_https_connection(
384
+ workspace_obj, workspace_private_sdk, host_name, config_file
385
+ )
386
+ ssh_cmd = _build_ssh_command(ssh_config, ctx_args, shell_command)
387
+ # Run the actual SSH session without any timeout
388
+ subprocess.run(ssh_cmd, check=False)
389
+
390
+
369
391
  def _execute_legacy_ssh(
370
392
  ssh_target_host: str, config_file: str, ctx_args: List[str], shell_command: str,
371
393
  ) -> None:
@@ -857,21 +879,32 @@ id should be used, specifying both will result in an error.
857
879
  )
858
880
 
859
881
  if cluster:
860
- https_connection_successful = _try_https_connection(
882
+ https_connection_successful = _test_https_connectivity(
861
883
  workspace_obj,
862
884
  workspace_private_sdk,
863
885
  host_name,
864
886
  config_file,
865
- ctx.args,
866
- shell_command,
867
887
  )
868
888
 
869
889
  except (ValueError, AttributeError, KeyError, TypeError):
870
890
  # If we can't get workspace/cluster info, proceed with legacy SSH
871
891
  pass
872
892
 
873
- # Run legacy SSH command if HTTPS wasn't successful or --legacy was specified
874
- if not https_connection_successful:
893
+ # Execute the appropriate SSH connection based on test results
894
+ if https_connection_successful:
895
+ # HTTPS connectivity test passed, run actual SSH session without timeout
896
+ _execute_https_ssh(
897
+ workspace_obj,
898
+ workspace_private_sdk,
899
+ host_name,
900
+ config_file,
901
+ ctx.args,
902
+ shell_command,
903
+ )
904
+ else:
905
+ # HTTPS test failed or --legacy was specified, use legacy SSH
906
+ if not legacy: # Only show message if we tried HTTPS first
907
+ print("Connecting via standard SSH...")
875
908
  _execute_legacy_ssh(
876
909
  ssh_target_host, config_file, ctx.args, shell_command
877
910
  )