Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Progress Reporting for long running operations #3243

Merged
merged 202 commits into from
May 25, 2017
Merged
Show file tree
Hide file tree
Changes from 185 commits
Commits
Show all changes
202 commits
Select commit Hold shift + click to select a range
b2fbfb7
minor changes
Apr 24, 2017
6fa7bc6
progress classes and added reportign
Apr 24, 2017
68b8f28
Merge branch 'master' of https://github.com/azure/azure-cli into prog…
Apr 25, 2017
2a9e669
repackage
Apr 25, 2017
8445e2f
check for value before getting it in parsed args
Apr 25, 2017
c71ebc6
Merge branch 'master' of https://github.com/azure/azure-cli into prog…
Apr 25, 2017
ac350ea
undoing check
Apr 25, 2017
d88ad72
minor
Apr 25, 2017
7491ec9
mvc for progress outline
Apr 26, 2017
3ca3bd7
Merge branch 'master' of https://github.com/azure/azure-cli into prog…
Apr 26, 2017
b08e133
ask user for feedback
Apr 26, 2017
e0d39fc
shell additions
Apr 26, 2017
5d1c333
repackaging + shell implementation start
Apr 26, 2017
8c7b18c
remove vscode
Apr 26, 2017
adea8f1
heart beat
Apr 27, 2017
7a9e789
progress bar
Apr 27, 2017
64c5a7a
minor changes
Apr 27, 2017
72a77ee
CLI spinning wheel
Apr 27, 2017
72b954d
upload download storage
Apr 28, 2017
fdfb13e
humanfriendly in dependencies
Apr 28, 2017
ae24830
clean up controller
Apr 28, 2017
b50e085
minor changes
Apr 28, 2017
bd6892d
split deterministic and nondeterministic
Apr 28, 2017
29ebff5
separate the reporter
Apr 29, 2017
de8b0c5
design simplification
May 1, 2017
16f94d5
minor
May 1, 2017
3d53b00
Merge branch 'master' of https://github.com/azure/azure-cli into prog…
May 1, 2017
a36962b
errors
May 1, 2017
b728bac
minor
May 1, 2017
2532835
clear changing
May 1, 2017
55c6ad9
update readme
May 1, 2017
8bc52a0
Revert "clear changing"
May 1, 2017
2c5ff53
Revert "update readme"
May 1, 2017
ace1cc7
minor
May 1, 2017
086f2a6
mute stderr
May 1, 2017
c1f55c6
varying number of parameters capability
May 1, 2017
93bffb3
hooking up shell with progress
May 1, 2017
32b878f
hooking up progress bar
May 1, 2017
771a0ee
message in determiniate progress
May 1, 2017
c75bd4d
add testing
May 1, 2017
217147a
cleaning code + writing tests
May 1, 2017
b386600
tests
May 2, 2017
6d9debf
removed files
May 2, 2017
5fa1228
shell + other modifications
May 2, 2017
2c1ff74
minor changes
May 2, 2017
b92392b
history
May 2, 2017
ae11bcc
pylint + formatting
May 2, 2017
454f42c
flake8
May 2, 2017
94cb45a
flake8
May 2, 2017
6a4239d
pylint
May 2, 2017
42ab25c
cleaning + comments
May 2, 2017
36cd5ca
tests
May 2, 2017
68941db
reviews
May 2, 2017
ab4d0dc
kwargs cleaning
May 2, 2017
dbb754f
clean
May 2, 2017
942d9a1
flake8
May 2, 2017
30533d8
tests + cleaning formatting
May 2, 2017
9f670e3
travis failing
May 2, 2017
8ddba04
removed namespace in nspkg
May 2, 2017
a304486
shell correction
May 2, 2017
7ac9df1
cleaning + formatting
May 2, 2017
66243c8
char hack
May 2, 2017
0618566
redir stderr
May 2, 2017
b49fb99
tests + patch
May 2, 2017
e21a6d6
change toolbar for 20 seconds
May 2, 2017
b4812be
feedback in toolbar
May 2, 2017
e375839
Merge branch 'master' into feedback
May 2, 2017
a97d154
tests
May 2, 2017
68d8854
flake8
May 2, 2017
1b56a53
test
May 2, 2017
6a6215b
tests
May 2, 2017
352a695
tests
May 2, 2017
0c6ed18
tests
May 2, 2017
73a199d
tests
May 2, 2017
47c295a
tests
May 2, 2017
243971c
tests
May 3, 2017
c0e9f0e
tests
May 3, 2017
1066afd
tests
May 3, 2017
affb745
tests
May 3, 2017
1b1cbfc
tests
May 3, 2017
a7acfcc
tests
May 3, 2017
5cc8bac
remove dead code
May 3, 2017
c5c8481
flake8
May 3, 2017
2145b54
Merge branch 'feedback' of https://github.com/oakeyc/azure-cli into f…
May 3, 2017
74d8e64
flake8
May 3, 2017
461f2ff
Merge branch 'master' of https://github.com/azure/azure-cli into feed…
May 3, 2017
517cb60
small changes
May 3, 2017
76fcb63
commenting out test
May 3, 2017
2a3768e
removed unnecessary subclass
May 3, 2017
ecb676e
mock up the progress for tests
May 3, 2017
dc798f0
wording
May 3, 2017
4c13385
pylint
May 3, 2017
4d34129
typo
May 3, 2017
11753a6
some style + renaming
May 3, 2017
6e86c34
keyword
May 3, 2017
d540b83
kwargs
May 3, 2017
efc725b
calling function
May 3, 2017
1d7c47f
calling function
May 3, 2017
f87c0a8
adding tests back
May 3, 2017
7e6f092
changed outstream
May 3, 2017
674d8c3
comment out
May 3, 2017
f76fa9d
flake8
May 3, 2017
569eb43
stringio
May 3, 2017
4d61e10
trying fdopen
May 3, 2017
76f9e33
reviewed changes
May 3, 2017
6fe564f
change to not close
May 4, 2017
056ea14
delete file
May 4, 2017
17d3501
tests
May 4, 2017
31583f4
remove completion tests
May 4, 2017
ea6d575
try fdopen
May 4, 2017
90254eb
change which tests dont run
May 4, 2017
63735c2
remove from longrunning
May 4, 2017
75fbe03
cleaning
May 4, 2017
16fe6d2
does this work?
May 4, 2017
0a1d367
patch storage
May 4, 2017
1537d47
write
May 4, 2017
2f519e2
write
May 4, 2017
83695c3
tests
May 4, 2017
bfb5751
tests
May 4, 2017
c57d889
formatting
May 4, 2017
847afa4
mock outstream
May 4, 2017
0b273c4
undo
May 4, 2017
9590bd3
making tests pass
May 4, 2017
561091a
Merge branch 'master' of https://github.com/oakeyc/azure-cli into pro…
May 4, 2017
7abb962
remodel to decouple the view
May 4, 2017
53e2fba
reorg
May 4, 2017
63920ab
patch tests
May 4, 2017
cf4a9eb
style
May 4, 2017
4fddd4b
remocked
May 4, 2017
0dc79d5
Merge branch 'master' of https://github.com/azure/azure-cli into prog…
May 4, 2017
19955f4
merge
May 4, 2017
d3d6c31
Merge branch 'master' into progress
May 4, 2017
5dc5472
pylint
May 4, 2017
3cebef4
Merge branch 'progress' of https://github.com/oakeyc/azure-cli into p…
May 4, 2017
570c066
reformat
May 4, 2017
f42233b
test
May 4, 2017
9f77800
typo
May 4, 2017
ce45cd6
outstream for indetstout
May 4, 2017
85dd6d1
flake8
May 4, 2017
b2a5406
vcr
May 4, 2017
9d46cba
mock view
May 4, 2017
cb21770
pylint
May 4, 2017
ce5d11b
minor
May 4, 2017
9ebacb0
remock
May 5, 2017
6f84dd9
remock
May 5, 2017
497c436
reorg
May 5, 2017
4344a57
test
May 5, 2017
d2464f0
change to cli config
May 5, 2017
d3d73e5
typos
May 5, 2017
d438c49
feedback as command
May 5, 2017
72a996d
utc
May 5, 2017
e3f7506
Merge branch 'master' into feedback
May 5, 2017
a38c823
remove from shell config
May 5, 2017
2a528f5
Merge branch 'feedback' of https://github.com/oakeyc/azure-cli into f…
May 5, 2017
bb78aba
flake8
May 5, 2017
7ca19c4
files
May 5, 2017
cc8a8c5
fix shell progress
May 5, 2017
ab1479d
remove extra windows characters
May 5, 2017
9a4cae2
write file
May 5, 2017
81281d3
fix test
May 5, 2017
7dfefdd
'cursor'
May 5, 2017
594f00e
small changes
May 5, 2017
1ada7aa
removed all the char windows
May 5, 2017
71eb091
change error for p2
May 5, 2017
1c1adf8
muting parsing
May 5, 2017
ff4dfe7
fix conflicts
May 8, 2017
5f62753
added progress back in
May 8, 2017
f2bb5ed
Merge branch 'feedback' of https://github.com/oakeyc/azure-cli into f…
May 8, 2017
e986cb6
Revert "Merge branch 'feedback' of https://github.com/oakeyc/azure-cl…
May 8, 2017
c755c5f
Revert "added progress back in"
May 8, 2017
f283591
revert changes
May 8, 2017
d489d53
remove with interupt
May 8, 2017
61e0e54
remove feedback
May 8, 2017
a7b32a0
shell progress tests
May 9, 2017
af4d40e
shell progress view test
May 9, 2017
af2f981
flake8
May 9, 2017
189adf8
patch back error code when you neeed it
May 12, 2017
8add8e5
small changes
May 12, 2017
ad3dc16
add dump for ending for views + force implement flush
May 15, 2017
5e8c1c7
flake8
May 15, 2017
4c4d057
mock test
May 15, 2017
0ab8de0
Merge branch 'master' of https://github.com/azure/azure-cli into prog…
May 15, 2017
1c027c5
Merge branch 'master' of https://github.com/azure/azure-cli into prog…
May 16, 2017
8a028ff
update dependencies
May 16, 2017
5f2d3fe
history
May 16, 2017
f2a912d
remove feedback stuff
May 17, 2017
347518e
remove excess
May 18, 2017
89711bb
progress with storage all good
May 19, 2017
705fb64
minor
May 19, 2017
437957e
flake8
May 19, 2017
7910d19
pylint
May 19, 2017
c157edc
minor
May 19, 2017
11b5740
flake8
May 19, 2017
a364f2f
Merge branch 'master' into progress
May 19, 2017
1b57307
import error
May 19, 2017
ae26f04
progress things
May 19, 2017
2726472
fixing tests
May 19, 2017
21d3cb0
flake8
May 19, 2017
b0c831e
tests
May 19, 2017
ebf183c
tests;
May 20, 2017
8014e06
interactive mode progress multi-process fix
May 22, 2017
d589c8d
updates
May 23, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ adal==0.4.3
applicationinsights==0.10.0
argcomplete==1.8.0
colorama==0.3.7
humanfriendly==2.4
jmespath
mock==1.3.0
paramiko==2.0.2
Expand Down
2 changes: 1 addition & 1 deletion scripts/automation/tests/verify_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import sys

