tilegrab 1.1.0__tar.gz → 1.2.0b1__tar.gz

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 (33) hide show
  1. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/PKG-INFO +2 -2
  2. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/pyproject.toml +2 -2
  3. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab/__init__.py +1 -1
  4. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab/cli.py +48 -118
  5. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab/dataset.py +6 -5
  6. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab/images.py +61 -7
  7. tilegrab-1.2.0b1/src/tilegrab/logs.py +84 -0
  8. tilegrab-1.2.0b1/src/tilegrab/tiles.py +261 -0
  9. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab.egg-info/PKG-INFO +2 -2
  10. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab.egg-info/SOURCES.txt +1 -1
  11. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab.egg-info/requires.txt +1 -1
  12. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/uv.lock +332 -61
  13. tilegrab-1.1.0/src/tilegrab/mosaic.py +0 -75
  14. tilegrab-1.1.0/src/tilegrab/tiles.py +0 -179
  15. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/.github/workflows/test.yml +0 -0
  16. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/.gitignore +0 -0
  17. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/.python-version +0 -0
  18. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/.vscode/settings.json +0 -0
  19. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/LICENSE +0 -0
  20. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/README.md +0 -0
  21. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/setup.cfg +0 -0
  22. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab/__main__.py +0 -0
  23. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab/downloader.py +0 -0
  24. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab/sources.py +0 -0
  25. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab.egg-info/dependency_links.txt +0 -0
  26. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab.egg-info/entry_points.txt +0 -0
  27. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab.egg-info/top_level.txt +0 -0
  28. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/tests/data/T.geojson +0 -0
  29. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/tests/test_dataset.py +0 -0
  30. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/tests/test_downloader.py +0 -0
  31. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/tests/test_sources.py +0 -0
  32. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/tests/test_tiles.py +0 -0
  33. {tilegrab-1.1.0 → tilegrab-1.2.0b1}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tilegrab
3
- Version: 1.1.0
3
+ Version: 1.2.0b1
4
4
  Summary: Fast geospatial map tile downloader and mosaicker
5
5
  Author: Thiwanka Munasinghe
6
6
  License-Expression: MIT
@@ -14,7 +14,7 @@ Requires-Python: >=3.9
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
16
  Requires-Dist: requests
17
- Requires-Dist: mercantile
17
+ Requires-Dist: rasterio
18
18
  Requires-Dist: pillow
19
19
  Requires-Dist: tqdm
20
20
  Requires-Dist: python-box
