Skip to content

Writing files — write and writeStream

DataGenerator.write writes a batch once; DataGenerator.writeStream writes a microbatch per trigger until a timeout. Both take .format(), .mode(), .option() / .options().

import os
from rand_engine import DataGenerator

spec = {
    "id": {"method": "pk", "kwargs": {"start": 1}},
    "name": {"method": "distincts", "kwargs": {"distincts": ["ana", "bia"]}},
    "active": {"method": "booleans", "kwargs": {}},
}
gen = DataGenerator(spec, seed=1).size(1_000)

gen.write.format("parquet").save("out/clients.parquet")
gen.write.format("csv").option("compression", "gzip").save("out/clients.csv")
gen.write.format("json").options(numFiles=4).mode("overwrite").save("out/clients_json")

assert os.path.exists("out/clients.parquet") and os.path.exists("out/clients.csv.gz")
assert len(os.listdir("out/clients_json")) == 4

Writer contract

  • csv and parquet are written by pyarrow; json by pandas (orient="records", one record per line).
  • Only the options below are accepted. Any other raises RandEngineError naming it and listing the accepted ones, before any file is deleted or written.
Format Options
csv sep (delimiter, default ,); compression: gzip, bz2, xz, zip; index: only False (Arrow writes no index)
parquet compression: snappy (default), gzip, zstd, brotli, lz4
json force_ascii, indent, compression (gzip, bz2, xz, zip); orient: only "records"
sqlite table (required, non-empty table name); overwrite or append; no file batching or file-format options
write (batch) numFiles (default 1); maxRowsPerBatch (positive int; rows generated and written per batch, so memory stays bounded)
writeStream timeout in seconds (default 20)
  • A column holding mixed types (e.g. ints and strings in one object column) raises RandEngineError naming the column for csv and parquet.
  • Parquet reads back equal to the frame in values, tz-aware columns included, and in dtype for every non-text column. A generated text column reads back as pd.StringDtype("pyarrow") under the default read and as the frame's pd.ArrowDtype(pa.string()) with pd.read_parquet(..., dtype_backend="pyarrow").
from rand_engine.validators.exceptions import RandEngineError

try:
    gen.write.format("parquet").option("engine", "fastparquet").save("out/bad.parquet")
except RandEngineError as error:
    assert "engine" in str(error) and "numFiles" in str(error)
else:
    raise AssertionError("expected RandEngineError")

Spark: a text column with nulls holds pd.NA. To convert such a frame with spark.createDataFrame, enable spark.sql.execution.arrow.pyspark.enabled first. Without it, spark.createDataFrame raises PySparkTypeError ([CANNOT_MERGE_TYPE]).

Batch: write

  • save(path) with numFiles 1 writes one file at path, the format and compression extension added when missing (out/clients.csv + gzip → out/clients.csv.gz; parquet never gets one).
  • With numFiles > 1, path (extensions stripped) is a folder of part_<id>.<ext> files sharing the size rows. The batch writer stages every new file beside the destination and commits only after staging succeeds. Only mode("append") adds staged parts to an existing folder; every other mode replaces the destination folder when the commit succeeds.
  • A final file has one schema: with maxRowsPerBatch every batch of a file must produce the same schema, and mode("append") to a single file must match the existing one.
  • Keys continue across the files: pk values never repeat between parts.
import pandas as pd

gen.write.format("csv").options(numFiles=3, sep=";").save("out/parts.csv")
parts = [pd.read_csv(f"out/parts/{f}", sep=";") for f in sorted(os.listdir("out/parts"))]
ids = pd.concat(parts)["id"]
assert len(ids) == 1_000 and ids.is_unique

spec_log = {"level": {"method": "distincts", "kwargs": {"distincts": ["info", "warn"]}}}
log = DataGenerator(spec_log, seed=2).size(1_000)
log.write.format("csv").options(numFiles=2, maxRowsPerBatch=100).save("out/log.csv")
log.write.format("csv").options(numFiles=2).mode("append").save("out/log.csv")
logs = os.listdir("out/log")
assert len(logs) == 4
assert sum(len(pd.read_csv(f"out/log/{f}")) for f in logs) == 2_000

SQLite table sink

SQLite is a batch-only output sink: use write.format("sqlite").option("table", name) and one of overwrite or append. Identifiers are quoted and row values are bound. An overwrite stages the selected table replacement while preserving unrelated tables; append is transactional. SQLite stores bools and signed 64-bit integers as INTEGER, floats as REAL, strings and ISO dates as TEXT, bytes as BLOB, and nulls as NULL. Mixed and all-null columns have no declared affinity. It does not create a primary-key constraint from a RandSpec key, resume files, or accept numFiles, maxRowsPerBatch, or other file options.

import sqlite3
from rand_engine.validators.exceptions import RandEngineError

sqlite_path = "out/clients.sqlite"
sqlite_table = 'client "events"'
gen.write.format("sqlite").option("table", sqlite_table).mode("overwrite").save(sqlite_path)
gen.write.format("sqlite").option("table", sqlite_table).mode("append").save(sqlite_path)
with sqlite3.connect(sqlite_path) as database:
    quoted = sqlite_table.replace('"', '""')
    assert database.execute(f'SELECT COUNT(*) FROM "{quoted}"').fetchone()[0] == 2_000

try:
    gen.writeStream.format("sqlite").option("table", "events").start("out/events.sqlite")
except RandEngineError:
    pass
else:
    raise AssertionError("SQLite is batch-only")

Stream: writeStream

trigger(frequency) only configures the pause after each write and returns the stream writer. start(path) writes one microbatch of size rows to path/part-<uuid>.<ext>, sleeps the configured number of seconds and repeats until timeout seconds have passed. mode("overwrite") (the default) clears the folder first. start blocks until the timeout.

gen.writeStream.format("json").trigger(frequency=0.1).option("timeout", 0.3).start("out/stream.json")
files = os.listdir("out/stream")
assert len(files) >= 2

CSV written by pyarrow

CSV output differs from pandas.DataFrame.to_csv:

  • booleans are written true / false;
  • integral floats are written 1, so they read back as int;
  • the header and every string value are quoted;
  • a naive timestamp's fraction follows its column's unit: ns .000000000, us six digits, ms three, s none;
  • an all-midnight naive datetime column is written with its full time (2020-01-01 00:00:00.000000000), not a bare date;
  • a tz-aware column is written in its pandas string form, quoted (2024-01-01 00:00:00+00:00, …-03:00);
  • a missing timestamp (NaT) in a multi-column frame is an empty field, never NaT;
  • in a one-column frame a null is written as a blank line, which pd.read_csv skips by default, so that row is lost on read-back;
  • line endings are always LF, on every OS.
gen.write.format("csv").save("out/check.csv")
with open("out/check.csv", newline="") as file:
    header, first = file.read().split("\n")[:2]
assert header == '"id","name","active"'
assert first.split(",")[2] in ("true", "false")

Returned writer reference

