python-template-project 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.
- python_template_project/__init__.py +0 -0
- python_template_project/__main__.py +8 -0
- python_template_project/cli/__init__.py +0 -0
- python_template_project/cli/__main__.py +8 -0
- python_template_project/cli/cli.py +217 -0
- python_template_project/config/__init__.py +0 -0
- python_template_project/config/config.py +557 -0
- python_template_project/core/__init__.py +0 -0
- python_template_project/core/base.py +397 -0
- python_template_project/core/logging.py +219 -0
- python_template_project/gui/__init__.py +0 -0
- python_template_project/gui/__main__.py +8 -0
- python_template_project/gui/gui.py +615 -0
- python_template_project-0.1.0.dist-info/METADATA +92 -0
- python_template_project-0.1.0.dist-info/RECORD +18 -0
- python_template_project-0.1.0.dist-info/WHEEL +4 -0
- python_template_project-0.1.0.dist-info/entry_points.txt +3 -0
- python_template_project-0.1.0.dist-info/licenses/LICENSE +24 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import zipfile
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import gpxpy
|
|
7
|
+
import srtm
|
|
8
|
+
from gpxpy.gpx import GPX, GPXTrackPoint, GPXWaypoint, GPXXMLSyntaxException
|
|
9
|
+
|
|
10
|
+
NAME = "python_template_project"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseGPXProcessor:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
input_: str | Path | list[str],
|
|
17
|
+
output=None,
|
|
18
|
+
min_dist=10,
|
|
19
|
+
date_format="%Y-%m-%d",
|
|
20
|
+
elevation=True,
|
|
21
|
+
logger=None,
|
|
22
|
+
):
|
|
23
|
+
# ensure that input is converted into a list[Path]
|
|
24
|
+
if isinstance(input_, str):
|
|
25
|
+
self.input = [Path(input_)]
|
|
26
|
+
elif isinstance(input_, Path):
|
|
27
|
+
self.input = [input_]
|
|
28
|
+
elif isinstance(input_, list):
|
|
29
|
+
self.input = [Path(p) for p in input_ if isinstance(p, str | Path)]
|
|
30
|
+
else:
|
|
31
|
+
raise ValueError("Input must be a string, Path, or list of strings/Paths.")
|
|
32
|
+
|
|
33
|
+
self.output = output
|
|
34
|
+
self.min_dist = min_dist
|
|
35
|
+
self.date_format = date_format
|
|
36
|
+
self.include_elevation = elevation
|
|
37
|
+
|
|
38
|
+
# Initialize SRTM elevation data
|
|
39
|
+
self.elevation_data = srtm.get_data()
|
|
40
|
+
self.logger = logger
|
|
41
|
+
|
|
42
|
+
def _get_output_folder(self) -> Path:
|
|
43
|
+
"""Get the output folder path, create if not exists."""
|
|
44
|
+
if self.output:
|
|
45
|
+
output_path = Path(self.output)
|
|
46
|
+
else:
|
|
47
|
+
timestamp = datetime.now().strftime(f"{self.date_format}_%H%M")
|
|
48
|
+
output_path = Path.cwd() / f"gpx_processed_{timestamp}"
|
|
49
|
+
|
|
50
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
return output_path
|
|
52
|
+
|
|
53
|
+
def _get_adjusted_elevation(self, point: GPXTrackPoint) -> int | float:
|
|
54
|
+
"""Get adjusted elevation from SRTM data, fallback to original elevation."""
|
|
55
|
+
try:
|
|
56
|
+
srtm_elevation = self.elevation_data.get_elevation(point.latitude, point.longitude)
|
|
57
|
+
if srtm_elevation is not None:
|
|
58
|
+
return round(srtm_elevation, 1)
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
# Fallback to original elevation or 0
|
|
63
|
+
return round(point.elevation or 0, 1)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def _calculate_distance(point1: GPXTrackPoint, point2: GPXTrackPoint) -> float:
|
|
67
|
+
"""Calculate distance between two GPX points in meters using Haversine formula."""
|
|
68
|
+
lat1, lon1 = math.radians(point1.latitude), math.radians(point1.longitude)
|
|
69
|
+
lat2, lon2 = math.radians(point2.latitude), math.radians(point2.longitude)
|
|
70
|
+
|
|
71
|
+
dlat = lat2 - lat1
|
|
72
|
+
dlon = lon2 - lon1
|
|
73
|
+
|
|
74
|
+
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
|
|
75
|
+
c = 2 * math.asin(math.sqrt(a))
|
|
76
|
+
|
|
77
|
+
# Earth's radius in meters
|
|
78
|
+
earth_radius = 6371000
|
|
79
|
+
return earth_radius * c
|
|
80
|
+
|
|
81
|
+
def _optimize_track_points(
|
|
82
|
+
self, track_points: list[GPXTrackPoint] | list[GPXWaypoint]
|
|
83
|
+
) -> list[GPXTrackPoint]:
|
|
84
|
+
"""Optimize track points by removing close points and cleaning metadata."""
|
|
85
|
+
if not track_points:
|
|
86
|
+
return track_points
|
|
87
|
+
|
|
88
|
+
optimized_points = [track_points[0]] # Always keep first point
|
|
89
|
+
|
|
90
|
+
for point in track_points[1:]:
|
|
91
|
+
# Check distance to last kept point
|
|
92
|
+
if self._calculate_distance(optimized_points[-1], point) >= self.min_dist:
|
|
93
|
+
optimized_points.append(point)
|
|
94
|
+
|
|
95
|
+
# Always keep last point if it's different from the last kept point
|
|
96
|
+
if len(track_points) > 1 and optimized_points[-1] != track_points[-1]:
|
|
97
|
+
optimized_points.append(track_points[-1])
|
|
98
|
+
|
|
99
|
+
# Clean and optimize each point
|
|
100
|
+
for point in optimized_points:
|
|
101
|
+
# Remove time information
|
|
102
|
+
point.time = None
|
|
103
|
+
|
|
104
|
+
# Round coordinates to 5 decimal places
|
|
105
|
+
point.latitude = round(point.latitude, 5)
|
|
106
|
+
point.longitude = round(point.longitude, 5)
|
|
107
|
+
|
|
108
|
+
# Set optimized elevation
|
|
109
|
+
if self.include_elevation:
|
|
110
|
+
point.elevation = self._get_adjusted_elevation(point)
|
|
111
|
+
else:
|
|
112
|
+
point.elevation = None
|
|
113
|
+
|
|
114
|
+
# Remove unnecessary extensions and metadata
|
|
115
|
+
point.extensions = None
|
|
116
|
+
point.symbol = None
|
|
117
|
+
point.type = None
|
|
118
|
+
point.comment = None
|
|
119
|
+
point.description = None
|
|
120
|
+
point.source = None
|
|
121
|
+
point.link = None
|
|
122
|
+
point.link_text = None
|
|
123
|
+
point.link_type = None
|
|
124
|
+
point.horizontal_dilution = None
|
|
125
|
+
point.vertical_dilution = None
|
|
126
|
+
point.position_dilution = None
|
|
127
|
+
point.age_of_dgps_data = None
|
|
128
|
+
point.dgps_id = None
|
|
129
|
+
|
|
130
|
+
return optimized_points
|
|
131
|
+
|
|
132
|
+
def _get_gpx_files(self) -> list[Path]:
|
|
133
|
+
"""Get all GPX files from input (file, folder, or zip)."""
|
|
134
|
+
gpx_files = []
|
|
135
|
+
for input_path in self.input:
|
|
136
|
+
if not isinstance(input_path, Path):
|
|
137
|
+
input_path = Path(input_path)
|
|
138
|
+
self.logger.debug(f"Input path: {input_path.absolute()}")
|
|
139
|
+
|
|
140
|
+
if input_path.is_file():
|
|
141
|
+
if input_path.suffix.lower() == ".gpx":
|
|
142
|
+
gpx_files.append(input_path)
|
|
143
|
+
elif input_path.suffix.lower() == ".zip":
|
|
144
|
+
gpx_files.extend(self._extract_gpx_from_zip(input_path))
|
|
145
|
+
elif input_path.is_dir():
|
|
146
|
+
# Get all GPX files in directory
|
|
147
|
+
gpx_files.extend(input_path.glob("*.gpx"))
|
|
148
|
+
|
|
149
|
+
# Get GPX files from ZIP files in directory
|
|
150
|
+
for zip_file in input_path.glob("*.zip"):
|
|
151
|
+
gpx_files.extend(self._extract_gpx_from_zip(zip_file))
|
|
152
|
+
|
|
153
|
+
return gpx_files
|
|
154
|
+
|
|
155
|
+
def _extract_gpx_from_zip(self, zip_path: Path) -> list[Path]:
|
|
156
|
+
"""Extract GPX files from ZIP archive to temporary location."""
|
|
157
|
+
gpx_files = []
|
|
158
|
+
temp_dir = Path.cwd() / "temp_gpx_extract"
|
|
159
|
+
temp_dir.mkdir(exist_ok=True)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
|
163
|
+
for file_info in zip_ref.infolist():
|
|
164
|
+
if file_info.filename.lower().endswith(".gpx"):
|
|
165
|
+
extracted_path = temp_dir / Path(file_info.filename).name
|
|
166
|
+
with open(extracted_path, "wb") as f:
|
|
167
|
+
f.write(zip_ref.read(file_info.filename))
|
|
168
|
+
gpx_files.append(extracted_path)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
self.logger.error(f"Error extracting ZIP file {zip_path}: {e}")
|
|
171
|
+
|
|
172
|
+
return gpx_files
|
|
173
|
+
|
|
174
|
+
def _load_gpx_file(self, gpx_path: Path) -> GPX | None:
|
|
175
|
+
"""Load and parse GPX file."""
|
|
176
|
+
try:
|
|
177
|
+
with open(gpx_path, "r", encoding="utf-8") as f:
|
|
178
|
+
return gpxpy.parse(f)
|
|
179
|
+
except GPXXMLSyntaxException as e:
|
|
180
|
+
self.logger.error(f"Error parsing GPX file {gpx_path}: {e}")
|
|
181
|
+
return None
|
|
182
|
+
except Exception as e:
|
|
183
|
+
self.logger.error(f"Error loading GPX file {gpx_path}: {e}")
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
def _save_gpx_file(self, gpx: GPX, output_path: Path, original_file: Path | None = None):
|
|
187
|
+
"""Save GPX object to file."""
|
|
188
|
+
try:
|
|
189
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
190
|
+
f.write(gpx.to_xml())
|
|
191
|
+
if original_file and original_file.exists():
|
|
192
|
+
self.logger.info(
|
|
193
|
+
f"Original gpx file size: "
|
|
194
|
+
f"{Path(original_file).stat().st_size / 1024:.2f} KB"
|
|
195
|
+
)
|
|
196
|
+
self.logger.info(
|
|
197
|
+
f"Processed gpx file size: {output_path.stat().st_size / 1024:.2f} KB"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
self.logger.error(f"Error saving GPX file {output_path}: {e}")
|
|
202
|
+
|
|
203
|
+
def compress_files(self):
|
|
204
|
+
"""Shrink the size of all given gpx files in self.input."""
|
|
205
|
+
gpx_files = self._get_gpx_files()
|
|
206
|
+
output_folder = self._get_output_folder()
|
|
207
|
+
|
|
208
|
+
self.logger.info(f"Processing {len(gpx_files)} GPX files...")
|
|
209
|
+
|
|
210
|
+
for gpx_file in gpx_files:
|
|
211
|
+
gpx = self._load_gpx_file(gpx_file)
|
|
212
|
+
if gpx is None:
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
# process and clean waypoints
|
|
216
|
+
for waypoint in gpx.waypoints:
|
|
217
|
+
self._optimize_waypoint(waypoint)
|
|
218
|
+
|
|
219
|
+
# Process all tracks
|
|
220
|
+
for track in gpx.tracks:
|
|
221
|
+
for segment in track.segments:
|
|
222
|
+
segment.points = self._optimize_track_points(segment.points)
|
|
223
|
+
|
|
224
|
+
# Process all routes
|
|
225
|
+
for route in gpx.routes:
|
|
226
|
+
route.points = self._optimize_track_points(route.points)
|
|
227
|
+
|
|
228
|
+
# Clean GPX metadata
|
|
229
|
+
gpx.time = None
|
|
230
|
+
gpx.extensions = None
|
|
231
|
+
|
|
232
|
+
# Save compressed file
|
|
233
|
+
output_path = output_folder / f"compressed_{gpx_file.name}"
|
|
234
|
+
self._save_gpx_file(gpx, output_path, gpx_file)
|
|
235
|
+
self.logger.info(f"Compressed: {gpx_file.name} -> {output_path}")
|
|
236
|
+
|
|
237
|
+
def _optimize_waypoint(self, waypoint):
|
|
238
|
+
# Round coordinates and elevation
|
|
239
|
+
waypoint.latitude = round(waypoint.latitude, 5)
|
|
240
|
+
waypoint.longitude = round(waypoint.longitude, 5)
|
|
241
|
+
if self.include_elevation:
|
|
242
|
+
waypoint.elevation = self._get_adjusted_elevation(waypoint)
|
|
243
|
+
else:
|
|
244
|
+
waypoint.elevation = None
|
|
245
|
+
|
|
246
|
+
# Clean metadata
|
|
247
|
+
waypoint.time = None
|
|
248
|
+
waypoint.extensions = None
|
|
249
|
+
waypoint.symbol = None
|
|
250
|
+
waypoint.type = None
|
|
251
|
+
waypoint.comment = None
|
|
252
|
+
waypoint.description = None
|
|
253
|
+
waypoint.source = None
|
|
254
|
+
waypoint.link = None
|
|
255
|
+
waypoint.link_text = None
|
|
256
|
+
waypoint.link_type = None
|
|
257
|
+
return waypoint
|
|
258
|
+
|
|
259
|
+
def merge_files(self):
|
|
260
|
+
"""Merge all files of self.input into one gpx file with reduced resolution."""
|
|
261
|
+
gpx_files = self._get_gpx_files()
|
|
262
|
+
output_folder = self._get_output_folder()
|
|
263
|
+
|
|
264
|
+
if not gpx_files:
|
|
265
|
+
self.logger.error("No GPX files found to merge.")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
self.logger.info(f"Merging {len(gpx_files)} GPX files...")
|
|
269
|
+
|
|
270
|
+
# Create new GPX object
|
|
271
|
+
merged_gpx = gpxpy.gpx.GPX()
|
|
272
|
+
merged_gpx.name = "Merged GPX Tracks"
|
|
273
|
+
merged_gpx.description = (
|
|
274
|
+
f"Merged from {len(gpx_files)} GPX files: {', '.join(str(f.name) for f in gpx_files)}"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
track_counter = 1
|
|
278
|
+
|
|
279
|
+
for gpx_file in gpx_files:
|
|
280
|
+
gpx = self._load_gpx_file(gpx_file)
|
|
281
|
+
if gpx is None:
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
# Add all waypoints from this file
|
|
285
|
+
for waypoint in gpx.waypoints:
|
|
286
|
+
waypoint = self._optimize_waypoint(waypoint)
|
|
287
|
+
waypoint.name = f"{waypoint.name}_{track_counter}"
|
|
288
|
+
merged_gpx.waypoints.append(waypoint)
|
|
289
|
+
|
|
290
|
+
# Add all tracks from this file
|
|
291
|
+
for track in gpx.tracks:
|
|
292
|
+
new_track = gpxpy.gpx.GPXTrack()
|
|
293
|
+
new_track.name = f"{track.name or track_counter}_{gpx_file.stem}"
|
|
294
|
+
|
|
295
|
+
for segment in track.segments:
|
|
296
|
+
optimized_points = self._optimize_track_points(segment.points)
|
|
297
|
+
if optimized_points:
|
|
298
|
+
new_segment = gpxpy.gpx.GPXTrackSegment()
|
|
299
|
+
new_segment.points = optimized_points
|
|
300
|
+
new_track.segments.append(new_segment)
|
|
301
|
+
|
|
302
|
+
if new_track.segments:
|
|
303
|
+
merged_gpx.tracks.append(new_track)
|
|
304
|
+
track_counter += 1
|
|
305
|
+
|
|
306
|
+
# Add all routes from this file
|
|
307
|
+
for route in gpx.routes:
|
|
308
|
+
new_route = gpxpy.gpx.GPXRoute()
|
|
309
|
+
new_route.name = f"{route.name or track_counter}_{gpx_file.stem}"
|
|
310
|
+
new_route.points = self._optimize_track_points(route.points)
|
|
311
|
+
|
|
312
|
+
if new_route.points:
|
|
313
|
+
merged_gpx.routes.append(new_route)
|
|
314
|
+
track_counter += 1
|
|
315
|
+
|
|
316
|
+
# Save merged file
|
|
317
|
+
output_path = output_folder / "merged_tracks.gpx"
|
|
318
|
+
self._save_gpx_file(merged_gpx, output_path)
|
|
319
|
+
self.logger.info(f"Merged file saved: {output_path}")
|
|
320
|
+
|
|
321
|
+
def extract_pois(self):
|
|
322
|
+
"""Merge every starting point of each track in all files
|
|
323
|
+
into one gpx file with many pois."""
|
|
324
|
+
|
|
325
|
+
gpx_files = self._get_gpx_files()
|
|
326
|
+
output_folder = self._get_output_folder()
|
|
327
|
+
|
|
328
|
+
if not gpx_files:
|
|
329
|
+
self.logger.error("No GPX files found to extract POIs from.")
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
self.logger.info(f"Extracting POIs from {len(gpx_files)} GPX files...")
|
|
333
|
+
|
|
334
|
+
# Create new GPX object for waypoints
|
|
335
|
+
poi_gpx = gpxpy.gpx.GPX()
|
|
336
|
+
poi_gpx.name = "Extracted Track Starting Points"
|
|
337
|
+
poi_gpx.description = f"Starting points extracted from {len(gpx_files)} GPX files"
|
|
338
|
+
|
|
339
|
+
poi_counter = 1
|
|
340
|
+
|
|
341
|
+
for gpx_file in gpx_files:
|
|
342
|
+
gpx = self._load_gpx_file(gpx_file)
|
|
343
|
+
if gpx is None:
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
# Extract starting points from tracks
|
|
347
|
+
for track_idx, track in enumerate(gpx.tracks):
|
|
348
|
+
for _segment_idx, segment in enumerate(track.segments):
|
|
349
|
+
if segment.points:
|
|
350
|
+
start_point = segment.points[0]
|
|
351
|
+
|
|
352
|
+
# Create waypoint from starting point
|
|
353
|
+
waypoint = gpxpy.gpx.GPXWaypoint(
|
|
354
|
+
latitude=round(start_point.latitude, 5),
|
|
355
|
+
longitude=round(start_point.longitude, 5),
|
|
356
|
+
elevation=self._get_adjusted_elevation(start_point),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
track_name = track.name or f"Track_{track_idx + 1}"
|
|
360
|
+
waypoint.name = f"POI_{poi_counter:03d}"
|
|
361
|
+
waypoint.description = f"Start of {track_name} from {gpx_file.name}"
|
|
362
|
+
waypoint.type = "Track Start"
|
|
363
|
+
|
|
364
|
+
poi_gpx.waypoints.append(waypoint)
|
|
365
|
+
poi_counter += 1
|
|
366
|
+
|
|
367
|
+
# Extract starting points from routes
|
|
368
|
+
for route_idx, route in enumerate(gpx.routes):
|
|
369
|
+
if route.points:
|
|
370
|
+
start_point = route.points[0]
|
|
371
|
+
|
|
372
|
+
# Create waypoint from starting point
|
|
373
|
+
waypoint = gpxpy.gpx.GPXWaypoint(
|
|
374
|
+
latitude=round(start_point.latitude, 5),
|
|
375
|
+
longitude=round(start_point.longitude, 5),
|
|
376
|
+
elevation=self._get_adjusted_elevation(start_point),
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
route_name = route.name or f"Route_{route_idx + 1}"
|
|
380
|
+
waypoint.name = f"POI_{poi_counter:03d}"
|
|
381
|
+
waypoint.description = f"Start of {route_name} from {gpx_file.name}"
|
|
382
|
+
waypoint.type = "Route Start"
|
|
383
|
+
|
|
384
|
+
poi_gpx.waypoints.append(waypoint)
|
|
385
|
+
poi_counter += 1
|
|
386
|
+
|
|
387
|
+
# Save POI file
|
|
388
|
+
output_path = output_folder / "extracted_pois.gpx"
|
|
389
|
+
self._save_gpx_file(poi_gpx, output_path)
|
|
390
|
+
self.logger.info(f"POI file saved with {len(poi_gpx.waypoints)} waypoints: {output_path}")
|
|
391
|
+
|
|
392
|
+
# Clean up temporary files
|
|
393
|
+
temp_dir = Path.cwd() / "temp_gpx_extract"
|
|
394
|
+
if temp_dir.exists():
|
|
395
|
+
import shutil
|
|
396
|
+
|
|
397
|
+
shutil.rmtree(temp_dir)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Centralized logging configuration for python_template_project.
|
|
2
|
+
|
|
3
|
+
This module provides a unified logging setup that supports:
|
|
4
|
+
- File logging with rotation
|
|
5
|
+
- GUI integration via custom handler
|
|
6
|
+
- Configurable log levels from config
|
|
7
|
+
- Structured logging with consistent formatting
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import logging.handlers
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from python_template_project.config.config import ConfigParameterManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GuiLogHandler(logging.Handler):
|
|
19
|
+
"""Custom logging handler that can write to GUI text widgets."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, gui_writer=None):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.gui_writer = gui_writer
|
|
24
|
+
self.setFormatter(
|
|
25
|
+
logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", datefmt="%H:%M:%S")
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def emit(self, record):
|
|
29
|
+
"""Emit a log record to the GUI if writer is available."""
|
|
30
|
+
if self.gui_writer:
|
|
31
|
+
try:
|
|
32
|
+
msg = self.format(record) + "\n"
|
|
33
|
+
self.gui_writer.write(msg)
|
|
34
|
+
except Exception:
|
|
35
|
+
# Fail silently to avoid recursive logging errors
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class LoggerManager:
|
|
40
|
+
"""Manages all logging configuration and handlers."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, config: ConfigParameterManager):
|
|
43
|
+
self.config = config
|
|
44
|
+
self.logger = logging.getLogger("python_template_project")
|
|
45
|
+
self.gui_handler = None
|
|
46
|
+
self.file_handler = None
|
|
47
|
+
self.console_handler = None
|
|
48
|
+
self._setup_logging()
|
|
49
|
+
|
|
50
|
+
def _setup_logging(self):
|
|
51
|
+
"""Configure all logging handlers and formatters."""
|
|
52
|
+
# Clear any existing handlers
|
|
53
|
+
self.logger.handlers.clear()
|
|
54
|
+
|
|
55
|
+
# Set log level from config
|
|
56
|
+
log_level = getattr(logging, self.config.app.log_level.default.upper())
|
|
57
|
+
self.logger.setLevel(log_level)
|
|
58
|
+
|
|
59
|
+
# Create formatters
|
|
60
|
+
detailed_formatter = logging.Formatter(
|
|
61
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s"
|
|
62
|
+
)
|
|
63
|
+
simple_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
64
|
+
|
|
65
|
+
# Setup file handler with rotation
|
|
66
|
+
self._setup_file_handler(detailed_formatter)
|
|
67
|
+
|
|
68
|
+
# Setup console handler
|
|
69
|
+
self._setup_console_handler(simple_formatter)
|
|
70
|
+
|
|
71
|
+
# Setup GUI handler (will be connected later if needed)
|
|
72
|
+
self._setup_gui_handler()
|
|
73
|
+
|
|
74
|
+
def _setup_file_handler(self, formatter):
|
|
75
|
+
"""Setup rotating file handler."""
|
|
76
|
+
log_dir = Path("logs")
|
|
77
|
+
log_dir.mkdir(exist_ok=True)
|
|
78
|
+
|
|
79
|
+
log_file = log_dir / "python_template_project.log"
|
|
80
|
+
|
|
81
|
+
# Use RotatingFileHandler to prevent huge log files
|
|
82
|
+
self.file_handler = logging.handlers.RotatingFileHandler(
|
|
83
|
+
log_file,
|
|
84
|
+
maxBytes=10 * 1024 * 1024, # 10MB
|
|
85
|
+
backupCount=5,
|
|
86
|
+
encoding="utf-8",
|
|
87
|
+
)
|
|
88
|
+
self.file_handler.setFormatter(formatter)
|
|
89
|
+
self.logger.addHandler(self.file_handler)
|
|
90
|
+
|
|
91
|
+
def _setup_console_handler(self, formatter):
|
|
92
|
+
"""Setup console handler for CLI output."""
|
|
93
|
+
self.console_handler = logging.StreamHandler(sys.stdout)
|
|
94
|
+
self.console_handler.setFormatter(formatter)
|
|
95
|
+
self.logger.addHandler(self.console_handler)
|
|
96
|
+
|
|
97
|
+
def _setup_gui_handler(self):
|
|
98
|
+
"""Setup GUI handler (initially without writer)."""
|
|
99
|
+
self.gui_handler = GuiLogHandler()
|
|
100
|
+
# Don't add to logger yet - will be done when GUI connects
|
|
101
|
+
|
|
102
|
+
def connect_gui_writer(self, gui_writer):
|
|
103
|
+
"""Connect a GUI writer to the logging system.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
gui_writer: Object with write() method (like the LogHandler from gui.py)
|
|
107
|
+
"""
|
|
108
|
+
if self.gui_handler:
|
|
109
|
+
# Remove old handler if it exists
|
|
110
|
+
if self.gui_handler in self.logger.handlers:
|
|
111
|
+
self.logger.removeHandler(self.gui_handler)
|
|
112
|
+
|
|
113
|
+
# Create new handler with GUI writer
|
|
114
|
+
self.gui_handler = GuiLogHandler(gui_writer)
|
|
115
|
+
self.logger.addHandler(self.gui_handler)
|
|
116
|
+
|
|
117
|
+
def disconnect_gui_writer(self):
|
|
118
|
+
"""Disconnect GUI writer (useful when GUI closes)."""
|
|
119
|
+
if self.gui_handler and self.gui_handler in self.logger.handlers:
|
|
120
|
+
self.logger.removeHandler(self.gui_handler)
|
|
121
|
+
|
|
122
|
+
def get_logger(self, name: str = None) -> logging.Logger:
|
|
123
|
+
"""Get a logger instance.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
name: Logger name (defaults to main project logger)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Logger instance
|
|
130
|
+
"""
|
|
131
|
+
if name:
|
|
132
|
+
return logging.getLogger(f"python_template_project.{name}")
|
|
133
|
+
return self.logger
|
|
134
|
+
|
|
135
|
+
def set_log_level(self, level: str):
|
|
136
|
+
"""Change log level dynamically.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
level: New log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
140
|
+
"""
|
|
141
|
+
log_level = getattr(logging, level.upper())
|
|
142
|
+
self.logger.setLevel(log_level)
|
|
143
|
+
|
|
144
|
+
# Update config
|
|
145
|
+
self.config.app.log_level.default = level.upper()
|
|
146
|
+
|
|
147
|
+
def log_config_summary(self):
|
|
148
|
+
"""Log current configuration summary."""
|
|
149
|
+
self.logger.info("=== Configuration Summary ===")
|
|
150
|
+
self.logger.info(f"Log level: {self.config.app.log_level.default}")
|
|
151
|
+
self.logger.info(f"Input: {self.config.cli.input.default}")
|
|
152
|
+
self.logger.info(f"Output: {self.config.cli.output.default}")
|
|
153
|
+
self.logger.info(f"Max workers: {self.config.app.max_workers.default}")
|
|
154
|
+
self.logger.info("==============================")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# Global logger manager instance
|
|
158
|
+
_logger_manager = None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def initialize_logging(config: ConfigParameterManager) -> LoggerManager:
|
|
162
|
+
"""Initialize the global logging system.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
config: Configuration manager instance
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
LoggerManager instance
|
|
169
|
+
"""
|
|
170
|
+
global _logger_manager
|
|
171
|
+
_logger_manager = LoggerManager(config)
|
|
172
|
+
return _logger_manager
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_logger(name: str = None) -> logging.Logger:
|
|
176
|
+
"""Get a logger instance.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
name: Logger name (optional)
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Logger instance
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
RuntimeError: If logging not initialized
|
|
186
|
+
"""
|
|
187
|
+
if _logger_manager is None:
|
|
188
|
+
raise RuntimeError("Logging not initialized. Call initialize_logging() first.")
|
|
189
|
+
return _logger_manager.get_logger(name)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def get_logger_manager() -> LoggerManager:
|
|
193
|
+
"""Get the global logger manager.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
LoggerManager instance
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
RuntimeError: If logging not initialized
|
|
200
|
+
"""
|
|
201
|
+
if _logger_manager is None:
|
|
202
|
+
raise RuntimeError("Logging not initialized. Call initialize_logging() first.")
|
|
203
|
+
return _logger_manager
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def connect_gui_logging(gui_writer):
|
|
207
|
+
"""Connect GUI writer to logging system.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
gui_writer: GUI writer object with write() method
|
|
211
|
+
"""
|
|
212
|
+
if _logger_manager:
|
|
213
|
+
_logger_manager.connect_gui_writer(gui_writer)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def disconnect_gui_logging():
|
|
217
|
+
"""Disconnect GUI from logging system."""
|
|
218
|
+
if _logger_manager:
|
|
219
|
+
_logger_manager.disconnect_gui_writer()
|
|
File without changes
|