Skip to content

Commit 6b1a7da

Browse files
Feature/output formats (#128)
* Added support for multiple output formats, ascii-table, csv, formatted-json, json * Added tests * Reverts dependency of texttable module, uses existing table logic instead Refactored find_format_method to return tabular format output instead of printing it. * Added format options to command secuirty-data list and legal-hold list and show subcommands * Set newline to default value * Filter json results * refactor- proper naming convention * Added changelog * Fix failing test * Added test for commands with -f option * imporvise docs * Fix 3.5 test failure
1 parent a63c4f1 commit 6b1a7da

11 files changed

Lines changed: 385 additions & 22 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
3535
- `--password` option added to `profile create` and `profile update` commands, enabling creating profiles while bypassing the interactive password prompt.
3636
- Profiles can now save multiple alert and file event checkpoints. The name of the checkpoint to be used for a given query should be passed to `-c` (`--use-checkpoint`).
3737
- `-y/--assume-yes` option added to `profile delete` and `profile delete-all` commands to not require interactive prompt.
38+
- Below subcommands accept argument `--format/-f` to display result in formats `csv`, `table`, `json`, `formatted-json`:
39+
- `code42 alert-rules list`
40+
- `code42 legal-hold list`
41+
- `code42 legal-hold show`
42+
- `code42 security-data saved-search list`
3843

3944
### Removed
4045

src/code42cli/cmds/alert_rules.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
from code42cli.file_readers import read_csv_arg
1515
from code42cli.options import OrderedGroup
1616
from code42cli.options import sdk_options
17-
from code42cli.util import find_format_width
18-
from code42cli.util import format_to_table
17+
from code42cli.output_formats import format_option
1918

2019

2120
class AlertRuleTypes:
@@ -75,13 +74,14 @@ def remove_user(state, rule_id, username):
7574

7675

7776
@alert_rules.command("list")
77+
@format_option
7878
@sdk_options()
79-
def list_alert_rules(state):
79+
def list_alert_rules(state, format=None):
8080
"""Fetch existing alert rules."""
8181
selected_rules = _get_all_rules_metadata(state.sdk)
8282
if selected_rules:
83-
rows, column_size = find_format_width(selected_rules, _HEADER_KEYS_MAP)
84-
format_to_table(rows, column_size)
83+
formatted_output = format(selected_rules, _HEADER_KEYS_MAP)
84+
echo(formatted_output)
8585

8686

8787
@alert_rules.command()

src/code42cli/cmds/enums.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class OutputFormat:
2+
TABLE = "TABLE"
3+
CSV = "CSV"
4+
JSON = "JSON"
5+
RAW = "RAW-JSON"
6+
7+
def __iter__(self):
8+
return iter([self.TABLE, self.CSV, self.JSON, self.RAW])

src/code42cli/cmds/legal_hold.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
from code42cli.file_readers import read_csv_arg
1818
from code42cli.options import OrderedGroup
1919
from code42cli.options import sdk_options
20-
from code42cli.util import find_format_width
20+
from code42cli.output_formats import format_option
2121
from code42cli.util import format_string_list_to_columns
22-
from code42cli.util import format_to_table
22+
2323

2424
_MATTER_KEYS_MAP = OrderedDict()
2525
_MATTER_KEYS_MAP["legalHoldUid"] = "Matter ID"
@@ -71,21 +71,23 @@ def remove_user(state, matter_id, username):
7171

7272

7373
@legal_hold.command("list")
74+
@format_option
7475
@sdk_options()
75-
def _list(state):
76+
def _list(state, format=None):
7677
"""Fetch existing legal hold matters."""
7778
matters = _get_all_active_matters(state.sdk)
7879
if matters:
79-
rows, column_size = find_format_width(matters, _MATTER_KEYS_MAP)
80-
format_to_table(rows, column_size)
80+
output = format(matters, _MATTER_KEYS_MAP)
81+
echo(output)
8182

8283

8384
@legal_hold.command()
8485
@click.argument("matter-id")
8586
@click.option("--include-inactive", is_flag=True)
8687
@click.option("--include-policy", is_flag=True)
88+
@format_option
8789
@sdk_options()
88-
def show(state, matter_id, include_inactive=False, include_policy=False):
90+
def show(state, matter_id, include_inactive=False, include_policy=False, format=None):
8991
"""Display details of a given legal hold matter."""
9092
matter = _check_matter_is_accessible(state.sdk, matter_id)
9193
matter["creator_username"] = matter["creator"]["username"]
@@ -103,10 +105,9 @@ def show(state, matter_id, include_inactive=False, include_policy=False):
103105
member["user"]["username"] for member in memberships if not member["active"]
104106
]
105107