DataGenerator.write returns a FileBatchWriter; DataGenerator.writeStream returns a FileStreamWriter. These are returned objects, not top-level rand_engine exports. Both inherit the format, mode and option chain from FileWriter; save completes a batch write, trigger configures a stream writer and returns it, and start performs blocking stream writes until the timeout.

Configure the file format, write mode and options of write and writeStream.

SQLite is an output-only batch format with a required table option; stream writers refuse it.

Examples:

>>> import os, tempfile
>>> import pandas as pd
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> path = os.path.join(tempfile.mkdtemp(), "class.csv")
>>> DataGenerator(spec, seed=42).size(10).write.format("csv").save(path)
>>> len(pd.read_csv(path))
10
Source code in rand_engine/file_handlers/writer.py
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
class FileWriter:
  """
  Configure the file format, write mode and options of ``write`` and ``writeStream``.

  SQLite is an output-only batch format with a required ``table`` option; stream writers refuse it.

  Examples
  --------
  >>> import os, tempfile
  >>> import pandas as pd
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> path = os.path.join(tempfile.mkdtemp(), "class.csv")
  >>> DataGenerator(spec, seed=42).size(10).write.format("csv").save(path)
  >>> len(pd.read_csv(path))
  10
  """

  def __init__(self, size_def, microbatch_def, schema_def=None):
    self.size_def = size_def
    self.microbatch_def = microbatch_def
    self.schema_def = schema_def
    self.write_format = "csv"
    self.write_mode = "overwrite"
    self.write_options = {}
    self.writer_method = self._FileWriter__map_methods()


  def __map_methods(self):
    """Map file formats to their corresponding FileHandler methods."""
    return {
      "csv": FileHandler.to_csv,
      "parquet": FileHandler.to_parquet,
      "json": FileHandler.to_json
    }


  def mode(self, write_mode: str) -> Callable:
    """
    Set the write mode of the next ``save`` or ``start``.

    Parameters
    ----------
    write_mode : str
        ``"overwrite"`` (default) replaces the output (file or folder); ``"append"`` adds to it.

    Returns
    -------
    FileWriter
        This writer, for chaining.

    Examples
    --------
    >>> import os, tempfile
    >>> import pandas as pd
    >>> from rand_engine import DataGenerator
    >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
    >>> gen = DataGenerator(spec, seed=42).size(10)
    >>> path = os.path.join(tempfile.mkdtemp(), "mode.csv")
    >>> gen.write.format("csv").save(path)
    >>> gen.write.mode("append").format("csv").save(path)
    >>> len(pd.read_csv(path))
    20
    """
    self.write_mode = write_mode
    return self


  def format(self, format: str) :
    """
    Set the output file format of the next ``save`` or ``start``.

    Parameters
    ----------
    format : str
        ``"csv"``, ``"parquet"``, ``"json"`` or batch-only ``"sqlite"``.

    Returns
    -------
    FileWriter
        This writer, for chaining.

    Examples
    --------
    >>> import os, tempfile
    >>> import pandas as pd
    >>> from rand_engine import DataGenerator
    >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
    >>> path = os.path.join(tempfile.mkdtemp(), "format.parquet")
    >>> DataGenerator(spec, seed=42).size(10).write.format("parquet").save(path)
    >>> len(pd.read_parquet(path))
    10
    """
    self.write_format = format
    return self


  def option(self, key: str, value: Any):
    """
    Set one write option of the next ``save`` or ``start``.

    Parameters
    ----------
    key : str
        Option name: ``"compression"`` (file codec), ``"numFiles"`` (parts per ``save``),
        ``"maxRowsPerBatch"`` (rows per write batch) or ``"timeout"`` (seconds a
        ``start`` runs, default 20). SQLite accepts only ``"table"``.
    value : Any
        Option value.

    Returns
    -------
    FileWriter
        This writer, for chaining.

    Examples
    --------
    >>> import os, tempfile
    >>> from rand_engine import DataGenerator
    >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
    >>> path = os.path.join(tempfile.mkdtemp(), "parts")
    >>> DataGenerator(spec, seed=42).size(10).write.option("numFiles", 2).format("csv").save(path)
    >>> len(os.listdir(path))
    2
    """
    self.write_options[key] = value
    return self


  def options(self, **kwargs):
    """
    Set several write options of the next ``save`` or ``start`` at once.

    Parameters
    ----------
    **kwargs : Any
        Option names and values, such as ``numFiles=2``; the keys are those of ``option``.

    Returns
    -------
    FileWriter
        This writer, for chaining.

    Examples
    --------
    >>> import os, tempfile
    >>> import pandas as pd
    >>> from rand_engine import DataGenerator
    >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
    >>> path = os.path.join(tempfile.mkdtemp(), "options")
    >>> gen = DataGenerator(spec, seed=42).size(10).write.options(numFiles=2, maxRowsPerBatch=3)
    >>> gen.format("csv").save(path)
    >>> sum(len(pd.read_csv(os.path.join(path, name))) for name in os.listdir(path))
    10
    """
    for key, value in kwargs.items():
      self.write_options[key] = value
    return self

__map_methods()

Map file formats to their corresponding FileHandler methods.

Source code in rand_engine/file_handlers/writer.py
33
34
35
36
37
38
39
def __map_methods(self):
  """Map file formats to their corresponding FileHandler methods."""
  return {
    "csv": FileHandler.to_csv,
    "parquet": FileHandler.to_parquet,
    "json": FileHandler.to_json
  }

format(format)

Set the output file format of the next save or start.

Parameters:

Name Type Description Default
format str

"csv", "parquet", "json" or batch-only "sqlite".

required

Returns:

Type Description
FileWriter

This writer, for chaining.

Examples:

>>> import os, tempfile
>>> import pandas as pd
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> path = os.path.join(tempfile.mkdtemp(), "format.parquet")
>>> DataGenerator(spec, seed=42).size(10).write.format("parquet").save(path)
>>> len(pd.read_parquet(path))
10
Source code in rand_engine/file_handlers/writer.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def format(self, format: str) :
  """
  Set the output file format of the next ``save`` or ``start``.

  Parameters
  ----------
  format : str
      ``"csv"``, ``"parquet"``, ``"json"`` or batch-only ``"sqlite"``.

  Returns
  -------
  FileWriter
      This writer, for chaining.

  Examples
  --------
  >>> import os, tempfile
  >>> import pandas as pd
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> path = os.path.join(tempfile.mkdtemp(), "format.parquet")
  >>> DataGenerator(spec, seed=42).size(10).write.format("parquet").save(path)
  >>> len(pd.read_parquet(path))
  10
  """
  self.write_format = format
  return self

mode(write_mode)

Set the write mode of the next save or start.

Parameters:

Name Type Description Default
write_mode str

"overwrite" (default) replaces the output (file or folder); "append" adds to it.

required

Returns:

Type Description
FileWriter

This writer, for chaining.

Examples:

>>> import os, tempfile
>>> import pandas as pd
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> gen = DataGenerator(spec, seed=42).size(10)
>>> path = os.path.join(tempfile.mkdtemp(), "mode.csv")
>>> gen.write.format("csv").save(path)
>>> gen.write.mode("append").format("csv").save(path)
>>> len(pd.read_csv(path))
20
Source code in rand_engine/file_handlers/writer.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def mode(self, write_mode: str) -> Callable:
  """
  Set the write mode of the next ``save`` or ``start``.

  Parameters
  ----------
  write_mode : str
      ``"overwrite"`` (default) replaces the output (file or folder); ``"append"`` adds to it.

  Returns
  -------
  FileWriter
      This writer, for chaining.

  Examples
  --------
  >>> import os, tempfile
  >>> import pandas as pd
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> gen = DataGenerator(spec, seed=42).size(10)
  >>> path = os.path.join(tempfile.mkdtemp(), "mode.csv")
  >>> gen.write.format("csv").save(path)
  >>> gen.write.mode("append").format("csv").save(path)
  >>> len(pd.read_csv(path))
  20
  """
  self.write_mode = write_mode
  return self

option(key, value)

Set one write option of the next save or start.

Parameters:

Name Type Description Default
key str

Option name: "compression" (file codec), "numFiles" (parts per save), "maxRowsPerBatch" (rows per write batch) or "timeout" (seconds a start runs, default 20). SQLite accepts only "table".

required
value Any

Option value.

required

Returns:

Type Description
FileWriter

This writer, for chaining.

Examples:

>>> import os, tempfile
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> path = os.path.join(tempfile.mkdtemp(), "parts")
>>> DataGenerator(spec, seed=42).size(10).write.option("numFiles", 2).format("csv").save(path)
>>> len(os.listdir(path))
2
Source code in rand_engine/file_handlers/writer.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def option(self, key: str, value: Any):
  """
  Set one write option of the next ``save`` or ``start``.

  Parameters
  ----------
  key : str
      Option name: ``"compression"`` (file codec), ``"numFiles"`` (parts per ``save``),
      ``"maxRowsPerBatch"`` (rows per write batch) or ``"timeout"`` (seconds a
      ``start`` runs, default 20). SQLite accepts only ``"table"``.
  value : Any
      Option value.

  Returns
  -------
  FileWriter
      This writer, for chaining.

  Examples
  --------
  >>> import os, tempfile
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> path = os.path.join(tempfile.mkdtemp(), "parts")
  >>> DataGenerator(spec, seed=42).size(10).write.option("numFiles", 2).format("csv").save(path)
  >>> len(os.listdir(path))
  2
  """
  self.write_options[key] = value
  return self

options(**kwargs)

Set several write options of the next save or start at once.

Parameters:

Name Type Description Default
**kwargs Any

Option names and values, such as numFiles=2; the keys are those of option.

{}

Returns:

Type Description
FileWriter

This writer, for chaining.

Examples:

>>> import os, tempfile
>>> import pandas as pd
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> path = os.path.join(tempfile.mkdtemp(), "options")
>>> gen = DataGenerator(spec, seed=42).size(10).write.options(numFiles=2, maxRowsPerBatch=3)
>>> gen.format("csv").save(path)
>>> sum(len(pd.read_csv(os.path.join(path, name))) for name in os.listdir(path))
10
Source code in rand_engine/file_handlers/writer.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def options(self, **kwargs):
  """
  Set several write options of the next ``save`` or ``start`` at once.

  Parameters
  ----------
  **kwargs : Any
      Option names and values, such as ``numFiles=2``; the keys are those of ``option``.

  Returns
  -------
  FileWriter
      This writer, for chaining.

  Examples
  --------
  >>> import os, tempfile
  >>> import pandas as pd
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> path = os.path.join(tempfile.mkdtemp(), "options")
  >>> gen = DataGenerator(spec, seed=42).size(10).write.options(numFiles=2, maxRowsPerBatch=3)
  >>> gen.format("csv").save(path)
  >>> sum(len(pd.read_csv(os.path.join(path, name))) for name in os.listdir(path))
  10
  """
  for key, value in kwargs.items():
    self.write_options[key] = value
  return self

Bases: FileWriter

Write the rows of a DataGenerator to files or one SQLite table, once per save.

Built by DataGenerator.write; chain format, mode and option first. SQLite accepts only a table name, quotes identifiers, binds values, and supports overwrite or append.

Examples:

