meerschaum.utils.daemon.RotatingFile

A file-like object which writes to rotating subfiles (native: meerschaum-jobs, rotating.rs).

  1#! /usr/bin/env python3
  2# -*- coding: utf-8 -*-
  3# vim:fenc=utf-8
  4
  5"""
  6A file-like object which writes to rotating subfiles (native: `meerschaum-jobs`, `rotating.rs`).
  7"""
  8
  9from __future__ import annotations
 10
 11import io
 12import pathlib
 13
 14from meerschaum.utils.typing import Optional, List
 15
 16
 17class RotatingFile(io.IOBase):
 18    """
 19    A `RotatingFile` may be treated like a normal file-like object.
 20    Under the hood, however, it will create new sub-files and delete old ones.
 21
 22    The stream redirection of v4 (`redirect_streams`, `start_log_fd_interception`) is not
 23    available: the job supervisor copies a job's output into its log.
 24    """
 25
 26    SEEK_BACK_ATTEMPTS: int = 5
 27
 28    def __init__(
 29        self,
 30        file_path: pathlib.Path,
 31        num_files_to_keep: Optional[int] = None,
 32        max_file_size: Optional[int] = None,
 33        redirect_streams: bool = False,
 34        write_timestamps: bool = False,
 35        timestamp_format: Optional[str] = None,
 36        write_callback=None,
 37    ):
 38        """
 39        Create a file-like object which manages other files.
 40
 41        Parameters
 42        ----------
 43        file_path: pathlib.Path
 44            The base path (`logs/<name>.log`); subfiles are `<name>.log.<n>`.
 45
 46        num_files_to_keep: int, default None
 47            How many sub-files to keep at any given time.
 48            Defaults to the configured value (5).
 49
 50        max_file_size: int, default None
 51            How large in bytes each sub-file can grow before another file is created.
 52            Defaults to the configured value (100_000).
 53
 54        write_timestamps: bool, default False
 55            If `True`, prepend the current UTC timestamp to each line of the file.
 56
 57        timestamp_format: str, default None
 58            If `write_timestamps` is `True`, use this format for the timestamps.
 59            Defaults to `'%Y-%m-%d %H:%M'`.
 60        """
 61        from meerschaum.config import get_config
 62        from meerschaum.jobs import _native
 63        if redirect_streams or write_callback is not None:
 64            raise NotImplementedError(
 65                "`redirect_streams` and `write_callback` are not supported in Meerschaum v5 "
 66                "(see the migration guide, 'Removed in 5.0')."
 67            )
 68        self.file_path = pathlib.Path(file_path)
 69        if num_files_to_keep is None:
 70            num_files_to_keep = get_config('jobs', 'logs', 'num_files_to_keep')
 71        if max_file_size is None:
 72            max_file_size = get_config('jobs', 'logs', 'max_file_size')
 73        if timestamp_format is None:
 74            timestamp_format = get_config('jobs', 'logs', 'timestamps', 'format')
 75        if num_files_to_keep < 1:
 76            raise ValueError("At least 1 file must be kept.")
 77        if max_file_size < 100:
 78            raise ValueError("Subfiles must contain at least 100 bytes.")
 79        self.num_files_to_keep = num_files_to_keep
 80        self.max_file_size = max_file_size
 81        self.write_timestamps = write_timestamps
 82        self.timestamp_format = timestamp_format
 83        self._core = _native.core().RotatingFile(
 84            str(self.file_path),
 85            num_files_to_keep,
 86            max_file_size,
 87            write_timestamps,
 88            timestamp_format,
 89        )
 90        self._closed = False
 91        self._lines_buffer: List[str] = []
 92
 93    @property
 94    def closed(self) -> bool:
 95        return self._closed
 96
 97    def readable(self) -> bool:
 98        return True
 99
