Skip to content

Commit d9d5ebb

Browse files
author
Juliya Smith
authored
Feature/autocomplete (#75)
1 parent 44c62c1 commit d9d5ebb

29 files changed

+674
-344
lines changed

CONTRIBUTING.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,18 +81,18 @@ def test_add_one_and_one_equals_two():
8181
See class documentation on the [Command](src/code42cli/commands.py) class for an explanation of its constructor parameters.
8282

8383
1. If you are creating a new top-level command, create a new instance of `Command` and add it to the list returned
84-
by `_load_top_commands()` function in `code42cli.main`.
84+
by `load_commands()` function in `code42cli.main.MainSubcommandLoader`.
8585

8686
2. If you are creating a new subcommand, find the top-level command that this will be a subcommand of in
87-
`_load_top_commands()` in `code42cli.main` and navigate to the function assigned to be its subcommand loader.
88-
Then, add a new instance of `Command` to the list returned by that function.
87+
`load_commands()` in `code42cli.main.MainSubcommandLoader` and navigate to its subcommand loader's `load_commands()`
88+
Then, add a new instance of `Command` to the list returned.
8989

9090
3. For commands that actually are executed (rather than just being groups), you will add a `handler` function as a constructor parameter.
9191
This will be the function that you want to execute when your command is run.
9292
* _Positional_ arguments of the handler will automatically become _required_ cli arguments.
9393
* The order that the positional arguments should be entered in on the cli is the same as the order in which they appear in the handler.
9494
* _Keyword_ arguments of the handler will automatically become _optional_ cli arguments
95-
* the cli argument name will be the same as the handler param name except with `_` replaced with `-`, and prefixed with `--` if optional
95+
* the cli argument name will be the same as the handler param name except with `_` replaced with `-`, and prefixed with `--` if optional.
9696

9797
For example, consider the following python function:
9898

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,19 @@ reported.
203203

204204
If you keep getting prompted for your password, try resetting with `code42 profile reset-pw`.
205205
If that doesn't work, delete your credentials file located at ~/.code42cli or the entry in keychain.
206+
207+
## Tab completion
208+
209+
For `zsh`, add these commands to your `.zshrc` file:
210+
211+
```bash
212+
C42_COMPLETER=$(which code42cli_completer)
213+
autoload bashcompinit && bashcompinit
214+
complete -C '$C42_COMPLETER' code42
215+
```
216+
217+
For bash, add just the first and last commands to your `.bash_profile`:
218+
```bash
219+
C42_COMPLETER=$(which code42cli_completer)
220+
complete -C '$C42_COMPLETER' code42
221+
```

bin/code42cli_completer

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# file inspired from awscli https://github.com/aws/aws-cli/blob/develop/bin/aws_completer
2+
3+
import os
4+
if os.environ.get('LC_CTYPE', '') == 'UTF-8':
5+
os.environ['LC_CTYPE'] = 'en_US.UTF-8'
6+
import code42cli.completer
7+
8+
if __name__ == '__main__':
9+
# bash exports COMP_LINE and COMP_POINT, tcsh COMMAND_LINE only
10+
cline = os.environ.get('COMP_LINE') or os.environ.get('COMMAND_LINE') or ''
11+
cpoint = int(os.environ.get('COMP_POINT') or len(cline))
12+
try:
13+
code42cli.completer.complete(cline, cpoint)
14+
except KeyboardInterrupt:
15+
# If the user hits Ctrl+C, we don't want to print
16+
# a trace
17+
pass

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,6 @@
5252
"Programming Language :: Python :: 3.8",
5353
"Programming Language :: Python :: Implementation :: CPython",
5454
],
55+
scripts=["bin/code42cli_completer"],
5556
entry_points={"console_scripts": ["code42=code42cli.main:main"]},
5657
)

src/code42cli/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
PRODUCT_NAME = u"code42cli"
2+
MAIN_COMMAND = u"code42"

src/code42cli/bulk.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import os, inspect, sys
1+
import os, inspect
22

33
from code42cli.compat import open, str
44
from code42cli.worker import Worker
@@ -93,8 +93,6 @@ def run(self):
9393
self._process_row(row)
9494
self.__worker.wait()
9595
self._print_results()
96-
sys.stdout.flush()
97-
9896

9997
def _process_row(self, row):
10098
if isinstance(row, dict):

src/code42cli/cmds/alerts/main.py

