Files
community.crypto/plugins/module_utils/time.py
Felix Fontein f758d94fba Add type hints and type checking (#885)
* Enable basic type checking.

* Fix first errors.

* Add changelog fragment.

* Add types to module_utils and plugin_utils (without module backends).

* Add typing hints for acme_* modules.

* Add typing to X.509 certificate modules, and add more helpers.

* Add typing to remaining module backends.

* Add typing for action, filter, and lookup plugins.

* Bump ansible-core 2.19 beta requirement for typing.

* Add more typing definitions.

* Add typing to some unit tests.
2025-05-11 18:00:11 +02:00

155 lines
5.1 KiB
Python

# Copyright (c) 2024, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
import datetime
import re
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
)
UTC = datetime.timezone.utc
def get_now_datetime(with_timezone: bool) -> datetime.datetime:
if with_timezone:
return datetime.datetime.now(tz=UTC)
return datetime.datetime.utcnow()
def ensure_utc_timezone(timestamp: datetime.datetime) -> datetime.datetime:
if timestamp.tzinfo is UTC:
return timestamp
if timestamp.tzinfo is None:
# We assume that naive datetime objects use timezone UTC!
return timestamp.replace(tzinfo=UTC)
return timestamp.astimezone(UTC)
def remove_timezone(timestamp: datetime.datetime) -> datetime.datetime:
# Convert to native datetime object
if timestamp.tzinfo is None:
return timestamp
if timestamp.tzinfo is not UTC:
timestamp = timestamp.astimezone(UTC)
return timestamp.replace(tzinfo=None)
def add_or_remove_timezone(
timestamp: datetime.datetime, with_timezone: bool
) -> datetime.datetime:
return (
ensure_utc_timezone(timestamp) if with_timezone else remove_timezone(timestamp)
)
def get_epoch_seconds(timestamp: datetime.datetime) -> float:
if timestamp.tzinfo is None:
# timestamp.timestamp() is offset by the local timezone if timestamp has no timezone
timestamp = ensure_utc_timezone(timestamp)
return timestamp.timestamp()
def from_epoch_seconds(
timestamp: int | float, with_timezone: bool
) -> datetime.datetime:
if with_timezone:
return datetime.datetime.fromtimestamp(timestamp, UTC)
return datetime.datetime.utcfromtimestamp(timestamp)
def convert_relative_to_datetime(
relative_time_string: str,
with_timezone: bool = False,
now: datetime.datetime | None = None,
) -> datetime.datetime | None:
"""Get a datetime.datetime or None from a string in the time format described in sshd_config(5)"""
parsed_result = re.match(
r"^(?P<prefix>[+-])((?P<weeks>\d+)[wW])?((?P<days>\d+)[dD])?((?P<hours>\d+)[hH])?((?P<minutes>\d+)[mM])?((?P<seconds>\d+)[sS]?)?$",
relative_time_string,
)
if parsed_result is None or len(relative_time_string) == 1:
# not matched or only a single "+" or "-"
return None
offset = datetime.timedelta(0)
if parsed_result.group("weeks") is not None:
offset += datetime.timedelta(weeks=int(parsed_result.group("weeks")))
if parsed_result.group("days") is not None:
offset += datetime.timedelta(days=int(parsed_result.group("days")))
if parsed_result.group("hours") is not None:
offset += datetime.timedelta(hours=int(parsed_result.group("hours")))
if parsed_result.group("minutes") is not None:
offset += datetime.timedelta(minutes=int(parsed_result.group("minutes")))
if parsed_result.group("seconds") is not None:
offset += datetime.timedelta(seconds=int(parsed_result.group("seconds")))
if now is None:
now = get_now_datetime(with_timezone=with_timezone)
else:
now = add_or_remove_timezone(now, with_timezone=with_timezone)
if parsed_result.group("prefix") == "+":
return now + offset
else:
return now - offset
def get_relative_time_option(
input_string: str,
input_name: str,
with_timezone: bool = False,
now: datetime.datetime | None = None,
) -> datetime.datetime:
"""
Return an absolute timespec if a relative timespec or an ASN1 formatted
string is provided.
The return value will be a datetime object.
"""
result = to_native(input_string)
if result is None:
raise OpenSSLObjectError(
f'The timespec "{input_string}" for {input_name} is not valid'
)
# Relative time
if result.startswith("+") or result.startswith("-"):
res = convert_relative_to_datetime(result, with_timezone=with_timezone, now=now)
if res is None:
raise OpenSSLObjectError(
f'The timespec "{input_string}" for {input_name} is invalid'
)
return res
# Absolute time
for date_fmt, length in [
(
"%Y%m%d%H%M%SZ",
15,
), # this also parses '202401020304Z', but as datetime(2024, 1, 2, 3, 0, 4)
("%Y%m%d%H%MZ", 13),
(
"%Y%m%d%H%M%S%z",
14 + 5,
), # this also parses '202401020304+0000', but as datetime(2024, 1, 2, 3, 0, 4, tzinfo=...)
("%Y%m%d%H%M%z", 12 + 5),
]:
if len(result) != length:
continue
try:
res = datetime.datetime.strptime(result, date_fmt)
except ValueError:
pass
else:
return add_or_remove_timezone(res, with_timezone=with_timezone)
raise OpenSSLObjectError(
f'The time spec "{input_string}" for {input_name} is invalid'
)