BERATools 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.
Files changed (44) hide show
  1. beratools/__init__.py +3 -0
  2. beratools/core/__init__.py +0 -0
  3. beratools/core/algo_centerline.py +476 -0
  4. beratools/core/algo_common.py +489 -0
  5. beratools/core/algo_cost.py +185 -0
  6. beratools/core/algo_dijkstra.py +492 -0
  7. beratools/core/algo_footprint_rel.py +693 -0
  8. beratools/core/algo_line_grouping.py +941 -0
  9. beratools/core/algo_merge_lines.py +255 -0
  10. beratools/core/algo_split_with_lines.py +296 -0
  11. beratools/core/algo_vertex_optimization.py +451 -0
  12. beratools/core/constants.py +56 -0
  13. beratools/core/logger.py +92 -0
  14. beratools/core/tool_base.py +126 -0
  15. beratools/gui/__init__.py +11 -0
  16. beratools/gui/assets/BERALogo.png +0 -0
  17. beratools/gui/assets/beratools.json +471 -0
  18. beratools/gui/assets/closed.gif +0 -0
  19. beratools/gui/assets/closed.png +0 -0
  20. beratools/gui/assets/gui.json +8 -0
  21. beratools/gui/assets/open.gif +0 -0
  22. beratools/gui/assets/open.png +0 -0
  23. beratools/gui/assets/tool.gif +0 -0
  24. beratools/gui/assets/tool.png +0 -0
  25. beratools/gui/bt_data.py +485 -0
  26. beratools/gui/bt_gui_main.py +700 -0
  27. beratools/gui/main.py +27 -0
  28. beratools/gui/tool_widgets.py +730 -0
  29. beratools/tools/__init__.py +7 -0
  30. beratools/tools/canopy_threshold_relative.py +769 -0
  31. beratools/tools/centerline.py +127 -0
  32. beratools/tools/check_seed_line.py +48 -0
  33. beratools/tools/common.py +622 -0
  34. beratools/tools/line_footprint_absolute.py +203 -0
  35. beratools/tools/line_footprint_fixed.py +480 -0
  36. beratools/tools/line_footprint_functions.py +884 -0
  37. beratools/tools/line_footprint_relative.py +75 -0
  38. beratools/tools/tool_template.py +72 -0
  39. beratools/tools/vertex_optimization.py +57 -0
  40. beratools-0.1.0.dist-info/METADATA +134 -0
  41. beratools-0.1.0.dist-info/RECORD +44 -0
  42. beratools-0.1.0.dist-info/WHEEL +4 -0
  43. beratools-0.1.0.dist-info/entry_points.txt +2 -0
  44. beratools-0.1.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,451 @@
