class HummingbotCLI: def __init__(self, input_handler: Callable, bindings: KeyBindings, completer: Completer): use_asyncio_event_loop() self.input_field = create_input_field(completer=completer) self.output_field = create_output_field() self.log_field = create_log_field() self.layout = generate_layout(self.input_field, self.output_field, self.log_field) self.bindings = bindings self.input_handler = input_handler self.input_field.accept_handler = self.accept self.app = Application(layout=self.layout, full_screen=True, key_bindings=self.bindings, style=load_style(), mouse_support=True, clipboard=PyperclipClipboard()) self.log_lines: Deque[str] = deque() self.log(HEADER) # settings self.prompt_text = ">>> " self.pending_input = None self.input_event = None self.hide_input = False async def run(self): await self.app.run_async().to_asyncio_future() def accept(self, buff): self.pending_input = self.input_field.text.strip() if self.input_event: self.input_event.set() try: if self.hide_input: output = '' else: output = '\n>>> {}'.format(self.input_field.text, ) self.input_field.buffer.append_to_history() except BaseException as e: output = str(e) self.log(output) self.input_handler(self.input_field.text) def clear_input(self): self.pending_input = None def log(self, text: str): self.log_lines.extend(str(text).split('\n')) while len(self.log_lines) > MAXIMUM_OUTPUT_PANE_LINE_COUNT: self.log_lines.popleft() new_text: str = "\n".join(self.log_lines) self.output_field.buffer.document = Document( text=new_text, cursor_position=len(new_text)) def change_prompt(self, prompt: str, is_password: bool = False): self.prompt_text = prompt processors = [] if is_password: processors.append(PasswordProcessor()) processors.append(BeforeInput(prompt)) self.input_field.control.input_processors = processors async def prompt(self, prompt: str, is_password: bool = False) -> str: self.change_prompt(prompt, is_password) self.app.invalidate() self.input_event = asyncio.Event() await self.input_event.wait() temp = self.pending_input self.clear_input() self.input_event = None if is_password: masked_string = "*" * len(temp) self.log(f"{prompt}{masked_string}") else: self.log(f"{prompt}{temp}") return temp def set_text(self, new_text: str): self.input_field.document = Document(text=new_text, cursor_position=len(new_text)) def toggle_hide_input(self): self.hide_input = not self.hide_input def exit(self): self.app.exit()
class HummingbotCLI: def __init__(self, input_handler: Callable, bindings: KeyBindings, completer: Completer): self.search_field = create_search_field() self.input_field = create_input_field(completer=completer) self.output_field = create_output_field() self.log_field = create_log_field(self.search_field) self.timer = create_timer() self.process_usage = create_process_monitor() self.trade_monitor = create_trade_monitor() self.layout = generate_layout(self.input_field, self.output_field, self.log_field, self.search_field, self.timer, self.process_usage, self.trade_monitor) # add self.to_stop_config to know if cancel is triggered self.to_stop_config: bool = False self.live_updates = False self.bindings = bindings self.input_handler = input_handler self.input_field.accept_handler = self.accept self.app: Optional[Application] = None # settings self.prompt_text = ">>> " self.pending_input = None self.input_event = None self.hide_input = False # start ui tasks loop = asyncio.get_event_loop() loop.create_task(start_timer(self.timer)) loop.create_task(start_process_monitor(self.process_usage)) loop.create_task(start_trade_monitor(self.trade_monitor)) async def run(self): self.app = Application(layout=self.layout, full_screen=True, key_bindings=self.bindings, style=load_style(), mouse_support=True, clipboard=PyperclipClipboard()) await self.app.run_async() def accept(self, buff): self.pending_input = self.input_field.text.strip() if self.input_event: self.input_event.set() try: if self.hide_input: output = '' else: output = '\n>>> {}'.format(self.input_field.text, ) self.input_field.buffer.append_to_history() except BaseException as e: output = str(e) self.log(output) self.input_handler(self.input_field.text) def clear_input(self): self.pending_input = None def log(self, text: str, save_log: bool = True): if save_log: if self.live_updates: self.output_field.log(text, silent=True) else: self.output_field.log(text) else: self.output_field.log(text, save_log=False) def change_prompt(self, prompt: str, is_password: bool = False): self.prompt_text = prompt processors = [] if is_password: processors.append(PasswordProcessor()) processors.append(BeforeInput(prompt)) self.input_field.control.input_processors = processors async def prompt(self, prompt: str, is_password: bool = False) -> str: self.change_prompt(prompt, is_password) self.app.invalidate() self.input_event = asyncio.Event() await self.input_event.wait() temp = self.pending_input self.clear_input() self.input_event = None if is_password: masked_string = "*" * len(temp) self.log(f"{prompt}{masked_string}") else: self.log(f"{prompt}{temp}") return temp def set_text(self, new_text: str): self.input_field.document = Document(text=new_text, cursor_position=len(new_text)) def toggle_hide_input(self): self.hide_input = not self.hide_input def exit(self): self.app.exit()
class ProgressBar(object): """ Progress bar context manager. Usage :: with ProgressBar(...) as pb: for item in pb(data): ... :param title: Text to be displayed above the progress bars. This can be a callable or formatted text as well. :param formatters: List of :class:`.Formatter` instances. :param bottom_toolbar: Text to be displayed in the bottom toolbar. This can be a callable or formatted text. :param style: :class:`prompt_toolkit.styles.BaseStyle` instance. :param key_bindings: :class:`.KeyBindings` instance. :param file: The file object used for rendering, by default `sys.stderr` is used. :param color_depth: `prompt_toolkit` `ColorDepth` instance. :param output: :class:`~prompt_toolkit.output.Output` instance. :param input: :class:`~prompt_toolkit.input.Input` instance. """ def __init__(self, title=None, formatters=None, bottom_toolbar=None, style=None, key_bindings=None, file=None, color_depth=None, output=None, input=None): assert formatters is None or (isinstance(formatters, list) and all( isinstance(fo, Formatter) for fo in formatters)) assert style is None or isinstance(style, BaseStyle) assert key_bindings is None or isinstance(key_bindings, KeyBindings) self.title = title self.formatters = formatters or create_default_formatters() self.bottom_toolbar = bottom_toolbar self.counters = [] self.style = style self.key_bindings = key_bindings # Note that we use __stderr__ as default error output, because that # works best with `patch_stdout`. self.color_depth = color_depth self.output = output or create_output(stdout=file or sys.__stderr__) self.input = input or get_default_input() self._thread = None self._loop = get_event_loop() self._previous_winch_handler = None self._has_sigwinch = False def __enter__(self): # Create UI Application. title_toolbar = ConditionalContainer( Window(FormattedTextControl(lambda: self.title), height=1, style='class:progressbar,title'), filter=Condition(lambda: self.title is not None)) bottom_toolbar = ConditionalContainer( Window(FormattedTextControl(lambda: self.bottom_toolbar, style='class:bottom-toolbar.text'), style='class:bottom-toolbar', height=1), filter=~is_done & renderer_height_is_known & Condition(lambda: self.bottom_toolbar is not None)) def width_for_formatter(formatter): # Needs to be passed as callable (partial) to the 'width' # parameter, because we want to call it on every resize. return formatter.get_width(progress_bar=self) progress_controls = [ Window(content=_ProgressControl(self, f), width=functools.partial(width_for_formatter, f)) for f in self.formatters ] self.app = Application( min_redraw_interval=.05, layout=Layout( HSplit([ title_toolbar, VSplit(progress_controls, height=lambda: D(preferred=len(self.counters), max=len(self.counters))), Window(), bottom_toolbar, ])), style=self.style, key_bindings=self.key_bindings, color_depth=self.color_depth, output=self.output, input=self.input) # Run application in different thread. def run(): with _auto_refresh_context(self.app, .3): try: self.app.run() except BaseException as e: traceback.print_exc() print(e) self._thread = threading.Thread(target=run) self._thread.start() # Attach WINCH signal handler in main thread. # (Interrupt that we receive during resize events.) self._has_sigwinch = hasattr(signal, 'SIGWINCH') and in_main_thread() if self._has_sigwinch: self._previous_winch_handler = self._loop.add_signal_handler( signal.SIGWINCH, self.app.invalidate) return self def __exit__(self, *a): # Quit UI application. if self.app.is_running: self.app.exit() # Remove WINCH handler. if self._has_sigwinch: self._loop.add_signal_handler(signal.SIGWINCH, self._previous_winch_handler) self._thread.join() def __call__(self, data=None, label='', remove_when_done=False, total=None): """ Start a new counter. :param label: Title text or description for this progress. (This can be formatted text as well). :param remove_when_done: When `True`, hide this progress bar. :param total: Specify the maximum value if it can't be calculated by calling ``len``. """ assert is_formatted_text(label) assert isinstance(remove_when_done, bool) counter = ProgressBarCounter(self, data, label=label, remove_when_done=remove_when_done, total=total) self.counters.append(counter) return counter def invalidate(self): self.app.invalidate()
class View(object): def __init__(self, configuration, exchange): self._project_name = configuration.project.name self._local_dir = configuration.local_dir self._remote_directory = None self._exchange = exchange self._loop = get_event_loop() self._current_screen = None self.root_container = HSplit([Window()]) self.layout = Layout(container=self.root_container) self._render() self.bindings = self._create_bindings() self.application = Application(layout=self.layout, key_bindings=self.bindings, full_screen=True) self._exchange.subscribe(Messages.REMOTE_DIRECTORY_SET, self._set_remote_dir) def _render(self): top_toolbar = self._render_top_toolbar() if self._current_screen is not None: main_container = self._current_screen.main_container else: main_container = Window() self.root_container.children = [top_toolbar, main_container] def mount(self, screen): """ Mount a screen into the view. The screen must have a `main_container` attribute and, optionally, a `bindings` attribute. """ if self._current_screen is not None: self._current_screen.stop() self._current_screen = screen if screen.bindings is not None: if screen.use_default_bindings: merged_key_bindings = merge_key_bindings( [self.bindings, screen.bindings]) self.application.key_bindings = merged_key_bindings else: self.application.key_bindings = screen.bindings else: # Screen does not define additional keybindings self.application.key_bindings = self.bindings self._render() screen.on_mount(self.application) def start(self): def run(): try: self.application.run() except Exception as e: traceback.print_exc() print(e) self._thread = threading.Thread(target=run) self._thread.start() self._register_resize_handler() def stop(self): if self.application.is_running: self.application.exit() self._remove_resize_handler() def _register_resize_handler(self): # The application receives the signal SIGWINCH # when the terminal has been resized. self._has_sigwinch = hasattr(signal, "SIGWINCH") if self._has_sigwinch: self._previous_winch_handler = self._loop.add_signal_handler( signal.SIGWINCH, self._on_resize) def _remove_resize_handler(self): # Remove WINCH handler. if self._has_sigwinch: self._loop.add_signal_handler(signal.SIGWINCH, self._previous_winch_handler) def _on_resize(self): logging.info("Handling application resize event.") self.application.invalidate() def _render_top_toolbar(self): remote_directory_text = (":{}".format(self._remote_directory) if self._remote_directory is not None else "") top_text = ("[Faculty Platform synchronizer] " "{local_dir} -> " "{project_name}{remote_directory_text}").format( local_dir=self._local_dir, project_name=self._project_name, remote_directory_text=remote_directory_text, ) top_toolbar = Window(FormattedTextControl(top_text), height=1, style="reverse") return top_toolbar def _create_bindings(self): bindings = KeyBindings() @bindings.add("c-c") @bindings.add("q") def _(event): self._exchange.publish(Messages.STOP_CALLED) return bindings def _set_remote_dir(self, directory): self._remote_directory = directory self._render()
class App: """Main app class to render the UI and run the application. It holds the top level layout of the UI and also contains several useful public method that user can leverage in their own customization. Its a bridge between all UI element and acts like a root level which has access to the entire application states. Similar to a app.js in React.js. Args: config: A :class:`~s3fm.api.config.Config` instance. no_history: Skip reading history. :class:`~s3fm.api.history.History` won't be loaded. """ def __init__(self, config: Config = None, no_history: bool = False) -> None: config = config or Config() self._style = Style.from_dict(dict(config.style)) self._rendered = False self._no_history = no_history self._layout_mode = LayoutMode.vertical self._border = config.app.border self._current_focus = Pane.left self._previous_focus = None self._filepane_focus = Pane.left self._custom_effects = config.app.custom_effects self._history = History( dir_max_size=config.history.dir_max_size, cmd_max_size=config.history.cmd_max_size, ) self._kb_mode = KBMode.normal self._error_mode = Condition(lambda: self._kb_mode == KBMode.error) self._command_mode = Condition(lambda: self._kb_mode == KBMode.command) self._normal_mode = Condition(lambda: self._kb_mode == KBMode.normal) self._search_mode = Condition(lambda: self._kb_mode == KBMode.search) self._reverse_search_mode = Condition( lambda: self._kb_mode == KBMode.reverse_search) self._error = "" self._error_type = ErrorType.error self._error_pane = ErrorPane( error=self._error_mode, message=lambda: self._error, error_type=lambda: self._error_type, ) self._layout_single = Condition( lambda: self._layout_mode == LayoutMode.single) self._layout_vertical = Condition( lambda: self._layout_mode == LayoutMode.vertical) self._left_pane = FilePane( pane_id=Pane.left, spinner_config=config.spinner, linemode_config=config.linemode, app_config=config.app, redraw=self.redraw, layout_single=self._layout_single, layout_vertical=self._layout_vertical, focus=lambda: self._filepane_focus, history=self._history, set_error=self.set_error, ) self._right_pane = FilePane( pane_id=Pane.right, spinner_config=config.spinner, linemode_config=config.linemode, app_config=config.app, redraw=self.redraw, layout_single=self._layout_single, layout_vertical=self._layout_vertical, focus=lambda: self._filepane_focus, history=self._history, set_error=self.set_error, ) self._command_pane = CommandPane(app=self) self._option_pane = OptionPane() self._kb = KB( app=self, kb_maps=config.kb.kb_maps, custom_kb_maps=config.kb.custom_kb_maps, custom_kb_lookup=config.kb.custom_kb_lookup, ) self._app = Application( layout=self.layout, full_screen=True, after_render=self._after_render, style=self._style, key_bindings=self._kb, ) def redraw(self) -> None: """Instruct the app to redraw itself to the terminal. This is useful when trying to force an UI update of the :class:`App`. """ self._app.invalidate() async def _load_pane_data(self, pane: FilePane) -> None: """Load the data for the target pane and refersh the app. Args: pane: A `FilePane` instance to load data. """ await pane.load_data() self.redraw() async def _render_task(self) -> None: """Read history and instruct left/right pane to load appropriate data. When `App` is created, `KB` is not activated and will only be activated once `History` is processed. This decision is made because `Hache` may cause the `App` UI to change and confuse the user. """ if not self._no_history: await self._history.read() self._left_pane.mode = self._history.left_mode self._right_pane.mode = self._history.right_mode self._left_pane.selected_file_index = self._history.left_index self._right_pane.selected_file_index = self._history.right_index self._left_pane.path = self._history.left_path self._right_pane.path = self._history.right_path self.pane_focus(self._history.focus) self.layout_switch(self._history.layout) self._kb.activated = True await asyncio.gather( self._load_pane_data(pane=self._left_pane), self._load_pane_data(pane=self._right_pane), ) def _after_render(self, _) -> None: """Run this function every time the `App` is re-rendered, same as `useEffect` in react.js. Using a class state `self._rendered` to force this function to only run once when the `App` is first created. Loading all relevant data in this method can turn the whole data loading into an async experience. """ for use_effect in self._custom_effects: use_effect(self) if not self._rendered: self._rendered = True self._left_pane.loading = True self._right_pane.loading = True asyncio.create_task(self._render_task()) async def run(self) -> None: """Start the application in async mode.""" await self._app.run_async() def pane_focus(self, pane: Pane) -> None: """Focus specified pane and set the focus state. Args: pane: Target pane to focus. Examples: >>> from s3fm.app import App >>> from s3fm.enums import Pane >>> app = App() # doctest: +SKIP >>> app.pane_focus(Pane.left) # doctest: +SKIP """ if pane in self.filepanes: self._kb_mode = KBMode.normal self._filepane_focus = pane else: _kb_mode_map = { CommandMode.command: KBMode.command, CommandMode.search: KBMode.search, CommandMode.reverse_search: KBMode.reverse_search, } self._kb_mode = _kb_mode_map.get(self._command_pane.mode, KBMode.command) self._previous_focus = self._current_focus self._current_focus = pane self._app.layout.focus(self.current_focus) def pane_focus_other(self) -> None: """Focus the other filepane. Theres only a maximum of 2 filepane in the app currently. Use this method to focus the other filepane. This method won't have any effect if the current UI only have one filepane. """ if not self._layout_single(): self.pane_focus(Pane.left if self._current_focus == Pane.right else Pane.right) def cmd_focus(self, mode=CommandMode.command) -> None: """Focus the commandpane. Args: mode: Command mode to set for the commandpane. """ self._command_pane.mode = mode self._command_pane.buffer.text = "" self.pane_focus(Pane.cmd) def cmd_exit(self) -> None: """Exit the commandpane and refocus the last focused filepane.""" self._command_pane.mode = CommandMode.clear self._command_pane.buffer.text = "" self.current_filepane.searched_indices = None self.pane_focus(self._previous_focus or Pane.left) def exit(self) -> None: """Exit the application and kill all spawed processes.""" self._history.left_mode = self._left_pane.mode self._history.right_mode = self._right_pane.mode self._history.left_index = self._left_pane.selected_file_index self._history.right_index = self._right_pane.selected_file_index self._history.left_path = self._left_pane.path self._history.right_path = self._right_pane.path self._history.focus = self._filepane_focus self._history.layout = self._layout_mode if not self._no_history: self._history.write() self._app.exit() def layout_switch(self, layout: LayoutMode) -> None: """Switch to a different layout. Args: layout: Desired layout mode to switch. Examples: >>> from s3fm.app import App >>> from s3fm.enums import LayoutMode >>> app = App() # doctest: +SKIP >>> app.layout_switch(LayoutMode.vertical) # doctest: +SKIP """ self._layout_mode = layout if layout != LayoutMode.single: self._app.layout = self.layout self.pane_focus(self._current_focus) def pane_swap(self, direction: Direction, layout: LayoutMode) -> None: """Swap panes left/right/up/down. This has side effects where it may cuase layout to change. When current layout is `LayoutMode.vertical` and switching up/down, layout will be changed to `LayoutMode.horizontal`. This function won't have any effect when theres only one filepane. Args: direction: Desired direction to swap. layout: Desired layout. Examples: >>> from s3fm.app import App >>> from s3fm.enums import Direction, LayoutMode >>> app = App() # doctest: +SKIP >>> app.pane_swap(Direction.left, LayoutMode.vertical) # doctest: +SKIP """ if self._layout_single(): return if (self._current_focus == Pane.right and (direction == Direction.right or direction == Direction.down) and self._layout_mode == layout): return if (self._current_focus == Pane.left and (direction == Direction.left or direction == Direction.up) and self._layout_mode == layout): return pane_swapped = False if not (self._current_focus == Pane.right and (direction == Direction.right or direction == Direction.down) and self._layout_mode != layout) and not ( self._current_focus == Pane.left and (direction == Direction.left or direction == Direction.up) and self._layout_mode != layout): pane_swapped = True self._left_pane, self._right_pane = self._right_pane, self._left_pane self._left_pane.id, self._right_pane.id = ( self._right_pane.id, self._left_pane.id, ) self._layout_mode = layout self._app.layout = self.layout if pane_swapped: self.pane_focus_other() else: self.pane_focus(self._current_focus) def set_error(self, exception: Optional["Notification"] = None) -> None: """Configure error notification for the application. This should only be used to set non-application error. Args: exception: A :class:`~s3fm.exceptions.Notification` instance. """ if not exception: self._kb_mode = KBMode.normal self._error = "" else: self._kb_mode = KBMode.error self._error = str(exception) self._error_type = exception.type @property def command_mode(self) -> Condition: """:class:`prompt_toolkit.filters.Condition`: A callable if current focus is commandpane.""" return self._command_mode @property def normal_mode(self) -> Condition: """:class:`prompt_toolkit.filters.Condition`: A callable if current focus is a filepane.""" return self._normal_mode @property def error_mode(self) -> Condition: """:class:`prompt_toolkit.filters.Condition`: A callable if the application has error.""" return self._error_mode @property def search_mode(self) -> Condition: """:class:`prompt_toolkit.filters.Condition`: A callable if the application is searching.""" return self._search_mode @property def reverse_search_mode(self) -> Condition: """:class:`prompt_toolkit.filters.Condition`: A callable if the application is reverse searching.""" return self._reverse_search_mode @property def current_focus(self) -> "Container": """:class:`prompt_toolkit.layout.Container`: Get current focused pane.""" try: return { **self.filepanes, Pane.cmd: self._command_pane, Pane.error: self._error_pane, }[self._current_focus] except KeyError: self.set_error( Notification("Unexpected focus.", error_type=ErrorType.warning)) self.pane_focus(Pane.left) return self.current_focus @property def current_filepane(self) -> FilePane: """:class:`~s3fm.ui.filepane.FilePane`: Get current focused filepane.""" try: return self.filepanes[self._filepane_focus] except KeyError: self.set_error( Notification("Unexpected focus.", error_type=ErrorType.warning)) self._filepane_focus = Pane.left return self.current_filepane @property def file_pane_focus(self) -> Pane: """Pane: Focused pane ID.""" return self._filepane_focus @property def filepanes(self) -> Dict[Pane, FilePane]: """Dict[Pane, FilePane]: Get pane mappings.""" return { Pane.left: self._left_pane, Pane.right: self._right_pane, } @property def layout(self) -> Layout: """:class:`prompt_toolkit.layout.Layout`: Get app layout dynamically.""" if self._layout_mode == LayoutMode.vertical: layout = HSplit([ VSplit([self._left_pane, self._right_pane]), self._command_pane ]) elif (self._layout_mode == LayoutMode.horizontal or self._layout_mode == LayoutMode.single): layout = HSplit( [self._left_pane, self._right_pane, self._command_pane]) else: self._layout_mode = LayoutMode.vertical self.set_error( Notification("Unexpected layout.", error_type=ErrorType.warning)) return self.layout if self._border: layout = Frame(layout) return Layout( FloatContainer( content=layout, floats=[Float(content=self._option_pane), self._error_pane], )) @property def kb(self) -> KB: """:class:`~s3fm.api.kb.KB`: KeyBindings.""" return self._kb @property def rendered(self) -> bool: """bool: :class:`App` rendered status.""" return self._rendered
class ProgressBar(object): """ Progress bar context manager. Usage :: with ProgressBar(...) as pb: for item in pb(data): ... :param title: Text to be displayed above the progress bars. This can be a callable or formatted text as well. :param formatters: List of :class:`.Formatter` instances. :param bottom_toolbar: Text to be displayed in the bottom toolbar. This can be a callable or formatted text. :param style: :class:`prompt_toolkit.styles.BaseStyle` instance. :param key_bindings: :class:`.KeyBindings` instance. :param file: The file object used for rendering, by default `sys.stderr` is used. :param color_depth: `prompt_toolkit` `ColorDepth` instance. :param output: :class:`~prompt_toolkit.output.Output` instance. :param input: :class:`~prompt_toolkit.input.Input` instance. """ def __init__(self, title=None, formatters=None, bottom_toolbar=None, style=None, key_bindings=None, file=None, color_depth=None, output=None, input=None): assert formatters is None or ( isinstance(formatters, list) and all(isinstance(fo, Formatter) for fo in formatters)) assert style is None or isinstance(style, BaseStyle) assert key_bindings is None or isinstance(key_bindings, KeyBindings) self.title = title self.formatters = formatters or create_default_formatters() self.bottom_toolbar = bottom_toolbar self.counters = [] self.style = style self.key_bindings = key_bindings # Note that we use __stderr__ as default error output, because that # works best with `patch_stdout`. self.color_depth = color_depth self.output = output or create_output(stdout=file or sys.__stderr__) self.input = input or get_default_input() self._thread = None self._loop = get_event_loop() self._previous_winch_handler = None self._has_sigwinch = False def __enter__(self): # Create UI Application. title_toolbar = ConditionalContainer( Window(FormattedTextControl(lambda: self.title), height=1, style='class:progressbar,title'), filter=Condition(lambda: self.title is not None)) bottom_toolbar = ConditionalContainer( Window(FormattedTextControl(lambda: self.bottom_toolbar, style='class:bottom-toolbar.text'), style='class:bottom-toolbar', height=1), filter=~is_done & renderer_height_is_known & Condition(lambda: self.bottom_toolbar is not None)) def width_for_formatter(formatter): # Needs to be passed as callable (partial) to the 'width' # parameter, because we want to call it on every resize. return formatter.get_width(progress_bar=self) progress_controls = [ Window( content=_ProgressControl(self, f), width=functools.partial(width_for_formatter, f)) for f in self.formatters ] self.app = Application( min_redraw_interval=.05, layout=Layout(HSplit([ title_toolbar, VSplit(progress_controls, height=lambda: D( preferred=len(self.counters), max=len(self.counters))), Window(), bottom_toolbar, ])), style=self.style, key_bindings=self.key_bindings, color_depth=self.color_depth, output=self.output, input=self.input) # Run application in different thread. def run(): with _auto_refresh_context(self.app, .3): try: self.app.run() except BaseException as e: traceback.print_exc() print(e) self._thread = threading.Thread(target=run) self._thread.start() # Attach WINCH signal handler in main thread. # (Interrupt that we receive during resize events.) self._has_sigwinch = hasattr(signal, 'SIGWINCH') and in_main_thread() if self._has_sigwinch: self._previous_winch_handler = self._loop.add_signal_handler( signal.SIGWINCH, self.app.invalidate) return self def __exit__(self, *a): # Quit UI application. if self.app.is_running: self.app.exit() # Remove WINCH handler. if self._has_sigwinch: self._loop.add_signal_handler(signal.SIGWINCH, self._previous_winch_handler) self._thread.join() def __call__(self, data=None, label='', remove_when_done=False, total=None): """ Start a new counter. :param label: Title text or description for this progress. (This can be formatted text as well). :param remove_when_done: When `True`, hide this progress bar. :param total: Specify the maximum value if it can't be calculated by calling ``len``. """ assert is_formatted_text(label) assert isinstance(remove_when_done, bool) counter = ProgressBarCounter( self, data, label=label, remove_when_done=remove_when_done, total=total) self.counters.append(counter) return counter def invalidate(self): self.app.invalidate()
class HummingbotCLI(PubSub): def __init__(self, input_handler: Callable, bindings: KeyBindings, completer: Completer, command_tabs: Dict[str, CommandTab]): super().__init__() self.command_tabs = command_tabs self.search_field = create_search_field() self.input_field = create_input_field(completer=completer) self.output_field = create_output_field() self.log_field = create_log_field(self.search_field) self.right_pane_toggle = create_log_toggle(self.toggle_right_pane) self.live_field = create_live_field() self.log_field_button = create_tab_button("Log-pane", self.log_button_clicked) self.timer = create_timer() self.process_usage = create_process_monitor() self.trade_monitor = create_trade_monitor() self.layout, self.layout_components = generate_layout( self.input_field, self.output_field, self.log_field, self.right_pane_toggle, self.log_field_button, self.search_field, self.timer, self.process_usage, self.trade_monitor, self.command_tabs) # add self.to_stop_config to know if cancel is triggered self.to_stop_config: bool = False self.live_updates = False self.bindings = bindings self.input_handler = input_handler self.input_field.accept_handler = self.accept self.app: Optional[Application] = None # settings self.prompt_text = ">>> " self.pending_input = None self.input_event = None self.hide_input = False # stdout redirection stack self._stdout_redirect_context: ExitStack = ExitStack() # start ui tasks loop = asyncio.get_event_loop() loop.create_task(start_timer(self.timer)) loop.create_task(start_process_monitor(self.process_usage)) loop.create_task(start_trade_monitor(self.trade_monitor)) def did_start_ui(self): self._stdout_redirect_context.enter_context( patch_stdout(log_field=self.log_field)) log_level = global_config_map.get("log_level").value init_logging("hummingbot_logs.yml", override_log_level=log_level) self.trigger_event(HummingbotUIEvent.Start, self) async def run(self): self.app = Application(layout=self.layout, full_screen=True, key_bindings=self.bindings, style=load_style(), mouse_support=True, clipboard=PyperclipClipboard()) await self.app.run_async(pre_run=self.did_start_ui) self._stdout_redirect_context.close() def accept(self, buff): self.pending_input = self.input_field.text.strip() if self.input_event: self.input_event.set() try: if self.hide_input: output = '' else: output = '\n>>> {}'.format(self.input_field.text, ) self.input_field.buffer.append_to_history() except BaseException as e: output = str(e) self.log(output) self.input_handler(self.input_field.text) def clear_input(self): self.pending_input = None def log(self, text: str, save_log: bool = True): if save_log: if self.live_updates: self.output_field.log(text, silent=True) else: self.output_field.log(text) else: self.output_field.log(text, save_log=False) def change_prompt(self, prompt: str, is_password: bool = False): self.prompt_text = prompt processors = [] if is_password: processors.append(PasswordProcessor()) processors.append(BeforeInput(prompt)) self.input_field.control.input_processors = processors async def prompt(self, prompt: str, is_password: bool = False) -> str: self.change_prompt(prompt, is_password) self.app.invalidate() self.input_event = asyncio.Event() await self.input_event.wait() temp = self.pending_input self.clear_input() self.input_event = None if is_password: masked_string = "*" * len(temp) self.log(f"{prompt}{masked_string}") else: self.log(f"{prompt}{temp}") return temp def set_text(self, new_text: str): self.input_field.document = Document(text=new_text, cursor_position=len(new_text)) def toggle_hide_input(self): self.hide_input = not self.hide_input def toggle_right_pane(self): if self.layout_components["pane_right"].filter(): self.layout_components["pane_right"].filter = lambda: False self.layout_components["item_top_toggle"].text = '< log pane' else: self.layout_components["pane_right"].filter = lambda: True self.layout_components["item_top_toggle"].text = '> log pane' def log_button_clicked(self): for tab in self.command_tabs.values(): tab.is_selected = False self.redraw_app() def tab_button_clicked(self, command_name: str): for tab in self.command_tabs.values(): tab.is_selected = False self.command_tabs[command_name].is_selected = True self.redraw_app() def exit(self): self.app.exit() def redraw_app(self): self.layout, self.layout_components = generate_layout( self.input_field, self.output_field, self.log_field, self.right_pane_toggle, self.log_field_button, self.search_field, self.timer, self.process_usage, self.trade_monitor, self.command_tabs) self.app.layout = self.layout self.app.invalidate() def tab_navigate_left(self): selected_tabs = [ t for t in self.command_tabs.values() if t.is_selected ] if not selected_tabs: return selected_tab: CommandTab = selected_tabs[0] if selected_tab.tab_index == 1: self.log_button_clicked() else: left_tab = [ t for t in self.command_tabs.values() if t.tab_index == selected_tab.tab_index - 1 ][0] self.tab_button_clicked(left_tab.name) def tab_navigate_right(self): current_tabs = [ t for t in self.command_tabs.values() if t.tab_index > 0 ] if not current_tabs: return selected_tab = [t for t in current_tabs if t.is_selected] if selected_tab: right_tab = [ t for t in current_tabs if t.tab_index == selected_tab[0].tab_index + 1 ] else: right_tab = [t for t in current_tabs if t.tab_index == 1] if right_tab: self.tab_button_clicked(right_tab[0].name) def close_buton_clicked(self, command_name: str): self.command_tabs[command_name].button = None self.command_tabs[command_name].close_button = None self.command_tabs[command_name].output_field = None self.command_tabs[command_name].is_selected = False for tab in self.command_tabs.values(): if tab.tab_index > self.command_tabs[command_name].tab_index: tab.tab_index -= 1 self.command_tabs[command_name].tab_index = 0 if self.command_tabs[command_name].task is not None: self.command_tabs[command_name].task.cancel() self.command_tabs[command_name].task = None self.redraw_app() def handle_tab_command(self, hummingbot: "HummingbotApplication", command_name: str, kwargs: Dict[str, Any]): if command_name not in self.command_tabs: return cmd_tab = self.command_tabs[command_name] if "close" in kwargs and kwargs["close"]: if cmd_tab.close_button is not None: self.close_buton_clicked(command_name) return if "close" in kwargs: kwargs.pop("close") if cmd_tab.button is None: cmd_tab.button = create_tab_button( command_name, lambda: self.tab_button_clicked(command_name)) cmd_tab.close_button = create_tab_button( "x", lambda: self.close_buton_clicked(command_name), 1, '', ' ') cmd_tab.output_field = create_live_field() cmd_tab.tab_index = max(t.tab_index for t in self.command_tabs.values()) + 1 self.tab_button_clicked(command_name) self.display_tab_output(cmd_tab, hummingbot, kwargs) def display_tab_output(self, command_tab: CommandTab, hummingbot: "HummingbotApplication", kwargs: Dict[Any, Any]): if command_tab.task is not None and not command_tab.task.done(): return if threading.current_thread() != threading.main_thread(): hummingbot.ev_loop.call_soon_threadsafe(self.display_tab_output, command_tab, hummingbot, kwargs) return command_tab.task = safe_ensure_future( command_tab.tab_class.display(command_tab.output_field, hummingbot, **kwargs))
class NoobitCLI: def __init__(self, # input_handler: Callable, # completer: Completer): ): completer = None self.process_usage = create_process_monitor() self.search_log_field = create_search_field("logs") self.search_out_field = create_search_field("ouput") self.input_field = create_input_field(completer=completer) self.output_field = create_output_field(self.search_out_field) # right hand window self.log_field = create_log_field(self.search_log_field) self.timer = create_timer() self.layout = generate_layout(self.input_field, self.output_field, self.log_field, self.search_log_field, self.search_out_field, self.timer, self.process_usage) # add self.to_stop_config to know if cancel is triggered self.to_stop_config: bool = False self.live_updates = False self.bindings = load_key_bindings(self) self.input_handler = self._input_handler self.input_field.accept_handler = self.accept self.app = Application(layout=self.layout, full_screen=True, key_bindings=self.bindings, style=load_style(), mouse_support=True, clipboard=PyperclipClipboard()) # settings self.prompt_text = ">>> " self.pending_input = None self.input_event = None self.hide_input = False # start ui tasks self.loop = asyncio.get_event_loop() self.loop.create_task(start_timer(self.timer)) #! maximaus added self.argparser = load_parser(self) self.client = httpx.AsyncClient() # TODO update type annotation self.ws: typing.Dict[str, KrakenWsPublic] = {} # TODO we dont want to hardcode this for every exchange self.symbols: typing.Dict = {} # "KRAKEN": None, # "BINANCE": None # } def _input_handler(self, raw_command): """parse input and map it to functions """ try: _handle_commands(self, raw_command) except Exception as e: self.log(e) async def run(self): await self.app.run_async() def accept(self, buff): self.pending_input = self.input_field.text.strip() if self.input_event: self.input_event.set() try: if self.hide_input: output = '' else: output = '\n>>> {}'.format(self.input_field.text,) self.input_field.buffer.append_to_history() except BaseException as e: output = str(e) self.log(output) self.input_handler(self.input_field.text) def clear_input(self): self.pending_input = None def log(self, text: str, save_log: bool = True): if save_log: if self.live_updates: self.output_field.log(text, silent=True) else: self.output_field.log(text) else: self.output_field.log(text, save_log=False) def clear(self): self.output_field.clear() def change_prompt(self, prompt: str, is_password: bool = False): self.prompt_text = prompt processors: typing.List[typing.Any] = [] if is_password: processors.append(PasswordProcessor()) processors.append(BeforeInput(prompt)) self.input_field.control.input_processors = processors async def prompt(self, prompt: str, is_password: bool = False) -> str: self.change_prompt(prompt, is_password) self.app.invalidate() self.input_event = asyncio.Event() await self.input_event.wait() temp = self.pending_input self.clear_input() self.input_event = None if is_password: masked_string = "*" * len(temp) self.log(f"{prompt}{masked_string}") else: self.log(f"{prompt}{temp}") return temp def set_text(self, new_text: str): self.input_field.document = Document(text=new_text, cursor_position=len(new_text)) def toggle_hide_input(self): self.hide_input = not self.hide_input def exit(self): self.app.exit() # ======================================== # == COMMANDS def set_vars(self, exchange: str, symbol: str, ordType: str, orderQty: float): if exchange: settings.EXCHANGE = exchange.upper() if symbol: settings.SYMBOL = symbol.upper() if ordType: settings.ORDTYPE = ordType.upper() if orderQty: settings.ORDQTY = orderQty # ======================================== # == TESTING THAT COUNTING WORKS def count(self, start: int, finish: int, step: int): async def enum(start, finish, step): n = start while n < finish: # write to a different field # save_log option means we will display the record, otherwise log gets cleared on each updated and replaced self.log_field.log(text=n, save_log=True) n += 1 await asyncio.sleep(1) # async def _await(start, finish, step): # await enum(start, finish, step) safe_ensure_future(self, enum(start, finish, step)) async def acount(self, start: int, finish: int, step: int): n = start while n < finish: self.log_field.log(text=n, save_log=True) n+=1 await asyncio.sleep(1) # ======================================== # == UTIL def check_symbols(self): _ok = True for exchange in ntypes.EXCHANGE: self.log_field.log(f"Checking symbols for {exchange}") if not self.symbols[exchange]: self.log_field.log(f"Please initialize symbols for {exchange}") _ok = False return _ok # ======================================== # == HELP async def help( self, #type: NoobitCLI command: str ): if command == 'all': self.log(self.argparser.format_help()) else: subparsers_actions = [ action for action in self.argparser._actions if isinstance(action, argparse._SubParsersAction)] for subparsers_action in subparsers_actions: subparser = subparsers_action.choices.get(command) if subparser: self.log(subparser.format_help()) async def list( self ): await self.help("all") # ======================================== # ADD API KEYS async def add_keys(self, exchange: str, key: str, secret: str): if not exchange: exchange = settings.EXCHANGE else: exchange = exchange.upper() from noobit_markets.path import APP_PATH import time import os self.log(APP_PATH) path = os.path.join(APP_PATH, "exchanges", exchange.lower(), "rest", ".env") self.log(path) with open(path, "a") as file: timeid = int(time.time()) file.write("\n") file.write(f"\n{exchange}_API_KEY_{timeid} = {key}") file.write(f"\n{exchange}_API_SECRET_{timeid} = {secret}") # TODO we should probably make a bogus private rest call to check if it works self.log_field.log("API Credentials added\n") # ======================================== # PUBLIC ENDPOINTS COMMANDS async def fetch_symbols(self): kraken_symbols = await KRAKEN.rest.public.symbols(self.client) binance_symbols = await BINANCE.rest.public.symbols(self.client) ftx_symbols = await FTX.rest.public.symbols(self.client) if kraken_symbols.is_ok(): self.symbols["KRAKEN"] = kraken_symbols else: self.log_field.log("Error fetching kraken symbols") self.log("Error fetching kraken symbols") self.log(kraken_symbols.value) if binance_symbols.is_ok(): self.symbols["BINANCE"] = binance_symbols else: self.log_field.log("Error fetching binance symbols") self.log("Error fetching binance symbols") self.log(binance_symbols.value) if ftx_symbols.is_ok(): self.symbols["FTX"] = ftx_symbols else: self.log_field.log("Error fetching ftx symbols") self.log("Error fetching ftx symbols") self.log(ftx_symbols.value) def show_symbols(self, exchange: str): from noobit_markets.base.models.rest.response import NSymbol cap_exch = exchange.upper() if cap_exch in ntypes.EXCHANGE.__members__.keys(): if cap_exch in self.symbols.keys(): self.log_field.log(f"Exchange is accepted: {cap_exch}") _sym = NSymbol(self.symbols[cap_exch]) if _sym.is_err(): self.log("Err") self.log(_sym.result) else: self.log("OK") self.log(_sym.table) else: self.log("Please initialize symbols for this exchange") else: self.log("Unknown Exchange requested") @ensure_symbols async def fetch_ohlc(self, exchange: str, symbol: str, timeframe: str, since: typing.Optional[int]=None): from noobit_markets.base.models.rest.response import NOhlc self.log_field.log("CALLED fetch_ohlc") if not symbol: if not settings.SYMBOL or settings.SYMBOL.isspace(): return Err("Please set or pass <symbol> argument") else: symbol = settings.SYMBOL else: symbol=symbol.upper() interface = globals()[exchange] _res = await interface.rest.public.ohlc(self.client, symbol.upper(), self.symbols[exchange].value, timeframe.upper(), since) _ohlc = NOhlc(_res) return _ohlc @ensure_symbols async def fetch_orderbook(self, exchange: str, symbol: str, depth: int): from noobit_markets.base.models.rest.response import NOrderBook self.log_field.log("CALLED fetch_orderbook") if not symbol: if not settings.SYMBOL or settings.SYMBOL.isspace(): return Err("Please set or pass <symbol> argument") else: symbol = settings.SYMBOL else: symbol=symbol.upper() interface = globals()[exchange] _res = await interface.rest.public.orderbook(self.client, symbol.upper(), self.symbols[exchange].value, depth) _book = NOrderBook(_res) return _book @ensure_symbols async def fetch_trades(self, exchange: str, symbol: str, since: typing.Optional[int]=None): from noobit_markets.base.models.rest.response import NTrades self.log_field.log("CALLED fetch_trades") if not symbol: if not settings.SYMBOL or settings.SYMBOL.isspace(): return Err("Please set or pass <symbol> argument") else: symbol = settings.SYMBOL else: symbol=symbol.upper() interface = globals()[exchange] _res = await interface.rest.public.trades(self.client, symbol, self.symbols[exchange].value, since) _trd = NTrades(_res) return _trd # ======================================== # PRIVATE ENDPOINTS COMMANDS @ensure_symbols async def fetch_balances(self, exchange: str): from noobit_markets.base.models.rest.response import NBalances self.log_field.log("CALLED fetch_balances") interface = globals()[exchange] _res = await interface.rest.private.balances(self.client, self.symbols[exchange].value) _bal = NBalances(_res) return _bal @ensure_symbols async def fetch_exposure(self, exchange: str): from noobit_markets.base.models.rest.response import NExposure self.log_field.log("CALLED fetch_exposure") self.log_field.log(f"Requested Exchange : {exchange.upper()}") interface = globals()[exchange] _res = await interface.rest.private.exposure(self.client, self.symbols[exchange].value) _exp = NExposure(_res) return _exp @ensure_symbols async def fetch_usertrades(self, exchange: str, symbol: str): from noobit_markets.base.models.rest.response import NTrades self.log_field.log("CALLED fetch_usertrades") if not symbol: symbol = settings.SYMBOL else: symbol=symbol.upper() self.log_field.log(f"Requested Symbol : {symbol}") self.log_field.log(f"Requested Exchange : {exchange}") interface = globals()[exchange] _res = await interface.rest.private.trades(self.client, symbol, self.symbols[exchange].value) _utr = NTrades(_res) return _utr @ensure_symbols async def fetch_openorders(self, exchange: str, symbol: str): from noobit_markets.base.models.rest.response import NOrders self.log_field.log("CALLED fetch_openorders") if not symbol: if not settings.SYMBOL or settings.SYMBOL.isspace(): return Err("Please set or pass <symbol> argument") else: symbol = settings.SYMBOL else: symbol=symbol.upper() self.log_field.log(f"Requested Symbol : {symbol}") self.log_field.log(f"Requested Exchange : {exchange}") interface = globals()[exchange] _res = await interface.rest.private.open_orders(self.client, symbol, self.symbols[exchange].value) _opo = NOrders(_res) return _opo @ensure_symbols async def create_neworder( self, exchange: str, symbol: str, ordType: str, clOrdID, orderQty: float, price: float, timeInForce: str = "GOOD-TIL-CANCEL", quoteOrderQty: typing.Optional[float] = None, stopPrice: typing.Optional[float] = None, *, side: str, blind: bool, split: typing.Optional[int] = None, delay: typing.Optional[int] = None, step: typing.Optional[float] = None, ): from noobit_markets.base.models.rest.response import NSingleOrder self.log_field.log("CALLED create_neworder") if not symbol: if not settings.SYMBOL or settings.SYMBOL.isspace(): return Err("Please set or pass <symbol> argument") else: symbol = settings.SYMBOL else: symbol=symbol.upper() if not ordType: if not settings.ORDTYPE or settings.ORDTYPE.isspace(): return Err("Please set or pass <ordType> argument") else: ordType = settings.ORDTYPE # TODO be consistent: either all noobit types in capital or in lowercase else: ordType = ordType.upper() if not orderQty: if not settings.ORDQTY: return Err("Please set or pass <orderQty> argument") else: orderQty = settings.ORDQTY if not timeInForce and ordType in ["LIMIT", "STOP-LOSS-LIMIT", "TAKE-PROFIT-LIMIT"]: self.log("WARNING : <timeInForce> not provided, defaulting to <GOOD-TIL-CANCEL>") timeInForce = "GOOD-TIL-CANCEL" interface = globals()[exchange] if split: # step is only for limit orders if step: if not ordType in ["LIMIT", "STOP_LIMIT", "TAKE_PROFIT"]: return Err(f"Argument <step>: Ordertype can not be {ordType}") # only one of delay or step if not any([delay, step]) or all([delay, step]): return Err("Please set only one of <delay> or <step> argument to split orders") else: _acc = [] acc_price = price for i in range(split): _res = await interface.rest.private.new_order( client=self.client, symbol=symbol, symbols_resp=self.symbols[exchange].value, side=side, ordType=ordType, clOrdID=clOrdID, # orderQty=round(orderQty/split, self.symbols[exchange].value.asset_pairs[symbol].volume_decimals), # TODO atm we limit to 2 decimal places, could check max decimals for pair, ALSO this can lead to keyerror if symbol is not on exchange orderQty=round(orderQty/split, 2), # TODO atm we limit to 2 decimal places, could check max decimals for pair, ALSO this can lead to keyerror if symbol is not on exchange price=acc_price, timeInForce=timeInForce, quoteOrderQty=quoteOrderQty, stopPrice=stopPrice ) if _res.is_ok(): if blind: _res.value.price = None _acc.append(_res.value) self.log_field.log(f"Successful order, count {len(_acc)}") else: self.log_field.log(f"Failed order") return _res if step: acc_price = round(float(acc_price + step), 2) # FIXME this will create decimal place precision errors sometimes, we need to round somehow (use context ??) await asyncio.sleep(1) else: await asyncio.sleep(delay) # avoid rate limiting try: if blind: self.log("Argument <blind>: Setting Order Price to <None>") _splitorders = NoobitResponseClosedOrders( exchange="KRAKEN", rawjson={}, orders=_acc ) # request coros always return a result # so we wrap the validated model in an OK container _nords = NOrders(Ok(_splitorders)) return _nords except ValidationError as e: return Err(e) else: if any([delay, step]): self.log_field.log("Argument <delay> or <step> require <split>") return Err("Argument <delay> or <step> require <split>") _res = await interface.rest.private.new_order( client=self.client, symbol=symbol, symbols_resp=self.symbols[exchange].value, side=side, ordType=ordType, clOrdID=clOrdID, orderQty=orderQty, price=price, timeInForce=timeInForce, quoteOrderQty=quoteOrderQty, stopPrice=stopPrice ) if blind: self.log("Argument <blind>: Setting Order Price to <None>") if _res.is_ok(): _res.value.price = None _nord = NSingleOrder(_res) return _nord # side argument isnt registered for some reason (in following partials) # create_buyorder: typing.Coroutine = functools.partialmethod(create_neworder, "BUY") # create_sellorder: typing.Coroutine = functools.partialmethod(create_neworder, "SELL") async def create_buyorder( self, exchange: str, symbol: str, ordType: str, clOrdID, orderQty: float, price: float, timeInForce: str, stopPrice: float, blind: bool, split: int, delay: int, step: int ): await self.create_neworder(exchange, symbol, ordType, clOrdID, orderQty, price, timeInForce, stopPrice, blind=blind, split=split, delay=delay, step=step, side="BUY") async def create_sellorder( self, exchange: str, symbol: str, ordType: str, clOrdID, orderQty: float, price: float, timeInForce: str, stopPrice: float, blind: bool, split: int, delay: int, step: int ): await self.create_neworder(exchange, symbol, ordType, clOrdID, orderQty, price, timeInForce, stopPrice, blind=blind, split=split, delay=delay, step=step, side="SELL") # TODO add remove_order to binance interface @ensure_symbols async def cancel_order( self, exchange: str, symbol: str, slice: str, all: bool ): self.log_field.log("CALLED cancel_order") # testlist = [x for x in range(0, 100)] # regex = re.compile(r"^\[[-]?[0-9]+:[-]?[0-9]+\]$") regex = "^\[[-]?[0-9]+:[-]?[0-9]+:[-]?[0-9]\]$" match = re.match(regex, slice) if not match: self.log(match) return Err(f"Argument position did not match regex - Given {slice}") else: # sliced = eval(f"testlist{position}") # self.log(sliced) # return Ok("SUCCESS") if not symbol: if not settings.SYMBOL or settings.SYMBOL.isspace(): return Err("Please set or pass <symbol> argument") else: symbol = settings.SYMBOL else: symbol=symbol.upper() self.log_field.log(f"Requested Symbol : {symbol}") self.log_field.log(f"Requested Exchange : {exchange}") interface = globals()[exchange] _res = await interface.rest.private.open_orders(self.client, symbol, self.symbols[exchange].value) if _res.is_err(): return _res.value else: _acc = [] _all_orders = sorted([order for order in _res.value.orders], key=lambda x: getattr(x, "price"), reverse=False) _sliced_orders = eval(f"_all_orders{slice}") for _order in _sliced_orders: _ord = await interface.rest.private.remove_order(self.client, symbol, self.symbols[exchange].value, _order.orderID) if _ord.is_ok(): _acc.append(_ord.value) else: return _ord await asyncio.sleep(1) try: _canceled_orders = NoobitResponseClosedOrders( exchange="KRAKEN", rawjson={}, orders=_acc ) # request coros always return a result # so we wrap the validated model in an OK container _nords = NOrders(Ok(_canceled_orders)) return _nords except ValidationError as e: return Err(e) # ======================================== # START WEBSOCKET PUBLIC STREAMS async def connect(self): import websockets from noobit_markets.exchanges.kraken.websockets.public.routing import msg_handler from noobit_markets.exchanges.kraken.websockets.public.api import KrakenWsPublic feed_map = { "trade": "trade", "ticker": "instrument", "book": "orderbook", "spread": "spread" } #! only connect Kraken for now client = await websockets.connect("wss://ws.kraken.com") self.ws["KRAKEN"] = KrakenWsPublic(client, msg_handler, self.loop, feed_map) self.log(client) #! only stream Kraken for now, need to call connect before async def stream_orderbook(self, exchange: str, symbol: str, depth: str): if not exchange: exchange = settings.EXCHANGE if not symbol: symbol = settings.SYMBOL self.log(self.ws[exchange]) self.log(self.ws[exchange].client) self.log(self.ws[exchange].orderbook) # while True: # self.log_field.log("Heartbeat") # await asyncio.sleep(2) async for msg in self.ws[exchange].orderbook(self.symbols[exchange].value, symbol, depth, True): self.log_field.log("Got new message from orderbook websocket")
class ChatTUI(object): def __init__(self, client=None, messages=None): self.client = client self.send = self.client.send # Style. self.classic_style = Style([ ("output-field", "bg:#000044 #ffffff"), ("input-field", "bg:#000000 #ffffff"), ("line", "#004400"), ]) self.dark_mode = Style([ ("output-field", "bg:#2b2b2b #ffffff"), ("input-field", "bg:#000000 #ffffff"), ("line", "#004400"), ]) self.dakes_theme = Style([ ("output-field", "bg:#004400 #ffffff"), ("input-field", "bg:#000000 #ffffff"), ("line", "#aa007f"), ]) self.themes = ["classic style", "dark mode", "dakes theme"] self.themes_help_txt = "Available themes are: \n" + str(self.themes) + \ '\nYou select a theme by typing: "!theme classic style"' self.themes_help_msg = Message(Message.server_user, self.themes_help_txt) self.style = self.classic_style self.welcome_txt = """ Welcome to honkuru. The peer-to-peer text chat. To send a message just type it and send with 'enter'. To display the help message type '!help'\n """ self.welcome_msg = Message(Message.server_user, self.welcome_txt) self.help_txt = """To display this help type: {} To display all available color themes type: {} To disconnect type: {} or Press Ctrl+C or Ctrl+Q""".format(Message.help, Message.theme, Message.disconnect) self.help_msg = Message(Message.server_user, self.help_txt) # reference to messages object of client self.manager = multiprocessing.Manager() self.messages = messages self.messages.append(self.welcome_msg) self.application = Application() # self.draw() def main(self): # The layout. self.output_field = TextArea(style="class:output-field", text=self.welcome_msg.message) self.input_field = TextArea( height=1, prompt=">>> ", style="class:input-field", multiline=False, wrap_lines=False, ) container = HSplit([ self.output_field, Window(height=1, char="-", style="class:line"), self.input_field, ]) self.input_field.accept_handler = self.send_message # The key bindings. kb = KeyBindings() @kb.add("c-c") @kb.add("c-q") def _(event): """ Pressing Ctrl-Q or Ctrl-C will exit the user interface. """ self.client.client_socket.send(Message.close_connection_client) sleep(2) event.app.exit() # Run application. self.application = Application( layout=Layout(container, focused_element=self.input_field), key_bindings=kb, style=self.style, mouse_support=True, full_screen=True, ) # _thread.start_new_thread(self.render_messages, ("Thread-1", 2,)) t = threading.Thread( target=self.render_messages, args=(), ) t.daemon = True t.start() self.application.run() def send_message(self, buff): """ Will send the message typed by calling the reference to the send function of the client Also handles some special command, like theme changing :param buff: :return: """ msg = self.input_field.text if msg and msg[0] == Message.command_prefix: # change color themes if Message.theme in msg: theme = msg.lstrip(Message.theme).lstrip() if theme == "classic style" or theme == "classic_style": self.style = self.classic_style elif theme == "dark mode" or theme == "dark_mode": self.style = self.dark_mode elif "dakes" in theme: self.style = self.dakes_theme else: self.messages.append(self.themes_help_msg) self.application.style = self.style # disconnect is handled by client elif Message.disconnect in msg: # self.disconnect() self.client.client_socket.sendall( Message.close_connection_client) # self.client.disconnect() # self.disconnect() # help Message elif Message.help in msg: self.messages.append(self.help_msg) else: self.client.send(msg) def render_messages(self): msg_len = 0 while True: sleep(0.01) if msg_len < len(self.messages): # print(self.messages) new_text = "" msg = "" for msg in self.messages: new_text = new_text + "\n" + msg.user + ": " + msg.message self.output_field.buffer.document = Document( text=new_text, cursor_position=len(new_text)) msg_len = len(self.messages) def disconnect(self): self.messages.append(Message(Message.client_user, "Disconnecting... ")) sleep(1) try: self.application.exit() except Exception: pass
class AppManager(): def __init__(self): self.browser = Browser() self.wm = WindowManager() self.current_dir = None self.current_station = None @Condition def is_not_dialog_active(): return not self.wm.is_dialog_active kb = KeyBindings() kb.add('tab', filter=is_not_dialog_active)(self.next_window) kb.add('s-tab', filter=is_not_dialog_active)(self.prev_window) kb.add('c-q', filter=is_not_dialog_active)(self.exit) kb.add('escape')(self.close_dialog) kb.add('c-p')(self.play) kb.add('c-s')(self.stop) kb.add('c-l', filter=is_not_dialog_active)(self.add_to_list) root = AppManager.format_dirs(self.browser.fetch()[0]) self.wm.append_folder(root, self.on_click_folder, 'No lists') self.app = Application(full_screen=True, layout=self.wm.layout, key_bindings=kb) self.instance = vlc.Instance('--input-repeat=-1', '--fullscreen', '--quiet', '--file-logging', '--logfile={vlc_log}', '--logmode=text', '--log-verbose=3') self.player = self.instance.media_player_new() def next_window(self, _ev=None): self.app.layout.focus(self.wm.next) def prev_window(self, _ev=None): self.app.layout.focus(self.wm.prev) def keep_window(self, _ev=None): self.app.layout.focus(self.wm.current) def exit(self, _ev): self.app.exit() def on_click_folder(self, dir_name): self.fetch(dir_name) self.app.layout = self.wm.layout self.next_window() self.current_dir = dir_name def on_click_station(self, station_name): stations = self.browser.stations[self.current_dir] station = next(x for x in stations if x['name'] == station_name) self.current_station = station self.play() def fetch(self, dir_name): dirs, stations = self.browser.fetch(dir_name) if len(dirs) > 0: self.wm.insert_folder(AppManager.format_dirs(dirs), self.on_click_folder) if len(stations) > 0: self.wm.show_stations(dir_name, AppManager.format_stations(stations), self.on_click_station, 'No stations') def play(self, _ev=None): log.info('currently playing: %s', self.current_station) media = self.instance.media_new(self.current_station['url']) self.wm.playing = HTML('<u>Playing</u>: ' + self.current_station['name']) self.app.layout = self.wm.layout self.keep_window() self.player.set_media(media) self.player.play() def stop(self, _ev=None): self.wm.playing = HTML('<u>Stopped</u>: ' + self.current_station['name']) self.app.layout = self.wm.layout self.keep_window() self.player.stop() def add_to_list(self, _ev=None): if self.current_station: def select_list(st_list): stations = list( map(lambda st: st[0], MyStations.get_stations(st_list))) if self.current_station['name'] in stations: log.info('%s station already in %s\'s list', self.current_station['name'], st_list) else: log.info('save %s in %s\'s list', self.current_station['name'], st_list) MyStations.save_station(st_list, str(self.current_station['name']), str(self.current_station['url'])) self.close_dialog() def new_list(buffer): log.info('new list: %s', buffer.text) select_list(buffer.text) radio_list = RadioList(values=list( map(lambda i: (i, i), MyStations.get_lists())), handler=select_list) text_input = TextArea(multiline=False, accept_handler=new_list) dialog = Dialog(title="Add to list", body=HSplit([text_input, radio_list], padding=1), width=40, with_background=True) self.wm.show_dialog(dialog) self.app.layout = self.wm.layout self.keep_window() def run(self): self.app.run() def close_dialog(self, _ev=None): self.wm.hide_dialog() self.app.layout = self.wm.layout self.keep_window() @staticmethod def format_dirs(dirs): list = [] for i, di in enumerate(dirs): list.append((i, di["title"])) return list @staticmethod def format_stations(stations): list = [] for i, di in enumerate(stations): list.append((i, di["name"])) return list