sibi-dst 0.3.42__py3-none-any.whl → 0.3.44__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 (30) hide show
  1. sibi_dst/df_helper/_artifact_updater_multi_wrapper.py +7 -2
  2. sibi_dst/df_helper/_df_helper.py +5 -2
  3. sibi_dst/df_helper/_parquet_artifact.py +33 -3
  4. sibi_dst/df_helper/_parquet_reader.py +5 -1
  5. sibi_dst/df_helper/backends/django/_load_from_db.py +1 -0
  6. sibi_dst/df_helper/backends/parquet/_filter_handler.py +2 -1
  7. sibi_dst/df_helper/backends/parquet/_parquet_options.py +2 -3
  8. sibi_dst/df_helper/backends/sqlalchemy/_db_connection.py +1 -0
  9. sibi_dst/df_helper/backends/sqlalchemy/_io_dask.py +2 -5
  10. sibi_dst/df_helper/core/_filter_handler.py +2 -1
  11. sibi_dst/osmnx_helper/__init__.py +2 -2
  12. sibi_dst/osmnx_helper/v1/basemaps/__init__.py +0 -0
  13. sibi_dst/osmnx_helper/{basemaps → v1/basemaps}/router_plotter.py +85 -30
  14. sibi_dst/osmnx_helper/v2/__init__.py +0 -0
  15. sibi_dst/osmnx_helper/v2/base_osm_map.py +153 -0
  16. sibi_dst/osmnx_helper/v2/basemaps/__init__.py +0 -0
  17. sibi_dst/osmnx_helper/v2/basemaps/utils.py +0 -0
  18. sibi_dst/utils/data_wrapper.py +4 -368
  19. sibi_dst/utils/df_utils.py +7 -0
  20. sibi_dst/utils/log_utils.py +6 -0
  21. sibi_dst/utils/parquet_saver.py +4 -2
  22. sibi_dst/utils/storage_manager.py +14 -7
  23. sibi_dst-0.3.44.dist-info/METADATA +194 -0
  24. {sibi_dst-0.3.42.dist-info → sibi_dst-0.3.44.dist-info}/RECORD +29 -24
  25. sibi_dst-0.3.42.dist-info/METADATA +0 -62
  26. /sibi_dst/osmnx_helper/{basemaps → v1}/__init__.py +0 -0
  27. /sibi_dst/osmnx_helper/{base_osm_map.py → v1/base_osm_map.py} +0 -0
  28. /sibi_dst/osmnx_helper/{basemaps → v1/basemaps}/calendar_html.py +0 -0
  29. /sibi_dst/osmnx_helper/{utils.py → v1/utils.py} +0 -0
  30. {sibi_dst-0.3.42.dist-info → sibi_dst-0.3.44.dist-info}/WHEEL +0 -0
@@ -8,8 +8,7 @@ import pandas as pd
8
8
  from IPython.display import display
9
9
  from tqdm import tqdm
10
10
 
11
- from sibi_dst.utils import Logger, FileAgeChecker
12
- from sibi_dst.utils import ParquetSaver
11
+ from sibi_dst.utils import Logger, FileAgeChecker, ParquetSaver
13
12
 
14
13
 
15
14
  class DataWrapper:
@@ -259,6 +258,8 @@ class DataWrapper:
259
258
  start_time = datetime.datetime.now()
260
259
 
261
260
  try:
261
+ self.logger.debug(f"Class Params: {self.class_params}")
262
+ self.logger.debug(f"Load Params: {self.load_params}")
262
263
  data = self.dataclass(**self.class_params)
