fauxdata-cli 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/.gitignore +1 -0
  2. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/PKG-INFO +9 -9
  3. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/README.md +7 -7
  4. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/pyproject.toml +2 -2
  5. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/src/fauxdata/__init__.py +1 -1
  6. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/src/fauxdata/commands/infer.py +24 -2
  7. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/tests/test_infer.py +67 -0
  8. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/LICENSE +0 -0
  9. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/schemas/events.yml +0 -0
  10. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/schemas/orders.yml +0 -0
  11. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/schemas/people.yml +0 -0
  12. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/src/fauxdata/commands/__init__.py +0 -0
  13. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/src/fauxdata/commands/agent.py +0 -0
  14. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/src/fauxdata/commands/generate.py +0 -0
  15. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/src/fauxdata/commands/init.py +0 -0
  16. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/src/fauxdata/commands/preview.py +0 -0
  17. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/src/fauxdata/commands/validate.py +0 -0
  18. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/src/fauxdata/generator.py +0 -0
  19. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/src/fauxdata/jsonmode.py +0 -0
  20. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/src/fauxdata/main.py +0 -0
  21. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/src/fauxdata/output.py +0 -0
  22. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/src/fauxdata/schema.py +0 -0
  23. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/src/fauxdata/validator.py +0 -0
  24. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/tests/__init__.py +0 -0
  25. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/tests/conftest.py +0 -0
  26. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/tests/test_agent.py +0 -0
  27. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/tests/test_cli.py +0 -0
  28. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/tests/test_generator.py +0 -0
  29. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/tests/test_new_fields.py +0 -0
  30. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/tests/test_output.py +0 -0
  31. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/tests/test_schema.py +0 -0
  32. {fauxdata_cli-0.2.1 → fauxdata_cli-0.2.2}/tests/test_validator.py +0 -0
@@ -3,3 +3,4 @@ tmp/
3
3
  __pycache__/
4
4
  *.egg-info/
5
5
  .coverage
6
+ /share.png
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fauxdata-cli
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: CLI for generating and validating fake datasets
5
5
  Project-URL: Homepage, https://aborruso.github.io/fauxdata/
6
6
  Project-URL: Repository, https://github.com/aborruso/fauxdata
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Requires-Python: >=3.11
16
- Requires-Dist: pointblank>=1.1.0
16
+ Requires-Dist: pointblank>=1.2.0
17
17
  Requires-Dist: polars>=1.0
18
18
  Requires-Dist: pyfiglet>=1.0
19
19
  Requires-Dist: pyyaml>=6.0
@@ -169,12 +169,12 @@ validation:
169
169
 
170
170
  | Type | Description | Options |
171
171
  |------|-------------|---------|
172
- | `int` | Integer | `min`, `max`, `unique` |
173
- | `float` | Floating point | `min`, `max`, `precision` |
172
+ | `int` | Integer | `min`, `max`, `unique`, `values` |
173
+ | `float` | Floating point | `min`, `max`, `precision`, `unique`, `values` |
174
174
  | `string` | Text | `preset`, `values`, `pattern`, `unique`, `min_length`, `max_length` |
175
175
  | `bool` | Boolean | — |
176
- | `date` | Date | `min`, `max` (ISO format) |
177
- | `datetime` | Datetime | `min`, `max` (ISO format) |
176
+ | `date` | Date | `min`, `max` (ISO format), `unique` |
177
+ | `datetime` | Datetime | `min`, `max` (ISO format), `unique` |
178
178
 
179
179
  ### Float precision
180
180
 
@@ -269,7 +269,7 @@ fauxdata generate schemas/people.yml --out - --format jsonl | wc -l
269
269
  | `--rows` | `-r` | from schema | Number of rows to generate |
270
270
  | `--out` | `-o` | from schema | Output path — use `-` for stdout |
271
271
  | `--format` | `-f` | from schema | Output format: `csv`, `parquet`, `json`, `jsonl`, `jsonlines` |
272
- | `--seed` | `-s` | from schema | Random seed for reproducibility |
272
+ | `--seed` | `-s` | from schema | Random seed for reproducibility: same seed, same data, on the same pointblank version |
273
273
  | `--validate` | `-v` | off | Run validation rules after generating |
