33# Licensed under the MIT License. See License.txt in the project root for license information.
44# --------------------------------------------------------------------------------------------
55from packaging .version import parse
6+ from typing import Callable , List , NamedTuple , Union
67
78from azure .cli .core .extension import ext_compat_with_cli , WHEEL_INFO_RE
89from azure .cli .core .extension ._index import get_index_extensions
@@ -17,6 +18,13 @@ class NoExtensionCandidatesError(Exception):
1718 pass
1819
1920
21+ class _ExtensionFilter (NamedTuple ):
22+ # A function that filters a list of extensions
23+ filter : Callable [[List [dict ]], List [dict ]]
24+ # Message of exception raised if a filter leaves no candidates
25+ on_empty_results_message : Union [str , Callable [[List [dict ]], str ]]
26+
27+
2028def _is_not_platform_specific (item ):
2129 parsed_filename = WHEEL_INFO_RE (item ['filename' ])
2230 p = parsed_filename .groupdict ()
@@ -55,38 +63,94 @@ def filter_func(item):
5563 return filter_func
5664
5765
58- def resolve_from_index (extension_name , cur_version = None , index_url = None , target_version = None , cli_ctx = None ):
59- """
60- Gets the download Url and digest for the matching extension
66+ def _get_latest_version (candidates : List [dict ]) -> List [dict ]:
67+ return [max (candidates , key = lambda c : parse (c ['metadata' ]['version' ]))]
68+
69+
70+ def _get_version_compatibility_feedback (candidates : List [dict ]) -> str :
71+ from .operations import check_version_compatibility
72+
73+ try :
74+ check_version_compatibility (_get_latest_version (candidates )[0 ].get ("metadata" ))
75+ return ""
76+ except CLIError as e :
77+ return e .args [0 ]
78+
6179
62- :param cur_version: threshold verssion to filter out extensions.
80+ def resolve_from_index (extension_name , cur_version = None , index_url = None , target_version = None , cli_ctx = None ):
81+ """Gets the download Url and digest for the matching extension
82+
83+ Args:
84+ extension_name (str): Name of
85+ cur_version (str, optional): Threshold version to filter out extensions. Defaults to None.
86+ index_url (str, optional): Defaults to None.
87+ target_version (str, optional): Version of extension to install. Defaults to latest version.
88+ cli_ctx (, optional): CLI Context. Defaults to None.
89+
90+ Raises:
91+ NoExtensionCandidatesError when an extension:
92+ * Doesn't exist
93+ * Has no versions compatible with the current platform
94+ * Has no versions more recent than currently installed version
95+ * Has no versions that are compatible with the version of azure cli
96+
97+ Returns:
98+ tuple: (Download Url, SHA digest)
6399 """
64100 candidates = get_index_extensions (index_url = index_url , cli_ctx = cli_ctx ).get (extension_name , [])
65101
66102 if not candidates :
67- raise NoExtensionCandidatesError ("No extension found with name '{}'" .format (extension_name ))
68-
69- filters = [_is_not_platform_specific , _is_compatible_with_cli_version ]
70- if not target_version :
71- filters .append (_is_greater_than_cur_version (cur_version ))
103+ raise NoExtensionCandidatesError (f"No extension found with name '{ extension_name } '" )
72104
73- for f in filters :
74- logger .debug ("Candidates %s" , [c ['filename' ] for c in candidates ])
75- candidates = list (filter (f , candidates ))
76- if not candidates :
77- raise NoExtensionCandidatesError ("No suitable extensions found." )
105+ # Helper to curry predicate functions
106+ def list_filter (f ):
107+ return lambda cs : list (filter (f , cs ))
78108
79- candidates_sorted = sorted (candidates , key = lambda c : parse (c ['metadata' ]['version' ]), reverse = True )
80- logger .debug ("Candidates %s" , [c ['filename' ] for c in candidates_sorted ])
109+ candidate_filters = [
110+ _ExtensionFilter (
111+ filter = list_filter (_is_not_platform_specific ),
112+ on_empty_results_message = f"No suitable extensions found for '{ extension_name } '."
113+ )
114+ ]
81115
82116 if target_version :
83- try :
84- chosen = [c for c in candidates_sorted if c ['metadata' ]['version' ] == target_version ][0 ]
85- except IndexError :
86- raise NoExtensionCandidatesError ('Extension with version {} not found' .format (target_version ))
117+ candidate_filters += [
118+ _ExtensionFilter (
119+ filter = list_filter (lambda c : c ['metadata' ]['version' ] == target_version ),
120+ on_empty_results_message = f"Version '{ target_version } ' not found for extension '{ extension_name } '"
121+ )
122+ ]
87123 else :
88- logger .debug ("Choosing the latest of the remaining candidates." )
89- chosen = candidates_sorted [0 ]
124+ candidate_filters += [
125+ _ExtensionFilter (
126+ filter = list_filter (_is_greater_than_cur_version (cur_version )),
127+ on_empty_results_message = f"Latest version of '{ extension_name } ' is already installed."
128+ )
129+ ]
130+
131+ candidate_filters += [
132+ _ExtensionFilter (
133+ filter = list_filter (_is_compatible_with_cli_version ),
134+ on_empty_results_message = _get_version_compatibility_feedback
135+ ),
136+ _ExtensionFilter (
137+ filter = _get_latest_version ,
138+ on_empty_results_message = f"No suitable extensions found for '{ extension_name } '."
139+ )
140+ ]
141+
142+ for candidate_filter , on_empty_results_message in candidate_filters :
143+ logger .debug ("Candidates %s" , [c ['filename' ] for c in candidates ])
144+ filtered_candidates = candidate_filter (candidates )
145+
146+ if not filtered_candidates and (on_empty_results_message is not None ):
147+ if not isinstance (on_empty_results_message , str ):
148+ on_empty_results_message = on_empty_results_message (candidates )
149+ raise NoExtensionCandidatesError (on_empty_results_message )
150+
151+ candidates = filtered_candidates
152+
153+ chosen = candidates [0 ]
90154
91155 logger .debug ("Chosen %s" , chosen )
92156 download_url , digest = chosen .get ('downloadUrl' ), chosen .get ('sha256Digest' )
0 commit comments