fauxdata-cli 0.1.4__tar.gz → 0.1.6__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 (40) hide show
  1. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/.claude/settings.local.json +3 -1
  2. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/.gitignore +1 -0
  3. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/LOG.md +16 -1
  4. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/PKG-INFO +26 -3
  5. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/README.md +24 -1
  6. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/pyproject.toml +2 -2
  7. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/src/fauxdata/__init__.py +1 -1
  8. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/src/fauxdata/commands/generate.py +11 -2
  9. fauxdata_cli-0.1.6/src/fauxdata/commands/infer.py +178 -0
  10. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/src/fauxdata/generator.py +27 -2
  11. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/src/fauxdata/main.py +29 -0
  12. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/src/fauxdata/schema.py +11 -0
  13. fauxdata_cli-0.1.6/tasks/todo.md +52 -0
  14. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/tests/test_generator.py +8 -0
  15. fauxdata_cli-0.1.6/tests/test_infer.py +171 -0
  16. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/tests/test_new_fields.py +37 -0
  17. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/uv.lock +188 -9
  18. fauxdata_cli-0.1.4/.coverage +0 -0
  19. fauxdata_cli-0.1.4/tasks/todo.md +0 -38
  20. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/.python-version +0 -0
  21. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/LICENSE +0 -0
  22. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/docs/deployment.md +0 -0
  23. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/docs/index.html +0 -0
  24. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/docs/share.png +0 -0
  25. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/schemas/events.yml +0 -0
  26. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/schemas/orders.yml +0 -0
  27. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/schemas/people.yml +0 -0
  28. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/share.png +0 -0
  29. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/src/fauxdata/commands/__init__.py +0 -0
  30. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/src/fauxdata/commands/init.py +0 -0
  31. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/src/fauxdata/commands/preview.py +0 -0
  32. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/src/fauxdata/commands/validate.py +0 -0
  33. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/src/fauxdata/output.py +0 -0
  34. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/src/fauxdata/validator.py +0 -0
  35. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/tests/__init__.py +0 -0
  36. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/tests/conftest.py +0 -0
  37. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/tests/test_cli.py +0 -0
  38. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/tests/test_output.py +0 -0
  39. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/tests/test_schema.py +0 -0
  40. {fauxdata_cli-0.1.4 → fauxdata_cli-0.1.6}/tests/test_validator.py +0 -0
@@ -4,7 +4,9 @@
4
4
  "WebFetch(domain:posit-dev.github.io)",
5
5
  "WebFetch(domain:posit.co)",
6
6
  "mcp__chrome-devtools__navigate_page",
7
- "mcp__chrome-devtools__lighthouse_audit"
7
+ "mcp__chrome-devtools__lighthouse_audit",
8
+ "WebFetch(domain:github.com)",
9
+ "Bash(gh issue *)"
8
10
  ]
9
11
  }
10
12
  }
@@ -2,3 +2,4 @@ tmp/
2
2
  .venv/
3
3
  __pycache__/
4
4
  *.egg-info/
5
+ .coverage
@@ -1,6 +1,21 @@
1
1
  # Log
2
2
 
3
- ## 2026-04-06 — agent-friendly CLI
3
+ ## 2026-07-04 — fix issue #2: `precision` float ignorato
4
+
5
+ - Bug: l'opzione `precision` dei float veniva letta nello schema ma mai applicata (`pb.float_field` non ha il parametro). Valori generati con precisione piena.
6
+ - Fix in `generator.py`: dopo `pb.generate_dataset`, arrotondo con `pl.col(...).round(precision)` le colonne `float` con `precision` impostato.
7
+ - Verifica: reproducer della issue → output atteso (`27.74, 169.64, …`). Test `test_generate_float_precision`. 103/103 pass.
8
+
9
+ ## 2026-06-12 — pointblank 0.25 + comando `infer`
10
+
11
+ - Bump `pointblank>=0.25` (uscito su PyPI). Schemi inclusi (people/orders/events) verificati: tutti PASS.
12
+ - Fase B regressioni: `rows_distinct` ora include righe con null (correttezza migliorata), lazy frame ok. fauxdata passa sempre DataFrame eager → nessun impatto.
13
+ - Nuovo comando **`fauxdata infer DATASET`**: da tabella reale (csv/parquet/json/jsonl) genera schema YAML (`columns:` + `validation:`) via `pb.schema_from_tbl`. Flag: `--out/-o`, `--name/-n`, `--rows/-r`, `--format/-f`, `--categorical-threshold`, `--detect-presets/--no-detect-presets`, `--sample-size`.
14
+ - `ColumnSchema`: nuovi campi `min_length`/`max_length` per stringhe (round-trip fedele); passati a `pb.string_field`. Validazione parsing (negativi, min>max).
15
+ - infer CSV: `try_parse_dates=True` → date/datetime tipizzati invece che congelati come categorici.
16
+ - generate: errore chiaro su conflitto unique+range stretto ("too narrow a range") invece del traceback criptico di pointblank.
17
+ - Limite noto: colonne a bassa cardinalità congelate ai valori reali (possibile leak su colonne sensibili) — documentato nel README.
18
+ - Test: +14 (`tests/test_infer.py` + length in `test_new_fields.py`). 102/102 pass, coverage 83.2%.
4
19
 
5
20
  - `init`: aggiunto `--description`, `--rows`, `--format`, `--yes`; questionary diventa fallback
6
21
  - Tutti i comandi: `epilog` con esempi in `--help`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fauxdata-cli
3
- Version: 0.1.4
3
+ Version: 0.1.6
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>=0.22
16
+ Requires-Dist: pointblank>=0.25
17
17
  Requires-Dist: polars>=1.0
18
18
  Requires-Dist: pyfiglet>=1.0
19
19
  Requires-Dist: pyyaml>=6.0
@@ -171,7 +171,7 @@ validation:
171
171
  |------|-------------|---------|
172
172
  | `int` | Integer | `min`, `max`, `unique` |
173
173
  | `float` | Floating point | `min`, `max` |
174
- | `string` | Text | `preset`, `values`, `unique` |
174
+ | `string` | Text | `preset`, `values`, `pattern`, `unique`, `min_length`, `max_length` |
175
175
  | `bool` | Boolean | — |
176
176
  | `date` | Date | `min`, `max` (ISO format) |
177
177
  | `datetime` | Datetime | `min`, `max` (ISO format) |
@@ -259,6 +259,29 @@ fauxdata generate schemas/people.yml --out - --format jsonl | wc -l
259
259
 
260
260
  When `--out -` is used, all output messages are suppressed and only data is written to stdout.
261
261
 
262
+ ### `fauxdata infer DATASET`
263
+
264
+ 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.
265
+
266
+ ```
267
+ fauxdata infer real_data.csv # -> real_data.yml
268
+ fauxdata infer real_data.parquet --out schema.yml --name people
269
+ fauxdata infer real_data.csv --categorical-threshold 50 --no-detect-presets
270
+ fauxdata infer big.parquet --sample-size 10000 --out - | head # stream to stdout
271
+ ```
272
+
273
+ | Option | Short | Default | Description |
274
+ |--------|-------|---------|-------------|
275
+ | `--out` | `-o` | `<name>.yml` | Output YAML path — use `-` for stdout |
276
+ | `--name` | `-n` | dataset stem | Schema name |
277
+ | `--rows` | `-r` | source rows | Rows to generate (written into the schema) |
278
+ | `--format` | `-f` | `csv` | Default output format written into the schema |
279
+ | `--categorical-threshold` | | `20` | Max unique values (int) or fraction (`0`–`1`) to treat a column as categorical |
280
+ | `--detect-presets` / `--no-detect-presets` | | on | Match string columns to known presets (email, url…) |
281
+ | `--sample-size` | | all rows | Sample N rows before analysis (for very large tables) |
282
+
283
+ > **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.
284
+
262
285
  ### `fauxdata validate DATASET SCHEMA`
