fixedwidthparser 2.0.0rc1__py3-none-manylinux_2_34_x86_64.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.
- fixedwidth/__init__.py +69 -0
- fixedwidth/_lib/.gitkeep +0 -0
- fixedwidth/_lib/FixedWidthParser.Native.so +0 -0
- fixedwidth/_native.py +248 -0
- fixedwidth/arrow_stream.py +127 -0
- fixedwidth/layout.py +94 -0
- fixedwidth/py.typed +0 -0
- fixedwidth/reader.py +157 -0
- fixedwidth/table.py +135 -0
- fixedwidth/types.py +136 -0
- fixedwidth/writer.py +183 -0
- fixedwidthparser-2.0.0rc1.dist-info/METADATA +111 -0
- fixedwidthparser-2.0.0rc1.dist-info/RECORD +14 -0
- fixedwidthparser-2.0.0rc1.dist-info/WHEEL +4 -0
fixedwidth/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Read and write fixed-width files through the FixedWidthParser native library.
|
|
2
|
+
|
|
3
|
+
import fixedwidth as fw
|
|
4
|
+
|
|
5
|
+
layout = fw.Layout([
|
|
6
|
+
fw.Column("codneg", 12, 12, fw.String),
|
|
7
|
+
fw.Column("preabe", 56, 13, fw.Float64, scale=2),
|
|
8
|
+
])
|
|
9
|
+
df = fw.read_pandas("COTAHIST_A2023.TXT", layout, fw.ReadOptions(header_lines=1, skip_invalid=True))
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from fixedwidth.arrow_stream import ArrowStream
|
|
13
|
+
from fixedwidth.layout import Layout
|
|
14
|
+
from fixedwidth.reader import read, read_arrow, read_arrow_stream, read_batches, read_pandas, read_polars
|
|
15
|
+
from fixedwidth.table import Table
|
|
16
|
+
from fixedwidth.types import (
|
|
17
|
+
Alignment,
|
|
18
|
+
Column,
|
|
19
|
+
ColumnType,
|
|
20
|
+
FixedWidthError,
|
|
21
|
+
LayoutOptions,
|
|
22
|
+
Overflow,
|
|
23
|
+
ParseError,
|
|
24
|
+
ReaderOptions,
|
|
25
|
+
ReadOptions,
|
|
26
|
+
TrimMode,
|
|
27
|
+
WriteOptions,
|
|
28
|
+
)
|
|
29
|
+
from fixedwidth.writer import write, write_bytes
|
|
30
|
+
|
|
31
|
+
String = ColumnType.STRING
|
|
32
|
+
Int64 = ColumnType.INT64
|
|
33
|
+
Float64 = ColumnType.FLOAT64
|
|
34
|
+
Decimal = ColumnType.DECIMAL
|
|
35
|
+
Date = ColumnType.DATE
|
|
36
|
+
Time = ColumnType.TIME
|
|
37
|
+
Timestamp = ColumnType.TIMESTAMP
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"Alignment",
|
|
41
|
+
"ArrowStream",
|
|
42
|
+
"Column",
|
|
43
|
+
"ColumnType",
|
|
44
|
+
"Date",
|
|
45
|
+
"Decimal",
|
|
46
|
+
"FixedWidthError",
|
|
47
|
+
"Float64",
|
|
48
|
+
"Int64",
|
|
49
|
+
"Layout",
|
|
50
|
+
"LayoutOptions",
|
|
51
|
+
"Overflow",
|
|
52
|
+
"ParseError",
|
|
53
|
+
"ReadOptions",
|
|
54
|
+
"ReaderOptions",
|
|
55
|
+
"String",
|
|
56
|
+
"Table",
|
|
57
|
+
"Time",
|
|
58
|
+
"Timestamp",
|
|
59
|
+
"TrimMode",
|
|
60
|
+
"WriteOptions",
|
|
61
|
+
"read",
|
|
62
|
+
"read_arrow",
|
|
63
|
+
"read_arrow_stream",
|
|
64
|
+
"read_batches",
|
|
65
|
+
"read_pandas",
|
|
66
|
+
"read_polars",
|
|
67
|
+
"write",
|
|
68
|
+
"write_bytes",
|
|
69
|
+
]
|
fixedwidth/_lib/.gitkeep
ADDED
|
File without changes
|
|
Binary file
|
fixedwidth/_native.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""ctypes binding to the FixedWidthParser NativeAOT shared library.
|
|
2
|
+
|
|
3
|
+
Everything here mirrors src/FixedWidthParser.Native/include/fixedwidth.h. If you change one, change both.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import ctypes
|
|
9
|
+
import os
|
|
10
|
+
import platform
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
FW_OK = 0
|
|
15
|
+
FW_EOF = -1
|
|
16
|
+
FW_BUFFER_TOO_SMALL = -2
|
|
17
|
+
FW_INVALID_HANDLE = -3
|
|
18
|
+
FW_INVALID_ARGUMENT = -4
|
|
19
|
+
FW_ERROR = -5
|
|
20
|
+
FW_PARSE_ERROR = -6
|
|
21
|
+
|
|
22
|
+
FW_ABI_VERSION = 1
|
|
23
|
+
|
|
24
|
+
FW_OPT_DEFAULT = 0
|
|
25
|
+
FW_OPT_FALSE = 1
|
|
26
|
+
FW_OPT_TRUE = 2
|
|
27
|
+
|
|
28
|
+
_u8p = ctypes.POINTER(ctypes.c_uint8)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ColumnSpec(ctypes.Structure):
|
|
32
|
+
_fields_ = [
|
|
33
|
+
("struct_size", ctypes.c_int32),
|
|
34
|
+
("name", _u8p),
|
|
35
|
+
("name_len", ctypes.c_int32),
|
|
36
|
+
("start", ctypes.c_int32),
|
|
37
|
+
("length", ctypes.c_int32),
|
|
38
|
+
("type", ctypes.c_int32),
|
|
39
|
+
("nullable", ctypes.c_int32),
|
|
40
|
+
("scale", ctypes.c_int32),
|
|
41
|
+
("format", _u8p),
|
|
42
|
+
("format_len", ctypes.c_int32),
|
|
43
|
+
("trim_char", ctypes.c_int32),
|
|
44
|
+
("trim_mode", ctypes.c_int32),
|
|
45
|
+
("alignment", ctypes.c_int32),
|
|
46
|
+
("padding", ctypes.c_int32),
|
|
47
|
+
("overflow", ctypes.c_int32),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class LayoutOptions(ctypes.Structure):
|
|
52
|
+
_fields_ = [("struct_size", ctypes.c_int32), ("line_length", ctypes.c_int32), ("filler", ctypes.c_int32)]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ReadOptions(ctypes.Structure):
|
|
56
|
+
_fields_ = [
|
|
57
|
+
("struct_size", ctypes.c_int32),
|
|
58
|
+
("header_lines", ctypes.c_int64),
|
|
59
|
+
("skip_invalid", ctypes.c_int32),
|
|
60
|
+
("degree_of_parallelism", ctypes.c_int32),
|
|
61
|
+
("encoding", ctypes.c_int32),
|
|
62
|
+
("max_line_length", ctypes.c_int32),
|
|
63
|
+
("skip_byte_order_mark", ctypes.c_int32),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ReaderOptions(ctypes.Structure):
|
|
68
|
+
_fields_ = [("struct_size", ctypes.c_int32), ("read", ReadOptions), ("batch_rows", ctypes.c_int64)]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Column(ctypes.Structure):
|
|
72
|
+
_fields_ = [
|
|
73
|
+
("type", ctypes.c_int32),
|
|
74
|
+
("scale", ctypes.c_int32),
|
|
75
|
+
("length", ctypes.c_int64),
|
|
76
|
+
("values", ctypes.c_void_p),
|
|
77
|
+
("validity", ctypes.c_void_p),
|
|
78
|
+
("data", ctypes.c_void_p),
|
|
79
|
+
("data_len", ctypes.c_int64),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Table(ctypes.Structure):
|
|
84
|
+
_fields_ = [("column_count", ctypes.c_int32), ("row_count", ctypes.c_int64), ("columns", ctypes.POINTER(Column))]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ReadResult(ctypes.Structure):
|
|
88
|
+
_fields_ = [("struct_size", ctypes.c_int32), ("lines_read", ctypes.c_int64), ("lines_skipped", ctypes.c_int64)]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ParseErrorInfo(ctypes.Structure):
|
|
92
|
+
_fields_ = [("struct_size", ctypes.c_int32), ("line_number", ctypes.c_int64), ("column", ctypes.c_int32)]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class WriteOptions(ctypes.Structure):
|
|
96
|
+
_fields_ = [
|
|
97
|
+
("struct_size", ctypes.c_int32),
|
|
98
|
+
("encoding", ctypes.c_int32),
|
|
99
|
+
("newline", _u8p),
|
|
100
|
+
("newline_len", ctypes.c_int32),
|
|
101
|
+
("degree_of_parallelism", ctypes.c_int32),
|
|
102
|
+
("batch_rows", ctypes.c_int64),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Buffer(ctypes.Structure):
|
|
107
|
+
_fields_ = [("data", ctypes.c_void_p), ("length", ctypes.c_int64)]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ArrowSchema(ctypes.Structure):
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ArrowArray(ctypes.Structure):
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
_ArrowSchemaRelease = ctypes.CFUNCTYPE(None, ctypes.POINTER(ArrowSchema))
|
|
119
|
+
_ArrowArrayRelease = ctypes.CFUNCTYPE(None, ctypes.POINTER(ArrowArray))
|
|
120
|
+
|
|
121
|
+
ArrowSchema._fields_ = [
|
|
122
|
+
("format", ctypes.c_char_p),
|
|
123
|
+
("name", ctypes.c_char_p),
|
|
124
|
+
("metadata", ctypes.c_char_p),
|
|
125
|
+
("flags", ctypes.c_int64),
|
|
126
|
+
("n_children", ctypes.c_int64),
|
|
127
|
+
("children", ctypes.POINTER(ctypes.POINTER(ArrowSchema))),
|
|
128
|
+
("dictionary", ctypes.POINTER(ArrowSchema)),
|
|
129
|
+
("release", _ArrowSchemaRelease),
|
|
130
|
+
("private_data", ctypes.c_void_p),
|
|
131
|
+
]
|
|
132
|
+
ArrowArray._fields_ = [
|
|
133
|
+
("length", ctypes.c_int64),
|
|
134
|
+
("null_count", ctypes.c_int64),
|
|
135
|
+
("offset", ctypes.c_int64),
|
|
136
|
+
("n_buffers", ctypes.c_int64),
|
|
137
|
+
("n_children", ctypes.c_int64),
|
|
138
|
+
("buffers", ctypes.POINTER(ctypes.c_void_p)),
|
|
139
|
+
("children", ctypes.POINTER(ctypes.POINTER(ArrowArray))),
|
|
140
|
+
("dictionary", ctypes.POINTER(ArrowArray)),
|
|
141
|
+
("release", _ArrowArrayRelease),
|
|
142
|
+
("private_data", ctypes.c_void_p),
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ArrowArrayStream(ctypes.Structure):
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
_ArrowStreamGetSchema = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.POINTER(ArrowArrayStream), ctypes.POINTER(ArrowSchema))
|
|
151
|
+
_ArrowStreamGetNext = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.POINTER(ArrowArrayStream), ctypes.POINTER(ArrowArray))
|
|
152
|
+
_ArrowStreamGetLastError = ctypes.CFUNCTYPE(ctypes.c_char_p, ctypes.POINTER(ArrowArrayStream))
|
|
153
|
+
_ArrowStreamRelease = ctypes.CFUNCTYPE(None, ctypes.POINTER(ArrowArrayStream))
|
|
154
|
+
|
|
155
|
+
ArrowArrayStream._fields_ = [
|
|
156
|
+
("get_schema", _ArrowStreamGetSchema),
|
|
157
|
+
("get_next", _ArrowStreamGetNext),
|
|
158
|
+
("get_last_error", _ArrowStreamGetLastError),
|
|
159
|
+
("release", _ArrowStreamRelease),
|
|
160
|
+
("private_data", ctypes.c_void_p),
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
_P = ctypes.POINTER
|
|
164
|
+
_SIGNATURES: dict[str, tuple[object, list[object]]] = {
|
|
165
|
+
"fw_abi_version": (ctypes.c_int32, []),
|
|
166
|
+
"fw_last_error": (ctypes.c_int32, [_u8p, ctypes.c_int32, _P(ctypes.c_int32)]),
|
|
167
|
+
"fw_last_parse_error": (ctypes.c_int32, [_P(ParseErrorInfo)]),
|
|
168
|
+
"fw_layout_create": (ctypes.c_int32, [_P(ColumnSpec), ctypes.c_int32, _P(LayoutOptions), _P(ctypes.c_void_p)]),
|
|
169
|
+
"fw_layout_free": (None, [ctypes.c_void_p]),
|
|
170
|
+
"fw_read_file": (ctypes.c_int32, [_u8p, ctypes.c_int32, ctypes.c_void_p, _P(ReadOptions), _P(Table), _P(ReadResult)]),
|
|
171
|
+
"fw_read_memory": (ctypes.c_int32, [ctypes.c_void_p, ctypes.c_int64, ctypes.c_void_p, _P(ReadOptions), _P(Table), _P(ReadResult)]),
|
|
172
|
+
"fw_free_table": (None, [_P(Table)]),
|
|
173
|
+
"fw_reader_open_file": (ctypes.c_int32, [_u8p, ctypes.c_int32, ctypes.c_void_p, _P(ReaderOptions), _P(ctypes.c_void_p)]),
|
|
174
|
+
"fw_reader_open_memory": (ctypes.c_int32, [ctypes.c_void_p, ctypes.c_int64, ctypes.c_void_p, _P(ReaderOptions), _P(ctypes.c_void_p)]),
|
|
175
|
+
"fw_reader_next": (ctypes.c_int32, [ctypes.c_void_p, _P(Table)]),
|
|
176
|
+
"fw_reader_close": (None, [ctypes.c_void_p]),
|
|
177
|
+
"fw_read_arrow_file": (ctypes.c_int32, [_u8p, ctypes.c_int32, ctypes.c_void_p, _P(ReadOptions), _P(ArrowArray), _P(ArrowSchema), _P(ReadResult)]),
|
|
178
|
+
"fw_read_arrow_memory": (ctypes.c_int32, [ctypes.c_void_p, ctypes.c_int64, ctypes.c_void_p, _P(ReadOptions), _P(ArrowArray), _P(ArrowSchema), _P(ReadResult)]),
|
|
179
|
+
"fw_read_arrow_stream_file": (ctypes.c_int32, [_u8p, ctypes.c_int32, ctypes.c_void_p, _P(ReaderOptions), _P(ArrowArrayStream)]),
|
|
180
|
+
"fw_read_arrow_stream_memory": (ctypes.c_int32, [ctypes.c_void_p, ctypes.c_int64, ctypes.c_void_p, _P(ReaderOptions), _P(ArrowArrayStream)]),
|
|
181
|
+
"fw_write_file": (ctypes.c_int32, [_u8p, ctypes.c_int32, _P(Table), ctypes.c_void_p, _P(WriteOptions)]),
|
|
182
|
+
"fw_write_memory": (ctypes.c_int32, [_P(Table), ctypes.c_void_p, _P(WriteOptions), _P(Buffer)]),
|
|
183
|
+
"fw_free_buffer": (None, [_P(Buffer)]),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def library_filename() -> str:
|
|
188
|
+
system = platform.system()
|
|
189
|
+
if system == "Windows":
|
|
190
|
+
return "FixedWidthParser.Native.dll"
|
|
191
|
+
if system == "Darwin":
|
|
192
|
+
return "FixedWidthParser.Native.dylib"
|
|
193
|
+
return "FixedWidthParser.Native.so"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _candidate_paths() -> list[Path]:
|
|
197
|
+
paths = []
|
|
198
|
+
override = os.environ.get("FIXEDWIDTH_NATIVE_LIB")
|
|
199
|
+
if override:
|
|
200
|
+
paths.append(Path(override))
|
|
201
|
+
paths.append(Path(__file__).parent / "_lib" / library_filename())
|
|
202
|
+
return paths
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@lru_cache(maxsize=1)
|
|
206
|
+
def load_library() -> ctypes.CDLL:
|
|
207
|
+
"""Loads the bundled library (or FIXEDWIDTH_NATIVE_LIB) once and checks its ABI version."""
|
|
208
|
+
tried = []
|
|
209
|
+
for path in _candidate_paths():
|
|
210
|
+
if not path.exists():
|
|
211
|
+
tried.append(str(path))
|
|
212
|
+
continue
|
|
213
|
+
lib = ctypes.CDLL(str(path))
|
|
214
|
+
for name, (restype, argtypes) in _SIGNATURES.items():
|
|
215
|
+
function = getattr(lib, name)
|
|
216
|
+
function.restype = restype
|
|
217
|
+
function.argtypes = argtypes
|
|
218
|
+
version = lib.fw_abi_version()
|
|
219
|
+
if version != FW_ABI_VERSION:
|
|
220
|
+
raise OSError(f"{path} has ABI version {version}; this package needs {FW_ABI_VERSION}")
|
|
221
|
+
return lib
|
|
222
|
+
raise OSError(
|
|
223
|
+
"FixedWidthParser.Native was not found (looked in " + ", ".join(tried) + "). "
|
|
224
|
+
"Build it with `python python/scripts/build_native.py` or set FIXEDWIDTH_NATIVE_LIB."
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def last_error() -> str:
|
|
229
|
+
lib = load_library()
|
|
230
|
+
buffer = (ctypes.c_uint8 * 4096)()
|
|
231
|
+
length = ctypes.c_int32()
|
|
232
|
+
status = lib.fw_last_error(buffer, len(buffer), ctypes.byref(length))
|
|
233
|
+
if status == FW_BUFFER_TOO_SMALL:
|
|
234
|
+
buffer = (ctypes.c_uint8 * length.value)()
|
|
235
|
+
lib.fw_last_error(buffer, len(buffer), ctypes.byref(length))
|
|
236
|
+
return bytes(buffer[: length.value]).decode("utf-8", "replace")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def last_parse_error() -> tuple[int, int]:
|
|
240
|
+
info = ParseErrorInfo(struct_size=ctypes.sizeof(ParseErrorInfo))
|
|
241
|
+
load_library().fw_last_parse_error(ctypes.byref(info))
|
|
242
|
+
return int(info.line_number), int(info.column)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def utf8(text: str) -> tuple[ctypes.Array, int]:
|
|
246
|
+
"""A UTF-8 buffer and its length, for a pointer + length argument."""
|
|
247
|
+
encoded = text.encode("utf-8")
|
|
248
|
+
return (ctypes.c_uint8 * max(1, len(encoded))).from_buffer_copy(encoded.ljust(1, b"\0")), len(encoded)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""An Arrow C stream exposed through the Arrow PyCapsule interface, with no pyarrow dependency."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
|
|
7
|
+
from fixedwidth import _native
|
|
8
|
+
from fixedwidth.types import FixedWidthError
|
|
9
|
+
|
|
10
|
+
_CAPSULE_NAME = b"arrow_array_stream"
|
|
11
|
+
|
|
12
|
+
_capsule_new = ctypes.pythonapi.PyCapsule_New
|
|
13
|
+
_capsule_new.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_void_p]
|
|
14
|
+
_capsule_new.restype = ctypes.py_object
|
|
15
|
+
|
|
16
|
+
_capsule_pointer = ctypes.pythonapi.PyCapsule_GetPointer
|
|
17
|
+
_capsule_pointer.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
18
|
+
_capsule_pointer.restype = ctypes.c_void_p
|
|
19
|
+
|
|
20
|
+
_ReadFn = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
|
|
21
|
+
_ErrorFn = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)
|
|
22
|
+
_ReleaseFn = ctypes.CFUNCTYPE(None, ctypes.c_void_p)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _Target:
|
|
26
|
+
"""The native stream behind a proxy, re-typed so ctypes releases the GIL in its callbacks, and the
|
|
27
|
+
caller's bytes a memory read reads lazily."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, stream: _native.ArrowArrayStream, keep: object) -> None:
|
|
30
|
+
self.stream = stream
|
|
31
|
+
self.keep = keep
|
|
32
|
+
self.address = ctypes.addressof(stream)
|
|
33
|
+
self.get_schema = _ReadFn(_address_of(stream.get_schema))
|
|
34
|
+
self.get_next = _ReadFn(_address_of(stream.get_next))
|
|
35
|
+
self.get_last_error = _ErrorFn(_address_of(stream.get_last_error))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _address_of(function: object) -> int:
|
|
39
|
+
return ctypes.cast(function, ctypes.c_void_p).value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_streams: dict[int, _Target] = {}
|
|
43
|
+
_proxies: dict[int, _native.ArrowArrayStream] = {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _target(stream: int) -> _Target:
|
|
47
|
+
return _streams[_native.ArrowArrayStream.from_address(stream).private_data]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@_ReadFn
|
|
51
|
+
def _get_schema(stream: int, out: int) -> int:
|
|
52
|
+
target = _target(stream)
|
|
53
|
+
return target.get_schema(target.address, out)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@_ReadFn
|
|
57
|
+
def _get_next(stream: int, out: int) -> int:
|
|
58
|
+
target = _target(stream)
|
|
59
|
+
return target.get_next(target.address, out)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@_ErrorFn
|
|
63
|
+
def _get_last_error(stream: int) -> int | None:
|
|
64
|
+
target = _target(stream)
|
|
65
|
+
return target.get_last_error(target.address)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _close(proxy: _native.ArrowArrayStream) -> None:
|
|
69
|
+
_release(_streams.pop(proxy.private_data).stream)
|
|
70
|
+
proxy.release = _native._ArrowStreamRelease()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@_ReleaseFn
|
|
74
|
+
def _release_proxy(stream: int) -> None:
|
|
75
|
+
_close(_native.ArrowArrayStream.from_address(stream))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _release(stream: _native.ArrowArrayStream) -> None:
|
|
79
|
+
if stream.release:
|
|
80
|
+
stream.release(ctypes.byref(stream))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _proxy(stream: _native.ArrowArrayStream, keep: object) -> _native.ArrowArrayStream:
|
|
84
|
+
target = _Target(stream, keep)
|
|
85
|
+
_streams[id(target)] = target
|
|
86
|
+
proxy = _native.ArrowArrayStream()
|
|
87
|
+
proxy.get_schema = ctypes.cast(_get_schema, _native._ArrowStreamGetSchema)
|
|
88
|
+
proxy.get_next = ctypes.cast(_get_next, _native._ArrowStreamGetNext)
|
|
89
|
+
proxy.get_last_error = ctypes.cast(_get_last_error, _native._ArrowStreamGetLastError)
|
|
90
|
+
proxy.release = ctypes.cast(_release_proxy, _native._ArrowStreamRelease)
|
|
91
|
+
proxy.private_data = id(target)
|
|
92
|
+
return proxy
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _destroy_capsule(capsule: int) -> None:
|
|
96
|
+
proxy = _proxies.pop(_capsule_pointer(capsule, _CAPSULE_NAME))
|
|
97
|
+
if proxy.release:
|
|
98
|
+
_close(proxy)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
_destroy_capsule_callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(_destroy_capsule)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ArrowStream:
|
|
105
|
+
"""A single-use stream of Arrow record batches implementing `__arrow_c_stream__`.
|
|
106
|
+
|
|
107
|
+
Hand it to any consumer of the Arrow PyCapsule interface — `polars.DataFrame(stream)`,
|
|
108
|
+
`pyarrow.RecordBatchReader.from_stream(stream)`. Once a consumer takes it, the stream is spent;
|
|
109
|
+
dropping it unconsumed releases the native read.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, stream: _native.ArrowArrayStream, keep: object = None) -> None:
|
|
113
|
+
self._stream: _native.ArrowArrayStream | None = stream
|
|
114
|
+
self._keep = keep
|
|
115
|
+
|
|
116
|
+
def __arrow_c_stream__(self, requested_schema: object = None) -> object:
|
|
117
|
+
if self._stream is None:
|
|
118
|
+
raise FixedWidthError("this Arrow stream was already consumed")
|
|
119
|
+
stream, self._stream = self._stream, None
|
|
120
|
+
proxy = _proxy(stream, self._keep)
|
|
121
|
+
address = ctypes.addressof(proxy)
|
|
122
|
+
_proxies[address] = proxy
|
|
123
|
+
return _capsule_new(address, _CAPSULE_NAME, _destroy_capsule_callback)
|
|
124
|
+
|
|
125
|
+
def __del__(self) -> None:
|
|
126
|
+
if getattr(self, "_stream", None) is not None:
|
|
127
|
+
_release(self._stream)
|
fixedwidth/layout.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Layout: the columns of a line, validated once by the native library and shared by every call."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
|
|
8
|
+
from fixedwidth import _native
|
|
9
|
+
from fixedwidth.types import Column, ColumnType, FixedWidthError, LayoutOptions, ParseError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check(status: int) -> None:
|
|
13
|
+
"""Raises the error a native status reports."""
|
|
14
|
+
if status == _native.FW_OK:
|
|
15
|
+
return
|
|
16
|
+
message = _native.last_error() or f"native status {status}"
|
|
17
|
+
if status == _native.FW_PARSE_ERROR:
|
|
18
|
+
line_number, column = _native.last_parse_error()
|
|
19
|
+
raise ParseError(message, line_number, column)
|
|
20
|
+
raise FixedWidthError(message)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ascii_option(value: str | None, what: str) -> int:
|
|
24
|
+
if value is None:
|
|
25
|
+
return 0
|
|
26
|
+
if len(value) != 1 or ord(value) > 127:
|
|
27
|
+
raise ValueError(f"{what} must be one ASCII character, got {value!r}")
|
|
28
|
+
return ord(value)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Layout:
|
|
32
|
+
"""How the columns of a line map to names and types; pass it to every read and write.
|
|
33
|
+
|
|
34
|
+
The native layout is immutable and may be shared across threads.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, columns: Sequence[Column], options: LayoutOptions | None = None) -> None:
|
|
38
|
+
self._lib = _native.load_library()
|
|
39
|
+
self._handle: int | None = None
|
|
40
|
+
self._columns = list(columns)
|
|
41
|
+
specs = (_native.ColumnSpec * max(1, len(self._columns)))()
|
|
42
|
+
keep = [self._fill(specs[i], column) for i, column in enumerate(self._columns)]
|
|
43
|
+
raw = None
|
|
44
|
+
if options is not None:
|
|
45
|
+
raw = _native.LayoutOptions(
|
|
46
|
+
struct_size=ctypes.sizeof(_native.LayoutOptions),
|
|
47
|
+
line_length=options.line_length,
|
|
48
|
+
filler=ascii_option(options.filler, "filler"),
|
|
49
|
+
)
|
|
50
|
+
handle = ctypes.c_void_p()
|
|
51
|
+
check(self._lib.fw_layout_create(specs, len(self._columns), ctypes.byref(raw) if raw else None, ctypes.byref(handle)))
|
|
52
|
+
self._handle = handle.value
|
|
53
|
+
del keep
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _fill(spec: _native.ColumnSpec, column: Column) -> tuple[object, ...]:
|
|
57
|
+
name, name_len = _native.utf8(column.name)
|
|
58
|
+
fmt, fmt_len = _native.utf8(column.format) if column.format is not None else (None, 0)
|
|
59
|
+
spec.struct_size = ctypes.sizeof(_native.ColumnSpec)
|
|
60
|
+
spec.name = ctypes.cast(name, ctypes.POINTER(ctypes.c_uint8))
|
|
61
|
+
spec.name_len = name_len
|
|
62
|
+
spec.start = column.start
|
|
63
|
+
spec.length = column.length
|
|
64
|
+
spec.type = int(ColumnType(column.type))
|
|
65
|
+
spec.nullable = int(column.nullable)
|
|
66
|
+
spec.scale = column.scale
|
|
67
|
+
spec.format = ctypes.cast(fmt, ctypes.POINTER(ctypes.c_uint8)) if fmt is not None else None
|
|
68
|
+
spec.format_len = fmt_len
|
|
69
|
+
spec.trim_char = ascii_option(column.trim_char, "trim_char")
|
|
70
|
+
spec.trim_mode = int(column.trim_mode or 0)
|
|
71
|
+
spec.alignment = int(column.alignment or 0)
|
|
72
|
+
spec.padding = ascii_option(column.padding, "padding")
|
|
73
|
+
spec.overflow = int(column.overflow or 0)
|
|
74
|
+
return name, fmt
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def columns(self) -> list[Column]:
|
|
78
|
+
return list(self._columns)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def names(self) -> list[str]:
|
|
82
|
+
return [column.name for column in self._columns]
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def handle(self) -> int:
|
|
86
|
+
return self._handle or 0
|
|
87
|
+
|
|
88
|
+
def __del__(self) -> None:
|
|
89
|
+
if getattr(self, "_handle", None):
|
|
90
|
+
self._lib.fw_layout_free(self._handle)
|
|
91
|
+
self._handle = None
|
|
92
|
+
|
|
93
|
+
def __repr__(self) -> str:
|
|
94
|
+
return f"Layout({len(self._columns)} columns: {', '.join(self.names)})"
|
fixedwidth/py.typed
ADDED
|
File without changes
|
fixedwidth/reader.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Reading: whole sources into a Table, Arrow, pandas or polars, and batched reads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from typing import Any, Union
|
|
9
|
+
|
|
10
|
+
from fixedwidth import _native
|
|
11
|
+
from fixedwidth.arrow_stream import ArrowStream
|
|
12
|
+
from fixedwidth.layout import Layout, check
|
|
13
|
+
from fixedwidth.table import Table, _Owner
|
|
14
|
+
from fixedwidth.types import ReaderOptions, ReadOptions, code_page
|
|
15
|
+
|
|
16
|
+
Source = Union[str, "os.PathLike[str]", bytes, bytearray, memoryview]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _Input:
|
|
20
|
+
"""A file path or a buffer, as the pointer + length arguments the native calls take."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, source: Source) -> None:
|
|
23
|
+
self.keep: object = None
|
|
24
|
+
if isinstance(source, (str, os.PathLike)):
|
|
25
|
+
self.path, self.path_len = _native.utf8(os.fspath(source))
|
|
26
|
+
return
|
|
27
|
+
self.path = None
|
|
28
|
+
view = memoryview(source)
|
|
29
|
+
if view.readonly or not view.contiguous:
|
|
30
|
+
data = bytes(view)
|
|
31
|
+
self.keep = data
|
|
32
|
+
self.address = ctypes.cast(ctypes.c_char_p(data), ctypes.c_void_p).value
|
|
33
|
+
else:
|
|
34
|
+
buffer = (ctypes.c_char * view.nbytes).from_buffer(view)
|
|
35
|
+
self.keep = (view, buffer)
|
|
36
|
+
self.address = ctypes.addressof(buffer)
|
|
37
|
+
self.length = view.nbytes
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def is_file(self) -> bool:
|
|
41
|
+
return self.path is not None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _read_options(options: ReadOptions | None) -> _native.ReadOptions | None:
|
|
45
|
+
if options is None:
|
|
46
|
+
return None
|
|
47
|
+
bom = options.skip_byte_order_mark
|
|
48
|
+
return _native.ReadOptions(
|
|
49
|
+
struct_size=ctypes.sizeof(_native.ReadOptions),
|
|
50
|
+
header_lines=options.header_lines,
|
|
51
|
+
skip_invalid=int(options.skip_invalid),
|
|
52
|
+
degree_of_parallelism=options.degree_of_parallelism,
|
|
53
|
+
encoding=code_page(options.encoding),
|
|
54
|
+
max_line_length=options.max_line_length,
|
|
55
|
+
skip_byte_order_mark=_native.FW_OPT_DEFAULT if bom is None else (_native.FW_OPT_TRUE if bom else _native.FW_OPT_FALSE),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _reader_options(options: ReaderOptions | None) -> _native.ReaderOptions | None:
|
|
60
|
+
if options is None:
|
|
61
|
+
return None
|
|
62
|
+
raw = _native.ReaderOptions(struct_size=ctypes.sizeof(_native.ReaderOptions), batch_rows=options.batch_rows)
|
|
63
|
+
read = _read_options(options.read)
|
|
64
|
+
if read is not None:
|
|
65
|
+
raw.read = read
|
|
66
|
+
return raw
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _ref(value: object) -> object:
|
|
70
|
+
return ctypes.byref(value) if value is not None else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def read(source: Source, layout: Layout, options: ReadOptions | None = None) -> Table:
|
|
74
|
+
"""Reads a whole file or buffer into a Table (see `ReadOptions` for header lines, skipping and threads)."""
|
|
75
|
+
lib = _native.load_library()
|
|
76
|
+
data = _Input(source)
|
|
77
|
+
raw = _read_options(options)
|
|
78
|
+
table = _native.Table()
|
|
79
|
+
result = _native.ReadResult(struct_size=ctypes.sizeof(_native.ReadResult))
|
|
80
|
+
if data.is_file:
|
|
81
|
+
status = lib.fw_read_file(data.path, data.path_len, layout.handle, _ref(raw), ctypes.byref(table), ctypes.byref(result))
|
|
82
|
+
else:
|
|
83
|
+
status = lib.fw_read_memory(data.address, data.length, layout.handle, _ref(raw), ctypes.byref(table), ctypes.byref(result))
|
|
84
|
+
check(status)
|
|
85
|
+
return Table(_Owner(table), layout.names, int(result.lines_read), int(result.lines_skipped))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def read_arrow(source: Source, layout: Layout, options: ReadOptions | None = None) -> Any:
|
|
89
|
+
"""Reads a whole file or buffer into a `pyarrow.Table`, handed over without a copy. Requires pyarrow."""
|
|
90
|
+
import pyarrow
|
|
91
|
+
|
|
92
|
+
lib = _native.load_library()
|
|
93
|
+
data = _Input(source)
|
|
94
|
+
raw = _read_options(options)
|
|
95
|
+
array = _native.ArrowArray()
|
|
96
|
+
schema = _native.ArrowSchema()
|
|
97
|
+
if data.is_file:
|
|
98
|
+
status = lib.fw_read_arrow_file(data.path, data.path_len, layout.handle, _ref(raw), ctypes.byref(array), ctypes.byref(schema), None)
|
|
99
|
+
else:
|
|
100
|
+
status = lib.fw_read_arrow_memory(data.address, data.length, layout.handle, _ref(raw), ctypes.byref(array), ctypes.byref(schema), None)
|
|
101
|
+
check(status)
|
|
102
|
+
batch = pyarrow.RecordBatch._import_from_c(ctypes.addressof(array), ctypes.addressof(schema))
|
|
103
|
+
return pyarrow.Table.from_batches([batch])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def read_pandas(source: Source, layout: Layout, options: ReadOptions | None = None) -> Any:
|
|
107
|
+
"""`read_arrow()` as a `pandas.DataFrame`; each Arrow buffer is freed as pandas takes it. Requires pyarrow and pandas."""
|
|
108
|
+
return read_arrow(source, layout, options).to_pandas(self_destruct=True, split_blocks=True)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def read_polars(source: Source, layout: Layout, options: ReadOptions | None = None) -> Any:
|
|
112
|
+
"""`read_arrow()` as a `polars.DataFrame`, zero-copy. Without pyarrow, reads sequentially through an Arrow stream."""
|
|
113
|
+
import polars
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
return polars.from_arrow(read_arrow(source, layout, options))
|
|
117
|
+
except ImportError:
|
|
118
|
+
return polars.DataFrame(read_arrow_stream(source, layout, ReaderOptions(read=options or ReadOptions())))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def read_batches(source: Source, layout: Layout, options: ReaderOptions | None = None) -> Iterator[Table]:
|
|
122
|
+
"""Reads sequentially, yielding a Table of at most `options.batch_rows` rows at a time."""
|
|
123
|
+
lib = _native.load_library()
|
|
124
|
+
data = _Input(source)
|
|
125
|
+
raw = _reader_options(options)
|
|
126
|
+
reader = ctypes.c_void_p()
|
|
127
|
+
if data.is_file:
|
|
128
|
+
status = lib.fw_reader_open_file(data.path, data.path_len, layout.handle, _ref(raw), ctypes.byref(reader))
|
|
129
|
+
else:
|
|
130
|
+
status = lib.fw_reader_open_memory(data.address, data.length, layout.handle, _ref(raw), ctypes.byref(reader))
|
|
131
|
+
check(status)
|
|
132
|
+
try:
|
|
133
|
+
while True:
|
|
134
|
+
table = _native.Table()
|
|
135
|
+
status = lib.fw_reader_next(reader, ctypes.byref(table))
|
|
136
|
+
if status == _native.FW_EOF:
|
|
137
|
+
return
|
|
138
|
+
check(status)
|
|
139
|
+
yield Table(_Owner(table), layout.names)
|
|
140
|
+
finally:
|
|
141
|
+
lib.fw_reader_close(reader)
|
|
142
|
+
del data
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def read_arrow_stream(source: Source, layout: Layout, options: ReaderOptions | None = None) -> ArrowStream:
|
|
146
|
+
"""The batches of `read_batches()` as a single-use Arrow PyCapsule stream, for `polars.DataFrame(stream)`
|
|
147
|
+
or `pyarrow.RecordBatchReader.from_stream(stream)`. Needs no third-party package to create."""
|
|
148
|
+
lib = _native.load_library()
|
|
149
|
+
data = _Input(source)
|
|
150
|
+
raw = _reader_options(options)
|
|
151
|
+
stream = _native.ArrowArrayStream()
|
|
152
|
+
if data.is_file:
|
|
153
|
+
status = lib.fw_read_arrow_stream_file(data.path, data.path_len, layout.handle, _ref(raw), ctypes.byref(stream))
|
|
154
|
+
else:
|
|
155
|
+
status = lib.fw_read_arrow_stream_memory(data.address, data.length, layout.handle, _ref(raw), ctypes.byref(stream))
|
|
156
|
+
check(status)
|
|
157
|
+
return ArrowStream(stream, keep=data)
|
fixedwidth/table.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Table: a columnar read whose numeric columns are NumPy views over the native buffers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from fixedwidth import _native
|
|
11
|
+
from fixedwidth.types import ColumnType
|
|
12
|
+
|
|
13
|
+
_ELEMENTS = {
|
|
14
|
+
ColumnType.INT64: (ctypes.c_int64, "int64"),
|
|
15
|
+
ColumnType.FLOAT64: (ctypes.c_double, "float64"),
|
|
16
|
+
ColumnType.DECIMAL: (ctypes.c_int64, "int64"),
|
|
17
|
+
ColumnType.DATE: (ctypes.c_int32, "int32"),
|
|
18
|
+
ColumnType.TIME: (ctypes.c_int64, "int64"),
|
|
19
|
+
ColumnType.TIMESTAMP: (ctypes.c_int64, "int64"),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _Owner:
|
|
24
|
+
"""Owns one native fw_table; frees it once nothing (the Table or any view) references it."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, table: _native.Table) -> None:
|
|
27
|
+
self.table = table
|
|
28
|
+
self._on_free: Callable[[bool], object] | None = None
|
|
29
|
+
|
|
30
|
+
def view(self, address: int, element: type, count: int) -> ctypes.Array:
|
|
31
|
+
buffer = (element * count).from_address(address)
|
|
32
|
+
buffer._owner = self # type: ignore[attr-defined] # a view keeps the native memory alive
|
|
33
|
+
return buffer
|
|
34
|
+
|
|
35
|
+
def __del__(self) -> None:
|
|
36
|
+
_native.load_library().fw_free_table(ctypes.byref(self.table))
|
|
37
|
+
if self._on_free is not None:
|
|
38
|
+
self._on_free(True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Table:
|
|
42
|
+
"""One read: `table[name]` is a NumPy view for numeric and date/time columns (as stored: int64,
|
|
43
|
+
float64, DECIMAL's unscaled int64, DATE's int32 days, TIME/TIMESTAMP's int64 microseconds) and a
|
|
44
|
+
list of `str` for strings. Null rows are marked by `validity(name)`. Views stay valid after the
|
|
45
|
+
Table is gone; the native memory is freed when the last one is."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, owner: _Owner, names: list[str], lines_read: int = 0, lines_skipped: int = 0) -> None:
|
|
48
|
+
self._owner = owner
|
|
49
|
+
self._names = names
|
|
50
|
+
self.lines_read = lines_read
|
|
51
|
+
self.lines_skipped = lines_skipped
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def num_rows(self) -> int:
|
|
55
|
+
return int(self._owner.table.row_count)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def column_names(self) -> list[str]:
|
|
59
|
+
return list(self._names)
|
|
60
|
+
|
|
61
|
+
def __len__(self) -> int:
|
|
62
|
+
return self.num_rows
|
|
63
|
+
|
|
64
|
+
def _column(self, name: str) -> _native.Column:
|
|
65
|
+
try:
|
|
66
|
+
return self._owner.table.columns[self._names.index(name)]
|
|
67
|
+
except ValueError:
|
|
68
|
+
raise KeyError(name) from None
|
|
69
|
+
|
|
70
|
+
def __getitem__(self, name: str) -> Any:
|
|
71
|
+
column = self._column(name)
|
|
72
|
+
kind = ColumnType(column.type)
|
|
73
|
+
if kind is ColumnType.STRING:
|
|
74
|
+
return _strings(column)
|
|
75
|
+
import numpy
|
|
76
|
+
|
|
77
|
+
element, dtype = _ELEMENTS[kind]
|
|
78
|
+
if column.length == 0:
|
|
79
|
+
return numpy.empty(0, dtype=dtype)
|
|
80
|
+
return numpy.frombuffer(self._owner.view(column.values, element, column.length), dtype=dtype)
|
|
81
|
+
|
|
82
|
+
def validity(self, name: str) -> Any:
|
|
83
|
+
"""A NumPy bool array, True where the row holds a value, or None when the column has no nulls."""
|
|
84
|
+
column = self._column(name)
|
|
85
|
+
if not column.validity:
|
|
86
|
+
return None
|
|
87
|
+
import numpy
|
|
88
|
+
|
|
89
|
+
bits = numpy.frombuffer(ctypes.string_at(column.validity, (column.length + 7) // 8), dtype=numpy.uint8)
|
|
90
|
+
return numpy.unpackbits(bits, bitorder="little")[: column.length].astype(bool)
|
|
91
|
+
|
|
92
|
+
def to_arrow(self) -> Any:
|
|
93
|
+
"""The same columns as a `pyarrow.Table`, sharing the native buffers. Requires pyarrow."""
|
|
94
|
+
import pyarrow
|
|
95
|
+
|
|
96
|
+
arrays = [self._arrow_column(name, pyarrow) for name in self._names]
|
|
97
|
+
return pyarrow.Table.from_arrays(arrays, names=self._names)
|
|
98
|
+
|
|
99
|
+
def _arrow_column(self, name: str, pa: Any) -> Any:
|
|
100
|
+
column = self._column(name)
|
|
101
|
+
kind = ColumnType(column.type)
|
|
102
|
+
n = int(column.length)
|
|
103
|
+
validity = pa.foreign_buffer(column.validity, (n + 7) // 8, base=self._owner) if column.validity else None
|
|
104
|
+
if kind is ColumnType.DECIMAL:
|
|
105
|
+
# ponytail: decimals go through Python objects; fw.read_arrow() hands them over natively.
|
|
106
|
+
mask = self.validity(name)
|
|
107
|
+
values = self[name]
|
|
108
|
+
decimals = [None if mask is not None and not mask[i] else Decimal(int(v)).scaleb(-column.scale) for i, v in enumerate(values)]
|
|
109
|
+
return pa.array(decimals, type=pa.decimal128(18, column.scale))
|
|
110
|
+
if kind is ColumnType.STRING:
|
|
111
|
+
offsets = pa.foreign_buffer(column.values, (n + 1) * 4, base=self._owner)
|
|
112
|
+
data = pa.foreign_buffer(column.data, column.data_len, base=self._owner) if column.data else pa.py_buffer(b"")
|
|
113
|
+
return pa.Array.from_buffers(pa.string(), n, [validity, offsets, data])
|
|
114
|
+
arrow_type = {
|
|
115
|
+
ColumnType.INT64: pa.int64(),
|
|
116
|
+
ColumnType.FLOAT64: pa.float64(),
|
|
117
|
+
ColumnType.DATE: pa.date32(),
|
|
118
|
+
ColumnType.TIME: pa.time64("us"),
|
|
119
|
+
ColumnType.TIMESTAMP: pa.timestamp("us"),
|
|
120
|
+
}[kind]
|
|
121
|
+
width = 4 if kind is ColumnType.DATE else 8
|
|
122
|
+
values = pa.foreign_buffer(column.values, n * width, base=self._owner) if n else pa.py_buffer(b"")
|
|
123
|
+
return pa.Array.from_buffers(arrow_type, n, [validity, values])
|
|
124
|
+
|
|
125
|
+
def __repr__(self) -> str:
|
|
126
|
+
return f"Table({self.num_rows} rows: {', '.join(self._names)})"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _strings(column: _native.Column) -> list[str]:
|
|
130
|
+
n = int(column.length)
|
|
131
|
+
if n == 0:
|
|
132
|
+
return []
|
|
133
|
+
offsets = (ctypes.c_int32 * (n + 1)).from_address(column.values)
|
|
134
|
+
data = ctypes.string_at(column.data, column.data_len) if column.data else b""
|
|
135
|
+
return [data[offsets[i] : offsets[i + 1]].decode("utf-8") for i in range(n)]
|
fixedwidth/types.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""The public value types: column types, column descriptions, options and errors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import KW_ONLY, dataclass, field
|
|
7
|
+
from enum import IntEnum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ColumnType(IntEnum):
|
|
11
|
+
"""FW_T_*: how a column is parsed and what its values are."""
|
|
12
|
+
|
|
13
|
+
STRING = 0
|
|
14
|
+
INT64 = 1
|
|
15
|
+
FLOAT64 = 2
|
|
16
|
+
DECIMAL = 3
|
|
17
|
+
DATE = 4
|
|
18
|
+
TIME = 5
|
|
19
|
+
TIMESTAMP = 6
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TrimMode(IntEnum):
|
|
23
|
+
BOTH = 0
|
|
24
|
+
TRAILING = 1
|
|
25
|
+
LEADING = 2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Alignment(IntEnum):
|
|
29
|
+
DEFAULT = 0
|
|
30
|
+
LEFT = 1
|
|
31
|
+
RIGHT = 2
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Overflow(IntEnum):
|
|
35
|
+
THROW = 0
|
|
36
|
+
TRUNCATE = 1
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class Column:
|
|
41
|
+
"""One column of a layout: its name, zero-based byte `start`, byte `length` and `type`.
|
|
42
|
+
|
|
43
|
+
- `nullable`: a blank column (only `trim_char` and spaces) reads as null instead of failing.
|
|
44
|
+
- `scale`: implied decimals. FLOAT64 divides digits-only text by 10**scale (B3's `N(11)V99`);
|
|
45
|
+
DECIMAL keeps the unscaled integer.
|
|
46
|
+
- `format`: the exact date/time format (.NET syntax, e.g. `yyyyMMdd`), or a number format on write.
|
|
47
|
+
- `trim_char`, `trim_mode`: what is stripped before parsing; a null is written as `trim_char`.
|
|
48
|
+
- `alignment`, `padding`, `overflow`: how a value is written. `padding='0'` keeps a sign leftmost.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
name: str
|
|
52
|
+
start: int
|
|
53
|
+
length: int
|
|
54
|
+
type: ColumnType
|
|
55
|
+
_: KW_ONLY
|
|
56
|
+
nullable: bool = False
|
|
57
|
+
scale: int = 0
|
|
58
|
+
format: str | None = None
|
|
59
|
+
trim_char: str | None = None
|
|
60
|
+
trim_mode: TrimMode | None = None
|
|
61
|
+
alignment: Alignment | None = None
|
|
62
|
+
padding: str | None = None
|
|
63
|
+
overflow: Overflow | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class LayoutOptions:
|
|
68
|
+
"""`line_length`: 0 ends lines at the last column; longer pads them with `filler` on write."""
|
|
69
|
+
|
|
70
|
+
line_length: int = 0
|
|
71
|
+
filler: str | None = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class ReadOptions:
|
|
76
|
+
"""Mirrors fw_read_options. `encoding` is a code page or a codec name (`"latin-1"`, `"cp1252"`)."""
|
|
77
|
+
|
|
78
|
+
header_lines: int = 0
|
|
79
|
+
skip_invalid: bool = False
|
|
80
|
+
degree_of_parallelism: int = 0
|
|
81
|
+
encoding: int | str | None = None
|
|
82
|
+
max_line_length: int = 0
|
|
83
|
+
skip_byte_order_mark: bool | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class ReaderOptions:
|
|
88
|
+
"""Mirrors fw_reader_options: a sequential read in tables of `batch_rows` rows (0 = 65,536)."""
|
|
89
|
+
|
|
90
|
+
read: ReadOptions = field(default_factory=ReadOptions)
|
|
91
|
+
batch_rows: int = 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class WriteOptions:
|
|
96
|
+
"""Mirrors fw_write_options."""
|
|
97
|
+
|
|
98
|
+
encoding: int | str | None = None
|
|
99
|
+
newline: str = "\n"
|
|
100
|
+
degree_of_parallelism: int = 0
|
|
101
|
+
batch_rows: int = 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class FixedWidthError(Exception):
|
|
105
|
+
"""Raised when the native library reports a failure."""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ParseError(FixedWidthError):
|
|
109
|
+
"""A line failed to parse. `line_number` is 1-based; `column` is the layout index, -1 for a short line."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, message: str, line_number: int, column: int) -> None:
|
|
112
|
+
super().__init__(message)
|
|
113
|
+
self.line_number = line_number
|
|
114
|
+
self.column = column
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
_CODE_PAGES = {
|
|
118
|
+
"utf-8": 0, "utf8": 0, "ascii": 20127, "us-ascii": 20127,
|
|
119
|
+
"latin-1": 28591, "latin1": 28591, "iso-8859-1": 28591, "iso8859-1": 28591,
|
|
120
|
+
"windows-1252": 1252, "cp1252": 1252, "iso-8859-15": 28605, "latin-9": 28605,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def code_page(encoding: int | str | None) -> int:
|
|
125
|
+
"""The code page the native options take for `encoding`."""
|
|
126
|
+
if encoding is None:
|
|
127
|
+
return 0
|
|
128
|
+
if isinstance(encoding, int):
|
|
129
|
+
return encoding
|
|
130
|
+
name = encoding.strip().lower().replace("_", "-")
|
|
131
|
+
if name in _CODE_PAGES:
|
|
132
|
+
return _CODE_PAGES[name]
|
|
133
|
+
match = re.fullmatch(r"(?:cp|windows-)(\d+)", name)
|
|
134
|
+
if match:
|
|
135
|
+
return int(match.group(1))
|
|
136
|
+
raise ValueError(f"unknown encoding {encoding!r}; pass a code page number")
|
fixedwidth/writer.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Writing: a pandas, polars or pyarrow table through a layout, to a file or to bytes. Requires pyarrow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from fixedwidth import _native
|
|
10
|
+
from fixedwidth.layout import Layout, check
|
|
11
|
+
from fixedwidth.types import (
|
|
12
|
+
Column,
|
|
13
|
+
ColumnType,
|
|
14
|
+
FixedWidthError,
|
|
15
|
+
WriteOptions,
|
|
16
|
+
code_page,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def write(data: Any, path: str | os.PathLike[str], layout: Layout, options: WriteOptions | None = None) -> None:
|
|
21
|
+
"""Writes every row of `data` (pandas, polars, pyarrow, or anything with `__arrow_c_stream__`) to `path`.
|
|
22
|
+
|
|
23
|
+
Columns are matched to the layout by name. A null writes as its column filled with `trim_char`."""
|
|
24
|
+
lib = _native.load_library()
|
|
25
|
+
table, keep = _native_table(data, layout)
|
|
26
|
+
raw, newline = _write_options(options)
|
|
27
|
+
name, name_len = _native.utf8(os.fspath(path))
|
|
28
|
+
check(lib.fw_write_file(name, name_len, ctypes.byref(table), layout.handle, ctypes.byref(raw) if raw else None))
|
|
29
|
+
del keep, newline
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def write_bytes(data: Any, layout: Layout, options: WriteOptions | None = None) -> bytes:
|
|
33
|
+
"""Same as `write()`, returning the bytes."""
|
|
34
|
+
lib = _native.load_library()
|
|
35
|
+
table, keep = _native_table(data, layout)
|
|
36
|
+
raw, newline = _write_options(options)
|
|
37
|
+
buffer = _native.Buffer()
|
|
38
|
+
check(lib.fw_write_memory(ctypes.byref(table), layout.handle, ctypes.byref(raw) if raw else None, ctypes.byref(buffer)))
|
|
39
|
+
try:
|
|
40
|
+
return ctypes.string_at(buffer.data, buffer.length) if buffer.length else b""
|
|
41
|
+
finally:
|
|
42
|
+
lib.fw_free_buffer(ctypes.byref(buffer))
|
|
43
|
+
del keep, newline
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _write_options(options: WriteOptions | None) -> tuple[_native.WriteOptions | None, object]:
|
|
47
|
+
if options is None:
|
|
48
|
+
return None, None
|
|
49
|
+
newline, newline_len = _native.utf8(options.newline)
|
|
50
|
+
raw = _native.WriteOptions(
|
|
51
|
+
struct_size=ctypes.sizeof(_native.WriteOptions),
|
|
52
|
+
encoding=code_page(options.encoding),
|
|
53
|
+
newline=ctypes.cast(newline, ctypes.POINTER(ctypes.c_uint8)),
|
|
54
|
+
newline_len=newline_len,
|
|
55
|
+
degree_of_parallelism=options.degree_of_parallelism,
|
|
56
|
+
batch_rows=options.batch_rows,
|
|
57
|
+
)
|
|
58
|
+
return raw, newline
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _arrow_table(data: Any) -> Any:
|
|
62
|
+
import pyarrow
|
|
63
|
+
|
|
64
|
+
if isinstance(data, pyarrow.RecordBatch):
|
|
65
|
+
return pyarrow.Table.from_batches([data])
|
|
66
|
+
if isinstance(data, pyarrow.Table):
|
|
67
|
+
return data
|
|
68
|
+
if type(data).__module__.startswith("pandas"):
|
|
69
|
+
return pyarrow.Table.from_pandas(data, preserve_index=False)
|
|
70
|
+
if hasattr(data, "__arrow_c_stream__"):
|
|
71
|
+
return pyarrow.table(data)
|
|
72
|
+
raise TypeError(f"cannot write {type(data).__name__}: pass a pandas, polars or pyarrow table")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _native_table(data: Any, layout: Layout) -> tuple[_native.Table, list[object]]:
|
|
76
|
+
import pyarrow
|
|
77
|
+
|
|
78
|
+
arrow = _arrow_table(data).combine_chunks()
|
|
79
|
+
columns = (_native.Column * max(1, len(layout.columns)))()
|
|
80
|
+
keep: list[object] = [columns]
|
|
81
|
+
for index, spec in enumerate(layout.columns):
|
|
82
|
+
if spec.name not in arrow.column_names:
|
|
83
|
+
raise FixedWidthError(f"column '{spec.name}' of the layout is not in the data")
|
|
84
|
+
chunked = arrow.column(spec.name)
|
|
85
|
+
array = chunked.chunk(0) if chunked.num_chunks else pyarrow.array([], type=chunked.type)
|
|
86
|
+
_fill(columns[index], spec, array, keep)
|
|
87
|
+
table = _native.Table(column_count=len(layout.columns), row_count=arrow.num_rows, columns=columns)
|
|
88
|
+
return table, keep
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _fill(target: _native.Column, spec: Column, array: Any, keep: list[object]) -> None:
|
|
92
|
+
import numpy
|
|
93
|
+
import pyarrow
|
|
94
|
+
|
|
95
|
+
kind = ColumnType(spec.type)
|
|
96
|
+
target.type = int(kind)
|
|
97
|
+
target.scale = spec.scale if kind is ColumnType.DECIMAL else 0
|
|
98
|
+
target.length = len(array)
|
|
99
|
+
if array.null_count:
|
|
100
|
+
valid = numpy.packbits(~array.is_null().to_numpy(zero_copy_only=False), bitorder="little")
|
|
101
|
+
keep.append(valid)
|
|
102
|
+
target.validity = valid.ctypes.data
|
|
103
|
+
if kind is ColumnType.STRING:
|
|
104
|
+
_fill_strings(target, array.cast(pyarrow.string()).fill_null(""), keep)
|
|
105
|
+
return
|
|
106
|
+
values = numpy.ascontiguousarray(_values(kind, spec, array, pyarrow, numpy))
|
|
107
|
+
keep.append(values)
|
|
108
|
+
target.values = values.ctypes.data if len(values) else None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _values(kind: ColumnType, spec: Column, array: Any, pa: Any, np: Any) -> Any:
|
|
112
|
+
if kind is ColumnType.INT64:
|
|
113
|
+
return array.cast(pa.int64()).fill_null(0).to_numpy(zero_copy_only=False)
|
|
114
|
+
if kind is ColumnType.FLOAT64:
|
|
115
|
+
return array.cast(pa.float64()).fill_null(0).to_numpy(zero_copy_only=False)
|
|
116
|
+
if kind is ColumnType.DECIMAL:
|
|
117
|
+
return _decimals(spec, array, pa, np)
|
|
118
|
+
if kind is ColumnType.DATE:
|
|
119
|
+
return array.cast(pa.date32()).cast(pa.int32()).fill_null(0).to_numpy(zero_copy_only=False)
|
|
120
|
+
if kind is ColumnType.TIME:
|
|
121
|
+
return array.cast(pa.time64("us")).cast(pa.int64()).fill_null(0).to_numpy(zero_copy_only=False)
|
|
122
|
+
if pa.types.is_timestamp(array.type) and array.type.tz is not None:
|
|
123
|
+
array = array.cast(pa.timestamp(array.type.unit))
|
|
124
|
+
return array.cast(pa.timestamp("us")).cast(pa.int64()).fill_null(0).to_numpy(zero_copy_only=False)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _decimals(spec: Column, array: Any, pa: Any, np: Any) -> Any:
|
|
128
|
+
"""The unscaled integers a DECIMAL column stores, each of which must fit an int64.
|
|
129
|
+
|
|
130
|
+
Decimals and integers scale exactly, and a decimal with more digits than the scale fails. Floats, binary
|
|
131
|
+
approximations to begin with, round half to even to the scale; NaN and infinity fail."""
|
|
132
|
+
if pa.types.is_decimal(array.type):
|
|
133
|
+
return np.array([_unscaled(v, spec) for v in array.to_pylist()], dtype=np.int64)
|
|
134
|
+
factor = 10**spec.scale
|
|
135
|
+
if pa.types.is_integer(array.type):
|
|
136
|
+
import pyarrow.compute
|
|
137
|
+
|
|
138
|
+
bounds = pyarrow.compute.min_max(array).as_py()
|
|
139
|
+
for value in (bounds["min"], bounds["max"]):
|
|
140
|
+
if value is not None:
|
|
141
|
+
_check_int64(value * factor, value, spec)
|
|
142
|
+
return array.cast(pa.int64()).fill_null(0).to_numpy(zero_copy_only=False) * factor
|
|
143
|
+
floats = array.cast(pa.float64()).fill_null(0).to_numpy(zero_copy_only=False)
|
|
144
|
+
with np.errstate(over="ignore", invalid="ignore"):
|
|
145
|
+
scaled = np.round(floats * factor)
|
|
146
|
+
# Written so NaN fails too; 2**63 is exact in a float64, the int64 range is not.
|
|
147
|
+
outside = ~((scaled >= -(2.0**63)) & (scaled < 2.0**63))
|
|
148
|
+
if outside.any():
|
|
149
|
+
value = floats[int(np.argmax(outside))]
|
|
150
|
+
raise FixedWidthError(f"value {value} of column '{spec.name}' is not finite or does not fit an int64 at scale {spec.scale}")
|
|
151
|
+
return scaled.astype(np.int64)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _unscaled(value: Any, spec: Column) -> int:
|
|
155
|
+
"""A decimal as the integer the column stores; more decimals than its scale would be lost, so they fail."""
|
|
156
|
+
if value is None:
|
|
157
|
+
return 0
|
|
158
|
+
scaled = value.scaleb(spec.scale)
|
|
159
|
+
if scaled != scaled.to_integral_value():
|
|
160
|
+
raise FixedWidthError(f"value {value} of column '{spec.name}' has more decimals than its scale {spec.scale}")
|
|
161
|
+
return _check_int64(int(scaled), value, spec)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _check_int64(unscaled: int, value: Any, spec: Column) -> int:
|
|
165
|
+
if not -(2**63) <= unscaled < 2**63:
|
|
166
|
+
raise FixedWidthError(f"value {value} of column '{spec.name}' does not fit an int64 at scale {spec.scale}")
|
|
167
|
+
return unscaled
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _fill_strings(target: _native.Column, array: Any, keep: list[object]) -> None:
|
|
171
|
+
import numpy
|
|
172
|
+
|
|
173
|
+
buffers = array.buffers()
|
|
174
|
+
n = len(array)
|
|
175
|
+
offsets = numpy.frombuffer(buffers[1], dtype=numpy.int32)[array.offset : array.offset + n + 1] if n else numpy.zeros(1, numpy.int32)
|
|
176
|
+
base = int(offsets[0])
|
|
177
|
+
rebased = numpy.ascontiguousarray(offsets - base, dtype=numpy.int32)
|
|
178
|
+
keep.append(rebased)
|
|
179
|
+
keep.append(array)
|
|
180
|
+
target.values = rebased.ctypes.data
|
|
181
|
+
data_len = int(rebased[-1])
|
|
182
|
+
target.data_len = data_len
|
|
183
|
+
target.data = buffers[2].address + base if data_len and buffers[2] is not None else None
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fixedwidthparser
|
|
3
|
+
Version: 2.0.0-rc.1
|
|
4
|
+
Summary: Read and write fixed-width files through the FixedWidthParser NativeAOT library
|
|
5
|
+
Project-URL: Homepage, https://github.com/GabrielMarquezMatte/FixedWidthParser
|
|
6
|
+
Author: Gabriel Matte
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
13
|
+
Provides-Extra: numpy
|
|
14
|
+
Requires-Dist: numpy>=1.21; extra == 'numpy'
|
|
15
|
+
Provides-Extra: pandas
|
|
16
|
+
Requires-Dist: pandas>=2.0; extra == 'pandas'
|
|
17
|
+
Requires-Dist: pyarrow>=14.0; extra == 'pandas'
|
|
18
|
+
Provides-Extra: polars
|
|
19
|
+
Requires-Dist: polars>=1.0; extra == 'polars'
|
|
20
|
+
Provides-Extra: pyarrow
|
|
21
|
+
Requires-Dist: pyarrow>=14.0; extra == 'pyarrow'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# fixedwidthparser
|
|
25
|
+
|
|
26
|
+
Read and write fixed-width files from Python through FixedWidthParser's NativeAOT library: no .NET runtime,
|
|
27
|
+
parallel reads, results as NumPy views, Arrow, pandas or polars.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install "fixedwidthparser[numpy]" # fw.read: NumPy views
|
|
31
|
+
pip install "fixedwidthparser[pandas]" # or [pyarrow], [polars]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Wheels ship for Windows x64, macOS (arm64, x64) and Linux (glibc and musl, x64 and arm64). The package installs
|
|
35
|
+
as `fixedwidthparser` and imports as `fixedwidth`. pyarrow has no musllinux wheels, so on Alpine and other
|
|
36
|
+
musl systems Arrow, pandas, polars and writing need a pyarrow built for them.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import fixedwidth as fw
|
|
40
|
+
|
|
41
|
+
# B3's COTAHIST quote history: 245-byte lines, a header (00) and a trailer (99) around the quotes (01).
|
|
42
|
+
layout = fw.Layout([
|
|
43
|
+
fw.Column("tipreg", 0, 2, fw.String),
|
|
44
|
+
fw.Column("data_pregao", 2, 8, fw.Date, format="yyyyMMdd"),
|
|
45
|
+
fw.Column("codneg", 12, 12, fw.String),
|
|
46
|
+
fw.Column("preabe", 56, 13, fw.Float64, scale=2), # N(11)V99: two implied decimals
|
|
47
|
+
fw.Column("quatot", 152, 18, fw.Int64),
|
|
48
|
+
])
|
|
49
|
+
options = fw.ReadOptions(header_lines=1, skip_invalid=True) # the trailer fails to parse and is skipped
|
|
50
|
+
|
|
51
|
+
df = fw.read_pandas("COTAHIST_A2023.TXT", layout, options)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Reading
|
|
55
|
+
|
|
56
|
+
| Function | Returns |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `fw.read(source, layout, options)` | `fw.Table`: NumPy views over the native columns, no copy |
|
|
59
|
+
| `fw.read_arrow(source, layout, options)` | `pyarrow.Table` (needs pyarrow) |
|
|
60
|
+
| `fw.read_pandas(source, layout, options)` | `pandas.DataFrame` (needs pyarrow and pandas) |
|
|
61
|
+
| `fw.read_polars(source, layout, options)` | `polars.DataFrame` |
|
|
62
|
+
| `fw.read_batches(source, layout, fw.ReaderOptions(batch_rows=...))` | an iterator of `fw.Table` |
|
|
63
|
+
| `fw.read_arrow_stream(source, layout, fw.ReaderOptions(...))` | an Arrow PyCapsule stream for `polars.DataFrame(...)` or `pyarrow.RecordBatchReader.from_stream(...)` |
|
|
64
|
+
|
|
65
|
+
`source` is a path, or `bytes`/`bytearray`/`memoryview` to read from memory. Every optional setting lives in
|
|
66
|
+
`fw.ReadOptions`: `header_lines`, `skip_invalid`, `degree_of_parallelism` (0 = one thread per processor,
|
|
67
|
+
1 = sequential), `encoding` (a code page or `"latin-1"`, `"cp1252"`, ...), `max_line_length` and
|
|
68
|
+
`skip_byte_order_mark`. A parallel read gives the same rows, in the same order, as a sequential one.
|
|
69
|
+
|
|
70
|
+
A line that fails to parse raises `fw.ParseError` with `line_number` and `column`, unless `skip_invalid` is set.
|
|
71
|
+
|
|
72
|
+
`fw.Table` maps a column name to a NumPy array (a list for strings), and has `num_rows`, `column_names`,
|
|
73
|
+
`lines_read`, `lines_skipped`, `validity(name)` (a bool array, `True` where the row holds a value, or `None`
|
|
74
|
+
when the column has no nulls) and `to_arrow()`, which shares the native buffers. Without pyarrow,
|
|
75
|
+
`read_polars` reads sequentially through the Arrow stream.
|
|
76
|
+
|
|
77
|
+
## Columns
|
|
78
|
+
|
|
79
|
+
`fw.Column(name, start, length, type, *, nullable, scale, format, trim_char, trim_mode, alignment, padding, overflow)`.
|
|
80
|
+
Positions are bytes. Types: `fw.String`, `fw.Int64`, `fw.Float64`, `fw.Decimal` (int64 unscaled, Arrow
|
|
81
|
+
`decimal128(18, scale)`), `fw.Date`, `fw.Time`, `fw.Timestamp`. `scale` is the number of implied decimals:
|
|
82
|
+
`0000000123456` with `scale=2` reads as `1234.56`. `format` uses .NET date syntax (`yyyyMMdd`, `HHmmss`).
|
|
83
|
+
`trim_mode`, `alignment` and `overflow` take `fw.TrimMode`, `fw.Alignment` and `fw.Overflow`.
|
|
84
|
+
|
|
85
|
+
`fw.Layout(columns, fw.LayoutOptions(line_length=..., filler=...))` sets how written lines are laid out:
|
|
86
|
+
`line_length` past the last column pads each line, and `filler` (default a space) fills the gaps between
|
|
87
|
+
columns.
|
|
88
|
+
|
|
89
|
+
## Writing
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
fw.write(df, "out.txt", layout, fw.WriteOptions(degree_of_parallelism=0))
|
|
93
|
+
data = fw.write_bytes(df, layout)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Takes pandas, polars, pyarrow or anything with `__arrow_c_stream__` (needs pyarrow), matching columns by name.
|
|
97
|
+
`padding="0"` writes zero-padded numbers with the sign leftmost; a null writes as its column filled with
|
|
98
|
+
`trim_char`. Parallel writes are byte-identical to serial ones. `fw.WriteOptions` also takes `encoding`,
|
|
99
|
+
`newline` (default `"\n"`) and `batch_rows`.
|
|
100
|
+
|
|
101
|
+
A `fw.Decimal` column stores its value times `10**scale` as an int64. Decimals and integers are scaled
|
|
102
|
+
exactly, and a decimal with more digits than `scale` raises. Floats round half to even to `scale`.
|
|
103
|
+
A value that is NaN, infinite or outside int64 once scaled raises before anything is written.
|
|
104
|
+
|
|
105
|
+
## Building from source
|
|
106
|
+
|
|
107
|
+
python python/scripts/build_native.py # requires the .NET 10 SDK; publishes the library into src/fixedwidth/_lib
|
|
108
|
+
pip install -e "python[dev,pandas,polars]"
|
|
109
|
+
pytest python/tests
|
|
110
|
+
|
|
111
|
+
To point at a library built elsewhere, set `FIXEDWIDTH_NATIVE_LIB` to its full path.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
fixedwidth/__init__.py,sha256=Yo5En8tVtsIsJ3Vhjbev8VVfiar-0yV836M5XZZfVT0,1545
|
|
2
|
+
fixedwidth/_native.py,sha256=IsamIha2UfosWu8JudFpZ7dD6sJQv50F7ISj8nP0Ba0,9061
|
|
3
|
+
fixedwidth/arrow_stream.py,sha256=Ojc-ib_QiDwnR1a5fjsEQwnIySTG34GaYgd8hEilZkY,4363
|
|
4
|
+
fixedwidth/layout.py,sha256=inffqA57gvwvgmai-V04TzFbtz5ubaAYRR1frh8XPLs,3589
|
|
5
|
+
fixedwidth/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
fixedwidth/reader.py,sha256=Q72J1HSWBM5caXb8Lv0iKrqOkyzKoSKXUs03finGIqo,6650
|
|
7
|
+
fixedwidth/table.py,sha256=OSZpckLf0ASSMfAENsDftELQZS9wmcuNJFCo6jKvmS0,5577
|
|
8
|
+
fixedwidth/types.py,sha256=ngVbA_dbQ5w-LYt34xDhXKEdisBQhNkcPGvvRR8551c,3805
|
|
9
|
+
fixedwidth/writer.py,sha256=-J1eGoM8-5YB_4hHRns-vrKSbsnsdvpNBkijFAGVkBw,7920
|
|
10
|
+
fixedwidth/_lib/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
fixedwidth/_lib/FixedWidthParser.Native.so,sha256=tTtcswDbo5YmGk03v2BlHbO-9MdDoQF1Dg9rpRv2oEg,2867944
|
|
12
|
+
fixedwidthparser-2.0.0rc1.dist-info/METADATA,sha256=tSU9wrY_K82WZ6_p8rlhgGwaMD6_9bgI2iyH9laf36E,5493
|
|
13
|
+
fixedwidthparser-2.0.0rc1.dist-info/WHEEL,sha256=2kooVln1mqHN7dntfiTqe3M4vnPcJFkeYpfgXu26Plw,106
|
|
14
|
+
fixedwidthparser-2.0.0rc1.dist-info/RECORD,,
|