Skip to content

Commit ebdd86f

Browse files
committed
Merge branch 'main' of github.com:dacort/faker-cli into main
2 parents 66844be + 5969e16 commit ebdd86f

5 files changed

Lines changed: 113 additions & 26 deletions

File tree

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ fake -n 10 pyint,user_name,date_this_year -f json
6666
{"pyint": 5306, "user_name": "mark12", "date_this_year": "2023-04-16"}
6767
```
6868

69+
### Column Names
70+
6971
Default column names aren't good enough for you? Fine, use your own.
7072

7173
```bash
@@ -85,9 +87,23 @@ fake -n 10 pyint,user_name,date_this_year -f json -c id,awesome_name,last_attent
8587
{"id": 1967, "awesome_name": "jmendoza", "last_attention_at": "2023-01-23"}
8688
```
8789

90+
### Provider Arguments
91+
92+
Some [Faker providers](https://faker.readthedocs.io/en/master/providers/baseprovider.html) (like `pyint`) take arguments. You can also specify those if you like, separated by semi-colons (_because some arguments take a comma-separated string :)_)
93+
94+
```bash
95+
fake -n 10 "pyint(1;100),credit_card_number(amex),pystr_format(?#-####)" -f json -c id,credit_card_number,license_plate
96+
```
97+
98+
And unique values are supported as well.
99+
100+
```bash
101+
fake -n 10 "unique.pyint(1;10),unique.name"
102+
```
103+
88104
### Parquet
89105

90-
OK, it had to happen, you can even write Parquet.
106+
OK, it had to happen, you can even write Parquet.
91107

92108
Install with the `parquet` module: `pip install faker-cli[parquet]`
93109

@@ -111,7 +127,6 @@ Install with the `delta` module: `pip install faker-cli[delta]`
111127
fake -n 10 pyint,user_name,date_this_year -f deltalake -o sample_data
112128
```
113129

114-
115130
## Templates
116131

