Skip to content

Importing

cibmangotree.importing

Modules:

Name Description
csv
excel
importer

Classes

Modules

csv

Classes:

Name Description
CSVImporter
Classes
CSVImporter

Bases: Importer['CsvImportSession']

Source code in src/cibmangotree/importing/csv.py
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
class CSVImporter(Importer["CsvImportSession"]):
    @property
    def name(self) -> str:
        return "CSV"

    def suggest(self, input_path: str) -> bool:
        return input_path.endswith(".csv") or input_path == "text/csv"

    def _detect_skip_rows_and_dialect(
        self, input_path: str | BytesIO
    ) -> tuple[int, csv.Dialect]:
        """Detect the number of rows to skip before CSV data begins and the CSV dialect."""
        skip_rows = 0

        try:
            with open(input_path, "r", encoding="utf8") as file:
                MAX_LINES = 50  # check the first 50 lines only
                lines = [line.strip() for i, line in enumerate(file) if i <= MAX_LINES]
                total_lines = len(lines)

                # Only analyze if we have enough lines
                if len(lines) >= 2:
                    # Parse each line and analyze content, keeping track of original line numbers
                    parsed_rows = []
                    line_numbers = []
                    for line_idx, line in enumerate(lines):
                        if not line:  # Skip empty lines
                            continue
                        try:
                            reader = csv.reader([line])
                            row = next(reader)
                            parsed_rows.append(row)
                            line_numbers.append(line_idx)
                        except Exception:
                            parsed_rows.append([line])  # Fallback for problematic lines
                            line_numbers.append(line_idx)

                    if len(parsed_rows) >= 2:
                        # Look for the actual CSV header (column names)
                        for i, row in enumerate(parsed_rows):
                            if self._looks_like_csv_header(row):
                                skip_rows = line_numbers[i]
                                break
                        else:
                            # Fallback: use field count analysis
                            field_counts = [len(row) for row in parsed_rows]
                            from collections import Counter

                            count_frequency = Counter(field_counts)
                            most_common_count = count_frequency.most_common(1)[0][0]

                            # Find first row that matches the most common field count
                            for i, count in enumerate(field_counts):
                                if count == most_common_count:
                                    skip_rows = line_numbers[i]
                                    break

                # Validate skip_rows doesn't exceed available lines
                if skip_rows >= total_lines:
                    skip_rows = 0  # Reset to safe default

                # Now detect dialect from the CSV content (after skip_rows)
                file.seek(0)
                for _ in range(skip_rows):
                    file.readline()

                sample = file.read(65536)
                dialect = Sniffer().sniff(sample)

        except Exception:
            # If anything fails, use defaults and try basic dialect detection
            skip_rows = 0
            try:
                with open(input_path, "r", encoding="utf8") as file:
                    sample = file.read(65536)
                    dialect = Sniffer().sniff(sample)
            except Exception:
                # Create a default dialect if everything fails
                class DefaultDialect:
                    delimiter = ","
                    quotechar = '"'

                dialect = DefaultDialect()

        return skip_rows, dialect

    def _looks_like_csv_header(self, row: list[str]) -> bool:
        """Check if a row looks like a CSV header with column names."""
        if not row or len(row) < 2:
            return False

        # Skip rows where most fields are empty (likely CSV notes with trailing commas)
        non_empty_fields = [field.strip() for field in row if field.strip()]
        if len(non_empty_fields) < len(row) // 2:
            return False

        # Look for typical CSV header characteristics
        header_indicators = 0

        for field in non_empty_fields:
            field = field.lower().strip()

            # Common column name patterns
            if any(
                word in field
                for word in [
                    "id",
                    "name",
                    "date",
                    "time",
                    "user",
                    "tweet",
                    "text",
                    "count",
                    "number",
                    "sent",
                    "screen",
                    "retweeted",
                    "favorited",
                ]
            ):
                header_indicators += 1

            # Short descriptive column names (not long sentences like CSV notes)
            if 3 <= len(field) <= 30 and not field.startswith(
                ("http", "www", "from ", "if you")
            ):
                header_indicators += 1

        # Consider it a CSV header if at least 50% of non-empty fields look like column names
        return header_indicators >= len(non_empty_fields) * 0.5

    def init_session(self, input_path: str | BytesIO):
        skip_rows, dialect = self._detect_skip_rows_and_dialect(input_path)

        return CsvImportSession(
            input_file=input_path,
            separator=dialect.delimiter,
            quote_char=dialect.quotechar,
            has_header=True,
            skip_rows=skip_rows,
        )

excel

Classes:

Name Description
ExcelImporter
Classes
ExcelImporter

Bases: Importer['ExcelImportSession']

Methods:

Name Description
init_session

Initialize Excel import session.

Source code in src/cibmangotree/importing/excel.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class ExcelImporter(Importer["ExcelImportSession"]):
    @property
    def name(self) -> str:
        return "Excel"

    def suggest(self, input_path: str) -> bool:
        return (
            input_path.endswith(".xlsx")
            or input_path
            == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        )

    def init_session(self, input_path: str | BytesIO):
        """
        Initialize Excel import session.

        For single-sheet files, automatically selects the sheet.
        For multi-sheet files, returns None (caller must provide sheet selection).
        """
        reader = read_excel(
            cast(str, input_path)
            if type(input_path) is not BytesIO
            else input_path.getvalue()
        )
        sheet_names = reader.sheet_names

        if not sheet_names or (len(sheet_names) == 0 or len(sheet_names) > 1):
            return None

        return ExcelImportSession(
            input_file=input_path,
            selected_sheet=sheet_names[0],
            sheet_names=sheet_names,
        )
Methods:
init_session(input_path)