ALLOWED_ERRORS = [
"has requirement azure-common[autorest]==1.1.4, but you have azure-common 1.1.5.",
"has requirement azure-common[autorest]==1.1.4, but you have azure-common 1.1.6.",
"has requirement azure-common~=1.1.5, but you have azure-common 1.1.4."
]

Expand Down
6 changes: 6 additions & 0 deletions src/azure-cli-core/azure/cli/core/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import azure.cli.core.azlogging as azlogging
from azure.cli.core.util import todict, truncate_text, CLIError, read_file_content
from azure.cli.core._config import az_config
import azure.cli.core.commands.progress as progress

import azure.cli.core.telemetry as telemetry

Expand Down Expand Up @@ -127,6 +128,11 @@ def __init__(self, configuration=None):

self.parser = AzCliCommandParser(prog='az', parents=[self.global_parser])
self.configuration = configuration
self.progress_controller = progress.ProgressHook()

def get_progress_controller(self):
self.progress_controller.init_progress(progress.get_progress_view())
return self.progress_controller

def initialize(self, configuration):
self.configuration = configuration
Expand Down
14 changes: 12 additions & 2 deletions src/azure-cli-core/azure/cli/core/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import azure.cli.core.azlogging as azlogging
import azure.cli.core.telemetry as telemetry
from azure.cli.core.util import CLIError
from azure.cli.core.application import APPLICATION
from azure.cli.core.prompting import prompt_y_n, NoTTYException
from azure.cli.core._config import az_config, DEFAULTS_SECTION
from azure.cli.core.profiles import ResourceType, supported_api_version
Expand Down Expand Up @@ -126,10 +125,13 @@ def __setattr__(self, name, value):

