@@ -105,7 +105,10 @@ def resolve(
105105 ignore_platform = ignore_platform ,
106106 )
107107 provider .cooldown = resolve_package_cooldown (ctx , req )
108- results = find_all_matching_from_provider (provider , req )
108+ max_age_cutoff = _compute_max_age_cutoff (ctx )
109+ results = find_all_matching_from_provider (
110+ provider , req , max_age_cutoff = max_age_cutoff
111+ )
109112 return results [0 ]
110113
111114
@@ -167,6 +170,24 @@ def resolve_package_cooldown(
167170 )
168171
169172
173+ def _compute_max_age_cutoff (
174+ ctx : context .WorkContext ,
175+ ) -> datetime .datetime | None :
176+ """Compute the cutoff time for max release age filtering.
177+
178+ Returns the oldest acceptable upload time, or None if disabled.
179+ Uses the cooldown's bootstrap_time for consistency across a single run.
180+ """
181+ if ctx .max_release_age is None :
182+ return None
183+ bootstrap_time = (
184+ ctx .cooldown .bootstrap_time
185+ if ctx .cooldown is not None
186+ else datetime .datetime .now (datetime .UTC )
187+ )
188+ return bootstrap_time - ctx .max_release_age
189+
190+
170191def extract_filename_from_url (url : str ) -> str :
171192 """Extract filename from URL and decode it."""
172193 path = urlparse (url ).path
@@ -203,13 +224,22 @@ def ending(self, state: typing.Any) -> None:
203224
204225
205226def find_all_matching_from_provider (
206- provider : BaseProvider , req : Requirement
227+ provider : BaseProvider ,
228+ req : Requirement ,
229+ max_age_cutoff : datetime .datetime | None = None ,
207230) -> list [tuple [str , Version ]]:
208231 """Find all matching candidates from provider without full dependency resolution.
209232
210233 This function collects ALL candidates that match the requirement, rather than
211234 performing full dependency resolution to find a single best candidate.
212235
236+ Args:
237+ provider: The provider to query for candidates.
238+ req: The requirement to match.
239+ max_age_cutoff: If set, reject candidates published before this time.
240+ If all candidates are older than the cutoff, all are kept and
241+ a warning is emitted to avoid empty resolution.
242+
213243 Returns list of (url, version) tuples sorted by version (highest first).
214244
215245 IMPORTANT: This bypasses resolvelib's full resolver to collect all matching
@@ -242,10 +272,47 @@ def find_all_matching_from_provider(
242272 f"Unable to resolve requirement specifier { req } with constraint { constraint } using { provider_desc } : { original_msg } "
243273 ) from err
244274
275+ # Materialize candidates so we can iterate more than once if filtering
276+ candidates_list = list (candidates )
277+
278+ if max_age_cutoff is not None :
279+ logger .info (
280+ "%s: found %d candidate(s) matching %s" ,
281+ req .name ,
282+ len (candidates_list ),
283+ req ,
284+ )
285+ max_age_days = (datetime .datetime .now (datetime .UTC ) - max_age_cutoff ).days
286+ filtered = [
287+ c
288+ for c in candidates_list
289+ if c .upload_time is None or c .upload_time >= max_age_cutoff
290+ ]
291+ dropped = len (candidates_list ) - len (filtered )
292+ if dropped :
293+ logger .info (
294+ "%s: have %d candidate(s) of %s published within %d days" ,
295+ req .name ,
296+ len (filtered ),
297+ req ,
298+ max_age_days ,
299+ )
300+ if filtered :
301+ candidates_list = filtered
302+ else :
303+ logger .warning (
304+ "%s: all %d candidate(s) of %s are older than %d days, "
305+ "keeping all to avoid empty resolution" ,
306+ req .name ,
307+ len (candidates_list ),
308+ req ,
309+ max_age_days ,
310+ )
311+
245312 # Convert candidates to list of (url, version) tuples
246313 # Candidates are sorted by version (highest first) by BaseProvider.find_matches()
247314 # which calls sorted(candidates, key=attrgetter("version", "build_tag"), reverse=True)
248- return [(candidate .url , candidate .version ) for candidate in candidates ]
315+ return [(c .url , c .version ) for c in candidates_list ]
249316
250317
251318def get_project_from_pypi (
0 commit comments