Lines changed: 46 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from code42cli.args import ArgConfig
2-
from code42cli.commands import Command
2+
from code42cli.commands import Command, SubcommandLoader
33
from code42cli.cmds.alerts.extraction import extract
44
from code42cli.cmds.search_shared import args, logger_factory
55
from code42cli.cmds.search_shared.enums import (
@@ -12,45 +12,51 @@
1212
from code42cli.cmds.search_shared.cursor_store import AlertCursorStore
1313

1414

15-
def load_subcommands():
16-
"""Sets up the `alerts` subcommand with all of its subcommands."""
17-
usage_prefix = u"code42 alerts"
18-
19-
print_func = Command(
20-
u"print",
21-
u"Print alerts to stdout",
22-
u"{} {}".format(usage_prefix, u"print <optional-args>"),
23-
handler=print_out,
24-
arg_customizer=_load_search_args,
25-
use_single_arg_obj=True,
26-
)
27-
28-
write = Command(
29-
u"write-to",
30-
u"Write alerts to the file with the given name.",
31-
u"{} {}".format(usage_prefix, u"write-to <filename> <optional-args>"),
32-
handler=write_to,
33-
arg_customizer=_load_write_to_args,
34-
use_single_arg_obj=True,
35-
)
36-
37-
send = Command(
38-
u"send-to",
39-
u"Send alerts to the given server address.",
40-
u"{} {}".format(usage_prefix, u"send-to <server-address> <optional-args>"),
41-
handler=send_to,
42-
arg_customizer=_load_send_to_args,
43-
use_single_arg_obj=True,
44-
)
45-
46-
clear = Command(
47-
u"clear-checkpoint",
48-
u"Remove the saved alert checkpoint from 'incremental' (-i) mode.",
49-
u"{} {}".format(usage_prefix, u"clear-checkpoint <optional-args>"),
50-
handler=clear_checkpoint,
51-
)
52-
53-
return [print_func, write, send, clear]
15+
class MainAlertsSubcommandLoader(SubcommandLoader):
16+
PRINT = u"print"
17+
WRITE_TO = u"write-to"
18+
SEND_TO = u"send-to"
19+
CLEAR_CHECKPOINT = u"clear-checkpoint"
20+
21+
def load_commands(self):
22+
"""Sets up the `alerts` subcommand with all of its subcommands."""
23+
usage_prefix = u"code42 alerts"
24+
25+
print_func = Command(
26+
self.PRINT,
27+
u"Print alerts to stdout",
28+
u"{} {}".format(usage_prefix, u"print <optional-args>"),
29+
handler=print_out,
30+
arg_customizer=_load_search_args,
31+
use_single_arg_obj=True,
32+
)
33+
34+
write = Command(
35+
self.WRITE_TO,
36+
u"Write alerts to the file with the given name.",
37+
u"{} {}".format(usage_prefix, u"write-to <filename> <optional-args>"),
38+
handler=write_to,
39+
arg_customizer=_load_write_to_args,
40+
use_single_arg_obj=True,
41+
)
42+
43+
send = Command(
44+
self.SEND_TO,
45+
u"Send alerts to the given server address.",
46+
u"{} {}".format(usage_prefix, u"send-to <server-address> <optional-args>"),
47+
handler=send_to,
48+
arg_customizer=_load_send_to_args,
49+
use_single_arg_obj=True,
50+
)
51+
52+
clear = Command(
53+
self.CLEAR_CHECKPOINT,
54+
u"Remove the saved alert checkpoint from 'incremental' (-i) mode.",
55+
u"{} {}".format(usage_prefix, u"clear-checkpoint <optional-args>"),
56+
handler=clear_checkpoint,
57+
)
58+
59+
return [print_func, write, send, clear]
5460

5561

5662
def clear_checkpoint(sdk, profile):

src/code42cli/cmds/alerts/rules/commands.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from code42cli.commands import Command
1+
from code42cli import MAIN_COMMAND
2+
from code42cli.commands import Command, SubcommandLoader
23
from code42cli.bulk import generate_template, BulkCommandType
34
from code42cli.cmds.alerts.rules.user_rule import (
45
add_user,
@@ -61,21 +62,24 @@ def _load_bulk_generate_template_description(argument_collection):
6162
cmd_type.set_choices(BulkCommandType())
6263

6364

64-
class AlertRulesBulkCommands(object):
65-
@staticmethod
66-
def load_commands():
67-
usage_prefix = u"code42 alert-rules bulk"
65+
class AlertRulesBulkSubcommandLoader(SubcommandLoader):
66+
GENERATE_TEMPLATE = u"generate-template"
67+
ADD = u"add"
68+
REMOVE = u"remove"
69+
70+
def load_commands(self):
71+
usage_prefix = u"{} alert-rules bulk".format(MAIN_COMMAND)
6872

6973
generate_template_cmd = Command(
70-
u"generate-template",
74+
self.GENERATE_TEMPLATE,
7175
u"Generate the necessary csv template needed for bulk adding users.",
7276
u"{} generate-template <cmd> <optional args>".format(usage_prefix),
7377
handler=_generate_template_file,
7478
arg_customizer=_load_bulk_generate_template_description,
7579
)
7680

7781
bulk_add = Command(
78-
u"add",
82+
self.ADD,
7983
u"Update alert rule criteria to add users and all their aliases. "
8084
u"CSV file format: rule_id,username",
8185
u"{} add <filename>".format(usage_prefix),
@@ -84,7 +88,7 @@ def load_commands():
8488
)
8589

8690
bulk_remove = Command(
87-
u"remove",
91+
self.REMOVE,
8892
u"Update alert rule criteria to remove users and all their aliases. "
8993
u"CSV file format: rule_id,username",
9094
u"{} remove <filename>".format(usage_prefix),
@@ -95,46 +99,55 @@ def load_commands():
9599
return [generate_template_cmd, bulk_add, bulk_remove]
96100

97101

98-
class AlertRulesCommands(object):
99-
@staticmethod
100-
def load_subcommands():
102+
class AlertRulesSubcommandLoader(SubcommandLoader):
103+
ADD_USER = u"add-user"
104+
REMOVE_USER = u"remove-user"
105+
LIST = u"list"
106+
SHOW = u"show"
107+
BULK = u"bulk"
108+
109+
def __init__(self, root_command_name):
110+
super(AlertRulesSubcommandLoader, self).__init__(root_command_name)
111+
self._bulk_subcommand_loader = AlertRulesBulkSubcommandLoader(self.BULK)
112+
113+
def load_commands(self):
101114
usage_prefix = u"code42 alert-rules"
102115

103116
add = Command(
104-
u"add-user",
117+
self.ADD_USER,
105118
u"Update alert rule criteria to monitor user aliases against the given username.",
106119
u"{} add-user --rule-id <id> --username <username>".format(usage_prefix),
107120
handler=add_user,
108121
arg_customizer=_customize_add_arguments,
109122
)
110123

111124
remove = Command(
112-
u"remove-user",
125+
self.REMOVE_USER,
113126
u"Update alert rule criteria to remove a user and all their aliases.",
114127
u"{} remove-user --rule-id <rule-id> --username <username>".format(usage_prefix),
115128
handler=remove_user,
116129
arg_customizer=_customize_remove_arguments,
117130
)
118131

119132
list_rules = Command(
120-
u"list",
133+
self.LIST,
121134
u"Fetch existing alert rules.",
122135
u"{} list".format(usage_prefix),
123136
handler=get_rules,
124137
)
125138

126139
show = Command(
127-
u"show",
140+
self.SHOW,
128141
u"Fetch configured alert-rules against the rule ID.",
129142
u"{} show <rule-id>".format(usage_prefix),
130143
handler=show_rule,
131144
arg_customizer=_customize_list_arguments,
132145
)
133146

134147
bulk = Command(
135-
u"bulk",
148+
self.BULK,
136149
u"Tools for executing bulk commands.",
137-
subcommand_loader=AlertRulesBulkCommands.load_commands,
150+
subcommand_loader=self._bulk_subcommand_loader,
138151
)
139152

140153
return [add, remove, list_rules, show, bulk]

src/code42cli/cmds/detectionlists/__init__.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from py42.exceptions import Py42BadRequestError
22

3+
from code42cli.cmds.detectionlists.commands import DetectionListSubcommandLoader
34
from code42cli.bulk import generate_template, run_bulk_process
45
from code42cli.file_readers import create_csv_reader, create_flat_file_reader
56
from code42cli.errors import UserAlreadyAddedError, UserDoesNotExistError, UnknownRiskTagError
67
from code42cli.cmds.detectionlists.enums import DetectionLists, DetectionListUserKeys, RiskTags
7-
from code42cli.cmds.detectionlists.commands import DetectionListCommandFactory
88
from code42cli.cmds.detectionlists.bulk import BulkDetectionList, BulkHighRiskEmployee
99

1010

@@ -46,13 +46,15 @@ class DetectionList(object):
4646
given `classmethods`.
4747
handlers (DetectionListHandlers): A DTO containing implementations for adding / removing
4848
users from specific lists.
49-
cmd_factory (DetectionListCommandFactory): A factory that creates detection list commands.
49+
cmd_factory (DetectionListSubcommandLoader): A factory that creates detection list commands.
5050
"""
5151

52-
def __init__(self, list_name, handlers, cmd_factory=None):
52+
def __init__(self, list_name, handlers, subcommand_loader=None):
5353
self.name = list_name
5454
self.handlers = handlers
55-
self.factory = cmd_factory or DetectionListCommandFactory(list_name)
55+
self.subcommand_loader = subcommand_loader or DetectionListSubcommandLoader(list_name)
56+
self.bulk_subcommand_loader = self.subcommand_loader.bulk_subcommand_loader
57+
self.bulk_subcommand_loader.load_commands = lambda: self._load_bulk_subcommands
5658

5759
@classmethod
5860
def create_high_risk_employee_list(cls, handlers):
@@ -82,39 +84,41 @@ def create_departing_employee_list(cls, handlers):
8284

8385
def load_subcommands(self):
8486
"""Loads high risk employee related subcommands"""
85-
bulk = self.factory.create_bulk_command(lambda: self._load_bulk_subcommands())
86-
add = self.factory.create_add_command(
87+
bulk = self.subcommand_loader.create_bulk_command()
88+
bulk.subcommand_loader.load_commands = lambda: self._load_bulk_subcommands()
89+
add = self.subcommand_loader.create_add_command(
8790
self.handlers.add_employee, self.handlers.load_add_description
8891
)
89-
remove = self.factory.create_remove_command(
92+
remove = self.subcommand_loader.create_remove_command(
9093
self.handlers.remove_employee, load_username_description
9194
)
9295
return [bulk, add, remove]
9396

9497
def _load_bulk_subcommands(self):
95-
96-
add = self.factory.create_bulk_add_command(self.bulk_add_employees)
97-
remove = self.factory.create_bulk_remove_command(self.bulk_remove_employees)
98+
add = self.bulk_subcommand_loader.create_bulk_add_command(self.bulk_add_employees)
99+
remove = self.bulk_subcommand_loader.create_bulk_remove_command(self.bulk_remove_employees)
98100
commands = [add, remove]
99101

100102
if self.name == DetectionLists.HIGH_RISK_EMPLOYEE:
101103
commands.extend(self._get_risk_tags_bulk_subcommands())
102104
else:
103-
generate_template_cmd = self.factory.create_bulk_generate_template_command(
105+
generate_template_cmd = self.bulk_subcommand_loader.create_bulk_generate_template_command(
104106
self.generate_template_file
105107
)
106108
commands.append(generate_template_cmd)
107109
return commands
108110

109111
def _get_risk_tags_bulk_subcommands(self):
110-
bulk_add_risk_tags = self.factory.create_bulk_add_risk_tags_command(self.bulk_add_risk_tags)
111-
bulk_remove_risk_tags = self.factory.create_bulk_remove_risk_tags_command(
112+
bulk_add_risk_tags = self.bulk_subcommand_loader.create_bulk_add_risk_tags_command(
113+
self.bulk_add_risk_tags
114+
)
115+
bulk_remove_risk_tags = self.bulk_subcommand_loader.create_bulk_remove_risk_tags_command(
112116
self.bulk_remove_risk_tags
113117
)
114118

115119
self.handlers.add_handler(u"add_risk_tags", add_risk_tags)
116120
self.handlers.add_handler(u"remove_risk_tags", remove_risk_tags)
117-
generate_template_cmd = self.factory.create_hre_bulk_generate_template_command(
121+
generate_template_cmd = self.bulk_subcommand_loader.create_hre_bulk_generate_template_command(
118122
self.generate_template_file
119123
)
120124
return [bulk_add_risk_tags, bulk_remove_risk_tags, generate_template_cmd]

src/code42cli/cmds/detectionlists/bulk.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ def __iter__(self):
1111

1212

1313
class BulkDetectionList(object):
14-
1514
def __init__(self):
1615
self.type = BulkCommandType
17-
16+
1817
def get_handler(self, handlers, cmd):
1918
handler = None
2019
if cmd == self.type.ADD:
@@ -25,7 +24,6 @@ def get_handler(self, handlers, cmd):
2524

2625

2726
class BulkHighRiskEmployee(BulkDetectionList):
28-
2927
def __init__(self):
3028
super(BulkHighRiskEmployee, self).__init__()
3129
self.type = HighRiskBulkCommandType

0 commit comments

Comments
 (0)