100    def writable(self) -> bool:
101        return True
102
103    def seekable(self) -> bool:
104        return True
105
106    def isatty(self) -> bool:
107        return False
108
109    def fileno(self) -> int:
110        raise io.UnsupportedOperation("A RotatingFile has no file descriptor.")
111
112    def read(self, *args, **kw) -> str:
113        """Return the text of every subfile."""
114        return self._core.read()
115
116    def readlines(self, *args, **kw) -> List[str]:
117        """Return the lines since the cursor."""
118        return self._core.readlines()
119
120    def readline(self, *args, **kw) -> str:
121        """Return the next line since the cursor ('' at the end)."""
122        if not self._lines_buffer:
123            self._lines_buffer = self._core.readlines()
124        return self._lines_buffer.pop(0) if self._lines_buffer else ''
125
126    def write(self, data: str) -> int:
127        """Write text, rotating subfiles as they fill."""
128        return self._core.write(data)
129
130    def writelines(self, lines) -> None:
131        for line in lines:
132            self.write(line)
133
134    def seek(self, position: int, *args, **kw) -> int:
135        """Move the read cursor in the latest subfile."""
136        self._lines_buffer = []
137        self._core.seek(position)
138        return position
139
140    def touch(self) -> None:
141        """Create the latest subfile."""
142        self._core.touch()
143
144    def refresh_cursor(self) -> None:
145        self._core.refresh_cursor()
146
147    def flush(self) -> None:
148        pass
149
150    def close(self) -> None:
151        """Close the open subfile handles."""
152        self._core.close()
153        self._closed = True
154
155    def delete(self, unused_only: bool = False) -> None:
156        """Delete the subfiles (`unused_only`: only those beyond the files to keep)."""
157        self._core.delete(unused_only)
158
159    def get_latest_subfile_index(self) -> int:
160        return self._core.get_latest_subfile_index()
161
162    def get_latest_subfile_path(self) -> pathlib.Path:
163        return pathlib.Path(self._core.get_latest_subfile_path())
164
165    def get_existing_subfile_indices(self) -> List[int]:
166        return self._core.get_existing_subfile_indices()
167
168    def get_existing_subfile_paths(self) -> List[pathlib.Path]:
169        return [pathlib.Path(p) for p in self._core.get_existing_subfile_paths()]
170
171    def get_subfile_path_from_index(self, index: int) -> pathlib.Path:
172        return pathlib.Path(self._core.get_subfile_path_from_index(index))
173
174    def get_subfile_name_from_index(self, index: int) -> str:
175        return self._core.get_subfile_name_from_index(index)
176
177    def get_index_from_subfile_name(self, subfile_name: str) -> int:
178        return self._core.get_index_from_subfile_name(subfile_name)
179
180    def get_timestamp_prefix_str(self) -> str:
181        return self._core.get_timestamp_prefix_str()
class RotatingFile(io.IOBase):
 18class RotatingFile(io.IOBase):
 19    """
 20    A `RotatingFile` may be treated like a normal file-like object.
 21    Under the hood, however, it will create new sub-files and delete old ones.
 22
 23    The stream redirection of v4 (`redirect_streams`, `start_log_fd_interception`) is not
 24    available: the job supervisor copies a job's output into its log.
 25    """
 26
 27    SEEK_BACK_ATTEMPTS: int = 5
 28
 29    def __init__(
 30        self,
 31        file_path: pathlib.Path,
 32        num_files_to_keep: Optional[int] = None,
 33        max_file_size: Optional[int] = None,
 34        redirect_streams: bool = False,
 35        write_timestamps: bool = False,
 36        timestamp_format: Optional[str] = None,
 37        write_callback=None,
 38    ):
 39        """
 40        Create a file-like object which manages other files.
 41
 42        Parameters
 43        ----------
 44        file_path: pathlib.Path
 45            The base path (`logs/<name>.log`); subfiles are `<name>.log.<n>`.
 46
 47        num_files_to_keep: int, default None
 48            How many sub-files to keep at any given time.
 49            Defaults to the configured value (5).
 50
 51        max_file_size: int, default None
 52            How large in bytes each sub-file can grow before another file is created.
 53            Defaults to the configured value (100_000).
 54
 55        write_timestamps: bool, default False
 56            If `True`, prepend the current UTC timestamp to each line of the file.
 57
 58        timestamp_format: str, default None
 59            If `write_timestamps` is `True`, use this format for the timestamps.
 60            Defaults to `'%Y-%m-%d %H:%M'`.
 61        """
 62        from meerschaum.config import get_config
 63        from meerschaum.jobs import _native
 64        if redirect_streams or write_callback is not None:
 65            raise NotImplementedError(
 66                "`redirect_streams` and `write_callback` are not supported in Meerschaum v5 "
 67                "(see the migration guide, 'Removed in 5.0')."
 68            )
 69        self.file_path = pathlib.Path(file_path)
 70        if num_files_to_keep is None:
 71            num_files_to_keep = get_config('jobs', 'logs', 'num_files_to_keep')
 72        if max_file_size is None:
 73            max_file_size = get_config('jobs', 'logs', 'max_file_size')
 74        if timestamp_format is None:
 75            timestamp_format = get_config('jobs', 'logs', 'timestamps', 'format')
 76        if num_files_to_keep < 1:
 77            raise ValueError("At least 1 file must be kept.")
 78        if max_file_size < 100:
 79            raise ValueError("Subfiles must contain at least 100 bytes.")
 80        self.num_files_to_keep = num_files_to_keep
 81        self.max_file_size = max_file_size
 82        self.write_timestamps = write_timestamps
 83        self.timestamp_format = timestamp_format
 84        self._core = _native.core().RotatingFile(
 85            str(self.file_path),
 86            num_files_to_keep,
 87            max_file_size,
 88            write_timestamps,
 89            timestamp_format,
 90        )
 91        self._closed = False
 92        self._lines_buffer: List[str] = []
 93
 94    @property
 95    def closed(self) -> bool:
 96        return self._closed
 97
 98    def readable(self) -> bool:
 99        return True
