Skip to content

Commit 73245b9

Browse files
author
Mohamed Zeidan
committed
fully agnostic design
1 parent 2272636 commit 73245b9

File tree

2 files changed

+124
-179
lines changed

2 files changed

+124
-179
lines changed

src/sagemaker/hyperpod/common/cli_decorators.py

Lines changed: 71 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -13,45 +13,27 @@
1313

1414
def _extract_resource_from_command(func) -> tuple[str, str]:
1515
"""
16-
Extract resource type and display name from command context - fully template-agnostic.
17-
No hardcoded mappings - works with any hyp-<noun> pattern.
16+
Extract resource type and display name from command context - template-agnostic.
17+
Simplified version focused on this codebase's specific Click usage patterns.
1818
1919
Returns:
2020
Tuple of (raw_resource_type, display_name) where:
21-
- raw_resource_type: for list commands (e.g., "jumpstart-endpoint")
22-
- display_name: for user messages (e.g., "JumpStart endpoint")
21+
- raw_resource_type: for list commands (e.g., "resource-type")
22+
- display_name: for user messages (e.g., "Resource Type")
2323
"""
2424
try:
25-
# Try multiple ways to get Click command name - template-agnostic
2625
command_name = None
2726

28-
# Method 1: Direct access to func.name (if available)
27+
# Method 1: Direct access to func.name (covers 90% of cases in this codebase)
2928
if hasattr(func, 'name') and func.name:
3029
command_name = func.name.lower()
3130

32-
# Method 2: Access Click command through function attributes
33-
elif hasattr(func, 'callback') and hasattr(func.callback, 'name'):
34-
command_name = func.callback.name.lower()
35-
36-
# Method 3: Check __wrapped__ attribute chain
31+
# Method 2: Check __wrapped__ attribute chain (for complex decorator combinations)
3732
elif hasattr(func, '__wrapped__'):
3833
wrapped = func.__wrapped__
3934
if hasattr(wrapped, 'name') and wrapped.name:
4035
command_name = wrapped.name.lower()
4136

42-
# Method 4: Inspect all function attributes for Click command info
43-
for attr_name in dir(func):
44-
if not attr_name.startswith('_'):
45-
try:
46-
attr_value = getattr(func, attr_name)
47-
if hasattr(attr_value, 'name') and isinstance(getattr(attr_value, 'name', None), str):
48-
attr_name_val = attr_value.name
49-
if attr_name_val and attr_name_val.startswith('hyp-'):
50-
command_name = attr_name_val.lower()
51-
break
52-
except:
53-
continue
54-
5537
# If we found a Click command name, parse it
5638
if command_name and command_name.startswith('hyp-'):
5739
resource_part = command_name[4:] # Remove 'hyp-' prefix
@@ -74,95 +56,70 @@ def _extract_resource_from_command(func) -> tuple[str, str]:
7456
def _format_display_name(resource_part: str) -> str:
7557
"""
7658
Format resource part into user-friendly display name.
77-
Template-agnostic formatting rules.
59+
Completely template-agnostic - no hardcoded template names.
7860
"""
79-
# Handle common patterns with proper capitalization
61+
# Split on hyphens and capitalize each part
8062
parts = resource_part.split('-')
81-
formatted_parts = []
82-
83-
for part in parts:
84-
if part.lower() == 'jumpstart':
85-
formatted_parts.append('JumpStart')
86-
elif part.lower() == 'pytorch':
87-
formatted_parts.append('PyTorch')
88-
else:
89-
# Capitalize first letter of other parts
90-
formatted_parts.append(part.capitalize())
91-
63+
formatted_parts = [part.capitalize() for part in parts]
9264
return ' '.join(formatted_parts)
9365

94-
def _detect_operation_type_from_function(func) -> str:
95-
"""
96-
Dynamically detect operation type from function name.
97-
Template-agnostic - works with any operation pattern.
98-
99-
Returns:
100-
Operation type string (e.g., "delete", "describe", "list")
101-
"""
102-
try:
103-
func_name = func.__name__.lower()
104-
105-
if 'delete' in func_name:
106-
return "delete"
107-
elif 'describe' in func_name or 'get' in func_name:
108-
return "describe"
109-
elif 'list' in func_name:
110-
return "list"
111-
elif 'create' in func_name:
112-
return "create"
113-
elif 'update' in func_name:
114-
return "update"
115-
116-
except (AttributeError, TypeError):
117-
pass
118-
119-
return "access" # Generic fallback
120-
12166
def _get_list_command_from_resource_type(raw_resource_type: str) -> str:
12267
"""
12368
Generate appropriate list command for resource type.
12469
Fully template-agnostic - constructs command directly from raw resource type.
12570
"""
126-
# raw_resource_type is already in the correct format (e.g., "jumpstart-endpoint")
71+
# raw_resource_type is already in the correct format (e.g., "resource-type")
12772
return f"hyp list hyp-{raw_resource_type}"
12873

