pyessential 0.1.0__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Abhishek Kumar
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,2 @@
1
+ include README.md LICENSE
2
+ recursive-include tests *.py
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyessential
3
+ Version: 0.1.0
4
+ Summary: A growing collection of Python utilities and decorators.
5
+ Home-page: https://github.com/Abhi39054/PyEssential.git
6
+ Author: Abhishek Kumar
7
+ Author-email: abhishekkumar39054@gmail.com
8
+ Maintainer: Abhishek Kumar
9
+ License: MIT
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # 🐍 PyEssential
22
+
23
+ **A growing collection of Python utilities and decorators to simplify common development tasks.**
24
+
25
+ ---
26
+
27
+ [![PyPI version](https://img.shields.io/pypi/v/pyessential.svg)](https://pypi.org/project/pyessential/)
28
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
29
+
30
+ ## ✨ Features
31
+
32
+ `PyEssential` aims to be the go-to library for small, reusable tools that you often find yourself writing from scratch.
33
+
34
+ * **Performance:** Includes the ready-to-use `@timeit` decorator for simple function benchmarking.
35
+ * **Code Clarity:** Provides utility functions for common tasks like string manipulation (e.g., casing). (Coming soon!)
36
+ * **Scalability:** Structured for easy addition of new utility groups (e.g., `networking`, `file_io`, `validation`).
37
+
38
+ ## 🚀 Installation
39
+
40
+ `PyEssential` is currenlty still in development stage. Once development is completed, It will be published in the Pypi for the community to use.
41
+
42
+
43
+ ```bash
44
+ pip install pyessential
45
+ ```
@@ -0,0 +1,25 @@
1
+ # 🐍 PyEssential
2
+
3
+ **A growing collection of Python utilities and decorators to simplify common development tasks.**
4
+
5
+ ---
6
+
7
+ [![PyPI version](https://img.shields.io/pypi/v/pyessential.svg)](https://pypi.org/project/pyessential/)
8
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
9
+
10
+ ## ✨ Features
11
+
12
+ `PyEssential` aims to be the go-to library for small, reusable tools that you often find yourself writing from scratch.
13
+
14
+ * **Performance:** Includes the ready-to-use `@timeit` decorator for simple function benchmarking.
15
+ * **Code Clarity:** Provides utility functions for common tasks like string manipulation (e.g., casing). (Coming soon!)
16
+ * **Scalability:** Structured for easy addition of new utility groups (e.g., `networking`, `file_io`, `validation`).
17
+
18
+ ## 🚀 Installation
19
+
20
+ `PyEssential` is currenlty still in development stage. Once development is completed, It will be published in the Pypi for the community to use.
21
+
22
+
23
+ ```bash
24
+ pip install pyessential
25
+ ```
@@ -0,0 +1,16 @@
1
+
2
+ from .decorators import timeit
3
+ from .generators import generate_random_int, generate_secret_key
4
+ from .logger import Logger
5
+
6
+ # Version and Author Info
7
+ __version__ = "0.1.0"
8
+ __author__ = "Abhishek Kumar"
9
+
10
+
11
+ __all__ = [
12
+ "timeit",
13
+ "generate_random_int",
14
+ "generate_secret_key",
15
+ "Logger",
16
+ ]
@@ -0,0 +1,3 @@
1
+ from .timer import timeit
2
+
3
+ __all__ = ["timeit"]
@@ -0,0 +1,13 @@
1
+ import time
2
+ from functools import wraps
3
+
4
+ def timeit(func):
5
+ """Decorator to measure and print function execution time."""
6
+ @wraps(func)
7
+ def wrapper(*args, **kwargs):
8
+ start_time = time.time()
9
+ result = func(*args, **kwargs)
10
+ execution_time = time.time() - start_time
11
+ print(f"[{func.__name__}] executed in {execution_time:.4f} seconds.")
12
+ return result
13
+ return wrapper
@@ -0,0 +1,31 @@
1
+ """
2
+ Name: pyessential.generators
3
+ Description: A module providing utility functions for generating random data.
4
+ Author: Abhishek Kumar
5
+ """
6
+
7
+ import secrets
8
+
9
+ def generate_random_int(min_value=0, max_value=100):
10
+ """Generate a random integer within a specified range.
11
+
12
+ Args:
13
+ min_value (int): The minimum value of the range. Default is 0.
14
+ max_value (int): The maximum value of the range. Default is 100.
15
+
16
+ Returns:
17
+ int: A randomly generated integer within the specified range.
18
+ """
19
+ return secrets.randbelow(max_value - min_value + 1) + min_value
20
+
21
+
22
+ def generate_secret_key(length=32):
23
+ """Generate a secure random secret key.
24
+
25
+ Args:
26
+ length (int): The length of the secret key. Default is 32.
27
+
28
+ Returns:
29
+ str: A securely generated random secret key.
30
+ """
31
+ return secrets.token_hex(length)
@@ -0,0 +1,265 @@
1
+ """
2
+ Logger utility for application logging.
3
+
4
+ This module provides the 'Logger' class, a robust logging utility that
5
+ integrates with Python's standard 'logging' library. It configures
6
+ three separate loggers/files for STDIN (ingress), STDOUT (general/info),
7
+ and STDERR (errors/critical) messages, with support for
8
+ timed log rotation and console output.
9
+
10
+ """
11
+
12
+ import os
13
+ import sys
14
+ import traceback
15
+ import logging
16
+ from datetime import datetime
17
+ from logging.handlers import TimedRotatingFileHandler
18
+
19
+
20
+ class Logger:
21
+ """
22
+ A logging utility class for managing application logs with separate streams.
23
+
24
+ It sets up three distinct, rotation-enabled log files:
25
+ 1. STDIN (for input/ingress events)
26
+ 2. STDOUT (for general debug/info/warning messages)
27
+ 3. STDERR (for error/critical messages and exceptions)
28
+
29
+ The class is designed to be used as a context manager to ensure log closure.
30
+
31
+ Example:
32
+ >>> log = Logger(log_name="my_app", enable_console=True)
33
+ >>> log.info("Application started.")
34
+ >>> log.error("Something went wrong!")
35
+
36
+ or
37
+
38
+ >>> with Logger(log_name="my_app", enable_console=True) as log:
39
+ ... log.info("Application started.")
40
+ ... log.error("Something went wrong!")
41
+ ...
42
+ """
43
+
44
+ _LEVEL_MAP = {
45
+ "debug":logging.DEBUG,
46
+ "info": logging.INFO,
47
+ "warning": logging.WARNING,
48
+ "error": logging.ERROR,
49
+ "critical": logging.CRITICAL,
50
+ }
51
+
52
+ def __init__(
53
+ self,
54
+ log_dir="../logs",
55
+ log_name="project",
56
+ level="debug",
57
+ when="midnight",
58
+ interval=1,
59
+ backup_count=7,
60
+ enable_console=False,
61
+ fmt="%(asctime)s.%(msecs)03d - %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s",
62
+ datefmt="%Y-%m-%d %H:%M:%S",
63
+ encoding="utf-8"
64
+ ):
65
+ """
66
+ Args:
67
+ log_dir (str) : Directory to store log files. Default is '../logs'.
68
+ log_name (str) : Base name for the three log files (<log_name>_stdin.log, <log_name>_stdout.log, <log_name>_stderr.log).
69
+ level (str) : Logging level. Default is 'debug'.
70
+ when (str) : Time interval for log rotation. Default is 'midnight'.
71
+ interval (int) : Interval for log rotation. Default is 1.
72
+ backup_count (int) : Number of backup log files to keep. Default is 7.
73
+ enable_console (bool) : Enable console logging. Default is False.
74
+ fmt (str) : Log message format. Default is "%(asctime)s.%(msecs)03d - %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s".
75
+ datefmt (str) : Date format in logs. Default is "%Y-%m-%d %H:%M:%S".
76
+ encoding (str) : Encoding for log files. Default is "utf-8".
77
+ """
78
+
79
+ self.log_dir = os.path.abspath(log_dir)
80
+ self.log_name = log_name
81
+
82
+ # Filenames (with 'logs' extension as is common; can be changed if needed)
83
+ self.stdin_path = os.path.join(self.log_dir, f"{self.log_name}_stdin.log")
84
+ self.stdout_path = os.path.join(self.log_dir, f"{self.log_name}_stdout.log")
85
+ self.error_path = os.path.join(self.log_dir, f"{self.log_name}_error.log")
86
+
87
+ # Ensure Directory
88
+ os.makedirs(self.log_dir, exist_ok=True)
89
+
90
+ # Common formatter
91
+ formatter = logging.Formatter(fmt=fmt, datefmt=datefmt)
92
+
93
+ # Create three dedicated loggers so routing is explicit and simple
94
+ self._stdin_logger = self._build_logger(
95
+ name=f"{self.log_name}.stdin",
96
+ filepath=self.stdin_path,
97
+ level=level,
98
+ when=when,
99
+ interval=interval,
100
+ backup_count=backup_count,
101
+ formatter=formatter,
102
+ encoding=encoding,
103
+ enable_console=False, # stdin is typically not logged to console
104
+ )
105
+
106
+ self._stdout_logger = self._build_logger(
107
+ name=f"{self.log_name}.stdout",
108
+ filepath=self.stdout_path,
109
+ level=level,
110
+ when=when,
111
+ interval=interval,
112
+ backup_count=backup_count,
113
+ formatter=formatter,
114
+ encoding=encoding,
115
+ enable_console=enable_console,
116
+ console_to_stderr=False # stdout -> sys.stdout
117
+ )
118
+
119
+ self._error_logger = self._build_logger(
120
+ name=f"{self.log_name}.error",
121
+ filepath=self.error_path,
122
+ level=level,
123
+ when=when,
124
+ interval=interval,
125
+ backup_count=backup_count,
126
+ formatter=formatter,
127
+ encoding=encoding,
128
+ enable_console=enable_console,
129
+ console_to_stderr=True # stderr -> sys.stderr
130
+ )
131
+
132
+ # --------------- Public Methods ---------------
133
+ def debug(self, msg, *args, exc_info=False, stacklevel=2, **kwargs):
134
+ """Log a debug message to stdout logger."""
135
+ self._stdout_logger.debug(msg, *args, exc_info=exc_info, stacklevel=stacklevel, **kwargs)
136
+
137
+ def info(self, msg, *args, exc_info=False, stacklevel=2, **kwargs):
138
+ """Log an info message to stdout logger."""
139
+ self._stdout_logger.info(msg, *args, exc_info=exc_info, stacklevel=stacklevel, **kwargs)
140
+
141
+ def warning(self, msg, *args, exc_info=False, stacklevel=2, **kwargs):
142
+ """Log a warning message to stdout logger."""
143
+ self._stdout_logger.warning(msg, *args, exc_info=exc_info, stacklevel=stacklevel, **kwargs)
144
+
145
+ def error(self, msg, *args, exc_info=False, stacklevel=2, **kwargs):
146
+ """Log an error message to error logger."""
147
+ self._error_logger.error(msg, *args, exc_info=exc_info, stacklevel=stacklevel, **kwargs)
148
+
149
+ def critical(self, msg, *args, exc_info=False, stacklevel=2, **kwargs):
150
+ """Log a critical message to error logger."""
151
+ self._error_logger.critical(msg, *args, exc_info=exc_info, stacklevel=stacklevel, **kwargs)
152
+
153
+ def exception(self, msg, *args, exc_info=False, stacklevel=2, **kwargs):
154
+ """Log an exception message to error logger."""
155
+ self._error_logger.error(msg, *args, exc_info=True, stacklevel=stacklevel, **kwargs)
156
+
157
+ def stdin(self, msg, *args, exc_info=False, stacklevel=2, **kwargs):
158
+ """Explicitly record 'input' or ingress events."""
159
+ self._stdin_logger.info(msg, *args, exc_info=exc_info, stacklevel=stacklevel, **kwargs)
160
+
161
+ def log_error(self, err: Exception):
162
+ """Log an exception with traceback to the error logger.
163
+
164
+ Args:
165
+ err (Exception): The exception instance to log.
166
+ """
167
+
168
+ exc_type, exc_obj, exc_tb = sys.exc_info()
169
+
170
+ if exc_tb is None:
171
+ # It means getting called outputside except block.
172
+ filename = "<unknown>"
173
+ lineno = -1
174
+ func_name = "<unknown>"
175
+ else:
176
+ filename = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
177
+ lineno = exc_tb.tb_lineno
178
+ func_name = traceback.extract_tb(exc_tb, 1)[0][2]
179
+
180
+ e_type = err.__class__.__name__
181
+
182
+ data = {
183
+ "exception_type": e_type,
184
+ "exception_message": str(err),
185
+ "filename": filename,
186
+ "lineno": lineno,
187
+ "function": func_name,
188
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
189
+ "traceback": traceback.format_exc()
190
+ }
191
+
192
+ # log to stderr file
193
+ self.error(data, exc_info=True, stacklevel=2)
194
+ return data
195
+
196
+ def close(self):
197
+ """
198
+ Flush and close all handlers explicitly.
199
+ """
200
+
201
+ for lg in (self._stdin_logger, self._stdout_logger, self._error_logger):
202
+ for h in list(lg.handlers):
203
+ try:
204
+ h.flush()
205
+ except Exception:
206
+ pass
207
+
208
+ try:
209
+ h.close()
210
+ except Exception:
211
+ pass
212
+
213
+ lg.removeHandler(h)
214
+
215
+ def __enter__(self):
216
+ return self
217
+
218
+ def __exit__(self, exc_type, exc_value, traceback):
219
+ self.close()
220
+
221
+
222
+ # --------------- Internals ---------------
223
+ @classmethod
224
+ def _build_logger(
225
+ cls,
226
+ name,
227
+ filepath,
228
+ level,
229
+ when,
230
+ interval,
231
+ backup_count,
232
+ formatter,
233
+ encoding,
234
+ enable_console=False,
235
+ console_to_stderr=False
236
+ ):
237
+
238
+ log_level = cls._LEVEL_MAP.get(level.lower(), logging.DEBUG)
239
+ logger = logging.getLogger(name)
240
+ logger.setLevel(log_level)
241
+ logger.handlers.clear() # Avoid duplicate handlers on re-initialization
242
+ logger.propagate = False # Prevent log messages from being propagated to the root logger
243
+
244
+ # Timed Rotating File Handler
245
+ fh = TimedRotatingFileHandler(
246
+ filename=filepath,
247
+ when=when,
248
+ interval=interval,
249
+ backupCount=backup_count,
250
+ encoding=encoding,
251
+ delay=True,
252
+ utc=False
253
+ )
254
+
255
+ fh.setFormatter(formatter)
256
+ logger.addHandler(fh)
257
+
258
+ if enable_console:
259
+ stream = sys.stderr if console_to_stderr else sys.stdout
260
+ sh = logging.StreamHandler(stream)
261
+ sh.setFormatter(formatter)
262
+ sh.setLevel(log_level)
263
+ logger.addHandler(sh)
264
+
265
+ return logger
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyessential
3
+ Version: 0.1.0
4
+ Summary: A growing collection of Python utilities and decorators.
5
+ Home-page: https://github.com/Abhi39054/PyEssential.git
6
+ Author: Abhishek Kumar
7
+ Author-email: abhishekkumar39054@gmail.com
8
+ Maintainer: Abhishek Kumar
9
+ License: MIT
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # 🐍 PyEssential
22
+
23
+ **A growing collection of Python utilities and decorators to simplify common development tasks.**
24
+
25
+ ---
26
+
27
+ [![PyPI version](https://img.shields.io/pypi/v/pyessential.svg)](https://pypi.org/project/pyessential/)
28
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
29
+
30
+ ## ✨ Features
31
+
32
+ `PyEssential` aims to be the go-to library for small, reusable tools that you often find yourself writing from scratch.
33
+
34
+ * **Performance:** Includes the ready-to-use `@timeit` decorator for simple function benchmarking.
35
+ * **Code Clarity:** Provides utility functions for common tasks like string manipulation (e.g., casing). (Coming soon!)
36
+ * **Scalability:** Structured for easy addition of new utility groups (e.g., `networking`, `file_io`, `validation`).
37
+
38
+ ## 🚀 Installation
39
+
40
+ `PyEssential` is currenlty still in development stage. Once development is completed, It will be published in the Pypi for the community to use.
41
+
42
+
43
+ ```bash
44
+ pip install pyessential
45
+ ```
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ setup.cfg
6
+ setup.py
7
+ pyessential/__init__.py
8
+ pyessential/generators.py
9
+ pyessential/logger.py
10
+ pyessential.egg-info/PKG-INFO
11
+ pyessential.egg-info/SOURCES.txt
12
+ pyessential.egg-info/dependency_links.txt
13
+ pyessential.egg-info/top_level.txt
14
+ pyessential/decorators/__init__.py
15
+ pyessential/decorators/timer.py
16
+ tests/test_decorators.py
17
+ tests/test_generators.py
18
+ tests/test_logger.py
@@ -0,0 +1 @@
1
+ pyessential
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,32 @@
1
+ [metadata]
2
+ name = pyessential
3
+ version = 0.1.0
4
+ author = Abhishek Kumar
5
+ author_email = abhishekkumar39054@gmail.com
6
+ maintainer = Abhishek Kumar
7
+ description = A growing collection of Python utilities and decorators.
8
+ long_description = file: README.md
9
+ long_description_content_type = text/markdown
10
+ url = https://github.com/Abhi39054/PyEssential.git
11
+ license = MIT
12
+ classifiers =
13
+ Programming Language :: Python :: 3
14
+ Programming Language :: Python :: 3.8
15
+ Programming Language :: Python :: 3.9
16
+ Programming Language :: Python :: 3.10
17
+ License :: OSI Approved :: MIT License
18
+ Operating System :: OS Independent
19
+
20
+ [options]
21
+ packages = find:
22
+ install_requires =
23
+ python_requires = >=3.8
24
+
25
+ [options.packages.find]
26
+ exclude =
27
+ tests*
28
+
29
+ [egg_info]
30
+ tag_build =
31
+ tag_date = 0
32
+
@@ -0,0 +1,6 @@
1
+
2
+ import setuptools
3
+
4
+ if __name__ == '__main__':
5
+ # This command automatically reads configuration from setup.cfg
6
+ setuptools.setup()
@@ -0,0 +1,23 @@
1
+ # tests/test_decorators.py
2
+ import time
3
+ from pyessential.decorators import timeit
4
+
5
+ # Example: Simple test to ensure the function returns correctly
6
+ def test_time_it_returns_correct_value():
7
+ @timeit
8
+ def add(a, b):
9
+ return a + b
10
+
11
+ # Check if the result is correct
12
+ assert add(2, 3) == 5
13
+
14
+ # Example: Test with some time delay
15
+ def test_time_it_execution_time(capsys):
16
+ @timeit
17
+ def sleep_a_bit():
18
+ time.sleep(0.01)
19
+
20
+ sleep_a_bit()
21
+
22
+ captured = capsys.readouterr()
23
+ assert "executed in" in captured.out
@@ -0,0 +1,59 @@
1
+ # tests/test_generators.py
2
+ import time
3
+ from pyessential.generators import generate_random_int, generate_secret_key
4
+
5
+
6
+ # Test to ensure the generated integer is within the specified range
7
+ def test_generate_random_int_within_range():
8
+ min_value = 0
9
+ max_value = 100
10
+ result = generate_random_int(min_value, max_value)
11
+ assert min_value <= result <= max_value, f"Result {result} is not within range {min_value}-{max_value}"
12
+
13
+ # Test to check the default range
14
+ def test_generate_random_int_default_range():
15
+ result = generate_random_int()
16
+ assert 0 <= result <= 100, f"Result {result} is not within default range 0-100"
17
+
18
+ # Test to check multiple calls produce different results
19
+ def test_generate_random_int_multiple_calls():
20
+ results = {generate_random_int() for _ in range(10)}
21
+ assert len(results) > 1, "Multiple calls to generate_random_int produced the same result"
22
+
23
+ # Test to check edge case where min_value equals max_value
24
+ def test_generate_random_int_edge_case():
25
+ min_value = 10
26
+ max_value = 10
27
+ result = generate_random_int(min_value, max_value)
28
+ assert result == min_value, f"Result {result} is not equal to the edge case value {min_value}"
29
+
30
+ # Test to check negative range
31
+ def test_generate_random_int_negative_range():
32
+ min_value = -10
33
+ max_value = -5
34
+ result = generate_random_int(min_value, max_value)
35
+ assert min_value <= result <= max_value, f"Result {result} is not within range {min_value}-{max_value}"
36
+
37
+ # Test to check large range
38
+ def test_generate_random_int_large_range():
39
+ min_value = 1000
40
+ max_value = 2000
41
+ result = generate_random_int(min_value, max_value)
42
+ assert min_value <= result <= max_value, f"Result {result} is not within range {min_value}-{max_value}"
43
+
44
+ # Test for generate secret key
45
+ def test_generate_secret_key_length():
46
+ length = 16
47
+ result = generate_secret_key(length)
48
+ assert len(result) == length * 2, f"Secret key length {len(result)} does not match expected length {length * 2}"
49
+
50
+ def test_generate_secret_key_different_calls():
51
+ result1 = generate_secret_key()
52
+ result2 = generate_secret_key()
53
+ assert result1 != result2, "Multiple calls to generate_secret_key produced the same result"
54
+
55
+ def test_generate_secret_key_default_length():
56
+ default_length = 32
57
+ result = generate_secret_key()
58
+ assert len(result) == default_length * 2 , f"Default secret key length {len(result)} does not match expected length {default_length * 2}"
59
+
@@ -0,0 +1,182 @@
1
+ import pytest
2
+ import os
3
+ import shutil
4
+ import re
5
+ import json
6
+ import sys
7
+ import logging
8
+ from pyessential.logger import Logger # Assuming the main Logger class is saved in 'logger.py'
9
+
10
+ # --- Configuration ---
11
+ TEMP_LOG_DIR = "./temp_pytest_logs"
12
+ TEST_LOG_NAME = "test_app"
13
+
14
+ def get_log_paths(log_dir, log_name):
15
+ """Utility function to generate expected log paths."""
16
+ return {
17
+ 'stdin': os.path.join(log_dir, f"{log_name}_stdin.log"),
18
+ 'stdout': os.path.join(log_dir, f"{log_name}_stdout.log"),
19
+ 'error': os.path.join(log_dir, f"{log_name}_error.log"),
20
+ }
21
+
22
+ def get_log_content(filepath):
23
+ """Utility function to read content from a log file."""
24
+ if not os.path.exists(filepath):
25
+ return ""
26
+ with open(filepath, 'r', encoding='utf-8') as f:
27
+ return f.read()
28
+
29
+ @pytest.fixture(scope="session")
30
+ def log_setup():
31
+ """
32
+ Pytest fixture to create and clean up the temporary log directory.
33
+ This runs once per test session.
34
+ """
35
+ # Setup
36
+ if os.path.exists(TEMP_LOG_DIR):
37
+ shutil.rmtree(TEMP_LOG_DIR)
38
+ os.makedirs(TEMP_LOG_DIR)
39
+
40
+ paths = get_log_paths(TEMP_LOG_DIR, TEST_LOG_NAME)
41
+
42
+ # Yield the log paths to the tests
43
+ yield paths
44
+
45
+ # Teardown
46
+ if os.path.exists(TEMP_LOG_DIR):
47
+ shutil.rmtree(TEMP_LOG_DIR)
48
+
49
+ @pytest.fixture(autouse=True)
50
+ def clean_logs(log_setup):
51
+ """
52
+ Pytest fixture to clean up individual log files before each test.
53
+ This mimics the setUp() method in unittest.
54
+ """
55
+ for path in log_setup.values():
56
+ if os.path.exists(path):
57
+ os.remove(path)
58
+
59
+ # Yield control back to the test
60
+ yield
61
+
62
+ # Use a test class to group related tests
63
+ class TestLoggerPytest:
64
+ """Test suite for the Logger class using Pytest conventions."""
65
+
66
+ def test_01_initialization_and_path_check(self, log_setup):
67
+ """Test if the Logger initializes correctly and reports the correct file paths."""
68
+ with Logger(log_dir=TEMP_LOG_DIR, log_name=TEST_LOG_NAME) as log:
69
+ # Check path properties
70
+ assert log.stdin_path.endswith(f"{TEST_LOG_NAME}_stdin.log")
71
+ assert log.stdout_path.endswith(f"{TEST_LOG_NAME}_stdout.log")
72
+ assert log.error_path.endswith(f"{TEST_LOG_NAME}_error.log")
73
+
74
+ # Re-initialize to check internal logger handlers
75
+ log_check = Logger(log_dir=TEMP_LOG_DIR, log_name=TEST_LOG_NAME)
76
+ # Check that only one FileHandler is present by default
77
+ assert len(log_check._stdout_logger.handlers) == 1
78
+ log_check.close()
79
+
80
+
81
+ def test_02_message_routing_and_content(self, log_setup):
82
+ """Test if messages are routed correctly to STDOUT, STDIN, and STDERR logs."""
83
+ message_info = "The process is running."
84
+ message_error = "Failed to connect to service."
85
+ message_stdin = "User input received."
86
+
87
+ with Logger(log_dir=TEMP_LOG_DIR, log_name=TEST_LOG_NAME, level="DEBUG") as log:
88
+ log.debug("A detailed debug message.")
89
+ log.info(message_info)
90
+ log.warning("Potential issue detected.")
91
+
92
+ log.error(message_error)
93
+ log.critical("System shutdown imminent.")
94
+
95
+ log.stdin(message_stdin)
96
+
97
+ # 1. Check STDOUT Log (debug, info, warning)
98
+ stdout_content = get_log_content(log_setup['stdout'])
99
+ assert message_info in stdout_content
100
+ assert message_error not in stdout_content
101
+ assert message_stdin not in stdout_content # STDIN messages go to separate file
102
+
103
+ # 2. Check STDERR Log (error, critical)
104
+ error_content = get_log_content(log_setup['error'])
105
+ assert message_error in error_content
106
+ assert "System shutdown imminent." in error_content
107
+ assert message_info not in error_content
108
+
109
+ # 3. Check STDIN Log (stdin)
110
+ stdin_content = get_log_content(log_setup['stdin'])
111
+ assert message_stdin in stdin_content
112
+ assert "INFO" in stdin_content # stdin uses info level internally
113
+ assert message_error not in stdin_content
114
+
115
+ def test_03_exception_and_traceback_logging(self, log_setup):
116
+ """Test logging of exceptions using both exception() and log_error()."""
117
+
118
+ with Logger(log_dir=TEMP_LOG_DIR, log_name=TEST_LOG_NAME) as log:
119
+ try:
120
+ raise KeyError("Test key error for structured logging.")
121
+ except KeyError as e:
122
+ # 1. Test log.exception()
123
+ log.exception("Handled error during dictionary lookup.")
124
+
125
+ # 2. Test log.log_error(e)
126
+ structured_data = log.log_error(e)
127
+
128
+ error_content = get_log_content(log_setup['error'])
129
+
130
+ # 1. Check for standard traceback (from log.exception())
131
+ assert "Traceback (most recent call last):" in error_content
132
+ assert "KeyError: 'Test key error for structured logging.'" in error_content
133
+ assert "Handled error during dictionary lookup." in error_content
134
+
135
+ # 2. Check for structured log data (from log.log_error). This should be a string representation of a dict.
136
+ # Since log_error logs the whole dict object, we check for stringified parts.
137
+ assert "'exception_type': 'KeyError'" in error_content
138
+ assert "'lineno':" in error_content
139
+
140
+ # 3. Check returned structured data integrity
141
+ assert structured_data["exception_type"] == "KeyError"
142
+ assert "Test key error for structured logging." in structured_data["exception_message"]
143
+
144
+ def test_04_console_handler_presence(self):
145
+ """Test if StreamHandlers are correctly added when enable_console is True."""
146
+ # Use a temporary logger instance for this test
147
+ log_console = Logger(log_dir=TEMP_LOG_DIR, log_name="console_test", enable_console=True)
148
+
149
+ # STDOUT logger should have FileHandler and StreamHandler (2 total)
150
+ assert len(log_console._stdout_logger.handlers) == 2
151
+ # STDERR logger should have FileHandler and StreamHandler (2 total)
152
+ assert len(log_console._error_logger.handlers) == 2
153
+ # STDIN logger should only have FileHandler (1 total)
154
+ assert len(log_console._stdin_logger.handlers) == 1
155
+
156
+ # Verify the console handler streams
157
+ sh_stdout = [h for h in log_console._stdout_logger.handlers if isinstance(h, logging.StreamHandler)]
158
+ assert sh_stdout[1].stream == sys.stdout
159
+
160
+ sh_error = [h for h in log_console._error_logger.handlers if isinstance(h, logging.StreamHandler)]
161
+ assert sh_error[1].stream == sys.stderr
162
+
163
+ log_console.close()
164
+
165
+ def test_05_context_manager_cleanup(self, log_setup):
166
+ """Test if the __exit__ method calls close() and cleans up handlers."""
167
+ log = Logger(log_dir=TEMP_LOG_DIR, log_name="cleanup_test")
168
+
169
+ # Handlers exist before exit
170
+ assert len(log._stdout_logger.handlers) > 0
171
+
172
+ # Enter and immediately exit the context manager
173
+ with log:
174
+ log.info("Testing cleanup.")
175
+
176
+ # Check if handlers were removed after __exit__ (which calls close)
177
+ assert len(log._stdout_logger.handlers) == 0
178
+ assert len(log._error_logger.handlers) == 0
179
+ assert len(log._stdin_logger.handlers) == 0
180
+
181
+ # Check if files were written
182
+ assert os.path.exists(log.stdout_path)