4040
4141"""
4242
43+ from __future__ import print_function
4344from cookielib import MozillaCookieJar
4445from getpass import getpass
46+ from itertools import cycle
47+ from threading import Event
48+ from threading import Thread
4549import glob
4650import json
4751import os .path
@@ -180,6 +184,65 @@ def save(self):
180184 return data
181185
182186
187+ _counter = 0
188+
189+
190+ def _newname (template = "Thread-{}" ):
191+ """Generate a new thread name."""
192+ global _counter
193+ _counter += 1
194+ return template .format (_counter )
195+
196+
197+ FRAMES = {
198+ 'arrow' : ['^' , '>' , 'v' , '<' ],
199+ 'dots' : ['...' , 'o..' , '.o.' , '..o' ],
200+ 'ligatures' : ['bq' , 'dp' , 'qb' , 'pd' ],
201+ 'lines' : [' ' , '-' , '=' , '#' , '=' , '-' ],
202+ 'slash' : ['-' , '\\ ' , '|' , '/' ],
203+ }
204+
205+
206+ class TextProgress (Thread ):
207+ """Show progress for a long-running operation on the command-line."""
208+
209+ def __init__ (self , group = None , target = None , name = None , args = (), kwargs = {}):
210+ name = name or _newname ("TextProgress-Thread-{}" )
211+ style = kwargs .get ('style' , 'dots' )
212+ super (TextProgress , self ).__init__ (
213+ group , target , name , args , kwargs )
214+ self .daemon = True
215+ self .cancelled = Event ()
216+ self .frames = cycle (FRAMES [style ])
217+
218+ def run (self ):
219+ """Write ASCII progress animation frames to stdout."""
220+ time .sleep (0.5 )
221+ self ._write_frame (self .frames .next (), erase = False )
222+ while not self .cancelled .is_set ():
223+ time .sleep (0.4 )
224+ self ._write_frame (self .frames .next ())
225+
226+ def cancel (self ):
227+ """Set the animation thread as cancelled."""
228+ self .cancelled .set ()
229+ # clear the animation
230+ sys .stdout .write ('\b ' * (len (self .frames .next ()) + 2 ))
231+ sys .stdout .flush ()
232+
233+ def _write_frame (self , frame , erase = True ):
234+ if erase :
235+ backspaces = '\b ' * (len (frame ) + 2 )
236+ else :
237+ backspaces = ''
238+ sys .stdout .write ("{} {} " .format (backspaces , frame ))
239+ # flush stdout or we won't see the frame
240+ sys .stdout .flush ()
241+
242+
243+ progress = TextProgress ()
244+
245+
183246def dictify (args ):
184247 """Converts a list of key=val strings into a python dict.
185248
@@ -289,12 +352,12 @@ def auth_register(self, args):
289352 if self .auth_login (login_args ) is False :
290353 print ('Login failed' )
291354 return
292- print
355+ print ()
293356 self .keys_add ({})
294- print
357+ print ()
295358 self .providers_discover ({})
296- print
297- print 'Use `deis create --flavor=ec2-us-east-1` to create a new formation'
359+ print ()
360+ print ( 'Use `deis create --flavor=ec2-us-east-1` to create a new formation' )
298361 else :
299362 print ('Registration failed' , response .content )
300363 return False
@@ -516,13 +579,13 @@ def containers_list(self, args):
516579 c_map = {}
517580 for item in data ['results' ]:
518581 c_map .setdefault (item ['type' ], []).append (item )
519- print
582+ print ()
520583 for c_type in c_map .keys ():
521584 command = procfile .get (c_type , '<none>' )
522585 print ("--- {c_type}: `{command}`" .format (** locals ()))
523586 for c in c_map [c_type ]:
524587 print ("{type}.{num} up {created} ({node})" .format (** c ))
525- print
588+ print ()
526589 else :
527590 print ('Error!' , response .text )
528591
@@ -542,10 +605,14 @@ def containers_scale(self, args):
542605 typ , count = type_num .split ('=' )
543606 body .update ({typ : int (count )})
544607 print ('Scaling containers... but first, coffee!' )
545- before = time .time ()
546- response = self ._dispatch ('post' ,
547- "/api/formations/{}/scale/containers" .format (formation ),
548- json .dumps (body ))
608+ try :
609+ progress .start ()
610+ before = time .time ()
611+ response = self ._dispatch ('post' ,
612+ "/api/formations/{}/scale/containers" .format (formation ),
613+ json .dumps (body ))
614+ finally :
615+ progress .cancel ()
549616 if response .status_code == requests .codes .ok : # @UndefinedVariable
550617 print ('done in {}s\n ' .format (int (time .time () - before )))
551618 self .containers_list ({})
@@ -625,7 +692,7 @@ def flavors_list(self, args):
625692 if response .status_code == requests .codes .ok : # @UndefinedVariable
626693 data = response .json ()
627694 if data ['count' ] == 0 :
628- print 'No flavors found'
695+ print ( 'No flavors found' )
629696 return
630697 print ("=== {owner} Flavors" .format (** data ['results' ][0 ]))
631698 for item in data ['results' ]:
@@ -664,7 +731,7 @@ def formations_create(self, args):
664731 try :
665732 self ._session .git_root () # check for a git repository
666733 except EnvironmentError :
667- print 'No git repository found, use `git init` to create one.'
734+ print ( 'No git repository found, use `git init` to create one' )
668735 return
669736 for opt in ('--id' ,):
670737 o = args .get (opt )
@@ -675,7 +742,7 @@ def formations_create(self, args):
675742 if flavor :
676743 response = self ._dispatch ('get' , '/api/flavors/{}' .format (flavor ))
677744 if response .status_code != 200 :
678- print 'Flavor not found'
745+ print ( 'Flavor not found' )
679746 return
680747 sys .stdout .write ('Creating formation... ' )
681748 sys .stdout .flush ()
@@ -697,7 +764,7 @@ def formations_create(self, args):
697764 print ('Git remote deis added' )
698765 # create default layers if a flavor was provided
699766 if flavor :
700- print
767+ print ()
701768 self .layers_create ({'<id>' : 'runtime' , '<flavor>' : flavor })
702769 self .layers_create ({'<id>' : 'proxy' , '<flavor>' : flavor })
703770 print ('\n Use `deis layers:scale proxy=1 runtime=1` to scale a basic formation' )
@@ -717,12 +784,12 @@ def formations_info(self, args):
717784 if response .status_code == requests .codes .ok : # @UndefinedVariable
718785 data = response .json ()
719786 print ("=== {} Formation" .format (formation ))
720- print
787+ print ()
721788 args = {'<formation>' : data ['id' ]}
722789 self .layers_list (args )
723- print
790+ print ()
724791 self .nodes_list (args )
725- print
792+ print ()
726793 self .containers_list (args )
727794 else :
728795 print ('Error!' , response .text )
@@ -737,7 +804,7 @@ def formations_list(self, args):
737804 if response .status_code == requests .codes .ok : # @UndefinedVariable
738805 data = response .json ()
739806 if data ['count' ] == 0 :
740- print 'No formations found'
807+ print ( 'No formations found' )
741808 return
742809 print ("=== {owner} Formations" .format (** data ['results' ][0 ]))
743810 for item in data ['results' ]:
@@ -762,19 +829,23 @@ def formations_destroy(self, args):
762829 if confirm == formation :
763830 pass
764831 else :
765- print """
832+ print ( """
766833 ! WARNING: Potentially Destructive Action
767834 ! This command will destroy: {formation}
768835 ! To proceed, type "{formation}" or re-run this command with --confirm={formation}
769- """ .format (** locals ())
836+ """ .format (** locals ()))
770837 confirm = raw_input ('> ' ).strip ('\n ' )
771838 if confirm != formation :
772839 print ('Destroy aborted' )
773840 return
774841 sys .stdout .write ("Destroying {}... " .format (formation ))
775842 sys .stdout .flush ()
776- before = time .time ()
777- response = self ._dispatch ('delete' , "/api/formations/{}" .format (formation ))
843+ try :
844+ progress .start ()
845+ before = time .time ()
846+ response = self ._dispatch ('delete' , "/api/formations/{}" .format (formation ))
847+ finally :
848+ progress .cancel ()
778849 if response .status_code in (requests .codes .no_content , # @UndefinedVariable
779850 requests .codes .not_found ): # @UndefinedVariable
780851 print ('done in {}s' .format (int (time .time () - before )))
@@ -825,9 +896,13 @@ def formations_converge(self, args):
825896 formation = self ._session .formation
826897 sys .stdout .write ('Converging {}... ' .format (formation ))
827898 sys .stdout .flush ()
828- before = time .time ()
829- response = self ._dispatch ('post' ,
830- "/api/formations/{}/converge" .format (formation ))
899+ try :
900+ progress .start ()
901+ before = time .time ()
902+ response = self ._dispatch ('post' ,
903+ "/api/formations/{}/converge" .format (formation ))
904+ finally :
905+ progress .cancel ()
831906 if response .status_code == requests .codes .ok : # @UndefinedVariable
832907 print ('done in {}s' .format (int (time .time () - before )))
833908 databag = json .loads (response .content )
@@ -866,13 +941,13 @@ def keys_add(self, args):
866941 path = pubkeys [int (inp ) - 1 ]
867942 key_id = path .split (os .path .sep )[- 1 ].replace ('.pub' , '' )
868943 except :
869- print 'Aborting'
944+ print ( 'Aborting' )
870945 return
871946 with open (path ) as f :
872947 data = f .read ()
873948 match = re .match (r'^(ssh-...) ([^ ]+) (.+)' , data )
874949 if not match :
875- print 'Could not parse public key material'
950+ print ( 'Could not parse public key material' )
876951 return
877952 key_type , key_str , _key_comment = match .groups ()
878953 body = {'id' : key_id , 'public' : "{0} {1}" .format (key_type , key_str )}
@@ -894,7 +969,7 @@ def keys_list(self, args):
894969 if response .status_code == requests .codes .ok : # @UndefinedVariable
895970 data = response .json ()
896971 if data ['count' ] == 0 :
897- print 'No keys found'
972+ print ( 'No keys found' )
898973 return
899974 print ("=== {owner} Keys" .format (** data ['results' ][0 ]))
900975 for key in data ['results' ]:
@@ -968,9 +1043,13 @@ def layers_create(self, args):
9681043 body ['run_list' ] = 'recipe[deis],recipe[deis::proxy]'
9691044 sys .stdout .write ("Creating {} layer... " .format (args ['<id>' ]))
9701045 sys .stdout .flush ()
971- before = time .time ()
972- response = self ._dispatch ('post' , "/api/formations/{}/layers" .format (formation ),
973- json .dumps (body ))
1046+ try :
1047+ progress .start ()
1048+ before = time .time ()
1049+ response = self ._dispatch ('post' , "/api/formations/{}/layers" .format (formation ),
1050+ json .dumps (body ))
1051+ finally :
1052+ progress .cancel ()
9741053 if response .status_code == requests .codes .created : # @UndefinedVariable
9751054 print ('done in {}s' .format (int (time .time () - before )))
9761055 else :
@@ -988,9 +1067,13 @@ def layers_destroy(self, args):
9881067 layer = args ['<id>' ] # noqa
9891068 sys .stdout .write ("Destroying {layer} layer... " .format (** locals ()))
9901069 sys .stdout .flush ()
991- before = time .time ()
992- response = self ._dispatch (
993- 'delete' , "/api/formations/{formation}/layers/{layer}" .format (** locals ()))
1070+ try :
1071+ progress .start ()
1072+ before = time .time ()
1073+ response = self ._dispatch (
1074+ 'delete' , "/api/formations/{formation}/layers/{layer}" .format (** locals ()))
1075+ finally :
1076+ progress .cancel ()
9941077 if response .status_code == requests .codes .no_content : # @UndefinedVariable
9951078 print ('done in {}s' .format (int (time .time () - before )))
9961079 else :
@@ -1033,11 +1116,15 @@ def layers_scale(self, args):
10331116 typ , count = type_num .split ('=' )
10341117 body .update ({typ : int (count )})
10351118 print ('Scaling layers... but first, coffee!' )
1036- before = time .time ()
1037- # TODO: add threaded spinner to print dots
1038- response = self ._dispatch ('post' ,
1039- "/api/formations/{}/scale/layers" .format (formation ),
1040- json .dumps (body ))
1119+ try :
1120+ progress .start ()
1121+ before = time .time ()
1122+ # TODO: add threaded spinner to print dots
1123+ response = self ._dispatch ('post' ,
1124+ "/api/formations/{}/scale/layers" .format (formation ),
1125+ json .dumps (body ))
1126+ finally :
1127+ progress .cancel ()
10411128 if response .status_code == requests .codes .ok : # @UndefinedVariable
10421129 print ('done in {}s\n ' .format (int (time .time () - before )))
10431130 print ('Use `git push deis master` to deploy to your formation' )
@@ -1128,9 +1215,13 @@ def nodes_destroy(self, args):
11281215 node = args ['<id>' ]
11291216 sys .stdout .write ("Destroying {}... " .format (node ))
11301217 sys .stdout .flush ()
1131- before = time .time ()
1132- response = self ._dispatch ('delete' ,
1133- "/api/formations/{formation}/nodes/{node}" .format (** locals ()))
1218+ try :
1219+ progress .start ()
1220+ before = time .time ()
1221+ response = self ._dispatch (
1222+ 'delete' , "/api/formations/{formation}/nodes/{node}" .format (** locals ()))
1223+ finally :
1224+ progress .cancel ()
11341225 if response .status_code == requests .codes .no_content : # @UndefinedVariable
11351226 print ('done in {}s\n ' .format (int (time .time () - before )))
11361227 else :
@@ -1222,7 +1313,7 @@ def providers_discover(self, args):
12221313 print ("Found EC2 credentials: {}" .format (os .environ ['AWS_ACCESS_KEY' ]))
12231314 inp = raw_input ('Import these credentials? (y/n) : ' )
12241315 if inp .lower ().strip ('\n ' ) != 'y' :
1225- print 'Aborting.'
1316+ print ( 'Aborting.' )
12261317 return
12271318 creds = {'access_key' : os .environ ['AWS_ACCESS_KEY' ],
12281319 'secret_key' : os .environ ['AWS_SECRET_KEY' ]}
@@ -1232,11 +1323,11 @@ def providers_discover(self, args):
12321323 response = self ._dispatch ('patch' , '/api/providers/ec2' ,
12331324 json .dumps (body ))
12341325 if response .status_code == requests .codes .ok : # @UndefinedVariable
1235- print 'done'
1326+ print ( 'done' )
12361327 else :
12371328 print ('Error!' , response .text )
12381329 else :
1239- print 'No credentials discovered, did you install the EC2 Command Line tools?'
1330+ print ( 'No credentials discovered, did you install the EC2 Command Line tools?' )
12401331 return
12411332
12421333 def providers_info (self , args ):
@@ -1262,7 +1353,7 @@ def providers_list(self, args):
12621353 if response .status_code == requests .codes .ok : # @UndefinedVariable
12631354 data = response .json ()
12641355 if data ['count' ] == 0 :
1265- print 'No providers found'
1356+ print ( 'No providers found' )
12661357 return
12671358 print ("=== {owner} Providers" .format (** data ['results' ][0 ]))
12681359 for item in data ['results' ]:
@@ -1372,7 +1463,7 @@ def main():
13721463 if help_flag :
13731464 if cmd != 'help' :
13741465 if cmd in dir (cli ):
1375- print trim (getattr (cli , cmd ).__doc__ )
1466+ print ( trim (getattr (cli , cmd ).__doc__ ) )
13761467 return
13771468 docopt (__doc__ , argv = ['--help' ])
13781469 # re-parse docopt with the relevant docstring
0 commit comments