100
101    def writable(self) -> bool:
102        return True
103
104    def seekable(self) -> bool:
105        return True
106
107    def isatty(self) -> bool:
108        return False
109
110    def fileno(self) -> int:
111        raise io.UnsupportedOperation("A RotatingFile has no file descriptor.")
112
113    def read(self, *args, **kw) -> str:
114        """Return the text of every subfile."""
115        return self._core.read()
116
117    def readlines(self, *args, **kw) -> List[str]:
118        """Return the lines since the cursor."""
119        return self._core.readlines()
120
121    def readline(self, *args, **kw) -> str:
122        """Return the next line since the cursor ('' at the end)."""
123        if not self._lines_buffer:
124            self._lines_buffer = self._core.readlines()
125        return self._lines_buffer.pop(0) if self._lines_buffer else ''
126
127    def write(self, data: str) -> int:
128        """Write text, rotating subfiles as they fill."""
129        return self._core.write(data)
130
131    def writelines(self, lines) -> None:
132        for line in lines:
133            self.write(line)
134
135    def seek(self, position: int, *args, **kw) -> int:
136        """Move the read cursor in the latest subfile."""
137        self._lines_buffer = []
138        self._core.seek(position)
139        return position
140
141    def touch(self) -> None:
142        """Create the latest subfile."""
143        self._core.touch()
144
145    def refresh_cursor(self) -> None:
146        self._core.refresh_cursor()
147
148    def flush(self) -> None:
149        pass
150
151    def close(self) -> None:
152        """Close the open subfile handles."""
153        self._core.close()
154        self._closed = True
155
156    def delete(self, unused_only: bool = False) -> None:
157        """Delete the subfiles (`unused_only`: only those beyond the files to keep)."""
158        self._core.delete(unused_only)
159
160    def get_latest_subfile_index(self) -> int:
161        return self._core.get_latest_subfile_index()
162
163    def get_latest_subfile_path(self) -> pathlib.Path:
164        return pathlib.Path(self._core.get_latest_subfile_path())
165
166    def get_existing_subfile_indices(self) -> List[int]:
167        return self._core.get_existing_subfile_indices()
168
169    def get_existing_subfile_paths(self) -> List[pathlib.Path]:
170        return [pathlib.Path(p) for p in self._core.get_existing_subfile_paths()]
171
172    def get_subfile_path_from_index(self, index: int) -> pathlib.Path:
173        return pathlib.Path(self._core.get_subfile_path_from_index(index))
174
175    def get_subfile_name_from_index(self, index: int) -> str:
176        return self._core.get_subfile_name_from_index(index)
177
178    def get_index_from_subfile_name(self, subfile_name: str) -> int:
179        return self._core.get_index_from_subfile_name(subfile_name)
180
181    def get_timestamp_prefix_str(self) -> str:
182        return self._core.get_timestamp_prefix_str()

