Skip to content

Commit 8814c9e

Browse files
author
Juliya Smith
authored
Feature/progress bar print (#71)
1 parent 9623d1f commit 8814c9e

File tree

17 files changed

+358
-158
lines changed

17 files changed

+358
-158
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
5151
- Warning message printed when ctrl-c is encountered in the middle of an operation that could cause incorrect checkpoint
5252
state, a second ctrl-c is required to quit while that operation is ongoing.
5353

54+
- A progress bar that displays during bulk commands.
55+
5456
### Fixed
5557

5658
- Fixed bug in bulk commands where value-less fields in csv files were treated as empty strings instead of None.

src/code42cli/bulk.py

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import os
2-
import inspect
3-
import csv
1+
import os, inspect
42

53
from code42cli.compat import open, str
64
from code42cli.worker import Worker
75
from code42cli.logger import get_main_cli_logger
86
from code42cli.args import SDK_ARG_NAME, PROFILE_ARG_NAME
7+
from code42cli.progress_bar import ProgressBar
8+
9+
10+
_logger = get_main_cli_logger()
911

1012

1113
class BulkCommandType(object):
@@ -29,7 +31,7 @@ def generate_template(handler, path=None):
2931
]
3032

3133
if len(args) <= 1:
32-
get_main_cli_logger().print_info(
34+
_logger.print_info(
3335
u"A blank file was generated because there are no csv headers needed for this command. "
3436
u"Simply enter one {} per line.".format(args[0])
3537
)
@@ -45,31 +47,28 @@ def _write_template_file(path, columns=None):
4547
new_file.write(u",".join(columns))
4648

4749

48-
def run_bulk_process(file_path, row_handler, reader=None):
50+
def run_bulk_process(row_handler, reader):
4951
"""Runs a bulk process.
5052
5153
Args:
52-
file_path (str or unicode): The path to the file feeding the data for the bulk process.
5354
row_handler (callable): A callable that you define to process values from the row as
5455
either *args or **kwargs.
5556
reader: (CSVReader or FlatFileReader, optional): A generator that reads rows and yields data into
5657
`row_handler`. If None, it will use a CSVReader. Defaults to None.
5758
"""
58-
reader = reader or CSVReader()
59-
processor = _create_bulk_processor(file_path, row_handler, reader)
59+
processor = _create_bulk_processor(row_handler, reader)
6060
processor.run()
6161

6262

63-
def _create_bulk_processor(file_path, row_handler, reader):
63+
def _create_bulk_processor(row_handler, reader):
6464
"""A factory method to create the bulk processor, useful for testing purposes."""
65-
return BulkProcessor(file_path, row_handler, reader)
65+
return BulkProcessor(row_handler, reader)
6666

6767

6868
class BulkProcessor(object):
6969
"""A class for bulk processing a file.
7070
7171
Args:
72-
file_path (str or unicode): The path to the file for processing.
7372
row_handler (callable): A callable that you define to process values from the row as
7473
either *args or **kwargs. For example, if it's a csv file with header `prop_a,prop_b`
7574
and first row `1,test`, then `row_handler` should receive kwargs
@@ -78,19 +77,22 @@ class BulkProcessor(object):
7877
reader (CSVReader or FlatFileReader): A generator that reads rows and yields data into `row_handler`.
7978
"""
8079

81-
def __init__(self, file_path, row_handler, reader):
82-
self.file_path = file_path
80+
def __init__(self, row_handler, reader, worker=None, progress_bar=None):
81+
total = reader.get_rows_count()
82+
self.file_path = reader.file_path
8383
self._row_handler = row_handler
8484
self._reader = reader
85-
self.__worker = Worker(5)
85+
self.__worker = worker or Worker(5, total)
86+
self._stats = self.__worker.stats
87+
self._progress_bar = progress_bar or ProgressBar(total)
8688

8789
def run(self):
8890
"""Processes the csv file specified in the ctor, calling `self.row_handler` on each row."""
8991
with open(self.file_path, newline=u"", encoding=u"utf8") as bulk_file:
9092
for row in self._reader(bulk_file=bulk_file):
9193
self._process_row(row)
9294
self.__worker.wait()
93-
self._print_result()
95+
self._print_results()
9496

9597
def _process_row(self, row):
9698
if isinstance(row, dict):
@@ -104,35 +106,20 @@ def _process_csv_row(self, row):
104106
row.pop(None, None)
105107
row_values = {key: val if val != u"" else None for key, val in row.items()}
106108
self.__worker.do_async(
107-
lambda *args, **kwargs: self._row_handler(*args, **kwargs), **row_values
109+
lambda *args, **kwargs: self._handle_row(*args, **kwargs), **row_values
108110
)
109111

110112
def _process_flat_file_row(self, row):
111113
if row:
112-
self.__worker.do_async(lambda *args, **kwargs: self._row_handler(*args, **kwargs), row)
113-
114-
def _print_result(self):
115-
stats = self.__worker.stats
116-
successes = stats.total - stats.total_errors
117-
logger = get_main_cli_logger()
118-
logger.print_and_log_info(
119-
u"{} processed successfully out of {}.".format(successes, stats.total)
120-
)
121-
if stats.total_errors:
122-
logger.print_errors_occurred_message()
123-
114+
self.__worker.do_async(lambda *args, **kwargs: self._handle_row(*args, **kwargs), row)
124115

125-
class CSVReader(object):
126-
"""A generator that yields header keys mapped to row values from a csv file."""
116+
def _handle_row(self, *args, **kwargs):
117+
message = str(self._stats)
118+
self._progress_bar.update(self._stats.total_processed, message)
119+
self._row_handler(*args, **kwargs)
127120

128-
def __call__(self, *args, **kwargs):
129-
for row in csv.DictReader(kwargs.get(u"bulk_file")):
130-
yield row
131-
132-
133-
class FlatFileReader(object):
134-
"""A generator that yields a single-value per row from a file."""
135-
136-
def __call__(self, *args, **kwargs):
137-
for row in kwargs[u"bulk_file"]:
138-
yield row
121+
def _print_results(self):
122+
self._progress_bar.clear_bar_and_print_final(str(self._stats))
123+
if self._stats.total_errors:
124+
logger = get_main_cli_logger()
125+
logger.print_errors_occurred_message()

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
from code42cli.errors import InvalidRuleTypeError
66
from code42cli.util import format_to_table, find_format_width
7-
from code42cli.bulk import run_bulk_process, CSVReader
7+
from code42cli.bulk import run_bulk_process
8+
from code42cli.file_readers import create_csv_reader
89
from code42cli.logger import get_main_cli_logger
910
from code42cli.cmds.detectionlists import get_user_id
1011
from code42cli.cmds.alerts.rules.enums import AlertRuleTypes
@@ -74,17 +75,13 @@ def get_rules(sdk, profile):
7475

7576

7677
def add_bulk_users(sdk, profile, file_name):
77-
run_bulk_process(
78-
file_name, lambda rule_id, username: add_user(sdk, profile, rule_id, username), CSVReader()
79-
)
78+
reader = create_csv_reader(file_name)
79+
run_bulk_process(lambda rule_id, username: add_user(sdk, profile, rule_id, username), reader)
8080

8181

8282
def remove_bulk_users(sdk, profile, file_name):
83-
run_bulk_process(
84-
file_name,
85-
lambda rule_id, username: remove_user(sdk, profile, rule_id, username),
86-
CSVReader(),
87-
)
83+
reader = create_csv_reader(file_name)
84+
run_bulk_process(lambda rule_id, username: remove_user(sdk, profile, rule_id, username), reader)
8885

8986

9087
def show_rule(sdk, profile, rule_id):

src/code42cli/cmds/detectionlists/__init__.py

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

3-
from code42cli.compat import str
3+
from code42cli.bulk import generate_template, run_bulk_process, BulkCommandType
4+
from code42cli.file_readers import create_csv_reader, create_flat_file_reader
5+
from code42cli.errors import UserAlreadyAddedError, UserDoesNotExistError, UnknownRiskTagError
46
from code42cli.cmds.detectionlists.commands import DetectionListCommandFactory
5-
from code42cli.bulk import generate_template, run_bulk_process, CSVReader, FlatFileReader
6-
from code42cli.errors import UserAlreadyAddedError, UnknownRiskTagError, UserDoesNotExistError
7-
from code42cli.bulk import BulkCommandType
87
from code42cli.cmds.detectionlists.enums import DetectionLists, DetectionListUserKeys, RiskTags
98

109

@@ -123,9 +122,8 @@ def bulk_add_employees(self, sdk, profile, csv_file):
123122
profile (Code42Profile): The profile under which to execute this command.
124123
csv_file (str or unicode): The path to the csv file containing rows of users.
125124
"""
126-
run_bulk_process(
127-
csv_file, lambda **kwargs: self._add_employee(sdk, profile, **kwargs), CSVReader()
128-
)
125+
reader = create_csv_reader(csv_file)
126+
run_bulk_process(lambda **kwargs: self._add_employee(sdk, profile, **kwargs), reader)
129127

130128
def bulk_remove_employees(self, sdk, profile, users_file):
131129
"""Takes a flat file with each row containing a username and removes them all from the
@@ -136,10 +134,9 @@ def bulk_remove_employees(self, sdk, profile, users_file):
136134
profile (Code42Profile): The profile under which to execute this command.
137135
users_file (str or unicode): The path to the file containing rows of user names.
138136
"""
137+
reader = create_flat_file_reader(users_file)
139138
run_bulk_process(
140-
users_file,
141-
lambda *args, **kwargs: self._remove_employee(sdk, profile, *args, **kwargs),
142-
FlatFileReader(),
139+
lambda *args, **kwargs: self._remove_employee(sdk, profile, *args, **kwargs), reader
143140
)
144141

145142
def _add_employee(self, sdk, profile, **kwargs):

src/code42cli/compat.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
if is_py2:
1515
from urlparse import urljoin, urlparse
1616

17-
str = unicode
17+
def _str(obj):
18+
return unicode(obj)
19+
20+
str = _str
1821

1922
import io
2023

src/code42cli/errors.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
ERRORED = False
22

3+
34
_FORMAT_VALUE_ERROR_MESSAGE = (
45
u"input must be a date/time string (e.g. 'YYYY-MM-DD', "
56
u"'YY-MM-DD HH:MM', 'YY-MM-DD HH:MM:SS'), or a short value in days, "
67
u"hours, or minutes (e.g. 30d, 24h, 15m)"
78
)
89

910

11+
class BadFileError(Exception):
12+
def __init__(self, file_path, *args, **kwargs):
13+
self.file_path = file_path
14+
super(BadFileError, self).__init__()
15+
16+
17+
class EmptyFileError(BadFileError):
18+
def __init__(self, file_path):
19+
super(EmptyFileError, self).__init__(file_path, u"Given empty file {}.".format(file_path))
20+
21+
1022
class Code42CLIError(Exception):
1123
pass
1224

src/code42cli/file_readers.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import csv
2+
3+
from code42cli.errors import BadFileError
4+
5+
6+
class CliFileReader(object):
7+
_ROWS_COUNT = -1
8+
9+
def __init__(self, file_path):
10+
self.file_path = file_path
11+
12+
def __call__(self, *args, **kwargs):
13+
pass
14+
15+
def get_rows_count(self):
16+
if self._ROWS_COUNT == -1:
17+
self._ROWS_COUNT = sum(1 for _ in open(self.file_path))
18+
if self._ROWS_COUNT == 0:
19+
raise BadFileError(u"Given empty file {}.".format(self.file_path))
20+
return self._ROWS_COUNT
21+
22+
23+
class CSVReader(CliFileReader):
24+
"""A generator that yields header keys mapped to row values from a csv file."""
25+
26+
def __init__(self, file_path):
27+
with open(file_path) as f:
28+
try:
29+
self.has_header = csv.Sniffer().has_header(next(f))
30+
except StopIteration:
31+
raise BadFileError(u"Given empty file {}.".format(file_path))
32+
super(CSVReader, self).__init__(file_path)
33+
34+
def __call__(self, *args, **kwargs):
35+
for row in csv.DictReader(kwargs.get(u"bulk_file")):
36+
yield row
37+
38+
def get_rows_count(self):
39+
rows_count = super(CSVReader, self).get_rows_count()
40+
return rows_count - 1 if self.has_header else rows_count
41+
42+
43+
class FlatFileReader(CliFileReader):
44+
"""A generator that yields a single-value per row from a file."""
45+
46+
def __call__(self, *args, **kwargs):
47+
for row in kwargs[u"bulk_file"]:
48+
yield row
49+
50+
51+
def create_csv_reader(file_path):
52+
return CSVReader(file_path)
53+
54+
55+
def create_flat_file_reader(file_path):
56+
return FlatFileReader(file_path)

src/code42cli/logger.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import copy
55

66
from code42cli.compat import str
7-
from code42cli.util import get_user_project_path, is_interactive
7+
from code42cli.util import get_user_project_path, is_interactive, color_text_red
88

99

1010
logger_deps_lock = Lock()
@@ -124,7 +124,39 @@ def _create_formatter_for_error_file():
124124

125125

126126
def _get_red_error_text(text):
127-
return u"\033[91mERROR: {}\033[0m".format(text)
127+
return color_text_red(u"ERROR: {}".format(text))
128+
129+
130+
def get_progress_logger(handler=None):
131+
logger = logging.getLogger(u"code42cli_progress_bar")
132+
if logger_has_handlers(logger):
133+
return logger
134+
135+
with logger_deps_lock:
136+
if not logger_has_handlers(logger):
137+
handler = handler or InPlaceStreamHandler()
138+
formatter = _get_standard_formatter()
139+
logger.setLevel(logging.INFO)
140+
return add_handler_to_logger(logger, handler, formatter)
141+
return logger
142+
143+
144+
class InPlaceStreamHandler(logging.StreamHandler):
145+
def __init__(self):
146+
super(InPlaceStreamHandler, self).__init__(sys.stdout)
147+
148+
def emit(self, record):
149+
# Borrowed some from python3's logging.StreamHandler to make work on python2.
150+
try:
151+
msg = u"\r{}\r".format(self.format(record))
152+
stream = self.stream
153+
stream.write(msg)
154+
self.flush()
155+
except RuntimeError as err:
156+
if u"recursion" in str(err):
157+
raise
158+
except Exception:
159+
self.handleError(record)
128160

129161

130162
class CliLogger(object):

src/code42cli/progress_bar.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from code42cli.logger import get_main_cli_logger, get_progress_logger
4+
5+
6+
class ProgressBar(object):
7+
_FILL = u"█"
8+
_LENGTH = 100
9+
10+
def __init__(self, total_items, logger=None):
11+
self._total_items = total_items
12+
self._logger = logger or get_progress_logger()
13+
14+
def update(self, iteration, message):
15+
bar = self._create_bar(iteration)
16+
progress = u"{} {} ".format(bar, message)
17+
self._logger.info(progress)
18+
19+
def _create_bar(self, iteration):
20+
fill_length = self._calculate_fill_length(iteration)
21+
return self._FILL * fill_length + u"-" * (self._LENGTH - fill_length)
22+
23+
def _calculate_fill_length(self, idx):
24+
filled_length = int(self._LENGTH * idx // self._total_items)
25+
return filled_length
26+
27+
def clear_bar_and_print_final(self, final_message):
28+
clear = (self._LENGTH + len(final_message)) * u" "
29+
self._logger.info(u"{}{}\n".format(final_message, clear))

0 commit comments

Comments
 (0)