>>> import os, tempfile
>>> import pandas as pd
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> path = os.path.join(tempfile.mkdtemp(), "batch_ages.csv")
>>> DataGenerator(spec, seed=42).size(10).write.format("csv").save(path)
>>> len(pd.read_csv(path))
10
Source code in rand_engine/file_handlers/_writer_batch.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
class FileBatchWriter(FileWriter):
  """
  Write the rows of a DataGenerator to files or one SQLite table, once per ``save``.

  Built by ``DataGenerator.write``; chain ``format``, ``mode`` and ``option`` first. SQLite
  accepts only a table name, quotes identifiers, binds values, and supports overwrite or append.

  Examples
  --------
  >>> import os, tempfile
  >>> import pandas as pd
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> path = os.path.join(tempfile.mkdtemp(), "batch_ages.csv")
  >>> DataGenerator(spec, seed=42).size(10).write.format("csv").save(path)
  >>> len(pd.read_csv(path))
  10
  """

  def __init__(self, size_def, microbatch_def, file_def=None):
    super().__init__(size_def, microbatch_def)
    # file_def() -> (catalog schema def, batch factory), evaluated once per final file.
    self.file_def = file_def or (
      lambda: (lambda exact=False: None, lambda size, offset: microbatch_def(size, offset)())
    )


  @staticmethod
  def _positive_integer(name: str, value: object) -> int:
    if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
      raise RandEngineError(f"writer option '{name}' must be a positive integer")
    return value


  @staticmethod
  def _resolved_size(value: object) -> int:
    if isinstance(value, bool) or not isinstance(value, int) or value < 0:
      raise RandEngineError("resolved size must be a non-negative integer")
    return value


  @staticmethod
  def _file_sizes(total: int, count: int) -> list[int]:
    rows, extra = divmod(total, count)
    return [rows + (index < extra) for index in range(count)]


  @staticmethod
  def _batches(size: int, limit: int | None) -> Iterator[int]:
    if size == 0:
      return
    if limit is None:
      yield size
      return
    remaining = size
    while remaining:
      batch_size = min(limit, remaining)
      yield batch_size
      remaining -= batch_size


  @staticmethod
  def _part_path(staging: Path, extension: str) -> Path:
    while True:
      path = staging / f"part_{str(uuid.uuid4())[:18]}.{extension}"
      if not path.exists():
        return path


  @staticmethod
  def _add_parts(staging: Path, destination: Path) -> None:
    if not destination.is_dir():
      raise RandEngineError(f"append destination '{destination}' is not a directory")
    moved = []
    try:
      for part in staging.iterdir():
        os.replace(part, destination / part.name)
        moved.append(destination / part.name)
    except Exception:
      for path in moved:
        path.unlink(missing_ok=True)
      raise


  @staticmethod
  def _commit_directory(staging: Path, destination: Path) -> None:
    backup = destination.with_name(f".{destination.name}.{uuid.uuid4().hex}.previous")
    moved_old = False
    try:
      if destination.exists():
        os.rename(destination, backup)
        moved_old = True
      os.rename(staging, destination)
    except Exception:
      if moved_old and backup.exists() and not destination.exists():
        os.rename(backup, destination)
      raise
    if moved_old:
      shutil.rmtree(backup)


  @staticmethod
  def _sqlite_identifier(value: str) -> str:
    return f'"{value.replace(chr(34), chr(34) * 2)}"'


  @staticmethod
  def _sqlite_value(value: object) -> tuple[object, str]:
    if value is None or value is pd.NA or value is pd.NaT:
      return None, "TEXT"
    if isinstance(value, (float, np.floating)) and np.isnan(value):
      return None, "REAL"
    if isinstance(value, (bool, np.bool_)):
      return int(value), "INTEGER"
    if isinstance(value, (int, np.integer)):
      converted = int(value)
      if not -(2 ** 63) <= converted < 2 ** 63:
        raise RandEngineError("sqlite integers must fit in signed 64 bits")
      return converted, "INTEGER"
    if isinstance(value, (float, np.floating)):
      return float(value), "REAL"
    if isinstance(value, str):
      return value, "TEXT"
    if isinstance(value, bytes):
      return value, "BLOB"
    if isinstance(value, dt.datetime):
      return value.isoformat(), "TEXT"
    if isinstance(value, dt.date):
      return value.isoformat(), "TEXT"
    raise RandEngineError(f"sqlite cannot write value of type '{type(value).__name__}'")


  @classmethod
  def _sqlite_frame(cls, frame) -> tuple[list[str], list[str | None], list[tuple[object, ...]]]:
    columns = list(frame.columns)
    if (
      any(not isinstance(column, str) or "\x00" in column for column in columns)
      or len(set(columns)) != len(columns)
    ):
      raise RandEngineError("sqlite column labels must be unique, NUL-free strings")
    types = [set() for _ in columns]
    rows = []
    for row in frame.itertuples(index=False, name=None):
      converted = []
      for index, value in enumerate(row):
        sqlite_value, sqlite_type = cls._sqlite_value(value)
        if sqlite_value is not None:
          types[index].add(sqlite_type)
        converted.append(sqlite_value)
      rows.append(tuple(converted))
    # A declared SQLite affinity coerces values on insert.  Leave heterogeneous
    # and all-null columns untyped so their bound values keep their storage class.
    return columns, [None if not values else next(iter(values)) if len(values) == 1 else "" for values in types], rows


  @classmethod
  def _sqlite_create(cls, database, table: str, columns: list[str], types: list[str | None]) -> None:
    definitions = ", ".join(
      f"{cls._sqlite_identifier(column)}{f' {sqlite_type}' if sqlite_type else ''}"
      for column, sqlite_type in zip(columns, types)
    )
    database.execute(f"CREATE TABLE {cls._sqlite_identifier(table)} ({definitions})")


  @classmethod
  def _sqlite_existing_schema(cls, database, table: str) -> list[tuple[str, str]]:
    return [
      (row[1], row[2].upper())
      for row in database.execute(f"PRAGMA table_info({cls._sqlite_identifier(table)})")
    ]


  @classmethod
  def _sqlite_insert(cls, database, table: str, columns: list[str], rows: list[tuple[object, ...]]) -> None:
    quoted_columns = ", ".join(cls._sqlite_identifier(column) for column in columns)
    placeholders = ", ".join("?" for _ in columns)
    database.executemany(
      f"INSERT INTO {cls._sqlite_identifier(table)} ({quoted_columns}) VALUES ({placeholders})",
      rows,
    )


  @staticmethod
  def _sqlite_schema_matches(existing: list[tuple[str, str]], columns: list[str], types: list[str | None]) -> bool:
    if [column for column, _type in existing] != columns:
      return False
    for (_column, existing_type), expected_type in zip(existing, types):
      if expected_type is None:
        continue
      if expected_type == "":
        if existing_type:
          return False
        continue
      if existing_type and existing_type != expected_type:
        return False
      if not existing_type:
        continue
    return True


  def _save_sqlite(self, path: str) -> None:
    options = dict(self.write_options)
    table = options.pop("table", None)
    if options:
      raise RandEngineError("sqlite accepts only the 'table' option")
    if not isinstance(table, str) or not table or "\x00" in table:
      raise RandEngineError("sqlite option 'table' must be a non-empty NUL-free string")
    if self.write_mode not in ("overwrite", "append"):
      raise RandEngineError("sqlite mode must be 'overwrite' or 'append'")

    total = self._resolved_size(self.size_def())
    _catalog_def, batch = self.file_def()
    columns, types, rows = self._sqlite_frame(batch(total, 0))
    destination = Path(path)
    destination.parent.mkdir(parents=True, exist_ok=True)

    if self.write_mode == "append":
      destination_existed = destination.exists()
      database = None
      try:
        database = sqlite3.connect(str(destination))
        database.execute("BEGIN")
        existing = self._sqlite_existing_schema(database, table)
        if existing:
          if not self._sqlite_schema_matches(existing, columns, types):
            raise RandEngineError("sqlite append schema does not match the existing table")
        else:
          self._sqlite_create(database, table, columns, types)
        self._sqlite_insert(database, table, columns, rows)
        database.commit()
      except RandEngineError:
        if database is not None:
          database.rollback()
        raise
      except sqlite3.Error as error:
        if database is not None:
          database.rollback()
        raise RandEngineError(f"sqlite write failed: {error}") from error
      finally:
        if database is not None:
          database.close()
      if not destination_existed and not destination.exists():
        raise RandEngineError("sqlite append did not create its destination")
      return

    descriptor, staging_name = tempfile.mkstemp(
      prefix=f".{destination.name}.", suffix=".staging", dir=destination.parent
    )
    os.close(descriptor)
    staging = Path(staging_name)
    try:
      if destination.exists():
        source = sqlite3.connect(str(destination))
        try:
          target = sqlite3.connect(str(staging))
          try:
            source.backup(target)
          finally:
            target.close()
        finally:
          source.close()
      database = sqlite3.connect(str(staging))
      try:
        database.execute("BEGIN")
        database.execute(f"DROP TABLE IF EXISTS {self._sqlite_identifier(table)}")
        self._sqlite_create(database, table, columns, types)
        self._sqlite_insert(database, table, columns, rows)
        database.commit()
      except sqlite3.Error as error:
        database.rollback()
        raise RandEngineError(f"sqlite write failed: {error}") from error
      finally:
        database.close()
      os.replace(staging, destination)
    finally:
      staging.unlink(missing_ok=True)


  def _open_session(
    self,
    path: Path,
    options: dict,
    *,
    append_from: Path | None = None,
    expected_schema: pa.Schema | None = None,
  ):
    factory = {
      "csv": FileHandler.open_csv,
      "json": FileHandler.open_json,
      "parquet": FileHandler.open_parquet,
    }.get(self.write_format)
    if factory is None:
      raise RandEngineError(f"unsupported write format '{self.write_format}'")
    return factory(
      str(path),
      options,
      append_from=str(append_from) if append_from is not None else None,
      expected_schema=expected_schema,
    )


  def _stage_files(
    self,
    staging: Path,
    file_sizes: list[int],
    extension: str,
    options: dict,
    offset: int,
    batch_limit: int | None,
    single_append_from: Path | None,
  ) -> None:
    single = len(file_sizes) == 1
    stable_schema = None
    for file_size in file_sizes:
      file_path = staging / f"output.{extension}" if single else self._part_path(staging, extension)
      # One spec evaluation per final file; a row-less file reuses the previous file's schema.
      catalog_def, batch = self.file_def() if file_size or stable_schema is None else (None, None)
      if not file_size:
        expected_schema = stable_schema or catalog_def(True)
      else:
        expected_schema = stable_schema if batch_limit else None
      with self._open_session(
        file_path,
        options,
        append_from=single_append_from if single else None,
        expected_schema=expected_schema,
      ) as session:
        for batch_size in self._batches(file_size, batch_limit):
          session.write(batch(batch_size, offset), catalog_def)
          offset += batch_size
          if batch_limit is not None and session.schema is None:
            raise RandEngineError("schema is indeterminate when row batching is enabled")
      if session.schema is not None:
        stable_schema = session.schema.remove_metadata()


  def save(self, path: str) -> None:
    """
    Generate ``size`` rows and write them to ``path``.

    Writes one file at ``path`` by default; with the ``numFiles`` option, a directory of
    that many part files. ``mode("append")`` adds the rows to what is already there.

    Parameters
    ----------
    path : str
        Relative or absolute path; a single file gets the format's extension.

    Raises
    ------
    RandEngineError
        If ``size`` was not set, or ``numFiles`` or ``maxRowsPerBatch`` is not a positive integer.

    Examples
    --------
    >>> import os, tempfile
    >>> import pandas as pd
    >>> from rand_engine import DataGenerator
    >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
    >>> path = os.path.join(tempfile.mkdtemp(), "save_parts")
    >>> DataGenerator(spec, seed=42).size(10).write.option("numFiles", 2).format("csv").save(path)
    >>> len(os.listdir(path))
    2
    >>> sum(len(pd.read_csv(os.path.join(path, name))) for name in os.listdir(path))
    10
    """
    if self.write_format == "sqlite":
      self._save_sqlite(path)
      return

    write_options = dict(self.write_options)
    num_files = self._positive_integer("numFiles", write_options.pop("numFiles", 1))
    batch_limit = write_options.pop("maxRowsPerBatch", None)
    if batch_limit is not None:
      batch_limit = self._positive_integer("maxRowsPerBatch", batch_limit)
    total = self._resolved_size(self.size_def())

    base_path, file_name, extension = FileHandler.handle_path(
      path, self.write_format, write_options
    )
    parent = Path(base_path or ".")
    destination = parent / file_name if num_files > 1 else parent / f"{file_name}.{extension}"
    file_sizes = self._file_sizes(total, num_files)
    self.writer_method[self.write_format](
      lambda: None, str(destination), write_options, ("numFiles",)
    )

    parent.mkdir(parents=True, exist_ok=True)
    staging = Path(tempfile.mkdtemp(prefix=f".{destination.name}.", suffix=".staging", dir=parent))
    committed = False
    try:
      append = self.write_mode == "append"
      append_from = destination if num_files == 1 and append and destination.exists() else None
      self._stage_files(
        staging,
        file_sizes,
        extension,
        write_options,
        0,
        batch_limit,
        append_from,
      )

      if num_files == 1:
        os.replace(staging / f"output.{extension}", destination)
        committed = True
      elif append and destination.exists():
        self._add_parts(staging, destination)
        committed = True
      else:
        self._commit_directory(staging, destination)
        committed = True
    finally:
      if staging.exists() and (not committed or num_files == 1 or append):
        shutil.rmtree(staging)

