TonieToolbox 0.1.0__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.
- TonieToolbox/__init__.py +5 -0
- TonieToolbox/__main__.py +145 -0
- TonieToolbox/audio_conversion.py +194 -0
- TonieToolbox/constants.py +14 -0
- TonieToolbox/dependency_manager.py +378 -0
- TonieToolbox/filename_generator.py +94 -0
- TonieToolbox/logger.py +57 -0
- TonieToolbox/ogg_page.py +588 -0
- TonieToolbox/opus_packet.py +219 -0
- TonieToolbox/tonie_analysis.py +522 -0
- TonieToolbox/tonie_file.py +411 -0
- TonieToolbox/tonie_header.proto +11 -0
- TonieToolbox/tonie_header_pb2.py +99 -0
- tonietoolbox-0.1.0.dist-info/METADATA +301 -0
- tonietoolbox-0.1.0.dist-info/RECORD +19 -0
- tonietoolbox-0.1.0.dist-info/WHEEL +5 -0
- tonietoolbox-0.1.0.dist-info/entry_points.txt +2 -0
- tonietoolbox-0.1.0.dist-info/licenses/LICENSE.md +674 -0
- tonietoolbox-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,378 @@
|
|
1
|
+
"""
|
2
|
+
Dependency management for the TonieToolbox package.
|
3
|
+
|
4
|
+
This module handles the download and management of external dependencies
|
5
|
+
required by the TonieToolbox package, such as FFmpeg and opus-tools.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import sys
|
10
|
+
import platform
|
11
|
+
import subprocess
|
12
|
+
import shutil
|
13
|
+
import zipfile
|
14
|
+
import tarfile
|
15
|
+
import urllib.request
|
16
|
+
from pathlib import Path
|
17
|
+
|
18
|
+
from .logger import get_logger
|
19
|
+
logger = get_logger('dependency_manager')
|
20
|
+
|
21
|
+
DEPENDENCIES = {
|
22
|
+
'ffmpeg': {
|
23
|
+
'windows': {
|
24
|
+
'url': 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl-shared.zip',
|
25
|
+
'bin_path': 'bin/ffmpeg.exe',
|
26
|
+
'extract_dir': 'ffmpeg'
|
27
|
+
},
|
28
|
+
'linux': {
|
29
|
+
'url': 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl-shared.tar.xz',
|
30
|
+
'bin_path': 'ffmpeg',
|
31
|
+
'extract_dir': 'ffmpeg'
|
32
|
+
},
|
33
|
+
'darwin': {
|
34
|
+
'url': 'https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip',
|
35
|
+
'bin_path': 'ffmpeg',
|
36
|
+
'extract_dir': 'ffmpeg'
|
37
|
+
}
|
38
|
+
},
|
39
|
+
'opusenc': {
|
40
|
+
'windows': {
|
41
|
+
'url': 'https://archive.mozilla.org/pub/opus/win32/opus-tools-0.2-opus-1.3.zip',
|
42
|
+
'bin_path': 'opusenc.exe',
|
43
|
+
'extract_dir': 'opusenc'
|
44
|
+
},
|
45
|
+
'linux': {
|
46
|
+
'package': 'opus-tools'
|
47
|
+
},
|
48
|
+
'darwin': {
|
49
|
+
'package': 'opus-tools'
|
50
|
+
}
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
def get_system():
|
55
|
+
"""Get the current operating system."""
|
56
|
+
system = platform.system().lower()
|
57
|
+
logger.debug("Detected operating system: %s", system)
|
58
|
+
return system
|
59
|
+
|
60
|
+
def get_user_data_dir():
|
61
|
+
"""Get the user data directory for storing downloaded dependencies."""
|
62
|
+
system = get_system()
|
63
|
+
|
64
|
+
if system == 'windows':
|
65
|
+
base_dir = os.environ.get('APPDATA', os.path.expanduser('~'))
|
66
|
+
elif system == 'darwin':
|
67
|
+
base_dir = os.path.expanduser('~/Library/Application Support')
|
68
|
+
else: # linux or other unix-like
|
69
|
+
base_dir = os.environ.get('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
|
70
|
+
|
71
|
+
app_dir = os.path.join(base_dir, 'TonieToolbox')
|
72
|
+
logger.debug("Using application data directory: %s", app_dir)
|
73
|
+
|
74
|
+
os.makedirs(app_dir, exist_ok=True)
|
75
|
+
return app_dir
|
76
|
+
|
77
|
+
def download_file(url, destination):
|
78
|
+
"""
|
79
|
+
Download a file from a URL to the specified destination.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
url (str): The URL of the file to download
|
83
|
+
destination (str): The path to save the file to
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
bool: True if download was successful, False otherwise
|
87
|
+
"""
|
88
|
+
try:
|
89
|
+
logger.info("Downloading %s to %s", url, destination)
|
90
|
+
headers = {'User-Agent': 'TonieToolbox-dependency-downloader/1.0'}
|
91
|
+
req = urllib.request.Request(url, headers=headers)
|
92
|
+
|
93
|
+
with urllib.request.urlopen(req) as response, open(destination, 'wb') as out_file:
|
94
|
+
file_size = int(response.info().get('Content-Length', 0))
|
95
|
+
downloaded = 0
|
96
|
+
block_size = 8192
|
97
|
+
|
98
|
+
logger.debug("File size: %d bytes", file_size)
|
99
|
+
|
100
|
+
while True:
|
101
|
+
buffer = response.read(block_size)
|
102
|
+
if not buffer:
|
103
|
+
break
|
104
|
+
|
105
|
+
downloaded += len(buffer)
|
106
|
+
out_file.write(buffer)
|
107
|
+
|
108
|
+
if file_size > 0:
|
109
|
+
percent = downloaded * 100 / file_size
|
110
|
+
logger.debug("Download progress: %.1f%%", percent)
|
111
|
+
|
112
|
+
logger.info("Download completed successfully")
|
113
|
+
return True
|
114
|
+
except Exception as e:
|
115
|
+
logger.error("Failed to download %s: %s", url, e)
|
116
|
+
return False
|
117
|
+
|
118
|
+
def extract_archive(archive_path, extract_dir):
|
119
|
+
"""
|
120
|
+
Extract an archive file to the specified directory.
|
121
|
+
|
122
|
+
Args:
|
123
|
+
archive_path (str): Path to the archive file
|
124
|
+
extract_dir (str): Directory to extract to
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
bool: True if extraction was successful, False otherwise
|
128
|
+
"""
|
129
|
+
try:
|
130
|
+
logger.info("Extracting %s to %s", archive_path, extract_dir)
|
131
|
+
os.makedirs(extract_dir, exist_ok=True)
|
132
|
+
|
133
|
+
if archive_path.endswith('.zip'):
|
134
|
+
logger.debug("Extracting ZIP archive")
|
135
|
+
with zipfile.ZipFile(archive_path, 'r') as zip_ref:
|
136
|
+
zip_ref.extractall(extract_dir)
|
137
|
+
logger.trace("Extracted files: %s", zip_ref.namelist())
|
138
|
+
elif archive_path.endswith(('.tar.gz', '.tgz')):
|
139
|
+
logger.debug("Extracting TAR.GZ archive")
|
140
|
+
with tarfile.open(archive_path, 'r:gz') as tar_ref:
|
141
|
+
tar_ref.extractall(extract_dir)
|
142
|
+
logger.trace("Extracted files: %s", tar_ref.getnames())
|
143
|
+
elif archive_path.endswith(('.tar.xz', '.txz')):
|
144
|
+
logger.debug("Extracting TAR.XZ archive")
|
145
|
+
with tarfile.open(archive_path, 'r:xz') as tar_ref:
|
146
|
+
tar_ref.extractall(extract_dir)
|
147
|
+
logger.trace("Extracted files: %s", tar_ref.getnames())
|
148
|
+
elif archive_path.endswith('.tar'):
|
149
|
+
logger.debug("Extracting TAR archive")
|
150
|
+
with tarfile.open(archive_path, 'r') as tar_ref:
|
151
|
+
tar_ref.extractall(extract_dir)
|
152
|
+
logger.trace("Extracted files: %s", tar_ref.getnames())
|
153
|
+
else:
|
154
|
+
logger.error("Unsupported archive format: %s", archive_path)
|
155
|
+
return False
|
156
|
+
|
157
|
+
logger.info("Archive extracted successfully")
|
158
|
+
return True
|
159
|
+
except Exception as e:
|
160
|
+
logger.error("Failed to extract %s: %s", archive_path, e)
|
161
|
+
return False
|
162
|
+
|
163
|
+
def find_binary_in_extracted_dir(extract_dir, binary_path):
|
164
|
+
"""
|
165
|
+
Find a binary file in the extracted directory structure.
|
166
|
+
|
167
|
+
Args:
|
168
|
+
extract_dir (str): Directory where the archive was extracted
|
169
|
+
binary_path (str): Path or name of the binary to find
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
str: Full path to the binary if found, None otherwise
|
173
|
+
"""
|
174
|
+
logger.debug("Looking for binary %s in %s", binary_path, extract_dir)
|
175
|
+
|
176
|
+
direct_path = os.path.join(extract_dir, binary_path)
|
177
|
+
if os.path.exists(direct_path):
|
178
|
+
logger.debug("Found binary at direct path: %s", direct_path)
|
179
|
+
return direct_path
|
180
|
+
|
181
|
+
logger.debug("Searching for binary in directory tree")
|
182
|
+
for root, _, files in os.walk(extract_dir):
|
183
|
+
for f in files:
|
184
|
+
if f == os.path.basename(binary_path) or f == binary_path:
|
185
|
+
full_path = os.path.join(root, f)
|
186
|
+
logger.debug("Found binary at: %s", full_path)
|
187
|
+
return full_path
|
188
|
+
|
189
|
+
logger.warning("Binary %s not found in %s", binary_path, extract_dir)
|
190
|
+
return None
|
191
|
+
|
192
|
+
def check_binary_in_path(binary_name):
|
193
|
+
"""
|
194
|
+
Check if a binary is available in PATH.
|
195
|
+
|
196
|
+
Args:
|
197
|
+
binary_name (str): Name of the binary to check
|
198
|
+
|
199
|
+
Returns:
|
200
|
+
str: Path to the binary if found, None otherwise
|
201
|
+
"""
|
202
|
+
logger.debug("Checking if %s is available in PATH", binary_name)
|
203
|
+
try:
|
204
|
+
path = shutil.which(binary_name)
|
205
|
+
if path:
|
206
|
+
logger.debug("Found %s at %s, verifying it works", binary_name, path)
|
207
|
+
|
208
|
+
if binary_name == 'opusenc':
|
209
|
+
# Try with --version flag first
|
210
|
+
cmd = [path, '--version']
|
211
|
+
result = subprocess.run(cmd,
|
212
|
+
stdout=subprocess.PIPE,
|
213
|
+
stderr=subprocess.PIPE,
|
214
|
+
timeout=5)
|
215
|
+
|
216
|
+
# If --version fails, try without arguments (opusenc shows help/version when run without args)
|
217
|
+
if result.returncode != 0:
|
218
|
+
logger.debug("opusenc --version failed, trying without arguments")
|
219
|
+
result = subprocess.run([path],
|
220
|
+
stdout=subprocess.PIPE,
|
221
|
+
stderr=subprocess.PIPE,
|
222
|
+
timeout=5)
|
223
|
+
else:
|
224
|
+
# For other binaries like ffmpeg
|
225
|
+
cmd = [path, '-version']
|
226
|
+
result = subprocess.run(cmd,
|
227
|
+
stdout=subprocess.PIPE,
|
228
|
+
stderr=subprocess.PIPE,
|
229
|
+
timeout=5)
|
230
|
+
|
231
|
+
if result.returncode == 0:
|
232
|
+
logger.debug("%s is available and working", binary_name)
|
233
|
+
return path
|
234
|
+
else:
|
235
|
+
logger.warning("%s found but returned error code %d", binary_name, result.returncode)
|
236
|
+
else:
|
237
|
+
logger.debug("%s not found in PATH", binary_name)
|
238
|
+
except Exception as e:
|
239
|
+
logger.warning("Error checking %s: %s", binary_name, e)
|
240
|
+
|
241
|
+
return None
|
242
|
+
|
243
|
+
def install_package(package_name):
|
244
|
+
"""
|
245
|
+
Attempt to install a package using the system's package manager.
|
246
|
+
|
247
|
+
Args:
|
248
|
+
package_name (str): Name of the package to install
|
249
|
+
|
250
|
+
Returns:
|
251
|
+
bool: True if installation was successful, False otherwise
|
252
|
+
"""
|
253
|
+
system = get_system()
|
254
|
+
logger.info("Attempting to install %s on %s", package_name, system)
|
255
|
+
|
256
|
+
try:
|
257
|
+
if system == 'linux':
|
258
|
+
# Try apt-get (Debian/Ubuntu)
|
259
|
+
if shutil.which('apt-get'):
|
260
|
+
logger.info("Installing %s using apt-get", package_name)
|
261
|
+
subprocess.run(['sudo', 'apt-get', 'update'], check=True)
|
262
|
+
subprocess.run(['sudo', 'apt-get', 'install', '-y', package_name], check=True)
|
263
|
+
return True
|
264
|
+
# Try yum (CentOS/RHEL)
|
265
|
+
elif shutil.which('yum'):
|
266
|
+
logger.info("Installing %s using yum", package_name)
|
267
|
+
subprocess.run(['sudo', 'yum', 'install', '-y', package_name], check=True)
|
268
|
+
return True
|
269
|
+
|
270
|
+
elif system == 'darwin':
|
271
|
+
# Try Homebrew
|
272
|
+
if shutil.which('brew'):
|
273
|
+
logger.info("Installing %s using homebrew", package_name)
|
274
|
+
subprocess.run(['brew', 'install', package_name], check=True)
|
275
|
+
return True
|
276
|
+
|
277
|
+
logger.warning("Could not automatically install %s. Please install it manually.", package_name)
|
278
|
+
return False
|
279
|
+
except subprocess.CalledProcessError as e:
|
280
|
+
logger.error("Failed to install %s: %s", package_name, e)
|
281
|
+
return False
|
282
|
+
|
283
|
+
def ensure_dependency(dependency_name):
|
284
|
+
"""
|
285
|
+
Ensure that a dependency is available, downloading it if necessary.
|
286
|
+
|
287
|
+
Args:
|
288
|
+
dependency_name (str): Name of the dependency ('ffmpeg' or 'opusenc')
|
289
|
+
|
290
|
+
Returns:
|
291
|
+
str: Path to the binary if available, None otherwise
|
292
|
+
"""
|
293
|
+
logger.info("Ensuring dependency: %s", dependency_name)
|
294
|
+
system = get_system()
|
295
|
+
|
296
|
+
if system not in ['windows', 'linux', 'darwin']:
|
297
|
+
logger.error("Unsupported operating system: %s", system)
|
298
|
+
return None
|
299
|
+
|
300
|
+
if dependency_name not in DEPENDENCIES:
|
301
|
+
logger.error("Unknown dependency: %s", dependency_name)
|
302
|
+
return None
|
303
|
+
|
304
|
+
# First check if it's already in PATH
|
305
|
+
bin_name = dependency_name if dependency_name != 'opusenc' else 'opusenc'
|
306
|
+
path_binary = check_binary_in_path(bin_name)
|
307
|
+
if path_binary:
|
308
|
+
logger.info("Found %s in PATH: %s", dependency_name, path_binary)
|
309
|
+
return path_binary
|
310
|
+
|
311
|
+
# If not in PATH, check if we should install via package manager
|
312
|
+
if 'package' in DEPENDENCIES[dependency_name].get(system, {}):
|
313
|
+
package_name = DEPENDENCIES[dependency_name][system]['package']
|
314
|
+
logger.info("%s not found. Attempting to install %s package...", dependency_name, package_name)
|
315
|
+
if install_package(package_name):
|
316
|
+
path_binary = check_binary_in_path(bin_name)
|
317
|
+
if path_binary:
|
318
|
+
logger.info("Successfully installed %s: %s", dependency_name, path_binary)
|
319
|
+
return path_binary
|
320
|
+
|
321
|
+
# If not installable via package manager or installation failed, try downloading
|
322
|
+
if 'url' not in DEPENDENCIES[dependency_name].get(system, {}):
|
323
|
+
logger.error("Cannot download %s for %s", dependency_name, system)
|
324
|
+
return None
|
325
|
+
|
326
|
+
# Set up paths
|
327
|
+
user_data_dir = get_user_data_dir()
|
328
|
+
dependency_info = DEPENDENCIES[dependency_name][system]
|
329
|
+
download_url = dependency_info['url']
|
330
|
+
extract_dir_name = dependency_info['extract_dir']
|
331
|
+
binary_path = dependency_info['bin_path']
|
332
|
+
|
333
|
+
extract_dir = os.path.join(user_data_dir, extract_dir_name)
|
334
|
+
logger.debug("Using extract directory: %s", extract_dir)
|
335
|
+
os.makedirs(extract_dir, exist_ok=True)
|
336
|
+
|
337
|
+
# Check if we already downloaded and extracted it
|
338
|
+
existing_binary = find_binary_in_extracted_dir(extract_dir, binary_path)
|
339
|
+
if existing_binary and os.path.exists(existing_binary):
|
340
|
+
logger.info("Using existing %s: %s", dependency_name, existing_binary)
|
341
|
+
return existing_binary
|
342
|
+
|
343
|
+
# Download and extract
|
344
|
+
archive_ext = '.zip' if download_url.endswith('zip') else '.tar.xz'
|
345
|
+
archive_path = os.path.join(user_data_dir, f"{dependency_name}{archive_ext}")
|
346
|
+
logger.debug("Using archive path: %s", archive_path)
|
347
|
+
|
348
|
+
if download_file(download_url, archive_path):
|
349
|
+
if extract_archive(archive_path, extract_dir):
|
350
|
+
binary = find_binary_in_extracted_dir(extract_dir, binary_path)
|
351
|
+
if binary:
|
352
|
+
# Make sure it's executable on Unix-like systems
|
353
|
+
if system in ['linux', 'darwin']:
|
354
|
+
logger.debug("Setting executable permissions on %s", binary)
|
355
|
+
os.chmod(binary, 0o755)
|
356
|
+
logger.info("Successfully set up %s: %s", dependency_name, binary)
|
357
|
+
return binary
|
358
|
+
|
359
|
+
logger.error("Failed to set up %s", dependency_name)
|
360
|
+
return None
|
361
|
+
|
362
|
+
def get_ffmpeg_binary():
|
363
|
+
"""
|
364
|
+
Get the path to the FFmpeg binary, downloading it if necessary.
|
365
|
+
|
366
|
+
Returns:
|
367
|
+
str: Path to the FFmpeg binary if available, None otherwise
|
368
|
+
"""
|
369
|
+
return ensure_dependency('ffmpeg')
|
370
|
+
|
371
|
+
def get_opus_binary():
|
372
|
+
"""
|
373
|
+
Get the path to the opusenc binary, downloading it if necessary.
|
374
|
+
|
375
|
+
Returns:
|
376
|
+
str: Path to the opusenc binary if available, None otherwise
|
377
|
+
"""
|
378
|
+
return ensure_dependency('opusenc')
|
@@ -0,0 +1,94 @@
|
|
1
|
+
"""
|
2
|
+
Module for generating intelligent output filenames for TonieToolbox.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
import re
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import List, Optional
|
9
|
+
from .logger import get_logger
|
10
|
+
|
11
|
+
logger = get_logger('filename_generator')
|
12
|
+
|
13
|
+
def sanitize_filename(filename: str) -> str:
|
14
|
+
"""
|
15
|
+
Sanitize a filename by removing invalid characters and trimming.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
filename: The filename to sanitize
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
A sanitized filename
|
22
|
+
"""
|
23
|
+
# Remove invalid characters for filenames
|
24
|
+
sanitized = re.sub(r'[<>:"/\\|?*]', '_', filename)
|
25
|
+
# Remove leading/trailing whitespace and dots
|
26
|
+
sanitized = sanitized.strip('. \t')
|
27
|
+
# Avoid empty filenames
|
28
|
+
if not sanitized:
|
29
|
+
return "tonie"
|
30
|
+
return sanitized
|
31
|
+
|
32
|
+
def guess_output_filename(input_filename: str, input_files: List[str] = None) -> str:
|
33
|
+
"""
|
34
|
+
Generate a sensible output filename based on input file or directory.
|
35
|
+
|
36
|
+
Logic:
|
37
|
+
1. For .lst files: Use the lst filename without extension
|
38
|
+
2. For directories: Use the directory name
|
39
|
+
3. For single files: Use the filename without extension
|
40
|
+
4. For multiple files: Use the common parent directory name
|
41
|
+
|
42
|
+
Args:
|
43
|
+
input_filename: The input filename or pattern
|
44
|
+
input_files: List of resolved input files (optional)
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
Generated output filename without extension
|
48
|
+
"""
|
49
|
+
logger.debug("Guessing output filename from input: %s", input_filename)
|
50
|
+
|
51
|
+
# Handle .lst files
|
52
|
+
if input_filename.lower().endswith('.lst'):
|
53
|
+
base = os.path.basename(input_filename)
|
54
|
+
name = os.path.splitext(base)[0]
|
55
|
+
logger.debug("Using .lst file name: %s", name)
|
56
|
+
return sanitize_filename(name)
|
57
|
+
|
58
|
+
# Handle directory pattern
|
59
|
+
if input_filename.endswith('/*') or input_filename.endswith('\\*'):
|
60
|
+
dir_path = input_filename[:-2] # Remove the /* or \* at the end
|
61
|
+
dir_name = os.path.basename(os.path.normpath(dir_path))
|
62
|
+
logger.debug("Using directory name: %s", dir_name)
|
63
|
+
return sanitize_filename(dir_name)
|
64
|
+
|
65
|
+
# Handle directory
|
66
|
+
if os.path.isdir(input_filename):
|
67
|
+
dir_name = os.path.basename(os.path.normpath(input_filename))
|
68
|
+
logger.debug("Using directory name: %s", dir_name)
|
69
|
+
return sanitize_filename(dir_name)
|
70
|
+
|
71
|
+
# Handle single file
|
72
|
+
if not input_files or len(input_files) == 1:
|
73
|
+
file_path = input_files[0] if input_files else input_filename
|
74
|
+
base = os.path.basename(file_path)
|
75
|
+
name = os.path.splitext(base)[0]
|
76
|
+
logger.debug("Using single file name: %s", name)
|
77
|
+
return sanitize_filename(name)
|
78
|
+
|
79
|
+
# Handle multiple files - try to find common parent directory
|
80
|
+
try:
|
81
|
+
# Find the common parent directory of all files
|
82
|
+
common_path = os.path.commonpath([os.path.abspath(f) for f in input_files])
|
83
|
+
dir_name = os.path.basename(common_path)
|
84
|
+
|
85
|
+
# If the common path is root or very short, use parent of first file instead
|
86
|
+
if len(dir_name) <= 1 or len(common_path) < 4:
|
87
|
+
dir_name = os.path.basename(os.path.dirname(os.path.abspath(input_files[0])))
|
88
|
+
|
89
|
+
logger.debug("Using common parent directory: %s", dir_name)
|
90
|
+
return sanitize_filename(dir_name)
|
91
|
+
except ValueError:
|
92
|
+
# Files might be on different drives
|
93
|
+
logger.debug("Could not determine common path, using generic name")
|
94
|
+
return "tonie_collection"
|
TonieToolbox/logger.py
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
"""
|
2
|
+
Logging configuration for the TonieToolbox package.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
import os
|
7
|
+
import sys
|
8
|
+
|
9
|
+
# Define log levels and their names
|
10
|
+
TRACE = 5 # Custom level for ultra-verbose debugging
|
11
|
+
logging.addLevelName(TRACE, 'TRACE')
|
12
|
+
|
13
|
+
# Create a method for the TRACE level
|
14
|
+
def trace(self, message, *args, **kwargs):
|
15
|
+
"""Log a message with TRACE level (more detailed than DEBUG)"""
|
16
|
+
if self.isEnabledFor(TRACE):
|
17
|
+
self.log(TRACE, message, *args, **kwargs)
|
18
|
+
|
19
|
+
# Add trace method to the Logger class
|
20
|
+
logging.Logger.trace = trace
|
21
|
+
|
22
|
+
def setup_logging(level=logging.INFO):
|
23
|
+
"""
|
24
|
+
Set up logging configuration for the entire application.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
level: Logging level (default: logging.INFO)
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
logging.Logger: Root logger instance
|
31
|
+
"""
|
32
|
+
# Configure root logger
|
33
|
+
logging.basicConfig(
|
34
|
+
level=level,
|
35
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
36
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
37
|
+
)
|
38
|
+
|
39
|
+
# Get the root logger
|
40
|
+
root_logger = logging.getLogger('TonieToolbox')
|
41
|
+
root_logger.setLevel(level)
|
42
|
+
|
43
|
+
return root_logger
|
44
|
+
|
45
|
+
def get_logger(name):
|
46
|
+
"""
|
47
|
+
Get a logger with the specified name.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
name: Logger name, typically the module name
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
logging.Logger: Logger instance
|
54
|
+
"""
|
55
|
+
# Get logger with proper hierarchical naming
|
56
|
+
logger = logging.getLogger(f'TonieToolbox.{name}')
|
57
|
+
return logger
|