Skip to content

Commit fe8ca24

Browse files
Merge pull request #1 from MurdoMaclachlan/dev
Release 0.2.0
2 parents d689cf9 + 9e73d75 commit fe8ca24

File tree

7 files changed

+331
-239
lines changed

7 files changed

+331
-239
lines changed

CHANGELOG.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ Usage of smooth-logger is, as it should be, quite simple.
1717
The `Logger` model provides a number of methods for your use:
1818

1919
- `Logger.clean()` erases all log entries currently in memory.
20-
- `Logger.define_output_path()` is primarily intended as an internal method; it detects the user's operating system and home folder and, using the provided program name and creates a log folder in the appropriate location (`~/.config/{program_name}` on Linux and macOS, `AppData\Roaming\{program_name}` on Windows).
2120
- `Logger.get()` allows you to retrieve either the most recent log entry or all log entries, optionally filtered by scope.
2221
- `Logger.get_time()` returns the full date & time, or optionally just the date, in ISO-8601 formatting.
2322
- `Logger.init_bar()` initialises the `ProgressBar` model imported from the `smooth_progress` dependency.
@@ -41,10 +40,10 @@ The scopes available, along with their default values and suggested use cases, a
4140

4241
Here is a simple example showing the initialisation of the logger:
4342

44-
```
43+
```py
4544
import smooth_logger
4645