class LongRunningOperation(object): # pylint: disable=too-few-public-methods

def __init__(self, start_msg='', finish_msg='', poller_done_interval_ms=1000.0):
def __init__(self, start_msg='', finish_msg='',
poller_done_interval_ms=1000.0, progress_controller=None):
self.start_msg = start_msg
self.finish_msg = finish_msg
self.poller_done_interval_ms = poller_done_interval_ms
from azure.cli.core.application import APPLICATION
self.progress_controller = progress_controller or APPLICATION.get_progress_controller()

def _delay(self):
time.sleep(self.poller_done_interval_ms / 1000.0)
Expand All @@ -138,7 +140,9 @@ def __call__(self, poller):
from msrest.exceptions import ClientException
logger.info("Starting long running operation '%s'", self.start_msg)
correlation_message = ''
self.progress_controller.begin()
while not poller.done():
self.progress_controller.add(message='Running')
try:
# pylint: disable=protected-access
correlation_id = json.loads(
Expand All @@ -151,8 +155,10 @@ def __call__(self, poller):
try:
self._delay()
except KeyboardInterrupt:
self.progress_controller.stop()
logger.error('Long running operation wait cancelled. %s', correlation_message)
raise

try:
result = poller.result()
except ClientException as client_exception:
Expand All @@ -161,6 +167,7 @@ def __call__(self, poller):
fault_type='failed-long-running-operation',
summary='Unexpected client exception in {}.'.format(LongRunningOperation.__name__))
message = getattr(client_exception, 'message', client_exception)
self.progress_controller.stop()