106-
rows, column_size = find_format_width([matter], _MATTER_KEYS_MAP)
108+
output = format([matter], _MATTER_KEYS_MAP)
109+
echo(output)
107110

108-
echo("")
109-
format_to_table(rows, column_size)
110111
_print_matter_members(active_usernames, member_type="active")
111112

112113
if include_inactive:

src/code42cli/cmds/securitydata.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from _collections import OrderedDict
12
from pprint import pformat
23

34
import click
@@ -15,11 +16,15 @@
1516
from code42cli.options import incompatible_with
1617
from code42cli.options import OrderedGroup
1718
from code42cli.options import sdk_options
18-
from code42cli.util import find_format_width
19-
from code42cli.util import format_to_table
19+
from code42cli.output_formats import format_option as format_output
20+
2021

2122
logger = get_main_cli_logger()
2223

24+
_HEADER_KEYS_MAP = OrderedDict()
25+
_HEADER_KEYS_MAP["name"] = "Name"
26+
_HEADER_KEYS_MAP["id"] = "Id"
27+
2328
search_options = searchopt.create_search_options("file events")
2429

2530
format_option = click.option(
@@ -210,12 +215,15 @@ def saved_search(state):
210215

211216

212217
@saved_search.command("list")
218+
@format_output
213219
@sdk_options()
214-
def _list(state):
220+
def _list(state, format=None):
215221
"""List available saved searches."""
216222
response = state.sdk.securitydata.savedsearches.get()
217-
header = {"name": "Name", "id": "Id"}
218-
format_to_table(*find_format_width(response["searches"], header))
223+
result = response["searches"]
224+
if result:
225+
output = format(result, _HEADER_KEYS_MAP)
226+
echo(output)
219227

220228

221229
@saved_search.command()

src/code42cli/output_formats.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import json
2+
3+
import click
4+
5+
from code42cli.cmds.enums import OutputFormat
6+
from code42cli.util import find_format_width
7+
from code42cli.util import format_to_table
8+
9+
10+
def output_format(_, __, value):
11+
if value is not None:
12+
if value == OutputFormat.CSV:
13+
return to_csv
14+
if value == OutputFormat.RAW:
15+
return to_json
16+
if value == OutputFormat.TABLE:
17+
return to_table
18+
if value == OutputFormat.JSON:
19+
return to_formatted_json
20+
# default option
21+
return to_table
22+
23+
24+
format_option = click.option(
25+
"-f",
26+
"--format",
27+
type=click.Choice(OutputFormat(), case_sensitive=False),
28+
help="The output format of the result. Defaults to table format.",
29+
callback=output_format,
30+
)
31+
32+
33+
def to_csv(output, header):
34+
columns = ",".join(header.values())
35+
36+
lines = []
37+
lines.append(columns)
38+
for row in output:
39+
items = [str(row[key]) for key in header.keys()]
40+
line = ",".join(items)
41+
lines.append(line)
42+
return "\n".join(lines)
43+
44+
45+
def to_table(output, header):
46+
rows, column_size = find_format_width(output, header)
47+
return format_to_table(rows, column_size)
48+
49+
50+
def _filter(output, header):
51+
return [{header[key]: row[key] for key in header.keys()} for row in output]
52+
53+
54+
def to_json(output, header=None):
55+
return json.dumps(_filter(output, header))
56+
57+
58+
def to_formatted_json(output, header=None):
59+
return json.dumps(_filter(output, header), indent=4)

src/code42cli/util.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,14 @@ def find_format_width(record, header):
6868

6969

7070
def format_to_table(rows, column_size):
71-
"""Prints result in left justified format in a tabular form."""
71+
"""Formats given rows into a string of left justified table."""
72+
lines = []
7273
for row in rows:
74+
line = ""
7375
for key in row.keys():
74-
echo(str(row[key]).ljust(column_size[key] + _PADDING_SIZE), nl=False)
75-
echo("")
76+
line += str(row[key]).ljust(column_size[key] + _PADDING_SIZE)
77+
lines.append(line)
78+
return "\n".join(lines)
7679

7780

7881
def format_string_list_to_columns(string_list, max_width=None):

tests/cmds/test_alert_rules.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@
2525
]
2626
}
2727