117132
The libary includes a couple templates that can be used to generate certain types of fake data easier.
@@ -130,4 +145,4 @@ How about CloudFront? Go ahead.
130145
fake -t cloudfront -n 10
131146
```
132147

133-
> **Warning**: Both of these templates are still being validated - please be cautious!
148+
> **Warning**: Both of these templates are still being validated - please be cautious!

faker_cli/cli.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import sys
2-
from typing import List
32

43
import click
54
from faker import Faker
65

6+
from faker_cli.parser import infer_column_names, parse_column_types
77
from faker_cli.templates import (
88
CloudFrontLogs,
99
CloudFrontWriter,
@@ -12,18 +12,6 @@
1212
)
1313
from faker_cli.writer import CSVWriter, JSONWriter
1414

15-
16-
def infer_column_names(col_names, col_types: str) -> List[str]:
17-
"""
18-
Infer column names from column types
19-
"""
20-
# For now, nothing special - but eventually we need to parse things out
21-
if col_names:
22-
return col_names.split(",")
23-
24-
return col_types.split(",")
25-
26-
2715
KLAS_MAPPER = {
2816
"csv": CSVWriter,
2917
"json": JSONWriter,
@@ -108,11 +96,22 @@ def main(num_rows, format, output, columns, template, column_types):
10896
return
10997

11098
# Now, if a template hasn't been provided, generate some fake data!
111-
col_types = column_types.split(",")
99+
# col_types = column_types.split(",")
100+
# Note that if args are provided, the column headers are less than ideal
101+
col_types = parse_column_types(column_types)
112102
headers = infer_column_names(columns, column_types)
113-
writer = KLAS_MAPPER.get(format)(sys.stdout, headers, output)
103+
format_klas = KLAS_MAPPER.get(format)
104+
if format_klas is None:
105+
raise click.ClickException(f"Format {format} not supported.")
106+
writer = format_klas(sys.stdout, headers, output)
114107
for i in range(num_rows):
115-
# TODO: Handle args
116-
row = [fake.format(ctype) for ctype in col_types]
117-
writer.write(row)
108+
writer.write(generate_row(fake, col_types))
118109
writer.close()
110+
111+
def generate_row(fake: Faker, column_types: list[tuple[str, list]]) -> list[str]:
112+
return [
113+
fake.format(ctype, *args)
114+
if not ctype.startswith("unique.")
115+
else fake.unique.format(ctype.removeprefix("unique."), *args)
116+
for ctype, args in column_types
117+
]

faker_cli/parser.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from typing import List, Tuple
2+
3+
def infer_column_names(col_names, col_types: str) -> List[str]:
4+
"""
5+
Infer column names from column types
6+
"""
7+
# For now, nothing special - but eventually we need to parse things out
8+
if col_names:
9+
return col_names.split(",")
10+
11+
return col_types.split(",")
12+
13+
14+
def parse_column_types(input_string: str) -> List[Tuple[str, List]]:
15+
"""
16+
Parse a string of the format "pyint(1;10),datetime,profile(ssn,birthdate)" and split it by commas with optional parenthese-inclosed arguments.
17+
"""
18+
import re
19+
20+
pattern = r"([\w\.]+)(\([^)]*\))?"
21+
matches = re.findall(pattern, input_string)
22+
23+
# Extract the matched groups
24+
result = [
25+
(match[0], ([try_convert_to_int(val) for val in match[1].strip("()").split(";")] if match[1] else []))
26+
for match in matches
27+
]
28+
29+
# print(result)
30+
# [('pyint', ['1', '10']), ('datetime', []), ('profile', ['ssn,birthdate'])]
31+
32+
return result
33+
34+
35+
def try_convert_to_int(s):
36+
try:
37+
return int(s)
38+
except ValueError:
39+
return s

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "faker-cli"
3-
version = "0.4.0"
3+
version = "0.5.0"
44
description = "Command-line fake data generator"
55
authors = ["Damon P. Cortesi <d.lifehacker@gmail.com>"]
66
readme = "README.md"

tests/test_cli.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
from faker_cli.cli import main
2-
from click.testing import CliRunner
31
import json
2+
43
import deltalake
4+
from click.testing import CliRunner
5+
6+
from faker_cli.cli import main
57

68

79
# Test that help is provided if the user provides no arguments
@@ -44,6 +46,7 @@ def test_numlines():
4446
lines = result.output.strip().splitlines()
4547
assert len(lines) == (6 if format == "csv" else 5)
4648

49+
4750
def test_custom_column_names():
4851
runner = CliRunner()
4952
result = runner.invoke(main, ["pyint,user_name", "-f", "json", "-c", "first,second"])
@@ -53,9 +56,10 @@ def test_custom_column_names():
5356
assert len(data.keys()) == 2
5457
assert list(data) == ["first", "second"]
5558

59+
5660
def test_deltalake_output(tmp_path):
5761
runner = CliRunner()
58-
file = tmp_path / 'table'
62+
file = tmp_path / "table"
5963
result = runner.invoke(main, ["pyint,user_name", "-f", "deltalake", "-o", file])
6064
assert result.exit_code == 0
6165
delta_table = deltalake.DeltaTable(file)
@@ -65,4 +69,34 @@ def test_deltalake_output(tmp_path):
6569

6670
column_names = arrow_table.column_names
6771
assert column_names == ["pyint", "user_name"]
68-
assert arrow_table.num_columns == 2
72+
assert arrow_table.num_columns == 2
73+
74+
75+
def test_provider_args():
76+
runner = CliRunner()
77+
result = runner.invoke(main, ["-n", "10", "pyint(1;10)", "-f", "json", "-c", "id"])
78+
assert result.exit_code == 0
79+
lines = result.output.strip().splitlines()
80+
for line in lines:
81+
data: dict = json.loads(line)
82+
assert data["id"] in range(1, 11)
83+
84+
85+
def test_unique_provider_args():
86+
runner = CliRunner()
87+
result = runner.invoke(main, ["-n", "10", "unique.pyint(1;10)", "-f", "json", "-c", "id"])
88+
assert result.exit_code == 0
89+
lines = result.output.strip().splitlines()
90+
vals = []
91+
for line in lines:
92+
data: dict = json.loads(line)
93+
assert data["id"] in range(1, 11)
94+
vals.append(data["id"])
95+
96+
assert set(vals) == set(range(1, 11))
97+
98+
99+
def test_unique_provider_args_limit():
100+
runner = CliRunner()
101+
result = runner.invoke(main, ["-n", "10", "unique.pyint(1;5)", "-f", "json", "-c", "id"])
102+
assert result.exit_code == 1

0 commit comments

Comments
 (0)