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 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
+ )
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.1.15'
21
- __version_tuple__ = version_tuple = (0, 1, 15)
20
+ __version__ = version = '0.1.18'
21
+ __version_tuple__ = version_tuple = (0, 1, 18)
@@ -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
- 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
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
- return round(point.elevation or 0, 1)
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
- lat1, lon1 = math.radians(point1.latitude), math.radians(point1.longitude)
69
- lat2, lon2 = math.radians(point2.latitude), math.radians(point2.longitude)
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
- dlat = lat2 - lat1
72
- dlon = lon2 - lon1
156
+ dlat = lat2 - lat1
157
+ dlon = lon2 - lon1
73
158
 
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))
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
- # Earth's radius in meters
78
- earth_radius = 6371000
79
- return earth_radius * c
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
- 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)
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
- # 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
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
- 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))
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
- 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...")
313
+ try:
314
+ gpx_files = self._get_gpx_files()
315
+ output_folder = self._get_output_folder()
209
316
 
210
- for gpx_file in gpx_files:
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
- # process and clean waypoints
216
- for waypoint in gpx.waypoints:
217
- self._optimize_waypoint(waypoint)
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
- # Process all tracks
220
- for track in gpx.tracks:
221
- for segment in track.segments:
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
- # Process all routes
225
- for route in gpx.routes:
226
- route.points = self._optimize_track_points(route.points)
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
- # Clean GPX metadata
229
- gpx.time = None
230
- gpx.extensions = None
334
+ # Process all routes
335
+ for route in gpx.routes:
336
+ route.points = self._optimize_track_points(route.points)
231
337
 
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}")
338
+ # Clean GPX metadata
339
+ gpx.time = None
340
+ gpx.extensions = None
236
341
 
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
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
- 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()
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
- if not gpx_files:
265
- self.logger.error("No GPX files found to merge.")
266
- return
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
- self.logger.info(f"Merging {len(gpx_files)} GPX files...")
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
- # 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
- )
366
+ waypoint.elevation = self._get_adjusted_elevation(waypoint)
276
367
 
277
- track_counter = 1
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
- for gpx_file in gpx_files:
280
- gpx = self._load_gpx_file(gpx_file)
281
- if gpx is None:
282
- continue
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
- # 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}")
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
- 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),
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
- 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
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
- # 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}")
539
+ # Clean up temporary files
540
+ temp_dir = Path.cwd() / "temp_gpx_extract"
541
+ if temp_dir.exists():
542
+ import shutil
391
543
 
392
- # Clean up temporary files
393
- temp_dir = Path.cwd() / "temp_gpx_extract"
394
- if temp_dir.exists():
395
- import shutil
544
+ shutil.rmtree(temp_dir)
396
545
 
397
- shutil.rmtree(temp_dir)
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-template-project
3
- Version: 0.1.15
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
- ## Feature overview
48
+ Download from [PyPI](https://pypi.org/).
48
49
 
49
- * **Testing:** Unit testing setup with `pytest`.
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
- /home/runner/work/_temp/xxxx_xxx.sh: line 1: .github/release_message.sh: Permission denied
53
+ pip install python-template-project
90
54
  ```
91
55
 
92
- You have to run these commands in your IDE Terminal or the git bash and then push the changes.
56
+ Run GUI from command line
57
+
93
58
  ```bash
94
- git update-index --chmod=+x ./.github/release_message.sh
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
+
@@ -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=OX-WIjJlMaFvqRmCfLtOYEOYoiov9NdOA089N36rG-g,513
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=0qLUlpc9zhbBgo_PGH-KsGHoNcmGbNPKIuLNpa9mMGI,15196
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.15.dist-info/licenses/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
18
- python_template_project-0.1.15.dist-info/METADATA,sha256=N0SI3VOLkBfla0VrMu1Xj6H0EQJNJhxYwkKSSZdMmOY,5555
19
- python_template_project-0.1.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- python_template_project-0.1.15.dist-info/entry_points.txt,sha256=LmgMiqxIVRdD7gkktVBBR0CR4AhkugqlgifuglkPhtI,148
21
- python_template_project-0.1.15.dist-info/top_level.txt,sha256=9TFlucRiaKsVFCd9mTKCxamJW6LmFnXFXeI7WIvv4AA,38
22
- python_template_project-0.1.15.dist-info/RECORD,,
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,,
@@ -1,3 +1,4 @@
1
1
  __init__
2
+ firewall_handler
2
3
  main
3
4
  python_template_project