129-
def _get_available_resource_count(raw_resource_type: str, namespace: str) -> int:
74+
def _check_resources_exist(raw_resource_type: str, namespace: str) -> bool:
13075
"""
131-
Get count of available resources in namespace - template-agnostic approach.
132-
Maps exact resource types to their SDK classes.
76+
Check if any resources exist in namespace - template-agnostic CLI approach.
77+
Uses the existing CLI commands to check for resource existence without importing template classes.
78+
Returns True if resources exist, False if no resources, None if unable to determine.
13379
"""
13480
try:
135-
# Direct mapping based on exact resource type - truly template-agnostic
136-
if raw_resource_type == "pytorch-job":
137-
from sagemaker.hyperpod.training.hyperpod_pytorch_job import HyperPodPytorchJob
138-
jobs = HyperPodPytorchJob.list(namespace=namespace)
139-
return len(jobs)
140-
141-
elif raw_resource_type == "jumpstart-endpoint":
142-
from sagemaker.hyperpod.inference.hp_jumpstart_endpoint import HPJumpStartEndpoint
143-
endpoints = HPJumpStartEndpoint.model_construct().list(namespace=namespace)
144-
return len(endpoints)
145-
146-
elif raw_resource_type == "custom-endpoint":
147-
from sagemaker.hyperpod.inference.hp_endpoint import HPEndpoint
148-
endpoints = HPEndpoint.model_construct().list(namespace=namespace)
149-
return len(endpoints)
81+
import subprocess
82+
83+
# Construct the list command that already exists (use hyp directly)
84+
cmd = ["hyp", "list", f"hyp-{raw_resource_type}"]
85+
if namespace != "default":
86+
cmd.extend(["--namespace", namespace])
87+
88+
logger.debug(f"Executing command to check resource existence: {' '.join(cmd)}")
89+
90+
result = subprocess.run(
91+
cmd,
92+
capture_output=True,
93+
text=True,
94+
timeout=15, # 15 second timeout
95+
check=False # Don't raise on non-zero exit
96+
)
97+
98+
if result.returncode == 0 and result.stdout.strip():
99+
# Check if output contains any data rows (simple heuristic: more than 2 lines means header + separator + data)
100+
lines = [line.strip() for line in result.stdout.strip().split('\n') if line.strip()]
150101

151-
# Future templates will be added here as exact matches
152-
# elif raw_resource_type == "llama-job":
153-
# from sagemaker.hyperpod.training.hyperpod_llama_job import HyperPodLlamaJob
154-
# jobs = HyperPodLlamaJob.list(namespace=namespace)
155-
# return len(jobs)
102+
# If we have more than 2 lines, likely we have: header + separator + at least one data row
103+
# This is much simpler and more reliable than parsing the table format
104+
has_data = len(lines) > 2
156105

106+
logger.debug(f"Found {len(lines)} lines in output, has_data: {has_data}")
107+
return has_data
108+
109+
# If command failed or no output, assume no resources
110+
logger.debug(f"List command failed or returned no data. Return code: {result.returncode}")
111+
return False
112+
113+
except subprocess.TimeoutExpired:
114+
logger.debug(f"List command timed out for {raw_resource_type}")
115+
return None
157116
except Exception as e:
158-
logger.debug(f"Failed to get resource count for {raw_resource_type}: {e}")
159-
160-
return -1 # Indicates count unavailable
117+
logger.debug(f"Failed to check resource existence for {raw_resource_type}: {e}")
118+
return None
161119

162120
def handle_cli_exceptions():
163121
"""
164122
Template-agnostic decorator that dynamically detects resource/operation types.
165-
Eliminates the need for hardcoded enums and makes CLI code template-agnostic.
166123
167124
This decorator:
168125
1. Dynamically detects resource type from Click command name
@@ -172,11 +129,11 @@ def handle_cli_exceptions():
172129
173130
Usage:
174131
@handle_cli_exceptions()
175-
@click.command("hyp-jumpstart-endpoint")
176-
def js_delete(name, namespace):
132+
@click.command("hyp-resource-type")
133+
def resource_delete(name, namespace):
177134
# Command logic here - no try/catch needed!
178-
# Resource type automatically detected as "JumpStart endpoint"
179-
# Operation type automatically detected as "delete"
135+
# Resource type automatically detected from command name
136+
# Operation type automatically detected from function name
180137
pass
181138
"""
182139
def decorator(func):
@@ -193,30 +150,29 @@ def wrapper(*args, **kwargs):
193150