47-
Log = smooth_logger.Logger("Program")
46+
Log = smooth_logger.Logger("Example", "~/.config/example")
4847
Log.new("This is a log message!", "INFO")
4948
```
5049

@@ -74,4 +73,8 @@ A roadmap of planned future improvements and features:
7473
to temporarily enable debug statements. This feature would probably see the most use from custom scopes.
7574

7675

77-
- Add an optional argument `notify: bool` to `Logger.new()` to allow log entries to be created and notified in one statement, rather than the two currently required.
76+
- Add an optional argument `notify: bool` to `Logger.new()` to allow log entries to be created and notified in one statement, rather than the two currently required.
77+
78+
- Rework `Logger.get()` to allow passing of a specific number of log values to be fetched. If these values exceed the number in the log, all matching log values should be returned, and a warning should be issued (but not returned).
79+
80+
- Possibly replace some internal warnings with Exceptions so they can be more easily-handled by end-user programs.

setup.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,16 @@ def readme():
77

88
setup(
99
name="smooth_logger",
10-
version="0.1.0",
10+
version="0.2.0",
1111
author="Murdo Maclachlan",
1212
author_email="[email protected]",
1313
description=(
14-
"A simple logger made primarily for my own personal use. Made from a"
15-
+ " combination of necessity and so much sloth that it overflowed into"
16-
+ " productivity."
14+
"A simple logger made primarily for my own personal use. Made from a combination of"
15+
+ " necessity and so much sloth that it overflowed into productivity."
1716
),
1817
long_description=readme(),
1918
long_description_content_type="text/markdown",
20-
url="https://codeberg.org/MurdoMaclachlan/smooth_logger",
19+
url="https://github.com/MurdoMaclachlan/smooth_logger",
2120
packages=find_packages(),
2221
install_requires=[
2322
"plyer",
@@ -29,6 +28,8 @@ def readme():
2928
"Programming Language :: Python :: 3.8",
3029
"Programming Language :: Python :: 3.9",
3130
"Programming Language :: Python :: 3.10",
31+
"Programming Language :: Python :: 3.11",
32+
"Programming Language :: Python :: 3.12",
3233
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
3334
"Operating System :: OS Independent",
3435
],

smooth_logger/LogEntry.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class LogEntry:
2+
"""
3+
Represents a single log entry, storing its timestamp, scope and message.
4+
"""
5+
def __init__(self: object, message: str, output: bool, scope: str, timestamp: str) -> None:
6+
self.message = message
7+
self.output = output
8+
self.scope = scope
9+
self.timestamp = timestamp
10+
self.rendered = (
11+
f"[{timestamp}] {scope}: {message}"
12+
if scope != "NOSCOPE" else
13+
f"{message}"
14+
)

smooth_logger/Logger.py

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
from datetime import datetime
2+
from os import environ, makedirs
3+
from os.path import expanduser, isdir
4+
from plyer import notification
5+
from smooth_progress import ProgressBar
6+
from time import time
7+
from .LogEntry import LogEntry
8+
9+
from plyer.facades import Notification
10+
from typing import Dict, List, Union
11+
12+
13+
class Logger:
14+
"""
15+
Class for controlling the entirety of logging. The logging works on a scope-based system where
16+
(almost) every message has a defined scope, and the scopes are each associated with a specific
17+
value between 0 and 2 inclusive. The meanings of the values are as follows:
18+
19+
0: disabled, do not print to console or save to log file
20+
1: enabled, print to console but do not save to log file
21+
2: maximum, print to console and save to log file
22+
"""
23+
def __init__(self,
24+
program_name: str,
25+
config_path: str = None,
26+
debug: int = 0,
27+
error: int = 2,
28+
fatal: int = 2,
29+
info: int = 1,
30+
warning: int = 2) -> None:
31+
self.bar: ProgressBar = ProgressBar()
32+
self.__is_empty: bool = True
33+
self.__log: List[LogEntry] = []
34+
self.__notifier: Notification = notification
35+
self.__program_name: str = program_name
36+
self.__scopes: Dict[str, int] = {
37+
"DEBUG": debug, # information for debugging the program
38+
"ERROR": error, # errors the program can recover from
39+
"FATAL": fatal, # errors that mean the program cannot continue
40+
"INFO": info, # general information for the user
41+
"WARNING": warning # things that could cause errors later on
42+
}
43+
self.__write_logs = False
44+
self.__output_path: str = (
45+
self.__define_output_path()
46+
if config_path is None else
47+
f"{config_path}/logs"
48+
)
49+
self.__create_log_folder()
50+
51+
def __create_log_entry(self, message: str, output: bool, scope: str) -> LogEntry:
52+
"""
53+
Creates a new log entry from given settings and appends it to the log.
54+
55+
:param message: the log message
56+
:param output: whether the message should be output to the log file
57+
:param scope: the scope of the message
58+
59+
:returns: the created log entry
60+
"""
61+
entry: LogEntry = LogEntry(message, output, scope, self.__get_time())
62+
self.__log.append(entry)
63+
return entry
64+
65+
def __create_log_folder(self) -> None:
66+
"""
67+
Creates the folder that will contain the log files.
68+
"""
69+
if not isdir(self.__output_path):
70+
print(f"Making path: {self.__output_path}")
71+
makedirs(self.__output_path, exist_ok=True)
72+
73+
def __define_output_path(self) -> str:
74+
"""
75+
Defines the appropriate output path for the log file, automatically detecting the user's
76+
config folder and using the given program name. If the detected operating system is not
77+
supported, exits.
78+
79+
Supported operating systems are: Linux, MacOS, Windows. Users of an unsupported operating
80+
system will have to pass a pre-defined config path of the following format:
81+
82+
{user_config_path}/{name_of_program_config_folder}
83+
84+
On Linux, with a program name of "test", this would format to:
85+
86+
/home/{user}/.config/test
87+
"""
88+
from sys import platform
89+
90+
os: str = "".join(list(platform)[:3])
91+
if os in ["dar", "lin", "win"]:
92+
path: str = (
93+
environ["APPDATA"] + f"\\{self.__program_name}\logs"
94+
if os == "win" else
95+
f"{expanduser('~')}/.config/{self.__program_name}/logs"
96+
)
97+
if not isdir(path):
98+
print(f"INFO: Making path: {path}")
99+
makedirs(path, exist_ok=True)
100+
return path
101+
else:
102+
print(
103+
f"FATAL: Could not automatically create output folder for operating system: {os}."
104+
+ "You will need to manually pass a pre-defined config_path."
105+
)
106+
exit()
107+
108+
def __display_log_entry(self,
109+
entry: LogEntry,
110+
scope: str,
111+
notify: bool,
112+
is_bar: bool,
113+
print_to_console: bool = True) -> None:
114+
"""
115+
Displays a given log entry as appropriate using further given settings.
116+
117+
:param entry: the entry to display
118+
:param scope: the scope of the entry
119+
:param notify: whether to show a desktop notification for the entry
120+
:param is_bar: whether the progress bar is active
121+
:param console: whether the message should be printed to the console
122+
"""
123+
if scope == "NOSCOPE" or (self.__scopes[scope] > 0 and print_to_console):
124+
print(entry.rendered)
125+
if is_bar:
126+
print(self.bar.state, end="\r", flush=True)
127+
if notify:
128+
self.notify(entry.message)
129+
130+
def __get_time(self, method: str = "time") -> str:
131+
"""
132+
Gets the current time and parses it to a human-readable format; either
133+
'YYYY-MM-DD HH:MM:SS' or 'YYYY-MM-DD'.
134+
135+
:param method: the format of the timestamp; either 'time' or 'date'.
136+
137+
:returns: a single date string
138+
"""
139+
if method in ["time", "date"]:
140+
return datetime.fromtimestamp(time()).strftime(
141+
("%Y-%m-%d", "%Y-%m-%d %H:%M:%S")[method == "time"]
142+
)
143+
else:
144+
self.new("Bad method passed to Logger.get_time().", "ERROR")
145+
return ""
146+
147+
def add_scope(self, name: str, value: int) -> bool:
148+
"""
149+
Adds a new logging scope for use with log entries. Users should be careful when doing this;
150+
custom scopes would be best added immediately following initialisation. If a 'Logger.new()'
151+
call is made before the scope it uses is added, it will generate a warning.
152+
153+
The recommended format for scope names is all uppercase, with no spaces or underscores.
154+
Custom scopes are instance specific and not hard saved.
155+
156+
:param name: the name of the new scope
157+
:param value: the default value of the new scope (0-2)
158+
159+
:return: a boolean sucess status
160+
"""
161+
if name in self.__scopes.keys():
162+
self.new(
163+
f"Attempt was made to add new scope with name {name}, but scope with this name "
164+
+ "already exists.",
165+
"WARNING"
166+
)
167+
else:
168+
self.__scopes[name] = value
169+
return True
170+
return False
171+
172+
def clean(self) -> None:
173+
"""
174+
Empties log array. Any log entries not saved to the output file will be lost.
175+
"""
176+
del self.__log[:]
177+
self.__is_empty = True
178+
self.__write_logs = False
179+
180+
def edit_scope(self, name: str, value: int) -> bool:
181+
"""
182+
Edits an existing scope's value. Edited values are instance specific and not hard saved.
183+
184+
:param name: the name of the scope to edit
185+
:param value: the new value of the scope (0-2)
186+
187+
:returns: a boolean success status
188+
"""
189+
if name in self.__scopes.keys():
190+
self.__scopes[name] = value
191+
return True
192+
else:
193+
self.new(
194+
f"Attempt was made to edit a scope with name {name}, but no scope with "
195+
+ "this name exists.",
196+
"WARNING"
197+
)
198+
return False
199+
200+
def get(self, mode: str = "all", scope: str = None) -> Union[List[LogEntry], LogEntry]:
201+
"""
202+
Returns item(s) in the log. The entries returned can be controlled by passing optional
203+
arguments.
204+
205+
If no entries match the query, nothing will be returned.
206+
207+
:param mode: optional; 'all' for all log entries or 'recent' for only the most recent one
208+
:param scope: optional; if passed, only entries matching its value will be returned
209+
210+
:returns: a single log entry or list of log entries, or nothing
211+
"""
212+
if self.__is_empty:
213+
pass
214+
elif scope is None:
215+
return (self.__log, self.__log[-1])[mode == "recent"]
216+
else:
217+
# return all log entries matching the query
218+
if mode == "all":
219+
data: list[LogEntry] = []
220+
for i in self.__log:
221+
if scope is None or i.scope == scope:
222+
data.append(i)
223+
if data:
224+
return data
225+
# iterate through the log in reverse to find the most recent entry matching the query
226+
elif mode == "recent":
227+
for i in range(len(self.__log)-1, 0):
228+
if scope is None or self.__log[i].scope == scope:
229+
return self.__log[i]
230+
else:
231+
self.new("Unknown mode passed to Logger.get().", "WARNING")
232+
233+
def init_bar(self, limit: int) -> None:
234+
"""
235+
Initiate and open the progress bar.
236+
237+
:param limit: the number of increments it should take to fill the bar
238+
"""
239+
self.bar = ProgressBar(limit=limit)
240+
self.bar.open()
241+
242+
def new(self,
243+
message: str,
244+
scope: str,
245+
print_to_console: bool = False,
246+
notify: bool = False) -> bool:
247+
"""
248+
Initiates a new log entry and prints it to the console. Optionally, if do_not_print is
249+
passed as True, it will only save the log and will not print anything (unless the scope is
250+
'NOSCOPE'; these messages are always printed).
251+
252+
:param message: the log message
253+
:param scope: the scope of the message
254+
:param print_to_console: optional, default True; whether the message should be printed to
255+
the console
256+
:param notify: optional, default False; whether the message should be displayed as a
257+
desktop notification
258+
259+
:returns: a boolean success status
260+
"""
261+
if scope in self.__scopes or scope == "NOSCOPE":
262+
output: bool = (self.__scopes[scope] == 2) if scope != "NOSCOPE" else False
263+
is_bar: bool = (self.bar is not None) and self.bar.opened
264+
265+
# if the progress bar is enabled, append any necessary empty characters to the message
266+
# to completely overwrite it upon output
267+
if is_bar and len(message) < len(self.bar.state):
268+
message += " " * (len(self.bar.state) - len(message))
269+
270+
entry: LogEntry = self.__create_log_entry(message, output, scope)
271+
self.__display_log_entry(entry, scope, notify, print_to_console, is_bar)
272+
273+
self.__write_logs = self.__write_logs or output
274+
self.__is_empty = False
275+
276+
return True
277+
else:
278+
self.new("Unknown scope passed to Logger.new()", "WARNING")
279+
return False
280+
281+
def notify(self, message: str) -> None:
282+
"""
283+
Displays a desktop notification with a given message.
284+
285+
:param message: the message to display
286+
"""
287+
self.__notifier.notify(title=self.__program_name, message=message)
288+
289+
def output(self) -> None:
290+
"""
291+
Writes all log entries with appropriate scopes to the log file. If the output path for the
292+
log file does not exist, it is created.
293+
294+
Log files are marked with the date, so each new day, a new file will be created.
295+
"""
296+
if self.__write_logs:
297+
with open(f"{self.__output_path}/log-{self.__get_time(method='date')}.txt",
298+
"at+") as log_file:
299+
for line in self.__log:
300+
if line.output:
301+
log_file.write(line.rendered + "\n")
302+
self.clean()

smooth_logger/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
from .logger import Logger
2-
from .logger import LogEntry
1+
from .Logger import Logger
2+
from .LogEntry import LogEntry

0 commit comments

Comments
 (0)