__map_methods()

Map file formats to their corresponding FileHandler methods.

Source code in rand_engine/file_handlers/writer.py
33
34
35
36
37
38
39
def __map_methods(self):
  """Map file formats to their corresponding FileHandler methods."""
  return {
    "csv": FileHandler.to_csv,
    "parquet": FileHandler.to_parquet,
    "json": FileHandler.to_json
  }

format(format)

Set the output file format of the next save or start.

Parameters:

Name Type Description Default
format str

"csv", "parquet", "json" or batch-only "sqlite".

required

Returns:

Type Description
FileWriter

This writer, for chaining.

Examples:

>>> import os, tempfile
>>> import pandas as pd
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> path = os.path.join(tempfile.mkdtemp(), "format.parquet")
>>> DataGenerator(spec, seed=42).size(10).write.format("parquet").save(path)
>>> len(pd.read_parquet(path))
10
Source code in rand_engine/file_handlers/writer.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def format(self, format: str) :
  """
  Set the output file format of the next ``save`` or ``start``.

  Parameters
  ----------
  format : str
      ``"csv"``, ``"parquet"``, ``"json"`` or batch-only ``"sqlite"``.

  Returns
  -------
  FileWriter
      This writer, for chaining.

  Examples
  --------
  >>> import os, tempfile
  >>> import pandas as pd
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> path = os.path.join(tempfile.mkdtemp(), "format.parquet")
  >>> DataGenerator(spec, seed=42).size(10).write.format("parquet").save(path)
  >>> len(pd.read_parquet(path))
  10
  """
  self.write_format = format
  return self

mode(write_mode)

Set the write mode of the next save or start.

Parameters:

Name Type Description Default
write_mode str

"overwrite" (default) replaces the output (file or folder); "append" adds to it.

required

Returns:

Type Description
FileWriter

This writer, for chaining.

Examples:

>>> import os, tempfile
>>> import pandas as pd
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> gen = DataGenerator(spec, seed=42).size(10)
>>> path = os.path.join(tempfile.mkdtemp(), "mode.csv")
>>> gen.write.format("csv").save(path)
>>> gen.write.mode("append").format("csv").save(path)
>>> len(pd.read_csv(path))
20
Source code in rand_engine/file_handlers/writer.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def mode(self, write_mode: str) -> Callable:
  """
  Set the write mode of the next ``save`` or ``start``.

  Parameters
  ----------
  write_mode : str
      ``"overwrite"`` (default) replaces the output (file or folder); ``"append"`` adds to it.

  Returns
  -------
  FileWriter
      This writer, for chaining.

  Examples
  --------
  >>> import os, tempfile
  >>> import pandas as pd
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> gen = DataGenerator(spec, seed=42).size(10)
  >>> path = os.path.join(tempfile.mkdtemp(), "mode.csv")
  >>> gen.write.format("csv").save(path)
  >>> gen.write.mode("append").format("csv").save(path)
  >>> len(pd.read_csv(path))
  20
  """
  self.write_mode = write_mode
  return self

