@@ -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,15 +224,28 @@ 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+ Raises ``ResolverException`` if all candidates are older than
241+ the cutoff.
242+
213243 Returns list of (url, version) tuples sorted by version (highest first).
214244
245+ Raises:
246+ ResolverException: If ``max_age_cutoff`` is set and every candidate
247+ is older than the cutoff.
248+
215249 IMPORTANT: This bypasses resolvelib's full resolver to collect all matching
216250 candidates. This is safe ONLY because BaseProvider.get_dependencies() returns
217251 an empty list (no transitive dependencies to resolve). The empty incompatibilities
@@ -242,10 +276,42 @@ def find_all_matching_from_provider(
242276 f"Unable to resolve requirement specifier { req } with constraint { constraint } using { provider_desc } : { original_msg } "
243277 ) from err
244278
279+ # Materialize candidates so we can iterate more than once if filtering
280+ candidates_list = list (candidates )
281+
282+ if max_age_cutoff is not None :
283+ logger .info (
284+ "%s: found %d candidate(s) matching %s" ,
285+ req .name ,
286+ len (candidates_list ),
287+ req ,
288+ )
289+ max_age_days = (datetime .datetime .now (datetime .UTC ) - max_age_cutoff ).days
290+ filtered = [
291+ c
292+ for c in candidates_list
293+ if c .upload_time is None or c .upload_time >= max_age_cutoff
294+ ]
295+ dropped = len (candidates_list ) - len (filtered )
296+ if dropped :
297+ logger .info (
298+ "%s: have %d candidate(s) of %s published within %d days" ,
299+ req .name ,
300+ len (filtered ),
301+ req ,
302+ max_age_days ,
303+ )
304+ if not filtered :
305+ raise resolvelib .resolvers .ResolverException (
306+ f"all { len (candidates_list )} candidate(s) for { req } were "
307+ f"published before the max-release-age cutoff"
308+ )
309+ candidates_list = filtered
310+
245311 # Convert candidates to list of (url, version) tuples
246312 # Candidates are sorted by version (highest first) by BaseProvider.find_matches()
247313 # which calls sorted(candidates, key=attrgetter("version", "build_tag"), reverse=True)
248- return [(candidate .url , candidate .version ) for candidate in candidates ]
314+ return [(c .url , c .version ) for c in candidates_list ]
249315
250316
251317def get_project_from_pypi (
0 commit comments