"""Utilities related to dates, times, intervals, and timezones."""from__future__importannotationsimportnumbersimportosimportrandomimportsysimporttimeas_timefromcalendarimportmonthrangefromdatetimeimportdate,datetime,timedeltafromdatetimeimporttimezoneasdatetime_timezonefromdatetimeimporttzinfofromtypesimportModuleTypefromtypingimportAny,Callablefromdateutilimporttzasdateutil_tzfromdateutil.parserimportisoparsefromkombu.utils.functionalimportreprcallfromkombu.utils.objectsimportcached_propertyfrom.functionalimportdictfilterfrom.textimportpluralizeifsys.version_info>=(3,9):fromzoneinfoimportZoneInfoelse:frombackports.zoneinfoimportZoneInfo__all__=('LocalTimezone','timezone','maybe_timedelta','delta_resolution','remaining','rate','weekday','humanize_seconds','maybe_iso8601','is_naive','make_aware','localize','to_utc','maybe_make_aware','ffwd','utcoffset','adjust_timestamp','get_exponential_backoff_interval',)C_REMDEBUG=os.environ.get('C_REMDEBUG',False)DAYNAMES='sun','mon','tue','wed','thu','fri','sat'WEEKDAYS=dict(zip(DAYNAMES,range(7)))MONTHNAMES='jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'YEARMONTHS=dict(zip(MONTHNAMES,range(1,13)))RATE_MODIFIER_MAP={'s':lambdan:n,'m':lambdan:n/60.0,'h':lambdan:n/60.0/60.0,}TIME_UNITS=(('day',60*60*24.0,lambdan:format(n,'.2f')),('hour',60*60.0,lambdan:format(n,'.2f')),('minute',60.0,lambdan:format(n,'.2f')),('second',1.0,lambdan:format(n,'.2f')),)ZERO=timedelta(0)_local_timezone=None
[文档]classLocalTimezone(tzinfo):"""Local time implementation. Provided in _Zone to the app when `enable_utc` is disabled. Otherwise, _Zone provides a UTC ZoneInfo instance as the timezone implementation for the application. Note: Used only when the :setting:`enable_utc` setting is disabled. """_offset_cache:dict[int,tzinfo]={}def__init__(self)->None:# This code is moved in __init__ to execute it as late as possible# See get_default_timezone().self.STDOFFSET=timedelta(seconds=-_time.timezone)if_time.daylight:self.DSTOFFSET=timedelta(seconds=-_time.altzone)else:self.DSTOFFSET=self.STDOFFSETself.DSTDIFF=self.DSTOFFSET-self.STDOFFSETsuper().__init__()def__repr__(self)->str:returnf'<LocalTimezone: UTC{int(self.DSTOFFSET.total_seconds()/3600):+03d}>'
[文档]deffromutc(self,dt:datetime)->datetime:# The base tzinfo class no longer implements a DST# offset aware .fromutc() in Python 3 (Issue #2306).offset=int(self.utcoffset(dt).seconds/60.0)try:tz=self._offset_cache[offset]exceptKeyError:tz=self._offset_cache[offset]=datetime_timezone(timedelta(minutes=offset))returntz.fromutc(dt.replace(tzinfo=tz))
class_Zone:"""Timezone class that provides the timezone for the application. If `enable_utc` is disabled, LocalTimezone is provided as the timezone provider through local(). Otherwise, this class provides a UTC ZoneInfo instance as the timezone provider for the application. Additionally this class provides a few utility methods for converting datetimes. """deftz_or_local(self,tzinfo:tzinfo|None=None)->tzinfo:"""Return either our local timezone or the provided timezone."""# pylint: disable=redefined-outer-nameiftzinfoisNone:returnself.localreturnself.get_timezone(tzinfo)defto_local(self,dt:datetime,local=None,orig=None):"""Converts a datetime to the local timezone."""ifis_naive(dt):dt=make_aware(dt,origorself.utc)returnlocalize(dt,self.tz_or_local(local))defto_system(self,dt:datetime)->datetime:"""Converts a datetime to the system timezone."""# tz=None is a special case since Python 3.3, and will# convert to the current local timezone (Issue #2306).returndt.astimezone(tz=None)defto_local_fallback(self,dt:datetime)->datetime:"""Converts a datetime to the local timezone, or the system timezone."""ifis_naive(dt):returnmake_aware(dt,self.local)returnlocalize(dt,self.local)defget_timezone(self,zone:str|tzinfo)->tzinfo:"""Returns ZoneInfo timezone if the provided zone is a string, otherwise return the zone."""ifisinstance(zone,str):returnZoneInfo(zone)returnzone@cached_propertydeflocal(self)->LocalTimezone:"""Return LocalTimezone instance for the application."""returnLocalTimezone()@cached_propertydefutc(self)->tzinfo:"""Return UTC timezone created with ZoneInfo."""returnself.get_timezone('UTC')timezone=_Zone()
[文档]defmaybe_timedelta(delta:int)->timedelta:"""Convert integer to timedelta, if argument is an integer."""ifisinstance(delta,numbers.Real):returntimedelta(seconds=delta)returndelta
[文档]defdelta_resolution(dt:datetime,delta:timedelta)->datetime:"""Round a :class:`~datetime.datetime` to the resolution of timedelta. If the :class:`~datetime.timedelta` is in days, the :class:`~datetime.datetime` will be rounded to the nearest days, if the :class:`~datetime.timedelta` is in hours the :class:`~datetime.datetime` will be rounded to the nearest hour, and so on until seconds, which will just return the original :class:`~datetime.datetime`. """delta=max(delta.total_seconds(),0)resolutions=((3,lambdax:x/86400),(4,lambdax:x/3600),(5,lambdax:x/60))args=dt.year,dt.month,dt.day,dt.hour,dt.minute,dt.secondforres,predicateinresolutions:ifpredicate(delta)>=1.0:returndatetime(*args[:res],tzinfo=dt.tzinfo)returndt
[文档]defremaining(start:datetime,ends_in:timedelta,now:Callable|None=None,relative:bool=False)->timedelta:"""Calculate the real remaining time for a start date and a timedelta. For example, "how many seconds left for 30 seconds after start?" Arguments: start (~datetime.datetime): Starting date. ends_in (~datetime.timedelta): The end delta. relative (bool): If enabled the end time will be calculated using :func:`delta_resolution` (i.e., rounded to the resolution of `ends_in`). now (Callable): Function returning the current time and date. Defaults to :func:`datetime.now(timezone.utc)`. Returns: ~datetime.timedelta: Remaining time. """now=nowordatetime.now(datetime_timezone.utc)end_date=start+ends_inifrelative:end_date=delta_resolution(end_date,ends_in).replace(microsecond=0)# Using UTC to calculate real time difference.# Python by default uses wall time in arithmetic between datetimes with# equal non-UTC timezones.now_utc=now.astimezone(timezone.utc)end_date_utc=end_date.astimezone(timezone.utc)ret=end_date_utc-now_utcifC_REMDEBUG:# pragma: no coverprint('rem: NOW:{!r} NOW_UTC:{!r} START:{!r} ENDS_IN:{!r} ''END_DATE:{} END_DATE_UTC:{!r} REM:{}'.format(now,now_utc,start,ends_in,end_date,end_date_utc,ret))returnret
[文档]defrate(r:str)->float:"""Convert rate string (`"100/m"`, `"2/h"` or `"0.5/s"`) to seconds."""ifr:ifisinstance(r,str):ops,_,modifier=r.partition('/')returnRATE_MODIFIER_MAP[modifieror's'](float(ops))or0returnror0return0
[文档]defweekday(name:str)->int:"""Return the position of a weekday: 0 - 7, where 0 is Sunday. Example: >>> weekday('sunday'), weekday('sun'), weekday('mon') (0, 0, 1) """abbreviation=name[0:3].lower()try:returnWEEKDAYS[abbreviation]exceptKeyError:# Show original day name in exception, instead of abbr.raiseKeyError(name)
defyearmonth(name:str)->int:"""Return the position of a month: 1 - 12, where 1 is January. Example: >>> yearmonth('january'), yearmonth('jan'), yearmonth('may') (1, 1, 5) """abbreviation=name[0:3].lower()try:returnYEARMONTHS[abbreviation]exceptKeyError:# Show original day name in exception, instead of abbr.raiseKeyError(name)
[文档]defhumanize_seconds(secs:int,prefix:str='',sep:str='',now:str='now',microseconds:bool=False)->str:"""Show seconds in human form. For example, 60 becomes "1 minute", and 7200 becomes "2 hours". Arguments: prefix (str): can be used to add a preposition to the output (e.g., 'in' will give 'in 1 second', but add nothing to 'now'). now (str): Literal 'now'. microseconds (bool): Include microseconds. """secs=float(format(float(secs),'.2f'))forunit,divider,formatterinTIME_UNITS:ifsecs>=divider:w=secs/float(divider)return'{}{}{}{}'.format(prefix,sep,formatter(w),pluralize(w,unit))ifmicrosecondsandsecs>0.0:return'{prefix}{sep}{0:.2f} seconds'.format(secs,sep=sep,prefix=prefix)returnnow
[文档]defmaybe_iso8601(dt:datetime|str|None)->None|datetime:"""Either ``datetime | str -> datetime`` or ``None -> None``."""ifnotdt:returnifisinstance(dt,datetime):returndtreturnisoparse(dt)
[文档]defis_naive(dt:datetime)->bool:"""Return True if :class:`~datetime.datetime` is naive, meaning it doesn't have timezone info set."""returndt.tzinfoisNoneordt.tzinfo.utcoffset(dt)isNone
def_can_detect_ambiguous(tz:tzinfo)->bool:"""Helper function to determine if a timezone can detect ambiguous times using dateutil."""returnisinstance(tz,ZoneInfo)orhasattr(tz,"is_ambiguous")def_is_ambiguous(dt:datetime,tz:tzinfo)->bool:"""Helper function to determine if a timezone is ambiguous using python's dateutil module. Returns False if the timezone cannot detect ambiguity, or if there is no ambiguity, otherwise True. In order to detect ambiguous datetimes, the timezone must be built using ZoneInfo, or have an is_ambiguous method. Previously, pytz timezones would throw an AmbiguousTimeError if the localized dt was ambiguous, but now we need to specifically check for ambiguity with dateutil, as pytz is deprecated. """return_can_detect_ambiguous(tz)anddateutil_tz.datetime_ambiguous(dt)
[文档]defmake_aware(dt:datetime,tz:tzinfo)->datetime:"""Set timezone for a :class:`~datetime.datetime` object."""dt=dt.replace(tzinfo=tz)if_is_ambiguous(dt,tz):dt=min(dt.replace(fold=0),dt.replace(fold=1))returndt
[文档]deflocalize(dt:datetime,tz:tzinfo)->datetime:"""Convert aware :class:`~datetime.datetime` to another timezone. Using a ZoneInfo timezone will give the most flexibility in terms of ambiguous DST handling. """ifis_naive(dt):# Ensure timezone aware datetimedt=make_aware(dt,tz)ifdt.tzinfo==ZoneInfo("UTC"):dt=dt.astimezone(tz)# Always safe to call astimezone on utc zonesreturndt
[文档]defto_utc(dt:datetime)->datetime:"""Convert naive :class:`~datetime.datetime` to UTC."""returnmake_aware(dt,timezone.utc)
[文档]defmaybe_make_aware(dt:datetime,tz:tzinfo|None=None,naive_as_utc:bool=True)->datetime:"""Convert dt to aware datetime, do nothing if dt is already aware."""ifis_naive(dt):ifnaive_as_utc:dt=to_utc(dt)returnlocalize(dt,timezone.utciftzisNoneelsetimezone.tz_or_local(tz),)returndt
[文档]classffwd:"""Version of ``dateutil.relativedelta`` that only supports addition."""def__init__(self,year=None,month=None,weeks=0,weekday=None,day=None,hour=None,minute=None,second=None,microsecond=None,**kwargs:Any):# pylint: disable=redefined-outer-name# weekday is also a function in outer scope.self.year=yearself.month=monthself.weeks=weeksself.weekday=weekdayself.day=dayself.hour=hourself.minute=minuteself.second=secondself.microsecond=microsecondself.days=weeks*7self._has_time=self.hourisnotNoneorself.minuteisnotNonedef__repr__(self)->str:returnreprcall('ffwd',(),self._fields(weeks=self.weeks,weekday=self.weekday))def__radd__(self,other:Any)->timedelta:ifnotisinstance(other,date):returnNotImplementedyear=self.yearorother.yearmonth=self.monthorother.monthday=min(monthrange(year,month)[1],self.dayorother.day)ret=other.replace(**dict(dictfilter(self._fields()),year=year,month=month,day=day))ifself.weekdayisnotNone:ret+=timedelta(days=(7-ret.weekday()+self.weekday)%7)returnret+timedelta(days=self.days)def_fields(self,**extra:Any)->dict[str,Any]:returndictfilter({'year':self.year,'month':self.month,'day':self.day,'hour':self.hour,'minute':self.minute,'second':self.second,'microsecond':self.microsecond,},**extra)
[文档]defutcoffset(time:ModuleType=_time,localtime:Callable[...,_time.struct_time]=_time.localtime)->float:"""Return the current offset to UTC in hours."""iflocaltime().tm_isdst:returntime.altzone//3600returntime.timezone//3600
[文档]defadjust_timestamp(ts:float,offset:int,here:Callable[...,float]=utcoffset)->float:"""Adjust timestamp based on provided utcoffset."""returnts-(offset-here())*3600
[文档]defget_exponential_backoff_interval(factor:int,retries:int,maximum:int,full_jitter:bool=False)->int:"""Calculate the exponential backoff wait time."""# Will be zero if factor equals 0countdown=min(maximum,factor*(2**retries))# Full jitter according to# https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/iffull_jitter:countdown=random.randrange(countdown+1)# Adjust according to maximum wait time and account for negative values.returnmax(0,countdown)