try:
message = '{} {}'.format(
Expand All @@ -176,6 +183,7 @@ def __call__(self, poller):

logger.info("Long running operation '%s' completed with result %s",
self.start_msg, result)
self.progress_controller.end()
return result


Expand Down Expand Up @@ -232,6 +240,8 @@ def __init__(self, name, handler, description=None, table_transformer=None,

@staticmethod
def _should_load_description():
from azure.cli.core.application import APPLICATION

return not APPLICATION.session['completer_active']

def load_arguments(self):
Expand Down
165 changes: 165 additions & 0 deletions src/azure-cli-core/azure/cli/core/commands/progress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
from __future__ import division
import sys
import platform

import humanfriendly
BAR_LEN = 70

if platform.system() == 'Windows':
humanfriendly.erase_line_code = ''


class ProgressViewBase(object):
""" a view base for progress reporting """
def __init__(self, out):
self.out = out

def write(self, args):
""" writes the progress """
raise NotImplementedError

def flush(self):
""" flushes the message out the pipeline"""
raise NotImplementedError

def clear(self):
""" resets the view to neutral """
pass


class ProgressReporter(object):
""" generic progress reporter """
def __init__(self, message='', value=None, total_value=None):
self.message = message
self.value = value
self.total_val = total_value
self.closed = False

def add(self, **kwargs):
"""
adds a progress report
:param kwargs: dictionary containing 'message', 'total_val', 'value'
"""
message = kwargs.get('message', self.message)
total_val = kwargs.get('total_val', self.total_val)
value = kwargs.get('value', self.value)
if value and total_val:
assert value >= 0 and value <= total_val and total_val >= 0
self.closed = value == total_val
self.total_val = total_val
self.value = value
self.message = message

def report(self):
""" report the progress """
percent = self.value / self.total_val if self.value is not None and self.total_val else None
return {'message': self.message, 'percent': percent}


class ProgressHook(object):
""" sends the progress to the view """
def __init__(self):
self.reporter = ProgressReporter()
self.active_progress = None

def init_progress(self, progress_view):
""" activate a view """
self.active_progress = progress_view

def add(self, **kwargs):
""" adds a progress report """
self.reporter.add(**kwargs)
self.update()

def update(self):
""" updates the view with the progress """
self.active_progress.write(self.reporter.report())
self.active_progress.flush()

def stop(self):
""" if there is an abupt stop before ending """
self.reporter.closed = True
self.add(message='Interrupted')
self.active_progress.clear()

def begin(self, **kwargs):
""" start reporting progress """
kwargs['message'] = kwargs.get('message', 'Starting')
self.add(**kwargs)

def end(self, **kwargs):
""" ending reporting of progress """
kwargs['message'] = kwargs.get('message', 'Finished')
self.reporter.closed = True
self.add(**kwargs)
self.active_progress.clear()

def is_running(self):
""" whether progress is continuing """
return not self.reporter.closed


class IndeterminateStandardOut(ProgressViewBase):
""" custom output for progress reporting """
def __init__(self, out=None):
super(IndeterminateStandardOut, self).__init__(
out if out else sys.stderr)
self.spinner = None

def write(self, args):
"""
writes the progress
:param args: dictionary containing key 'message'
"""
if self.spinner is None:
self.spinner = humanfriendly.Spinner(
label='In Progress', stream=self.out, hide_cursor=False)
msg = args.get('message', 'In Progress')
self.spinner.step(label=msg)

def flush(self):
self.out.flush()


def _format_value(msg, percent):
bar_len = BAR_LEN - len(msg) - 1
completed = int(bar_len * percent)

message = '\r{}['.format(msg)
message += ('#' * completed).ljust(bar_len)
message += '] {:.4%}'.format(percent)
return message


class DeterminateStandardOut(ProgressViewBase):
""" custom output for progress reporting """
def __init__(self, out=None):
super(DeterminateStandardOut, self).__init__(out if out else sys.stderr)

def write(self, args):
"""
writes the progress
:param args: args is a dictionary containing 'percent', 'message'
"""
percent = args.get('percent', 0)
message = args.get('message', '')

if percent:
percent = percent
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line does nothing right? Should remove if true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right; I just removed it

progress = _format_value(message, percent)
self.out.write(progress)

def flush(self):
pass


def get_progress_view(determinant=False, outstream=sys.stderr):
""" gets your view """
if determinant:
return DeterminateStandardOut(out=outstream)
else:
return IndeterminateStandardOut(out=outstream)
1 change: 0 additions & 1 deletion src/azure-cli-core/azure/cli/core/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,6 @@ def validation_error(self, message):
def error(self, message):
telemetry.set_user_fault('parse error: {}'.format(message))
self._handle_command_package_error(message)

args = {'prog': self.prog, 'message': message}
logger.error('%(prog)s: error: %(message)s', args)
self.print_usage(sys.stderr)
Expand Down
1 change: 1 addition & 0 deletions src/azure-cli-core/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
'applicationinsights',
'argcomplete>=1.8.0',
'colorama',
'humanfriendly',
'jmespath',
'msrest>=0.4.4',
'msrestazure>=0.4.7',
Expand Down
100 changes: 100 additions & 0 deletions src/azure-cli-core/tests/test_progress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
import unittest

import azure.cli.core.commands.progress as progress


class MockOutstream(progress.ProgressViewBase):
""" mock outstream for testing """
def __init__(self):
self.string = ''

def write(self, message):
self.string = message

def flush(self):
pass


class TestProgress(unittest.TestCase): # pylint: disable=too-many-public-methods
""" test the progress reporting """

def test_progress_indicator_det_model(self):
""" test the progress reporter """
reporter = progress.ProgressReporter()
args = reporter.report()
self.assertEqual(args['message'], '')
self.assertEqual(args['percent'], None)

reporter.add(message='Progress', total_val=10, value=0)
self.assertEqual(reporter.message, 'Progress')
self.assertEqual(reporter.value, 0)
self.assertEqual(reporter.total_val, 10)
args = reporter.report()
self.assertEqual(args['message'], 'Progress')
self.assertEqual(args['percent'], 0)

with self.assertRaises(AssertionError):
reporter.add(message='In words', total_val=-1, value=10)
with self.assertRaises(AssertionError):
reporter.add(message='In words', total_val=1, value=-10)
with self.assertRaises(AssertionError):
reporter.add(message='In words', total_val=30, value=12340)

reporter = progress.ProgressReporter()
message = reporter.report()
self.assertEqual(message['message'], '')

reporter.add(message='Progress')
self.assertEqual(reporter.message, 'Progress')

message = reporter.report()
self.assertEqual(message['message'], 'Progress')

def test_progress_indicator_indet_stdview(self):
""" tests the indeterminate progress standardout view """
outstream = MockOutstream()
view = progress.IndeterminateStandardOut(out=outstream)
view.write({})
self.assertEqual(view.spinner.label, 'In Progress')
before = view.spinner.total
view.write({})
after = view.spinner.total
self.assertTrue(after >= before)
view.write({'message': 'TESTING'})

def test_progress_indicator_det_stdview(self):
""" test the determinate progress standardout view """
outstream = MockOutstream()
view = progress.DeterminateStandardOut(out=outstream)
view.write({'message': 'hihi', 'percent': .5})
# 95 length, 48 complete, 4 dec percent
bar_str = ('#' * int(.5 * 65)).ljust(65)
self.assertEqual(outstream.string, '\rhihi[{}] {:.4%}'.format(bar_str, .5))

view.write({'message': '', 'percent': .9})
# 99 length, 90 complete, 4 dec percent
bar_str = ('#' * int(.9 * 69)).ljust(69)
self.assertEqual(outstream.string, '\r[{}] {:.4%}'.format(bar_str, .9))

def test_progress_indicator_controller(self):
""" tests the controller for progress reporting """
controller = progress.ProgressHook()
view = MockOutstream()

controller.init_progress(view)
self.assertTrue(view == controller.active_progress)

controller.begin()

self.assertEqual(controller.active_progress.string['message'], 'Starting')

controller.end()
self.assertEqual(controller.active_progress.string['message'], 'Finished')


if __name__ == '__main__':
unittest.main()
Loading