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.
- pyessential-0.1.0/LICENSE +21 -0
- pyessential-0.1.0/MANIFEST.in +2 -0
- pyessential-0.1.0/PKG-INFO +45 -0
- pyessential-0.1.0/README.md +25 -0
- pyessential-0.1.0/pyessential/__init__.py +16 -0
- pyessential-0.1.0/pyessential/decorators/__init__.py +3 -0
- pyessential-0.1.0/pyessential/decorators/timer.py +13 -0
- pyessential-0.1.0/pyessential/generators.py +31 -0
- pyessential-0.1.0/pyessential/logger.py +265 -0
- pyessential-0.1.0/pyessential.egg-info/PKG-INFO +45 -0
- pyessential-0.1.0/pyessential.egg-info/SOURCES.txt +18 -0
- pyessential-0.1.0/pyessential.egg-info/dependency_links.txt +1 -0
- pyessential-0.1.0/pyessential.egg-info/top_level.txt +1 -0
- pyessential-0.1.0/pyproject.toml +3 -0
- pyessential-0.1.0/setup.cfg +32 -0
- pyessential-0.1.0/setup.py +6 -0
- pyessential-0.1.0/tests/test_decorators.py +23 -0
- pyessential-0.1.0/tests/test_generators.py +59 -0
- pyessential-0.1.0/tests/test_logger.py +182 -0
|
@@ -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,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
|
+
[](https://pypi.org/project/pyessential/)
|
|
28
|
+
[](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
|
+
[](https://pypi.org/project/pyessential/)
|
|
8
|
+
[](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,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
|
+
[](https://pypi.org/project/pyessential/)
|
|
28
|
+
[](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
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyessential
|
|
@@ -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,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)
|