263
286
 
264
287
  ```
@@ -147,7 +147,7 @@ validation:
147
147
  |------|-------------|---------|
148
148
  | `int` | Integer | `min`, `max`, `unique` |
149
149
  | `float` | Floating point | `min`, `max` |
150
- | `string` | Text | `preset`, `values`, `unique` |
150
+ | `string` | Text | `preset`, `values`, `pattern`, `unique`, `min_length`, `max_length` |
151
151
  | `bool` | Boolean | — |
152
152
  | `date` | Date | `min`, `max` (ISO format) |
153
153
  | `datetime` | Datetime | `min`, `max` (ISO format) |
@@ -235,6 +235,29 @@ fauxdata generate schemas/people.yml --out - --format jsonl | wc -l
235
235
 
236
236
  When `--out -` is used, all output messages are suppressed and only data is written to stdout.
237
237
 
238
+ ### `fauxdata infer DATASET`
239
+
240
+ 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.
241
+
242
+ ```
243
+ fauxdata infer real_data.csv # -> real_data.yml
244
+ fauxdata infer real_data.parquet --out schema.yml --name people
245
+ fauxdata infer real_data.csv --categorical-threshold 50 --no-detect-presets
246
+ fauxdata infer big.parquet --sample-size 10000 --out - | head # stream to stdout
247
+ ```
248
+
249
+ | Option | Short | Default | Description |
250
+ |--------|-------|---------|-------------|
251
+ | `--out` | `-o` | `<name>.yml` | Output YAML path — use `-` for stdout |
252
+ | `--name` | `-n` | dataset stem | Schema name |
253
+ | `--rows` | `-r` | source rows | Rows to generate (written into the schema) |
254
+ | `--format` | `-f` | `csv` | Default output format written into the schema |
255
+ | `--categorical-threshold` | | `20` | Max unique values (int) or fraction (`0`–`1`) to treat a column as categorical |
256
+ | `--detect-presets` / `--no-detect-presets` | | on | Match string columns to known presets (email, url…) |
257
+ | `--sample-size` | | all rows | Sample N rows before analysis (for very large tables) |
258
+
259
+ > **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.
260
+
238
261
  ### `fauxdata validate DATASET SCHEMA`
239
262
 
240
263
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fauxdata-cli"
3
- version = "0.1.4"
3
+ version = "0.1.6"
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>=0.22",
16
+ "pointblank>=0.25",
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.1.4"
3
+ __version__ = "0.1.6"
@@ -19,6 +19,15 @@ from fauxdata.validator import validate_dataset
19
19
  console = Console()
20
20
 
21
21
 
22
+ def _generate(schema, n, rng_seed):
23
+ """Generate, translating generation errors into a clean CLI exit."""
24
+ try:
25
+ return generate_dataset(schema, rows=n, seed=rng_seed)
26
+ except ValueError as e:
27
+ rprint(f"[red]Error:[/red] {e}")
28
+ raise typer.Exit(code=1)
29
+
30
+
22
31
  def run(
23
32
  schema_path: str,
24
33
  rows: Optional[int] = None,
@@ -54,12 +63,12 @@ def run(
54
63
  rprint(Panel(f"[bold cyan]fauxdata generate[/bold cyan] [dim]{schema_path}[/dim]", expand=False))
55
64
 
56
65
  if stdout_mode:
57
- df = generate_dataset(schema, rows=n, seed=rng_seed)
66
+ df = _generate(schema, n, rng_seed)
58
67
  write_stdout(df, output_fmt)
59
68
  return
60
69
 
61
70
  with console.status(f"[bold green]Generating {n} rows...[/bold green]"):
62
- df = generate_dataset(schema, rows=n, seed=rng_seed)
71
+ df = _generate(schema, n, rng_seed)
63
72
 
64
73
  _print_schema_table(schema, n, rng_seed)
65
74
 
@@ -0,0 +1,178 @@
1
+ """fauxdata infer command - build a YAML schema from a real dataset.
2
+
3
+ Uses pointblank's ``schema_from_tbl`` (>=0.25) to infer rich constraints
4
+ (min/max, unique, null rate, allowed values, presets, string lengths) from an
5
+ existing table, then serializes them into a fauxdata YAML schema that can be
6
+ fed back to ``fauxdata generate``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+
13
+ import pointblank as pb
14
+ import polars as pl
15
+ import typer
16
+ import yaml
17
+ from rich import print as rprint
18
+ from rich.panel import Panel
19
+
20
+ # Polars dtype name (str) -> fauxdata col_type
21
+ _DTYPE_MAP = {
22
+ "Int": "int",
23
+ "UInt": "int",
24
+ "Float": "float",
25
+ "String": "string",
26
+ "Utf8": "string",
27
+ "Boolean": "bool",
28
+ "Datetime": "datetime",
29
+ "Date": "date",
30
+ }
31
+
32
+
33
+ def _col_type_from_dtype(dtype: str) -> str:
34
+ """Map a pointblank/polars dtype name to a fauxdata col_type."""
35
+ for prefix, ctype in _DTYPE_MAP.items():
36
+ if dtype.startswith(prefix):
37
+ return ctype
38
+ return "string"
39
+
40
+
41
+ def _field_to_spec(field) -> dict:
42
+ """Convert an inferred pointblank Field into a fauxdata column spec dict."""
43
+ attrs = vars(field)
44
+ col_type = _col_type_from_dtype(attrs.get("dtype", "String"))
45
+ spec: dict = {"type": col_type}
46
+
47
+ if attrs.get("unique"):
48
+ spec["unique"] = True
49
+ if attrs.get("nullable"):
50
+ spec["nullable"] = True
51
+ np = attrs.get("null_probability")
52
+ if np:
53
+ spec["null_probability"] = round(float(np), 4)
54
+
55
+ if col_type in ("int", "float"):
56
+ if attrs.get("min_val") is not None:
57
+ spec["min"] = attrs["min_val"]
58
+ if attrs.get("max_val") is not None:
59
+ spec["max"] = attrs["max_val"]
60
+ if attrs.get("allowed"):
61
+ spec["values"] = list(attrs["allowed"])
62
+
63
+ elif col_type in ("date", "datetime"):
64
+ if attrs.get("min_date") is not None:
65
+ spec["min"] = str(attrs["min_date"])
66
+ if attrs.get("max_date") is not None:
67
+ spec["max"] = str(attrs["max_date"])
68
+
69
+ elif col_type == "string":
70
+ if attrs.get("preset"):
71
+ spec["preset"] = attrs["preset"]
72
+ elif attrs.get("allowed"):
73
+ spec["values"] = list(attrs["allowed"])
74
+ elif attrs.get("pattern"):
75
+ spec["pattern"] = attrs["pattern"]
76
+ if attrs.get("min_length") is not None:
77
+ spec["min_length"] = attrs["min_length"]
78
+ if attrs.get("max_length") is not None:
79
+ spec["max_length"] = attrs["max_length"]
80
+
81
+ return spec
82
+
83
+
84
+ def _build_validation(columns: list[tuple[str, dict]]) -> list[dict]:
85
+ """Derive validation rules from the inferred column specs."""
86
+ rules: list[dict] = []
87
+
88
+ not_null = [name for name, spec in columns if not spec.get("nullable")]
89
+ if not_null:
90
+ rules.append({"rule": "col_vals_not_null", "columns": not_null})
91
+
92
+ for name, spec in columns:
93
+ if spec["type"] in ("int", "float") and "min" in spec and "max" in spec:
94
+ rules.append({"rule": "col_vals_between", "column": name,
95
+ "min": spec["min"], "max": spec["max"]})
96
+
97
+ for name, spec in columns:
98
+ if "values" in spec:
99
+ rules.append({"rule": "col_vals_in_set", "column": name, "values": list(spec["values"])})
100
+
101
+ unique_cols = [name for name, spec in columns if spec.get("unique")]
102
+ if unique_cols:
103
+ rules.append({"rule": "rows_distinct", "columns": unique_cols})
104
+
105
+ return rules
106
+
107
+
108
+ def _load_table(path: str) -> pl.DataFrame:
109
+ ext = path.rsplit(".", 1)[-1].lower()
110
+ if ext == "csv":
111
+ # try_parse_dates so date/datetime columns are typed (not frozen as categorical strings)
112
+ return pl.read_csv(path, try_parse_dates=True)
113
+ if ext == "parquet":
114
+ return pl.read_parquet(path)
115
+ if ext == "json":
116
+ return pl.read_json(path)
117
+ if ext in ("jsonl", "ndjson"):
118
+ return pl.read_ndjson(path)
119
+ rprint(f"[red]Unsupported file format: .{ext}[/red]")
120
+ raise typer.Exit(code=1)
121
+
122
+
123
+ def run(
124
+ dataset_path: str,
125
+ out: str | None = None,
126
+ name: str | None = None,
127
+ rows: int | None = None,
128
+ fmt: str = "csv",
129
+ categorical_threshold: float = 20,
130
+ detect_presets: bool = True,
131
+ sample_size: int | None = None,
132
+ ):
133
+ """Infer a fauxdata YAML schema from a real dataset."""
134
+ rprint(Panel(
135
+ f"[bold cyan]fauxdata infer[/bold cyan] [dim]{dataset_path}[/dim]",
136
+ expand=False,
137
+ ))
138
+
139
+ df = _load_table(dataset_path)
140
+ rprint(f" Loaded [bold]{len(df)}[/bold] rows, [bold]{len(df.columns)}[/bold] columns")
141
+
142
+ pb_schema = pb.schema_from_tbl(
143
+ df,
144
+ categorical_threshold=categorical_threshold,
145
+ detect_presets=detect_presets,
146
+ sample_size=sample_size,
147
+ )
148
+
149
+ columns = [(col_name, _field_to_spec(field)) for col_name, field in pb_schema.columns]
150
+
151
+ schema_name = name or Path(dataset_path).stem
152
+ out_rows = rows if rows is not None else len(df)
153
+
154
+ schema_dict = {
155
+ "name": schema_name,
156
+ "description": f"Inferred from {Path(dataset_path).name}",
157
+ "rows": out_rows,
158
+ "seed": 42,
159
+ "output": {"format": fmt, "path": f"{schema_name}.{fmt}"},
160
+ "columns": {col_name: spec for col_name, spec in columns},
161
+ "validation": _build_validation(columns),
162
+ }
163
+
164
+ text = yaml.safe_dump(schema_dict, sort_keys=False, allow_unicode=True, default_flow_style=False)
165
+
166
+ out_path = out or f"{schema_name}.yml"
167
+ if out_path == "-":
168
+ import sys
169
+ sys.stdout.write(text)
170
+ return
171
+
172
+ Path(out_path).write_text(text)
173
+ rprint(f"[green]Created[/green] [bold]{out_path}[/bold] "
174
+ f"([bold]{len(columns)}[/bold] columns, "
175
+ f"[bold]{len(schema_dict['validation'])}[/bold] validation rules)")
176
+ rprint("[dim]Generate synthetic data with:[/dim]")
177
+ rprint(f" [cyan]fauxdata generate {out_path} --validate[/cyan]")
178
+ rprint(f"schema_path: {out_path}")
@@ -16,7 +16,27 @@ def generate_dataset(schema: SchemaConfig, rows: int | None = None, seed: int |
16
16
  pb_schema = _build_pb_schema(schema)
17
17
  country = schema.locale or "US"
18
18
 
19
- df = pb.generate_dataset(pb_schema, n=n, seed=rng_seed, country=country)
19
+ try:
20
+ df = pb.generate_dataset(pb_schema, n=n, seed=rng_seed, country=country)
21
+ except ValueError as e:
22
+ if "unique values" in str(e):
23
+ unique_cols = [c.name for c in schema.columns if c.unique]
24
+ raise ValueError(
25
+ f"Cannot generate {n} rows: a unique column has too narrow a range "
26
+ f"(unique columns: {', '.join(unique_cols) or 'n/a'}). "
27
+ f"Reduce --rows, widen min/max, or drop 'unique'."
28
+ ) from e
29
+ raise
30
+
31
+ # pointblank has no float precision option: round after generation
32
+ round_exprs = [
33
+ pl.col(col.name).round(col.precision)
34
+ for col in schema.columns
35
+ if col.col_type == "float" and col.precision is not None
36
+ ]
37
+ if round_exprs:
38
+ df = df.with_columns(round_exprs)
39
+
20
40
  return df
21
41
 
22
42
 
@@ -82,7 +102,12 @@ def _col_to_field(col: ColumnSchema):
82
102
  elif col.preset:
83
103
  return pb.string_field(preset=col.preset, nullable=nullable, unique=unique, **np_kwargs)
84
104
  else:
85
- return pb.string_field(nullable=nullable, unique=unique, **np_kwargs)
105
+ len_kwargs = {}
106
+ if col.min_length is not None:
107
+ len_kwargs["min_length"] = col.min_length
108
+ if col.max_length is not None:
109
+ len_kwargs["max_length"] = col.max_length
110
+ return pb.string_field(nullable=nullable, unique=unique, **len_kwargs, **np_kwargs)
86
111
 
87
112
  else:
88
113
  return pb.string_field(nullable=nullable, **np_kwargs)
@@ -65,6 +65,35 @@ def init_cmd(
65
65
  run(name=name, description=description, rows=rows, fmt=fmt, yes=yes)
66
66
 
67
67
 
68
+ @app.command(
69
+ "infer",
70
+ epilog=(
71
+ "Examples:\n\n"
72
+ " fauxdata infer real_data.csv\n\n"
73
+ " fauxdata infer real_data.parquet --out schema.yml --name people\n\n"
74
+ " fauxdata infer real_data.csv --categorical-threshold 50 --no-detect-presets\n\n"
75
+ " fauxdata infer big.parquet --sample-size 10000 --out - | head\n"
76
+ ),
77
+ )
78
+ def infer_cmd(
79
+ dataset: str = typer.Argument(..., help="Path to a real dataset (csv, parquet, json, jsonl)"),
80
+ out: Optional[str] = typer.Option(None, "--out", "-o", help="Output YAML path (use - for stdout; default: <name>.yml)"),
81
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Schema name (default: dataset file stem)"),
82
+ rows: Optional[int] = typer.Option(None, "--rows", "-r", help="Rows to generate (default: rows in source table)"),
83
+ fmt: str = typer.Option("csv", "--format", "-f", help="Default output format in the schema: csv, parquet, json, jsonl"),
84
+ categorical_threshold: float = typer.Option(20, "--categorical-threshold", help="Max unique values (int) or fraction (0-1) to treat a column as categorical"),
85
+ detect_presets: bool = typer.Option(True, "--detect-presets/--no-detect-presets", help="Match string columns to known presets (email, url, ...)"),
86
+ sample_size: Optional[int] = typer.Option(None, "--sample-size", help="Sample N rows before analysis (for very large tables)"),
87
+ ):
88
+ """Infer a YAML schema from a real dataset (powered by pointblank schema inference)."""
89
+ from fauxdata.commands.infer import run
90
+ run(
91
+ dataset_path=dataset, out=out, name=name, rows=rows, fmt=fmt,
92
+ categorical_threshold=categorical_threshold, detect_presets=detect_presets,
93
+ sample_size=sample_size,
94
+ )
95
+
96
+
68
97
  @app.command(
69
98
  "generate",
70
99
  epilog=(
@@ -59,6 +59,8 @@ class ColumnSchema:
59
59
  values: list | None = None # for in_set
60
60
  pattern: str | None = None # regex pattern for string generation
61
61
  null_probability: float | None = None # e.g. 0.1 = 10% nulls
62
+ min_length: int | None = None # min string length (string columns)
63
+ max_length: int | None = None # max string length (string columns)
62
64
 
63
65
 
64
66
  @dataclass
@@ -148,6 +150,13 @@ def _parse_column(name: str, data: dict) -> ColumnSchema:
148
150
  if null_probability is not None and not (0.0 <= float(null_probability) <= 1.0):
149
151
  raise ValueError(f"Column '{name}': null_probability must be between 0.0 and 1.0")
150
152
 
153
+ min_length = data.get("min_length", None)
154
+ max_length = data.get("max_length", None)
155
+ if min_length is not None and int(min_length) < 0:
156
+ raise ValueError(f"Column '{name}': min_length must be >= 0")
157
+ if min_length is not None and max_length is not None and int(min_length) > int(max_length):
158
+ raise ValueError(f"Column '{name}': min_length must be <= max_length")
159
+
151
160
  return ColumnSchema(
152
161
  name=name,
153
162
  col_type=col_type,
@@ -161,6 +170,8 @@ def _parse_column(name: str, data: dict) -> ColumnSchema:
161
170
  values=data.get("values", None),
162
171
  pattern=data.get("pattern", None),
163
172
  null_probability=float(null_probability) if null_probability is not None else None,
173
+ min_length=int(min_length) if min_length is not None else None,
174
+ max_length=int(max_length) if max_length is not None else None,
164
175
  )
165
176
 
166
177
 
@@ -0,0 +1,52 @@
1
+ # Impatto pointblank v0.25.0 su fauxdata
2
+
3
+ ## Verdetto
4
+ Sì, impatto **forte**. La release introduce `schema_from_tbl()` / `Schema.from_table()`:
5
+ inferisce vincoli ricchi (min/max, unique, null rate, `allowed=`, preset come email/uuid)
6
+ da una tabella reale, pensati esplicitamente per la generazione sintetica.
7
+ È esattamente la premessa del progetto ("real fake datasets"): dato reale → schema → dato finto.
8
+
9
+ ## BLOCCO — non installabile da PyPI
10
+ - v0.25.0 è **solo taggata su GitHub**, PyPI è fermo a 0.24.0.
11
+ - Non si può fare `pointblank>=0.25` + pubblicare fauxdata finché non esce su PyPI.
12
+ - API verificata sul tag git (non dal summary): firma confermata
13
+ `schema_from_tbl(tbl, *, infer_constraints=True, categorical_threshold=20, detect_presets=True, sample_size=None)`.
14
+
15
+ ## Fase A — nuovo comando `fauxdata infer` (la feature)
16
+ Flusso: tabella reale (CSV/Parquet via polars) → `schema_from_tbl` → YAML fauxdata → `generate`.
17
+ - Nuovo `commands/infer.py`: legge file, chiama `schema_from_tbl`, serializza in YAML fauxdata.
18
+ - Convertitore Field→ColumnSchema: mapping `dtype` (Int64/String/Float64/...) → `col_type`,
19
+ `allowed`→`values`, `preset`, `min_val/max_val`→`min/max`, `unique`, `null_probability`.
20
+ - Flag da esporre: `--categorical-threshold`, `--no-detect-presets`, `--sample-size`, `--out schema.yml`.
21
+ - Mapping **lossy** da segnalare: `StringField.min_length/max_length` non hanno campo nel YAML fauxdata.
22
+ - Test di accettazione (vero criterio): infer da tabella campione → YAML → `generate` →
23
+ output rispetta gli stessi vincoli (range, set, preset). NON basta "ritorna uno Schema".
24
+
25
+ ## Fase B — verifiche di regressione (cambi di comportamento)
26
+ - `rows_distinct()`: in 0.25 le righe con null **non** sono più escluse. fauxdata lo usa nei
27
+ `VALID_RULES` del validator → può cambiare pass/fail su schemi esistenti. Da testare.
28
+ - Lazy frame non più collezionati prematuramente: verificare che generator/validator non assuma DataFrame eager.
29
+
30
+ ## Fuori scope (non rilevante per fauxdata)
31
+ - `Contract`/`Pipeline`, attachments multi-modali in `prompt()`, fix MCP server.
32
+
33
+ ## Decisioni (prese 2026-06-12)
34
+ - **Timing**: solo piano. NON scrivere codice finché pointblank 0.25.0 non è su PyPI (ora è solo tag git, PyPI=0.24.0).
35
+ - **Validation**: `infer` genera sia `columns:` sia `validation:` (regole derivate dai vincoli — es. `col_vals_between` da min/max, `col_vals_in_set` da allowed, `col_vals_not_null`, `rows_distinct` per colonne unique).
36
+ - **Lunghezza stringhe**: aggiungere `min_length`/`max_length` a `ColumnSchema` per round-trip fedele
37
+ → tocca `schema.py` (parse + dataclass), `generator.py` (`pb.string_field(min_length=, max_length=)`), test.
38
+
39
+ ## Trigger di ripresa
40
+ Quando `pip index versions pointblank` (o PyPI) mostra 0.25.0:
41
+ 1. Bump `pyproject.toml`: `pointblank>=0.25`, `uv sync`.
42
+ 2. Fase B prima (regressioni rows_distinct / lazy frame) — sono rischi su codice esistente.
43
+ 3. Poi Fase A (comando `infer` + estensione min_length/max_length).
44
+
45
+ ## Review (completato 2026-06-12)
46
+ - **Fatto**: bump 0.25, comando `infer`, campi `min_length`/`max_length`, validation derivate, fix datetime CSV, errore chiaro unique+range. 102/102 test, coverage 83.2%.
47
+ - **Fase B esito**: nessuna modifica necessaria. Schemi inclusi tutti PASS; `rows_distinct`/lazy frame non impattano (fauxdata usa DataFrame eager).
48
+ - **Limiti noti documentati (README)**:
49
+ 1. Bassa cardinalità → valori reali congelati in `values:` (leak su colonne sensibili). Mitigazione: `--categorical-threshold` / editing manuale.
50
+ 2. `unique` + range int stretto non amplifica oltre la sorgente. Errore chiaro a runtime.
51
+ - **Non fatto (decisione utente)**: bump versione pacchetto (0.1.4 → 0.1.5) e publish PyPI — non richiesto, lasciato all'utente.
52
+ - **Possibili migliorie future** (docs/future-ideas.md candidate): opzione `infer` per non-categorizzare colonne testuali libere; flag per amplificazione (rilassa unique).
@@ -87,6 +87,14 @@ def test_generate_seed_reproducibility(minimal_schema):
87
87
  assert df1.equals(df2)
88
88
 
89
89
 
90
+ def test_generate_float_precision():
91
+ """float `precision` rounds generated values to the requested decimals."""
92
+ schema = _make_schema([ColumnSchema(name="f", col_type="float", min=1.0, max=200.0, precision=2)])
93
+ df = generate_dataset(schema, rows=20, seed=1)
94
+ for v in df["f"].to_list():
95
+ assert v == round(v, 2)
96
+
97
+
90
98
  def test_generate_all_types():
91
99
  """Generate a dataset with all supported column types."""
92
100
  schema = _make_schema([
@@ -0,0 +1,171 @@
1
+ """Tests for the infer command and schema inference (pointblank >=0.25)."""
2
+
3
+ import polars as pl
4
+ import pytest
5
+ import yaml
6
+
7
+ from fauxdata.commands.infer import (
8
+ _col_type_from_dtype,
9
+ _field_to_spec,
10
+ _build_validation,
11
+ )
12
+ from fauxdata.commands import infer as infer_mod
13
+ from fauxdata.schema import load_schema
14
+ from fauxdata.generator import generate_dataset
15
+ from fauxdata.validator import validate_dataset
16
+
17
+
18
+ # --- dtype mapping ---
19
+
20
+ @pytest.mark.parametrize("dtype,expected", [
21
+ ("Int64", "int"),
22
+ ("UInt32", "int"),
23
+ ("Float64", "float"),
24
+ ("String", "string"),
25
+ ("Utf8", "string"),
26
+ ("Boolean", "bool"),
27
+ ("Date", "date"),
28
+ ("Datetime(time_unit='us')", "datetime"),
29
+ ("SomethingWeird", "string"), # fallback
30
+ ])
31
+ def test_col_type_from_dtype(dtype, expected):
32
+ assert _col_type_from_dtype(dtype) == expected
33
+
34
+
35
+ # --- field -> spec ---
36
+
37
+ def _spec_of(df, col):
38
+ import pointblank as pb
39
+ s = pb.schema_from_tbl(df)
40
+ fields = dict(s.columns)
41
+ return _field_to_spec(fields[col])
42
+
43
+
44
+ def test_field_to_spec_int_range_unique():
45
+ df = pl.DataFrame({"id": [1, 2, 3, 4, 5]})
46
+ spec = _spec_of(df, "id")
47
+ assert spec["type"] == "int"
48
+ assert spec["min"] == 1
49
+ assert spec["max"] == 5
50
+ assert spec["unique"] is True
51
+
52
+
53
+ def test_field_to_spec_email_preset():
54
+ df = pl.DataFrame({"email": [f"u{i}@ex.com" for i in range(6)]})
55
+ spec = _spec_of(df, "email")
56
+ assert spec["type"] == "string"
57
+ assert spec.get("preset") == "email"
58
+
59
+
60
+ def test_field_to_spec_categorical_values():
61
+ df = pl.DataFrame({"status": ["a", "b", "a", "b", "a", "b"]})
62
+ spec = _spec_of(df, "status")
63
+ assert spec["type"] == "string"
64
+ assert sorted(spec["values"]) == ["a", "b"]
65
+
66
+
67
+ # --- validation derivation ---
68
+
69
+ def test_build_validation_rules():
70
+ columns = [
71
+ ("id", {"type": "int", "unique": True, "min": 1, "max": 9}),
72
+ ("status", {"type": "string", "values": ["a", "b"]}),
73
+ ("note", {"type": "string", "nullable": True}),
74
+ ]
75
+ rules = _build_validation(columns)
76
+ kinds = [r["rule"] for r in rules]
77
+ assert "col_vals_not_null" in kinds
78
+ assert "col_vals_between" in kinds
79
+ assert "col_vals_in_set" in kinds
80
+ assert "rows_distinct" in kinds
81
+ # nullable column excluded from not_null
82
+ nn = next(r for r in rules if r["rule"] == "col_vals_not_null")
83
+ assert "note" not in nn["columns"]
84
+ assert "id" in nn["columns"]
85
+
86
+
87
+ def test_build_validation_in_set_is_copy():
88
+ """values in the rule must not alias the column spec list (no YAML anchor)."""
89
+ vals = ["a", "b"]
90
+ columns = [("status", {"type": "string", "values": vals})]
91
+ rules = _build_validation(columns)
92
+ rule_vals = next(r for r in rules if r["rule"] == "col_vals_in_set")["values"]
93
+ assert rule_vals == vals
94
+ assert rule_vals is not vals
95
+
96
+
97
+ # --- end-to-end round trip ---
98
+
99
+ def test_infer_roundtrip(tmp_path):
100
+ real = pl.DataFrame({
101
+ "id": list(range(1, 9)),
102
+ "email": [f"u{i}@ex.com" for i in range(8)],
103
+ "age": [20, 35, 40, 55, 60, 25, 33, 48],
104
+ "status": ["active", "inactive"] * 4,
105
+ "price": [1.5, 2.0, 3.25, 4.0, 5.5, 2.2, 3.3, 4.4],
106
+ })
107
+ src = tmp_path / "real.csv"
108
+ real.write_csv(src)
109
+
110
+ out = tmp_path / "inferred.yml"
111
+ infer_mod.run(dataset_path=str(src), out=str(out))
112
+
113
+ assert out.exists()
114
+ data = yaml.safe_load(out.read_text())
115
+ assert set(data["columns"]) == {"id", "email", "age", "status", "price"}
116
+ assert data["columns"]["email"]["preset"] == "email"
117
+ assert data["rows"] == 8
118
+
119
+ # the inferred schema generates valid data that passes its own rules.
120
+ # Use the schema's own row count (calibrated to the source size): inferred
121
+ # unique columns over a narrow integer range cannot yield many more rows.
122
+ schema = load_schema(out)
123
+ df = generate_dataset(schema)
124
+ all_passed, _ = validate_dataset(df, schema)
125
+ assert all_passed
126
+
127
+
128
+ def test_infer_datetime_from_csv_typed(tmp_path):
129
+ """date/datetime CSV columns are inferred as typed, not frozen as categorical strings."""
130
+ import datetime
131
+ real = pl.DataFrame({
132
+ "ts": [datetime.datetime(2020, 1, 1, 8, 30) + datetime.timedelta(days=i * 40) for i in range(8)],
133
+ })
134
+ src = tmp_path / "dt.csv"
135
+ real.write_csv(src)
136
+ out = tmp_path / "dt.yml"
137
+ infer_mod.run(dataset_path=str(src), out=str(out))
138
+ data = yaml.safe_load(out.read_text())
139
+ assert data["columns"]["ts"]["type"] == "datetime"
140
+ # round-trips through generation
141
+ schema = load_schema(out)
142
+ assert len(generate_dataset(schema)) == schema.rows
143
+
144
+
145
+ def test_generate_unique_range_too_narrow_clear_error():
146
+ """Amplifying a unique column beyond its range raises a clear, named error."""
147
+ from fauxdata.schema import SchemaConfig, ColumnSchema
148
+ schema = SchemaConfig(
149
+ name="x", rows=5, seed=1,
150
+ columns=[ColumnSchema(name="id", col_type="int", min=1, max=5, unique=True)],
151
+ )
152
+ with pytest.raises(ValueError, match="too narrow a range"):
153
+ generate_dataset(schema, rows=100)
154
+
155
+
156
+ def test_infer_unsupported_format(tmp_path):
157
+ import typer
158
+ bad = tmp_path / "data.xyz"
159
+ bad.write_text("nope")
160
+ with pytest.raises(typer.Exit):
161
+ infer_mod.run(dataset_path=str(bad))
162
+
163
+
164
+ def test_infer_stdout(tmp_path, capsys):
165
+ real = pl.DataFrame({"id": [1, 2, 3]})
166
+ src = tmp_path / "r.csv"
167
+ real.write_csv(src)
168
+ infer_mod.run(dataset_path=str(src), out="-")
169
+ captured = capsys.readouterr()
170
+ assert "columns:" in captured.out
171
+ assert "name: r" in captured.out
@@ -167,3 +167,40 @@ columns:
167
167
  "--out", str(out), "--format", "csv"])
168
168
  assert result.exit_code == 0, result.output
169
169
  assert out.exists()
170
+
171
+
172
+ # --- min_length / max_length (string columns) ---
173
+
174
+ def test_min_max_length_parsing():
175
+ schema = _parse_schema({
176
+ "name": "x",
177
+ "columns": {"code": {"type": "string", "min_length": 4, "max_length": 8}},
178
+ })
179
+ col = schema.columns[0]
180
+ assert col.min_length == 4
181
+ assert col.max_length == 8
182
+
183
+
184
+ def test_min_length_negative_rejected():
185
+ with pytest.raises(ValueError, match="min_length"):
186
+ _parse_schema({
187
+ "name": "x",
188
+ "columns": {"code": {"type": "string", "min_length": -1}},
189
+ })
190
+
191
+
192
+ def test_min_length_gt_max_length_rejected():
193
+ with pytest.raises(ValueError, match="min_length"):
194
+ _parse_schema({
195
+ "name": "x",
196
+ "columns": {"code": {"type": "string", "min_length": 9, "max_length": 4}},
197
+ })
198
+
199
+
200
+ def test_generate_respects_string_length():
201
+ schema = SchemaConfig(
202
+ name="x", rows=10, seed=1,
203
+ columns=[ColumnSchema(name="code", col_type="string", min_length=6, max_length=6)],
204
+ )
205
+ df = generate_dataset(schema)
206
+ assert df["code"].str.len_chars().to_list() == [6] * 10
@@ -29,6 +29,76 @@ wheels = [
29
29
  { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
30
30
  ]
31
31
 
32
+ [[package]]
33
+ name = "cffi"
34
+ version = "2.0.0"
35
+ source = { registry = "https://pypi.org/simple" }
36
+ dependencies = [
37
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
38
+ ]
39
+ sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
40
+ wheels = [
41
+ { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
42
+ { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
43
+ { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
44
+ { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
45
+ { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
46
+ { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
47
+ { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
48
+ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
49
+ { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
50
+ { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
51
+ { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
52
+ { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
53
+ { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
54
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
55
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
56
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
57
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
58
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
59
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
60
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
61
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
62
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
63
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
64
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
65
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
66
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
67
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
68
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
69
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
70
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
71
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
72
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
73
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
74
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
75
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
76
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
77
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
78
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
79
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
80
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
81
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
82
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
83
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
84
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
85
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
86
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
87
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
88
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
89
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
90
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
91
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
92
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
93
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
94
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
95
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
96
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
97
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
98
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
99
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
100
+ ]
101
+
32
102
  [[package]]
33
103
  name = "charset-normalizer"
34
104
  version = "3.4.5"
@@ -250,7 +320,7 @@ wheels = [
250
320
 
251
321
  [[package]]
252
322
  name = "fauxdata-cli"
253
- version = "0.1.4"
323
+ version = "0.1.6"
254
324
  source = { editable = "." }
255
325
  dependencies = [
256
326
  { name = "pointblank" },
@@ -270,7 +340,7 @@ dev = [
270
340
 
271
341
  [package.metadata]
272
342
  requires-dist = [
273
- { name = "pointblank", specifier = ">=0.22" },
343
+ { name = "pointblank", specifier = ">=0.25" },
274
344
  { name = "polars", specifier = ">=1.0" },
275
345
  { name = "pyfiglet", specifier = ">=1.0" },
276
346
  { name = "pyyaml", specifier = ">=6.0" },
@@ -287,20 +357,21 @@ dev = [
287
357
 
288
358
  [[package]]
289
359
  name = "great-tables"
290
- version = "0.21.0"
360
+ version = "0.22.0"
291
361
  source = { registry = "https://pypi.org/simple" }
292
362
  dependencies = [
293
363
  { name = "babel" },
294
- { name = "commonmark" },
295
364
  { name = "faicons" },
296
365
  { name = "htmltools" },
297
366
  { name = "importlib-metadata" },
298
367
  { name = "importlib-resources" },
368
+ { name = "multimark" },
369
+ { name = "nokap" },
299
370
  { name = "typing-extensions" },
300
371
  ]
301
- sdist = { url = "https://files.pythonhosted.org/packages/4a/62/2f323fa5d907a741ad84db4cb4e9f8eb56fa2149a4f571e761951390d2cb/great_tables-0.21.0.tar.gz", hash = "sha256:40869cf972ea679710a0985c54c28e6787435e9b26ca295a8848ca5fbc02259a", size = 13002962, upload-time = "2026-03-03T20:25:28.93Z" }
372
+ sdist = { url = "https://files.pythonhosted.org/packages/fc/be/3e35429e915bc355cc69f522d79ccc122858ae038738ca3dcd8a7d51c854/great_tables-0.22.0.tar.gz", hash = "sha256:ccf031d90cab599328232bc32afadef215da1457dbc025d0972048cc2050fd57", size = 26110797, upload-time = "2026-06-12T01:07:28.678Z" }
302
373
  wheels = [
303
- { url = "https://files.pythonhosted.org/packages/78/24/cd32cb847edfa9b658bcb4647b3f4247c09ef7dc1f4f2cba5a620d266865/great_tables-0.21.0-py3-none-any.whl", hash = "sha256:d2f300f44cffb47d59e4dd4038d115633806b20275d39616e26f137627818b6b", size = 1405818, upload-time = "2026-03-03T20:25:27.21Z" },
374
+ { url = "https://files.pythonhosted.org/packages/85/46/0fd6ca17e3b40b657af79aaf50e2e54f736f4ad5585e201da702c52c7c2a/great_tables-0.22.0-py3-none-any.whl", hash = "sha256:7a7f81c2d1eb66d88166e864ac820966c23f04e4ecb41eeca8f9df1e1b21764d", size = 1455612, upload-time = "2026-06-12T01:07:26.815Z" },
304
375
  ]
305
376
 
306
377
  [[package]]
@@ -376,6 +447,33 @@ wheels = [
376
447
  { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
377
448
  ]
378
449
 
450
+ [[package]]
451
+ name = "multimark"
452
+ version = "0.2.0"
453
+ source = { registry = "https://pypi.org/simple" }
454
+ dependencies = [
455
+ { name = "cffi" },
456
+ { name = "click" },
457
+ ]
458
+ sdist = { url = "https://files.pythonhosted.org/packages/c2/6a/9e3029b89074f2ce705dd1a94eaf1372a49ac63ac47712a0f2410c574add/multimark-0.2.0.tar.gz", hash = "sha256:15a53111c52e0a65c200b57a5068a618734af52fc5d6512e8b9884fadbb2b8b8", size = 436111, upload-time = "2026-06-09T20:48:52.833Z" }
459
+ wheels = [
460
+ { url = "https://files.pythonhosted.org/packages/c2/65/c080aa324ea650b78c7396a26395b0e8bbc7ace40651c3c9929e5222da85/multimark-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:483f51c416fa8a83cfe430b020afcdd08fa8e33a1335851c8ec9f617f9c561c7", size = 132784, upload-time = "2026-06-09T20:48:33.057Z" },
461
+ { url = "https://files.pythonhosted.org/packages/8e/1e/d69bf6d93ad964e8d3efce6904c609eb4733f8bcfee33ceddb838a6e52df/multimark-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cf194a957dfeccb1d9d6cf38b787762c377895fdebbe9064533c6c1b13c65dd1", size = 130431, upload-time = "2026-06-09T20:48:33.928Z" },
462
+ { url = "https://files.pythonhosted.org/packages/dc/fc/65505a111b35dcc0fccac6fc880e7b1d957606c8ead1d7792d6d292f2090/multimark-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd2ec35a273afe6a8f3101b4db1f850488c1be1af9525be53150129baf956db", size = 450398, upload-time = "2026-06-09T20:48:34.836Z" },
463
+ { url = "https://files.pythonhosted.org/packages/00/42/346e9d7a49acde3240cde23492467c69b6f749ea932c0135dc490bc16cd4/multimark-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e05fe7c1badd7648e869eb5a65153d9d0b5dfbcdfaae795caaf0b77fbf92da73", size = 452478, upload-time = "2026-06-09T20:48:36.012Z" },
464
+ { url = "https://files.pythonhosted.org/packages/0d/2d/e93c35a182373a9fbf56b78464372f9f909271ac65d879af51c0a6bc4b45/multimark-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:54cecb32ad96ca8672e320adc537c462807ca0bf9a3a421b68fba93c9708d5b1", size = 124924, upload-time = "2026-06-09T20:48:37.34Z" },
465
+ { url = "https://files.pythonhosted.org/packages/c6/ee/b016667bcdb49630c8536146084d2726393c3123844021d18f559ac60145/multimark-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4be13eebff92579dfedda2d976e2d7cc6c4e1fd7d26c9b0a5a92de48f941146b", size = 132971, upload-time = "2026-06-09T20:48:38.477Z" },
466
+ { url = "https://files.pythonhosted.org/packages/8c/ed/be18fa30222de97e206e1e3d4e4bd425a3c21dfe22b5650a81e015548c80/multimark-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:77ce50f531536f97007e090895327552f1a4a9847129f34f5afb6d6f8e00bce7", size = 130481, upload-time = "2026-06-09T20:48:39.517Z" },
467
+ { url = "https://files.pythonhosted.org/packages/33/50/c5fbecf55ff3c05133d062653b22e63a14fa1a9bc890ac759d8826a48f36/multimark-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f093afd5696e58207bbc10854005f9f0ef4e34874437d266e53ed1b819f2cff", size = 451011, upload-time = "2026-06-09T20:48:40.531Z" },
468
+ { url = "https://files.pythonhosted.org/packages/90/be/ae21b6d68eb6674086a60720c4d54367f4ef2ce963f33177f3710bdac9f6/multimark-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4498fea15ca97c578982f349d2521cd45895409e9d000f285e410ff0b82915af", size = 452975, upload-time = "2026-06-09T20:48:41.615Z" },
469
+ { url = "https://files.pythonhosted.org/packages/28/85/0f361fa06cfaf62758fefef2d11141bbc58672a2c9d625674891b93fc23b/multimark-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ce9daae00ddc4f0f0cf07482d71b4f2bc95fac5aa81e8b3084252a8a23427dc3", size = 124934, upload-time = "2026-06-09T20:48:42.696Z" },
470
+ { url = "https://files.pythonhosted.org/packages/36/75/0759e525cd4b53eeaa6b53b819d630fd2b1c5b73e6561e6b83a16a3ea4fc/multimark-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:390941f897850a4347d61fb4d8890810adc0c964a7d4565fc77c4cf30c0b5ea0", size = 132975, upload-time = "2026-06-09T20:48:43.567Z" },
471
+ { url = "https://files.pythonhosted.org/packages/e4/f7/33f0c8b6814b2e034956df0bdb971fb64d88277fd1ddbf71ea5e135823f7/multimark-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a8527fe99aa56089228fe132540398c908e179a2a2259f4da006d5f6aab7217", size = 130476, upload-time = "2026-06-09T20:48:44.49Z" },
472
+ { url = "https://files.pythonhosted.org/packages/fd/08/e6b1816b4ca68bf20a72f4784d924b12fe96083c0edbd4dc2e52e1795920/multimark-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b0bb3060b3a4b050bd1c6c75bbcbc8d0e705a70af73d3269c9637a2f4e08123", size = 450977, upload-time = "2026-06-09T20:48:45.341Z" },
473
+ { url = "https://files.pythonhosted.org/packages/c4/4b/a5acdb28bc9e8ee638d8af74938780188efd5ae0102928ee33cd508cba72/multimark-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b5542fee4d73498e3bc1055375a1370d6ca72916804a7c8484fa4409fd635b6", size = 452983, upload-time = "2026-06-09T20:48:46.322Z" },
474
+ { url = "https://files.pythonhosted.org/packages/7f/65/8c550c71d1dc79ba04de57177a11e809b8a27ac4e56726ec487dccd68c95/multimark-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:c3ff54935aa0724390955c12a91d85d4e04664c2c8da147714226a4ac905b387", size = 124938, upload-time = "2026-06-09T20:48:47.507Z" },
475
+ ]
476
+
379
477
  [[package]]
380
478
  name = "narwhals"
381
479
  version = "2.17.0"
@@ -385,6 +483,19 @@ wheels = [
385
483
  { url = "https://files.pythonhosted.org/packages/4b/27/20770bd6bf8fbe1e16f848ba21da9df061f38d2e6483952c29d2bb5d1d8b/narwhals-2.17.0-py3-none-any.whl", hash = "sha256:2ac5307b7c2b275a7d66eeda906b8605e3d7a760951e188dcfff86e8ebe083dd", size = 444897, upload-time = "2026-02-23T09:44:32.006Z" },
386
484
  ]
387
485
 
486
+ [[package]]
487
+ name = "nokap"
488
+ version = "0.1.0"
489
+ source = { registry = "https://pypi.org/simple" }
490
+ dependencies = [
491
+ { name = "click" },
492
+ { name = "websockets" },
493
+ ]
494
+ sdist = { url = "https://files.pythonhosted.org/packages/b9/f7/a4f3d6f89773df40884bc10790977c80a9ea17ad526ea0b0cd35a703595c/nokap-0.1.0.tar.gz", hash = "sha256:63071cf5e7c4f0487532f6281c6ea85d90673d5cd835ffe5e64c913989742eb9", size = 77047, upload-time = "2026-06-08T23:19:17.959Z" }
495
+ wheels = [
496
+ { url = "https://files.pythonhosted.org/packages/73/dc/c35af5ccf8dbf3268750c54a3c0830f486b8faec38c811aff92335aa99ba/nokap-0.1.0-py3-none-any.whl", hash = "sha256:36f2cae8d4251945dbe311ef68916c7eddfe51da490aabe0d68c5d13294b2539", size = 36233, upload-time = "2026-06-08T23:19:16.725Z" },
497
+ ]
498
+
388
499
  [[package]]
389
500
  name = "packaging"
390
501
  version = "26.0"
@@ -405,7 +516,7 @@ wheels = [
405
516
 
406
517
  [[package]]
407
518
  name = "pointblank"
408
- version = "0.22.0"
519
+ version = "0.25.0"
409
520
  source = { registry = "https://pypi.org/simple" }
410
521
  dependencies = [
411
522
  { name = "click" },
@@ -418,9 +529,9 @@ dependencies = [
418
529
  { name = "rich" },
419
530
  { name = "typing-extensions" },
420
531
  ]
421
- sdist = { url = "https://files.pythonhosted.org/packages/ec/9c/b52c357956f51db8aa5065261cb7741b2f8f70955f704da269b1734b7692/pointblank-0.22.0.tar.gz", hash = "sha256:f98d8528124335f9cfc8b0c3e836ba0351a2268a67a4d5cc7f02bc267246c4a6", size = 62742390, upload-time = "2026-02-26T15:51:45.446Z" }
532
+ sdist = { url = "https://files.pythonhosted.org/packages/1a/fe/ff52d7657fe7141d39ae8588e42eb0ea93f90e06ee296ce21a154c0bced2/pointblank-0.25.0.tar.gz", hash = "sha256:131909470d2734595b8ce4ea56ddf7634be5b64b9e18d78d008849109a5ca55f", size = 43398380, upload-time = "2026-06-12T16:52:38.28Z" }
422
533
  wheels = [
423
- { url = "https://files.pythonhosted.org/packages/5c/03/9c53e3ce83c7d5a74fba490d4ca47030c09d5eabb1fc2c3f78e35600538e/pointblank-0.22.0-py3-none-any.whl", hash = "sha256:a7b03862b41e799b387c894665da1de091be9de7d4f6db4fa66b66a078cd6b7b", size = 18961667, upload-time = "2026-02-26T15:51:42.25Z" },
534
+ { url = "https://files.pythonhosted.org/packages/e8/7c/43ffba3f120c1c5ea2d00c13df1d9ecc24be7dde22e75262ce6ec0b094f0/pointblank-0.25.0-py3-none-any.whl", hash = "sha256:3998f6b9c3fd61b0aac487466969dbbc79e22bcd463c2da476439a9f5ad235c5", size = 19060424, upload-time = "2026-06-12T16:52:35.543Z" },
424
535
  ]
425
536
 
426
537
  [[package]]
@@ -463,6 +574,15 @@ wheels = [
463
574
  { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
464
575
  ]
465
576
 
577
+ [[package]]
578
+ name = "pycparser"
579
+ version = "3.0"
580
+ source = { registry = "https://pypi.org/simple" }
581
+ sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
582
+ wheels = [
583
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
584
+ ]
585
+
466
586
  [[package]]
467
587
  name = "pyfiglet"
468
588
  version = "1.0.4"
@@ -711,6 +831,65 @@ wheels = [
711
831
  { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" },
712
832
  ]
713
833
 
834
+ [[package]]
835
+ name = "websockets"
836
+ version = "16.0"
837
+ source = { registry = "https://pypi.org/simple" }
838
+ sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
839
+ wheels = [
840
+ { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" },
841
+ { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" },
842
+ { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" },
843
+ { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" },
844
+ { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" },
845
+ { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" },
846
+ { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" },
847
+ { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" },
848
+ { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" },
849
+ { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" },
850
+ { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" },
851
+ { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" },
852
+ { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" },
853
+ { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" },
854
+ { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" },
855
+ { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" },
856
+ { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" },
857
+ { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" },
858
+ { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" },
859
+ { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" },
860
+ { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" },
861
+ { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" },
862
+ { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" },
863
+ { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" },
864
+ { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" },
865
+ { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" },
866
+ { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" },
867
+ { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
868
+ { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
869
+ { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
870
+ { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" },
871
+ { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" },
872
+ { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" },
873
+ { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" },
874
+ { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" },
875
+ { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" },
876
+ { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" },
877
+ { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" },
878
+ { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" },
879
+ { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" },
880
+ { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" },
881
+ { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" },
882
+ { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
883
+ { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
884
+ { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
885
+ { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" },
886
+ { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" },
887
+ { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" },
888
+ { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" },
889
+ { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" },
890
+ { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
891
+ ]
892
+
714
893
  [[package]]
715
894
  name = "zipp"
716
895
  version = "3.23.0"
Binary file
@@ -1,38 +0,0 @@
1
- # Agent-friendly CLI improvements
2
-
3
- ## Obiettivo
4
- Applicare i principi per CLI usabili dagli agenti AI.
5
-
6
- ## Fase 1 — Non-interactive `init`
7
-
8
- - [ ] Aggiungere `--description`, `--rows`, `--format`, `--yes` come flag a `init_cmd` in `main.py`
9
- - [ ] Aggiornare `run()` in `commands/init.py` per accettare questi parametri
10
- - [ ] Rendere i prompt questionary fallback (solo quando il flag è None)
11
- - [ ] `--yes` salta la conferma di sovrascrittura
12
-
13
- ## Fase 2 — Esempi in `--help`
14
-
15
- - [ ] Aggiungere `epilog` con esempi a ogni comando in `main.py` (init, generate, validate, preview)
16
-
17
- ## Fase 3 — `--dry-run` per `generate`
18
-
19
- - [ ] Aggiungere flag `--dry-run` a `generate_cmd` in `main.py`
20
- - [ ] Implementare dry-run in `commands/generate.py`: mostra cosa farebbe senza scrivere file
21
-
22
- ## Fase 4 — Output strutturato su successo
23
-
24
- - [ ] `generate`: output su successo mostra chiave=valore (output_path, format, rows)
25
- - [ ] `init`: output su successo mostra schema_path
26
-
27
- ## Domande aperte
28
-
29
- - Aggiungere anche `--quiet` / `--json` per output machine-readable?
30
- - `validate` e `preview` hanno già flag, sufficienti?
31
-
32
- ## Review
33
-
34
- - Tutte e 4 le fasi completate; 79/79 test passano, coverage 82%
35
- - `init` ora fully non-interactive con `--name --description --rows --format --yes`
36
- - `generate` ha `--dry-run` e output strutturato chiave: valore
37
- - Ogni comando ha esempi nel `--help` via `epilog`
38
- - Venv ricreato con `--clear` (cartella rinominata da `real_fake_datasets`)
File without changes
File without changes