274
274
  | `--json` | | off | Print only a JSON summary to stdout (see [Using fauxdata with AI agents](#using-fauxdata-with-ai-agents)) |
275
275
 
@@ -277,7 +277,7 @@ When `--out -` is used, all output messages are suppressed and only data is writ
277
277
 
278
278
  ### `fauxdata infer DATASET`
279
279
 
280
- Build a YAML schema from a **real** dataset. fauxdata inspects the table (via pointblank's schema inference) and writes a schema with inferred ranges, categorical value sets, string presets (email, url…), uniqueness, null rates, and string lengths — plus matching `validation:` rules. Feed the result back to `fauxdata generate` to produce synthetic data that mirrors the real shape, without ever shipping the real values.
280
+ Build a YAML schema from a **real** dataset. fauxdata inspects the table (via pointblank's schema inference) and writes a schema with inferred ranges, categorical value sets, string presets (email, url…), uniqueness, null rates, and string lengths — plus matching `validation:` rules. Feed the result back to `fauxdata generate` to produce synthetic data with the same columns, ranges and value sets, without copying the real rows. Each column is generated independently, so correlations between columns, rules such as `end_date > start_date`, and category frequencies are not preserved.
281
281
 
282
282
  ```
283
283
  fauxdata infer real_data.csv # -> real_data.yml
@@ -296,7 +296,7 @@ fauxdata infer big.parquet --sample-size 10000 --out - | head # stream to stdou
296
296
  | `--detect-presets` / `--no-detect-presets` | | on | Match string columns to known presets (email, url…) |
297
297
  | `--sample-size` | | all rows | Sample N rows before analysis (for very large tables) |
298
298
 
299
- > **Notes.** Low-cardinality columns (≤ `--categorical-threshold` distinct values) are frozen to their exact source values via `values:` — desirable for categories like `status`, but it means sensitive low-cardinality columns are reproduced verbatim. Raise the threshold or edit the schema if that's not what you want. Also, a column inferred as `unique` over a narrow integer range cannot generate many more rows than the source; widen `min`/`max` or drop `unique` to amplify.
299
+ > **Notes.** Low-cardinality columns (≤ `--categorical-threshold` distinct values) are frozen to their exact source values via `values:` — desirable for categories like `status`, but it means sensitive low-cardinality columns are reproduced verbatim. Raise the threshold or edit the schema if that's not what you want. This also applies to columns whose name suggests a place, company or job (`region`, `city`, `company`...). Personal presets are never frozen, so a low-cardinality column such as `product_name` still gets people's names: use `--no-detect-presets` or edit the schema. Also, a column inferred as `unique` over a narrow integer range cannot generate many more rows than the source; widen `min`/`max` or drop `unique` to amplify.
300
300
 
301
301
  ### `fauxdata validate DATASET SCHEMA`
302
302
 
@@ -145,12 +145,12 @@ validation:
145
145
 
146
146
  | Type | Description | Options |
147
147
  |------|-------------|---------|
148
- | `int` | Integer | `min`, `max`, `unique` |
149
- | `float` | Floating point | `min`, `max`, `precision` |
148
+ | `int` | Integer | `min`, `max`, `unique`, `values` |
149
+ | `float` | Floating point | `min`, `max`, `precision`, `unique`, `values` |
150
150
  | `string` | Text | `preset`, `values`, `pattern`, `unique`, `min_length`, `max_length` |
151
151
  | `bool` | Boolean | — |
152
- | `date` | Date | `min`, `max` (ISO format) |
153
- | `datetime` | Datetime | `min`, `max` (ISO format) |
152
+ | `date` | Date | `min`, `max` (ISO format), `unique` |
153
+ | `datetime` | Datetime | `min`, `max` (ISO format), `unique` |
154
154
 
155
155
  ### Float precision
156
156
 
@@ -245,7 +245,7 @@ fauxdata generate schemas/people.yml --out - --format jsonl | wc -l
245
245
  | `--rows` | `-r` | from schema | Number of rows to generate |
246
246
  | `--out` | `-o` | from schema | Output path — use `-` for stdout |
247
247
  | `--format` | `-f` | from schema | Output format: `csv`, `parquet`, `json`, `jsonl`, `jsonlines` |
248
- | `--seed` | `-s` | from schema | Random seed for reproducibility |
248
+ | `--seed` | `-s` | from schema | Random seed for reproducibility: same seed, same data, on the same pointblank version |
249
249
  | `--validate` | `-v` | off | Run validation rules after generating |
250
250
  | `--json` | | off | Print only a JSON summary to stdout (see [Using fauxdata with AI agents](#using-fauxdata-with-ai-agents)) |
251
251
 
@@ -253,7 +253,7 @@ When `--out -` is used, all output messages are suppressed and only data is writ
253
253
 
254
254
  ### `fauxdata infer DATASET`
255
255
 
256
- Build a YAML schema from a **real** dataset. fauxdata inspects the table (via pointblank's schema inference) and writes a schema with inferred ranges, categorical value sets, string presets (email, url…), uniqueness, null rates, and string lengths — plus matching `validation:` rules. Feed the result back to `fauxdata generate` to produce synthetic data that mirrors the real shape, without ever shipping the real values.
256
+ Build a YAML schema from a **real** dataset. fauxdata inspects the table (via pointblank's schema inference) and writes a schema with inferred ranges, categorical value sets, string presets (email, url…), uniqueness, null rates, and string lengths — plus matching `validation:` rules. Feed the result back to `fauxdata generate` to produce synthetic data with the same columns, ranges and value sets, without copying the real rows. Each column is generated independently, so correlations between columns, rules such as `end_date > start_date`, and category frequencies are not preserved.
257
257
 
258
258
  ```
259
259
  fauxdata infer real_data.csv # -> real_data.yml
@@ -272,7 +272,7 @@ fauxdata infer big.parquet --sample-size 10000 --out - | head # stream to stdou
272
272
  | `--detect-presets` / `--no-detect-presets` | | on | Match string columns to known presets (email, url…) |
273
273
  | `--sample-size` | | all rows | Sample N rows before analysis (for very large tables) |
274
274
 
275
- > **Notes.** Low-cardinality columns (≤ `--categorical-threshold` distinct values) are frozen to their exact source values via `values:` — desirable for categories like `status`, but it means sensitive low-cardinality columns are reproduced verbatim. Raise the threshold or edit the schema if that's not what you want. Also, a column inferred as `unique` over a narrow integer range cannot generate many more rows than the source; widen `min`/`max` or drop `unique` to amplify.
275
+ > **Notes.** Low-cardinality columns (≤ `--categorical-threshold` distinct values) are frozen to their exact source values via `values:` — desirable for categories like `status`, but it means sensitive low-cardinality columns are reproduced verbatim. Raise the threshold or edit the schema if that's not what you want. This also applies to columns whose name suggests a place, company or job (`region`, `city`, `company`...). Personal presets are never frozen, so a low-cardinality column such as `product_name` still gets people's names: use `--no-detect-presets` or edit the schema. Also, a column inferred as `unique` over a narrow integer range cannot generate many more rows than the source; widen `min`/`max` or drop `unique` to amplify.
276
276
 
277
277
  ### `fauxdata validate DATASET SCHEMA`
278
278
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fauxdata-cli"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "CLI for generating and validating fake datasets"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -13,7 +13,7 @@ classifiers = [
13
13
  "Operating System :: OS Independent",
14
14
  ]
15
15
  dependencies = [
16
- "pointblank>=1.1.0",
16
+ "pointblank>=1.2.0",
17
17
  "typer>=0.12",
18
18
  "rich>=13",
19
19
  "pyfiglet>=1.0",
@@ -1,3 +1,3 @@
1
1
  """fauxdata - CLI for generating and validating fake datasets."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.2"
@@ -31,6 +31,11 @@ _DTYPE_MAP = {
31
31
  "Date": "date",
32
32
  }
33
33
 
34
+ # Presets pointblank matches on the column name alone, skipping categorical detection.
35
+ # Personal presets (name, phone, address...) are left out on purpose: restoring their
36
+ # source values would copy real people's data into the schema.
37
+ _CATEGORICAL_PRESETS = {"state", "city", "country", "country_code_2", "currency_code", "company", "job"}
38
+
34
39
 
35
40
  def _col_type_from_dtype(dtype: str) -> str:
36
41
  """Map a pointblank/polars dtype name to a fauxdata col_type."""
@@ -83,6 +88,19 @@ def _field_to_spec(field) -> dict:
83
88
  return spec
84
89
 
85
90
 
91
+ def _restore_categorical(spec: dict, series: pl.Series, threshold: int | float) -> dict:
92
+ """Replace a name-matched preset with the source values on a low-cardinality column."""
93
+ if spec.get("preset") not in _CATEGORICAL_PRESETS or spec.get("unique"):
94
+ return spec
95
+ limit = threshold if isinstance(threshold, int) else int(threshold * series.len())
96
+ values = series.drop_nulls().unique().sort().to_list()
97
+ if len(values) > limit:
98
+ return spec
99
+ del spec["preset"]
100
+ spec["values"] = values
101
+ return spec
102
+
103
+
86
104
  def _build_validation(columns: list[tuple[str, dict]]) -> list[dict]:
87
105
  """Derive validation rules from the inferred column specs."""
88
106
  rules: list[dict] = []
@@ -160,14 +178,18 @@ def run(
160
178
  if not json_out:
161
179
  rprint(f" Loaded [bold]{len(df)}[/bold] rows, [bold]{len(df.columns)}[/bold] columns")
162
180
 
181
+ threshold = _threshold(categorical_threshold)
163
182
  pb_schema = pb.schema_from_tbl(
164
183
  df,
165
- categorical_threshold=_threshold(categorical_threshold),
184
+ categorical_threshold=threshold,
166
185
  detect_presets=detect_presets,
167
186
  sample_size=sample_size,
168
187
  )
169
188
 
170
- columns = [(col_name, _field_to_spec(field)) for col_name, field in pb_schema.columns]
189
+ columns = [
190
+ (col_name, _restore_categorical(_field_to_spec(field), df[col_name], threshold))
191
+ for col_name, field in pb_schema.columns
192
+ ]
171
193
 
172
194
  schema_name = name or Path(dataset_path).stem
173
195
  out_rows = rows if rows is not None else len(df)
@@ -220,3 +220,70 @@ def test_infer_roundtrip_nulls_and_low_cardinality_int(tmp_path):
220
220
  df = generate_dataset(schema, rows=500, seed=3)
221
221
  all_passed, results = validate_dataset(df, schema)
222
222
  assert all_passed, [r for r in results if not r["ok"]]
223
+
224
+
225
+ # --- name-matched presets on categorical columns ---
226
+
227
+ def _infer_spec(tmp_path, column, values, **kwargs):
228
+ src = tmp_path / "real.csv"
229
+ pl.DataFrame({column: values}).write_csv(src)
230
+ out = tmp_path / "inferred.yml"
231
+ infer_mod.run(dataset_path=str(src), out=str(out), **kwargs)
232
+ return yaml.safe_load(out.read_text()), out
233
+
234
+
235
+ @pytest.mark.parametrize("column", ["region", "state", "city", "country", "currency", "company", "job"])
236
+ def test_infer_low_cardinality_keeps_source_values(tmp_path, column):
237
+ """A column matched to a preset only by its name stays categorical when values repeat."""
238
+ source = ["nord", "centro", "sud"] * 20
239
+ schema, out = _infer_spec(tmp_path, column, source)
240
+ spec = schema["columns"][column]
241
+ assert "preset" not in spec
242
+ assert sorted(spec["values"]) == ["centro", "nord", "sud"]
243
+
244
+ loaded = load_schema(out)
245
+ df = generate_dataset(loaded, rows=200, seed=1)
246
+ assert set(df[column].to_list()) <= {"nord", "centro", "sud"}
247
+ all_passed, results = validate_dataset(df, loaded)
248
+ assert all_passed, [r for r in results if not r["ok"]]
249
+
250
+
251
+ @pytest.mark.parametrize("column,preset", [("name", "name"), ("first_name", "first_name"), ("phone", "phone_number"), ("address", "address")])
252
+ def test_infer_personal_preset_never_frozen(tmp_path, column, preset):
253
+ """Personal presets are kept, so real people's values never reach the schema."""
254
+ schema, out = _infer_spec(tmp_path, column, ["Mario Rossi", "Anna Bianchi"] * 20)
255
+ assert schema["columns"][column]["preset"] == preset
256
+ assert "Mario Rossi" not in out.read_text()
257
+
258
+
259
+ def test_infer_high_cardinality_keeps_preset(tmp_path):
260
+ schema, _ = _infer_spec(tmp_path, "city", [f"city {i % 30}" for i in range(60)])
261
+ assert schema["columns"]["city"]["preset"] == "city"
262
+
263
+
264
+ def test_infer_unique_column_keeps_preset(tmp_path):
265
+ schema, _ = _infer_spec(tmp_path, "city", [f"city {i}" for i in range(10)])
266
+ spec = schema["columns"]["city"]
267
+ assert spec["preset"] == "city"
268
+ assert spec["unique"] is True
269
+
270
+
271
+ def test_infer_fractional_threshold_applies_to_preset_columns(tmp_path):
272
+ values = [f"r{i % 8}" for i in range(100)]
273
+ assert "values" in _infer_spec(tmp_path, "region", values, categorical_threshold=0.1)[0]["columns"]["region"]
274
+ assert "preset" in _infer_spec(tmp_path, "region", values, categorical_threshold=0.05)[0]["columns"]["region"]
275
+
276
+
277
+ def test_infer_no_detect_presets_unchanged(tmp_path):
278
+ schema, _ = _infer_spec(tmp_path, "region", ["a", "b"] * 10, detect_presets=False)
279
+ assert sorted(schema["columns"]["region"]["values"]) == ["a", "b"]
280
+
281
+
282
+ def test_infer_categorical_with_nulls_keeps_nullable(tmp_path):
283
+ schema, out = _infer_spec(tmp_path, "region", ["nord", None, "sud", "nord"] * 10)
284
+ spec = schema["columns"]["region"]
285
+ assert sorted(spec["values"]) == ["nord", "sud"]
286
+ assert spec["nullable"] is True
287
+ loaded = load_schema(out)
288
+ all_passed, results = validate_dataset(generate_dataset(loaded, rows=200, seed=1), loaded)
289
+ assert all_passed, [r for r in results if not r["ok"]]
File without changes