Initialize Excel import session.

For single-sheet files, automatically selects the sheet. For multi-sheet files, returns None (caller must provide sheet selection).

Source code in src/cibmangotree/importing/excel.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def init_session(self, input_path: str | BytesIO):
    """
    Initialize Excel import session.

    For single-sheet files, automatically selects the sheet.
    For multi-sheet files, returns None (caller must provide sheet selection).
    """
    reader = read_excel(
        cast(str, input_path)
        if type(input_path) is not BytesIO
        else input_path.getvalue()
    )
    sheet_names = reader.sheet_names

    if not sheet_names or (len(sheet_names) == 0 or len(sheet_names) > 1):
        return None

    return ExcelImportSession(
        input_file=input_path,
        selected_sheet=sheet_names[0],
        sheet_names=sheet_names,
    )

importer

Classes:

Name Description
Importer
ImporterSession

The ImporterSession interface handles the ongoing configuration of an import.

Classes
Importer

Bases: ABC

Methods:

Name Description
init_session

Produces an initial import session object that contains all the configuration

suggest

Check if the importer can handle the given file. This should be fairly

Attributes:

Name Type Description
name str

The name of the importer. It will be quoted in the UI in texts such as

Source code in src/cibmangotree/importing/importer.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class Importer[SessionType](ABC):
    @property
    @abstractmethod
    def name(self) -> str:
        """
        The name of the importer. It will be quoted in the UI in texts such as
        "Imported as `name`, so keep it to a format name."
        """
        pass

    @abstractmethod
    def suggest(self, input_path: str) -> bool:
        """
        Check if the importer can handle the given file. This should be fairly
        restrictive based on reasonable assumptions, as it is only used for the
        initial importer suggestion. The user can always override the suggestion.
        """
        pass

    @abstractmethod
    def init_session(self, input_path: str | BytesIO) -> SessionType | None:
        """
        Produces an initial import session object that contains all the configuration
        needed for the import. The user can either accept this configuration or
        customize it.

        Return None here if the importer cannot figure out how to configure the
        import parameters. This doesn't necessarily mean that the file cannot be
        loaded; the UI will force the user to customize the import session if the
        user wants to proceed with this importer.
        """
        pass
Attributes
name abstractmethod property

The name of the importer. It will be quoted in the UI in texts such as "Imported as name, so keep it to a format name."

Methods:
init_session(input_path) abstractmethod

Produces an initial import session object that contains all the configuration needed for the import. The user can either accept this configuration or customize it.

Return None here if the importer cannot figure out how to configure the import parameters. This doesn't necessarily mean that the file cannot be loaded; the UI will force the user to customize the import session if the user wants to proceed with this importer.

Source code in src/cibmangotree/importing/importer.py
54
55
56
57
58
59
60
61
62
63
64
65
66
@abstractmethod
def init_session(self, input_path: str | BytesIO) -> SessionType | None:
    """
    Produces an initial import session object that contains all the configuration
    needed for the import. The user can either accept this configuration or
    customize it.

    Return None here if the importer cannot figure out how to configure the
    import parameters. This doesn't necessarily mean that the file cannot be
    loaded; the UI will force the user to customize the import session if the
    user wants to proceed with this importer.
    """
    pass
suggest(input_path) abstractmethod

Check if the importer can handle the given file. This should be fairly restrictive based on reasonable assumptions, as it is only used for the initial importer suggestion. The user can always override the suggestion.

Source code in src/cibmangotree/importing/importer.py
45
46
47
48
49
50
51
52
@abstractmethod
def suggest(self, input_path: str) -> bool:
    """
    Check if the importer can handle the given file. This should be fairly
    restrictive based on reasonable assumptions, as it is only used for the
    initial importer suggestion. The user can always override the suggestion.
    """
    pass
ImporterSession

Bases: ABC

The ImporterSession interface handles the ongoing configuration of an import. It keeps the configuration state and can load a preview of the data from the input file.

Methods:

Name Description
import_as_parquet

Import the data from the input file to the output file in the Parquet format.

load_preview

Attempt to load a preview of the data from the input file.

Source code in src/cibmangotree/importing/importer.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class ImporterSession(ABC):
    """
    The ImporterSession interface handles the ongoing configuration of an import.
    It keeps the configuration state and can load a preview of the data from the input file.
    """

    @abstractmethod
    def load_preview(self, n_records: int) -> pl.DataFrame | None:
        """
        Attempt to load a preview of the data from the input file.

        Return None here if it is sure that the file cannot be loaded with the current
        configuration. Only throw an execption in the case of unexpected errors.
        """
        pass

    @abstractmethod
    def import_as_parquet(self, output_path: str) -> None:
        """
        Import the data from the input file to the output file in the Parquet format.
        """
        pass
Methods:
import_as_parquet(output_path) abstractmethod

Import the data from the input file to the output file in the Parquet format.

Source code in src/cibmangotree/importing/importer.py
24
25
26
27
28
29
@abstractmethod
def import_as_parquet(self, output_path: str) -> None:
    """
    Import the data from the input file to the output file in the Parquet format.
    """
    pass
load_preview(n_records) abstractmethod

Attempt to load a preview of the data from the input file.

Return None here if it is sure that the file cannot be loaded with the current configuration. Only throw an execption in the case of unexpected errors.

Source code in src/cibmangotree/importing/importer.py
14
15
16
17
18
19
20
21
22
@abstractmethod
def load_preview(self, n_records: int) -> pl.DataFrame | None:
    """
    Attempt to load a preview of the data from the input file.

    Return None here if it is sure that the file cannot be loaded with the current
    configuration. Only throw an execption in the case of unexpected errors.
    """
    pass