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.
Files changed (43) hide show
  1. dtypeflow/__init__.py +5 -0
  2. dtypeflow/core/__init__.py +1 -0
  3. dtypeflow/core/candidate/__init__.py +40 -0
  4. dtypeflow/core/candidate/base.py +37 -0
  5. dtypeflow/core/candidate/factory.py +258 -0
  6. dtypeflow/core/candidate/primitive.py +79 -0
  7. dtypeflow/core/dataset.py +46 -0
  8. dtypeflow/core/dtype/__init__.py +70 -0
  9. dtypeflow/core/dtype/base.py +14 -0
  10. dtypeflow/core/dtype/hierachy.md +51 -0
  11. dtypeflow/core/dtype/nested/__init__.py +5 -0
  12. dtypeflow/core/dtype/nested/dictionary.py +14 -0
  13. dtypeflow/core/dtype/primitive/__init__.py +39 -0
  14. dtypeflow/core/dtype/primitive/base.py +6 -0
  15. dtypeflow/core/dtype/primitive/boolean.py +20 -0
  16. dtypeflow/core/dtype/primitive/float.py +40 -0
  17. dtypeflow/core/dtype/primitive/integer/__init__.py +31 -0
  18. dtypeflow/core/dtype/primitive/integer/base.py +17 -0
  19. dtypeflow/core/dtype/primitive/integer/signed.py +37 -0
  20. dtypeflow/core/dtype/primitive/integer/unsigned.py +37 -0
  21. dtypeflow/core/dtype/primitive/string.py +14 -0
  22. dtypeflow/core/override/__init__.py +41 -0
  23. dtypeflow/core/override/base.py +22 -0
  24. dtypeflow/core/override/primitive.py +158 -0
  25. dtypeflow/core/profile/__init__.py +39 -0
  26. dtypeflow/core/profile/base.py +50 -0
  27. dtypeflow/core/profile/primitive.py +166 -0
  28. dtypeflow/core/profiler.py +169 -0
  29. dtypeflow/core/schema.py +33 -0
  30. dtypeflow/core/table.py +29 -0
  31. dtypeflow/py.typed +0 -0
  32. dtypeflow/tui/__init__.py +17 -0
  33. dtypeflow/tui/app.py +747 -0
  34. dtypeflow/tui/app.tcss +220 -0
  35. dtypeflow/tui/compatibility.py +38 -0
  36. dtypeflow/tui/dto.py +44 -0
  37. dtypeflow/tui/service.py +213 -0
  38. dtypeflow/utils.py +32 -0
  39. dtypeflow-1.0.0.dist-info/METADATA +93 -0
  40. dtypeflow-1.0.0.dist-info/RECORD +43 -0
  41. dtypeflow-1.0.0.dist-info/WHEEL +4 -0
  42. dtypeflow-1.0.0.dist-info/entry_points.txt +3 -0
  43. 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
+ )
@@ -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)
@@ -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()