194151
# Dynamically detect resource and operation types
195152
raw_resource_type, display_name = _extract_resource_from_command(func)
196-
operation_type = _detect_operation_type_from_function(func)
197153

198154
try:
199-
# Get available resource count for contextual message
200-
available_count = _get_available_resource_count(raw_resource_type, namespace)
155+
# Check if any resources exist for contextual message
156+
resources_exist = _check_resources_exist(raw_resource_type, namespace)
201157
list_command = _get_list_command_from_resource_type(raw_resource_type)
202158
namespace_flag = f" --namespace {namespace}" if namespace != "default" else ""
203159

204-
if available_count == 0:
160+
if resources_exist is False:
205161
# No resources exist in namespace
206162
enhanced_message = (
207163
f"❓ {display_name} '{name}' not found in namespace '{namespace}'. "
208164
f"No resources of this type exist in the namespace. "
209165
f"Use '{list_command}' to check for available resources."
210166
)
211-
elif available_count > 0:
167+
elif resources_exist is True:
212168
# Resources exist in namespace
213169
enhanced_message = (
214170
f"❓ {display_name} '{name}' not found in namespace '{namespace}'. "
215-
f"Please check the resource name. There are {available_count} resources in this namespace. "
171+
f"Please check the resource name - other resources exist in this namespace. "
216172
f"Use '{list_command}{namespace_flag}' to see available resources."
217173
)
218174
else:
219-
# Count unavailable - fallback to basic contextual message
175+
# Unable to determine - fallback to basic contextual message
220176
enhanced_message = (
221177
f"❓ {display_name} '{name}' not found in namespace '{namespace}'. "
222178
f"Please check the resource name and try again. "
@@ -225,6 +181,7 @@ def wrapper(*args, **kwargs):
225181

226182
click.echo(enhanced_message)
227183
sys.exit(1)
184+
return # Prevent fallback execution in tests
228185

229186
except Exception:
230187
# Fallback to basic message (no ❓ emoji for fallback)
@@ -234,6 +191,7 @@ def wrapper(*args, **kwargs):
234191
)
235192
click.echo(fallback_message)
236193
sys.exit(1)
194+
return # Prevent fallback execution in tests
237195

238196
# Check if this might be a wrapped 404 in a regular Exception
239197
elif "404" in str(e) or "not found" in str(e).lower():
@@ -259,24 +217,27 @@ def wrapper(*args, **kwargs):
259217
raw_resource_type, display_name = _extract_resource_from_command(func)
260218

261219
try:
262-
# Get available resource count for contextual message
263-
available_count = _get_available_resource_count(raw_resource_type, namespace)
220+
# Check if any resources exist for contextual message
221+
resources_exist = _check_resources_exist(raw_resource_type, namespace)
264222
list_command = _get_list_command_from_resource_type(raw_resource_type)
265223
namespace_flag = f" --namespace {namespace}" if namespace != "default" else ""
266224

267-
if available_count == 0:
225+
if resources_exist is False:
226+
# No resources exist in namespace
268227
enhanced_message = (
269228
f"❓ {display_name} '{name}' not found in namespace '{namespace}'. "
270229
f"No resources of this type exist in the namespace. "
271230
f"Use '{list_command}' to check for available resources."
272231
)
273-
elif available_count > 0:
232+
elif resources_exist is True:
233+
# Resources exist in namespace
274234
enhanced_message = (
275235
f"❓ {display_name} '{name}' not found in namespace '{namespace}'. "
276-
f"Please check the resource name. There are {available_count} resources in this namespace. "
236+
f"Please check the resource name - other resources exist in this namespace. "
277237
f"Use '{list_command}{namespace_flag}' to see available resources."
278238
)
279239
else:
240+
# Unable to determine - fallback to basic contextual message
280241
enhanced_message = (
281242
f"❓ {display_name} '{name}' not found in namespace '{namespace}'. "
282243
f"Please check the resource name and try again. "
@@ -285,12 +246,13 @@ def wrapper(*args, **kwargs):
285246

286247
click.echo(enhanced_message)
287248
sys.exit(1)
249+
return # Prevent fallback execution in tests
288250

289251
except Exception:
290252
# Fall through to standard handling
291253
pass
292254

293-
# For non-404 errors, use standard handling
255+
# For non-404 errors, use standard handling
294256
click.echo(str(e))
295257
sys.exit(1)
296258

0 commit comments

Comments
 (0)