263
264
  df = data.load_period(
264
265
  dt_field=self.date_field,
@@ -297,369 +298,4 @@ class DataWrapper:
297
298
  msg = f"Failed processing {date}: {str(error)}"
298
299
  self.logger.error(msg)
299
300
 
300
- # Helper functions omitted for brevity (date conversion, path normalization)
301
-
302
- # import datetime
303
- # from concurrent.futures import ThreadPoolExecutor
304
- # from typing import Type, Any, Dict, Optional, Union
305
- # from threading import Lock
306
- # import fsspec
307
- # import pandas as pd
308
- # from IPython.display import display
309
- # from tqdm import tqdm
310
- #
311
- # from sibi_dst.utils import Logger, DateUtils
312
- # from sibi_dst.utils import ParquetSaver
313
- #
314
- #
315
- # class DataWrapper:
316
- # """
317
- # Utility class for handling file-based operations, including processing and saving data
318
- # in Parquet format, while managing a hierarchy of conditions such as overwrite, history
319
- # threshold, and missing file detection.
320
- #
321
- # This class aims to simplify the process of managing large datasets stored in a filesystem.
322
- # It allows for controlled updates to data files based on parameters set by the user, with
323
- # support for different filesystem types and options.
324
- #
325
- # It also provides features like logging actions, managing processing threads, generating
326
- # update plans, checking file age, and dynamically creating date ranges for data operations.
327
- #
328
- # The design supports flexible integration with user-defined classes (dataclasses) to define
329
- # custom loading and processing behavior.
330
- #
331
- # :ivar dataclass: The user-defined class for data processing.
332
- # :type dataclass: Type
333
- # :ivar date_field: The name of the date field in the user-defined class.
334
- # :type date_field: str
335
- # :ivar data_path: Base path for the dataset storage.
336
- # :type data_path: str
337
- # :ivar parquet_filename: File name for the Parquet file.
338
- # :type parquet_filename: str
339
- # :ivar start_date: Start date for processing.
340
- # :type start_date: datetime.date
341
- # :ivar end_date: End date for processing.
342
- # :type end_date: datetime.date
343
- # :ivar fs: File system object for managing files.
344
- # :type fs: Optional[fsspec.AbstractFileSystem]
345
- # :ivar filesystem_type: Type of the filesystem (e.g., "file", "s3").
346
- # :type filesystem_type: str
347
- # :ivar filesystem_options: Additional options for initializing the filesystem.
348
- # :type filesystem_options: Optional[Dict]
349
- # :ivar verbose: Flag to enable verbose logging.
350
- # :type verbose: bool
351
- # :ivar class_params: Parameters to initialize the dataclass.
352
- # :type class_params: Optional[Dict]
353
- # :ivar load_params: Additional parameters for loading functions.
354
- # :type load_params: Optional[Dict]
355
- # :ivar reverse_order: Flag to reverse the order of date range generation.
356
- # :type reverse_order: bool
357
- # :ivar overwrite: Whether to overwrite all files during processing.
358
- # :type overwrite: bool
359
- # :ivar ignore_missing: Whether to ignore missing files.
360
- # :type ignore_missing: bool
361
- # :ivar logger: Logger instance for logging information.
362
- # :type logger: Optional[Logger]
363
- # :ivar max_age_minutes: Maximum file age threshold in minutes.
364
- # :type max_age_minutes: int
365
- # :ivar history_days_threshold: Number of days for the history threshold.
366
- # :type history_days_threshold: int
367
- # :ivar show_progress: Flag to enable progress display.
368
- # :type show_progress: bool
369
- # :ivar timeout: Timeout in seconds for processing tasks with threads.
370
- # :type timeout: Optional[int]
371
- # """
372
- # DEFAULT_MAX_AGE_MINUTES = 1440
373
- # DEFAULT_HISTORY_DAYS_THRESHOLD = 30
374
- #
375
- # def __init__(self,
376
- # dataclass: Type,
377
- # date_field: str,
378
- # data_path: str,
379
- # parquet_filename: str,
380
- # start_date: Any,
381
- # end_date: Any,
382
- # fs: Optional[fsspec.AbstractFileSystem] = None,
383
- # filesystem_type: str = "file",
384
- # filesystem_options: Optional[Dict] = None,
385
- # verbose: bool = False,
386
- # class_params: Optional[Dict] = None,
387
- # load_params: Optional[Dict] = None,
388
- # reverse_order: bool = False,
389
- # overwrite: bool = False,
390
- # ignore_missing: bool = False,
391
- # logger: Logger = None,
392
- # max_age_minutes: int = DEFAULT_MAX_AGE_MINUTES,
393
- # history_days_threshold: int = DEFAULT_HISTORY_DAYS_THRESHOLD,
394
- # show_progress: bool = False,
395
- # timeout: float = 60):
396
- # self.dataclass = dataclass
397
- # self.date_field = date_field
398
- # self.data_path = self.ensure_forward_slash(data_path)
399
- # self.parquet_filename = parquet_filename
400
- # self.filesystem_type = filesystem_type
401
- # self.filesystem_options = filesystem_options or {}
402
- # self.fs = fs
403
- # self.verbose = verbose
404
- # self.class_params = class_params or {}
405
- # self.load_params = load_params or {}
406
- # self.reverse_order = reverse_order
407
- # self.overwrite = overwrite
408
- # self.ignore_missing = ignore_missing
409
- # self.logger = logger or Logger.default_logger(logger_name=self.dataclass.__name__)
410
- # self.max_age_minutes = max_age_minutes
411
- # self.history_days_threshold = history_days_threshold
412
- # self.show_progress = show_progress
413
- # self.timeout = timeout
414
- #
415
- # self.start_date = self.convert_to_date(start_date)
416
- # self.end_date = self.convert_to_date(end_date)
417
- # self._lock = Lock()
418
- # self.processed_dates = []
419
- # self.date_utils = DateUtils(logger=self.logger)
420
- # if self.fs is None:
421
- # with self._lock:
422
- # if self.fs is None:
423
- # self.fs = fsspec.filesystem(self.filesystem_type, **self.filesystem_options)
424
- #
425
- # @staticmethod
426
- # def convert_to_date(date: Union[datetime.date, str]) -> datetime.date:
427
- # if isinstance(date, datetime.date):
428
- # return date
429
- # try:
430
- # return pd.to_datetime(date).date()
431
- # except ValueError as e:
432
- # raise ValueError(f"Error converting {date} to datetime: {e}")
433
- #
434
- # @staticmethod
435
- # def ensure_forward_slash(path: str) -> str:
436
- # return path if path.endswith('/') else path + '/'
437
- #
438
- # def generate_date_range(self):
439
- # """Generate a range of dates between start_date and end_date."""
440
- # date_range = pd.date_range(self.start_date, self.end_date, freq='D')
441
- # if self.reverse_order:
442
- # date_range = date_range[::-1]
443
- # for date in date_range:
444
- # yield date.date()
445
- #
446
- # def process(self, max_retries: int = 3):
447
- # """
448
- # Processes update tasks by generating an update plan, filtering required updates, and distributing
449
- # the workload across threads based on priority levels.
450
- #
451
- # This method operates by assessing required updates through generated conditions,
452
- # grouping them by priority levels, and processing them in parallel threads.
453
- # Each thread handles the updates for a specific priority level, ensuring a streamlined approach
454
- # to handling the updates efficiently.
455
- #
456
- # :param max_retries: Maximum number of retries for a task after a timeout. Defaults to 3.
457
- # :raises TimeoutError: If a thread processing a priority level exceeds the allowed timeout duration.
458
- # :return: None
459
- # """
460
- # update_plan_table = self.generate_update_plan_with_conditions()
461
- #
462
- # # Filter out rows that do not require updates (priority 0 means skip)
463
- # with self._lock:
464
- # update_plan_table = update_plan_table[
465
- # (update_plan_table["update_required"] == True) & (update_plan_table["update_priority"] != 0)
466
- # ]
467
- # # Display the update plan table to the user if requested
468
- # if len(update_plan_table.index) == 0:
469
- # return
470
- # if self.show_progress:
471
- # display(update_plan_table)
472
- # # Group by priority
473
- # with self._lock:
474
- # priorities = sorted(update_plan_table["update_priority"].unique())
475
- #
476
- # # We will process each priority level in its own thread.
477
- # # Each thread will handle all dates associated with that priority.
478
- # def process_priority(priority):
479
- # # Extract dates for the current priority
480
- # dates_to_process = update_plan_table[
481
- # update_plan_table["update_priority"] == priority
482
- # ]["date"].tolist()
483
- #
484
- # # If show_progress is True, wrap in a progress bar
485
- # date_iterator = dates_to_process
486
- # if self.show_progress:
487
- # date_iterator = tqdm(date_iterator,
488
- # desc=f"Processing priority {priority}:{self.dataclass.__name__}",
489
- # unit="date")
490
- #
491
- # # Process each date for this priority
492
- # for current_date in date_iterator:
493
- # self.process_date(current_date)
494
- #
495
- # # Launch a separate thread for each priority
496
- # with ThreadPoolExecutor(max_workers=len(priorities)) as executor:
497
- # futures = {executor.submit(process_priority, p): p for p in priorities}
498
- # retries = {p: 0 for p in priorities} # Track retry counts for each priority
499
- #
500
- # while futures:
501
- # for future in list(futures.keys()):
502
- # try:
503
- # future.result(timeout=self.timeout)
504
- # del futures[future] # Remove completed future
505
- # except TimeoutError:
506
- # priority = futures[future]
507
- # retries[priority] += 1
508
- #
509
- # if retries[priority] <= max_retries:
510
- # self.logger.warning(
511
- # f"Thread for priority {priority} timed out. Retrying ({retries[priority]}/{max_retries})..."
512
- # )
513
- # new_future = executor.submit(process_priority, priority)
514
- # futures[new_future] = priority
515
- # else:
516
- # self.logger.error(
517
- # f"Thread for priority {priority} timed out. Max retries ({max_retries}) exceeded. Skipping."
518
- # )
519
- # del futures[future] # Remove the timed-out future
520
- # except Exception as e:
521
- # self.logger.error(f"Error processing priority {futures[future]}: {e}")
522
- # del futures[future] # Remove the failed future
523
- #
524
- # def process_date(self, date: datetime.date):
525
- # """
526
- # Processes data for a given date and saves it as a Parquet file.
527
- #
528
- # This method processes data for the specified date by loading the data
529
- # corresponding to that day, saving it into a structured storage format
530
- # (Parquet), and logging relevant information such as processing time
531
- # and errors that may occur during the process. It uses provided
532
- # dataclass and parameters to operate and ensures the data is stored
533
- # in a structured folder hierarchy.
534
- #
535
- # :param date: The specific date for which data processing and saving should occur
536
- # :type date: datetime.date
537
- # :return: None
538
- # """
539
- # folder = f'{self.data_path}{date.year}/{date.month:02d}/{date.day:02d}/'
540
- # full_parquet_filename = f"{folder}{self.parquet_filename}"
541
- #
542
- # start_time = datetime.datetime.now()
543
- # self.logger.info(f"Processing date: {date}")
544
- # self.logger.info(f"Processing {full_parquet_filename}...")
545
- #
546
- # data_object = self.dataclass(**self.class_params)
547
- # df = data_object.load_period(dt_field=self.date_field, start=date, end=date)
548
- #
549
- # if len(df.index) == 0:
550
- # self.logger.error("No data found for the specified date.")
551
- # return
552
- #
553
- # with self._lock:
554
- # parquet_saver = ParquetSaver(df, parquet_storage_path=folder, logger=self.logger, fs=self.fs)
555
- # parquet_saver.save_to_parquet(self.parquet_filename, clear_existing=True)
556
- #
557
- # end_time = datetime.datetime.now()
558
- # duration_seconds = (end_time - start_time).total_seconds()
559
- # self.logger.info(
560
- # f"Data saved to {full_parquet_filename}. Processing time: {duration_seconds:.2f} seconds"
561
- # )
562
- #
563
- # self.processed_dates.append(date)
564
- # self.logger.info(f"Finished processing date: {date}")
565
- #
566
- # def generate_update_plan_with_conditions(self):
567
- # """
568
- # Generates an update plan for data files based on specific conditions. The function evaluates the need for updating or
569
- # overwriting data files for a given date range. Conditions include file existence, whether the file falls within a
570
- # specified historical threshold, and the necessity to overwrite or handle missing files. A priority map is utilized to
571
- # assign priority levels to update categories.
572
- #
573
- # :raises FileNotFoundError: If any file is referenced that does not exist and the ``ignore_missing`` property is set to False.
574
- # :raises AttributeError: If any required attribute like ``fs``, ``dataclass``, or others are not properly set or initialized.
575
- #
576
- # :return: A Pandas DataFrame representing the update plan, where each row contains information about a date, the conditions
577
- # evaluated for that date, and the determined update priority.
578
- # :rtype: pandas.DataFrame
579
- # """
580
- # rows = []
581
- #
582
- # today = datetime.date.today()
583
- # history_start_date = today - datetime.timedelta(days=self.history_days_threshold)
584
- # priority_map = {
585
- # "file is recent":0,
586
- # "overwrite": 1,
587
- # "history_days": 2,
588
- # "missing_files": 3
589
- # }
590
- # date_range = self.generate_date_range()
591
- # if self.show_progress:
592
- # date_range = tqdm(date_range, desc=f"Evaluating update plan:{self.dataclass.__name__}", unit="date")
593
- #
594
- # for current_date in date_range:
595
- # folder = f'{self.data_path}{current_date.year}/{current_date.month:02d}/{current_date.day:02d}/'
596
- # full_parquet_filename = f"{folder}{self.parquet_filename}"
597
- #
598
- # file_exists = self.fs.exists(full_parquet_filename)
599
- # within_history = history_start_date <= current_date <= today
600
- # missing_file = not file_exists and not self.ignore_missing
601
- # category = None
602
- # update_required = False
603
- #
604
- # # Hierarchy 1: Overwrite
605
- # if self.overwrite:
606
- # category = "overwrite"
607
- # update_required = True
608
- # elif missing_file and current_date < today:
609
- # category = "missing_files"
610
- # update_required = True
611
- #
612
- # elif within_history:
613
- # if file_exists:
614
- # if self.date_utils.is_file_older_than(
615
- # full_parquet_filename,
616
- # max_age_minutes=self.max_age_minutes,
617
- # fs=self.fs,
618
- # ignore_missing=self.ignore_missing,
619
- # verbose=self.verbose
620
- # ):
621
- # category = "history_days"
622
- # update_required = True
623
- # else:
624
- # category = "file is recent"
625
- # update_required = False
626
- # else:
627
- # category = "missing_files"
628
- # update_required = True
629
- # else:
630
- # category = "No Update Required"
631
- # update_required = False
632
- #
633
- # # Collect condition descriptions for the update plan table
634
- # row = {
635
- # "date": current_date,
636
- # "file_exists": file_exists,
637
- # "within_history": within_history,
638
- # "missing_file": missing_file,
639
- # "update_required": update_required,
640
- # "update_category": category,
641
- # "datawrapper class": self.dataclass.__name__,
642
- # "update_priority": priority_map.get(category, 0)
643
- # }
644
- # rows.append(row)
645
- #
646
- # update_plan_table = pd.DataFrame(rows)
647
- # return update_plan_table
648
- #
649
- # # # wrapper.process()
650
- # # # wrapper = DataWrapper(
651
- # # # dataclass=YourDataClass,
652
- # # # date_field="created_at",
653
- # # # data_path="s3://your-bucket-name/path/to/data",
654
- # # # parquet_filename="data.parquet",
655
- # # # start_date="2022-01-01",
656
- # # # end_date="2022-12-31",
657
- # # # filesystem_type="s3",
658
- # # # filesystem_options={
659
- # # # "key": "your_aws_access_key",
660
- # # # "secret": "your_aws_secret_key",
661
- # # # "client_kwargs": {"endpoint_url": "https://s3.amazonaws.com"}
662
- # # # },
663
- # # # verbose=True
664
- # # #)
665
- # # #wrapper.process()
301
+
@@ -14,6 +14,13 @@ class DfUtils:
14
14
  """
15
15
  self.logger = logger or Logger.default_logger(logger_name=self.__class__.__name__)
16
16
 
17
+ @classmethod
18
+ def compute_to_list(cls, series):
19
+ return series.compute().tolist() if hasattr(series, "compute") else series.tolist()
20
+
21
+ def extract_unique_values(self, df, *columns):
22
+ return {col: self.compute_to_list(df[col].dropna().unique()) for col in columns}
23
+
17
24
  def align_and_merge_by_type(self, df_left, df_right, type_mapping, how='left'):
18
25
  """
19
26
  Align column data types in two DataFrames based on a type mapping dictionary and perform the merge.
@@ -26,6 +26,12 @@ class Logger:
26
26
  :type logger: logging.Logger
27
27
  """
28
28
 
29
+ DEBUG = logging.DEBUG
30
+ INFO = logging.INFO
31
+ WARNING = logging.WARNING
32
+ ERROR = logging.ERROR
33
+ CRITICAL = logging.CRITICAL
34
+
29
35
  def __init__(self, log_dir: str, logger_name: str, log_file: str, log_level: int = logging.DEBUG):
30
36
  """
31
37
  Initialize the Logger instance.
@@ -1,4 +1,5 @@
1
1
  import base64
2
+ import logging
2
3
  from pathlib import Path
3
4
  from typing import Optional
4
5
 
@@ -16,7 +17,7 @@ from sibi_dst.utils import Logger
16
17
 
17
18
 
18
19
  class ParquetSaver:
19
- def __init__(self, df_result, parquet_storage_path, logger=None, fs=None):
20
+ def __init__(self, df_result, parquet_storage_path, logger=None, fs=None, debug=False):
20
21
  """
21
22
  Initialize ParquetSaver.
22
23
  :param df_result: Dask DataFrame to save.
@@ -26,8 +27,9 @@ class ParquetSaver:
26
27
  """
27
28
  self.df_result = df_result
28
29
  self.parquet_storage_path = parquet_storage_path.rstrip("/")
30
+ self.debug = debug
29
31
  self.logger = logger or Logger.default_logger(logger_name=self.__class__.__name__)
30
-
32
+ self.logger.set_level(logging.DEBUG if self.debug else logging.INFO)
31
33
  self.fs = fs
32
34
  self.protocol = self.parquet_storage_path.split(":")[0]
33
35
 
@@ -4,13 +4,14 @@ import fsspec
4
4
 
5
5
 
6
6
  class StorageManager:
7
- def __init__(self, storage_path, fs_type="file", fs_options=None):
7
+ def __init__(self, storage_path, fs_type="file", fs_options=None, debug=False):
8
8
  """
9
9
  Initializes the StorageManager with the base storage path and file system settings.
10
10
  :param storage_path: Base path for the storage (e.g., "s3://my-bucket").
11
11
  :param fs_type: File system type (e.g., "file", "s3").
12
12
  :param fs_options: Dictionary of options for fsspec file system (e.g., credentials).
13
13
  """
14
+ self.debug = debug
14
15
  # Ensure the storage_path ends with a slash for consistency
15
16
  self.storage_path = storage_path.rstrip("/")
16
17
  self.fs_type = fs_type
@@ -35,9 +36,11 @@ class StorageManager:
35
36
  :param dirs_to_create: List of subdirectories to create.
36
37
  :param clear_existing: Whether to clear existing directories.
37
38
  """
38
- print(f"Setting up directories under: {base_path}")
39
+ if self.debug:
40
+ print(f"Setting up directories under: {base_path}")
39
41
  if clear_existing:
40
- print(f"Warning: All existing contents in {base_path} will be removed.")
42
+ if self.debug:
43
+ print(f"Warning: All existing contents in {base_path} will be removed.")
41
44
  if self.fs.exists(base_path):
42
45
  self.fs.rm(base_path, recursive=True)
43
46
 
@@ -47,7 +50,8 @@ class StorageManager:
47
50
  # Create subdirectories
48
51
  for sub_directory in dirs_to_create:
49
52
  sub_path = self.join_paths(base_path, sub_directory)
50
- print(f"Creating directory: {sub_path}")
53
+ if self.debug:
54
+ print(f"Creating directory: {sub_path}")
51
55
  if clear_existing and self.fs.exists(sub_path):
52
56
  self.fs.rm(sub_path, recursive=True)
53
57
  self.fs.mkdirs(sub_path, exist_ok=True)
@@ -63,7 +67,8 @@ class StorageManager:
63
67
  # Ensure directories exist (optionally clear existing ones)
64
68
  for depot, sub_directories in depots.items():
65
69
  depot_path = self.join_paths(self.storage_path, depot)
66
- print(f"Rebuilding depot at: {depot_path}")
70
+ if self.debug:
71
+ print(f"Rebuilding depot at: {depot_path}")
67
72
  self.setup_directories(depot_path, sub_directories, clear_existing=clear_existing)
68
73
 
69
74
  # Generate depot_paths dictionary
@@ -89,9 +94,11 @@ class StorageManager:
89
94
  :param depots: Dictionary where keys are depot names and values are subdirectory lists.
90
95
  :param clear_existing: Whether to clear existing directories.
91
96
  """
92
- print("Rebuilding depot structure...")
97
+ if self.debug:
98
+ print("Rebuilding depot structure...")
93
99
  self.rebuild_depot_paths(depots, clear_existing=clear_existing)
94
- print("Rebuild complete.")
100
+ if self.debug:
101
+ print("Rebuild complete.")
95
102
 
96
103
  def get_fs_instance(self):
97
104
  return fsspec.filesystem(self.fs_type, **self.fs_options)