option(key, value)

Set one write option of the next save or start.

Parameters:

Name Type Description Default
key str

Option name: "compression" (file codec), "numFiles" (parts per save), "maxRowsPerBatch" (rows per write batch) or "timeout" (seconds a start runs, default 20). SQLite accepts only "table".

required
value Any

Option value.

required

Returns:

Type Description
FileWriter

This writer, for chaining.

Examples:

>>> import os, tempfile
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> path = os.path.join(tempfile.mkdtemp(), "parts")
>>> DataGenerator(spec, seed=42).size(10).write.option("numFiles", 2).format("csv").save(path)
>>> len(os.listdir(path))
2
Source code in rand_engine/file_handlers/writer.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def option(self, key: str, value: Any):
  """
  Set one write option of the next ``save`` or ``start``.

  Parameters
  ----------
  key : str
      Option name: ``"compression"`` (file codec), ``"numFiles"`` (parts per ``save``),
      ``"maxRowsPerBatch"`` (rows per write batch) or ``"timeout"`` (seconds a
      ``start`` runs, default 20). SQLite accepts only ``"table"``.
  value : Any
      Option value.

  Returns
  -------
  FileWriter
      This writer, for chaining.

  Examples
  --------
  >>> import os, tempfile
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> path = os.path.join(tempfile.mkdtemp(), "parts")
  >>> DataGenerator(spec, seed=42).size(10).write.option("numFiles", 2).format("csv").save(path)
  >>> len(os.listdir(path))
  2
  """
  self.write_options[key] = value
  return self

options(**kwargs)

Set several write options of the next save or start at once.

Parameters:

Name Type Description Default
**kwargs Any

Option names and values, such as numFiles=2; the keys are those of option.

{}

Returns:

Type Description
FileWriter

This writer, for chaining.

Examples:

>>> import os, tempfile
>>> import pandas as pd
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> path = os.path.join(tempfile.mkdtemp(), "options")
>>> gen = DataGenerator(spec, seed=42).size(10).write.options(numFiles=2, maxRowsPerBatch=3)
>>> gen.format("csv").save(path)
>>> sum(len(pd.read_csv(os.path.join(path, name))) for name in os.listdir(path))
10
Source code in rand_engine/file_handlers/writer.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def options(self, **kwargs):
  """
  Set several write options of the next ``save`` or ``start`` at once.

  Parameters
  ----------
  **kwargs : Any
      Option names and values, such as ``numFiles=2``; the keys are those of ``option``.

  Returns
  -------
  FileWriter
      This writer, for chaining.

  Examples
  --------
  >>> import os, tempfile
  >>> import pandas as pd
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> path = os.path.join(tempfile.mkdtemp(), "options")
  >>> gen = DataGenerator(spec, seed=42).size(10).write.options(numFiles=2, maxRowsPerBatch=3)
  >>> gen.format("csv").save(path)
  >>> sum(len(pd.read_csv(os.path.join(path, name))) for name in os.listdir(path))
  10
  """
  for key, value in kwargs.items():
    self.write_options[key] = value
  return self

save(path)

Generate size rows and write them to path.

Writes one file at path by default; with the numFiles option, a directory of that many part files. mode("append") adds the rows to what is already there.

Parameters:

Name Type Description Default
path str

Relative or absolute path; a single file gets the format's extension.

required

Raises:

Type Description
RandEngineError

If size was not set, or numFiles or maxRowsPerBatch is not a positive integer.

Examples:

>>> import os, tempfile
>>> import pandas as pd
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> path = os.path.join(tempfile.mkdtemp(), "save_parts")
>>> DataGenerator(spec, seed=42).size(10).write.option("numFiles", 2).format("csv").save(path)
>>> len(os.listdir(path))
2
>>> sum(len(pd.read_csv(os.path.join(path, name))) for name in os.listdir(path))
10
Source code in rand_engine/file_handlers/_writer_batch.py
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
def save(self, path: str) -> None:
  """
  Generate ``size`` rows and write them to ``path``.

  Writes one file at ``path`` by default; with the ``numFiles`` option, a directory of
  that many part files. ``mode("append")`` adds the rows to what is already there.

  Parameters
  ----------
  path : str
      Relative or absolute path; a single file gets the format's extension.

  Raises
  ------
  RandEngineError
      If ``size`` was not set, or ``numFiles`` or ``maxRowsPerBatch`` is not a positive integer.

  Examples
  --------
  >>> import os, tempfile
  >>> import pandas as pd
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> path = os.path.join(tempfile.mkdtemp(), "save_parts")
  >>> DataGenerator(spec, seed=42).size(10).write.option("numFiles", 2).format("csv").save(path)
  >>> len(os.listdir(path))
  2
  >>> sum(len(pd.read_csv(os.path.join(path, name))) for name in os.listdir(path))
  10
  """
  if self.write_format == "sqlite":
    self._save_sqlite(path)
    return

  write_options = dict(self.write_options)
  num_files = self._positive_integer("numFiles", write_options.pop("numFiles", 1))
  batch_limit = write_options.pop("maxRowsPerBatch", None)
  if batch_limit is not None:
    batch_limit = self._positive_integer("maxRowsPerBatch", batch_limit)
  total = self._resolved_size(self.size_def())

  base_path, file_name, extension = FileHandler.handle_path(
    path, self.write_format, write_options
  )
  parent = Path(base_path or ".")
  destination = parent / file_name if num_files > 1 else parent / f"{file_name}.{extension}"
  file_sizes = self._file_sizes(total, num_files)
  self.writer_method[self.write_format](
    lambda: None, str(destination), write_options, ("numFiles",)
  )

  parent.mkdir(parents=True, exist_ok=True)
  staging = Path(tempfile.mkdtemp(prefix=f".{destination.name}.", suffix=".staging", dir=parent))
  committed = False
  try:
    append = self.write_mode == "append"
    append_from = destination if num_files == 1 and append and destination.exists() else None
    self._stage_files(
      staging,
      file_sizes,
      extension,
      write_options,
      0,
      batch_limit,
      append_from,
    )

    if num_files == 1:
      os.replace(staging / f"output.{extension}", destination)
      committed = True
    elif append and destination.exists():
      self._add_parts(staging, destination)
      committed = True
    else:
      self._commit_directory(staging, destination)
      committed = True
  finally:
    if staging.exists() and (not committed or num_files == 1 or append):
      shutil.rmtree(staging)

Bases: FileWriter

Write a DataGenerator stream to a directory, one microbatch file per pass.

Built by DataGenerator.writeStream; chain format, trigger and option("timeout", seconds) first, then call start(path).

Examples:

>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> stream = DataGenerator(spec, seed=42).size(10).writeStream
>>> stream.format("csv").trigger(frequency=0.1).option("timeout", 0.3) is stream
True
Source code in rand_engine/file_handlers/_writer_stream.py
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class FileStreamWriter(FileWriter):
  """
  Write a DataGenerator stream to a directory, one microbatch file per pass.

  Built by ``DataGenerator.writeStream``; chain ``format``, ``trigger`` and
  ``option("timeout", seconds)`` first, then call ``start(path)``.

  Examples
  --------
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> stream = DataGenerator(spec, seed=42).size(10).writeStream
  >>> stream.format("csv").trigger(frequency=0.1).option("timeout", 0.3) is stream
  True
  """

  def __init__(self, size_def, microbatch_def):
    super().__init__(size_def, microbatch_def)
    self.freq = 1


  def trigger(self, frequency: float):
    """
    Set the pause between two microbatch writes of the next ``start``.

    Parameters
    ----------
    frequency : float
        Seconds to sleep after each write, fractions allowed; default 1.

    Returns
    -------
    FileStreamWriter
        This writer, for chaining.

    Examples
    --------
    >>> from rand_engine import DataGenerator
    >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
    >>> stream = DataGenerator(spec, seed=42).size(10).writeStream
    >>> stream.trigger(frequency=0.5) is stream
    True
    """
    self.freq = frequency
    return self


  def __handle_filenames(self, path: str, ext: str) -> Generator:
    while True:
      yield f"{path}/part-{str(uuid.uuid4())}.{ext}"

  def __writer(self, dataframe, file_gen, write_options):
    return self.writer_method[self.write_format](dataframe, next(file_gen), write_options, ("timeout",))


  def start(self, path):
    """
    Write one microbatch file per pass under ``path`` until the ``timeout`` option elapses.

    Each pass writes ``size`` rows to ``path/part-<uuid>.<ext>`` and sleeps ``trigger``
    seconds. The ``timeout`` option (default 20 seconds) ends the loop after the pass that
    crosses it. Blocks for that duration; in ``overwrite`` mode it clears ``path`` first.

    Parameters
    ----------
    path : str
        Directory to write the part files into, created if missing.

    Raises
    ------
    RandEngineError
        If ``size`` was not set.

    Examples
    --------
    >>> import os, tempfile
    >>> import pandas as pd
    >>> from rand_engine import DataGenerator
    >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
    >>> target = os.path.join(tempfile.mkdtemp(), "ages")
    >>> stream = DataGenerator(spec, seed=42).size(10).writeStream
    >>> stream.format("csv").trigger(frequency=0.1).option("timeout", 0.3).start(target)
    >>> files = os.listdir(target)
    >>> bool(files) and all(len(pd.read_csv(os.path.join(target, name))) == 10 for name in files)
    True
    """
    if self.write_format == "sqlite":
      raise RandEngineError("sqlite is available only through write.save")
    base_path, file_name_cleaned, ext = FileHandler.handle_path(path, self.write_format, self.write_options)
    path = f"{base_path}/{file_name_cleaned}"
    size = self.size_def()
    dataframe, offset = self.microbatch_def(size, 0), size
    write_options = dict(self.write_options)
    timeout = write_options.pop("timeout", 20)
    file_gen = self.__handle_filenames(path, ext)
    write = self.__writer(dataframe, file_gen, write_options)
    os.makedirs(path, exist_ok=True)
    if self.write_mode == "overwrite":
      if os.path.exists(path):
        for f in os.listdir(path):
          os.remove(os.path.join(path, f))
    start_time = time.time()
    while True:
      write()
      time.sleep(self.freq)
      if time.time() - start_time > timeout: break
      size = self.size_def()
      dataframe, offset = self.microbatch_def(size, offset), offset + size
      write = self.__writer(dataframe, file_gen, write_options)

__map_methods()

Map file formats to their corresponding FileHandler methods.

Source code in rand_engine/file_handlers/writer.py
33
34
35
36
37
38
39
def __map_methods(self):
  """Map file formats to their corresponding FileHandler methods."""
  return {
    "csv": FileHandler.to_csv,
    "parquet": FileHandler.to_parquet,
    "json": FileHandler.to_json
  }

format(format)

Set the output file format of the next save or start.

Parameters:

Name Type Description Default
format str

"csv", "parquet", "json" or batch-only "sqlite".

required

Returns:

Type Description
FileWriter

This writer, for chaining.

Examples:

>>> import os, tempfile
>>> import pandas as pd
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> path = os.path.join(tempfile.mkdtemp(), "format.parquet")
>>> DataGenerator(spec, seed=42).size(10).write.format("parquet").save(path)
>>> len(pd.read_parquet(path))
10
Source code in rand_engine/file_handlers/writer.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def format(self, format: str) :
  """
  Set the output file format of the next ``save`` or ``start``.

  Parameters
  ----------
  format : str
      ``"csv"``, ``"parquet"``, ``"json"`` or batch-only ``"sqlite"``.

  Returns
  -------
  FileWriter
      This writer, for chaining.

  Examples
  --------
  >>> import os, tempfile
  >>> import pandas as pd
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> path = os.path.join(tempfile.mkdtemp(), "format.parquet")
  >>> DataGenerator(spec, seed=42).size(10).write.format("parquet").save(path)
  >>> len(pd.read_parquet(path))
  10
  """
  self.write_format = format
  return self

mode(write_mode)

Set the write mode of the next save or start.

Parameters:

Name Type Description Default
write_mode str

"overwrite" (default) replaces the output (file or folder); "append" adds to it.

required

Returns:

Type Description
FileWriter

This writer, for chaining.

Examples:

>>> import os, tempfile
>>> import pandas as pd
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> gen = DataGenerator(spec, seed=42).size(10)
>>> path = os.path.join(tempfile.mkdtemp(), "mode.csv")
>>> gen.write.format("csv").save(path)
>>> gen.write.mode("append").format("csv").save(path)
>>> len(pd.read_csv(path))
20
Source code in rand_engine/file_handlers/writer.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def mode(self, write_mode: str) -> Callable:
  """
  Set the write mode of the next ``save`` or ``start``.

  Parameters
  ----------
  write_mode : str
      ``"overwrite"`` (default) replaces the output (file or folder); ``"append"`` adds to it.

  Returns
  -------
  FileWriter
      This writer, for chaining.

  Examples
  --------
  >>> import os, tempfile
  >>> import pandas as pd
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> gen = DataGenerator(spec, seed=42).size(10)
  >>> path = os.path.join(tempfile.mkdtemp(), "mode.csv")
  >>> gen.write.format("csv").save(path)
  >>> gen.write.mode("append").format("csv").save(path)
  >>> len(pd.read_csv(path))
  20
  """
  self.write_mode = write_mode
  return self

option(key, value)

Set one write option of the next save or start.

Parameters:

Name Type Description Default
key str

Option name: "compression" (file codec), "numFiles" (parts per save), "maxRowsPerBatch" (rows per write batch) or "timeout" (seconds a start runs, default 20). SQLite accepts only "table".

