11import asyncio
2+ import calendar
23import contextlib
34import datetime
45import os # noqa
56import pathlib
67import pickle
78import re
9+ import time
810from collections import defaultdict
911from http .cookies import BaseCookie , Morsel , SimpleCookie
12+ from math import ceil
1013from typing import ( # noqa
1114 DefaultDict ,
1215 Dict ,
2427from yarl import URL
2528
2629from .abc import AbstractCookieJar , ClearCookiePredicate
27- from .helpers import is_ip_address , next_whole_second
30+ from .helpers import is_ip_address
2831from .typedefs import LooseCookies , PathLike , StrOrURL
2932
3033__all__ = ("CookieJar" , "DummyCookieJar" )
@@ -52,9 +55,22 @@ class CookieJar(AbstractCookieJar):
5255
5356 DATE_YEAR_RE = re .compile (r"(\d{2,4})" )
5457
55- MAX_TIME = datetime .datetime .max .replace (tzinfo = datetime .timezone .utc )
56-
57- MAX_32BIT_TIME = datetime .datetime .fromtimestamp (2 ** 31 - 1 , datetime .timezone .utc )
58+ # calendar.timegm() fails for timestamps after datetime.datetime.max
59+ # Minus one as a loss of precision occurs when timestamp() is called.
60+ MAX_TIME = (
61+ int (datetime .datetime .max .replace (tzinfo = datetime .timezone .utc ).timestamp ()) - 1
62+ )
63+ try :
64+ calendar .timegm (time .gmtime (MAX_TIME ))
65+ except OSError :
66+ # Hit the maximum representable time on Windows
67+ # https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/localtime-localtime32-localtime64
68+ MAX_TIME = calendar .timegm ((3000 , 12 , 31 , 23 , 59 , 59 , - 1 , - 1 , - 1 ))
69+ except OverflowError :
70+ # #4515: datetime.max may not be representable on 32-bit platforms
71+ MAX_TIME = 2 ** 31 - 1
72+ # Avoid minuses in the future, 3x faster
73+ SUB_MAX_TIME = MAX_TIME - 1
5874
5975 def __init__ (
6076 self ,
@@ -83,14 +99,8 @@ def __init__(
8399 for url in treat_as_secure_origin
84100 ]
85101 self ._treat_as_secure_origin = treat_as_secure_origin
86- self ._next_expiration = next_whole_second ()
87- self ._expirations : Dict [Tuple [str , str , str ], datetime .datetime ] = {}
88- # #4515: datetime.max may not be representable on 32-bit platforms
89- self ._max_time = self .MAX_TIME
90- try :
91- self ._max_time .timestamp ()
92- except OverflowError :
93- self ._max_time = self .MAX_32BIT_TIME
102+ self ._next_expiration : float = ceil (time .time ())
103+ self ._expirations : Dict [Tuple [str , str , str ], float ] = {}
94104
95105 def save (self , file_path : PathLike ) -> None :
96106 file_path = pathlib .Path (file_path )
@@ -104,14 +114,14 @@ def load(self, file_path: PathLike) -> None:
104114
105115 def clear (self , predicate : Optional [ClearCookiePredicate ] = None ) -> None :
106116 if predicate is None :
107- self ._next_expiration = next_whole_second ( )
117+ self ._next_expiration = ceil ( time . time () )
108118 self ._cookies .clear ()
109119 self ._host_only_cookies .clear ()
110120 self ._expirations .clear ()
111121 return
112122
113123 to_del = []
114- now = datetime . datetime . now ( datetime . timezone . utc )
124+ now = time . time ( )
115125 for (domain , path ), cookie in self ._cookies .items ():
116126 for name , morsel in cookie .items ():
117127 key = (domain , path , name )
@@ -127,13 +137,11 @@ def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
127137 del self ._expirations [(domain , path , name )]
128138 self ._cookies [(domain , path )].pop (name , None )
129139
130- next_expiration = min (self ._expirations .values (), default = self ._max_time )
131- try :
132- self ._next_expiration = next_expiration .replace (
133- microsecond = 0
134- ) + datetime .timedelta (seconds = 1 )
135- except OverflowError :
136- self ._next_expiration = self ._max_time
140+ self ._next_expiration = (
141+ min (* self ._expirations .values (), self .SUB_MAX_TIME ) + 1
142+ if self ._expirations
143+ else self .MAX_TIME
144+ )
137145
138146 def clear_domain (self , domain : str ) -> None :
139147 self .clear (lambda x : self ._is_domain_match (domain , x ["domain" ]))
@@ -149,9 +157,7 @@ def __len__(self) -> int:
149157 def _do_expiration (self ) -> None :
150158 self .clear (lambda x : False )
151159
152- def _expire_cookie (
153- self , when : datetime .datetime , domain : str , path : str , name : str
154- ) -> None :
160+ def _expire_cookie (self , when : float , domain : str , path : str , name : str ) -> None :
155161 self ._next_expiration = min (self ._next_expiration , when )
156162 self ._expirations [(domain , path , name )] = when
157163
@@ -209,12 +215,7 @@ def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> No
209215 if max_age :
210216 try :
211217 delta_seconds = int (max_age )
212- try :
213- max_age_expiration = datetime .datetime .now (
214- datetime .timezone .utc
215- ) + datetime .timedelta (seconds = delta_seconds )
216- except OverflowError :
217- max_age_expiration = self ._max_time
218+ max_age_expiration = min (time .time () + delta_seconds , self .MAX_TIME )
218219 self ._expire_cookie (max_age_expiration , domain , path , name )
219220 except ValueError :
220221 cookie ["max-age" ] = ""
@@ -323,7 +324,7 @@ def _is_path_match(req_path: str, cookie_path: str) -> bool:
323324 return non_matching .startswith ("/" )
324325
325326 @classmethod
326- def _parse_date (cls , date_str : str ) -> Optional [datetime . datetime ]:
327+ def _parse_date (cls , date_str : str ) -> Optional [int ]:
327328 """Implements date string parsing adhering to RFC 6265."""
328329 if not date_str :
329330 return None
@@ -384,9 +385,7 @@ def _parse_date(cls, date_str: str) -> Optional[datetime.datetime]:
384385 if year < 1601 or hour > 23 or minute > 59 or second > 59 :
385386 return None
386387
387- return datetime .datetime (
388- year , month , day , hour , minute , second , tzinfo = datetime .timezone .utc
389- )
388+ return calendar .timegm ((year , month , day , hour , minute , second , - 1 , - 1 , - 1 ))
390389
391390
392391class DummyCookieJar (AbstractCookieJar ):
0 commit comments