Skip to content

Commit a4781f4

Browse files
committed
Fixed #417 - Make transform works with json file
* Update Changelog * Create functions to work on `transfrom` from JSON to JSON * Add code to validate the extension for both input and output are the same * Update test `configuration` to increase converage * Added/Updated test code
1 parent a1c18fe commit a4781f4

File tree

10 files changed

+169
-29
lines changed

10 files changed

+169
-29
lines changed

docs/CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Release 4.0.2
33

44
* Upgrade license-expression library to v1.2
5+
* Enhance the `transform` to also work with JSON file
56

67

78
2019-10-17

src/attributecode/cmd.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -401,17 +401,17 @@ def print_config_help(ctx, param, value):
401401

402402

403403
@about.command(cls=AboutCommand,
404-
short_help='Transform a CSV by applying renamings, filters and checks.')
404+
short_help='Transform a CSV/JSON by applying renamings, filters and checks.')
405405

406406
@click.argument('location',
407407
required=True,
408-
callback=partial(validate_extensions, extensions=('.csv',)),
408+
callback=partial(validate_extensions, extensions=('.csv', '.json',)),
409409
metavar='LOCATION',
410410
type=click.Path(exists=True, dir_okay=False, readable=True, resolve_path=True))
411411

412412
@click.argument('output',
413413
required=True,
414-
callback=partial(validate_extensions, extensions=('.csv',)),
414+
callback=partial(validate_extensions, extensions=('.csv', '.json',)),
415415
metavar='OUTPUT',
416416
type=click.Path(exists=False, dir_okay=False, writable=True, resolve_path=True))
417417

@@ -438,30 +438,39 @@ def print_config_help(ctx, param, value):
438438

439439
def transform(location, output, configuration, quiet, verbose): # NOQA
440440
"""
441-
Transform the CSV file at LOCATION by applying renamings, filters and checks
442-
and write a new CSV to OUTPUT.
441+
Transform the CSV/JSON file at LOCATION by applying renamings, filters and checks
442+
and write a new CSV/JSON to OUTPUT.
443443
444-
LOCATION: Path to a CSV file.
444+
LOCATION: Path to a CSV/JSON file.
445445
446-
OUTPUT: Path to CSV inventory file to create.
446+
OUTPUT: Path to CSV/JSON inventory file to create.
447447
"""
448448
from attributecode.transform import transform_csv_to_csv
449+
from attributecode.transform import transform_json_to_json
449450
from attributecode.transform import Transformer
450451

451-
if not quiet:
452-
print_version()
453-
click.echo('Transforming CSV...')
454452

455453
if not configuration:
456454
transformer = Transformer.default()
457455
else:
458456
transformer = Transformer.from_file(configuration)
459457

460-
errors = transform_csv_to_csv(location, output, transformer)
458+
if location.endswith('.csv') and output.endswith('.csv'):
459+
errors = transform_csv_to_csv(location, output, transformer)
460+
elif location.endswith('.json') and output.endswith('.json'):
461+
errors = transform_json_to_json(location, output, transformer)
462+
else:
463+
msg = 'Extension for the input and output need to be the same.'
464+
click.echo(msg)
465+
sys.exit()
466+
467+
if not quiet:
468+
print_version()
469+
click.echo('Transforming...')
461470

462471
errors_count = report_errors(errors, quiet, verbose, log_file_loc=output + '-error.log')
463472
if not quiet and not errors:
464-
msg = 'Transformed CSV written to {output}.'.format(**locals())
473+
msg = 'Transformed file written to {output}.'.format(**locals())
465474
click.echo(msg)
466475
sys.exit(errors_count)
467476

src/attributecode/transform.py

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from collections import Counter
2121
from collections import OrderedDict
2222
import io
23+
import json
2324

2425
import attr
2526

@@ -40,27 +41,46 @@
4041
def transform_csv_to_csv(location, output, transformer):
4142
"""
4243
Read a CSV file at `location` and write a new CSV file at `output`. Apply
43-
transformations using the `transformer` Tranformer.
44+
transformations using the `transformer` Transformer.
4445
Return a list of Error objects.
4546
"""
4647
if not transformer:
4748
raise ValueError('Cannot transform without Transformer')
4849

