diff options
author | Darius Makovsky <traveltissues@protonmail.com> | 2020-01-08 16:30:47 +0000 |
---|---|---|
committer | Jürg Billeter <j@bitron.ch> | 2020-02-05 16:11:32 +0100 |
commit | c4dafc8477f0787d622164d5c45fd9081af25a00 (patch) | |
tree | 797fc7afee13ef4634bf768ea8afdcec1cb7cd38 | |
parent | 59b26c9697a23966ae72db7fed0e8af4258e4317 (diff) | |
download | buildstream-c4dafc8477f0787d622164d5c45fd9081af25a00.tar.gz |
utils.py: Add file timestamp helpers
-rw-r--r-- | requirements/requirements.in | 1 | ||||
-rw-r--r-- | requirements/requirements.txt | 1 | ||||
-rw-r--r-- | src/buildstream/utils.py | 77 | ||||
-rw-r--r-- | tests/internals/utils_move_atomic.py | 23 |
4 files changed, 101 insertions, 1 deletions
diff --git a/requirements/requirements.in b/requirements/requirements.in index 50bb523da..ca38d710e 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -9,3 +9,4 @@ ruamel.yaml.clib >= 0.1.2 setuptools pyroaring ujson +python-dateutil >= 2.7.0 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 962090823..ab7a3a1f7 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -8,6 +8,7 @@ ruamel.yaml==0.16.5 setuptools==39.0.1 pyroaring==0.2.9 ujson==1.35 +python-dateutil==2.8.1 ## The following requirements were added by pip freeze: MarkupSafe==1.1.1 ruamel.yaml.clib==0.2.0 diff --git a/src/buildstream/utils.py b/src/buildstream/utils.py index 88314b263..9593f3e75 100644 --- a/src/buildstream/utils.py +++ b/src/buildstream/utils.py @@ -33,10 +33,12 @@ from stat import S_ISDIR import subprocess import tempfile import time +import datetime import itertools from contextlib import contextmanager from pathlib import Path from typing import Callable, IO, Iterable, Iterator, Optional, Tuple, Union +from dateutil import parser as dateutil_parser import psutil @@ -133,6 +135,81 @@ class FileListResult: return ret +def _make_timestamp(timepoint: float) -> str: + """Obtain the ISO 8601 timestamp represented by the time given in seconds. + + Args: + timepoint (float): the time since the epoch in seconds + + Returns: + (str): the timestamp specified by https://www.ietf.org/rfc/rfc3339.txt + with a UTC timezone code 'Z'. + + """ + assert isinstance(timepoint, float), "Time to render as timestamp must be a float: {}".format(str(timepoint)) + try: + return datetime.datetime.utcfromtimestamp(timepoint).strftime("%Y-%m-%dT%H:%M:%S.%fZ") + except (OverflowError, TypeError): + raise UtilError("Failed to make UTC timestamp from {}".format(timepoint)) + + +def _get_file_mtimestamp(fullpath: str) -> str: + """Obtain the ISO 8601 timestamp represented by the mtime of the + file at the given path.""" + assert isinstance(fullpath, str), "Path to file must be a string: {}".format(str(fullpath)) + try: + mtime = os.path.getmtime(fullpath) + except OSError: + raise UtilError("Failed to get mtime of file at {}".format(fullpath)) + return _make_timestamp(mtime) + + +def _parse_timestamp(timestamp: str) -> float: + """Parse an ISO 8601 timestamp as specified in + https://www.ietf.org/rfc/rfc3339.txt. Only timestamps with the UTC code + 'Z' or an offset are valid. For example: '2019-12-12T10:23:01.54Z' or + '2019-12-12T10:23:01.54+00:00'. + + Args: + timestamp (str): the timestamp + + Returns: + (float): The time in seconds since epoch represented by the + timestamp. + + Raises: + UtilError: if extraction of seconds fails + """ + assert isinstance(timestamp, str), "Timestamp to parse must be a string: {}".format(str(timestamp)) + try: + errmsg = "Failed to parse given timestamp: " + timestamp + parsed_time = dateutil_parser.isoparse(timestamp) + if parsed_time.tzinfo: + return parsed_time.timestamp() + raise UtilError(errmsg) + except (ValueError, OverflowError, TypeError): + raise UtilError(errmsg) + + +def _set_file_mtime(fullpath: str, seconds: Union[int, float]) -> None: + """Set the access and modification times of the file at the given path + to the given time. The time of the file will be set with nanosecond + resolution if supported. + + Args: + fullpath (str): the string representing the path to the file + timestamp (int, float): the time in seconds since the UNIX epoch + """ + assert isinstance(fullpath, str), "Path to file must be a string: {}".format(str(fullpath)) + assert isinstance(seconds, (int, float)), "Mtime to set must be a float or integer: {}".format(str(seconds)) + set_mtime = seconds * 10 ** 9 + try: + os.utime(fullpath, times=None, ns=(int(set_mtime), int(set_mtime))) + except OSError: + errmsg = "Failed to set the times of the file at {} to {}".format(fullpath, str(seconds)) + raise UtilError(errmsg) + + def list_relative_paths(directory: str) -> Iterator[str]: """A generator for walking directory relative paths diff --git a/tests/internals/utils_move_atomic.py b/tests/internals/utils_move_atomic.py index cda020809..dd417cb66 100644 --- a/tests/internals/utils_move_atomic.py +++ b/tests/internals/utils_move_atomic.py @@ -3,7 +3,13 @@ import pytest -from buildstream.utils import move_atomic, DirectoryExistsError +from buildstream.utils import ( + move_atomic, + DirectoryExistsError, + _get_file_mtimestamp, + _set_file_mtime, + _parse_timestamp, +) @pytest.fixture @@ -89,3 +95,18 @@ def test_move_to_existing_non_empty_dir(src, tmp_path): with pytest.raises(DirectoryExistsError): move_atomic(src, dst) + + +def test_move_to_empty_dir_set_mtime(src, tmp_path): + dst = tmp_path.joinpath("dst") + move_atomic(src, dst) + assert dst.joinpath("test").exists() + _dst = str(dst) + # set the mtime via stamp + timestamp1 = "2020-01-08T11:05:50.832123Z" + _set_file_mtime(_dst, _parse_timestamp(timestamp1)) + assert timestamp1 == _get_file_mtimestamp(_dst) + # reset the mtime using an offset stamp + timestamp2 = "2010-02-12T12:05:50.832123+01:00" + _set_file_mtime(_dst, _parse_timestamp(timestamp2)) + assert _get_file_mtimestamp(_dst) == "2010-02-12T11:05:50.832123Z" |