From 9c86959afcf63f04308711277e6a821f13f9708d Mon Sep 17 00:00:00 2001 From: Sergey Vasilyev Date: Mon, 9 Oct 2023 22:28:46 +0200 Subject: [PATCH 1/3] Refactor Vault to use None for "nothing to expire" instead of max datetime Signed-off-by: Sergey Vasilyev --- kopf/_cogs/structs/credentials.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/kopf/_cogs/structs/credentials.py b/kopf/_cogs/structs/credentials.py index 9a40bb3f..25b4fa3b 100644 --- a/kopf/_cogs/structs/credentials.py +++ b/kopf/_cogs/structs/credentials.py @@ -118,7 +118,7 @@ def __init__( self._current = {} self._invalid = collections.defaultdict(list) self._lock = asyncio.Lock() - self._next_expiration = datetime.datetime.max + self._next_expiration: Optional[datetime.datetime] = None if __src is not None: self._update_converted(__src) @@ -230,7 +230,9 @@ async def expire(self) -> None: and not blocked from reappearing. """ now = datetime.datetime.utcnow() - if now >= self._next_expiration: # quick & lockless for speed: it is done on every API call + + # Quick & lockless for speed: it is done on every API call, we have no time for locks. + if self._next_expiration is not None and now >= self._next_expiration: async with self._lock: for key, item in list(self._current.items()): if item.info.expiration is not None and now >= item.info.expiration: @@ -385,4 +387,4 @@ def _update_expiration(self) -> None: for item in self._current.values() if item.info.expiration is not None ] - self._next_expiration = min(expirations + [datetime.datetime.max]) + self._next_expiration = min(expirations) if expirations else None From 34d53cefb8569b39b23b0803d499f04a82566377 Mon Sep 17 00:00:00 2001 From: Sergey Vasilyev Date: Mon, 9 Oct 2023 22:50:07 +0200 Subject: [PATCH 2/3] Rename internal functions for ISO-8601 timestamp parsing & rendering Signed-off-by: Sergey Vasilyev --- kopf/_core/actions/progression.py | 34 +++++++++++++------------------ 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/kopf/_core/actions/progression.py b/kopf/_core/actions/progression.py index 65367ee5..1154b5c6 100644 --- a/kopf/_core/actions/progression.py +++ b/kopf/_core/actions/progression.py @@ -62,9 +62,9 @@ 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(), - stopped=_datetime_fromisoformat(__d.get('stopped')), - delayed=_datetime_fromisoformat(__d.get('delayed')), + started=_parse_iso8601(__d.get('started')) or datetime.datetime.utcnow(), + stopped=_parse_iso8601(__d.get('stopped')), + delayed=_parse_iso8601(__d.get('delayed')), purpose=__d.get('purpose') if __d.get('purpose') else None, retries=__d.get('retries') or 0, success=__d.get('success') or False, @@ -76,9 +76,9 @@ def from_storage(cls, __d: progress.ProgressRecord) -> "HandlerState": def for_storage(self) -> progress.ProgressRecord: return progress.ProgressRecord( - started=None if self.started is None else _datetime_toisoformat(self.started), - stopped=None if self.stopped is None else _datetime_toisoformat(self.stopped), - delayed=None if self.delayed is None else _datetime_toisoformat(self.delayed), + started=None if self.started is None else _format_iso8601(self.started), + stopped=None if self.stopped is None else _format_iso8601(self.stopped), + delayed=None if self.delayed is None else _format_iso8601(self.delayed), purpose=None if self.purpose is None else str(self.purpose), retries=None if self.retries is None else int(self.retries), success=None if self.success is None else bool(self.success), @@ -355,30 +355,24 @@ def deliver_results( @overload -def _datetime_toisoformat(val: None) -> None: ... +def _format_iso8601(val: None) -> None: ... @overload -def _datetime_toisoformat(val: datetime.datetime) -> str: ... +def _format_iso8601(val: datetime.datetime) -> str: ... -def _datetime_toisoformat(val: Optional[datetime.datetime]) -> Optional[str]: - if val is None: - return None - else: - return val.isoformat(timespec='microseconds') +def _format_iso8601(val: Optional[datetime.datetime]) -> Optional[str]: + return None if val is None else val.isoformat(timespec='microseconds') @overload -def _datetime_fromisoformat(val: None) -> None: ... +def _parse_iso8601(val: None) -> None: ... @overload -def _datetime_fromisoformat(val: str) -> datetime.datetime: ... +def _parse_iso8601(val: str) -> datetime.datetime: ... -def _datetime_fromisoformat(val: Optional[str]) -> Optional[datetime.datetime]: - if val is None: - return None - else: - return datetime.datetime.fromisoformat(val) +def _parse_iso8601(val: Optional[str]) -> Optional[datetime.datetime]: + return None if val is None else datetime.datetime.fromisoformat(val) From dddaa5ea426d24bd84c8959bbfddcb5200183ab0 Mon Sep 17 00:00:00 2001 From: Sergey Vasilyev Date: Mon, 9 Oct 2023 22:08:05 +0200 Subject: [PATCH 3/3] Convert internal timestamps to TZ-aware, treat user-provided TZ-naive ones as UTC The old TZ-naive way is deprecated in Python 3.12 and soon will be removed. Switch to TZ-aware time for "now", and for all places that compare against "now". Signed-off-by: Sergey Vasilyev --- docs/authentication.rst | 3 ++ docs/errors.rst | 2 +- docs/probing.rst | 4 +-- examples/13-hooks/example.py | 2 +- kopf/_cogs/clients/events.py | 8 ++--- kopf/_cogs/structs/credentials.py | 31 +++++++++++-------- kopf/_core/actions/application.py | 2 +- kopf/_core/actions/execution.py | 6 ++-- kopf/_core/actions/progression.py | 12 ++++--- kopf/_core/engines/peering.py | 9 +++--- kopf/_core/engines/probing.py | 6 ++-- tests/authentication/test_vault.py | 9 +++--- tests/handling/daemons/conftest.py | 2 +- .../handling/indexing/test_index_exclusion.py | 10 +++--- tests/handling/test_cause_logging.py | 4 +-- tests/handling/test_delays.py | 12 +++---- tests/handling/test_timing_consistency.py | 7 +++-- tests/peering/test_peer_patching.py | 2 +- tests/peering/test_peers.py | 17 +++++----- tests/persistence/test_states.py | 17 +++++----- 20 files changed, 89 insertions(+), 76 deletions(-) diff --git a/docs/authentication.rst b/docs/authentication.rst index 11e3f31e..b38468d6 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -54,6 +54,9 @@ or an instance of :class:`kopf.ConnectionInfo`:: expiration=datetime.datetime(2099, 12, 31, 23, 59, 59), ) +Both TZ-naive & TZ-aware expiration times are supported. +The TZ-naive timestamps are always treated as UTC. + As with any other handlers, the login handler can be async if the network communication is needed and async mode is supported:: 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..3742f863 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(), # seen in `kubectl describe ...` + 'lastTimestamp': now.isoformat(), # seen in `kubectl get events` + 'eventTime': now.isoformat(), } try: diff --git a/kopf/_cogs/structs/credentials.py b/kopf/_cogs/structs/credentials.py index 25b4fa3b..cba84467 100644 --- a/kopf/_cogs/structs/credentials.py +++ b/kopf/_cogs/structs/credentials.py @@ -61,7 +61,7 @@ class ConnectionInfo: private_key_data: Optional[bytes] = None default_namespace: Optional[str] = None # used for cluster objects' k8s-events. priority: int = 0 - expiration: Optional[datetime.datetime] = None # TZ-naive, the same as utcnow() + expiration: Optional[datetime.datetime] = None # TZ-aware or TZ-naive (implies UTC) _T = TypeVar('_T', bound=object) @@ -229,15 +229,19 @@ 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) # Quick & lockless for speed: it is done on every API call, we have no time for locks. if self._next_expiration is not None and now >= self._next_expiration: async with self._lock: for key, item in list(self._current.items()): - if item.info.expiration is not None and now >= item.info.expiration: - await self._flush_caches(item) - del self._current[key] + expiration = item.info.expiration + if expiration is not None: + if expiration.tzinfo is None: + expiration = expiration.replace(tzinfo=datetime.timezone.utc) + if now >= expiration: + await self._flush_caches(item) + del self._current[key] self._update_expiration() need_reauth = not self._current # i.e. nothing is left at all @@ -317,11 +321,12 @@ async def populate( await self._ready.turn_to(True) def is_empty(self) -> bool: - now = datetime.datetime.utcnow() - return all( - item.info.expiration is not None and now >= item.info.expiration # i.e. expired - for key, item in self._current.items() - ) + now = datetime.datetime.now(datetime.timezone.utc) + expirations = [ + dt if dt is None or dt.tzinfo is not None else dt.replace(tzinfo=datetime.timezone.utc) + for dt in (item.info.expiration for item in self._current.values()) + ] + return all(dt is not None and now >= dt for dt in expirations) # i.e. expired async def wait_for_readiness(self) -> None: await self._ready.wait_for(True) @@ -383,8 +388,8 @@ def _update_converted( def _update_expiration(self) -> None: expirations = [ - item.info.expiration - for item in self._current.values() - if item.info.expiration is not None + dt if dt.tzinfo is not None else dt.replace(tzinfo=datetime.timezone.utc) + for dt in (item.info.expiration for item in self._current.values()) + if dt is not None ] self._next_expiration = min(expirations) if expirations else None 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 1154b5c6..337d2998 100644 --- a/kopf/_core/actions/progression.py +++ b/kopf/_core/actions/progression.py @@ -18,6 +18,8 @@ from typing import Any, Collection, Dict, Iterable, Iterator, \ Mapping, NamedTuple, Optional, overload +import iso8601 + from kopf._cogs.configs import progress from kopf._cogs.structs import bodies, ids, patches from kopf._core.actions import execution @@ -54,7 +56,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 +64,7 @@ def from_scratch(cls, *, purpose: Optional[str] = None) -> "HandlerState": def from_storage(cls, __d: progress.ProgressRecord) -> "HandlerState": return cls( active=False, - started=_parse_iso8601(__d.get('started')) or datetime.datetime.utcnow(), + started=_parse_iso8601(__d.get('started')) or datetime.datetime.now(datetime.timezone.utc), stopped=_parse_iso8601(__d.get('stopped')), delayed=_parse_iso8601(__d.get('delayed')), purpose=__d.get('purpose') if __d.get('purpose') else None, @@ -104,7 +106,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 +315,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() @@ -375,4 +377,4 @@ def _parse_iso8601(val: str) -> datetime.datetime: ... def _parse_iso8601(val: Optional[str]) -> Optional[datetime.datetime]: - return None if val is None else datetime.datetime.fromisoformat(val) + return None if val is None else iso8601.parse_date(val) # always TZ-aware diff --git a/kopf/_core/engines/peering.py b/kopf/_core/engines/peering.py index 6c511f8e..75f66502 100644 --- a/kopf/_core/engines/peering.py +++ b/kopf/_core/engines/peering.py @@ -68,10 +68,9 @@ 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()) - self.lastseen = self.lastseen.replace(tzinfo=None) # only the naive utc -- for comparison + datetime.datetime.now(datetime.timezone.utc)) 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 +148,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 +278,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/authentication/test_vault.py b/tests/authentication/test_vault.py index fa099cac..38da0043 100644 --- a/tests/authentication/test_vault.py +++ b/tests/authentication/test_vault.py @@ -1,6 +1,7 @@ import datetime import freezegun +import iso8601 import pytest from kopf._cogs.structs.credentials import ConnectionInfo, LoginError, Vault, VaultKey @@ -55,7 +56,7 @@ async def test_yielding_after_population(mocker): @freezegun.freeze_time('2020-01-01T00:00:00') async def test_yielding_items_before_expiration(mocker): - future = datetime.datetime(2020, 1, 1, 0, 0, 0, 1) + future = iso8601.parse_date('2020-01-01T00:00:00.000001') key1 = VaultKey('some-key') info1 = ConnectionInfo(server='https://expected/', expiration=future) vault = Vault() @@ -74,8 +75,8 @@ async def test_yielding_items_before_expiration(mocker): @pytest.mark.parametrize('delta', [0, 1]) @freezegun.freeze_time('2020-01-01T00:00:00') async def test_yielding_ignores_expired_items(mocker, delta): - future = datetime.datetime(2020, 1, 1, 0, 0, 0, 1) - past = datetime.datetime(2020, 1, 1) - datetime.timedelta(microseconds=delta) + future = iso8601.parse_date('2020-01-01T00:00:00.000001') + past = iso8601.parse_date('2020-01-01') - datetime.timedelta(microseconds=delta) key1 = VaultKey('some-key') key2 = VaultKey('other-key') info1 = ConnectionInfo(server='https://expected/', expiration=past) @@ -96,7 +97,7 @@ async def test_yielding_ignores_expired_items(mocker, delta): @pytest.mark.parametrize('delta', [0, 1]) @freezegun.freeze_time('2020-01-01T00:00:00') async def test_yielding_when_everything_is_expired(mocker, delta): - past = datetime.datetime(2020, 1, 1) - datetime.timedelta(microseconds=delta) + past = iso8601.parse_date('2020-01-01') - datetime.timedelta(microseconds=delta) key1 = VaultKey('some-key') info1 = ConnectionInfo(server='https://expected/', expiration=past) vault = Vault() diff --git a/tests/handling/daemons/conftest.py b/tests/handling/daemons/conftest.py index bdd0f846..fc6f2ae5 100644 --- a/tests/handling/daemons/conftest.py +++ b/tests/handling/daemons/conftest.py @@ -104,7 +104,7 @@ def frozen_time(): A helper to simulate time movements to step over long sleeps/timeouts. """ # TODO LATER: Either freezegun should support the system clock, or find something else. - with freezegun.freeze_time("2020-01-01 00:00:00") as frozen: + with freezegun.freeze_time("2020-01-01T00:00:00") as frozen: # Use freezegun-supported time instead of system clocks -- for testing purposes only. # NB: Patch strictly after the time is frozen -- to use fake_time(), not real time(). with patch('time.monotonic', time.time), patch('time.perf_counter', time.time): diff --git a/tests/handling/indexing/test_index_exclusion.py b/tests/handling/indexing/test_index_exclusion.py index f015613e..1deeb58d 100644 --- a/tests/handling/indexing/test_index_exclusion.py +++ b/tests/handling/indexing/test_index_exclusion.py @@ -1,8 +1,8 @@ import asyncio -import datetime import logging import freezegun +import iso8601 import pytest from kopf._cogs.aiokits.aiotoggles import Toggle @@ -76,7 +76,7 @@ async def test_temporary_failures_with_expired_delays_are_reindexed( resource, namespace, settings, registry, memories, indexers, index, caplog, event_type, handlers): caplog.set_level(logging.DEBUG) body = {'metadata': {'namespace': namespace, 'name': 'name1'}} - delayed = datetime.datetime(2020, 12, 31, 23, 59, 59, 0) + delayed = iso8601.parse_date('2020-12-31T23:59:59') memory = await memories.recall(raw_body=body) memory.indexing_memory.indexing_state = State({'index_fn': HandlerState(delayed=delayed)}) await process_resource_event( @@ -153,9 +153,9 @@ async def test_removed_and_remembered_on_permanent_errors( @freezegun.freeze_time('2020-12-31T00:00:00') @pytest.mark.parametrize('delay_kwargs, expected_delayed', [ - (dict(), datetime.datetime(2020, 12, 31, 0, 1, 0)), - (dict(delay=0), datetime.datetime(2020, 12, 31, 0, 0, 0)), - (dict(delay=9), datetime.datetime(2020, 12, 31, 0, 0, 9)), + (dict(), iso8601.parse_date('2020-12-31T00:01:00')), + (dict(delay=0), iso8601.parse_date('2020-12-31T00:00:00')), + (dict(delay=9), iso8601.parse_date('2020-12-31T00:00:09')), (dict(delay=None), None), ]) @pytest.mark.usefixtures('indexed_123') diff --git a/tests/handling/test_cause_logging.py b/tests/handling/test_cause_logging.py index b6ca44bb..f6a673d0 100644 --- a/tests/handling/test_cause_logging.py +++ b/tests/handling/test_cause_logging.py @@ -1,8 +1,8 @@ import asyncio -import datetime import logging import freezegun +import iso8601 import pytest import kopf @@ -106,7 +106,7 @@ async def test_diffs_not_logged_if_absent(registry, settings, resource, handlers # Timestamps: time zero (0), before (B), after (A), and time zero+1s (1). -TS0 = datetime.datetime(2020, 12, 31, 23, 59, 59, 123456) +TS0 = iso8601.parse_date('2020-12-31T23:59:59.123456') TS1_ISO = '2021-01-01T00:00:00.123456' diff --git a/tests/handling/test_delays.py b/tests/handling/test_delays.py index 3b00dd74..1659f051 100644 --- a/tests/handling/test_delays.py +++ b/tests/handling/test_delays.py @@ -1,8 +1,8 @@ import asyncio -import datetime import logging import freezegun +import iso8601 import pytest import kopf @@ -18,7 +18,7 @@ @pytest.mark.parametrize('cause_reason', HANDLER_REASONS) @pytest.mark.parametrize('now, delayed_iso, delay', [ - ['2020-01-01T00:00:00', '2020-01-01T00:04:56.789000', 4 * 60 + 56.789], + ['2020-01-01T00:00:00', '2020-01-01T00:04:56.789000+00:00', 4 * 60 + 56.789], ], ids=['fast']) async def test_delayed_handlers_progress( registry, settings, handlers, resource, cause_mock, cause_reason, @@ -66,8 +66,8 @@ async def test_delayed_handlers_progress( @pytest.mark.parametrize('cause_reason', HANDLER_REASONS) @pytest.mark.parametrize('now, delayed_iso, delay', [ - ['2020-01-01T00:00:00', '2020-01-01T00:04:56.789000', 4 * 60 + 56.789], - ['2020-01-01T00:00:00', '2099-12-31T23:59:59.000000', WAITING_KEEPALIVE_INTERVAL], + ['2020-01-01T00:00:00', '2020-01-01T00:04:56.789000+00:00', 4 * 60 + 56.789], + ['2020-01-01T00:00:00', '2099-12-31T23:59:59.000000+00:00', WAITING_KEEPALIVE_INTERVAL], ], ids=['fast', 'slow']) async def test_delayed_handlers_sleep( registry, settings, handlers, resource, cause_mock, cause_reason, @@ -76,8 +76,8 @@ async def test_delayed_handlers_sleep( # Simulate the original persisted state of the resource. # Make sure the finalizer is added since there are mandatory deletion handlers. - started_dt = datetime.datetime.fromisoformat('2000-01-01T00:00:00') # long time ago is fine. - delayed_dt = datetime.datetime.fromisoformat(delayed_iso) + started_dt = iso8601.parse_date('2000-01-01T00:00:00') # long time ago is fine. + delayed_dt = iso8601.parse_date(delayed_iso) event_type = None if cause_reason == Reason.RESUME else 'irrelevant' event_body = { 'metadata': {'finalizers': [settings.persistence.finalizer]}, diff --git a/tests/handling/test_timing_consistency.py b/tests/handling/test_timing_consistency.py index 97e485d1..81d001ab 100644 --- a/tests/handling/test_timing_consistency.py +++ b/tests/handling/test_timing_consistency.py @@ -2,6 +2,7 @@ import datetime import freezegun +import iso8601 import kopf from kopf._cogs.structs.ephemera import Memo @@ -34,7 +35,7 @@ async def test_consistent_awakening(registry, settings, resource, k8s_mocked, mo """ # Simulate that the object is scheduled to be awakened between the watch-event and sleep. - ts0 = datetime.datetime(2019, 12, 30, 10, 56, 43) + ts0 = iso8601.parse_date('2019-12-30T10:56:43') tsA_triggered = "2019-12-30T10:56:42.999999" ts0_scheduled = "2019-12-30T10:56:43.000000" tsB_delivered = "2019-12-30T10:56:43.000001" @@ -56,7 +57,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 +69,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 diff --git a/tests/peering/test_peer_patching.py b/tests/peering/test_peer_patching.py index f7c2803f..64cc025e 100644 --- a/tests/peering/test_peer_patching.py +++ b/tests/peering/test_peer_patching.py @@ -46,7 +46,7 @@ async def test_touching_a_peer_stores_it( patch = await patch_mock.call_args_list[0][0][0].json() assert set(patch['status']) == {'id1'} assert patch['status']['id1']['priority'] == 0 - assert patch['status']['id1']['lastseen'] == '2020-12-31T23:59:59.123456' + assert patch['status']['id1']['lastseen'] == '2020-12-31T23:59:59.123456+00:00' assert patch['status']['id1']['lifetime'] == 60 diff --git a/tests/peering/test_peers.py b/tests/peering/test_peers.py index b677a55c..a0291d48 100644 --- a/tests/peering/test_peers.py +++ b/tests/peering/test_peers.py @@ -1,6 +1,7 @@ import datetime import freezegun +import iso8601 from kopf._core.engines.peering import Peer @@ -10,14 +11,14 @@ def test_defaults(): peer = Peer(identity='id') assert peer.identity == 'id' assert peer.lifetime == datetime.timedelta(seconds=60) - assert peer.lastseen == datetime.datetime(2020, 12, 31, 23, 59, 59, 123456) + assert peer.lastseen == iso8601.parse_date('2020-12-31T23:59:59.123456') @freezegun.freeze_time('2020-12-31T23:59:59.123456') def test_repr(): peer = Peer(identity='some-id') text = repr(peer) - assert text == "" + assert text == "" @freezegun.freeze_time('2020-12-31T23:59:59.123456') @@ -47,13 +48,13 @@ def test_creation_with_lifetime_unspecified(): @freezegun.freeze_time('2020-12-31T23:59:59.123456') def test_creation_with_lastseen_as_string(): peer = Peer(identity='id', lastseen='2020-01-01T12:34:56.789123') - assert peer.lastseen == datetime.datetime(2020, 1, 1, 12, 34, 56, 789123) + assert peer.lastseen == iso8601.parse_date('2020-01-01T12:34:56.789123') @freezegun.freeze_time('2020-12-31T23:59:59.123456') def test_creation_with_lastseen_unspecified(): peer = Peer(identity='id') - assert peer.lastseen == datetime.datetime(2020, 12, 31, 23, 59, 59, 123456) + assert peer.lastseen == iso8601.parse_date('2020-12-31T23:59:59.123456') @freezegun.freeze_time('2020-12-31T23:59:59.123456') @@ -64,8 +65,8 @@ def test_creation_as_alive(): lastseen='2020-12-31T23:59:50.123456', # less than 10 seconds before "now" ) assert peer.lifetime == datetime.timedelta(seconds=10) - assert peer.lastseen == datetime.datetime(2020, 12, 31, 23, 59, 50, 123456) - assert peer.deadline == datetime.datetime(2021, 1, 1, 0, 0, 0, 123456) + assert peer.lastseen == iso8601.parse_date('2020-12-31T23:59:50.123456') + assert peer.deadline == iso8601.parse_date('2021-01-01T00:00:00.123456') assert peer.is_dead is False @@ -77,6 +78,6 @@ def test_creation_as_dead(): lastseen='2020-12-31T23:59:49.123456', # 10 seconds before "now" sharp ) assert peer.lifetime == datetime.timedelta(seconds=10) - assert peer.lastseen == datetime.datetime(2020, 12, 31, 23, 59, 49, 123456) - assert peer.deadline == datetime.datetime(2020, 12, 31, 23, 59, 59, 123456) + assert peer.lastseen == iso8601.parse_date('2020-12-31T23:59:49.123456') + assert peer.deadline == iso8601.parse_date('2020-12-31T23:59:59.123456') assert peer.is_dead is True diff --git a/tests/persistence/test_states.py b/tests/persistence/test_states.py index 32714230..d0b0b1b1 100644 --- a/tests/persistence/test_states.py +++ b/tests/persistence/test_states.py @@ -2,6 +2,7 @@ from unittest.mock import Mock import freezegun +import iso8601 import pytest from kopf._cogs.configs.progress import SmartProgressStorage, StatusProgressStorage @@ -12,14 +13,14 @@ from kopf._core.intents.causes import HANDLER_REASONS, Reason # Timestamps: time zero (0), before (B), after (A), and time zero+1s (1). -TSB = datetime.datetime(2020, 12, 31, 23, 59, 59) -TS0 = datetime.datetime(2020, 12, 31, 23, 59, 59, 123456) -TS1 = datetime.datetime(2021, 1, 1, 00, 00, 00, 123456) -TSA = datetime.datetime(2020, 12, 31, 23, 59, 59, 999999) -TSB_ISO = '2020-12-31T23:59:59.000000' -TS0_ISO = '2020-12-31T23:59:59.123456' -TS1_ISO = '2021-01-01T00:00:00.123456' -TSA_ISO = '2020-12-31T23:59:59.999999' +TSB_ISO = '2020-12-31T23:59:59.000000+00:00' +TS0_ISO = '2020-12-31T23:59:59.123456+00:00' +TS1_ISO = '2021-01-01T00:00:00.123456+00:00' +TSA_ISO = '2020-12-31T23:59:59.999999+00:00' +TSB = iso8601.parse_date(TSB_ISO) +TS0 = iso8601.parse_date(TS0_ISO) +TS1 = iso8601.parse_date(TS1_ISO) +TSA = iso8601.parse_date(TSA_ISO) ZERO_DELTA = datetime.timedelta(seconds=0)