4950
rows = read_csv_rows(location)
5051

51-
column_names, data, errors = transform_data(rows, transformer)
52+
column_names, data, errors = transform_csv(rows, transformer)
5253

5354
if errors:
5455
return errors
5556
else:
5657
write_csv(output, data, column_names)
5758
return []
5859

60+
def transform_json_to_json(location, output, transformer):
61+
"""
62+
Read a JSON file at `location` and write a new JSON file at `output`. Apply
63+
transformations using the `transformer` Transformer.
64+
Return a list of Error objects.
65+
"""
66+
if not transformer:
67+
raise ValueError('Cannot transform without Transformer')
5968

60-
def transform_data(rows, transformer):
69+
data = read_json(location)
70+
71+
new_data, errors = transform_json(data, transformer)
72+
73+
if errors:
74+
return errors
75+
else:
76+
write_json(output, new_data)
77+
return []
78+
79+
80+
def transform_csv(rows, transformer):
6181
"""
6282
Read a list of list of CSV-like data `rows` and apply transformations using the
63-
`transformer` Tranformer.
83+
`transformer` Transformer.
6484
Return a tuple of:
6585
([column names...], [transformed ordered dict...], [Error objects..])
6686
"""
@@ -90,12 +110,54 @@ def transform_data(rows, transformer):
90110
column_names = [c for c in column_names if c in transformer.column_filters]
91111

92112
errors = transformer.check_required_columns(data)
93-
if errors:
94-
return column_names, data, errors
95113

96114
return column_names, data, errors
97115

98116

