dtypeflow 1.0.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.
- dtypeflow/__init__.py +5 -0
- dtypeflow/core/__init__.py +1 -0
- dtypeflow/core/candidate/__init__.py +40 -0
- dtypeflow/core/candidate/base.py +37 -0
- dtypeflow/core/candidate/factory.py +258 -0
- dtypeflow/core/candidate/primitive.py +79 -0
- dtypeflow/core/dataset.py +46 -0
- dtypeflow/core/dtype/__init__.py +70 -0
- dtypeflow/core/dtype/base.py +14 -0
- dtypeflow/core/dtype/hierachy.md +51 -0
- dtypeflow/core/dtype/nested/__init__.py +5 -0
- dtypeflow/core/dtype/nested/dictionary.py +14 -0
- dtypeflow/core/dtype/primitive/__init__.py +39 -0
- dtypeflow/core/dtype/primitive/base.py +6 -0
- dtypeflow/core/dtype/primitive/boolean.py +20 -0
- dtypeflow/core/dtype/primitive/float.py +40 -0
- dtypeflow/core/dtype/primitive/integer/__init__.py +31 -0
- dtypeflow/core/dtype/primitive/integer/base.py +17 -0
- dtypeflow/core/dtype/primitive/integer/signed.py +37 -0
- dtypeflow/core/dtype/primitive/integer/unsigned.py +37 -0
- dtypeflow/core/dtype/primitive/string.py +14 -0
- dtypeflow/core/override/__init__.py +41 -0
- dtypeflow/core/override/base.py +22 -0
- dtypeflow/core/override/primitive.py +158 -0
- dtypeflow/core/profile/__init__.py +39 -0
- dtypeflow/core/profile/base.py +50 -0
- dtypeflow/core/profile/primitive.py +166 -0
- dtypeflow/core/profiler.py +169 -0
- dtypeflow/core/schema.py +33 -0
- dtypeflow/core/table.py +29 -0
- dtypeflow/py.typed +0 -0
- dtypeflow/tui/__init__.py +17 -0
- dtypeflow/tui/app.py +747 -0
- dtypeflow/tui/app.tcss +220 -0
- dtypeflow/tui/compatibility.py +38 -0
- dtypeflow/tui/dto.py +44 -0
- dtypeflow/tui/service.py +213 -0
- dtypeflow/utils.py +32 -0
- dtypeflow-1.0.0.dist-info/METADATA +93 -0
- dtypeflow-1.0.0.dist-info/RECORD +43 -0
- dtypeflow-1.0.0.dist-info/WHEEL +4 -0
- dtypeflow-1.0.0.dist-info/entry_points.txt +3 -0
- dtypeflow-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal, InvalidOperation
|
|
4
|
+
from typing import TYPE_CHECKING, Final
|
|
5
|
+
|
|
6
|
+
from dtypeflow.core.dataset import Dataset
|
|
7
|
+
from dtypeflow.core.dtype import (
|
|
8
|
+
FLOAT_TYPES,
|
|
9
|
+
SIGNED_INTEGER_TYPES,
|
|
10
|
+
UNSIGNED_INTEGER_TYPES,
|
|
11
|
+
BooleanDtype,
|
|
12
|
+
Float16Dtype,
|
|
13
|
+
Float32Dtype,
|
|
14
|
+
Float64Dtype,
|
|
15
|
+
Int8Dtype,
|
|
16
|
+
Int16Dtype,
|
|
17
|
+
Int32Dtype,
|
|
18
|
+
Int64Dtype,
|
|
19
|
+
StringDtype,
|
|
20
|
+
UInt8Dtype,
|
|
21
|
+
UInt16Dtype,
|
|
22
|
+
UInt32Dtype,
|
|
23
|
+
UInt64Dtype,
|
|
24
|
+
)
|
|
25
|
+
from dtypeflow.core.profile import (
|
|
26
|
+
BooleanProfile,
|
|
27
|
+
Float16Profile,
|
|
28
|
+
Float32Profile,
|
|
29
|
+
Float64Profile,
|
|
30
|
+
Int8Profile,
|
|
31
|
+
Int16Profile,
|
|
32
|
+
Int32Profile,
|
|
33
|
+
Int64Profile,
|
|
34
|
+
Profile,
|
|
35
|
+
StringProfile,
|
|
36
|
+
UInt8Profile,
|
|
37
|
+
UInt16Profile,
|
|
38
|
+
UInt32Profile,
|
|
39
|
+
UInt64Profile,
|
|
40
|
+
)
|
|
41
|
+
from dtypeflow.utils import read_csv
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from collections.abc import Mapping
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
from dtypeflow.core.dtype import Dtype
|
|
48
|
+
PROFILE_MAPPER: Final[dict[type[Dtype], type[Profile]]] = {
|
|
49
|
+
BooleanDtype: BooleanProfile,
|
|
50
|
+
UInt8Dtype: UInt8Profile,
|
|
51
|
+
UInt16Dtype: UInt16Profile,
|
|
52
|
+
UInt32Dtype: UInt32Profile,
|
|
53
|
+
UInt64Dtype: UInt64Profile,
|
|
54
|
+
Int8Dtype: Int8Profile,
|
|
55
|
+
Int16Dtype: Int16Profile,
|
|
56
|
+
Int32Dtype: Int32Profile,
|
|
57
|
+
Int64Dtype: Int64Profile,
|
|
58
|
+
Float16Dtype: Float16Profile,
|
|
59
|
+
Float32Dtype: Float32Profile,
|
|
60
|
+
Float64Dtype: Float64Profile,
|
|
61
|
+
StringDtype: StringProfile,
|
|
62
|
+
}
|
|
63
|
+
_BOOLEAN_UNIQUE_COUNT: Final[int] = 2
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _try_int(value: str, /) -> int | None:
|
|
67
|
+
try:
|
|
68
|
+
return int(value)
|
|
69
|
+
except ValueError:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _try_decimal(value: str, /) -> Decimal | None:
|
|
74
|
+
try:
|
|
75
|
+
return Decimal(value)
|
|
76
|
+
except InvalidOperation:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _pick_integer_type(min_value: int, max_value: int, /) -> type[Dtype] | None:
|
|
81
|
+
integer_types = SIGNED_INTEGER_TYPES if min_value < 0 else UNSIGNED_INTEGER_TYPES
|
|
82
|
+
for integer_type in integer_types:
|
|
83
|
+
if (
|
|
84
|
+
min_value >= integer_type.LOWER_BOUND
|
|
85
|
+
and max_value <= integer_type.HIGHER_BOUND
|
|
86
|
+
):
|
|
87
|
+
return integer_type
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _pick_float_type(max_abs: Decimal, max_digits: int, /) -> type[Dtype] | None:
|
|
92
|
+
for float_type in FLOAT_TYPES:
|
|
93
|
+
if (
|
|
94
|
+
max_abs <= float_type.MAX_ABSOLUTE_VALUE
|
|
95
|
+
and max_digits <= float_type.MAX_SIGNIFICANT_DIGITS
|
|
96
|
+
):
|
|
97
|
+
return float_type
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def classify(*values: str) -> tuple[type[Dtype], Mapping[str, int | Decimal] | None]:
|
|
102
|
+
if len(values) == _BOOLEAN_UNIQUE_COUNT:
|
|
103
|
+
return (BooleanDtype, None)
|
|
104
|
+
is_integer = True
|
|
105
|
+
min_int: int | None = None
|
|
106
|
+
max_int: int | None = None
|
|
107
|
+
int_by_value: dict[str, int] = {}
|
|
108
|
+
is_decimal = True
|
|
109
|
+
max_abs: Decimal | None = None
|
|
110
|
+
max_digits = 0
|
|
111
|
+
decimal_by_value: dict[str, Decimal] = {}
|
|
112
|
+
for value in values:
|
|
113
|
+
if is_integer:
|
|
114
|
+
as_int = _try_int(value)
|
|
115
|
+
if as_int is None:
|
|
116
|
+
is_integer = False
|
|
117
|
+
else:
|
|
118
|
+
int_by_value[value] = as_int
|
|
119
|
+
min_int = as_int if min_int is None else min(min_int, as_int)
|
|
120
|
+
max_int = as_int if max_int is None else max(max_int, as_int)
|
|
121
|
+
if is_decimal:
|
|
122
|
+
as_decimal = _try_decimal(value)
|
|
123
|
+
if as_decimal is None:
|
|
124
|
+
is_decimal = False
|
|
125
|
+
else:
|
|
126
|
+
decimal_by_value[value] = as_decimal
|
|
127
|
+
abs_value = abs(as_decimal)
|
|
128
|
+
max_abs = abs_value if max_abs is None else max(max_abs, abs_value)
|
|
129
|
+
max_digits = max(max_digits, len(as_decimal.as_tuple().digits))
|
|
130
|
+
if is_integer and min_int is not None and (max_int is not None):
|
|
131
|
+
integer_type = _pick_integer_type(min_int, max_int)
|
|
132
|
+
if integer_type is not None:
|
|
133
|
+
return (integer_type, int_by_value)
|
|
134
|
+
if is_decimal and max_abs is not None:
|
|
135
|
+
float_type = _pick_float_type(max_abs, max_digits)
|
|
136
|
+
if float_type is not None:
|
|
137
|
+
return (float_type, decimal_by_value)
|
|
138
|
+
return (StringDtype, None)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def profile_column(*values: str | None) -> Profile:
|
|
142
|
+
stripped: tuple[str | None, ...] = tuple(
|
|
143
|
+
value.strip() if value is not None else None for value in values
|
|
144
|
+
)
|
|
145
|
+
non_nan: tuple[int, ...] = tuple(
|
|
146
|
+
index for (index, value) in enumerate(stripped) if value
|
|
147
|
+
)
|
|
148
|
+
nan: tuple[int, ...] = tuple(
|
|
149
|
+
index for (index, value) in enumerate(stripped) if not value
|
|
150
|
+
)
|
|
151
|
+
value_to_index: dict[str, int] = {stripped[index]: index for index in non_nan}
|
|
152
|
+
(dtype, parsed) = classify(*value_to_index)
|
|
153
|
+
sort_key = (
|
|
154
|
+
(lambda item: parsed[item[0]]) if parsed is not None else lambda item: item[0]
|
|
155
|
+
)
|
|
156
|
+
uniques: tuple[int, ...] = tuple(
|
|
157
|
+
index for (_, index) in sorted(value_to_index.items(), key=sort_key)
|
|
158
|
+
)
|
|
159
|
+
return PROFILE_MAPPER[dtype](*stripped, uniques=uniques, non_nan=non_nan, nan=nan)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def profile(source: str | Path, /) -> Dataset:
|
|
163
|
+
return Dataset(
|
|
164
|
+
source,
|
|
165
|
+
**{
|
|
166
|
+
column: profile_column(*values)
|
|
167
|
+
for (column, values) in read_csv(source).items()
|
|
168
|
+
},
|
|
169
|
+
)
|
dtypeflow/core/schema.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import pyarrow as pa
|
|
7
|
+
import pyarrow.parquet as pq
|
|
8
|
+
|
|
9
|
+
from dtypeflow.utils import read_csv
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from dtypeflow.core.dataset import Dataset
|
|
13
|
+
from dtypeflow.core.override import Override
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Schema:
|
|
17
|
+
def __init__(self, *overrides: Override) -> None:
|
|
18
|
+
self._overrides: tuple[Override, ...] = overrides
|
|
19
|
+
|
|
20
|
+
def apply(self, dataset: Dataset, /, *, destination: str | Path) -> None:
|
|
21
|
+
if isinstance(destination, str):
|
|
22
|
+
destination: Path = Path(destination)
|
|
23
|
+
columns: dict[str, Override] = {}
|
|
24
|
+
for override in self._overrides:
|
|
25
|
+
if override.column not in dataset:
|
|
26
|
+
msg = f"{override!r} is not valid for {dataset!r}."
|
|
27
|
+
raise ValueError(msg)
|
|
28
|
+
columns[override.column] = override
|
|
29
|
+
raw: dict[str, tuple[str, ...]] = dict(read_csv(dataset.source))
|
|
30
|
+
arrays = []
|
|
31
|
+
for column, override in columns.items():
|
|
32
|
+
arrays.append(override.transform(*raw.pop(column)))
|
|
33
|
+
pq.write_table(pa.Table.from_arrays(arrays, names=list(columns)), destination)
|
dtypeflow/core/table.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
|
|
6
|
+
from typing_extensions import override
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Table(Mapping[str, tuple[str, ...]]):
|
|
10
|
+
def __init__(self, **columns: Iterable[str]) -> None:
|
|
11
|
+
self._values: MappingProxyType[str, tuple[str, ...]] = MappingProxyType(
|
|
12
|
+
{name: tuple(column_values) for (name, column_values) in columns.items()},
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
@override
|
|
16
|
+
def __getitem__(self, name: str) -> tuple[str, ...]:
|
|
17
|
+
return self._values[name]
|
|
18
|
+
|
|
19
|
+
@override
|
|
20
|
+
def __iter__(self) -> Iterator[str]:
|
|
21
|
+
return iter(self._values)
|
|
22
|
+
|
|
23
|
+
@override
|
|
24
|
+
def __len__(self) -> int:
|
|
25
|
+
return len(self._values)
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
def __repr__(self) -> str:
|
|
29
|
+
return f"{type(self).__name__}({dict(self._values)!r})"
|
dtypeflow/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
__all__: list[str] = ["run"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run() -> None:
|
|
9
|
+
from dtypeflow.tui.app import DtypeflowApp
|
|
10
|
+
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
prog="dtypeflow",
|
|
13
|
+
description="Interactive terminal UI to convert a CSV file to Parquet.",
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument("path", nargs="?", help="Path to a CSV file to load on startup")
|
|
16
|
+
args = parser.parse_args()
|
|
17
|
+
DtypeflowApp(initial_path=args.path).run()
|