1
+ """
2
+ Copyright (C) 2025 Applied Geospatial Research Group.
3
+
4
+ This script is licensed under the GNU General Public License v3.0.
5
+ See <https://gnu.org/licenses/gpl-3.0> for full license details.
6
+
7
+ Author: Richard Zeng
8
+
9
+ Description:
10
+ This script is part of the BERA Tools.
11
+ Webpage: https://github.com/appliedgrg/beratools
12
+
13
+ The purpose of this script is to move line vertices to the right
14
+ seismic line courses for improved alignment and analysis in
15
+ geospatial data processing.
16
+ """
17
+
18
+ from pathlib import Path
19
+
20
+ import geopandas as gpd
21
+ import numpy as np
22
+ import pandas as pd
23
+ import shapely.geometry as sh_geom
24
+ from shapely import STRtree
25
+
26
+ import beratools.core.algo_common as algo_common
27
+ import beratools.core.algo_cost as algo_cost
28
+ import beratools.core.constants as bt_const
29
+ import beratools.core.tool_base as bt_base
30
+ import beratools.tools.common as bt_common
31
+ from beratools.core import algo_dijkstra
32
+
33
+
34
+ def update_line_end_pt(line, index, new_vertex):
35
+ if not line:
36
+ return None
37
+
38
+ if index >= len(line.coords) or index < -1:
39
+ return line
40
+
41
+ coords = list(line.coords)
42
+ if len(coords[index]) == 2:
43
+ coords[index] = (new_vertex.x, new_vertex.y)
44
+ elif len(coords[index]) == 3:
45
+ coords[index] = (new_vertex.x, new_vertex.y, 0.0)
46
+
47
+ return sh_geom.LineString(coords)
48
+
49
+
50
+ class _SingleLine:
51
+ """Single line object with anchor point."""
52
+
53
+ def __init__(self, line_gdf, line_no, end_no, search_distance):
54
+ self.line_gdf = line_gdf
55
+ self.line = self.line_gdf.geometry[0]
56
+ self.line_no = line_no
57
+ self.end_no = end_no
58
+ self.search_distance = search_distance
59
+ self.anchor = None
60
+
61
+ self.add_anchors_to_line()
62
+
63
+ def is_valid(self):
64
+ return self.line.is_valid
65
+
66
+ def line_coord_list(self):
67
+ return algo_common.line_coord_list(self.line)
68
+
69
+ def get_end_vertex(self):
70
+ return self.line_coord_list()[self.end_no]
71
+
72
+ def touches_point(self, vertex):
73
+ return algo_common.points_are_close(vertex, self.get_end_vertex())
74
+
75
+ def get_angle(self):
76
+ return algo_common.get_angle(self.line, self.end_no)
77
+
78
+ def add_anchors_to_line(self):
79
+ """
80
+ Append new vertex to vertex group, by calculating distance to existing vertices.
81
+
82
+ An anchor point will be added together with line
83
+ """
84
+ # Calculate anchor point for each vertex
85
+ point = self.get_end_vertex()
86
+ line_string = self.line
87
+ index = self.end_no
88
+ pts = algo_common.line_coord_list(line_string)
89
+
90
+ pt_1 = None
91
+ pt_2 = None
92
+ if index == 0:
93
+ pt_1 = point
94
+ pt_2 = pts[1]
95
+ elif index == -1:
96
+ pt_1 = point
97
+ pt_2 = pts[-2]
98
+
99
+ # Calculate anchor point
100
+ dist_pt = 0.0
101
+ if pt_1 and pt_2:
102
+ dist_pt = pt_1.distance(pt_2)
103
+
104
+ # TODO: check why two points are the same
105
+ if np.isclose(dist_pt, 0.0):
106
+ print("Points are close, return")
107
+ return None
108
+
109
+ X = pt_1.x + (pt_2.x - pt_1.x) * self.search_distance / dist_pt
110
+ Y = pt_1.y + (pt_2.y - pt_1.y) * self.search_distance / dist_pt
111
+ self.anchor = sh_geom.Point(X, Y) # add anchor point
112
+
113
+
114
+ class _Vertex:
115
+ """Vertex object with multiple lines."""
116
+
117
+ def __init__(self, line_obj):
118
+ self.vertex = line_obj.get_end_vertex()
119
+ self.search_distance = line_obj.search_distance
120
+
121
+ self.cost_footprint = None
122
+ self.vertex_opt = None # optimized vertex
123
+ self.centerlines = None
124
+ self.anchors = None
125
+ self.in_raster = None
126
+ self.line_radius = None
127
+ self.lines = [] # SingleLine objects
128
+
129
+ self.add_line(line_obj)
130
+
131
+ def add_line(self, line_obj):
132
+ self.lines.append(line_obj)
133
+
134
+ def generate_anchor_pairs(self):
135
+ """
136
+ Extend line following outward direction to length of search_distance.
137
+
138
+ Use the end point as anchor point.
139
+
140
+ vertex: input intersection with all related lines
141
+ return:
142
+ one or two pairs of anchors according to numbers of lines
143
+ intersected.
144
+ two pairs anchors return when 3 or 4 lines intersected
145
+ one pair anchors return when 1 or 2 lines intersected.
146
+ """
147
+ lines = self.get_lines()
148
+ vertex = self.get_vertex()
149
+ slopes = []
150
+ for line in self.lines:
151
+ slopes.append(line.get_angle())
152
+
153
+ index = 0 # the index of line which paired with first line.
154
+ pt_start_1 = None
155
+ pt_end_1 = None
156
+ pt_start_2 = None
157
+ pt_end_2 = None
158
+
159
+ if len(slopes) == 4:
160
+ # get sort order of angles
161
+ index = np.argsort(slopes)
162
+
163
+ # first anchor pair (first and third in the sorted array)
164
+ pt_start_1 = self.lines[index[0]].anchor
165
+ pt_end_1 = self.lines[index[2]].anchor
166
+
167
+ pt_start_2 = self.lines[index[1]].anchor
168
+ pt_end_2 = self.lines[index[3]].anchor
169
+ elif len(slopes) == 3:
170
+ # find the largest difference between angles
171
+ angle_diff = [
172
+ abs(slopes[0] - slopes[1]),
173
+ abs(slopes[0] - slopes[2]),
174
+ abs(slopes[1] - slopes[2]),
175
+ ]
176
+ angle_diff_norm = [2 * np.pi - i if i > np.pi else i for i in angle_diff]
177
+ index = np.argmax(angle_diff_norm)
178
+ pairs = [(0, 1), (0, 2), (1, 2)]
179
+ pair = pairs[index]
180
+
181
+ # first anchor pair
182
+ pt_start_1 = self.lines[pair[0]].anchor
183
+ pt_end_1 = self.lines[pair[1]].anchor
184
+
185
+ # the rest one index
186
+ remain = list({0, 1, 2} - set(pair))[0] # the remaining index
187
+
188
+ try:
189
+ pt_start_2 = lines[remain][2]
190
+ # symmetry point of pt_start_2 regarding vertex["point"]
191
+ X = vertex.x - (pt_start_2.x - vertex.x)
192
+ Y = vertex.y - (pt_start_2.y - vertex.y)
193
+ pt_end_2 = sh_geom.Point(X, Y)
194
+ except Exception as e:
195
+ print(e)
196
+
197
+ # this scenario only use two anchors
198
+ # and find the closest point on least cost path
199
+ elif len(slopes) == 2:
200
+ pt_start_1 = self.lines[0].anchor
201
+ pt_end_1 = self.lines[1].anchor
202
+ elif len(slopes) == 1:
203
+ pt_start_1 = self.lines[0].anchor
204
+ # symmetry point of pt_start_1 regarding vertex["point"]
205
+ X = vertex.x - (pt_start_1.x - vertex.x)
206
+ Y = vertex.y - (pt_start_1.y - vertex.y)
207
+ pt_end_1 = sh_geom.Point(X, Y)
208
+
209
+ if not pt_start_1 or not pt_end_1:
210
+ print("Anchors not found")
211
+
212
+ # if points are outside of cost footprint, set to None
213
+ points = [pt_start_1, pt_end_1, pt_start_2, pt_end_2]
214
+ for index, pt in enumerate(points):
215
+ if pt:
216
+ if not self.cost_footprint.contains(sh_geom.Point(pt)):
217
+ points[index] = None
218
+
219
+ if len(slopes) == 4 or len(slopes) == 3:
220
+ if None in points:
221
+ return None
222
+ else:
223
+ return points
224
+ elif len(slopes) == 2 or len(slopes) == 1:
225
+ if None in (pt_start_1, pt_end_1):
226
+ return None
227
+ else:
228
+ return pt_start_1, pt_end_1
229
+
230
+ def compute(self):
231
+ try:
232
+ self.anchors = self.generate_anchor_pairs()
233
+ except Exception as e:
234
+ print(e)
235
+
236
+ if not self.anchors:
237
+ if bt_const.BT_DEBUGGING:
238
+ print("No anchors retrieved")
239
+ return None
240
+
241
+ centerline_1 = None
242
+ centerline_2 = None
243
+ intersection = None
244
+
245
+ if bt_const.CenterlineFlags.USE_SKIMAGE_GRAPH:
246
+ find_lc_path = algo_dijkstra.find_least_cost_path_skimage
247
+ else:
248
+ find_lc_path = algo_dijkstra.find_least_cost_path
249
+
250
+ try:
251
+ if len(self.anchors) == 4:
252
+ seed_line = sh_geom.LineString(self.anchors[0:2])
253
+
254
+ raster_clip, out_meta = bt_common.clip_raster(self.in_raster, seed_line, self.line_radius)
255
+ raster_clip, _ = algo_cost.cost_raster(raster_clip, out_meta)
256
+ centerline_1 = find_lc_path(raster_clip, out_meta, seed_line)
257
+ seed_line = sh_geom.LineString(self.anchors[2:4])
258
+
259
+ raster_clip, out_meta = bt_common.clip_raster(self.in_raster, seed_line, self.line_radius)
260
+ raster_clip, _ = algo_cost.cost_raster(raster_clip, out_meta)
261
+ centerline_2 = find_lc_path(raster_clip, out_meta, seed_line)
262
+
263
+ if centerline_1 and centerline_2:
264
+ intersection = algo_common.intersection_of_lines(centerline_1, centerline_2)
265
+ elif len(self.anchors) == 2:
266
+ seed_line = sh_geom.LineString(self.anchors)
267
+
268
+ raster_clip, out_meta = bt_common.clip_raster(self.in_raster, seed_line, self.line_radius)
269
+ raster_clip, _ = algo_cost.cost_raster(raster_clip, out_meta)
270
+ centerline_1 = find_lc_path(raster_clip, out_meta, seed_line)
271
+
272
+ if centerline_1:
273
+ intersection = algo_common.closest_point_to_line(self.get_vertex(), centerline_1)
274
+ except Exception as e:
275
+ print(e)
276
+
277
+ # Update vertices according to intersection, new center lines are returned
278
+ if type(intersection) is sh_geom.MultiPoint:
279
+ intersection = intersection.centroid
280
+
281
+ self.centerlines = [centerline_1, centerline_2]
282
+ self.vertex_opt = intersection
283
+
284
+ def get_lines(self):
285
+ lines = [item.line for item in self.lines]
286
+ return lines
287
+
288
+ def get_vertex(self):
289
+ return self.vertex
290
+
291
+
292
+ class VertexGrouping:
293
+ """A class used to group vertices and perform vertex optimization."""
294
+
295
+ def __init__(
296
+ self,
297
+ in_line,
298
+ in_raster,
299
+ search_distance,
300
+ line_radius,
301
+ out_line,
302
+ processes,
303
+ verbose,
304
+ in_layer=None,
305
+ out_layer=None,
306
+ ):
307
+ self.in_line = in_line
308
+ self.in_raster = in_raster
309
+ self.line_radius = float(line_radius)
310
+ self.search_distance = float(search_distance)
311
+ self.out_line = out_line
312
+ self.processes = processes
313
+ self.verbose = verbose
314
+ self.parallel_mode = bt_const.PARALLEL_MODE
315
+ self.in_layer = in_layer
316
+ self.out_layer = out_layer
317
+
318
+ self.crs = None
319
+ self.vertex_grp = []
320
+ self.sindex = None
321
+
322
+ self.line_list = []
323
+ self.line_visited = None
324
+
325
+ # calculate cost raster footprint
326
+ self.cost_footprint = algo_common.generate_raster_footprint(self.in_raster, latlon=False)
327
+
328
+ def set_parallel_mode(self, parallel_mode):
329
+ self.parallel_mode = parallel_mode
330
+
331
+ def create_vertex_group(self, line_obj):
332
+ """
333
+ Create a new vertex group.
334
+
335
+ Args:
336
+ line_obj : _SingleLine
337
+
338
+ """
339
+ # all end points not added will stay with this vertex
340
+ vertex = line_obj.get_end_vertex()
341
+ vertex_obj = _Vertex(line_obj)
342
+ search = self.sindex.query(vertex.buffer(bt_const.SMALL_BUFFER))
343
+
344
+ # add more vertices to the new group
345
+ for i in search:
346
+ line = self.line_list[i]
347
+ if i == line_obj.line_no:
348
+ continue
349
+
350
+ if not self.line_visited[i][0]:
351
+ new_line = _SingleLine(line, i, 0, self.search_distance)
352
+ if new_line.touches_point(vertex):
353
+ vertex_obj.add_line(new_line)
354
+ self.line_visited[i][0] = True
355
+
356
+ if not self.line_visited[i][-1]:
357
+ new_line = _SingleLine(line, i, -1, self.search_distance)
358
+ if new_line.touches_point(vertex):
359
+ vertex_obj.add_line(new_line)
360
+ self.line_visited[i][-1] = True
361
+
362
+ vertex_obj.in_raster = self.in_raster
363
+
364
+ vertex_obj.line_radius = self.line_radius
365
+ vertex_obj.cost_footprint = self.cost_footprint
366
+ self.vertex_grp.append(vertex_obj)
367
+
368
+ def create_all_vertex_groups(self):
369
+ self.line_list = algo_common.prepare_lines_gdf(self.in_line, layer=self.in_layer, proc_segments=True)
370
+ self.sindex = STRtree([item.geometry[0] for item in self.line_list])
371
+ self.line_visited = [{0: False, -1: False} for _ in range(len(self.line_list))]
372
+
373
+ i = 0
374
+ for line_no in range(len(self.line_list)):
375
+ if not self.line_visited[line_no][0]:
376
+ line = _SingleLine(self.line_list[line_no], line_no, 0, self.search_distance)
377
+
378
+ if not line.is_valid:
379
+ print(f"Line {line['line_no']} is invalid")
380
+ continue
381
+
382
+ self.create_vertex_group(line)
383
+ self.line_visited[line_no][0] = True
384
+ i += 1
385
+
386
+ if not self.line_visited[line_no][-1]:
387
+ line = _SingleLine(self.line_list[line_no], line_no, -1, self.search_distance)
388
+
389
+ if not line.is_valid:
390
+ print(f"Line {line['line_no']} is invalid")
391
+ continue
392
+
393
+ self.create_vertex_group(line)
394
+ self.line_visited[line_no][-1] = True
395
+ i += 1
396
+
397
+ def update_all_lines(self):
398
+ for vertex_obj in self.vertex_grp:
399
+ for line in vertex_obj.lines:
400
+ if not vertex_obj.vertex_opt:
401
+ continue
402
+
403
+ old_line = self.line_list[line.line_no].geometry[0]
404
+ self.line_list[line.line_no].geometry = [
405
+ update_line_end_pt(old_line, line.end_no, vertex_obj.vertex_opt)
406
+ ]
407
+
408
+ def save_all_layers(self, line_file):
409
+ line_file = Path(line_file)
410
+ lines = pd.concat(self.line_list)
411
+ lines.to_file(line_file, layer=self.out_layer)
412
+
413
+ aux_file = line_file
414
+ if line_file.suffix == ".shp":
415
+ file_stem = line_file.stem
416
+ aux_file = line_file.with_stem(file_stem + "_aux").with_suffix(".gpkg")
417
+
418
+ lc_paths = []
419
+ anchors = []
420
+ vertices = []
421
+ for item in self.vertex_grp:
422
+ if item.centerlines:
423
+ lc_paths.extend(item.centerlines)
424
+ if item.anchors:
425
+ anchors.extend(item.anchors)
426
+ if item.vertex_opt:
427
+ vertices.append(item.vertex_opt)
428
+
429
+ lc_paths = [item for item in lc_paths if item is not None]
430
+ anchors = [item for item in anchors if item is not None]
431
+ vertices = [item for item in vertices if item is not None]
432
+
433
+ lc_paths = gpd.GeoDataFrame(geometry=lc_paths, crs=lines.crs)
434
+ anchors = gpd.GeoDataFrame(geometry=anchors, crs=lines.crs)
435
+ vertices = gpd.GeoDataFrame(geometry=vertices, crs=lines.crs)
436
+
437
+ lc_paths.to_file(aux_file, layer="lc_paths")
438
+ anchors.to_file(aux_file, layer="anchors")
439
+ vertices.to_file(aux_file, layer="vertices")
440
+
441
+ def compute(self):
442
+ vertex_grp = bt_base.execute_multiprocessing(
443
+ algo_common.process_single_item,
444
+ self.vertex_grp,
445
+ "Vertex Optimization",
446
+ self.processes,
447
+ bt_const.PARALLEL_MODE.MULTIPROCESSING,
448
+ verbose=self.verbose,
449
+ )
450
+
451
+ self.vertex_grp = vertex_grp
@@ -0,0 +1,56 @@
1
+ """
2
+ Copyright (C) 2025 Applied Geospatial Research Group.
3
+
4
+ This script is licensed under the GNU General Public License v3.0.
5
+ See <https://gnu.org/licenses/gpl-3.0> for full license details.
6
+
7
+ Author: Richard Zeng
8
+
9
+ Description:
10
+ This script is part of the BERA Tools.
11
+ Webpage: https://github.com/appliedgrg/beratools
12
+
13
+ The purpose of this script is to provide common constants.
14
+ """
15
+
16
+ import enum
17
+
18
+ import numpy as np
19
+
20
+ NADDatum = ["NAD83 Canadian Spatial Reference System", "North American Datum 1983"]
21
+
22
+ ASSETS_PATH = "assets"
23
+ BT_DEBUGGING = False
24
+ BT_UID = "BT_UID"
25
+ BT_GROUP = "BT_GROUP"
26
+
27
+ BT_EPSILON = np.finfo(float).eps
28
+ BT_NODATA_COST = np.inf
29
+ BT_NODATA = -9999
30
+
31
+ LP_SEGMENT_LENGTH = 500
32
+ FP_CORRIDOR_THRESHOLD = 2.5
33
+ SMALL_BUFFER = 1e-3
34
+
35
+
36
+ class CenterlineFlags(enum.Flag):
37
+ """Flags for the centerline algorithm."""
38
+
39
+ USE_SKIMAGE_GRAPH = False
40
+ DELETE_HOLES = True
41
+ SIMPLIFY_POLYGON = True
42
+
43
+
44
+ @enum.unique
45
+ class ParallelMode(enum.IntEnum):
46
+ """Defines the parallel mode for the algorithms."""
47
+
48
+ SEQUENTIAL = 1
49
+ MULTIPROCESSING = 2
50
+ CONCURRENT = 3
51
+ DASK = 4
52
+ SLURM = 5
53
+ # RAY = 6
54
+
55
+
56
+ PARALLEL_MODE = ParallelMode.MULTIPROCESSING
@@ -0,0 +1,92 @@
1
+ """
2
+ Copyright (C) 2025 Applied Geospatial Research Group.
3
+
4
+ This script is licensed under the GNU General Public License v3.0.
5
+ See <https://gnu.org/licenses/gpl-3.0> for full license details.
6
+
7
+ Author: Richard Zeng
8
+
9
+ Description:
10
+ This script is part of the BERA Tools.
11
+ Webpage: https://github.com/appliedgrg/beratools
12
+
13
+ The purpose of this script is to provide logger functions.
14
+ """
15
+
16
+ import logging
17
+ import logging.handlers
18
+ import sys
19
+
20
+ from beratools.gui.bt_data import BTData
21
+
22
+ bt = BTData()
23
+
24
+
25
+ class NoParsingFilter(logging.Filter):
26
+ """
27
+ Filter to exclude log messages that start with "parsing".
28
+
29
+ This is useful to avoid cluttering the log with parsing-related messages.
30
+ """
31
+
32
+ def filter(self, record):
33
+ return not record.getMessage().startswith("parsing")
34
+
35
+
36
+ class Logger(object):
37
+ """
38
+ Logger class to handle logging in the BERA Tools application.
39
+
40
+ This class sets up a logger that outputs to both the console and a file.
41
+ It allows for different logging levels for console and file outputs.
42
+ It also provides a method to print messages directly to the logger.
43
+ """
44
+
45
+ def __init__(self, name, console_level=logging.INFO, file_level=logging.INFO):
46
+ self.logger = logging.getLogger(name)
47
+ self.name = name
48
+ self.console_level = console_level
49
+ self.file_level = file_level
50
+
51
+ self.setup_logger()
52
+
53
+ def get_logger(self):
54
+ return self.logger
55
+
56
+ def print(self, msg, flush=True):
57
+ """
58
+ Re-define print in logging.
59
+
60
+ Args:
61
+ msg :
62
+ flush :
63
+
64
+ """
65
+ self.logger.info(msg)
66
+ if flush:
67
+ for handler in self.logger.handlers:
68
+ handler.flush()
69
+
70
+ def setup_logger(self):
71
+ # Change root logger level from WARNING (default) to NOTSET
72
+ # in order for all messages to be delegated.
73
+ logging.getLogger().setLevel(logging.NOTSET)
74
+ log_file = bt.get_logger_file_name(self.name)
75
+
76
+ # Add stdout handler, with level INFO
77
+ console_handler = logging.StreamHandler(sys.stdout)
78
+ console_handler.setLevel(self.console_level)
79
+ formatter = logging.Formatter("%(message)s")
80
+ console_handler.setFormatter(formatter)
81
+ logging.getLogger().addHandler(console_handler)
82
+
83
+ # Add file rotating handler, 5MB size limit, 5 backups
84
+ rotating_handler = logging.handlers.RotatingFileHandler(
85
+ filename=log_file, maxBytes=5 * 1000 * 1000, backupCount=5
86
+ )
87
+
88
+ rotating_handler.setLevel(self.file_level)
89
+ formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
90
+ rotating_handler.setFormatter(formatter)
91
+ logging.getLogger().addHandler(rotating_handler)
92
+ logging.getLogger().addFilter(NoParsingFilter())