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 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
- - True async CSV reading and writing
9
- - Streaming support for large files (incremental reading, no full file load)
10
- - Context manager support (`async with`)
11
- - aiocsv compatibility (AsyncReader/AsyncWriter aliases)
12
- - CSV-specific exception types (CSVError, CSVFieldCountError)
13
- - RFC 4180 compliant CSV parsing and writing
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
- >>> import asyncio
17
- >>> from rapcsv import Reader, Writer
18
- >>>
19
- >>> async def main():
20
- ... async with Writer("output.csv") as writer:
21
- ... await writer.write_row(["name", "age"])
22
- ... await writer.write_row(["Alice", "30"])
23
- ...
24
- ... async with Reader("output.csv") as reader:
25
- ... row = await reader.read_row()
26
- ... print(row) # ['name', 'age']
27
- >>>
28
- >>> asyncio.run(main())
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 Reader, Writer, CSVError, CSVFieldCountError # type: ignore[import-not-found]
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 Reader, Writer, CSVError, CSVFieldCountError
40
- except ImportError:
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
  ]