required
value Any

Option value.

required

Returns:

Type Description
FileWriter

This writer, for chaining.

Examples:

>>> import os, tempfile
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> path = os.path.join(tempfile.mkdtemp(), "parts")
>>> DataGenerator(spec, seed=42).size(10).write.option("numFiles", 2).format("csv").save(path)
>>> len(os.listdir(path))
2
Source code in rand_engine/file_handlers/writer.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def option(self, key: str, value: Any):
  """
  Set one write option of the next ``save`` or ``start``.

  Parameters
  ----------
  key : str
      Option name: ``"compression"`` (file codec), ``"numFiles"`` (parts per ``save``),
      ``"maxRowsPerBatch"`` (rows per write batch) or ``"timeout"`` (seconds a
      ``start`` runs, default 20). SQLite accepts only ``"table"``.
  value : Any
      Option value.

  Returns
  -------
  FileWriter
      This writer, for chaining.

  Examples
  --------
  >>> import os, tempfile
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> path = os.path.join(tempfile.mkdtemp(), "parts")
  >>> DataGenerator(spec, seed=42).size(10).write.option("numFiles", 2).format("csv").save(path)
  >>> len(os.listdir(path))
  2
  """
  self.write_options[key] = value
  return self

options(**kwargs)

Set several write options of the next save or start at once.

Parameters:

Name Type Description Default
**kwargs Any

Option names and values, such as numFiles=2; the keys are those of option.

{}

Returns:

Type Description
FileWriter

This writer, for chaining.

Examples:

>>> import os, tempfile
>>> import pandas as pd
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> path = os.path.join(tempfile.mkdtemp(), "options")
>>> gen = DataGenerator(spec, seed=42).size(10).write.options(numFiles=2, maxRowsPerBatch=3)
>>> gen.format("csv").save(path)
>>> sum(len(pd.read_csv(os.path.join(path, name))) for name in os.listdir(path))
10
Source code in rand_engine/file_handlers/writer.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def options(self, **kwargs):
  """
  Set several write options of the next ``save`` or ``start`` at once.

  Parameters
  ----------
  **kwargs : Any
      Option names and values, such as ``numFiles=2``; the keys are those of ``option``.

  Returns
  -------
  FileWriter
      This writer, for chaining.

  Examples
  --------
  >>> import os, tempfile
  >>> import pandas as pd
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> path = os.path.join(tempfile.mkdtemp(), "options")
  >>> gen = DataGenerator(spec, seed=42).size(10).write.options(numFiles=2, maxRowsPerBatch=3)
  >>> gen.format("csv").save(path)
  >>> sum(len(pd.read_csv(os.path.join(path, name))) for name in os.listdir(path))
  10
  """
  for key, value in kwargs.items():
    self.write_options[key] = value
  return self

start(path)

Write one microbatch file per pass under path until the timeout option elapses.

Each pass writes size rows to path/part-<uuid>.<ext> and sleeps trigger seconds. The timeout option (default 20 seconds) ends the loop after the pass that crosses it. Blocks for that duration; in overwrite mode it clears path first.

Parameters:

Name Type Description Default
path str

Directory to write the part files into, created if missing.

required

Raises:

Type Description
RandEngineError

If size was not set.

Examples:

>>> import os, tempfile
>>> import pandas as pd
>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> target = os.path.join(tempfile.mkdtemp(), "ages")
>>> stream = DataGenerator(spec, seed=42).size(10).writeStream
>>> stream.format("csv").trigger(frequency=0.1).option("timeout", 0.3).start(target)
>>> files = os.listdir(target)
>>> bool(files) and all(len(pd.read_csv(os.path.join(target, name))) == 10 for name in files)
True
Source code in rand_engine/file_handlers/_writer_stream.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def start(self, path):
  """
  Write one microbatch file per pass under ``path`` until the ``timeout`` option elapses.

  Each pass writes ``size`` rows to ``path/part-<uuid>.<ext>`` and sleeps ``trigger``
  seconds. The ``timeout`` option (default 20 seconds) ends the loop after the pass that
  crosses it. Blocks for that duration; in ``overwrite`` mode it clears ``path`` first.

  Parameters
  ----------
  path : str
      Directory to write the part files into, created if missing.

  Raises
  ------
  RandEngineError
      If ``size`` was not set.

  Examples
  --------
  >>> import os, tempfile
  >>> import pandas as pd
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> target = os.path.join(tempfile.mkdtemp(), "ages")
  >>> stream = DataGenerator(spec, seed=42).size(10).writeStream
  >>> stream.format("csv").trigger(frequency=0.1).option("timeout", 0.3).start(target)
  >>> files = os.listdir(target)
  >>> bool(files) and all(len(pd.read_csv(os.path.join(target, name))) == 10 for name in files)
  True
  """
  if self.write_format == "sqlite":
    raise RandEngineError("sqlite is available only through write.save")
  base_path, file_name_cleaned, ext = FileHandler.handle_path(path, self.write_format, self.write_options)
  path = f"{base_path}/{file_name_cleaned}"
  size = self.size_def()
  dataframe, offset = self.microbatch_def(size, 0), size
  write_options = dict(self.write_options)
  timeout = write_options.pop("timeout", 20)
  file_gen = self.__handle_filenames(path, ext)
  write = self.__writer(dataframe, file_gen, write_options)
  os.makedirs(path, exist_ok=True)
  if self.write_mode == "overwrite":
    if os.path.exists(path):
      for f in os.listdir(path):
        os.remove(os.path.join(path, f))
  start_time = time.time()
  while True:
    write()
    time.sleep(self.freq)
    if time.time() - start_time > timeout: break
    size = self.size_def()
    dataframe, offset = self.microbatch_def(size, offset), offset + size
    write = self.__writer(dataframe, file_gen, write_options)

trigger(frequency)

Set the pause between two microbatch writes of the next start.

Parameters:

Name Type Description Default
frequency float

Seconds to sleep after each write, fractions allowed; default 1.

required

Returns:

Type Description
FileStreamWriter

This writer, for chaining.

Examples:

>>> from rand_engine import DataGenerator
>>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
>>> stream = DataGenerator(spec, seed=42).size(10).writeStream
>>> stream.trigger(frequency=0.5) is stream
True
Source code in rand_engine/file_handlers/_writer_stream.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def trigger(self, frequency: float):
  """
  Set the pause between two microbatch writes of the next ``start``.

  Parameters
  ----------
  frequency : float
      Seconds to sleep after each write, fractions allowed; default 1.

  Returns
  -------
  FileStreamWriter
      This writer, for chaining.

  Examples
  --------
  >>> from rand_engine import DataGenerator
  >>> spec = {"age": {"method": "integers", "kwargs": {"min": 18, "max": 80}}}
  >>> stream = DataGenerator(spec, seed=42).size(10).writeStream
  >>> stream.trigger(frequency=0.5) is stream
  True
  """
  self.freq = frequency
  return self