@@ -7,7 +7,7 @@ requires = [
7
7
 
8
8
  [project]
9
9
  name = "tilegrab"
10
- version = "1.1.0"
10
+ version = "1.2.0b1"
11
11
  description = "Fast geospatial map tile downloader and mosaicker"
12
12
  readme = "README.md"
13
13
  authors = [
@@ -25,7 +25,7 @@ keywords = [
25
25
 
26
26
  dependencies = [
27
27
  "requests",
28
- "mercantile",
28
+ "rasterio",
29
29
  "pillow",
30
30
  "tqdm",
31
31
  "python-box",
@@ -1,4 +1,4 @@
1
- version = "1.1.0"
1
+ __version__ = "1.2.0b1"
2
2
  # from tilegrab import downloader
3
3
  # from tilegrab import sources
4
4
  # from tilegrab import mosaic
@@ -1,91 +1,16 @@
1
1
  #!/usr/bin/env python3
2
2
  import logging
3
3
  import argparse
4
- import random
5
- import sys
6
4
  from pathlib import Path
7
5
  from tilegrab.downloader import Downloader
8
6
  from tilegrab.images import TileImageCollection
7
+ from tilegrab.logs import setup_logging
9
8
  from tilegrab.tiles import TilesByShape, TilesByBBox
10
9
  from tilegrab.dataset import GeoDataset
11
- from tilegrab import version
12
-
13
- # Configure root logger
14
- # logging.basicConfig(
15
- # level=logging.INFO,
16
- # format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
17
- # handlers=[
18
- # logging.StreamHandler(sys.stdout),
19
- # logging.FileHandler('tilegrab.log')
20
- # ]
21
- # )
22
-
23
- # Normal colors
24
- BLACK = "\033[30m"
25
- RED = "\033[31m"
26
- GREEN = "\033[32m"
27
- YELLOW = "\033[33m"
28
- BLUE = "\033[34m"
29
- MAGENTA = "\033[35m"
30
- CYAN = "\033[36m"
31
- GRAY = "\033[90m"
32
- WHITE = "\033[37m"
33
-
34
- # Bright colors
35
- BBLACK = "\033[90m"
36
- BRED = "\033[91m"
37
- BGREEN = "\033[92m"
38
- BYELLOW = "\033[93m"
39
- BBLUE = "\033[94m"
40
- BMAGENTA = "\033[95m"
41
- BCYAN = "\033[96m"
42
- BGRAY = "\033[97m"
43
- BWHITE = "\033[97m"
44
-
45
- RESET = "\033[0m"
46
-
47
-
48
- class LogFormatter(logging.Formatter):
49
- NAME_WIDTH = 14
50
-
51
- LEVEL_MAP = {
52
- logging.CRITICAL: f'{RED}‼ {RESET}',
53
- logging.ERROR: f'{RED}✖ {RESET}',
54
- logging.WARNING: f'{YELLOW}⚠ {RESET}',
55
- logging.INFO: f'{BLUE}• {RESET}',
56
- logging.DEBUG: f'{GRAY}· {RESET}',
57
- logging.NOTSET: f'{CYAN}- {RESET}',
58
- }
59
-
60
- def format(self, record):
61
- record.level_letter = self.LEVEL_MAP.get(record.levelno, '?')
62
-
63
- short = record.name.rsplit('.', 1)[-1]
64
- record.short_name = f"{short:<{self.NAME_WIDTH}}"
65
-
66
- return super().format(record)
67
-
68
-
69
- console_formatter = LogFormatter(
70
- f' %(level_letter)s %(message)s'
71
- )
72
- file_formatter = logging.Formatter(
73
- '%(asctime)s %(levelname)s %(name)s - %(message)s'
74
- )
75
-
76
- console = logging.StreamHandler(sys.stdout)
77
- console.setFormatter(console_formatter )
78
-
79
- file = logging.FileHandler('tilegrab.log')
80
- file.setFormatter(file_formatter)
81
-
82
- logging.basicConfig(
83
- level=logging.INFO,
84
- handlers=[console, file],
85
- )
10
+ from tilegrab import __version__
86
11
 
87
- logger = logging.getLogger(__name__)
88
12
 
13
+ logger = logging.getLogger(__name__)
89
14
 
90
15
  def parse_args() -> argparse.Namespace:
91
16
  p = argparse.ArgumentParser(
@@ -127,13 +52,22 @@ def parse_args() -> argparse.Namespace:
127
52
  "--key", type=str, default=None, help="API key where required by source"
128
53
  )
129
54
 
55
+ # Create a named group for merged output format
56
+ mosaic_out_group = p.add_argument_group(
57
+ title="Mosaic export formats", description="Formats for the output mosaic image"
58
+ )
59
+ mosaic_group = mosaic_out_group.add_mutually_exclusive_group(required=True)
60
+ mosaic_group.add_argument("--jpg", action="store_true", help="JPG image; no geo-reference")
61
+ mosaic_group.add_argument("--png", action="store_true", help="PNG image; no geo-reference")
62
+ mosaic_group.add_argument("--tiff", action="store_true", help="GeoTiff image; with geo-reference")
63
+
130
64
  # other options
131
65
  p.add_argument("--zoom", type=int, required=True, help="Zoom level (integer)")
132
66
  p.add_argument(
133
- "--out",
67
+ "--tiles-out",
134
68
  type=Path,
135
69
  default=Path.cwd() / "saved_tiles",
136
- help="Output directory (default: ./saved_tiles)",
70
+ help="Output directory for downloaded tiles (default: ./saved_tiles)",
137
71
  )
138
72
  p.add_argument(
139
73
  "--download-only",
@@ -148,56 +82,47 @@ def parse_args() -> argparse.Namespace:
148
82
  p.add_argument(
149
83
  "--no-progress", action="store_false", help="Hide download progress bar"
150
84
  )
85
+ p.add_argument("--quiet", action="store_true", help="Hide all prints")
86
+ p.add_argument("--debug", action="store_true", help="Enable debug logging")
151
87
  p.add_argument(
152
- "--quiet", action="store_true", help="Hide all prints"
153
- )
154
- p.add_argument(
155
- "--debug", action="store_true", help="Enable debug logging"
156
- )
157
- p.add_argument(
158
- "--test", action="store_true", help="Only for testing purposes, not for normal use"
88
+ "--test",
89
+ action="store_true",
90
+ help="Only for testing purposes, not for normal use",
159
91
  )
160
92
 
161
93
  return p.parse_args()
162
94
 
163
95
 
164
96
  def main():
97
+ LOG_LEVEL = logging.INFO
98
+ ENABLE_CLI_LOG = True
99
+ ENABLE_FILE_LOG = True
100
+
165
101
  args = parse_args()
166
-
167
- # Adjust logging level
168
102
  if args.debug:
169
- logging.getLogger().setLevel(logging.DEBUG)
170
- # logger.debug("Debug logging enabled")
171
- elif args.quiet:
172
- console.close()
173
-
174
-
103
+ LOG_LEVEL = logging.DEBUG
104
+ if args.quiet:
105
+ ENABLE_CLI_LOG = False
175
106
 
176
- BANNER_NORMAL, BANNER_BRIGHT = random.choice([
177
- (RED, BRED),
178
- (GREEN, BGREEN),
179
- (YELLOW, BYELLOW),
180
- (BLUE, BBLUE),
181
- (MAGENTA, BMAGENTA),
182
- (CYAN, BCYAN),
183
- (GRAY, BGRAY),
184
- ])
107
+ setup_logging(ENABLE_CLI_LOG, ENABLE_FILE_LOG, LOG_LEVEL)
185
108
 
186
109
  if not args.quiet:
187
110
  print()
188
- print(f"{WHITE} " + ("-" * 60) + f"{RESET}")
189
- print(f"{BWHITE} TileGrab v{version}{RESET}".rjust(50))
190
- print(f"{WHITE} " + ("-" * 60) + f"{RESET}")
191
-
111
+ print(f"\033[37m " + ("-" * 60) + "\033[0m")
112
+ print(f"\033[97m TileGrab v{__version__}\033[0m".rjust(50))
113
+ print(f"\033[37m " + ("-" * 60) + "\033[0m")
114
+
192
115
  try:
193
116
  dataset = GeoDataset(args.source)
194
117
  logger.info(f"Dataset loaded successfully from {args.source}")
195
118
 
196
119
  _tmp = "bbox" if args.bbox else "shape" if args.shape else "DnE"
197
- logger.info(f"""Downloading tiles using {_tmp}
120
+ logger.info(
121
+ f"""Downloading tiles using {_tmp}
198
122
  - minX: {dataset.bbox.minx:.4f} - minY: {dataset.bbox.miny:.4f}
199
123
  - maxX: {dataset.bbox.maxx:.4f} - maxY: {dataset.bbox.maxy:.4f}
200
- - zoom: {args.zoom}""")
124
+ - zoom: {args.zoom}"""
125
+ )
201
126
 
202
127
  if args.shape:
203
128
  tiles = TilesByShape(dataset, zoom=args.zoom)
@@ -210,34 +135,39 @@ def main():
210
135
  # Choose source provider
211
136
  if args.osm:
212
137
  from tilegrab.sources import OSM
138
+
213
139
  logger.info("Using OpenStreetMap (OSM) as tile source")
214
140
  source = OSM(api_key=args.key) if args.key else OSM()
215
141
  elif args.google_sat:
216
142
  from tilegrab.sources import GoogleSat
143
+
217
144
  logger.info("Using Google Satellite as tile source")
218
145
  source = GoogleSat(api_key=args.key) if args.key else GoogleSat()
219
146
  elif args.esri_sat:
220
147
  from tilegrab.sources import ESRIWorldImagery
148
+
221
149
  logger.info("Using ESRI World Imagery as tile source")
222
- source = ESRIWorldImagery(api_key=args.key) if args.key else ESRIWorldImagery()
150
+ source = (
151
+ ESRIWorldImagery(api_key=args.key) if args.key else ESRIWorldImagery()
152
+ )
223
153
  else:
224
154
  logger.error("No tile source selected")
225
155
  raise SystemExit("No tile source selected")
226
-
227
- downloader = Downloader(tiles, source, args.out)
156
+
157
+ downloader = Downloader(tiles, source, args.tiles_out)
228
158
  result: TileImageCollection
229
159
 
230
160
  if args.mosaic_only:
231
- result = TileImageCollection(args.out)
232
- result.load(tiles)
161
+ result = TileImageCollection(args.tiles_out)
162
+ result.load(tiles)
233
163
  else:
234
164
  result = downloader.run(show_progress=args.no_progress)
235
165
  logger.info(f"Download result: {result}")
236
-
237
- if not args.download_only: result.mosaic()
166
+
167
+ if not args.download_only:
168
+ result.mosaic(tiff=args.tiff, png=args.png)
238
169
  logger.info("Done")
239
170
 
240
-
241
171
  except Exception as e:
242
172
  logger.exception("Fatal error during execution")
243
173
  raise SystemExit(1)
@@ -6,6 +6,7 @@ from typing import Union
6
6
 
7
7
  logger = logging.getLogger(__name__)
8
8
 
9
+ TILE_EPSG = 4326 #Web Mercator - 3857 | 4326 - WGS84
9
10
  class GeoDataset:
10
11
 
11
12
  @property
@@ -63,14 +64,14 @@ class GeoDataset:
63
64
  logger.critical("Dataset has no CRS defined")
64
65
  raise RuntimeError("Missing CRS")
65
66
 
66
- if epsg != 4326:
67
- logger.info(f"Reprojecting dataset from EPSG:{epsg} to EPSG:4326")
68
- gdf = gdf.to_crs(epsg=4326)
67
+ if epsg != TILE_EPSG: #Web Mercator
68
+ logger.info(f"Reprojecting dataset from EPSG:{epsg} to EPSG:{TILE_EPSG}")
69
+ gdf = gdf.to_crs(epsg=TILE_EPSG)
69
70
  else:
70
- logger.debug("Dataset already in EPSG:4326")
71
+ logger.debug(f"Dataset already in EPSG:{TILE_EPSG}")
71
72
 
72
73
  self.original_epsg = epsg
73
- self.current_epsg = 4326
74
+ self.current_epsg = TILE_EPSG
74
75
  self.source = gdf
75
76
  self.source_path = source_path
76
77
  logger.info(f"GeoDataset initialized successfully: {len(gdf)} features")
@@ -3,7 +3,7 @@ from dataclasses import dataclass
3
3
  from io import BytesIO
4
4
  from pathlib import Path, PosixPath, WindowsPath
5
5
  import re
6
- from typing import Any, List, Optional, Tuple, Union
6
+ from typing import Any, List, Union
7
7
  from PIL import Image as PLIImage
8
8
  from box import Box
9
9
  from tilegrab.tiles import Tile, TileCollection
@@ -132,16 +132,30 @@ class TileImage:
132
132
  def url(self) -> Union[str, None]:
133
133
  return self._tile.url
134
134
 
135
-
135
+ WEB_MERCATOR_EXTENT = 20037508.342789244
136
+ EPSG = 3857
136
137
  class TileImageCollection:
137
138
  images: List[TileImage] = []
138
139
  width: int = 0
139
140
  height: int = 0
140
141
 
142
+ def mosaic_bounds(self, x_min, y_min, x_max, y_max, z):
143
+ n = 2 ** z
144
+ tile_size_m = 2 * WEB_MERCATOR_EXTENT / n
145
+
146
+ xmin = (WEB_MERCATOR_EXTENT*-1) + x_min * tile_size_m
147
+ xmax = (WEB_MERCATOR_EXTENT*-1) + (x_max + 1) * tile_size_m
148
+
149
+ ymax = WEB_MERCATOR_EXTENT - y_min * tile_size_m
150
+ ymin = WEB_MERCATOR_EXTENT - (y_max + 1) * tile_size_m
151
+
152
+ return xmin, ymin, xmax, ymax
153
+
141
154
  def load(self, tile_collection:TileCollection):
142
155
  logger.info("Start loading saved ImageTiles")
143
156
  pat = re.compile(r'^([0-9]+)_([0-9]+)_([0-9]+)\.[A-Za-z0-9]+$')
144
157
  image_col = [p for p in self.path.glob(f"*.*") if p.is_file()]
158
+ self.zoom = tile_collection.to_list[0].z
145
159
 
146
160
  for tile in tile_collection.to_list:
147
161
  found_matching_image = False
@@ -169,6 +183,8 @@ class TileImageCollection:
169
183
  logger.debug(f"Image appended to collection: {img.name}")
170
184
  img.save()
171
185
 
186
+ self.zoom = img.tile.z
187
+
172
188
  def __init__(self, path: Union[Path, str]) -> None:
173
189
  self.path = Path(path)
174
190
  logger.info(f"TileImageCollection initialized at {self.path}")
@@ -195,12 +211,15 @@ class TileImageCollection:
195
211
 
196
212
  self.width = int((maxx - minx + 1) * self.images[0].width)
197
213
  self.height = int((maxy - miny + 1) * self.images[0].height)
214
+ self.minx, self.maxx = minx, maxx
215
+ self.miny, self.maxy = miny, maxy
216
+
198
217
  logger.info(f"Collection dimensions calculated: {self.width}x{self.height}")
199
218
 
200
- def mosaic(self):
219
+ def mosaic(self, tiff:bool=True, png:bool=False):
201
220
  logger.info("Starting mosaic creation")
202
221
  self._update_collection_dim()
203
-
222
+
204
223
  logger.info(f"Mosaicking {len(self.images)} images into {self.width}x{self.height}")
205
224
  merged_image = PLIImage.new("RGB", (self.width, self.height))
206
225
 
@@ -210,9 +229,44 @@ class TileImageCollection:
210
229
  logger.debug(f"Pasting image at position ({px}, {py}): {image.name}")
211
230
  merged_image.paste(image.image, (px, py))
212
231
 
213
- output_path = "merged_output.png"
214
- merged_image.save(output_path)
215
- logger.info(f"Mosaic saved to {output_path}")
232
+ # TODO: fix this monkey patch
233
+ if tiff:
234
+ output_path = "mosaic.tiff"
235
+ import numpy as np
236
+ from rasterio.transform import from_bounds
237
+ import rasterio
238
+
239
+ data = np.array(merged_image)
240
+ data = data.transpose(2, 0, 1)
241
+
242
+ width_px, height_px = merged_image.size
243
+ xmin, ymin, xmax, ymax = self.mosaic_bounds(
244
+ self.minx, self.miny, self.maxx, self.maxy, self.images[0].tile.z
245
+ )
246
+
247
+ transform = from_bounds(
248
+ xmin, ymin, xmax, ymax,
249
+ width_px, height_px
250
+ )
251
+
252
+ with rasterio.open(
253
+ output_path,
254
+ "w",
255
+ driver="GTiff",
256
+ height=height_px,
257
+ width=width_px,
258
+ count=data.shape[0],
259
+ dtype=data.dtype,
260
+ crs=f"EPSG:{EPSG}",
261
+ transform=transform,
262
+ ) as dst:
263
+ dst.write(data)
264
+
265
+ logger.info(f"Mosaic saved to {output_path}")
266
+ if png:
267
+ output_path = "mosaic.png"
268
+ merged_image.save(output_path)
269
+ logger.info(f"Mosaic saved to {output_path}")
216
270
 
217
271
  def export_collection(self, type: ExportType):
218
272
  logger.info(f"Exporting collection as type {type}")
@@ -0,0 +1,84 @@
1
+ import logging
2
+ import sys
3
+
4
+ # Normal colors
5
+ BLACK = "\033[30m"
6
+ RED = "\033[31m"
7
+ GREEN = "\033[32m"
8
+ YELLOW = "\033[33m"
9
+ BLUE = "\033[34m"
10
+ MAGENTA = "\033[35m"
11
+ CYAN = "\033[36m"
12
+ GRAY = "\033[90m"
13
+ WHITE = "\033[37m"
14
+
15
+ # Bright colors
16
+ BBLACK = "\033[90m"
17
+ BRED = "\033[91m"
18
+ BGREEN = "\033[92m"
19
+ BYELLOW = "\033[93m"
20
+ BBLUE = "\033[94m"
21
+ BMAGENTA = "\033[95m"
22
+ BCYAN = "\033[96m"
23
+ BGRAY = "\033[97m"
24
+ BWHITE = "\033[97m"
25
+
26
+ RESET = "\033[0m"
27
+
28
+
29
+ class CLILogFormatter(logging.Formatter):
30
+ NAME_WIDTH = 20
31
+ LEVEL_MAP = {
32
+ logging.CRITICAL: f'{RED}‼ {RESET}',
33
+ logging.ERROR: f'{RED}✖ {RESET}',
34
+ logging.WARNING: f'{YELLOW}⚠ {RESET}',
35
+ logging.INFO: f'{BLUE}• {RESET}',
36
+ logging.DEBUG: f'{GRAY}· {RESET}',
37
+ logging.NOTSET: f'{CYAN}- {RESET}',
38
+ }
39
+
40
+ def __init__(self, fmt=None):
41
+ super().__init__(fmt or ' %(level_icon)s %(message)s')
42
+
43
+ def format(self, record):
44
+ record.level_icon = self.LEVEL_MAP.get(record.levelno, '?')
45
+ short = record.name.rsplit('.', 1)[-1]
46
+ record.short_name = f"{short:<{self.NAME_WIDTH}}"
47
+
48
+ return super().format(record)
49
+
50
+ class FileLogFormatter(logging.Formatter):
51
+ def __init__(self):
52
+ super().__init__(
53
+ '%(asctime)s %(levelname)s %(name)s - %(message)s'
54
+ )
55
+
56
+ def create_cli_handler(level=logging.INFO):
57
+ handler = logging.StreamHandler(sys.stdout)
58
+ handler.setLevel(level)
59
+ handler.setFormatter(CLILogFormatter())
60
+ return handler
61
+
62
+ def create_file_handler(path="tilegrab.log", level=logging.DEBUG):
63
+ handler = logging.FileHandler(path)
64
+ handler.setLevel(level)
65
+ handler.setFormatter(FileLogFormatter())
66
+ return handler
67
+
68
+ def setup_logging(
69
+ enable_cli=True,
70
+ enable_file=True,
71
+ level=logging.INFO
72
+ ):
73
+ handlers = []
74
+
75
+ if enable_cli:
76
+ handlers.append(create_cli_handler(level))
77
+
78
+ if enable_file:
79
+ handlers.append(create_file_handler(level=level))
80
+
81
+ logging.basicConfig(
82
+ level=level,
83
+ handlers=handlers,
84
+ )