A RotatingFile may be treated like a normal file-like object. Under the hood, however, it will create new sub-files and delete old ones.

The stream redirection of v4 (redirect_streams, start_log_fd_interception) is not available: the job supervisor copies a job's output into its log.

RotatingFile( file_path: pathlib.Path, num_files_to_keep: int | None = None, max_file_size: int | None = None, redirect_streams: bool = False, write_timestamps: bool = False, timestamp_format: str | None = None, write_callback=None)
29    def __init__(
30        self,
31        file_path: pathlib.Path,
32        num_files_to_keep: Optional[int] = None,
33        max_file_size: Optional[int] = None,
34        redirect_streams: bool = False,
35        write_timestamps: bool = False,
36        timestamp_format: Optional[str] = None,
37        write_callback=None,
38    ):
39        """
40        Create a file-like object which manages other files.
41
42        Parameters
43        ----------
44        file_path: pathlib.Path
45            The base path (`logs/<name>.log`); subfiles are `<name>.log.<n>`.
46
47        num_files_to_keep: int, default None
48            How many sub-files to keep at any given time.
49            Defaults to the configured value (5).
50
51        max_file_size: int, default None
52            How large in bytes each sub-file can grow before another file is created.
53            Defaults to the configured value (100_000).
54
55        write_timestamps: bool, default False
56            If `True`, prepend the current UTC timestamp to each line of the file.
57
58        timestamp_format: str, default None
59            If `write_timestamps` is `True`, use this format for the timestamps.
60            Defaults to `'%Y-%m-%d %H:%M'`.
61        """
62        from meerschaum.config import get_config
63        from meerschaum.jobs import _native
64        if redirect_streams or write_callback is not None:
65            raise NotImplementedError(
66                "`redirect_streams` and `write_callback` are not supported in Meerschaum v5 "
67                "(see the migration guide, 'Removed in 5.0')."
68            )
69        self.file_path = pathlib.Path(file_path)
70        if num_files_to_keep is None:
71            num_files_to_keep = get_config('jobs', 'logs', 'num_files_to_keep')
72        if max_file_size is None:
73            max_file_size = get_config('jobs', 'logs', 'max_file_size')
74        if timestamp_format is None:
75            timestamp_format = get_config('jobs', 'logs', 'timestamps', 'format')
76        if num_files_to_keep < 1:
77            raise ValueError("At least 1 file must be kept.")
78        if max_file_size < 100:
79            raise ValueError("Subfiles must contain at least 100 bytes.")
80        self.num_files_to_keep = num_files_to_keep
81        self.max_file_size = max_file_size
82        self.write_timestamps = write_timestamps
83        self.timestamp_format = timestamp_format
84        self._core = _native.core().RotatingFile(
85            str(self.file_path),
86            num_files_to_keep,
87            max_file_size,
88            write_timestamps,
89            timestamp_format,
90        )
91        self._closed = False
92        self._lines_buffer: List[str] = []

Create a file-like object which manages other files.

Parameters
  • file_path (pathlib.Path): The base path (logs/<name>.log); subfiles are <name>.log.<n>.
  • num_files_to_keep (int, default None): How many sub-files to keep at any given time. Defaults to the configured value (5).
  • max_file_size (int, default None): How large in bytes each sub-file can grow before another file is created. Defaults to the configured value (100_000).
  • write_timestamps (bool, default False): If True, prepend the current UTC timestamp to each line of the file.
  • timestamp_format (str, default None): If write_timestamps is True, use this format for the timestamps. Defaults to '%Y-%m-%d %H:%M'.
SEEK_BACK_ATTEMPTS: int = 5
file_path
num_files_to_keep
max_file_size
write_timestamps
timestamp_format
closed: bool
94    @property
95    def closed(self) -> bool:
96        return self._closed
def readable(self) -> bool:
98    def readable(self) -> bool:
99        return True

Return whether object was opened for reading.

If False, read() will raise OSError.

def writable(self) -> bool:
101    def writable(self) -> bool:
102        return True

Return whether object was opened for writing.

If False, write() will raise OSError.

def seekable(self) -> bool:
104    def seekable(self) -> bool:
105        return True

Return whether object supports random access.

If False, seek(), tell() and truncate() will raise OSError. This method may need to do a test seek().

def isatty(self) -> bool:
107    def isatty(self) -> bool:
108        return False

