pythonLogs 3.0.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ LOG_LEVEL=DEBUG
2
+ LOG_TIMEZONE=UTC
3
+ LOG_ENCODING=UTF-8
4
+ LOG_APPNAME=app
5
+ LOG_FILENAME=app.log
6
+ LOG_DIRECTORY=/app/logs
7
+ LOG_DAYS_TO_KEEP=30
8
+ LOG_STREAM_HANDLER=True
9
+ LOG_SHOW_LOCATION=False
10
+ LOG_DATE_FORMAT=%Y-%m-%dT%H:%M:%S
11
+
12
+ # SizeRotatingLog
13
+ LOG_MAX_FILE_SIZE_MB=10
14
+
15
+ # TimedRotatingLog
16
+ LOG_ROTATE_WHEN=midnight
17
+ LOG_ROTATE_AT_UTC=True
pythonLogs/__init__.py ADDED
@@ -0,0 +1,55 @@
1
+ import logging
2
+ from importlib.metadata import version
3
+ from typing import Literal, NamedTuple
4
+ from .timed_rotating import TimedRotatingLog
5
+ from .size_rotating import SizeRotatingLog
6
+ from .basic_log import BasicLog
7
+
8
+
9
+ __all__ = (
10
+ "BasicLog",
11
+ "TimedRotatingLog",
12
+ "SizeRotatingLog",
13
+ )
14
+
15
+ __title__ = "pythonLogs"
16
+ __author__ = "Daniel Costa"
17
+ __email__ = "danieldcsta@gmail.com>"
18
+ __license__ = "MIT"
19
+ __copyright__ = "Copyright 2024-present ddc"
20
+ _req_python_version = (3, 10, 0)
21
+
22
+
23
+ try:
24
+ _version = tuple(int(x) for x in version(__title__).split("."))
25
+ except ModuleNotFoundError:
26
+ _version = (0, 0, 0)
27
+
28
+
29
+ class VersionInfo(NamedTuple):
30
+ major: int
31
+ minor: int
32
+ micro: int
33
+ releaselevel: Literal["alpha", "beta", "candidate", "final"]
34
+ serial: int
35
+
36
+
37
+ __version__ = _version
38
+ __version_info__: VersionInfo = VersionInfo(
39
+ major=__version__[0],
40
+ minor=__version__[1],
41
+ micro=__version__[2],
42
+ releaselevel="final",
43
+ serial=0
44
+ )
45
+ __req_python_version__: VersionInfo = VersionInfo(
46
+ major=_req_python_version[0],
47
+ minor=_req_python_version[1],
48
+ micro=_req_python_version[2],
49
+ releaselevel="final",
50
+ serial=0
51
+ )
52
+
53
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
54
+
55
+ del logging, NamedTuple, Literal, VersionInfo, version, _version, _req_python_version
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ import logging
3
+ from typing import Optional
4
+ from pythonLogs.log_utils import get_format, get_level, get_timezone_function
5
+ from pythonLogs.settings import LogSettings
6
+
7
+
8
+ class BasicLog:
9
+ def __init__(
10
+ self,
11
+ level: Optional[str] = None,
12
+ name: Optional[str] = None,
13
+ encoding: Optional[str] = None,
14
+ datefmt: Optional[str] = None,
15
+ timezone: Optional[str] = None,
16
+ showlocation: Optional[bool] = None,
17
+ ):
18
+ _settings = LogSettings()
19
+ self.level = get_level(level or _settings.level)
20
+ self.appname = name or _settings.appname
21
+ self.encoding = encoding or _settings.encoding
22
+ self.datefmt = datefmt or _settings.date_format
23
+ self.timezone = timezone or _settings.timezone
24
+ self.showlocation = showlocation or _settings.show_location
25
+
26
+ def init(self):
27
+ logger = logging.getLogger(self.appname)
28
+ logger.setLevel(self.level)
29
+ logging.Formatter.converter = get_timezone_function(self.timezone)
30
+ _format = get_format(self.showlocation, self.appname, self.timezone)
31
+ logging.basicConfig(datefmt=self.datefmt, encoding=self.encoding, format=_format)
32
+ return logger
@@ -0,0 +1,264 @@
1
+ # -*- encoding: utf-8 -*-
2
+ import errno
3
+ import gzip
4
+ import logging.handlers
5
+ import os
6
+ import shutil
7
+ import sys
8
+ import time
9
+ from datetime import datetime, timedelta, timezone as dttz
10
+ from time import struct_time
11
+ from typing import Any, Callable
12
+ import pytz
13
+
14
+
15
+ def get_stream_handler(
16
+ level: int,
17
+ formatter: logging.Formatter,
18
+ ) -> logging.StreamHandler:
19
+
20
+ stream_hdlr = logging.StreamHandler()
21
+ stream_hdlr.setFormatter(formatter)
22
+ stream_hdlr.setLevel(level)
23
+ return stream_hdlr
24
+
25
+
26
+ def get_logger_and_formatter(
27
+ name: str,
28
+ datefmt: str,
29
+ show_location: bool,
30
+ timezone: str,
31
+ ) -> [logging.Logger, logging.Formatter]:
32
+
33
+ logger = logging.getLogger(name)
34
+ for handler in logger.handlers[:]:
35
+ handler.close()
36
+ logger.removeHandler(handler)
37
+
38
+ formatt = get_format(show_location, name, timezone)
39
+ formatter = logging.Formatter(formatt, datefmt=datefmt)
40
+ formatter.converter = get_timezone_function(timezone)
41
+ return logger, formatter
42
+
43
+
44
+ def check_filename_instance(filenames: list | tuple) -> None:
45
+ if not isinstance(filenames, list | tuple):
46
+ err_msg = f"Unable to parse filenames. Filename instance is not list or tuple. | {filenames}"
47
+ write_stderr(err_msg)
48
+ raise TypeError(err_msg)
49
+
50
+
51
+ def check_directory_permissions(directory_path: str) -> None:
52
+ if os.path.isdir(directory_path) and not os.access(directory_path, os.W_OK | os.X_OK):
53
+ err_msg = f"Unable to access directory | {directory_path}"
54
+ write_stderr(err_msg)
55
+ raise PermissionError(err_msg)
56
+
57
+ try:
58
+ if not os.path.isdir(directory_path):
59
+ os.makedirs(directory_path, mode=0o755, exist_ok=True)
60
+ except PermissionError as e:
61
+ err_msg = f"Unable to create directory | {directory_path}"
62
+ write_stderr(f"{err_msg} | {repr(e)}")
63
+ raise PermissionError(err_msg)
64
+
65
+
66
+ def remove_old_logs(logs_dir: str, days_to_keep: int) -> None:
67
+ files_list = list_files(logs_dir, ends_with=".gz")
68
+ for file in files_list:
69
+ try:
70
+ if is_older_than_x_days(file, days_to_keep):
71
+ delete_file(file)
72
+ except Exception as e:
73
+ write_stderr(f"Unable to delete {days_to_keep} days old logs | {file} | {repr(e)}")
74
+
75
+
76
+ def list_files(directory: str, ends_with: str) -> tuple:
77
+ """
78
+ List all files in the given directory
79
+ and returns them in a list sorted by creation time in ascending order
80
+ :param directory:
81
+ :param ends_with:
82
+ :return: tuple
83
+ """
84
+
85
+ try:
86
+ result: list = []
87
+ if os.path.isdir(directory):
88
+ result: list = [os.path.join(directory, f) for f in os.listdir(directory) if f.lower().endswith(ends_with)]
89
+ result.sort(key=os.path.getmtime)
90
+ return tuple(result)
91
+ except Exception as e:
92
+ write_stderr(repr(e))
93
+ raise e
94
+
95
+
96
+ def delete_file(path: str) -> bool:
97
+ """
98
+ Remove the given file and returns True if the file was successfully removed
99
+ :param path:
100
+ :return: True
101
+ """
102
+ try:
103
+ if os.path.isfile(path):
104
+ os.remove(path)
105
+ elif os.path.exists(path):
106
+ shutil.rmtree(path)
107
+ else:
108
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
109
+ except OSError as e:
110
+ write_stderr(repr(e))
111
+ raise e
112
+ return True
113
+
114
+
115
+ def is_older_than_x_days(path: str, days: int) -> bool:
116
+ """
117
+ Check if a file or directory is older than the specified number of days
118
+ :param path:
119
+ :param days:
120
+ :return:
121
+ """
122
+
123
+ if not os.path.exists(path):
124
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
125
+
126
+ try:
127
+ if int(days) in (0, 1):
128
+ cutoff_time = datetime.today()
129
+ else:
130
+ cutoff_time = datetime.today() - timedelta(days=int(days))
131
+ except ValueError as e:
132
+ write_stderr(repr(e))
133
+ raise e
134
+
135
+ file_timestamp = os.stat(path).st_mtime
136
+ file_time = datetime.fromtimestamp(file_timestamp)
137
+
138
+ if file_time < cutoff_time:
139
+ return True
140
+ return False
141
+
142
+
143
+ def write_stderr(msg: str) -> None:
144
+ """
145
+ Write msg to stderr
146
+ :param msg:
147
+ :return: None
148
+ """
149
+
150
+ obj = datetime.now(dttz.utc)
151
+ dt = obj.astimezone(pytz.timezone(os.getenv("LOG_TIMEZONE", "UTC")))
152
+ dt_timezone = dt.strftime("%Y-%m-%dT%H:%M:%S.%f:%z")
153
+ sys.stderr.write(f"[{dt_timezone}]:[ERROR]:{msg}\n")
154
+
155
+
156
+ def get_level(level: str) -> logging:
157
+ """
158
+ Get logging level
159
+ :param level:
160
+ :return: level
161
+ """
162
+
163
+ if not isinstance(level, str):
164
+ write_stderr(f"Unable to get log level. Setting default level to: 'INFO' ({logging.INFO})")
165
+ return logging.INFO
166
+
167
+ match level.lower():
168
+ case "debug":
169
+ return logging.DEBUG
170
+ case "warning" | "warn":
171
+ return logging.WARNING
172
+ case "error":
173
+ return logging.ERROR
174
+ case "critical" | "crit":
175
+ return logging.CRITICAL
176
+ case _:
177
+ return logging.INFO
178
+
179
+
180
+ def get_log_path(directory: str, filename: str) -> str:
181
+ """
182
+ Get log file path
183
+ :param directory:
184
+ :param filename:
185
+ :return: path as str
186
+ """
187
+
188
+ log_file_path = str(os.path.join(directory, filename))
189
+ err_message = f"Unable to open log file for writing | {log_file_path}"
190
+
191
+ try:
192
+ open(log_file_path, "a+").close()
193
+ except PermissionError as e:
194
+ write_stderr(f"{err_message} | {repr(e)}")
195
+ raise PermissionError(err_message)
196
+ except FileNotFoundError as e:
197
+ write_stderr(f"{err_message} | {repr(e)}")
198
+ raise FileNotFoundError(err_message)
199
+ except OSError as e:
200
+ write_stderr(f"{err_message} | {repr(e)}")
201
+ raise e
202
+
203
+ return log_file_path
204
+
205
+
206
+ def get_format(show_location: bool, name: str, timezone: str) -> str:
207
+ _debug_fmt = ""
208
+ _logger_name = ""
209
+
210
+ if name:
211
+ _logger_name = f"[{name}]:"
212
+
213
+ if show_location:
214
+ _debug_fmt = "[%(filename)s:%(funcName)s:%(lineno)d]:"
215
+
216
+ if timezone == "localtime":
217
+ utc_offset = time.strftime("%z")
218
+ else:
219
+ utc_offset = datetime.now(pytz.timezone(timezone)).strftime("%z")
220
+
221
+ fmt = f"[%(asctime)s.%(msecs)03d{utc_offset}]:[%(levelname)s]:{_logger_name}{_debug_fmt}%(message)s"
222
+ return fmt
223
+
224
+
225
+ def gzip_file_with_sufix(file_path, sufix) -> str | None:
226
+ """
227
+ gzip file
228
+ :param file_path:
229
+ :param sufix:
230
+ :return: bool
231
+ """
232
+
233
+ if os.path.isfile(file_path):
234
+ sfname, sext = os.path.splitext(file_path)
235
+ renamed_dst = f"{sfname}_{sufix}{sext}.gz"
236
+
237
+ try:
238
+ with open(file_path, "rb") as fin:
239
+ with gzip.open(renamed_dst, "wb") as fout:
240
+ fout.writelines(fin)
241
+ except Exception as e:
242
+ write_stderr(f"Unable to gzip log file | {file_path} | {repr(e)}")
243
+ raise e
244
+
245
+ try:
246
+ delete_file(file_path)
247
+ except OSError as e:
248
+ write_stderr(f"Unable to delete source log file | {file_path} | {repr(e)}")
249
+ raise e
250
+
251
+ return renamed_dst
252
+
253
+
254
+ def get_timezone_function(
255
+ time_zone: str,
256
+ ) -> Callable[[float | None, Any], struct_time] | Callable[[Any], struct_time]:
257
+
258
+ match time_zone.lower():
259
+ case "utc":
260
+ return time.gmtime
261
+ case "localtime":
262
+ return time.localtime
263
+ case _:
264
+ return lambda *args: datetime.now(tz=pytz.timezone(time_zone)).timetuple()
pythonLogs/settings.py ADDED
@@ -0,0 +1,45 @@
1
+ # -*- encoding: utf-8 -*-
2
+ from enum import Enum
3
+ from dotenv import load_dotenv
4
+ from pydantic import Field
5
+ from pydantic_settings import BaseSettings, SettingsConfigDict
6
+ from typing import Optional
7
+
8
+
9
+ class LogLevel(str, Enum):
10
+ """log levels"""
11
+
12
+ CRITICAL = "CRITICAL"
13
+ CRIT = "CRIT"
14
+ ERROR = "ERROR"
15
+ WARNING = "WARNING"
16
+ WARN = "WARN"
17
+ INFO = "INFO"
18
+ DEBUG = "DEBUG"
19
+
20
+
21
+ class LogSettings(BaseSettings):
22
+ """If any ENV variable is omitted, it falls back to default values here"""
23
+
24
+ load_dotenv()
25
+
26
+ level: Optional[LogLevel] = Field(default=LogLevel.INFO)
27
+ appname: Optional[str] = Field(default="app")
28
+ directory: Optional[str] = Field(default="/app/logs")
29
+ filename: Optional[str] = Field(default="app.log")
30
+ encoding: Optional[str] = Field(default="UTF-8")
31
+ date_format: Optional[str] = Field(default="%Y-%m-%dT%H:%M:%S")
32
+ days_to_keep: Optional[int] = Field(default=30)
33
+ timezone: Optional[str] = Field(default="UTC")
34
+ stream_handler: Optional[bool] = Field(default=True)
35
+ show_location: Optional[bool] = Field(default=False)
36
+
37
+ # SizeRotatingLog
38
+ max_file_size_mb: Optional[int] = Field(default=10)
39
+
40
+ # TimedRotatingLog
41
+ rotate_when: Optional[str] = Field(default="midnight")
42
+ rotate_at_utc: Optional[bool] = Field(default=True)
43
+ rotate_file_sufix: Optional[str] = Field(default="%Y%m%d")
44
+
45
+ model_config = SettingsConfigDict(env_prefix="LOG_", env_file=".env", extra="allow")
@@ -0,0 +1,105 @@
1
+ # -*- encoding: utf-8 -*-
2
+ import logging.handlers
3
+ import os
4
+ from typing import Optional
5
+ from pythonLogs.log_utils import (
6
+ check_directory_permissions,
7
+ check_filename_instance,
8
+ get_level,
9
+ get_log_path,
10
+ get_logger_and_formatter,
11
+ get_stream_handler,
12
+ gzip_file_with_sufix,
13
+ list_files,
14
+ remove_old_logs,
15
+ write_stderr,
16
+ )
17
+ from pythonLogs.settings import LogSettings
18
+
19
+
20
+ class SizeRotatingLog:
21
+ def __init__(
22
+ self,
23
+ level: Optional[str] = None,
24
+ name: Optional[str] = None,
25
+ directory: Optional[str] = None,
26
+ filenames: Optional[list | tuple] = None,
27
+ maxmbytes: Optional[int] = None,
28
+ daystokeep: Optional[int] = None,
29
+ encoding: Optional[str] = None,
30
+ datefmt: Optional[str] = None,
31
+ timezone: Optional[str] = None,
32
+ streamhandler: Optional[bool] = None,
33
+ showlocation: Optional[bool] = None,
34
+ ):
35
+ _settings = LogSettings()
36
+ self.level = get_level(level or _settings.level)
37
+ self.appname = name or _settings.appname
38
+ self.directory = directory or _settings.directory
39
+ self.filenames = filenames or (_settings.filename,)
40
+ self.maxmbytes = maxmbytes or _settings.max_file_size_mb
41
+ self.daystokeep = daystokeep or _settings.days_to_keep
42
+ self.encoding = encoding or _settings.encoding
43
+ self.datefmt = datefmt or _settings.date_format
44
+ self.timezone = timezone or _settings.timezone
45
+ self.streamhandler = streamhandler or _settings.stream_handler
46
+ self.showlocation = showlocation or _settings.show_location
47
+
48
+ def init(self):
49
+ check_filename_instance(self.filenames)
50
+ check_directory_permissions(self.directory)
51
+
52
+ logger, formatter = get_logger_and_formatter(self.appname, self.datefmt, self.showlocation, self.timezone)
53
+ logger.setLevel(self.level)
54
+
55
+ for file in self.filenames:
56
+ log_file_path = get_log_path(self.directory, file)
57
+
58
+ file_handler = logging.handlers.RotatingFileHandler(
59
+ filename=log_file_path,
60
+ mode="a",
61
+ maxBytes=self.maxmbytes * 1024 * 1024,
62
+ backupCount=self.daystokeep,
63
+ encoding=self.encoding,
64
+ delay=False,
65
+ errors=None,
66
+ )
67
+ file_handler.rotator = GZipRotatorSize(self.directory, self.daystokeep)
68
+ file_handler.setFormatter(formatter)
69
+ file_handler.setLevel(self.level)
70
+ logger.addHandler(file_handler)
71
+
72
+ if self.streamhandler:
73
+ stream_hdlr = get_stream_handler(self.level, formatter)
74
+ logger.addHandler(stream_hdlr)
75
+
76
+ return logger
77
+
78
+
79
+ class GZipRotatorSize:
80
+ def __init__(self, dir_logs: str, daystokeep: int):
81
+ self.directory = dir_logs
82
+ self.daystokeep = daystokeep
83
+
84
+ def __call__(self, source: str, dest: str) -> None:
85
+ remove_old_logs(self.directory, self.daystokeep)
86
+ if os.path.isfile(source) and os.stat(source).st_size > 0:
87
+ source_filename, _ = os.path.basename(source).split(".")
88
+ new_file_number = self._get_new_file_number(self.directory, source_filename)
89
+ if os.path.isfile(source):
90
+ gzip_file_with_sufix(source, new_file_number)
91
+
92
+ @staticmethod
93
+ def _get_new_file_number(directory, source_filename):
94
+ new_file_number = 1
95
+ previous_gz_files = list_files(directory, ends_with=".gz")
96
+ for gz_file in previous_gz_files:
97
+ if source_filename in gz_file:
98
+ try:
99
+ oldest_file_name = gz_file.split(".")[0].split("_")
100
+ if len(oldest_file_name) > 1:
101
+ new_file_number = int(oldest_file_name[1]) + 1
102
+ except ValueError as e:
103
+ write_stderr(f"Unable to get previous gz log file number | {gz_file} | {repr(e)}")
104
+ raise
105
+ return new_file_number
@@ -0,0 +1,94 @@
1
+ # -*- encoding: utf-8 -*-
2
+ import logging.handlers
3
+ import os
4
+ from typing import Optional
5
+ from pythonLogs.log_utils import (
6
+ check_directory_permissions,
7
+ check_filename_instance,
8
+ get_level,
9
+ get_log_path,
10
+ get_logger_and_formatter,
11
+ get_stream_handler,
12
+ gzip_file_with_sufix,
13
+ remove_old_logs,
14
+ )
15
+ from pythonLogs.settings import LogSettings
16
+
17
+
18
+ class TimedRotatingLog:
19
+ """
20
+ Current 'rotating_when' events supported for TimedRotatingLogs:
21
+ midnight - roll over at midnight
22
+ W{0-6} - roll over on a certain day; 0 - Monday
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ level: Optional[str] = None,
28
+ name: Optional[str] = None,
29
+ directory: Optional[str] = None,
30
+ filenames: Optional[list | tuple] = None,
31
+ when: Optional[str] = None,
32
+ sufix: Optional[str] = None,
33
+ daystokeep: Optional[int] = None,
34
+ encoding: Optional[str] = None,
35
+ datefmt: Optional[str] = None,
36
+ timezone: Optional[str] = None,
37
+ streamhandler: Optional[bool] = None,
38
+ showlocation: Optional[bool] = None,
39
+ rotateatutc: Optional[bool] = None,
40
+ ):
41
+ _settings = LogSettings()
42
+ self.level = get_level(level or _settings.level)
43
+ self.appname = name or _settings.appname
44
+ self.directory = directory or _settings.directory
45
+ self.filenames = filenames or (_settings.filename,)
46
+ self.when = when or _settings.rotate_when
47
+ self.sufix = sufix or _settings.rotate_file_sufix
48
+ self.daystokeep = daystokeep or _settings.days_to_keep
49
+ self.encoding = encoding or _settings.encoding
50
+ self.datefmt = datefmt or _settings.date_format
51
+ self.timezone = timezone or _settings.timezone
52
+ self.streamhandler = streamhandler or _settings.stream_handler
53
+ self.showlocation = showlocation or _settings.show_location
54
+ self.rotateatutc = rotateatutc or _settings.rotate_at_utc
55
+
56
+ def init(self):
57
+ check_filename_instance(self.filenames)
58
+ check_directory_permissions(self.directory)
59
+
60
+ logger, formatter = get_logger_and_formatter(self.appname, self.datefmt, self.showlocation, self.timezone)
61
+ logger.setLevel(self.level)
62
+
63
+ for file in self.filenames:
64
+ log_file_path = get_log_path(self.directory, file)
65
+
66
+ file_handler = logging.handlers.TimedRotatingFileHandler(
67
+ filename=log_file_path,
68
+ encoding=self.encoding,
69
+ when=self.when,
70
+ utc=self.rotateatutc,
71
+ backupCount=self.daystokeep,
72
+ )
73
+ file_handler.suffix = self.sufix
74
+ file_handler.rotator = GZipRotatorTimed(self.directory, self.daystokeep)
75
+ file_handler.setFormatter(formatter)
76
+ file_handler.setLevel(self.level)
77
+ logger.addHandler(file_handler)
78
+
79
+ if self.streamhandler:
80
+ stream_hdlr = get_stream_handler(self.level, formatter)
81
+ logger.addHandler(stream_hdlr)
82
+
83
+ return logger
84
+
85
+
86
+ class GZipRotatorTimed:
87
+ def __init__(self, dir_logs: str, days_to_keep: int):
88
+ self.dir = dir_logs
89
+ self.days_to_keep = days_to_keep
90
+
91
+ def __call__(self, source: str, dest: str) -> None:
92
+ remove_old_logs(self.dir, self.days_to_keep)
93
+ sufix = os.path.splitext(dest)[1].replace(".", "")
94
+ gzip_file_with_sufix(source, sufix)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present ddc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.1
2
+ Name: pythonLogs
3
+ Version: 3.0.12
4
+ Summary: Easy logs with rotations
5
+ Home-page: https://pypi.org/project/pythonLogs
6
+ License: MIT
7
+ Keywords: python3,python-3,python,log,logging,logger,logutils,log-utils,pythonLogs
8
+ Author: Daniel Costa
9
+ Author-email: danieldcsta@gmail.com
10
+ Maintainer: Daniel Costa
11
+ Requires-Python: >=3.10,<4.0
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Other Environment
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Natural Language :: English
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3 :: Only
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Requires-Dist: pydantic-settings (>=2.7.1,<3.0.0)
26
+ Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
27
+ Requires-Dist: pytz (>=2024.2,<2025.0)
28
+ Project-URL: Repository, https://github.com/ddc/pythonLogs
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Easy logs with rotations
32
+
33
+ [![Donate](https://img.shields.io/badge/Donate-PayPal-brightgreen.svg?style=plastic)](https://www.paypal.com/ncp/payment/6G9Z78QHUD4RJ)
34
+ [![License](https://img.shields.io/pypi/l/pythonLogs)](https://github.com/ddc/pythonLogs/blob/main/LICENSE)
35
+ [![PyPi](https://img.shields.io/pypi/v/pythonLogs.svg)](https://pypi.python.org/pypi/pythonLogs)
36
+ [![PyPI Downloads](https://static.pepy.tech/badge/pythonLogs)](https://pepy.tech/projects/pythonLogs)
37
+ [![codecov](https://codecov.io/gh/ddc/pythonLogs/graph/badge.svg?token=QsjwsmYzgD)](https://codecov.io/gh/ddc/pythonLogs)
38
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
39
+ [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A//actions-badge.atrox.dev/ddc/pythonLogs/badge?ref=main&label=build&logo=none)](https://actions-badge.atrox.dev/ddc/pythonLogs/goto?ref=main)
40
+ [![Python](https://img.shields.io/pypi/pyversions/pythonLogs.svg)](https://www.python.org)
41
+
42
+
43
+
44
+ # Logs
45
+ + Parameters for all classes are declared as OPTIONAL
46
+ + If any [.env](./pythonLogs/.env.example) variable is omitted, it falls back to default values here: [settings.py](pythonLogs/settings.py)
47
+ + Function arguments will overwrite any env variable
48
+ + Timezone parameter can also accept `localtime`, default to `UTC`
49
+ + This parameter is only to display the timezone datetime inside the log file
50
+ + For timed rotation, only UTC and localtime are supported, meaning it will rotate at UTC or localtime
51
+ + env variable to change between UTC and localtime is `LOG_ROTATE_AT_UTC` and default to True
52
+ + Streamhandler parameter will add stream handler along with file handler
53
+ + Showlocation parameter will show the filename and the line number where the message originated
54
+
55
+
56
+
57
+
58
+ # Install
59
+ ```shell
60
+ pip install pythonLogs
61
+ ```
62
+
63
+
64
+
65
+ # BasicLog
66
+ + Setup Logging
67
+ + This is just a basic log, it does not use any file
68
+ ```python
69
+ from pythonLogs import BasicLog
70
+ logger = BasicLog(
71
+ level="debug",
72
+ name="app",
73
+ timezone="America/Sao_Paulo",
74
+ showlocation=False,
75
+ ).init()
76
+ logger.warning("This is a warning example")
77
+ ```
78
+ #### Example of output
79
+ `[2024-10-08T19:08:56.918-0300]:[WARNING]:[app]:This is a warning example`
80
+
81
+
82
+
83
+
84
+
85
+ # SizeRotatingLog
86
+ + Setup Logging
87
+ + Logs will rotate based on the file size using the `maxmbytes` variable
88
+ + Rotated logs will have a sequence number starting from 1: `app.log_1.gz, app.log_2.gz`
89
+ + Logs will be deleted based on the `daystokeep` variable, defaults to 30
90
+ ```python
91
+ from pythonLogs import SizeRotatingLog
92
+ logger = SizeRotatingLog(
93
+ level="debug",
94
+ name="app",
95
+ directory="/app/logs",
96
+ filenames=["main.log", "app1.log"],
97
+ maxmbytes=5,
98
+ daystokeep=7,
99
+ timezone="America/Chicago",
100
+ streamhandler=True,
101
+ showlocation=False
102
+ ).init()
103
+ logger.warning("This is a warning example")
104
+ ```
105
+ #### Example of output
106
+ `[2024-10-08T19:08:56.918-0500]:[WARNING]:[app]:This is a warning example`
107
+
108
+
109
+
110
+
111
+
112
+ # TimedRotatingLog
113
+ + Setup Logging
114
+ + Logs will rotate based on `when` variable to a `.gz` file, defaults to `midnight`
115
+ + Rotated log will have the sufix variable on its name: `app_20240816.log.gz`
116
+ + Logs will be deleted based on the `daystokeep` variable, defaults to 30
117
+ + Current 'when' events supported:
118
+ + midnight — roll over at midnight
119
+ + W{0-6} - roll over on a certain day; 0 - Monday
120
+ ```python
121
+ from pythonLogs import TimedRotatingLog
122
+ logger = TimedRotatingLog(
123
+ level="debug",
124
+ name="app",
125
+ directory="/app/logs",
126
+ filenames=["main.log", "app2.log"],
127
+ when="midnight",
128
+ daystokeep=7,
129
+ timezone="UTC",
130
+ streamhandler=True,
131
+ showlocation=False
132
+ ).init()
133
+ logger.warning("This is a warning example")
134
+ ```
135
+ #### Example of output
136
+ `[2024-10-08T19:08:56.918-0000]:[WARNING]:[app]:This is a warning example`
137
+
138
+
139
+
140
+
141
+
142
+ ## Env Variables (Optional)
143
+ ```
144
+ LOG_LEVEL=DEBUG
145
+ LOG_TIMEZONE=America/Chicago
146
+ LOG_ENCODING=UTF-8
147
+ LOG_APPNAME=app
148
+ LOG_FILENAME=app.log
149
+ LOG_DIRECTORY=/app/logs
150
+ LOG_DAYS_TO_KEEP=30
151
+ LOG_STREAM_HANDLER=True
152
+ LOG_SHOW_LOCATION=False
153
+ LOG_DATE_FORMAT=%Y-%m-%dT%H:%M:%S
154
+
155
+ # SizeRotatingLog
156
+ LOG_MAX_FILE_SIZE_MB=10
157
+
158
+ # TimedRotatingLog
159
+ LOG_ROTATE_WHEN=midnight
160
+ LOG_ROTATE_AT_UTC=True
161
+ ```
162
+
163
+
164
+
165
+
166
+ # Source Code
167
+ ### Build
168
+ ```shell
169
+ poetry build -f wheel
170
+ ```
171
+
172
+
173
+
174
+ # Run Tests and Get Coverage Report using Poe
175
+ ```shell
176
+ poetry update --with test
177
+ poe tests
178
+ ```
179
+
180
+
181
+
182
+ # License
183
+ Released under the [MIT License](LICENSE)
184
+
185
+
186
+
187
+
188
+ # Buy me a cup of coffee
189
+ + [GitHub Sponsor](https://github.com/sponsors/ddc)
190
+ + [ko-fi](https://ko-fi.com/ddcsta)
191
+ + [Paypal](https://www.paypal.com/ncp/payment/6G9Z78QHUD4RJ)
192
+
@@ -0,0 +1,11 @@
1
+ pythonLogs/.env.example,sha256=PKCAXeBtgyXXJgr96grR4Uyql87-59mUw0KXkf_dCGY,326
2
+ pythonLogs/__init__.py,sha256=6LJlbq-Mw0gDgGbtZUMOYkjLqPk1vN7n4hCOAMyDHRo,1301
3
+ pythonLogs/basic_log.py,sha256=f41CdpK7BSolxXCcSgecHCqIzymFUAXYKA7dSTulyvw,1229
4
+ pythonLogs/log_utils.py,sha256=UDkYgqHK0v7EzZudWa3sslcLF9eYPoZ9ZfMZVZVt9fw,7413
5
+ pythonLogs/settings.py,sha256=k9DrTfNaDcnGfG8k-czkGtuBKe7SxQjziABaGzB9GT0,1459
6
+ pythonLogs/size_rotating.py,sha256=kf1Za1OSyxN9Bge81dNF620WtaiG2g9moqTIiRkI_Yk,3980
7
+ pythonLogs/timed_rotating.py,sha256=4SHTsWrPM7yiqIJVM9PPf67N1aJlWhCunwGz3ygWD-w,3399
8
+ pythonlogs-3.0.12.dist-info/LICENSE,sha256=3fEBeO1ARuqTVV1QJzrGVVmXZz_okGweH0a-MeA0Qpc,1068
9
+ pythonlogs-3.0.12.dist-info/METADATA,sha256=VvJX2ZFRatoMdGMW7FOxX7X-ZMICuHRHbe0qi8h_tbo,5849
10
+ pythonlogs-3.0.12.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
11
+ pythonlogs-3.0.12.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any