diff --git a/docs/errors.rst b/docs/errors.rst index 784e30e3..1139cc4f 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -56,7 +56,7 @@ is no need to retry over time, as it will not become better:: @kopf.on.create('kopfexamples') def create_fn(spec, **_): valid_until = datetime.datetime.fromisoformat(spec['validUntil']) - if valid_until <= datetime.datetime.utcnow(): + if valid_until <= datetime.datetime.now(datetime.timezone.utc): raise kopf.PermanentError("The object is not valid anymore.") See also: :ref:`never-again-filters` to prevent handlers from being invoked diff --git a/docs/probing.rst b/docs/probing.rst index 601ad40a..c7a92819 100644 --- a/docs/probing.rst +++ b/docs/probing.rst @@ -76,7 +76,7 @@ probing handlers: @kopf.on.probe(id='now') def get_current_timestamp(**kwargs): - return datetime.datetime.utcnow().isoformat() + return datetime.datetime.now(datetime.timezone.utc).isoformat() @kopf.on.probe(id='random') def get_random_value(**kwargs): @@ -91,7 +91,7 @@ The handler results will be reported as the content of the liveness response: .. code-block:: console $ curl http://localhost:8080/healthz - {"now": "2019-11-07T18:03:52.513803", "random": 765846} + {"now": "2019-11-07T18:03:52.513803+00:00", "random": 765846} .. note:: The liveness status report is simplistic and minimalistic at the moment. diff --git a/examples/13-hooks/example.py b/examples/13-hooks/example.py index 3bd66466..2f44c194 100644 --- a/examples/13-hooks/example.py +++ b/examples/13-hooks/example.py @@ -55,7 +55,7 @@ async def login_fn(**kwargs): certificate_path=cert.filename() if cert else None, # can be a temporary file private_key_path=pkey.filename() if pkey else None, # can be a temporary file default_namespace=config.namespace, - expiration=datetime.datetime.utcnow() + datetime.timedelta(seconds=30), + expiration=datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=30), ) diff --git a/kopf/_cogs/clients/events.py b/kopf/_cogs/clients/events.py index 3bca4bb6..2a7793e6 100644 --- a/kopf/_cogs/clients/events.py +++ b/kopf/_cogs/clients/events.py @@ -49,7 +49,7 @@ async def post_event( suffix = message[-MAX_MESSAGE_LENGTH // 2 + (len(infix) - len(infix) // 2):] message = f'{prefix}{infix}{suffix}' - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) body = { 'metadata': { 'namespace': namespace, @@ -67,9 +67,9 @@ async def post_event( 'involvedObject': full_ref, - 'firstTimestamp': now.isoformat() + 'Z', # '2019-01-28T18:25:03.000000Z' -- seen in `kubectl describe ...` - 'lastTimestamp': now.isoformat() + 'Z', # '2019-01-28T18:25:03.000000Z' - seen in `kubectl get events` - 'eventTime': now.isoformat() + 'Z', # '2019-01-28T18:25:03.000000Z' + 'firstTimestamp': now.isoformat(), # '2019-01-28T18:25:03.000000+00:00' -- seen in `kubectl describe ...` + 'lastTimestamp': now.isoformat(), # '2019-01-28T18:25:03.000000+00:00' - seen in `kubectl get events` + 'eventTime': now.isoformat(), # '2019-01-28T18:25:03.000000+00:00' } try: diff --git a/kopf/_cogs/structs/credentials.py b/kopf/_cogs/structs/credentials.py index 9a40bb3f..040ee9a3 100644 --- a/kopf/_cogs/structs/credentials.py +++ b/kopf/_cogs/structs/credentials.py @@ -229,7 +229,9 @@ async def expire(self) -> None: Unlike invalidation, the expired credentials are not remembered and not blocked from reappearing. """ - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) + if self._next_expiration.tzinfo is None: + now = now.replace(tzinfo=None) # for comparability if now >= self._next_expiration: # quick & lockless for speed: it is done on every API call async with self._lock: for key, item in list(self._current.items()): @@ -315,7 +317,7 @@ async def populate( await self._ready.turn_to(True) def is_empty(self) -> bool: - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) return all( item.info.expiration is not None and now >= item.info.expiration # i.e. expired for key, item in self._current.items() diff --git a/kopf/_core/actions/application.py b/kopf/_core/actions/application.py index 805dda23..22f8e99b 100644 --- a/kopf/_core/actions/application.py +++ b/kopf/_core/actions/application.py @@ -89,7 +89,7 @@ async def apply( logger.debug(f"Sleeping was interrupted by new changes, {unslept_delay} seconds left.") else: # Any unique always-changing value will work; not necessary a timestamp. - value = datetime.datetime.utcnow().isoformat() + value = datetime.datetime.now(datetime.timezone.utc).isoformat() touch = patches.Patch() settings.persistence.progress_storage.touch(body=body, patch=touch, value=value) await patch_and_check( diff --git a/kopf/_core/actions/execution.py b/kopf/_core/actions/execution.py index 37573f69..89cb095c 100644 --- a/kopf/_core/actions/execution.py +++ b/kopf/_core/actions/execution.py @@ -113,7 +113,7 @@ def finished(self) -> bool: @property def sleeping(self) -> bool: ts = self.delayed - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) return not self.finished and ts is not None and ts > now @property @@ -122,7 +122,7 @@ def awakened(self) -> bool: @property def runtime(self) -> datetime.timedelta: - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) return now - (self.started if self.started else now) @@ -277,7 +277,7 @@ async def execute_handler_once( handler=handler, cause=cause, retry=state.retries, - started=state.started or datetime.datetime.utcnow(), # "or" is for type-checking. + started=state.started or datetime.datetime.now(datetime.timezone.utc), # "or" is for type-checking. runtime=state.runtime, settings=settings, lifecycle=lifecycle, # just a default for the sub-handlers, not used directly. diff --git a/kopf/_core/actions/progression.py b/kopf/_core/actions/progression.py index 65367ee5..780d8cf0 100644 --- a/kopf/_core/actions/progression.py +++ b/kopf/_core/actions/progression.py @@ -54,7 +54,7 @@ class HandlerState(execution.HandlerState): def from_scratch(cls, *, purpose: Optional[str] = None) -> "HandlerState": return cls( active=True, - started=datetime.datetime.utcnow(), + started=datetime.datetime.now(datetime.timezone.utc), purpose=purpose, ) @@ -62,7 +62,7 @@ def from_scratch(cls, *, purpose: Optional[str] = None) -> "HandlerState": def from_storage(cls, __d: progress.ProgressRecord) -> "HandlerState": return cls( active=False, - started=_datetime_fromisoformat(__d.get('started')) or datetime.datetime.utcnow(), + started=_datetime_fromisoformat(__d.get('started')) or datetime.datetime.now(datetime.timezone.utc), stopped=_datetime_fromisoformat(__d.get('stopped')), delayed=_datetime_fromisoformat(__d.get('delayed')), purpose=__d.get('purpose') if __d.get('purpose') else None, @@ -104,7 +104,7 @@ def with_outcome( self, outcome: execution.Outcome, ) -> "HandlerState": - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) cls = type(self) return cls( active=self.active, @@ -313,7 +313,7 @@ def delays(self) -> Collection[float]: processing routine, based on all delays of different origin: e.g. postponed daemons, stopping daemons, temporarily failed handlers. """ - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) return [ max(0, (handler_state.delayed - now).total_seconds()) if handler_state.delayed else 0 for handler_state in self._states.values() @@ -381,4 +381,7 @@ def _datetime_fromisoformat(val: Optional[str]) -> Optional[datetime.datetime]: if val is None: return None else: - return datetime.datetime.fromisoformat(val) + dt = datetime.datetime.fromisoformat(val) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=datetime.timezone.utc) + return dt diff --git a/kopf/_core/engines/peering.py b/kopf/_core/engines/peering.py index 6c511f8e..8eafccf0 100644 --- a/kopf/_core/engines/peering.py +++ b/kopf/_core/engines/peering.py @@ -68,10 +68,10 @@ def __init__( self.priority = priority self.lifetime = datetime.timedelta(seconds=int(lifetime)) self.lastseen = (iso8601.parse_date(lastseen) if lastseen is not None else - datetime.datetime.utcnow()) + datetime.datetime.now(datetime.timezone.utc)) self.lastseen = self.lastseen.replace(tzinfo=None) # only the naive utc -- for comparison self.deadline = self.lastseen + self.lifetime - self.is_dead = self.deadline <= datetime.datetime.utcnow() + self.is_dead = self.deadline <= datetime.datetime.now(datetime.timezone.utc) def __repr__(self) -> str: clsname = self.__class__.__name__ @@ -149,7 +149,7 @@ async def process_peering_event( # are expected to expire, and force the immediate re-evaluation by a certain change of self. # This incurs an extra PATCH request besides usual keepalives, but in the complete silence # from other peers that existed a moment earlier, this should not be a problem. - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) delays = [(peer.deadline - now).total_seconds() for peer in same_peers + prio_peers] unslept = await aiotime.sleep(delays, wakeup=stream_pressure) if unslept is None and delays: @@ -279,7 +279,7 @@ def detect_own_id(*, manual: bool) -> Identity: user = getpass.getuser() host = hostnames.get_descriptive_hostname() - now = datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S") + now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d%H%M%S") rnd = ''.join(random.choices('abcdefhijklmnopqrstuvwxyz0123456789', k=3)) return Identity(f'{user}@{host}' if manual else f'{user}@{host}/{now}/{rnd}') diff --git a/kopf/_core/engines/probing.py b/kopf/_core/engines/probing.py index 51675a46..0c1bbb99 100644 --- a/kopf/_core/engines/probing.py +++ b/kopf/_core/engines/probing.py @@ -48,10 +48,10 @@ async def get_health( # Recollect the data on-demand, and only if is is older that a reasonable caching period. # Protect against multiple parallel requests performing the same heavy activity. - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) if probing_timestamp is None or now - probing_timestamp >= probing_max_age: async with probing_lock: - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) if probing_timestamp is None or now - probing_timestamp >= probing_max_age: activity_results = await activities.run_activity( @@ -64,7 +64,7 @@ async def get_health( ) probing_container.clear() probing_container.update(activity_results) - probing_timestamp = datetime.datetime.utcnow() + probing_timestamp = datetime.datetime.now(datetime.timezone.utc) return aiohttp.web.json_response(probing_container) diff --git a/tests/conftest.py b/tests/conftest.py index 43bae928..bbafa261 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,6 +38,10 @@ def pytest_configure(config): # TODO: Remove when fixed in https://github.com/pytest-dev/pytest-asyncio/issues/460: config.addinivalue_line('filterwarnings', 'ignore:There is no current event loop:DeprecationWarning:pytest_asyncio') + # Python 3.12 transitional period: + config.addinivalue_line('filterwarnings', 'ignore:datetime.datetime.utcfromtimestamp.*:DeprecationWarning:dateutil') + config.addinivalue_line('filterwarnings', 'ignore:datetime.datetime.utcfromtimestamp.*:DeprecationWarning:freezegun') + def pytest_addoption(parser): parser.addoption("--only-e2e", action="store_true", help="Execute end-to-end tests only.") diff --git a/tests/handling/test_timing_consistency.py b/tests/handling/test_timing_consistency.py index 97e485d1..15650c3b 100644 --- a/tests/handling/test_timing_consistency.py +++ b/tests/handling/test_timing_consistency.py @@ -56,7 +56,7 @@ def move_to_tsB(*_, **__): # Simulate the call as if the event has just arrived on the watch-stream. # Another way (the same effect): process_changing_cause() and its result. with freezegun.freeze_time(tsA_triggered) as frozen_dt: - assert datetime.datetime.utcnow() < ts0 # extra precaution + assert datetime.datetime.now(datetime.timezone.utc) < ts0 # extra precaution await process_resource_event( lifecycle=kopf.lifecycles.all_at_once, registry=registry, @@ -68,7 +68,7 @@ def move_to_tsB(*_, **__): raw_event={'type': 'ADDED', 'object': body}, event_queue=asyncio.Queue(), ) - assert datetime.datetime.utcnow() > ts0 # extra precaution + assert datetime.datetime.now(datetime.timezone.utc) > ts0 # extra precaution assert state_store.called