Return whether this is an 'interactive' stream.

Return False if it can't be determined.

def fileno(self) -> int:
110    def fileno(self) -> int:
111        raise io.UnsupportedOperation("A RotatingFile has no file descriptor.")

Return underlying file descriptor if one exists.

Raise OSError if the IO object does not use a file descriptor.

def read(self, *args, **kw) -> str:
113    def read(self, *args, **kw) -> str:
114        """Return the text of every subfile."""
115        return self._core.read()

Return the text of every subfile.

def readlines(self, *args, **kw) -> List[str]:
117    def readlines(self, *args, **kw) -> List[str]:
118        """Return the lines since the cursor."""
119        return self._core.readlines()

Return the lines since the cursor.

def readline(self, *args, **kw) -> str:
121    def readline(self, *args, **kw) -> str:
122        """Return the next line since the cursor ('' at the end)."""
123        if not self._lines_buffer:
124            self._lines_buffer = self._core.readlines()
125        return self._lines_buffer.pop(0) if self._lines_buffer else ''

Return the next line since the cursor ('' at the end).

def write(self, data: str) -> int:
127    def write(self, data: str) -> int:
128        """Write text, rotating subfiles as they fill."""
129        return self._core.write(data)

Write text, rotating subfiles as they fill.

def writelines(self, lines) -> None:
131    def writelines(self, lines) -> None:
132        for line in lines:
133            self.write(line)

Write a list of lines to stream.

Line separators are not added, so it is usual for each of the lines provided to have a line separator at the end.

def seek(self, position: int, *args, **kw) -> int:
135    def seek(self, position: int, *args, **kw) -> int:
136        """Move the read cursor in the latest subfile."""
137        self._lines_buffer = []
138        self._core.seek(position)
139        return position

Move the read cursor in the latest subfile.

def touch(self) -> None:
141    def touch(self) -> None:
142        """Create the latest subfile."""
143        self._core.touch()

Create the latest subfile.

def refresh_cursor(self) -> None:
145    def refresh_cursor(self) -> None:
146        self._core.refresh_cursor()
def flush(self) -> None:
148    def flush(self) -> None:
149        pass

Flush write buffers, if applicable.

This is not implemented for read-only and non-blocking streams.

def close(self) -> None:
151    def close(self) -> None:
152        """Close the open subfile handles."""
153        self._core.close()
154        self._closed = True

Close the open subfile handles.

def delete(self, unused_only: bool = False) -> None:
156    def delete(self, unused_only: bool = False) -> None:
157        """Delete the subfiles (`unused_only`: only those beyond the files to keep)."""
158        self._core.delete(unused_only)

Delete the subfiles (unused_only: only those beyond the files to keep).

def get_latest_subfile_index(self) -> int:
160    def get_latest_subfile_index(self) -> int:
161        return self._core.get_latest_subfile_index()
def get_latest_subfile_path(self) -> pathlib.Path:
163    def get_latest_subfile_path(self) -> pathlib.Path:
164        return pathlib.Path(self._core.get_latest_subfile_path())
def get_existing_subfile_indices(self) -> List[int]:
166    def get_existing_subfile_indices(self) -> List[int]:
167        return self._core.get_existing_subfile_indices()
def get_existing_subfile_paths(self) -> List[pathlib.Path]:
169    def get_existing_subfile_paths(self) -> List[pathlib.Path]:
170        return [pathlib.Path(p) for p in self._core.get_existing_subfile_paths()]
def get_subfile_path_from_index(self, index: int) -> pathlib.Path:
172    def get_subfile_path_from_index(self, index: int) -> pathlib.Path:
173        return pathlib.Path(self._core.get_subfile_path_from_index(index))
def get_subfile_name_from_index(self, index: int) -> str:
175    def get_subfile_name_from_index(self, index: int) -> str:
176        return self._core.get_subfile_name_from_index(index)
def get_index_from_subfile_name(self, subfile_name: str) -> int:
178    def get_index_from_subfile_name(self, subfile_name: str) -> int:
179        return self._core.get_index_from_subfile_name(subfile_name)
def get_timestamp_prefix_str(self) -> str:
181    def get_timestamp_prefix_str(self) -> str:
182        return self._core.get_timestamp_prefix_str()