28+
TEST_RULE_RESPONSE = {
29+
"ruleMetadata": [
30+
{
31+
"observerRuleId": TEST_RULE_ID,
32+
"type": "FED_FILE_TYPE_MISMATCH",
33+
"isEnabled": True,
34+
"ruleSource": "NOTVALID",
35+
"name": "Test",
36+
"severity": "high",
37+
}
38+
]
39+
}
40+
2841
TEST_USER_RULE_RESPONSE = {
2942
"ruleMetadata": [
3043
{
@@ -275,3 +288,19 @@ def test_remove_bulk_users_uses_expected_arguments(runner, mocker, cli_state):
275288
cli, ["alert-rules", "bulk", "add", "test_remove.csv"], obj=cli_state
276289
)
277290
assert bulk_processor.call_args[0][1] == [{"rule_id": "test", "username": "value"}]
291+
292+
293+
def test_list_cmd_prints_no_rules_found_when_f_is_passed_and_response_is_empty(
294+
runner, cli_state
295+
):
296+
cli_state.sdk.alerts.rules.get_all.return_value = [TEST_EMPTY_RULE_RESPONSE]
297+
result = runner.invoke(cli, ["alert-rules", "list", "-f", "csv"], obj=cli_state)
298+
assert cli_state.sdk.alerts.rules.get_all.call_count == 1
299+
assert "No alert rules found" in result.output
300+
301+
302+
def test_list_cmd_formats_to_csv_when_format_is_passed(runner, cli_state):
303+
cli_state.sdk.alerts.rules.get_all.return_value = [TEST_RULE_RESPONSE]
304+
result = runner.invoke(cli, ["alert-rules", "list", "-f", "csv"], obj=cli_state)
305+
assert cli_state.sdk.alerts.rules.get_all.call_count == 1
306+
assert "RuleId,Name,Severity,Type,Source,Enabled" in result.output

tests/cmds/test_legal_hold.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,30 @@
6363
TEST_PRESERVATION_POLICY_UID
6464
)
6565

66+
TEST_LEGAL_HOLD_LIST = [
67+
{
68+
"legalHolds": [
69+
{
70+
"legalHoldUid": "932880202064992021",
71+
"name": "test",
72+
"description": "",
73+
"active": True,
74+
"creationDate": "2019-12-19T20:32:10.763Z",
75+
"lastModified": "2019-12-19T20:32:10.781Z",
76+
"creator": {
77+
"userUid": "921286907298179098",
78+
"username": "[email protected]",
79+
"email": "[email protected]",
80+
},
81+
"holdPolicyUid": "901109555892625150",
82+
"creator_username": "[email protected]",
83+
},
84+
],
85+
}
86+
]
87+
88+
TEST_LEGAL_HOLD_EMPTY_LIST = [{"legalHolds": []}]
89+
6690

6791
@pytest.fixture
6892
def preservation_policy_response(mocker):
@@ -371,3 +395,36 @@ def test_remove_bulk_users_uses_expected_arguments(runner, mocker, cli_state):
371395
assert bulk_processor.call_args[0][1] == [
372396
{"matter_id": "test", "username": "value"}
373397
]
398+
399+
400+
def test_list_with_format_option_returns_expected_format(runner, cli_state):
401+
cli_state.sdk.legalhold.get_all_matters.return_value = TEST_LEGAL_HOLD_LIST
402+
403+
result = runner.invoke(cli, ["legal-hold", "list", "-f", "csv"], obj=cli_state)
404+
assert "Matter ID,Name,Description,Creator,Creation Date" in result.output
405+
assert "932880202064992021" in result.output
406+
407+
408+
def test_list_with_format_option_returns_no_response_when_response_is_empty(
409+
runner, cli_state
410+
):
411+
cli_state.sdk.legalhold.get_all_matters.return_value = TEST_LEGAL_HOLD_EMPTY_LIST
412+
result = runner.invoke(cli, ["legal-hold", "list", "-f", "csv"], obj=cli_state)
413+
assert "Matter ID,Name,Description,Creator,Creation Date" not in result.output
414+
415+
416+
def test_show_with_format_option_returns_expected_format(
417+
runner, cli_state, check_matter_accessible_success, get_user_id_success
418+
):
419+
cli_state.sdk.legalhold.get_all_matter_custodians.return_value = (
420+
ACTIVE_AND_INACTIVE_LEGAL_HOLD_MEMBERSHIPS_RESULT
421+
)
422+
result = runner.invoke(
423+
cli, ["legal-hold", "show", TEST_MATTER_ID, "-f", "csv"], obj=cli_state
424+
)
425+
426+
assert "Matter ID,Name,Description,Creator,Creation Date" in result.output
427+
assert (
428+
"88888,Test_Matter,,[email protected],2020-01-01T00:00:00.000-06:00"
429+
in result.output
430+
)

tests/cmds/test_securitydata.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@
1919
CURSOR_TIMESTAMP = 1579500000.0
2020

2121

22+
TEST_LIST_RESPONSE = {
23+
"searches": [
24+
{
25+
"id": "a083f08d-8f33-4cbd-81c4-8d1820b61185",
26+
"name": "test-events",
27+
"notes": "py42 is here",
28+
},
29+
]
30+
}
31+
32+
TEST_EMPTY_LIST_RESPONSE = {"searches": []}
33+
34+
2235
@pytest.fixture
2336
def file_event_extractor(mocker):
2437
mock = mocker.patch(
@@ -691,3 +704,24 @@ def test_search_with_or_query_flag_produces_expected_query(runner, cli_state):
691704
str(cli_state.sdk.securitydata.search_file_events.call_args[0][0])
692705
)
693706
assert actual_query == expected_query
707+
708+
709+
def test_saved_search_list_with_format_option_returns_csv_formatted_response(
710+
runner, cli_state
711+
):
712+
cli_state.sdk.securitydata.savedsearches.get.return_value = TEST_LIST_RESPONSE
713+
result = runner.invoke(
714+
cli, ["security-data", "saved-search", "list", "-f", "csv"], obj=cli_state
715+
)
716+
assert "Name,Id" in result.output
717+
assert "test-events,a083f08d-8f33-4cbd-81c4-8d1820b61185" in result.output
718+
719+
720+
def test_saved_search_list_with_format_option_does_not_return_when_response_is_empty(
721+
runner, cli_state
722+
):
723+
cli_state.sdk.securitydata.savedsearches.get.return_value = TEST_EMPTY_LIST_RESPONSE
724+
result = runner.invoke(
725+
cli, ["security-data", "saved-search", "list", "-f", "csv"], obj=cli_state
726+
)
727+
assert "Name,Id" not in result.output

0 commit comments

Comments
 (0)