1313
1414def _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]:
7456def _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-
12166def _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
162120def 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