Spaces:
				
			
			
	
			
			
					
		Running
		
	
	
	
			
			
	
	
	
	
		
		
					
		Running
		
	| # `gradio_logsview` | |
| <img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-%20orange"> | |
| Visualize logs in your Gradio app | |
| ## Installation | |
| ```bash | |
| pip install gradio_logsview | |
| ``` | |
| ## Usage | |
| ```python | |
| import logging | |
| import random | |
| import time | |
| import gradio as gr | |
| from gradio_logsview import LogsView | |
| def random_values(failing: bool = False): | |
| for i in range(10): | |
| logging.log( | |
| random.choice( | |
| [ # Random levels | |
| logging.INFO, | |
| logging.DEBUG, | |
| logging.WARNING, | |
| logging.ERROR, | |
| logging.CRITICAL, | |
| ] | |
| ), | |
| f"Value {i+1}", # Random values | |
| ) | |
| time.sleep(random.uniform(0, 1)) | |
| if failing and i == 5: | |
| raise ValueError("Failing!!") | |
| def fn_process_success(): | |
| yield from LogsView.run_process(["python", "-u", "demo/script.py"]) | |
| def fn_process_failing(): | |
| yield from LogsView.run_process(["python", "-u", "demo/script.py", "--failing"]) | |
| def fn_thread_success(): | |
| yield from LogsView.run_thread(random_values, log_level=logging.INFO, failing=False) | |
| def fn_thread_failing(): | |
| yield from LogsView.run_thread(random_values, log_level=logging.INFO, failing=True) | |
| markdown_top = """ | |
| # LogsView Demo | |
| This demo shows how to use the `LogsView` component to display logs from a process or a thread in real-time. | |
| Click on any button to launch a process or a thread and see the logs displayed in real-time. | |
| In the thread example, logs are generated randomly with different log levels. | |
| In the process example, logs are generated by a Python script but any command can be executed. | |
| """ | |
| markdown_bottom = """ | |
| ## How to run in a thread? | |
| With `LogsView.run_thread`, you can run a function in a separate thread and capture logs in real-time. | |
| You can configure which logs to capture (log level and logger name). | |
| ```py | |
| from gradio_logsview import LogsView | |
| def fn_thread(): | |
| # Run `my_function` in a separate thread | |
| # All logs above `INFO` level will be captured and displayed in real-time. | |
| yield from LogsView.run_thread(my_function, log_level=logging.INFO, arg1="value1") | |
| with gr.Blocks() as demo: | |
| logs = LogsView() | |
| btn = gr.Button("Run thread") | |
| btn.click(fn_thread, outputs=logs) | |
| ``` | |
| ## How to run in a process? | |
| With `LogsView.run_process`, you can run a command in a separate process and capture logs from the process in real-time. | |
| ```py | |
| from gradio_logsview import LogsView | |
| def fn_process(): | |
| # Run a process and capture all logs from the process | |
| yield from LogsView.run_process( | |
| cmd=["mergekit-yaml", "config.yaml", "merge", "--copy-", "--cuda", "--low-cpu-memory"] | |
| ) | |
| with gr.Blocks() as demo: | |
| logs = LogsView() | |
| btn = gr.Button("Run process") | |
| btn.click(fn_process, outputs=logs) | |
| ``` | |
| """ | |
| with gr.Blocks() as demo: | |
| gr.Markdown(markdown_top) | |
| with gr.Row(): | |
| btn_thread_success = gr.Button("Run thread (success)") | |
| btn_thread_failing = gr.Button("Run thread (failing)") | |
| with gr.Row(): | |
| btn_process_success = gr.Button("Run process (success)") | |
| btn_process_failing = gr.Button("Run process (failing)") | |
| logs = LogsView() | |
| gr.Markdown(markdown_bottom) | |
| btn_thread_failing.click(fn_thread_failing, outputs=logs) | |
| btn_thread_success.click(fn_thread_success, outputs=logs) | |
| btn_process_failing.click(fn_process_failing, outputs=logs) | |
| btn_process_success.click(fn_process_success, outputs=logs) | |
| if __name__ == "__main__": | |
| demo.launch() | |
| ``` | |
| ## `LogsView` | |
| ### Initialization | |
| <table> | |
| <thead> | |
| <tr> | |
| <th align="left">name</th> | |
| <th align="left" style="width: 25%;">type</th> | |
| <th align="left">default</th> | |
| <th align="left">description</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td align="left"><code>value</code></td> | |
| <td align="left" style="width: 25%;"> | |
| ```python | |
| str | Callable | tuple[str] | None | |
| ``` | |
| </td> | |
| <td align="left"><code>None</code></td> | |
| <td align="left">Default value to show in the code editor. If callable, the function will be called whenever the app loads to set the initial value of the component.</td> | |
| </tr> | |
| <tr> | |
| <td align="left"><code>every</code></td> | |
| <td align="left" style="width: 25%;"> | |
| ```python | |
| float | None | |
| ``` | |
| </td> | |
| <td align="left"><code>None</code></td> | |
| <td align="left">If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise. The event can be accessed (e.g. to cancel it) via this component's .load_event attribute.</td> | |
| </tr> | |
| <tr> | |
| <td align="left"><code>lines</code></td> | |
| <td align="left" style="width: 25%;"> | |
| ```python | |
| int | |
| ``` | |
| </td> | |
| <td align="left"><code>5</code></td> | |
| <td align="left">None</td> | |
| </tr> | |
| <tr> | |
| <td align="left"><code>label</code></td> | |
| <td align="left" style="width: 25%;"> | |
| ```python | |
| str | None | |
| ``` | |
| </td> | |
| <td align="left"><code>None</code></td> | |
| <td align="left">The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.</td> | |
| </tr> | |
| <tr> | |
| <td align="left"><code>show_label</code></td> | |
| <td align="left" style="width: 25%;"> | |
| ```python | |
| bool | None | |
| ``` | |
| </td> | |
| <td align="left"><code>None</code></td> | |
| <td align="left">if True, will display label.</td> | |
| </tr> | |
| <tr> | |
| <td align="left"><code>container</code></td> | |
| <td align="left" style="width: 25%;"> | |
| ```python | |
| bool | |
| ``` | |
| </td> | |
| <td align="left"><code>True</code></td> | |
| <td align="left">If True, will place the component in a container - providing some extra padding around the border.</td> | |
| </tr> | |
| <tr> | |
| <td align="left"><code>scale</code></td> | |
| <td align="left" style="width: 25%;"> | |
| ```python | |
| int | None | |
| ``` | |
| </td> | |
| <td align="left"><code>None</code></td> | |
| <td align="left">relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.</td> | |
| </tr> | |
| <tr> | |
| <td align="left"><code>min_width</code></td> | |
| <td align="left" style="width: 25%;"> | |
| ```python | |
| int | |
| ``` | |
| </td> | |
| <td align="left"><code>160</code></td> | |
| <td align="left">minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.</td> | |
| </tr> | |
| <tr> | |
| <td align="left"><code>visible</code></td> | |
| <td align="left" style="width: 25%;"> | |
| ```python | |
| bool | |
| ``` | |
| </td> | |
| <td align="left"><code>True</code></td> | |
| <td align="left">If False, component will be hidden.</td> | |
| </tr> | |
| <tr> | |
| <td align="left"><code>elem_id</code></td> | |
| <td align="left" style="width: 25%;"> | |
| ```python | |
| str | None | |
| ``` | |
| </td> | |
| <td align="left"><code>None</code></td> | |
| <td align="left">An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.</td> | |
| </tr> | |
| <tr> | |
| <td align="left"><code>elem_classes</code></td> | |
| <td align="left" style="width: 25%;"> | |
| ```python | |
| list[str] | str | None | |
| ``` | |
| </td> | |
| <td align="left"><code>None</code></td> | |
| <td align="left">An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.</td> | |
| </tr> | |
| <tr> | |
| <td align="left"><code>render</code></td> | |
| <td align="left" style="width: 25%;"> | |
| ```python | |
| bool | |
| ``` | |
| </td> | |
| <td align="left"><code>True</code></td> | |
| <td align="left">If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.</td> | |
| </tr> | |
| </tbody></table> | |
| ### Events | |
| | name | description | | |
| |:-----|:------------| | |
| | `change` | Triggered when the value of the LogsView changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input. | | |
| | `input` | This listener is triggered when the user changes the value of the LogsView. | | |
| | `focus` | This listener is triggered when the LogsView is focused. | | |
| | `blur` | This listener is triggered when the LogsView is unfocused/blurred. | | |
| ### User function | |
| The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both). | |
| - When used as an Input, the component only impacts the input signature of the user function. | |
| - When used as an output, the component only impacts the return signature of the user function. | |
| The code snippet below is accurate in cases where the component is used as both an input and an output. | |
| - **As output:** Is passed, passes the code entered as a `str`. | |
| - **As input:** Should return, expects a list of `Log` logs. | |
| ```python | |
| def predict( | |
| value: LogsView | |
| ) -> list[Log]: | |
| return value | |
| ``` | |
| ## `Log` | |
| ```python | |
| @dataclass | |
| class Log: | |
| level: Literal[ | |
| "INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL" | |
| ] | |
| message: str | |
| timestamp: str | |
| ``` | |
| ## `LogsView` | |
| ```python | |
| class LogsView(Component): | |
| EVENTS = [ | |
| Events.change, | |
| Events.input, | |
| Events.focus, | |
| Events.blur, | |
| ] | |
| def __init__( | |
| self, | |
| value: str | Callable | tuple[str] | None = None, | |
| *, | |
| every: float | None = None, | |
| lines: int = 5, | |
| label: str | None = None, | |
| show_label: bool | None = None, | |
| container: bool = True, | |
| scale: int | None = None, | |
| min_width: int = 160, | |
| visible: bool = True, | |
| elem_id: str | None = None, | |
| elem_classes: list[str] | str | None = None, | |
| render: bool = True, | |
| ): | |
| self.language = "shell" | |
| self.lines = lines | |
| self.interactive = False | |
| super().__init__( | |
| label=label, | |
| every=every, | |
| show_label=show_label, | |
| container=container, | |
| scale=scale, | |
| min_width=min_width, | |
| visible=visible, | |
| elem_id=elem_id, | |
| elem_classes=elem_classes, | |
| render=render, | |
| value=value, | |
| ) | |
| def preprocess(self, payload: str | None) -> "LogsView": | |
| raise NotImplementedError( | |
| "LogsView cannot be used as an input component." | |
| ) | |
| def postprocess(self, value: List[Log]) -> List[Log]: | |
| return value | |
| def api_info(self) -> dict[str, Any]: | |
| return { | |
| "items": { | |
| "level": "string", | |
| "message": "string", | |
| "timestamp": "number", | |
| }, | |
| "title": "Logs", | |
| "type": "array", | |
| } | |
| def example_payload(self) -> Any: | |
| return [ | |
| Log( | |
| "INFO", | |
| "Hello World", | |
| datetime.now().isoformat(), | |
| ) | |
| ] | |
| def example_value(self) -> Any: | |
| return [ | |
| Log( | |
| "INFO", | |
| "Hello World", | |
| datetime.now().isoformat(), | |
| ) | |
| ] | |
| @classmethod | |
| def run_process( | |
| cls, | |
| command: List[str], | |
| date_format: str = "%Y-%m-%d %H:%M:%S", | |
| ) -> Generator[List[Log], None, None]: | |
| process = subprocess.Popen( | |
| command, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT, | |
| text=True, | |
| ) | |
| if process.stdout is None: | |
| raise ValueError("stdout is None") | |
| logs = [] | |
| def _log(level: str, message: str): | |
| log = Log( | |
| level=level, | |
| message=message, | |
| timestamp=datetime.now().strftime( | |
| date_format | |
| ), | |
| ) | |
| logs.append(log) | |
| return logs | |
| _log("INFO", f"Running {' '.join(command)}") | |
| for line in process.stdout: | |
| yield _log("INFO", line.strip()) | |
| # TODO: what if task is cancelled but process is still running? | |
| process.stdout.close() | |
| return_code = process.wait() | |
| if return_code: | |
| yield _log( | |
| "ERROR", | |
| f"Process exited with code {return_code}", | |
| ) | |
| else: | |
| yield _log( | |
| "INFO", "Process exited successfully" | |
| ) | |
| @classmethod | |
| def run_thread( | |
| cls, | |
| fn: Callable, | |
| log_level: int = logging.INFO, | |
| logger_name: str | None = None, | |
| date_format: str = "%Y-%m-%d %H:%M:%S", | |
| **kwargs, | |
| ) -> Generator[List[Log], None, None]: | |
| logs = [ | |
| Log( | |
| level="INFO", | |
| message=f"Running {fn.__name__}({', '.join(f'{k}={v}' for k, v in kwargs.items())})", | |
| timestamp=datetime.now().strftime( | |
| date_format | |
| ), | |
| ) | |
| ] | |
| yield logs | |
| thread = Thread( | |
| target=non_failing_fn(fn), kwargs=kwargs | |
| ) | |
| def _log(record: logging.LogRecord) -> bool: | |
| if record.thread != thread.ident: | |
| return False # Skip if not from the thread | |
| if logger_name and not record.name.startswith( | |
| logger_name | |
| ): | |
| return False # Skip if not from the logger | |
| if record.levelno < log_level: | |
| return False # Skip if too verbose | |
| log = Log( | |
| level=record.levelname, | |
| message=record.getMessage(), | |
| timestamp=datetime.fromtimestamp( | |
| record.created | |
| ).strftime(date_format), | |
| ) | |
| logs.append(log) | |
| return True | |
| with capture_logging(log_level) as log_queue: | |
| thread.start() | |
| # Loop to capture and yield logs from the thread | |
| while thread.is_alive(): | |
| while True: | |
| try: | |
| if _log(log_queue.get_nowait()): | |
| yield logs | |
| except queue.Empty: | |
| break | |
| thread.join( | |
| timeout=0.1 | |
| ) # adjust the timeout as needed | |
| # After the thread completes, yield any remaining logs | |
| while True: | |
| try: | |
| if _log(log_queue.get_nowait()): | |
| yield logs | |
| except queue.Empty: | |
| break | |
| logs.append( | |
| Log( | |
| level="INFO", | |
| message="Thread completed successfully", | |
| timestamp=datetime.now().strftime( | |
| date_format | |
| ), | |
| ) | |
| ) | |
| ``` | |