117+
def transform_json(data, transformer):
118+
"""
119+
Read a dictionary and apply transformations using the
120+
`transformer` Transformer.
121+
Return a new list of dictionary.
122+
"""
123+
124+
if not transformer:
125+
return data
126+
127+
errors = []
128+
new_data = []
129+
renamings = transformer.column_renamings
130+
if isinstance(data, list):
131+
for item in data:
132+
element, err = process_json_keys(item, renamings, transformer)
133+
for e in element:
134+
new_data.append(e)
135+
for e in err:
136+
errors.append(e)
137+
else:
138+
new_data, errors = process_json_keys(data, renamings, transformer)
139+
140+
return new_data, errors
141+
142+
143+
def process_json_keys(data, renamings, transformer):
144+
o_dict = OrderedDict()
145+
for k in data.keys():
146+
if k in renamings.keys():
147+
for r_key in renamings.keys():
148+
if k == r_key:
149+
o_dict[renamings[r_key]] = data[k]
150+
else:
151+
o_dict[k] = data[k]
152+
new_data = [o_dict]
153+
154+
if transformer.column_filters:
155+
new_data = list(transformer.filter_columns(new_data))
156+
157+
errors = transformer.check_required_columns(new_data)
158+
return new_data, errors
159+
160+
99161
tranformer_config_help = '''
100162
A transform configuration file is used to describe which transformations and
101163
validations to apply to a source CSV file. This is a simple text file using YAML
@@ -266,6 +328,15 @@ def read_csv_rows(location):
266328
yield row
267329

268330

331+
def read_json(location):
332+
"""
333+
Yield rows (as a list of values) from a CSV file at `location`.
334+
"""
335+
with io.open(location, encoding='utf-8', errors='replace') as jsonfile:
336+
data = json.load(jsonfile, object_pairs_hook=OrderedDict)
337+
return data
338+
339+
269340
def write_csv(location, data, column_names): # NOQA
270341
"""
271342
Write a CSV file at `location` the `data` list of ordered dicts using the
@@ -275,3 +346,11 @@ def write_csv(location, data, column_names): # NOQA
275346
writer = csv.DictWriter(csvfile, fieldnames=column_names)
276347
writer.writeheader()
277348
writer.writerows(data)
349+
350+
351+
def write_json(location, data):
352+
"""
353+
Write a JSON file at `location` the `data` list of ordered dicts.
354+
"""
355+
with open(location, 'w') as jsonfile:
356+
json.dump(data, jsonfile, indent=3)

tests/test_transform.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
from attributecode import Error
3131
from attributecode import gen
3232
from attributecode.transform import read_csv_rows
33-
from attributecode.transform import transform_data
33+
from attributecode.transform import read_json
34+
from attributecode.transform import transform_csv
35+
from attributecode.transform import transform_json
3436
from attributecode.transform import Transformer
3537

3638

@@ -40,6 +42,30 @@ def test_transform_data(self):
4042
configuration = get_test_loc('test_transform/configuration')
4143
rows = read_csv_rows(test_file)
4244
transformer = Transformer.from_file(configuration)
43-
col_name, data, err = transform_data(rows, transformer)
44-
expect = [u'about_resource', u'name']
45+
col_name, data, err = transform_csv(rows, transformer)
46+
expect = [u'about_resource', u'name', u'version']
4547
assert col_name == expect
48+
49+
def test_transform_data_json(self):
50+
test_file = get_test_loc('test_transform/input.json')
51+
configuration = get_test_loc('test_transform/configuration')
52+
json_data = read_json(test_file)
53+
transformer = Transformer.from_file(configuration)
54+
data, err = transform_json(json_data, transformer)
55+
keys = []
56+
for item in data:
57+
keys = item.keys()
58+
expect = [u'about_resource', u'name', u'version']
59+
assert keys == expect
60+
61+
def test_transform_data_json_as_array(self):
62+
test_file = get_test_loc('test_transform/input_as_array.json')
63+
configuration = get_test_loc('test_transform/configuration')
64+
json_data = read_json(test_file)
65+
transformer = Transformer.from_file(configuration)
66+
data, err = transform_json(json_data, transformer)
67+
keys = []
68+
for item in data:
69+
keys = item.keys()
70+
expect = [u'about_resource', u'name', u'version']
71+
assert keys == expect

tests/testdata/test_cmd/help/about_help.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ Commands:
1818
errors and warnings.
1919
gen Generate .ABOUT files from an inventory as CSV or JSON.
2020
inventory Collect the inventory of .ABOUT files to a CSV or JSON file.
21-
transform Transform a CSV by applying renamings, filters and checks.
21+
transform Transform a CSV/JSON by applying renamings, filters and checks.

tests/testdata/test_cmd/help/about_transform_help.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
Usage: about transform [OPTIONS] LOCATION OUTPUT
22

3-
Transform the CSV file at LOCATION by applying renamings, filters and checks
4-
and write a new CSV to OUTPUT.
3+
Transform the CSV/JSON file at LOCATION by applying renamings, filters and
4+
checks and write a new CSV/JSON to OUTPUT.
55

6-
LOCATION: Path to a CSV file.
6+
LOCATION: Path to a CSV/JSON file.
77

8-
OUTPUT: Path to CSV inventory file to create.
8+
OUTPUT: Path to CSV/JSON inventory file to create.
99

1010
Options:
1111
-c, --configuration FILE Path to an optional YAML configuration file. See
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
column_renamings:
22
'Directory/Filename' : about_resource
3-
Component: name
3+
Component: name
4+
column_filters:
5+
- about_resource
6+
- name
7+
- version
8+
required_columns:
9+
- name
10+
- version
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
Directory/Filename,Component
2-
/tmp/test.c, test,c
1+
Directory/Filename,Component,version,notes
2+
/tmp/test.c, test.c,1,test
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"Directory/Filename": "/aboutcode-toolkit/",
3+
"Component": "AboutCode-toolkit",
4+
"version": "1.2.3",
5+
"note": "test"
6+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"Directory/Filename": "/aboutcode-toolkit/",
4+
"Component": "AboutCode-toolkit",
5+
"version": "1.0"
6+
},
7+
{
8+
"Directory/Filename": "/aboutcode-toolkit1/",
9+
"Component": "AboutCode-toolkit1",
10+
"version": "1.1"
11+
}
12+
]

0 commit comments

Comments
 (0)