22import logging
33import sys
44import time
5+ from pathlib import Path
56from concurrent .futures import ThreadPoolExecutor , as_completed
7+ from collections import defaultdict
68
79import yaml
810from bioblend import toolshed
1618def retry_with_backoff (func , * args , ** kwargs ):
1719 MAX_RETRIES = 5
1820 backoff = 2
19- last_exception = None
2021
2122 for attempt in range (MAX_RETRIES ):
2223 try :
2324 return func (* args , ** kwargs )
2425 except Exception as e :
25- last_exception = e
2626 error_msg = str (e )
2727 if any (
2828 code in error_msg
@@ -34,14 +34,9 @@ def retry_with_backoff(func, *args, **kwargs):
3434 )
3535 time .sleep (backoff )
3636 backoff = min (backoff * 2 , 60 )
37- else :
38- logger .error (f"All { MAX_RETRIES } attempts failed" )
39- else :
40- raise
41-
42- if last_exception :
43- raise last_exception
44- raise Exception ("Retry failed with no exception captured" )
37+ continue
38+ raise e
39+ raise Exception ("Retry failed after max attempts" )
4540
4641
4742def get_tool_versions (ts , name , owner , revision ):
@@ -80,12 +75,28 @@ def fetch_versions_parallel(ts, name, owner, revisions, max_workers=10):
8075
8176def fix_uninstallable (lockfile_name , toolshed_url ):
8277 ts = toolshed .ToolShedInstance (url = toolshed_url )
83- with open (lockfile_name ) as f :
78+ lockfile_path = Path (lockfile_name )
79+ with open (lockfile_path ) as f :
8480 lockfile = yaml .safe_load (f ) or {}
8581 locked_tools = lockfile .get ("tools" , [])
8682 total = len (locked_tools )
8783
88- logger .info (f"Processing { total } tools from { lockfile_name } ..." )
84+ uninstallable_file = lockfile_path .with_name (
85+ lockfile_path .name .replace (".yaml.lock" , ".uninstallable_revisions.yaml" )
86+ )
87+
88+ removed_map = defaultdict (set )
89+ try :
90+ with open (uninstallable_file ) as f :
91+ uninstallable_data = yaml .safe_load (f ) or {}
92+ for t in uninstallable_data .get ("tools" , []):
93+ removed_map [(t ["name" ], t ["owner" ])] = set (
94+ t .get ("removed_revisions" , [])
95+ )
96+ except FileNotFoundError :
97+ pass
98+
99+ logger .info (f"Processing { total } tools from { lockfile_path .name } ..." )
89100 changed , skipped = 0 , 0
90101
91102 for i , tool in enumerate (locked_tools ):
@@ -95,38 +106,40 @@ def fix_uninstallable(lockfile_name, toolshed_url):
95106 )
96107
97108 name , owner = tool .get ("name" ), tool .get ("owner" )
98- revisions = tool .get ("revisions" , [])
109+ current_revisions = set ( tool .get ("revisions" , []) )
99110 try :
100- installable = retry_with_backoff (
111+ installable_list = retry_with_backoff (
101112 ts .repositories .get_ordered_installable_revisions , name , owner
102113 )
103114 except Exception as e :
104115 logger .warning (f"{ name } ,{ owner } : could not get installable revisions ({ e } )" )
105116 continue
106117
107- uninstallable = set ( revisions ) - set (installable )
118+ uninstallable = current_revisions - set (installable_list )
108119 if not uninstallable :
109120 skipped += 1
110121 continue
111122
112- all_revs = list (uninstallable ) + list ( installable )
123+ all_revs = list (uninstallable ) + installable_list
113124 version_cache = fetch_versions_parallel (ts , name , owner , all_revs )
114125
115- to_remove = []
126+ installable_signatures = {}
127+ for rev in installable_list :
128+ sig = frozenset (version_cache .get (rev , []))
129+ if sig :
130+ installable_signatures [sig ] = rev
131+ to_remove = set ()
132+
116133 for cur in uninstallable :
117- cur_versions = version_cache .get (cur , set ( ))
118- if not cur_versions :
119- if installable :
120- nxt = installable [ 0 ]
134+ cur_sig = frozenset ( version_cache .get (cur , [] ))
135+ if not cur_sig :
136+ if installable_list :
137+ nxt = installable_list [ - 1 ]
121138 logger .info (f"{ name } ,{ owner } : unverifiable { cur } , keeping { nxt } " )
122- to_remove .append (cur )
123- continue
139+ to_remove .add (cur )
140+ continue
124141
125- nxt = None
126- for cand in reversed (installable ):
127- if version_cache .get (cand ) == cur_versions :
128- nxt = cand
129- break
142+ nxt = installable_signatures .get (cur_sig )
130143
131144 if not nxt :
132145 logger .warning (
@@ -135,30 +148,38 @@ def fix_uninstallable(lockfile_name, toolshed_url):
135148 sys .exit (1 )
136149
137150 logger .info (f"{ name } ,{ owner } : removing { cur } in favor of { nxt } " )
138- if nxt not in revisions :
139- revisions .append (nxt )
140- to_remove .append (cur )
151+ if nxt not in current_revisions :
152+ tool [ " revisions" ] .append (nxt )
153+ to_remove .add (cur )
141154
142155 if to_remove :
143156 changed += 1
144- tool ["revisions" ] = sorted (set (revisions ) - set (to_remove ))
157+ tool ["revisions" ] = sorted (set (tool ["revisions" ]) - to_remove )
158+ removed_map [(name , owner )].update (to_remove )
145159
146160 logger .info (
147161 f"Completed: { total } tools processed, { skipped } skipped, { changed } changed"
148162 )
149163
150- with open (lockfile_name , "w" ) as f :
164+ with open (lockfile_path , "w" ) as f :
151165 yaml .dump (lockfile , f , sort_keys = False , default_flow_style = False )
152166
167+ uninstallable_output = {
168+ "tools" : [
169+ {"name" : n , "owner" : o , "removed_revisions" : sorted (revs )}
170+ for (n , o ), revs in removed_map .items ()
171+ ]
172+ }
173+ with open (uninstallable_file , "w" ) as f :
174+ yaml .dump (uninstallable_output , f , sort_keys = False , default_flow_style = False )
175+
153176
154177if __name__ == "__main__" :
155178 parser = argparse .ArgumentParser ()
156- parser .add_argument (
157- "lockfile" , type = argparse .FileType ("r" ), help = "Tool.yaml.lock file"
158- )
179+ parser .add_argument ("lockfile" , help = "Tool.yaml.lock file path" )
159180 parser .add_argument (
160181 "--toolshed" , default = "https://toolshed.g2.bx.psu.edu" , help = "Toolshed base URL"
161182 )
162183 args = parser .parse_args ()
163184
164- fix_uninstallable (args .lockfile . name , args .toolshed )
185+ fix_uninstallable (args .lockfile , args .toolshed )
0 commit comments