python-template-project 0.1.15__py3-none-any.whl → 0.1.18__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.
- firewall_handler.py +198 -0
- python_template_project/_version.py +2 -2
- python_template_project/core/base.py +401 -249
- {python_template_project-0.1.15.dist-info → python_template_project-0.1.18.dist-info}/METADATA +33 -43
- {python_template_project-0.1.15.dist-info → python_template_project-0.1.18.dist-info}/RECORD +9 -8
- {python_template_project-0.1.15.dist-info → python_template_project-0.1.18.dist-info}/top_level.txt +1 -0
- {python_template_project-0.1.15.dist-info → python_template_project-0.1.18.dist-info}/WHEEL +0 -0
- {python_template_project-0.1.15.dist-info → python_template_project-0.1.18.dist-info}/entry_points.txt +0 -0
- {python_template_project-0.1.15.dist-info → python_template_project-0.1.18.dist-info}/licenses/LICENSE +0 -0
firewall_handler.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import socket
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import traceback
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.request
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FirewallHandler:
|
|
12
|
+
"""Handles firewall detection and configuration for network access."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, logger=None):
|
|
15
|
+
self.logger = logger
|
|
16
|
+
self.test_urls = [
|
|
17
|
+
"http://httpbin.org/get",
|
|
18
|
+
"http://www.google.com",
|
|
19
|
+
"https://httpbin.org/get",
|
|
20
|
+
"https://www.google.com",
|
|
21
|
+
]
|
|
22
|
+
self.srtm_urls = [
|
|
23
|
+
"https://cloud.sdsc.edu/v1/AUTH_opentopography/Raster/SRTM_GL1/",
|
|
24
|
+
"http://cloud.sdsc.edu/v1/AUTH_opentopography/Raster/SRTM_GL1/",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
def check_network_access(self, timeout: int = 5) -> bool:
|
|
28
|
+
"""Check if network access is available."""
|
|
29
|
+
self.logger.info("Checking network connectivity...")
|
|
30
|
+
|
|
31
|
+
# First check basic connectivity
|
|
32
|
+
if not self._check_basic_connectivity():
|
|
33
|
+
self.logger.warning("Basic network connectivity failed")
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
# Then check HTTP access
|
|
37
|
+
if not self._check_http_access(timeout):
|
|
38
|
+
self.logger.warning("HTTP access failed")
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
# Finally check SRTM-specific URLs
|
|
42
|
+
if not self._check_srtm_access(timeout):
|
|
43
|
+
self.logger.warning("SRTM service access failed")
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
self.logger.info("Network access verified successfully")
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
def _check_basic_connectivity(self) -> bool:
|
|
50
|
+
"""Check basic network connectivity using socket."""
|
|
51
|
+
try:
|
|
52
|
+
# Try to connect to DNS servers
|
|
53
|
+
for host, port in [("8.8.8.8", 53), ("1.1.1.1", 53)]:
|
|
54
|
+
try:
|
|
55
|
+
with socket.create_connection((host, port), timeout=3):
|
|
56
|
+
return True
|
|
57
|
+
except (TimeoutError, OSError):
|
|
58
|
+
continue
|
|
59
|
+
return False
|
|
60
|
+
except Exception as e:
|
|
61
|
+
self.logger.debug(f"Basic connectivity check failed: {e}")
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def _check_http_access(self, timeout: int) -> bool:
|
|
65
|
+
"""Check HTTP access to common websites."""
|
|
66
|
+
for url in self.test_urls:
|
|
67
|
+
try:
|
|
68
|
+
with urllib.request.urlopen(url, timeout=timeout) as response:
|
|
69
|
+
if response.status == 200:
|
|
70
|
+
self.logger.debug(f"HTTP access confirmed via {url}")
|
|
71
|
+
return True
|
|
72
|
+
except (TimeoutError, urllib.error.URLError, urllib.error.HTTPError) as e:
|
|
73
|
+
self.logger.debug(f"HTTP access failed for {url}: {e}")
|
|
74
|
+
continue
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
def _check_srtm_access(self, timeout: int) -> bool:
|
|
78
|
+
"""Check access to SRTM-specific URLs."""
|
|
79
|
+
for url in self.srtm_urls:
|
|
80
|
+
try:
|
|
81
|
+
with urllib.request.urlopen(url, timeout=timeout) as response:
|
|
82
|
+
if response.status in [200, 403]: # 403 is expected for directory listing
|
|
83
|
+
self.logger.debug(f"SRTM access confirmed via {url}")
|
|
84
|
+
return True
|
|
85
|
+
except (TimeoutError, urllib.error.URLError, urllib.error.HTTPError) as e:
|
|
86
|
+
self.logger.debug(f"SRTM access failed for {url}: {e}")
|
|
87
|
+
continue
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
def handle_firewall_issue(self):
|
|
91
|
+
"""Handle firewall issues by attempting to create rules and inform user."""
|
|
92
|
+
self.logger.info("Attempting to resolve firewall issues...")
|
|
93
|
+
|
|
94
|
+
# Get current executable path
|
|
95
|
+
exe_path = self._get_executable_path()
|
|
96
|
+
if not exe_path:
|
|
97
|
+
self.logger.error("Could not determine executable path")
|
|
98
|
+
self._show_manual_instructions()
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# Try to create firewall rules
|
|
102
|
+
if self._is_windows():
|
|
103
|
+
self._handle_windows_firewall(exe_path)
|
|
104
|
+
else:
|
|
105
|
+
self._handle_linux_firewall(exe_path)
|
|
106
|
+
|
|
107
|
+
def _get_executable_path(self) -> str | None:
|
|
108
|
+
"""Get the path of the current executable."""
|
|
109
|
+
try:
|
|
110
|
+
if getattr(sys, "frozen", False):
|
|
111
|
+
# PyInstaller executable
|
|
112
|
+
return sys.executable
|
|
113
|
+
else:
|
|
114
|
+
# Python script
|
|
115
|
+
return sys.executable
|
|
116
|
+
except Exception as e:
|
|
117
|
+
self.logger.error(f"Error getting executable path: {e}")
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
def _is_windows(self) -> bool:
|
|
121
|
+
"""Check if running on Windows."""
|
|
122
|
+
return os.name == "nt"
|
|
123
|
+
|
|
124
|
+
def _handle_windows_firewall(self, exe_path: str):
|
|
125
|
+
"""Handle Windows firewall configuration."""
|
|
126
|
+
self.logger.info("Attempting to configure Windows Firewall...")
|
|
127
|
+
|
|
128
|
+
app_name = Path(exe_path).stem
|
|
129
|
+
|
|
130
|
+
# Commands to add firewall rules
|
|
131
|
+
commands = [
|
|
132
|
+
# Inbound rule
|
|
133
|
+
[
|
|
134
|
+
"netsh",
|
|
135
|
+
"advfirewall",
|
|
136
|
+
"firewall",
|
|
137
|
+
"add",
|
|
138
|
+
"rule",
|
|
139
|
+
f"name={app_name}_Inbound",
|
|
140
|
+
"dir=in",
|
|
141
|
+
"action=allow",
|
|
142
|
+
f"program={exe_path}",
|
|
143
|
+
"enable=yes",
|
|
144
|
+
],
|
|
145
|
+
# Outbound rule
|
|
146
|
+
[
|
|
147
|
+
"netsh",
|
|
148
|
+
"advfirewall",
|
|
149
|
+
"firewall",
|
|
150
|
+
"add",
|
|
151
|
+
"rule",
|
|
152
|
+
f"name={app_name}_Outbound",
|
|
153
|
+
"dir=out",
|
|
154
|
+
"action=allow",
|
|
155
|
+
f"program={exe_path}",
|
|
156
|
+
"enable=yes",
|
|
157
|
+
],
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
success = False
|
|
161
|
+
for cmd in commands:
|
|
162
|
+
try:
|
|
163
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
164
|
+
self.logger.info(
|
|
165
|
+
f"Firewall rule added successfully: {' '.join(cmd)} result: {result}"
|
|
166
|
+
)
|
|
167
|
+
success = True
|
|
168
|
+
except subprocess.CalledProcessError as e:
|
|
169
|
+
self.logger.warning(f"Failed to add firewall rule: {e}")
|
|
170
|
+
self.logger.debug(f"Command: {' '.join(cmd)}")
|
|
171
|
+
self.logger.debug(f"Error output: {e.stderr}")
|
|
172
|
+
except Exception as e:
|
|
173
|
+
self.logger.error(f"Unexpected error adding firewall rule: {e}")
|
|
174
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
175
|
+
|
|
176
|
+
if success:
|
|
177
|
+
self.logger.info(
|
|
178
|
+
"Windows Firewall rules added successfully. Please restart the application."
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
self.logger.warning("Failed to automatically configure Windows Firewall.")
|
|
182
|
+
self._show_manual_instructions()
|
|
183
|
+
|
|
184
|
+
def _handle_linux_firewall(self, exe_path: str):
|
|
185
|
+
"""Handle Linux firewall configuration."""
|
|
186
|
+
self.logger.info("Detected Linux system. Firewall configuration may be needed.")
|
|
187
|
+
self._show_manual_instructions()
|
|
188
|
+
|
|
189
|
+
def _show_manual_instructions(self):
|
|
190
|
+
"""Show general manual instructions"""
|
|
191
|
+
self.logger.info(
|
|
192
|
+
"Please check your firewall settings and ensure the application has network access.\n"
|
|
193
|
+
"Refer to the documentation for manual configuration steps."
|
|
194
|
+
"quick solution:"
|
|
195
|
+
"Start your EXE as administrator:"
|
|
196
|
+
"Right-click on EXE → Run as administrator"
|
|
197
|
+
"Or temporarily deactivate the firewall:"
|
|
198
|
+
)
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import math
|
|
2
|
+
import traceback
|
|
2
3
|
import zipfile
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
import gpxpy
|
|
7
|
-
import srtm
|
|
8
8
|
from gpxpy.gpx import GPX, GPXTrackPoint, GPXWaypoint, GPXXMLSyntaxException
|
|
9
9
|
|
|
10
|
+
# Optional SRTM import with fallback
|
|
11
|
+
try:
|
|
12
|
+
import srtm
|
|
13
|
+
|
|
14
|
+
SRTM_AVAILABLE = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
SRTM_AVAILABLE = False
|
|
17
|
+
srtm = None
|
|
18
|
+
|
|
10
19
|
NAME = "python_template_project"
|
|
11
20
|
|
|
12
21
|
|
|
@@ -34,11 +43,72 @@ class BaseGPXProcessor:
|
|
|
34
43
|
self.min_dist = min_dist
|
|
35
44
|
self.date_format = date_format
|
|
36
45
|
self.include_elevation = elevation
|
|
37
|
-
|
|
38
|
-
# Initialize SRTM elevation data
|
|
39
|
-
self.elevation_data = srtm.get_data()
|
|
40
46
|
self.logger = logger
|
|
41
47
|
|
|
48
|
+
# Initialize SRTM elevation data only if elevation is requested and SRTM is available
|
|
49
|
+
self.elevation_data = None
|
|
50
|
+
self.srtm_available = False
|
|
51
|
+
|
|
52
|
+
if self.include_elevation:
|
|
53
|
+
self._initialize_elevation_data()
|
|
54
|
+
|
|
55
|
+
def _initialize_elevation_data(self):
|
|
56
|
+
"""Initialize SRTM elevation data with proper error handling."""
|
|
57
|
+
if not SRTM_AVAILABLE:
|
|
58
|
+
self.logger.warning(
|
|
59
|
+
"SRTM library not available. "
|
|
60
|
+
"Elevation data will use original GPX values or default to 0."
|
|
61
|
+
)
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
self.logger.info("Initializing SRTM elevation data...")
|
|
66
|
+
self.elevation_data = srtm.get_data()
|
|
67
|
+
self.srtm_available = True
|
|
68
|
+
self.logger.info("SRTM elevation data initialized successfully.")
|
|
69
|
+
except AssertionError as e:
|
|
70
|
+
self.logger.error(f"SRTM initialization failed with AssertionError: {e}")
|
|
71
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
72
|
+
self._handle_srtm_failure("SRTM assertion failed - possibly network or firewall issue")
|
|
73
|
+
except Exception as e:
|
|
74
|
+
self.logger.error(f"SRTM initialization failed: {e}")
|
|
75
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
76
|
+
self._handle_srtm_failure(f"SRTM initialization error: {str(e)}")
|
|
77
|
+
|
|
78
|
+
def _handle_srtm_failure(self, error_msg: str):
|
|
79
|
+
"""Handle SRTM initialization failure with firewall check."""
|
|
80
|
+
self.srtm_available = False
|
|
81
|
+
self.elevation_data = None
|
|
82
|
+
|
|
83
|
+
self.logger.warning(f"SRTM elevation data unavailable: {error_msg}")
|
|
84
|
+
self.logger.info("Checking for network/firewall issues...")
|
|
85
|
+
|
|
86
|
+
# Import and use firewall handler
|
|
87
|
+
try:
|
|
88
|
+
from firewall_handler import FirewallHandler
|
|
89
|
+
|
|
90
|
+
firewall_handler = FirewallHandler(logger=self.logger)
|
|
91
|
+
|
|
92
|
+
if not firewall_handler.check_network_access():
|
|
93
|
+
self.logger.warning("Network access appears to be blocked.")
|
|
94
|
+
firewall_handler.handle_firewall_issue()
|
|
95
|
+
else:
|
|
96
|
+
self.logger.info(
|
|
97
|
+
"Network access seems available. SRTM issue may be service-related."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
except ImportError:
|
|
101
|
+
self.logger.warning(
|
|
102
|
+
"FirewallHandler not available. Continuing without network diagnostics."
|
|
103
|
+
)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
self.logger.error(f"Error during firewall check: {e}")
|
|
106
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
107
|
+
|
|
108
|
+
self.logger.info(
|
|
109
|
+
"Continuing in offline mode - using original elevation data from GPX files."
|
|
110
|
+
)
|
|
111
|
+
|
|
42
112
|
def _get_output_folder(self) -> Path:
|
|
43
113
|
"""Get the output folder path, create if not exists."""
|
|
44
114
|
if self.output:
|
|
@@ -52,31 +122,49 @@ class BaseGPXProcessor:
|
|
|
52
122
|
|
|
53
123
|
def _get_adjusted_elevation(self, point: GPXTrackPoint) -> int | float:
|
|
54
124
|
"""Get adjusted elevation from SRTM data, fallback to original elevation."""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
125
|
+
# If elevation is not requested, return None
|
|
126
|
+
if not self.include_elevation:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
# If SRTM is available and working, try to get elevation data
|
|
130
|
+
if self.srtm_available and self.elevation_data:
|
|
131
|
+
try:
|
|
132
|
+
srtm_elevation = self.elevation_data.get_elevation(point.latitude, point.longitude)
|
|
133
|
+
if srtm_elevation is not None:
|
|
134
|
+
return round(srtm_elevation, 1)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
self.logger.info(
|
|
137
|
+
f"Error getting SRTM elevation "
|
|
138
|
+
f"for point ({point.latitude}, {point.longitude}): {e}"
|
|
139
|
+
)
|
|
140
|
+
# Don't disable SRTM entirely for single point failures
|
|
141
|
+
pass
|
|
61
142
|
|
|
62
143
|
# Fallback to original elevation or 0
|
|
63
|
-
|
|
144
|
+
original_elevation = (
|
|
145
|
+
point.elevation if hasattr(point, "elevation") and point.elevation is not None else 0
|
|
146
|
+
)
|
|
147
|
+
return round(original_elevation, 1)
|
|
64
148
|
|
|
65
149
|
@staticmethod
|
|
66
150
|
def _calculate_distance(point1: GPXTrackPoint, point2: GPXTrackPoint) -> float:
|
|
67
151
|
"""Calculate distance between two GPX points in meters using Haversine formula."""
|
|
68
|
-
|
|
69
|
-
|
|
152
|
+
try:
|
|
153
|
+
lat1, lon1 = math.radians(point1.latitude), math.radians(point1.longitude)
|
|
154
|
+
lat2, lon2 = math.radians(point2.latitude), math.radians(point2.longitude)
|
|
70
155
|
|
|
71
|
-
|
|
72
|
-
|
|
156
|
+
dlat = lat2 - lat1
|
|
157
|
+
dlon = lon2 - lon1
|
|
73
158
|
|
|
74
|
-
|
|
75
|
-
|
|
159
|
+
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
|
|
160
|
+
c = 2 * math.asin(math.sqrt(a))
|
|
76
161
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
162
|
+
# Earth's radius in meters
|
|
163
|
+
earth_radius = 6371000
|
|
164
|
+
return earth_radius * c
|
|
165
|
+
except (AttributeError, TypeError, ValueError):
|
|
166
|
+
# Handle cases where points might have invalid coordinates
|
|
167
|
+
return 0.0
|
|
80
168
|
|
|
81
169
|
def _optimize_track_points(
|
|
82
170
|
self, track_points: list[GPXTrackPoint] | list[GPXWaypoint]
|
|
@@ -85,70 +173,86 @@ class BaseGPXProcessor:
|
|
|
85
173
|
if not track_points:
|
|
86
174
|
return track_points
|
|
87
175
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
176
|
+
try:
|
|
177
|
+
optimized_points = [track_points[0]] # Always keep first point
|
|
178
|
+
|
|
179
|
+
for point in track_points[1:]:
|
|
180
|
+
# Check distance to last kept point
|
|
181
|
+
if self._calculate_distance(optimized_points[-1], point) >= self.min_dist:
|
|
182
|
+
optimized_points.append(point)
|
|
183
|
+
|
|
184
|
+
# Always keep last point if it's different from the last kept point
|
|
185
|
+
if len(track_points) > 1 and optimized_points[-1] != track_points[-1]:
|
|
186
|
+
optimized_points.append(track_points[-1])
|
|
187
|
+
|
|
188
|
+
# Clean and optimize each point
|
|
189
|
+
for point in optimized_points:
|
|
190
|
+
try:
|
|
191
|
+
# Remove time information
|
|
192
|
+
point.time = None
|
|
193
|
+
|
|
194
|
+
# Round coordinates to 5 decimal places
|
|
195
|
+
if hasattr(point, "latitude") and point.latitude is not None:
|
|
196
|
+
point.latitude = round(point.latitude, 5)
|
|
197
|
+
if hasattr(point, "longitude") and point.longitude is not None:
|
|
198
|
+
point.longitude = round(point.longitude, 5)
|
|
199
|
+
|
|
200
|
+
# Set optimized elevation
|
|
201
|
+
point.elevation = self._get_adjusted_elevation(point)
|
|
202
|
+
|
|
203
|
+
# Remove unnecessary extensions and metadata
|
|
204
|
+
point.extensions = None
|
|
205
|
+
if hasattr(point, "symbol"):
|
|
206
|
+
point.symbol = None
|
|
207
|
+
if hasattr(point, "type"):
|
|
208
|
+
point.type = None
|
|
209
|
+
point.comment = None
|
|
210
|
+
point.description = None
|
|
211
|
+
point.source = None
|
|
212
|
+
point.link = None
|
|
213
|
+
point.link_text = None
|
|
214
|
+
point.link_type = None
|
|
215
|
+
point.horizontal_dilution = None
|
|
216
|
+
point.vertical_dilution = None
|
|
217
|
+
point.position_dilution = None
|
|
218
|
+
point.age_of_dgps_data = None
|
|
219
|
+
point.dgps_id = None
|
|
220
|
+
except Exception as e:
|
|
221
|
+
self.logger.warning(f"Error optimizing point: {e}")
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
return optimized_points
|
|
107
225
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
226
|
+
except Exception as e:
|
|
227
|
+
self.logger.error(f"Error optimizing track points: {e}")
|
|
228
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
229
|
+
return track_points # Return original points if optimization fails
|
|
131
230
|
|
|
132
231
|
def _get_gpx_files(self) -> list[Path]:
|
|
133
232
|
"""Get all GPX files from input (file, folder, or zip)."""
|
|
134
233
|
gpx_files = []
|
|
135
234
|
for input_path in self.input:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if input_path.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
235
|
+
try:
|
|
236
|
+
if not isinstance(input_path, Path):
|
|
237
|
+
input_path = Path(input_path)
|
|
238
|
+
self.logger.debug(f"Input path: {input_path.absolute()}")
|
|
239
|
+
|
|
240
|
+
if input_path.is_file():
|
|
241
|
+
if input_path.suffix.lower() == ".gpx":
|
|
242
|
+
gpx_files.append(input_path)
|
|
243
|
+
elif input_path.suffix.lower() == ".zip":
|
|
244
|
+
gpx_files.extend(self._extract_gpx_from_zip(input_path))
|
|
245
|
+
elif input_path.is_dir():
|
|
246
|
+
# Get all GPX files in directory
|
|
247
|
+
gpx_files.extend(input_path.glob("*.gpx"))
|
|
248
|
+
|
|
249
|
+
# Get GPX files from ZIP files in directory
|
|
250
|
+
for zip_file in input_path.glob("*.zip"):
|
|
251
|
+
gpx_files.extend(self._extract_gpx_from_zip(zip_file))
|
|
252
|
+
except Exception as e:
|
|
253
|
+
self.logger.error(f"Error processing input path {input_path}: {e}")
|
|
254
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
255
|
+
continue
|
|
152
256
|
|
|
153
257
|
return gpx_files
|
|
154
258
|
|
|
@@ -156,9 +260,10 @@ class BaseGPXProcessor:
|
|
|
156
260
|
"""Extract GPX files from ZIP archive to temporary location."""
|
|
157
261
|
gpx_files = []
|
|
158
262
|
temp_dir = Path.cwd() / "temp_gpx_extract"
|
|
159
|
-
temp_dir.mkdir(exist_ok=True)
|
|
160
263
|
|
|
161
264
|
try:
|
|
265
|
+
temp_dir.mkdir(exist_ok=True)
|
|
266
|
+
|
|
162
267
|
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
|
163
268
|
for file_info in zip_ref.infolist():
|
|
164
269
|
if file_info.filename.lower().endswith(".gpx"):
|
|
@@ -168,6 +273,7 @@ class BaseGPXProcessor:
|
|
|
168
273
|
gpx_files.append(extracted_path)
|
|
169
274
|
except Exception as e:
|
|
170
275
|
self.logger.error(f"Error extracting ZIP file {zip_path}: {e}")
|
|
276
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
171
277
|
|
|
172
278
|
return gpx_files
|
|
173
279
|
|
|
@@ -181,6 +287,7 @@ class BaseGPXProcessor:
|
|
|
181
287
|
return None
|
|
182
288
|
except Exception as e:
|
|
183
289
|
self.logger.error(f"Error loading GPX file {gpx_path}: {e}")
|
|
290
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
184
291
|
return None
|
|
185
292
|
|
|
186
293
|
def _save_gpx_file(self, gpx: GPX, output_path: Path, original_file: Path | None = None):
|
|
@@ -199,199 +306,244 @@ class BaseGPXProcessor:
|
|
|
199
306
|
|
|
200
307
|
except Exception as e:
|
|
201
308
|
self.logger.error(f"Error saving GPX file {output_path}: {e}")
|
|
309
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
202
310
|
|
|
203
311
|
def compress_files(self):
|
|
204
312
|
"""Shrink the size of all given gpx files in self.input."""
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
self.logger.info(f"Processing {len(gpx_files)} GPX files...")
|
|
313
|
+
try:
|
|
314
|
+
gpx_files = self._get_gpx_files()
|
|
315
|
+
output_folder = self._get_output_folder()
|
|
209
316
|
|
|
210
|
-
|
|
211
|
-
gpx = self._load_gpx_file(gpx_file)
|
|
212
|
-
if gpx is None:
|
|
213
|
-
continue
|
|
317
|
+
self.logger.info(f"Processing {len(gpx_files)} GPX files...")
|
|
214
318
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
319
|
+
for gpx_file in gpx_files:
|
|
320
|
+
try:
|
|
321
|
+
gpx = self._load_gpx_file(gpx_file)
|
|
322
|
+
if gpx is None:
|
|
323
|
+
continue
|
|
218
324
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
segment.points = self._optimize_track_points(segment.points)
|
|
325
|
+
# process and clean waypoints
|
|
326
|
+
for waypoint in gpx.waypoints:
|
|
327
|
+
self._optimize_waypoint(waypoint)
|
|
223
328
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
329
|
+
# Process all tracks
|
|
330
|
+
for track in gpx.tracks:
|
|
331
|
+
for segment in track.segments:
|
|
332
|
+
segment.points = self._optimize_track_points(segment.points)
|
|
227
333
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
334
|
+
# Process all routes
|
|
335
|
+
for route in gpx.routes:
|
|
336
|
+
route.points = self._optimize_track_points(route.points)
|
|
231
337
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
self.logger.info(f"Compressed: {gpx_file.name} -> {output_path}")
|
|
338
|
+
# Clean GPX metadata
|
|
339
|
+
gpx.time = None
|
|
340
|
+
gpx.extensions = None
|
|
236
341
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
342
|
+
# Save compressed file
|
|
343
|
+
output_path = output_folder / f"compressed_{gpx_file.name}"
|
|
344
|
+
self._save_gpx_file(gpx, output_path, gpx_file)
|
|
345
|
+
self.logger.info(f"Compressed: {gpx_file.name} -> {output_path}")
|
|
258
346
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
347
|
+
except Exception as e:
|
|
348
|
+
self.logger.error(f"Error processing file {gpx_file}: {e}")
|
|
349
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
350
|
+
continue
|
|
263
351
|
|
|
264
|
-
|
|
265
|
-
self.logger.error("
|
|
266
|
-
|
|
352
|
+
except Exception as e:
|
|
353
|
+
self.logger.error(f"Error in compress_files: {e}")
|
|
354
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
355
|
+
raise
|
|
267
356
|
|
|
268
|
-
|
|
357
|
+
def _optimize_waypoint(self, waypoint):
|
|
358
|
+
"""Optimize waypoint with error handling."""
|
|
359
|
+
try:
|
|
360
|
+
# Round coordinates and elevation
|
|
361
|
+
if hasattr(waypoint, "latitude") and waypoint.latitude is not None:
|
|
362
|
+
waypoint.latitude = round(waypoint.latitude, 5)
|
|
363
|
+
if hasattr(waypoint, "longitude") and waypoint.longitude is not None:
|
|
364
|
+
waypoint.longitude = round(waypoint.longitude, 5)
|
|
269
365
|
|
|
270
|
-
|
|
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
|
-
)
|
|
366
|
+
waypoint.elevation = self._get_adjusted_elevation(waypoint)
|
|
276
367
|
|
|
277
|
-
|
|
368
|
+
# Clean metadata
|
|
369
|
+
waypoint.time = None
|
|
370
|
+
waypoint.extensions = None
|
|
371
|
+
if hasattr(waypoint, "symbol"):
|
|
372
|
+
waypoint.symbol = None
|
|
373
|
+
if hasattr(waypoint, "type"):
|
|
374
|
+
waypoint.type = None
|
|
375
|
+
waypoint.comment = None
|
|
376
|
+
waypoint.description = None
|
|
377
|
+
waypoint.source = None
|
|
378
|
+
waypoint.link = None
|
|
379
|
+
waypoint.link_text = None
|
|
380
|
+
waypoint.link_type = None
|
|
381
|
+
return waypoint
|
|
382
|
+
except Exception as e:
|
|
383
|
+
self.logger.warning(f"Error optimizing waypoint: {e}")
|
|
384
|
+
return waypoint
|
|
278
385
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
386
|
+
def merge_files(self):
|
|
387
|
+
"""Merge all files of self.input into one gpx file with reduced resolution."""
|
|
388
|
+
try:
|
|
389
|
+
gpx_files = self._get_gpx_files()
|
|
390
|
+
output_folder = self._get_output_folder()
|
|
391
|
+
|
|
392
|
+
if not gpx_files:
|
|
393
|
+
self.logger.error("No GPX files found to merge.")
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
self.logger.info(f"Merging {len(gpx_files)} GPX files...")
|
|
397
|
+
|
|
398
|
+
# Create new GPX object
|
|
399
|
+
merged_gpx = gpxpy.gpx.GPX()
|
|
400
|
+
merged_gpx.name = "Merged GPX Tracks"
|
|
401
|
+
merged_gpx.description = f"Merged from {len(gpx_files)} GPX files."
|
|
402
|
+
|
|
403
|
+
track_counter = 1
|
|
404
|
+
|
|
405
|
+
for gpx_file in gpx_files:
|
|
406
|
+
try:
|
|
407
|
+
gpx = self._load_gpx_file(gpx_file)
|
|
408
|
+
if gpx is None:
|
|
409
|
+
continue
|
|
410
|
+
|
|
411
|
+
# Add all waypoints from this file
|
|
412
|
+
for waypoint in gpx.waypoints:
|
|
413
|
+
waypoint = self._optimize_waypoint(waypoint)
|
|
414
|
+
waypoint.name = f"{waypoint.name}_{track_counter}"
|
|
415
|
+
merged_gpx.waypoints.append(waypoint)
|
|
416
|
+
|
|
417
|
+
# Add all tracks from this file
|
|
418
|
+
for track in gpx.tracks:
|
|
419
|
+
new_track = gpxpy.gpx.GPXTrack()
|
|
420
|
+
new_track.name = f"{track.name or track_counter}_{gpx_file.stem}"
|
|
421
|
+
|
|
422
|
+
for segment in track.segments:
|
|
423
|
+
optimized_points = self._optimize_track_points(segment.points)
|
|
424
|
+
if optimized_points:
|
|
425
|
+
new_segment = gpxpy.gpx.GPXTrackSegment()
|
|
426
|
+
new_segment.points = optimized_points
|
|
427
|
+
new_track.segments.append(new_segment)
|
|
428
|
+
|
|
429
|
+
if new_track.segments:
|
|
430
|
+
merged_gpx.tracks.append(new_track)
|
|
431
|
+
track_counter += 1
|
|
432
|
+
|
|
433
|
+
# Add all routes from this file
|
|
434
|
+
for route in gpx.routes:
|
|
435
|
+
new_route = gpxpy.gpx.GPXRoute()
|
|
436
|
+
new_route.name = f"{route.name or track_counter}_{gpx_file.stem}"
|
|
437
|
+
new_route.points = self._optimize_track_points(route.points)
|
|
438
|
+
|
|
439
|
+
if new_route.points:
|
|
440
|
+
merged_gpx.routes.append(new_route)
|
|
441
|
+
track_counter += 1
|
|
442
|
+
|
|
443
|
+
except Exception as e:
|
|
444
|
+
self.logger.error(f"Error processing file {gpx_file} during merge: {e}")
|
|
445
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
446
|
+
continue
|
|
447
|
+
|
|
448
|
+
# Save merged file
|
|
449
|
+
output_path = output_folder / "merged_tracks.gpx"
|
|
450
|
+
self._save_gpx_file(merged_gpx, output_path)
|
|
451
|
+
self.logger.info(f"Merged file saved: {output_path}")
|
|
283
452
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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}")
|
|
453
|
+
except Exception as e:
|
|
454
|
+
self.logger.error(f"Error in merge_files: {e}")
|
|
455
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
456
|
+
raise
|
|
320
457
|
|
|
321
458
|
def extract_pois(self):
|
|
322
459
|
"""Merge every starting point of each track in all files
|
|
323
460
|
into one gpx file with many pois."""
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
461
|
+
try:
|
|
462
|
+
gpx_files = self._get_gpx_files()
|
|
463
|
+
output_folder = self._get_output_folder()
|
|
464
|
+
|
|
465
|
+
if not gpx_files:
|
|
466
|
+
self.logger.error("No GPX files found to extract POIs from.")
|
|
467
|
+
return
|
|
468
|
+
|
|
469
|
+
self.logger.info(f"Extracting POIs from {len(gpx_files)} GPX files...")
|
|
470
|
+
|
|
471
|
+
# Create new GPX object for waypoints
|
|
472
|
+
poi_gpx = gpxpy.gpx.GPX()
|
|
473
|
+
poi_gpx.name = "Extracted Track Starting Points"
|
|
474
|
+
poi_gpx.description = f"Starting points extracted from {len(gpx_files)} GPX files"
|
|
475
|
+
|
|
476
|
+
poi_counter = 1
|
|
477
|
+
|
|
478
|
+
for gpx_file in gpx_files:
|
|
479
|
+
try:
|
|
480
|
+
gpx = self._load_gpx_file(gpx_file)
|
|
481
|
+
if gpx is None:
|
|
482
|
+
continue
|
|
483
|
+
|
|
484
|
+
# Extract starting points from tracks
|
|
485
|
+
for track_idx, track in enumerate(gpx.tracks):
|
|
486
|
+
for _segment_idx, segment in enumerate(track.segments):
|
|
487
|
+
if segment.points:
|
|
488
|
+
start_point = segment.points[0]
|
|
489
|
+
|
|
490
|
+
# Create waypoint from starting point
|
|
491
|
+
waypoint = gpxpy.gpx.GPXWaypoint(
|
|
492
|
+
latitude=round(start_point.latitude, 5),
|
|
493
|
+
longitude=round(start_point.longitude, 5),
|
|
494
|
+
elevation=self._get_adjusted_elevation(start_point),
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
track_name = track.name or f"Track_{track_idx + 1}"
|
|
498
|
+
waypoint.name = f"POI_{poi_counter:03d}"
|
|
499
|
+
waypoint.description = f"Start of {track_name} from {gpx_file.name}"
|
|
500
|
+
waypoint.type = "Track Start"
|
|
501
|
+
|
|
502
|
+
poi_gpx.waypoints.append(waypoint)
|
|
503
|
+
poi_counter += 1
|
|
504
|
+
|
|
505
|
+
# Extract starting points from routes
|
|
506
|
+
for route_idx, route in enumerate(gpx.routes):
|
|
507
|
+
if route.points:
|
|
508
|
+
start_point = route.points[0]
|
|
509
|
+
|
|
510
|
+
# Create waypoint from starting point
|
|
511
|
+
waypoint = gpxpy.gpx.GPXWaypoint(
|
|
512
|
+
latitude=round(start_point.latitude, 5),
|
|
513
|
+
longitude=round(start_point.longitude, 5),
|
|
514
|
+
elevation=self._get_adjusted_elevation(start_point),
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
route_name = route.name or f"Route_{route_idx + 1}"
|
|
518
|
+
waypoint.name = f"POI_{poi_counter:03d}"
|
|
519
|
+
waypoint.description = f"Start of {route_name} from {gpx_file.name}"
|
|
520
|
+
waypoint.type = "Route Start"
|
|
521
|
+
|
|
522
|
+
poi_gpx.waypoints.append(waypoint)
|
|
523
|
+
poi_counter += 1
|
|
524
|
+
|
|
525
|
+
except Exception as e:
|
|
526
|
+
self.logger.error(
|
|
527
|
+
f"Error processing file {gpx_file} during POI extraction: {e}"
|
|
377
528
|
)
|
|
529
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
530
|
+
continue
|
|
378
531
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
poi_counter += 1
|
|
532
|
+
# Save POI file
|
|
533
|
+
output_path = output_folder / "extracted_pois.gpx"
|
|
534
|
+
self._save_gpx_file(poi_gpx, output_path)
|
|
535
|
+
self.logger.info(
|
|
536
|
+
f"POI file saved with {len(poi_gpx.waypoints)} waypoints: {output_path}"
|
|
537
|
+
)
|
|
386
538
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
539
|
+
# Clean up temporary files
|
|
540
|
+
temp_dir = Path.cwd() / "temp_gpx_extract"
|
|
541
|
+
if temp_dir.exists():
|
|
542
|
+
import shutil
|
|
391
543
|
|
|
392
|
-
|
|
393
|
-
temp_dir = Path.cwd() / "temp_gpx_extract"
|
|
394
|
-
if temp_dir.exists():
|
|
395
|
-
import shutil
|
|
544
|
+
shutil.rmtree(temp_dir)
|
|
396
545
|
|
|
397
|
-
|
|
546
|
+
except Exception as e:
|
|
547
|
+
self.logger.error(f"Error in extract_pois: {e}")
|
|
548
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
549
|
+
raise
|
{python_template_project-0.1.15.dist-info → python_template_project-0.1.18.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-template-project
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.18
|
|
4
4
|
Summary: Feature-rich Python project template designed for robustness and ease of use.
|
|
5
5
|
Author: pamagister
|
|
6
6
|
Requires-Python: <3.12,>=3.10
|
|
@@ -43,54 +43,44 @@ A feature-rich Python project template with with auto-generated CLI, GUI and par
|
|
|
43
43
|
|
|
44
44
|
This template provides a solid foundation for your next Python project, incorporating best practices for testing, automation, and distribution. It streamlines the development process with a comprehensive set of pre-configured tools and workflows, allowing you to focus on writing code.
|
|
45
45
|
|
|
46
|
+
## Installation
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
Download from [PyPI](https://pypi.org/).
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
* **CI/CD:** [GitHub Actions](https://github.com/features/actions) for automated builds (Windows, macOS), unit tests, and code checks.
|
|
51
|
-
* **Code Formatting and Linting:** Pre-commit hook with the [RUFF auto-formatter](https://docs.astral.sh/ruff/) to ensure consistent code style.
|
|
52
|
-
* **Automated Builds:** GitHub pipeline for automatically building a Windows executable and a macOS installer.
|
|
53
|
-
* **Package Management:** Utilizes [uv](https://docs.astral.sh/uv/getting-started/), an extremely fast Python package manager, with dependencies managed in `pyproject.toml`.
|
|
54
|
-
* **Parameter-Driven Automation:**
|
|
55
|
-
* Automatic generation of a configuration file from parameter definitions.
|
|
56
|
-
* Automatic generation of a Command-Line Interface (CLI) from the same parameters.
|
|
57
|
-
* Automatic generation of CLI API documentation.
|
|
58
|
-
* **Documentation:** Configuration for publishing documentation on [Read the Docs](https://about.readthedocs.com/) using [mkdocs](https://www.mkdocs.org/) .
|
|
59
|
-
* **Workflow Automation:** A `Makefile` is included to simplify and automate common development tasks.
|
|
60
|
-
* **Minimalist GUI:** Comes with a basic GUI based on [tkinker](https://tkdocs.com/tutorial/index.html) that includes an auto-generated settings menu based on your defined parameters.
|
|
61
|
-
* **Release pipeline:** Automated releases unsing the Makefile `make release` command, which creates a new tag and pushes it to the remote repo. The `release` pipeline will automatically create a new release on GitHub and trigger a release on PyPI.
|
|
62
|
-
* **[setuptools](https://pypi.org/project/setuptools/)** is used to package the project and manage dependencies.
|
|
63
|
-
* **[setuptools-scm](https://pypi.org/project/setuptools-scm/)** is used to automatically generate the `_version.py` file from the `pyproject.toml` file.
|
|
50
|
+
💾 For more installation options see [install](getting-started/install.md).
|
|
64
51
|
|
|
65
|
-
## Getting started
|
|
66
|
-
|
|
67
|
-
1. **Create** a new [github repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-new-repository).
|
|
68
|
-
2. **Clone** [github repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) the into your local file system.
|
|
69
|
-
```bash
|
|
70
|
-
git clone https://github.com/YOUR-USERNAME/YOUR-REPOSITORY
|
|
71
|
-
```
|
|
72
|
-
4. **Install** [uv package and project manager](https://docs.astral.sh/uv/getting-started/installation/)
|
|
73
|
-
```bash
|
|
74
|
-
pip install uv
|
|
75
|
-
```
|
|
76
|
-
5. **Generate** [a sample project](https://docs.astral.sh/uv/guides/projects/#creating-a-new-project) with uv
|
|
77
|
-
6. Add and **push** the artifacts to your github repository
|
|
78
|
-
```bash
|
|
79
|
-
git push
|
|
80
|
-
```
|
|
81
|
-
8. Copy the content of this repository into your repository
|
|
82
|
-
|
|
83
|
-
## Troubleshooting
|
|
84
|
-
|
|
85
|
-
### Problems with release pipeline
|
|
86
|
-
|
|
87
|
-
If you get this error below:
|
|
88
52
|
```bash
|
|
89
|
-
|
|
53
|
+
pip install python-template-project
|
|
90
54
|
```
|
|
91
55
|
|
|
92
|
-
|
|
56
|
+
Run GUI from command line
|
|
57
|
+
|
|
93
58
|
```bash
|
|
94
|
-
|
|
59
|
+
python-template-project-gui
|
|
95
60
|
```
|
|
96
61
|
|
|
62
|
+
## How to use this template
|
|
63
|
+
|
|
64
|
+
🐍 For details, see the [Getting Started](develop/01_getting_started_dev.md) guide.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## Feature overview
|
|
68
|
+
|
|
69
|
+
* 📦 **Package Management:** Utilizes [uv](https://docs.astral.sh/uv/getting-started/), an extremely fast Python package manager, with dependencies managed in `pyproject.toml`.
|
|
70
|
+
* ✅ **Code Formatting and Linting:** Pre-commit hook with the [RUFF auto-formatter](https://docs.astral.sh/ruff/) to ensure consistent code style.
|
|
71
|
+
* 🧪 **Testing:** Unit testing framework with [pytest](https://docs.pytest.org/en/latest/).
|
|
72
|
+
* 📊 **Code coverage reports** using [codecov](https://about.codecov.io/sign-up/)
|
|
73
|
+
* 🔄 **CI/CD:** [GitHub Actions](https://github.com/features/actions) for automated builds (Windows, macOS), unit tests, and code checks.
|
|
74
|
+
* 💾 **Automated Builds:** GitHub pipeline for automatically building a Windows executable and a macOS installer.
|
|
75
|
+
* 💬 **Parameter-Driven Automation:**
|
|
76
|
+
* Automatic generation of a configuration file from parameter definitions.
|
|
77
|
+
* Automatic generation of a Command-Line Interface (CLI) from the same parameters.
|
|
78
|
+
* Automatic generation of CLI API documentation.
|
|
79
|
+
* Automatic generation of change log using **gitchangelog** to keep a HISTORY.md file up to date.
|
|
80
|
+
* 📃 **Documentation:** Configuration for publishing documentation on [Read the Docs](https://about.readthedocs.com/) using [mkdocs](https://www.mkdocs.org/) .
|
|
81
|
+
* 🖼️ **Minimalist GUI:** Comes with a basic GUI based on [tkinker](https://tkdocs.com/tutorial/index.html) that includes an auto-generated settings menu based on your defined parameters.
|
|
82
|
+
* 🖥️ **Workflow Automation:** A `Makefile` is included to simplify and automate common development tasks.
|
|
83
|
+
* 🛳️ **Release pipeline:** Automated releases unsing the Makefile `make release` command, which creates a new tag and pushes it to the remote repo. The `release` pipeline will automatically create a new release on GitHub and trigger a release on [PyPI](https://pypi.org.
|
|
84
|
+
* **[setuptools](https://pypi.org/project/setuptools/)** is used to package the project and manage dependencies.
|
|
85
|
+
* **[setuptools-scm](https://pypi.org/project/setuptools-scm/)** is used to automatically generate the `_version.py` file from the `pyproject.toml` file.
|
|
86
|
+
|
{python_template_project-0.1.15.dist-info → python_template_project-0.1.18.dist-info}/RECORD
RENAMED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
firewall_handler.py,sha256=qT0dInkeKHulpCQD2x3HH7_lMxz4lwdkIPxEvBSl3No,7336
|
|
2
3
|
main.py,sha256=d6ZpnMxcPBf860gQmuYwPVJg1di6r2CR6gSvFLkAZkk,4448
|
|
3
4
|
python_template_project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
5
|
python_template_project/__main__.py,sha256=JS28LiHoU9Xo4c_RvyLBTDKHqdZRRytJLj6qyrg26mQ,195
|
|
5
|
-
python_template_project/_version.py,sha256=
|
|
6
|
+
python_template_project/_version.py,sha256=mhAYdSWzD7l6mXhR3foHESbkj_QM4Asoa3WlsLLYkfM,513
|
|
6
7
|
python_template_project/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
8
|
python_template_project/cli/__main__.py,sha256=qnGkoNNbhnvLTA24X8YTEDxRqDbNEAzgjp9Ki_JYIhs,184
|
|
8
9
|
python_template_project/cli/cli.py,sha256=RvoYKJDjP-PYlX4m5xFnUsuHVJw_3-AgpQwOxhPAH5g,6974
|
|
9
10
|
python_template_project/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
11
|
python_template_project/config/config.py,sha256=bVqFIY8oScfhv2LpjcNoyiGqrIyVz5Ri3VjVnem1ZPA,17924
|
|
11
12
|
python_template_project/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
python_template_project/core/base.py,sha256=
|
|
13
|
+
python_template_project/core/base.py,sha256=qZiQzA9ib-bxH-f3p9Rxzj8DD6nQQnhykd_Gqg4B3a4,22886
|
|
13
14
|
python_template_project/core/logging.py,sha256=D99ZAXY9KZvmV7oVlAy3S3fuDf9G7vHvIk277xB7068,6944
|
|
14
15
|
python_template_project/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
16
|
python_template_project/gui/__main__.py,sha256=5AaiBxNCAKWcuSQFcQ_lsEQfEsXm6BA5-i5MScdf30c,184
|
|
16
17
|
python_template_project/gui/gui.py,sha256=qJ9SawFywwT7ffPndTzhuLEu2KkzjBspmSccAXgUC2k,21725
|
|
17
|
-
python_template_project-0.1.
|
|
18
|
-
python_template_project-0.1.
|
|
19
|
-
python_template_project-0.1.
|
|
20
|
-
python_template_project-0.1.
|
|
21
|
-
python_template_project-0.1.
|
|
22
|
-
python_template_project-0.1.
|
|
18
|
+
python_template_project-0.1.18.dist-info/licenses/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
|
|
19
|
+
python_template_project-0.1.18.dist-info/METADATA,sha256=jAhxpJFP6uHiFbIqV8YtMjiPNJ0P_aqrQI0ZdPjlJJ8,5059
|
|
20
|
+
python_template_project-0.1.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
python_template_project-0.1.18.dist-info/entry_points.txt,sha256=LmgMiqxIVRdD7gkktVBBR0CR4AhkugqlgifuglkPhtI,148
|
|
22
|
+
python_template_project-0.1.18.dist-info/top_level.txt,sha256=O-Yg0tzEwgH58MhdcYWpJsgZSUPg6SBbOUwlmVHZo3A,55
|
|
23
|
+
python_template_project-0.1.18.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|