rapcsv 0.1.2__cp313-cp313-manylinux_2_28_x86_64.whl → 0.2.0__cp313-cp313-manylinux_2_28_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.
- rapcsv/__init__.py +392 -27
- rapcsv/_rapcsv.cpython-313-x86_64-linux-gnu.so +0 -0
- rapcsv/_rapcsv.pyi +434 -13
- rapcsv-0.2.0.dist-info/METADATA +194 -0
- rapcsv-0.2.0.dist-info/RECORD +7 -0
- rapcsv-0.1.2.dist-info/METADATA +0 -361
- rapcsv-0.1.2.dist-info/RECORD +0 -7
- {rapcsv-0.1.2.dist-info → rapcsv-0.2.0.dist-info}/WHEEL +0 -0
rapcsv/__init__.py
CHANGED
|
@@ -4,55 +4,420 @@ rapcsv provides true async CSV reading and writing for Python, backed by Rust an
|
|
|
4
4
|
Unlike libraries that wrap blocking I/O in async syntax, rapcsv guarantees that all CSV
|
|
5
5
|
operations execute **outside the Python GIL**, ensuring event loops never stall under load.
|
|
6
6
|
|
|
7
|
-
Features
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
Features
|
|
8
|
+
--------
|
|
9
|
+
- True async CSV reading and writing
|
|
10
|
+
- Streaming support for large files (incremental reading, no full file load)
|
|
11
|
+
- Context manager support (``async with``)
|
|
12
|
+
- aiocsv compatibility (AsyncReader/AsyncWriter aliases)
|
|
13
|
+
- CSV-specific exception types (CSVError, CSVFieldCountError)
|
|
14
|
+
- RFC 4180 compliant CSV parsing and writing
|
|
14
15
|
|
|
15
|
-
Example
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
Example
|
|
17
|
+
-------
|
|
18
|
+
.. code-block:: python
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
from rapcsv import Reader, Writer
|
|
22
|
+
|
|
23
|
+
async def main():
|
|
24
|
+
async with Writer("output.csv") as writer:
|
|
25
|
+
await writer.write_row(["name", "age"])
|
|
26
|
+
await writer.write_row(["Alice", "30"])
|
|
27
|
+
|
|
28
|
+
async with Reader("output.csv") as reader:
|
|
29
|
+
row = await reader.read_row()
|
|
30
|
+
print(row) # ['name', 'age']
|
|
31
|
+
|
|
32
|
+
asyncio.run(main())
|
|
29
33
|
|
|
30
34
|
For more information, see: https://github.com/eddiethedean/rapcsv
|
|
31
35
|
"""
|
|
32
36
|
|
|
33
|
-
from typing import List
|
|
37
|
+
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@runtime_checkable
|
|
41
|
+
class WithAsyncRead(Protocol):
|
|
42
|
+
"""Protocol for async file-like objects with read method.
|
|
43
|
+
|
|
44
|
+
This protocol defines the interface for async file-like objects that can be used
|
|
45
|
+
with rapcsv's Reader and AsyncDictReader classes. Objects implementing this
|
|
46
|
+
protocol must provide an async ``read()`` method.
|
|
47
|
+
|
|
48
|
+
Examples
|
|
49
|
+
--------
|
|
50
|
+
Objects from ``aiofiles`` and ``rapfiles`` implement this protocol:
|
|
51
|
+
|
|
52
|
+
.. code-block:: python
|
|
53
|
+
|
|
54
|
+
import aiofiles
|
|
55
|
+
from rapcsv import Reader
|
|
56
|
+
|
|
57
|
+
async with aiofiles.open("data.csv", mode="r") as f:
|
|
58
|
+
reader = Reader(f) # f implements WithAsyncRead
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
async def read(self, size: int) -> str:
|
|
62
|
+
"""Read up to size bytes/characters from the file.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
size: Number of bytes/characters to read.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
String containing the read data.
|
|
69
|
+
"""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@runtime_checkable
|
|
74
|
+
class WithAsyncWrite(Protocol):
|
|
75
|
+
"""Protocol for async file-like objects with write method.
|
|
76
|
+
|
|
77
|
+
This protocol defines the interface for async file-like objects that can be used
|
|
78
|
+
with rapcsv's Writer and AsyncDictWriter classes. Objects implementing this
|
|
79
|
+
protocol must provide an async ``write()`` method.
|
|
80
|
+
|
|
81
|
+
Examples
|
|
82
|
+
--------
|
|
83
|
+
Objects from ``aiofiles`` and ``rapfiles`` implement this protocol:
|
|
84
|
+
|
|
85
|
+
.. code-block:: python
|
|
86
|
+
|
|
87
|
+
import aiofiles
|
|
88
|
+
from rapcsv import Writer
|
|
89
|
+
|
|
90
|
+
async with aiofiles.open("output.csv", mode="w") as f:
|
|
91
|
+
writer = Writer(f) # f implements WithAsyncWrite
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
async def write(self, data: str) -> None:
|
|
95
|
+
"""Write data to the file.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
data: String data to write to the file.
|
|
99
|
+
"""
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# Internal helper function to call async file methods from any thread
|
|
104
|
+
# This schedules the call on the event loop using run_coroutine_threadsafe
|
|
105
|
+
def _call_file_method_threadsafe(file_handle, method_name, event_loop, *args):
|
|
106
|
+
"""Call an async file method from any thread by scheduling it on the event loop.
|
|
107
|
+
|
|
108
|
+
This is used by the Rust extension to call rapfiles methods from spawn_blocking threads.
|
|
109
|
+
The function schedules the async method call on the provided event loop using
|
|
110
|
+
``asyncio.run_coroutine_threadsafe()``.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
file_handle: The file handle object with async methods.
|
|
114
|
+
method_name: Name of the method to call (e.g., "write", "read").
|
|
115
|
+
event_loop: The event loop to schedule the call on.
|
|
116
|
+
*args: Arguments to pass to the method.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The result of the async method call.
|
|
120
|
+
|
|
121
|
+
Note:
|
|
122
|
+
This is an internal helper function and should not be called directly
|
|
123
|
+
by user code.
|
|
124
|
+
"""
|
|
125
|
+
import asyncio
|
|
126
|
+
|
|
127
|
+
# Create a coroutine that calls the method
|
|
128
|
+
async def _call_method():
|
|
129
|
+
method = getattr(file_handle, method_name)
|
|
130
|
+
result = method(*args)
|
|
131
|
+
# If it returns a Future or coroutine, await it
|
|
132
|
+
if hasattr(result, "__await__"):
|
|
133
|
+
return await result
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
# Schedule the coroutine on the event loop
|
|
137
|
+
# This will execute on the event loop thread, not the current thread
|
|
138
|
+
future = asyncio.run_coroutine_threadsafe(_call_method(), event_loop)
|
|
139
|
+
return future.result()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Internal helper function to wrap Futures into coroutines for run_coroutine_threadsafe
|
|
143
|
+
# This is used by the Rust extension to convert rapfiles Futures to coroutines
|
|
144
|
+
def _await_wrapper(fut):
|
|
145
|
+
"""Wrap a Future into a coroutine using types.coroutine.
|
|
146
|
+
|
|
147
|
+
This function is called from Rust code to convert asyncio.Future objects
|
|
148
|
+
(like those returned by rapfiles) into coroutines that can be used with
|
|
149
|
+
``asyncio.run_coroutine_threadsafe()``.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
fut: A Future or coroutine object to wrap.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
A coroutine object that can be awaited.
|
|
156
|
+
|
|
157
|
+
Note:
|
|
158
|
+
This function must work even when called from a thread without an event loop.
|
|
159
|
+
This is an internal helper function and should not be called directly
|
|
160
|
+
by user code.
|
|
161
|
+
"""
|
|
162
|
+
import inspect
|
|
163
|
+
import types
|
|
164
|
+
|
|
165
|
+
# Check if already a coroutine using inspect (doesn't need event loop)
|
|
166
|
+
if inspect.iscoroutine(fut):
|
|
167
|
+
return fut
|
|
168
|
+
|
|
169
|
+
# It's a Future - wrap it in a generator function, then use types.coroutine
|
|
170
|
+
# This approach doesn't require an event loop to be running
|
|
171
|
+
# The generator will be executed on the event loop via run_coroutine_threadsafe
|
|
172
|
+
def _gen_wrapper(fut):
|
|
173
|
+
# yield from will work when the coroutine is executed on the event loop
|
|
174
|
+
# This doesn't execute immediately - it just creates a generator
|
|
175
|
+
return (yield from fut)
|
|
176
|
+
|
|
177
|
+
# Wrap the generator function with types.coroutine to make it a coroutine function
|
|
178
|
+
coro_func = types.coroutine(_gen_wrapper)
|
|
179
|
+
|
|
180
|
+
# Call it to get a coroutine object (doesn't execute yet, no event loop needed)
|
|
181
|
+
# The coroutine will be executed later on the event loop
|
|
182
|
+
return coro_func(fut)
|
|
183
|
+
|
|
34
184
|
|
|
35
185
|
try:
|
|
36
|
-
from _rapcsv import
|
|
186
|
+
from _rapcsv import (
|
|
187
|
+
AsyncDictReader,
|
|
188
|
+
AsyncDictWriter,
|
|
189
|
+
CSVError,
|
|
190
|
+
CSVFieldCountError,
|
|
191
|
+
Reader,
|
|
192
|
+
Writer,
|
|
193
|
+
) # type: ignore[import-not-found]
|
|
37
194
|
except ImportError:
|
|
38
195
|
try:
|
|
39
|
-
from rapcsv._rapcsv import
|
|
40
|
-
|
|
196
|
+
from rapcsv._rapcsv import (
|
|
197
|
+
AsyncDictReader,
|
|
198
|
+
AsyncDictWriter,
|
|
199
|
+
CSVError,
|
|
200
|
+
CSVFieldCountError,
|
|
201
|
+
Reader,
|
|
202
|
+
Writer,
|
|
203
|
+
)
|
|
204
|
+
except ImportError as err:
|
|
41
205
|
raise ImportError(
|
|
42
206
|
"Could not import _rapcsv. Make sure rapcsv is built with maturin."
|
|
43
|
-
)
|
|
207
|
+
) from err
|
|
44
208
|
|
|
45
209
|
# API compatibility with aiocsv
|
|
46
210
|
# aiocsv uses AsyncReader and AsyncWriter as class names
|
|
47
211
|
AsyncReader = Reader
|
|
212
|
+
"""Alias for :class:`Reader` for aiocsv compatibility.
|
|
213
|
+
|
|
214
|
+
This alias allows drop-in replacement of aiocsv code.
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
.. code-block:: python
|
|
218
|
+
|
|
219
|
+
from rapcsv import AsyncReader # Instead of from aiocsv import AsyncReader
|
|
220
|
+
reader = AsyncReader("data.csv")
|
|
221
|
+
"""
|
|
222
|
+
|
|
48
223
|
AsyncWriter = Writer
|
|
224
|
+
"""Alias for :class:`Writer` for aiocsv compatibility.
|
|
225
|
+
|
|
226
|
+
This alias allows drop-in replacement of aiocsv code.
|
|
227
|
+
|
|
228
|
+
Example:
|
|
229
|
+
.. code-block:: python
|
|
230
|
+
|
|
231
|
+
from rapcsv import AsyncWriter # Instead of from aiocsv import AsyncWriter
|
|
232
|
+
writer = AsyncWriter("output.csv")
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
__version__: str = "0.2.0"
|
|
236
|
+
|
|
237
|
+
# Dialect presets for common CSV formats
|
|
238
|
+
EXCEL_DIALECT: Dict[str, Any] = {
|
|
239
|
+
"delimiter": ",",
|
|
240
|
+
"quotechar": '"',
|
|
241
|
+
"lineterminator": "\r\n",
|
|
242
|
+
"quoting": 1, # QUOTE_MINIMAL
|
|
243
|
+
"double_quote": True,
|
|
244
|
+
}
|
|
245
|
+
"""Excel-compatible CSV dialect preset.
|
|
246
|
+
|
|
247
|
+
This dialect matches Microsoft Excel's CSV format:
|
|
248
|
+
- Delimiter: comma (``,``)
|
|
249
|
+
- Quote character: double quote (``"``)
|
|
250
|
+
- Line terminator: CRLF (``\\r\\n``)
|
|
251
|
+
- Quoting: QUOTE_MINIMAL (1)
|
|
252
|
+
- Double quote: enabled
|
|
253
|
+
|
|
254
|
+
Example:
|
|
255
|
+
.. code-block:: python
|
|
256
|
+
|
|
257
|
+
from rapcsv import Writer, EXCEL_DIALECT
|
|
258
|
+
|
|
259
|
+
writer = Writer("output.csv", **EXCEL_DIALECT)
|
|
260
|
+
await writer.write_row(["col1", "col2"])
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
UNIX_DIALECT: Dict[str, Any] = {
|
|
264
|
+
"delimiter": ",",
|
|
265
|
+
"quotechar": '"',
|
|
266
|
+
"lineterminator": "\n",
|
|
267
|
+
"quoting": 1, # QUOTE_MINIMAL
|
|
268
|
+
"double_quote": True,
|
|
269
|
+
}
|
|
270
|
+
"""Unix-compatible CSV dialect preset.
|
|
271
|
+
|
|
272
|
+
This dialect uses Unix-style line endings:
|
|
273
|
+
- Delimiter: comma (``,``)
|
|
274
|
+
- Quote character: double quote (``"``)
|
|
275
|
+
- Line terminator: LF (``\\n``)
|
|
276
|
+
- Quoting: QUOTE_MINIMAL (1)
|
|
277
|
+
- Double quote: enabled
|
|
278
|
+
|
|
279
|
+
Example:
|
|
280
|
+
.. code-block:: python
|
|
281
|
+
|
|
282
|
+
from rapcsv import Writer, UNIX_DIALECT
|
|
283
|
+
|
|
284
|
+
writer = Writer("output.csv", **UNIX_DIALECT)
|
|
285
|
+
await writer.write_row(["col1", "col2"])
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
RFC4180_DIALECT: Dict[str, Any] = {
|
|
289
|
+
"delimiter": ",",
|
|
290
|
+
"quotechar": '"',
|
|
291
|
+
"lineterminator": "\r\n",
|
|
292
|
+
"quoting": 1, # QUOTE_MINIMAL
|
|
293
|
+
"double_quote": True,
|
|
294
|
+
}
|
|
295
|
+
"""RFC 4180 compliant CSV dialect preset.
|
|
296
|
+
|
|
297
|
+
This dialect strictly follows RFC 4180 specification:
|
|
298
|
+
- Delimiter: comma (``,``)
|
|
299
|
+
- Quote character: double quote (``"``)
|
|
300
|
+
- Line terminator: CRLF (``\\r\\n``)
|
|
301
|
+
- Quoting: QUOTE_MINIMAL (1)
|
|
302
|
+
- Double quote: enabled
|
|
303
|
+
|
|
304
|
+
Example:
|
|
305
|
+
.. code-block:: python
|
|
306
|
+
|
|
307
|
+
from rapcsv import Writer, RFC4180_DIALECT
|
|
308
|
+
|
|
309
|
+
writer = Writer("output.csv", **RFC4180_DIALECT)
|
|
310
|
+
await writer.write_row(["col1", "col2"])
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def convert_types(row: List[str], converters: Optional[Dict[int, Any]] = None) -> List[Any]:
|
|
315
|
+
"""Convert row fields to appropriate types.
|
|
316
|
+
|
|
317
|
+
Automatically infers types (int, float, bool) or applies custom converter functions
|
|
318
|
+
per column. If a converter fails, the original string value is preserved.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
row: List of string values from CSV row.
|
|
322
|
+
converters: Optional dictionary mapping column index (int) to converter
|
|
323
|
+
function (callable). If provided, only specified columns are converted
|
|
324
|
+
using the provided functions. Other columns use automatic inference.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
List with converted values. Types may be int, float, bool, or str.
|
|
328
|
+
|
|
329
|
+
Examples
|
|
330
|
+
--------
|
|
331
|
+
.. code-block:: python
|
|
332
|
+
|
|
333
|
+
from rapcsv import Reader, convert_types
|
|
334
|
+
|
|
335
|
+
reader = Reader("data.csv")
|
|
336
|
+
row = await reader.read_row() # ['Alice', '30', 'NYC']
|
|
337
|
+
|
|
338
|
+
# Automatic type conversion
|
|
339
|
+
converted = convert_types(row)
|
|
340
|
+
# ['Alice', 30, 'NYC'] # age converted to int
|
|
341
|
+
|
|
342
|
+
# Per-column converters
|
|
343
|
+
converters = {1: int, 2: str.upper}
|
|
344
|
+
converted = convert_types(row, converters)
|
|
345
|
+
# ['Alice', 30, 'NYC'] # column 1 to int, column 2 to uppercase
|
|
346
|
+
"""
|
|
347
|
+
if converters:
|
|
348
|
+
result = []
|
|
349
|
+
for i, value in enumerate(row):
|
|
350
|
+
if i in converters:
|
|
351
|
+
try:
|
|
352
|
+
result.append(converters[i](value))
|
|
353
|
+
except (ValueError, TypeError):
|
|
354
|
+
result.append(value) # Keep original if conversion fails
|
|
355
|
+
else:
|
|
356
|
+
result.append(_auto_convert(value))
|
|
357
|
+
return result
|
|
358
|
+
else:
|
|
359
|
+
return [_auto_convert(v) for v in row]
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _auto_convert(value: str) -> Any:
|
|
363
|
+
"""Automatically convert a string value to appropriate type.
|
|
364
|
+
|
|
365
|
+
Attempts type conversion in the following order:
|
|
366
|
+
1. Integer (if no decimal point or scientific notation)
|
|
367
|
+
2. Float
|
|
368
|
+
3. Boolean (true/false, yes/no, 1/0, on/off)
|
|
369
|
+
4. Original string (if no conversion succeeds)
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
value: String value to convert.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
Converted value (int, float, bool, or str).
|
|
376
|
+
|
|
377
|
+
Note:
|
|
378
|
+
This is an internal helper function used by ``convert_types()``.
|
|
379
|
+
Empty strings are returned as-is.
|
|
380
|
+
"""
|
|
381
|
+
if not value or value.strip() == "":
|
|
382
|
+
return value
|
|
383
|
+
|
|
384
|
+
# Try int
|
|
385
|
+
try:
|
|
386
|
+
if "." not in value and "e" not in value.lower():
|
|
387
|
+
return int(value)
|
|
388
|
+
except ValueError:
|
|
389
|
+
pass
|
|
390
|
+
|
|
391
|
+
# Try float
|
|
392
|
+
try:
|
|
393
|
+
return float(value)
|
|
394
|
+
except ValueError:
|
|
395
|
+
pass
|
|
396
|
+
|
|
397
|
+
# Try bool
|
|
398
|
+
lower = value.lower().strip()
|
|
399
|
+
if lower in ("true", "yes", "1", "on"):
|
|
400
|
+
return True
|
|
401
|
+
if lower in ("false", "no", "0", "off", ""):
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
# Return as string
|
|
405
|
+
return value
|
|
406
|
+
|
|
49
407
|
|
|
50
|
-
__version__: str = "0.1.2"
|
|
51
408
|
__all__: List[str] = [
|
|
52
409
|
"Reader",
|
|
53
410
|
"Writer",
|
|
411
|
+
"AsyncDictReader",
|
|
412
|
+
"AsyncDictWriter",
|
|
54
413
|
"AsyncReader", # aiocsv compatibility
|
|
55
414
|
"AsyncWriter", # aiocsv compatibility
|
|
56
415
|
"CSVError",
|
|
57
416
|
"CSVFieldCountError",
|
|
417
|
+
"WithAsyncRead", # Protocol for type checking
|
|
418
|
+
"WithAsyncWrite", # Protocol for type checking
|
|
419
|
+
"EXCEL_DIALECT", # Dialect preset
|
|
420
|
+
"UNIX_DIALECT", # Dialect preset
|
|
421
|
+
"RFC4180_DIALECT", # Dialect preset
|
|
422
|
+
"convert_types", # Type conversion utility
|
|
58
423
|
]
|
|
Binary file
|