yay
Browse files- app.py +2 -1
- gradio_logsview-0.0.3-py3-none-any.whl +0 -0
- space.py +26 -13
- src/README.md +1 -1
- src/backend/gradio_logsview/__init__.py +2 -2
- src/backend/gradio_logsview/logsview.py +153 -114
- src/demo/app.py +44 -8
- src/demo/space.py +4 -2
- src/pyproject.toml +1 -1
app.py
CHANGED
@@ -3,6 +3,7 @@ import random
|
|
3 |
import time
|
4 |
|
5 |
import gradio as gr
|
|
|
6 |
from gradio_logsview import LogsView
|
7 |
|
8 |
|
@@ -82,7 +83,7 @@ from gradio_logsview import LogsView
|
|
82 |
def fn_process():
|
83 |
# Run a process and capture all logs from the process
|
84 |
yield from LogsView.run_process(
|
85 |
-
cmd=[mergekit-yaml", "config.yaml", "merge", "--copy-", "--cuda", "--low-cpu-memory"]
|
86 |
)
|
87 |
|
88 |
with gr.Blocks() as demo:
|
|
|
3 |
import time
|
4 |
|
5 |
import gradio as gr
|
6 |
+
|
7 |
from gradio_logsview import LogsView
|
8 |
|
9 |
|
|
|
83 |
def fn_process():
|
84 |
# Run a process and capture all logs from the process
|
85 |
yield from LogsView.run_process(
|
86 |
+
cmd=["mergekit-yaml", "config.yaml", "merge", "--copy-", "--cuda", "--low-cpu-memory"]
|
87 |
)
|
88 |
|
89 |
with gr.Blocks() as demo:
|
gradio_logsview-0.0.3-py3-none-any.whl
ADDED
Binary file (324 kB). View file
|
|
space.py
CHANGED
@@ -3,7 +3,8 @@ import random
|
|
3 |
import time
|
4 |
|
5 |
import gradio as gr
|
6 |
-
|
|
|
7 |
|
8 |
|
9 |
def random_values(failing: bool = False):
|
@@ -26,19 +27,27 @@ def random_values(failing: bool = False):
|
|
26 |
|
27 |
|
28 |
def fn_process_success():
|
29 |
-
|
|
|
|
|
30 |
|
31 |
|
32 |
def fn_process_failing():
|
33 |
-
|
|
|
|
|
34 |
|
35 |
|
36 |
def fn_thread_success():
|
37 |
-
|
|
|
|
|
38 |
|
39 |
|
40 |
def fn_thread_failing():
|
41 |
-
|
|
|
|
|
42 |
|
43 |
|
44 |
markdown_top = """
|
@@ -56,16 +65,15 @@ markdown_bottom = """
|
|
56 |
## Installation
|
57 |
|
58 |
```
|
59 |
-
pip install https://huggingface.co/spaces/Wauplin/gradio_logsview/resolve/main/gradio_logsview-0.0.
|
60 |
```
|
61 |
|
62 |
or add this line to your `requirements.txt`:
|
63 |
|
64 |
```
|
65 |
-
gradio_logsview@https://huggingface.co/spaces/Wauplin/gradio_logsview/resolve/main/gradio_logsview-0.0.
|
66 |
```
|
67 |
|
68 |
-
|
69 |
## How to run in a thread?
|
70 |
|
71 |
With `LogsView.run_thread`, you can run a function in a separate thread and capture logs in real-time.
|
@@ -77,7 +85,9 @@ from gradio_logsview import LogsView
|
|
77 |
def fn_thread():
|
78 |
# Run `my_function` in a separate thread
|
79 |
# All logs above `INFO` level will be captured and displayed in real-time.
|
80 |
-
|
|
|
|
|
81 |
|
82 |
with gr.Blocks() as demo:
|
83 |
logs = LogsView()
|
@@ -94,9 +104,11 @@ from gradio_logsview import LogsView
|
|
94 |
|
95 |
def fn_process():
|
96 |
# Run a process and capture all logs from the process
|
97 |
-
|
98 |
-
|
|
|
99 |
)
|
|
|
100 |
|
101 |
with gr.Blocks() as demo:
|
102 |
logs = LogsView()
|
@@ -110,8 +122,9 @@ with gr.Blocks() as demo:
|
|
110 |
- [ ] format logs client-side (front-end)
|
111 |
- [ ] scrollable logs if more than N lines (front-end)
|
112 |
- [ ] format each log only once (front-end)
|
113 |
-
- [
|
114 |
-
- [
|
|
|
115 |
- [ ] disable interactivity + remove all code editing logic (both?)
|
116 |
- [ ] how to handle progress bars? (i.e when logs are overwritten in terminal)
|
117 |
"""
|
|
|
3 |
import time
|
4 |
|
5 |
import gradio as gr
|
6 |
+
|
7 |
+
from gradio_logsview import LogsView, LogsViewRunner
|
8 |
|
9 |
|
10 |
def random_values(failing: bool = False):
|
|
|
27 |
|
28 |
|
29 |
def fn_process_success():
|
30 |
+
runner = LogsViewRunner()
|
31 |
+
yield from runner.run_process(["python", "-u", "demo/script.py"])
|
32 |
+
yield runner.log(f"Runner: {runner}")
|
33 |
|
34 |
|
35 |
def fn_process_failing():
|
36 |
+
runner = LogsViewRunner()
|
37 |
+
yield from runner.run_process(["python", "-u", "demo/script.py", "--failing"])
|
38 |
+
yield runner.log(f"Runner: {runner}")
|
39 |
|
40 |
|
41 |
def fn_thread_success():
|
42 |
+
runner = LogsViewRunner()
|
43 |
+
yield from runner.run_thread(random_values, log_level=logging.INFO, failing=False)
|
44 |
+
yield runner.log(f"Runner: {runner}")
|
45 |
|
46 |
|
47 |
def fn_thread_failing():
|
48 |
+
runner = LogsViewRunner()
|
49 |
+
yield from runner.run_thread(random_values, log_level=logging.INFO, failing=True)
|
50 |
+
yield runner.log(f"Runner: {runner}")
|
51 |
|
52 |
|
53 |
markdown_top = """
|
|
|
65 |
## Installation
|
66 |
|
67 |
```
|
68 |
+
pip install https://huggingface.co/spaces/Wauplin/gradio_logsview/resolve/main/gradio_logsview-0.0.3-py3-none-any.whl
|
69 |
```
|
70 |
|
71 |
or add this line to your `requirements.txt`:
|
72 |
|
73 |
```
|
74 |
+
gradio_logsview@https://huggingface.co/spaces/Wauplin/gradio_logsview/resolve/main/gradio_logsview-0.0.3-py3-none-any.whl
|
75 |
```
|
76 |
|
|
|
77 |
## How to run in a thread?
|
78 |
|
79 |
With `LogsView.run_thread`, you can run a function in a separate thread and capture logs in real-time.
|
|
|
85 |
def fn_thread():
|
86 |
# Run `my_function` in a separate thread
|
87 |
# All logs above `INFO` level will be captured and displayed in real-time.
|
88 |
+
runner = LogsViewRunner() # Initialize the runner
|
89 |
+
yield from runner.run_thread(my_function, log_level=logging.INFO, arg1="value1")
|
90 |
+
yield runner.log(f"Runner: {runner}") # Log any message
|
91 |
|
92 |
with gr.Blocks() as demo:
|
93 |
logs = LogsView()
|
|
|
104 |
|
105 |
def fn_process():
|
106 |
# Run a process and capture all logs from the process
|
107 |
+
runner = LogsViewRunner() # Initialize the runner
|
108 |
+
yield from runner.run_process(
|
109 |
+
cmd=["mergekit-yaml", "config.yaml", "merge", "--copy-", "--cuda", "--low-cpu-memory"]
|
110 |
)
|
111 |
+
yield runner.log(f"Runner: {runner}") # Log any message
|
112 |
|
113 |
with gr.Blocks() as demo:
|
114 |
logs = LogsView()
|
|
|
122 |
- [ ] format logs client-side (front-end)
|
123 |
- [ ] scrollable logs if more than N lines (front-end)
|
124 |
- [ ] format each log only once (front-end)
|
125 |
+
- [x] stop process if `run_process` gets cancelled (back-end)
|
126 |
+
- [x] correctly pass error stacktrace in `run_thread` (back-end)
|
127 |
+
- [ ] correctly catch print statements in `run_thread` (back-end)
|
128 |
- [ ] disable interactivity + remove all code editing logic (both?)
|
129 |
- [ ] how to handle progress bars? (i.e when logs are overwritten in terminal)
|
130 |
"""
|
src/README.md
CHANGED
@@ -97,7 +97,7 @@ from gradio_logsview import LogsView
|
|
97 |
def fn_process():
|
98 |
# Run a process and capture all logs from the process
|
99 |
yield from LogsView.run_process(
|
100 |
-
cmd=[mergekit-yaml", "config.yaml", "merge", "--copy-", "--cuda", "--low-cpu-memory"]
|
101 |
)
|
102 |
|
103 |
with gr.Blocks() as demo:
|
|
|
97 |
def fn_process():
|
98 |
# Run a process and capture all logs from the process
|
99 |
yield from LogsView.run_process(
|
100 |
+
cmd=["mergekit-yaml", "config.yaml", "merge", "--copy-", "--cuda", "--low-cpu-memory"]
|
101 |
)
|
102 |
|
103 |
with gr.Blocks() as demo:
|
src/backend/gradio_logsview/__init__.py
CHANGED
@@ -1,3 +1,3 @@
|
|
1 |
-
from .logsview import LogsView
|
2 |
|
3 |
-
__all__ = ["LogsView"]
|
|
|
1 |
+
from .logsview import Log, LogsView, LogsViewRunner
|
2 |
|
3 |
+
__all__ = ["Log", "LogsView", "LogsViewRunner"]
|
src/backend/gradio_logsview/logsview.py
CHANGED
@@ -1,12 +1,10 @@
|
|
1 |
"""LogsView() custom component"""
|
2 |
|
3 |
-
from __future__ import annotations
|
4 |
-
|
5 |
import logging
|
6 |
import queue
|
7 |
import subprocess
|
8 |
-
import threading
|
9 |
import time
|
|
|
10 |
from contextlib import contextmanager
|
11 |
from dataclasses import dataclass
|
12 |
from datetime import datetime
|
@@ -14,19 +12,163 @@ from functools import wraps
|
|
14 |
from logging.handlers import QueueHandler
|
15 |
from queue import Queue
|
16 |
from threading import Thread
|
17 |
-
from typing import Any, Callable, Generator,
|
18 |
|
19 |
from gradio.components.base import Component
|
20 |
from gradio.events import Events
|
21 |
|
|
|
|
|
22 |
|
23 |
@dataclass
|
24 |
class Log:
|
25 |
-
level:
|
26 |
message: str
|
27 |
timestamp: str
|
28 |
|
29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
class LogsView(Component):
|
31 |
"""
|
32 |
Creates a component to visualize logs from a subprocess in real-time.
|
@@ -86,7 +228,7 @@ class LogsView(Component):
|
|
86 |
value=value,
|
87 |
)
|
88 |
|
89 |
-
def preprocess(self, payload: str | None) ->
|
90 |
"""
|
91 |
Parameters:
|
92 |
payload: string corresponding to the code
|
@@ -117,111 +259,6 @@ class LogsView(Component):
|
|
117 |
def example_value(self) -> Any:
|
118 |
return [Log("INFO", "Hello World", datetime.now().isoformat())]
|
119 |
|
120 |
-
@classmethod
|
121 |
-
def run_process(
|
122 |
-
cls, command: List[str], date_format: str = "%Y-%m-%d %H:%M:%S"
|
123 |
-
) -> Generator[List[Log], None, None]:
|
124 |
-
"""Run a command in a subprocess and yield logs in real-time."""
|
125 |
-
process = subprocess.Popen(
|
126 |
-
command,
|
127 |
-
stdout=subprocess.PIPE,
|
128 |
-
stderr=subprocess.STDOUT,
|
129 |
-
text=True,
|
130 |
-
)
|
131 |
-
|
132 |
-
if process.stdout is None:
|
133 |
-
raise ValueError("stdout is None")
|
134 |
-
|
135 |
-
logs = []
|
136 |
-
|
137 |
-
def _log(level: str, message: str):
|
138 |
-
log = Log(
|
139 |
-
level=level,
|
140 |
-
message=message,
|
141 |
-
timestamp=datetime.now().strftime(date_format),
|
142 |
-
)
|
143 |
-
logs.append(log)
|
144 |
-
return logs
|
145 |
-
|
146 |
-
_log("INFO", f"Running {' '.join(command)}")
|
147 |
-
for line in process.stdout:
|
148 |
-
yield _log("INFO", line.strip())
|
149 |
-
|
150 |
-
# TODO: what if task is cancelled but process is still running?
|
151 |
-
|
152 |
-
process.stdout.close()
|
153 |
-
return_code = process.wait()
|
154 |
-
if return_code:
|
155 |
-
yield _log("ERROR", f"Process exited with code {return_code}")
|
156 |
-
else:
|
157 |
-
yield _log("INFO", "Process exited successfully")
|
158 |
-
|
159 |
-
@classmethod
|
160 |
-
def run_thread(
|
161 |
-
cls,
|
162 |
-
fn: Callable,
|
163 |
-
log_level: int = logging.INFO,
|
164 |
-
logger_name: str | None = None,
|
165 |
-
date_format: str = "%Y-%m-%d %H:%M:%S",
|
166 |
-
**kwargs,
|
167 |
-
) -> Generator[List[Log], None, None]:
|
168 |
-
"""Run a function in a thread and capture logs in real-time to yield them."""
|
169 |
-
logs = [
|
170 |
-
Log(
|
171 |
-
level="INFO",
|
172 |
-
message=f"Running {fn.__name__}({', '.join(f'{k}={v}' for k, v in kwargs.items())})",
|
173 |
-
timestamp=datetime.now().strftime(date_format),
|
174 |
-
)
|
175 |
-
]
|
176 |
-
yield logs
|
177 |
-
|
178 |
-
thread = Thread(target=non_failing_fn(fn), kwargs=kwargs)
|
179 |
-
|
180 |
-
def _log(record: logging.LogRecord) -> bool:
|
181 |
-
"""Handle log record and return True if log should be yielded."""
|
182 |
-
if record.thread != thread.ident:
|
183 |
-
return False # Skip if not from the thread
|
184 |
-
if logger_name and not record.name.startswith(logger_name):
|
185 |
-
return False # Skip if not from the logger
|
186 |
-
if record.levelno < log_level:
|
187 |
-
return False # Skip if too verbose
|
188 |
-
log = Log(
|
189 |
-
level=record.levelname,
|
190 |
-
message=record.getMessage(),
|
191 |
-
timestamp=datetime.fromtimestamp(record.created).strftime(date_format),
|
192 |
-
)
|
193 |
-
logs.append(log)
|
194 |
-
return True
|
195 |
-
|
196 |
-
with capture_logging(log_level) as log_queue:
|
197 |
-
thread.start()
|
198 |
-
|
199 |
-
# Loop to capture and yield logs from the thread
|
200 |
-
while thread.is_alive():
|
201 |
-
while True:
|
202 |
-
try:
|
203 |
-
if _log(log_queue.get_nowait()):
|
204 |
-
yield logs
|
205 |
-
except queue.Empty:
|
206 |
-
break
|
207 |
-
thread.join(timeout=0.1) # adjust the timeout as needed
|
208 |
-
|
209 |
-
# After the thread completes, yield any remaining logs
|
210 |
-
while True:
|
211 |
-
try:
|
212 |
-
if _log(log_queue.get_nowait()):
|
213 |
-
yield logs
|
214 |
-
except queue.Empty:
|
215 |
-
break
|
216 |
-
|
217 |
-
logs.append(
|
218 |
-
Log(
|
219 |
-
level="INFO",
|
220 |
-
message="Thread completed successfully",
|
221 |
-
timestamp=datetime.now().strftime(date_format),
|
222 |
-
)
|
223 |
-
)
|
224 |
-
|
225 |
|
226 |
@contextmanager
|
227 |
def capture_logging(log_level: int) -> Generator[Queue, None, None]:
|
@@ -240,11 +277,13 @@ def capture_logging(log_level: int) -> Generator[Queue, None, None]:
|
|
240 |
|
241 |
|
242 |
def non_failing_fn(fn: Callable, *args, **kwargs) -> Callable:
|
|
|
|
|
243 |
@wraps(fn)
|
244 |
def _inner(*args, **kwargs):
|
245 |
try:
|
246 |
-
|
247 |
except Exception as e:
|
248 |
-
|
249 |
|
250 |
-
return _inner
|
|
|
1 |
"""LogsView() custom component"""
|
2 |
|
|
|
|
|
3 |
import logging
|
4 |
import queue
|
5 |
import subprocess
|
|
|
6 |
import time
|
7 |
+
import traceback
|
8 |
from contextlib import contextmanager
|
9 |
from dataclasses import dataclass
|
10 |
from datetime import datetime
|
|
|
12 |
from logging.handlers import QueueHandler
|
13 |
from queue import Queue
|
14 |
from threading import Thread
|
15 |
+
from typing import Any, Callable, Generator, List, Literal, NoReturn
|
16 |
|
17 |
from gradio.components.base import Component
|
18 |
from gradio.events import Events
|
19 |
|
20 |
+
LOGGING_LEVEL_T = Literal["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"]
|
21 |
+
|
22 |
|
23 |
@dataclass
|
24 |
class Log:
|
25 |
+
level: LOGGING_LEVEL_T
|
26 |
message: str
|
27 |
timestamp: str
|
28 |
|
29 |
|
30 |
+
class LogsViewRunner:
|
31 |
+
def __init__(self, date_format: str = "%Y-%m-%d %H:%M:%S"):
|
32 |
+
self.date_format = date_format
|
33 |
+
self.logs: List[Log] = []
|
34 |
+
|
35 |
+
# Runner state
|
36 |
+
self.completed = False
|
37 |
+
self.error = False
|
38 |
+
self.process: subprocess.Popen | None = None
|
39 |
+
self.thread: Thread | None = None
|
40 |
+
|
41 |
+
def log(
|
42 |
+
self,
|
43 |
+
message: str,
|
44 |
+
level: LOGGING_LEVEL_T = "INFO",
|
45 |
+
timestamp: datetime | float | int | str | None = None,
|
46 |
+
) -> List[Log]:
|
47 |
+
if timestamp is None:
|
48 |
+
timestamp = time.time()
|
49 |
+
if isinstance(timestamp, (float, int)):
|
50 |
+
timestamp = datetime.fromtimestamp(timestamp)
|
51 |
+
if isinstance(timestamp, datetime):
|
52 |
+
timestamp = timestamp.strftime(self.date_format)
|
53 |
+
self.logs.append(Log(level=level, message=message, timestamp=timestamp))
|
54 |
+
return self.logs
|
55 |
+
|
56 |
+
def run_process(
|
57 |
+
self, command: List[str], **kwargs
|
58 |
+
) -> Generator[List[Log], None, None]:
|
59 |
+
"""Run a command in a subprocess and yield logs in real-time."""
|
60 |
+
self.process = subprocess.Popen(
|
61 |
+
command,
|
62 |
+
stdout=subprocess.PIPE,
|
63 |
+
stderr=subprocess.STDOUT,
|
64 |
+
text=True,
|
65 |
+
**kwargs,
|
66 |
+
)
|
67 |
+
|
68 |
+
if self.process.stdout is None:
|
69 |
+
raise ValueError("stdout is None")
|
70 |
+
|
71 |
+
yield self.log(f"Running {' '.join(command)}")
|
72 |
+
for line in self.process.stdout:
|
73 |
+
yield self.log(line.strip())
|
74 |
+
|
75 |
+
self.process.stdout.close()
|
76 |
+
return_code = self.process.wait()
|
77 |
+
if return_code:
|
78 |
+
yield self.log(f"Process exited with code {return_code}", level="ERROR")
|
79 |
+
self.completed = True
|
80 |
+
self.error = True
|
81 |
+
else:
|
82 |
+
yield self.log("Process exited successfully", level="INFO")
|
83 |
+
self.completed = True
|
84 |
+
self.error = False
|
85 |
+
|
86 |
+
def run_thread(
|
87 |
+
self,
|
88 |
+
fn: Callable,
|
89 |
+
log_level: int = logging.INFO,
|
90 |
+
logger_name: str | None = None,
|
91 |
+
**kwargs,
|
92 |
+
) -> Generator[List[Log], None, None]:
|
93 |
+
"""Run a function in a thread and capture logs in real-time to yield them."""
|
94 |
+
yield self.log(
|
95 |
+
f"Running {fn.__name__}({', '.join(f'{k}={v}' for k, v in kwargs.items())})"
|
96 |
+
)
|
97 |
+
|
98 |
+
error_queue, wrapped_fn = non_failing_fn(fn)
|
99 |
+
self.thread = Thread(target=wrapped_fn, kwargs=kwargs)
|
100 |
+
|
101 |
+
def _log(record: logging.LogRecord) -> bool:
|
102 |
+
"""Handle log record and return True if log should be yielded."""
|
103 |
+
if record.thread != self.thread.ident:
|
104 |
+
return False # Skip if not from the thread
|
105 |
+
if logger_name and not record.name.startswith(logger_name):
|
106 |
+
return False # Skip if not from the logger
|
107 |
+
if record.levelno < log_level:
|
108 |
+
return False # Skip if too verbose
|
109 |
+
self.log(
|
110 |
+
message=record.getMessage(),
|
111 |
+
level=record.levelname,
|
112 |
+
timestamp=record.created,
|
113 |
+
)
|
114 |
+
return True
|
115 |
+
|
116 |
+
with capture_logging(log_level) as log_queue:
|
117 |
+
# Start thread and loop to capture and yield logs from the thread
|
118 |
+
self.thread.start()
|
119 |
+
while self.thread.is_alive():
|
120 |
+
while True:
|
121 |
+
try:
|
122 |
+
if _log(log_queue.get_nowait()):
|
123 |
+
yield self.logs
|
124 |
+
except queue.Empty:
|
125 |
+
break
|
126 |
+
self.thread.join(timeout=0.1) # adjust the timeout as needed
|
127 |
+
|
128 |
+
# After the thread completes, yield any remaining logs
|
129 |
+
while True:
|
130 |
+
try:
|
131 |
+
if _log(log_queue.get_nowait()):
|
132 |
+
yield self.logs
|
133 |
+
except queue.Empty:
|
134 |
+
break
|
135 |
+
|
136 |
+
try:
|
137 |
+
error = error_queue.get_nowait()
|
138 |
+
except queue.Empty:
|
139 |
+
error = None
|
140 |
+
if error is not None:
|
141 |
+
msg = (
|
142 |
+
f"Error in '{fn.__name__}':"
|
143 |
+
+ "\n"
|
144 |
+
+ "\n".join(
|
145 |
+
line.strip("\n")
|
146 |
+
for line in traceback.format_tb(error.__traceback__)
|
147 |
+
if line.strip()
|
148 |
+
)
|
149 |
+
+ "\n\n"
|
150 |
+
+ str(error)
|
151 |
+
)
|
152 |
+
yield self.log(msg, level="ERROR")
|
153 |
+
self.completed = True
|
154 |
+
self.error = True
|
155 |
+
else:
|
156 |
+
yield self.log("Thread completed successfully")
|
157 |
+
self.completed = True
|
158 |
+
self.error = False
|
159 |
+
|
160 |
+
def __repr__(self) -> str:
|
161 |
+
return f"<LogsViewRunner nb_logs={len(self.logs)} completed={self.completed} error={self.error}>"
|
162 |
+
|
163 |
+
def __del__(self):
|
164 |
+
if self.process and self.process.poll() is None:
|
165 |
+
print(f"Killing process: {self.process}")
|
166 |
+
self.process.kill()
|
167 |
+
if self.thread and self.thread.is_alive():
|
168 |
+
print(f"Joining thread: {self.thread}")
|
169 |
+
self.thread.join()
|
170 |
+
|
171 |
+
|
172 |
class LogsView(Component):
|
173 |
"""
|
174 |
Creates a component to visualize logs from a subprocess in real-time.
|
|
|
228 |
value=value,
|
229 |
)
|
230 |
|
231 |
+
def preprocess(self, payload: str | None) -> NoReturn:
|
232 |
"""
|
233 |
Parameters:
|
234 |
payload: string corresponding to the code
|
|
|
259 |
def example_value(self) -> Any:
|
260 |
return [Log("INFO", "Hello World", datetime.now().isoformat())]
|
261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
262 |
|
263 |
@contextmanager
|
264 |
def capture_logging(log_level: int) -> Generator[Queue, None, None]:
|
|
|
277 |
|
278 |
|
279 |
def non_failing_fn(fn: Callable, *args, **kwargs) -> Callable:
|
280 |
+
error_queue = queue.Queue()
|
281 |
+
|
282 |
@wraps(fn)
|
283 |
def _inner(*args, **kwargs):
|
284 |
try:
|
285 |
+
fn(*args, **kwargs)
|
286 |
except Exception as e:
|
287 |
+
error_queue.put(e)
|
288 |
|
289 |
+
return error_queue, _inner
|
src/demo/app.py
CHANGED
@@ -3,7 +3,7 @@ import random
|
|
3 |
import time
|
4 |
|
5 |
import gradio as gr
|
6 |
-
from gradio_logsview import LogsView
|
7 |
|
8 |
|
9 |
def random_values(failing: bool = False):
|
@@ -26,19 +26,27 @@ def random_values(failing: bool = False):
|
|
26 |
|
27 |
|
28 |
def fn_process_success():
|
29 |
-
|
|
|
|
|
30 |
|
31 |
|
32 |
def fn_process_failing():
|
33 |
-
|
|
|
|
|
34 |
|
35 |
|
36 |
def fn_thread_success():
|
37 |
-
|
|
|
|
|
38 |
|
39 |
|
40 |
def fn_thread_failing():
|
41 |
-
|
|
|
|
|
42 |
|
43 |
|
44 |
markdown_top = """
|
@@ -53,6 +61,18 @@ In the process example, logs are generated by a Python script but any command ca
|
|
53 |
|
54 |
|
55 |
markdown_bottom = """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
## How to run in a thread?
|
57 |
|
58 |
With `LogsView.run_thread`, you can run a function in a separate thread and capture logs in real-time.
|
@@ -64,7 +84,9 @@ from gradio_logsview import LogsView
|
|
64 |
def fn_thread():
|
65 |
# Run `my_function` in a separate thread
|
66 |
# All logs above `INFO` level will be captured and displayed in real-time.
|
67 |
-
|
|
|
|
|
68 |
|
69 |
with gr.Blocks() as demo:
|
70 |
logs = LogsView()
|
@@ -81,15 +103,29 @@ from gradio_logsview import LogsView
|
|
81 |
|
82 |
def fn_process():
|
83 |
# Run a process and capture all logs from the process
|
84 |
-
|
85 |
-
|
|
|
86 |
)
|
|
|
87 |
|
88 |
with gr.Blocks() as demo:
|
89 |
logs = LogsView()
|
90 |
btn = gr.Button("Run process")
|
91 |
btn.click(fn_process, outputs=logs)
|
92 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
"""
|
94 |
|
95 |
with gr.Blocks() as demo:
|
|
|
3 |
import time
|
4 |
|
5 |
import gradio as gr
|
6 |
+
from gradio_logsview import LogsView, LogsViewRunner
|
7 |
|
8 |
|
9 |
def random_values(failing: bool = False):
|
|
|
26 |
|
27 |
|
28 |
def fn_process_success():
|
29 |
+
runner = LogsViewRunner()
|
30 |
+
yield from runner.run_process(["python", "-u", "demo/script.py"])
|
31 |
+
yield runner.log(f"Runner: {runner}")
|
32 |
|
33 |
|
34 |
def fn_process_failing():
|
35 |
+
runner = LogsViewRunner()
|
36 |
+
yield from runner.run_process(["python", "-u", "demo/script.py", "--failing"])
|
37 |
+
yield runner.log(f"Runner: {runner}")
|
38 |
|
39 |
|
40 |
def fn_thread_success():
|
41 |
+
runner = LogsViewRunner()
|
42 |
+
yield from runner.run_thread(random_values, log_level=logging.INFO, failing=False)
|
43 |
+
yield runner.log(f"Runner: {runner}")
|
44 |
|
45 |
|
46 |
def fn_thread_failing():
|
47 |
+
runner = LogsViewRunner()
|
48 |
+
yield from runner.run_thread(random_values, log_level=logging.INFO, failing=True)
|
49 |
+
yield runner.log(f"Runner: {runner}")
|
50 |
|
51 |
|
52 |
markdown_top = """
|
|
|
61 |
|
62 |
|
63 |
markdown_bottom = """
|
64 |
+
## Installation
|
65 |
+
|
66 |
+
```
|
67 |
+
pip install https://huggingface.co/spaces/Wauplin/gradio_logsview/resolve/main/gradio_logsview-0.0.3-py3-none-any.whl
|
68 |
+
```
|
69 |
+
|
70 |
+
or add this line to your `requirements.txt`:
|
71 |
+
|
72 |
+
```
|
73 |
+
gradio_logsview@https://huggingface.co/spaces/Wauplin/gradio_logsview/resolve/main/gradio_logsview-0.0.3-py3-none-any.whl
|
74 |
+
```
|
75 |
+
|
76 |
## How to run in a thread?
|
77 |
|
78 |
With `LogsView.run_thread`, you can run a function in a separate thread and capture logs in real-time.
|
|
|
84 |
def fn_thread():
|
85 |
# Run `my_function` in a separate thread
|
86 |
# All logs above `INFO` level will be captured and displayed in real-time.
|
87 |
+
runner = LogsViewRunner() # Initialize the runner
|
88 |
+
yield from runner.run_thread(my_function, log_level=logging.INFO, arg1="value1")
|
89 |
+
yield runner.log(f"Runner: {runner}") # Log any message
|
90 |
|
91 |
with gr.Blocks() as demo:
|
92 |
logs = LogsView()
|
|
|
103 |
|
104 |
def fn_process():
|
105 |
# Run a process and capture all logs from the process
|
106 |
+
runner = LogsViewRunner() # Initialize the runner
|
107 |
+
yield from runner.run_process(
|
108 |
+
cmd=["mergekit-yaml", "config.yaml", "merge", "--copy-", "--cuda", "--low-cpu-memory"]
|
109 |
)
|
110 |
+
yield runner.log(f"Runner: {runner}") # Log any message
|
111 |
|
112 |
with gr.Blocks() as demo:
|
113 |
logs = LogsView()
|
114 |
btn = gr.Button("Run process")
|
115 |
btn.click(fn_process, outputs=logs)
|
116 |
```
|
117 |
+
|
118 |
+
## TODO
|
119 |
+
|
120 |
+
- [ ] display logs with colors (front-end)
|
121 |
+
- [ ] format logs client-side (front-end)
|
122 |
+
- [ ] scrollable logs if more than N lines (front-end)
|
123 |
+
- [ ] format each log only once (front-end)
|
124 |
+
- [x] stop process if `run_process` gets cancelled (back-end)
|
125 |
+
- [x] correctly pass error stacktrace in `run_thread` (back-end)
|
126 |
+
- [ ] correctly catch print statements in `run_thread` (back-end)
|
127 |
+
- [ ] disable interactivity + remove all code editing logic (both?)
|
128 |
+
- [ ] how to handle progress bars? (i.e when logs are overwritten in terminal)
|
129 |
"""
|
130 |
|
131 |
with gr.Blocks() as demo:
|
src/demo/space.py
CHANGED
@@ -1,7 +1,9 @@
|
|
1 |
|
|
|
|
|
2 |
import gradio as gr
|
|
|
3 |
from app import demo as app
|
4 |
-
import os
|
5 |
|
6 |
_docs = {'LogsView': {'description': 'Creates a component to visualize logs from a subprocess in real-time.', 'members': {'__init__': {'value': {'type': 'str | Callable | tuple[str] | None', 'default': 'None', 'description': '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.'}, 'every': {'type': 'float | None', 'default': 'None', 'description': "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."}, 'lines': {'type': 'int', 'default': '5', 'description': None}, 'label': {'type': 'str | None', 'default': 'None', 'description': '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.'}, 'show_label': {'type': 'bool | None', 'default': 'None', 'description': 'if True, will display label.'}, 'container': {'type': 'bool', 'default': 'True', 'description': 'If True, will place the component in a container - providing some extra padding around the border.'}, 'scale': {'type': 'int | None', 'default': 'None', 'description': '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.'}, 'min_width': {'type': 'int', 'default': '160', 'description': '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.'}, 'visible': {'type': 'bool', 'default': 'True', 'description': 'If False, component will be hidden.'}, 'elem_id': {'type': 'str | None', 'default': 'None', 'description': 'An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'elem_classes': {'type': 'list[str] | str | None', 'default': 'None', 'description': '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.'}, 'render': {'type': 'bool', 'default': 'True', 'description': '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.'}}, 'postprocess': {'value': {'type': 'list[Log]', 'description': 'Expects a list of `Log` logs.'}}, 'preprocess': {'return': {'type': 'LogsView', 'description': 'Passes the code entered as a `str`.'}, 'value': None}}, 'events': {'change': {'type': None, 'default': None, 'description': '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': {'type': None, 'default': None, 'description': 'This listener is triggered when the user changes the value of the LogsView.'}, 'focus': {'type': None, 'default': None, 'description': 'This listener is triggered when the LogsView is focused.'}, 'blur': {'type': None, 'default': None, 'description': 'This listener is triggered when the LogsView is unfocused/blurred.'}}}, '__meta__': {'additional_interfaces': {'Log': {'source': '@dataclass\nclass Log:\n level: Literal[\n "INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"\n ]\n message: str\n timestamp: str'}, 'LogsView': {'source': 'class LogsView(Component):\n EVENTS = [\n Events.change,\n Events.input,\n Events.focus,\n Events.blur,\n ]\n\n def __init__(\n self,\n value: str | Callable | tuple[str] | None = None,\n *,\n every: float | None = None,\n lines: int = 5,\n label: str | None = None,\n show_label: bool | None = None,\n container: bool = True,\n scale: int | None = None,\n min_width: int = 160,\n visible: bool = True,\n elem_id: str | None = None,\n elem_classes: list[str] | str | None = None,\n render: bool = True,\n ):\n self.language = "shell"\n self.lines = lines\n self.interactive = False\n super().__init__(\n label=label,\n every=every,\n show_label=show_label,\n container=container,\n scale=scale,\n min_width=min_width,\n visible=visible,\n elem_id=elem_id,\n elem_classes=elem_classes,\n render=render,\n value=value,\n )\n\n def preprocess(self, payload: str | None) -> "LogsView":\n raise NotImplementedError(\n "LogsView cannot be used as an input component."\n )\n\n def postprocess(self, value: List[Log]) -> List[Log]:\n return value\n\n def api_info(self) -> dict[str, Any]:\n return {\n "items": {\n "level": "string",\n "message": "string",\n "timestamp": "number",\n },\n "title": "Logs",\n "type": "array",\n }\n\n def example_payload(self) -> Any:\n return [\n Log(\n "INFO",\n "Hello World",\n datetime.now().isoformat(),\n )\n ]\n\n def example_value(self) -> Any:\n return [\n Log(\n "INFO",\n "Hello World",\n datetime.now().isoformat(),\n )\n ]\n\n @classmethod\n def run_process(\n cls,\n command: List[str],\n date_format: str = "%Y-%m-%d %H:%M:%S",\n ) -> Generator[List[Log], None, None]:\n process = subprocess.Popen(\n command,\n stdout=subprocess.PIPE,\n stderr=subprocess.STDOUT,\n text=True,\n )\n\n if process.stdout is None:\n raise ValueError("stdout is None")\n\n logs = []\n\n def _log(level: str, message: str):\n log = Log(\n level=level,\n message=message,\n timestamp=datetime.now().strftime(\n date_format\n ),\n )\n logs.append(log)\n return logs\n\n _log("INFO", f"Running {\' \'.join(command)}")\n for line in process.stdout:\n yield _log("INFO", line.strip())\n\n # TODO: what if task is cancelled but process is still running?\n\n process.stdout.close()\n return_code = process.wait()\n if return_code:\n yield _log(\n "ERROR",\n f"Process exited with code {return_code}",\n )\n else:\n yield _log(\n "INFO", "Process exited successfully"\n )\n\n @classmethod\n def run_thread(\n cls,\n fn: Callable,\n log_level: int = logging.INFO,\n logger_name: str | None = None,\n date_format: str = "%Y-%m-%d %H:%M:%S",\n **kwargs,\n ) -> Generator[List[Log], None, None]:\n logs = [\n Log(\n level="INFO",\n message=f"Running {fn.__name__}({\', \'.join(f\'{k}={v}\' for k, v in kwargs.items())})",\n timestamp=datetime.now().strftime(\n date_format\n ),\n )\n ]\n yield logs\n\n thread = Thread(\n target=non_failing_fn(fn), kwargs=kwargs\n )\n\n def _log(record: logging.LogRecord) -> bool:\n if record.thread != thread.ident:\n return False # Skip if not from the thread\n if logger_name and not record.name.startswith(\n logger_name\n ):\n return False # Skip if not from the logger\n if record.levelno < log_level:\n return False # Skip if too verbose\n log = Log(\n level=record.levelname,\n message=record.getMessage(),\n timestamp=datetime.fromtimestamp(\n record.created\n ).strftime(date_format),\n )\n logs.append(log)\n return True\n\n with capture_logging(log_level) as log_queue:\n thread.start()\n\n # Loop to capture and yield logs from the thread\n while thread.is_alive():\n while True:\n try:\n if _log(log_queue.get_nowait()):\n yield logs\n except queue.Empty:\n break\n thread.join(\n timeout=0.1\n ) # adjust the timeout as needed\n\n # After the thread completes, yield any remaining logs\n while True:\n try:\n if _log(log_queue.get_nowait()):\n yield logs\n except queue.Empty:\n break\n\n logs.append(\n Log(\n level="INFO",\n message="Thread completed successfully",\n timestamp=datetime.now().strftime(\n date_format\n ),\n )\n )'}}, 'user_fn_refs': {'LogsView': ['Log', 'LogsView']}}}
|
7 |
|
@@ -122,7 +124,7 @@ from gradio_logsview import LogsView
|
|
122 |
def fn_process():
|
123 |
# Run a process and capture all logs from the process
|
124 |
yield from LogsView.run_process(
|
125 |
-
cmd=[mergekit-yaml", "config.yaml", "merge", "--copy-", "--cuda", "--low-cpu-memory"]
|
126 |
)
|
127 |
|
128 |
with gr.Blocks() as demo:
|
|
|
1 |
|
2 |
+
import os
|
3 |
+
|
4 |
import gradio as gr
|
5 |
+
|
6 |
from app import demo as app
|
|
|
7 |
|
8 |
_docs = {'LogsView': {'description': 'Creates a component to visualize logs from a subprocess in real-time.', 'members': {'__init__': {'value': {'type': 'str | Callable | tuple[str] | None', 'default': 'None', 'description': '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.'}, 'every': {'type': 'float | None', 'default': 'None', 'description': "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."}, 'lines': {'type': 'int', 'default': '5', 'description': None}, 'label': {'type': 'str | None', 'default': 'None', 'description': '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.'}, 'show_label': {'type': 'bool | None', 'default': 'None', 'description': 'if True, will display label.'}, 'container': {'type': 'bool', 'default': 'True', 'description': 'If True, will place the component in a container - providing some extra padding around the border.'}, 'scale': {'type': 'int | None', 'default': 'None', 'description': '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.'}, 'min_width': {'type': 'int', 'default': '160', 'description': '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.'}, 'visible': {'type': 'bool', 'default': 'True', 'description': 'If False, component will be hidden.'}, 'elem_id': {'type': 'str | None', 'default': 'None', 'description': 'An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'elem_classes': {'type': 'list[str] | str | None', 'default': 'None', 'description': '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.'}, 'render': {'type': 'bool', 'default': 'True', 'description': '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.'}}, 'postprocess': {'value': {'type': 'list[Log]', 'description': 'Expects a list of `Log` logs.'}}, 'preprocess': {'return': {'type': 'LogsView', 'description': 'Passes the code entered as a `str`.'}, 'value': None}}, 'events': {'change': {'type': None, 'default': None, 'description': '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': {'type': None, 'default': None, 'description': 'This listener is triggered when the user changes the value of the LogsView.'}, 'focus': {'type': None, 'default': None, 'description': 'This listener is triggered when the LogsView is focused.'}, 'blur': {'type': None, 'default': None, 'description': 'This listener is triggered when the LogsView is unfocused/blurred.'}}}, '__meta__': {'additional_interfaces': {'Log': {'source': '@dataclass\nclass Log:\n level: Literal[\n "INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"\n ]\n message: str\n timestamp: str'}, 'LogsView': {'source': 'class LogsView(Component):\n EVENTS = [\n Events.change,\n Events.input,\n Events.focus,\n Events.blur,\n ]\n\n def __init__(\n self,\n value: str | Callable | tuple[str] | None = None,\n *,\n every: float | None = None,\n lines: int = 5,\n label: str | None = None,\n show_label: bool | None = None,\n container: bool = True,\n scale: int | None = None,\n min_width: int = 160,\n visible: bool = True,\n elem_id: str | None = None,\n elem_classes: list[str] | str | None = None,\n render: bool = True,\n ):\n self.language = "shell"\n self.lines = lines\n self.interactive = False\n super().__init__(\n label=label,\n every=every,\n show_label=show_label,\n container=container,\n scale=scale,\n min_width=min_width,\n visible=visible,\n elem_id=elem_id,\n elem_classes=elem_classes,\n render=render,\n value=value,\n )\n\n def preprocess(self, payload: str | None) -> "LogsView":\n raise NotImplementedError(\n "LogsView cannot be used as an input component."\n )\n\n def postprocess(self, value: List[Log]) -> List[Log]:\n return value\n\n def api_info(self) -> dict[str, Any]:\n return {\n "items": {\n "level": "string",\n "message": "string",\n "timestamp": "number",\n },\n "title": "Logs",\n "type": "array",\n }\n\n def example_payload(self) -> Any:\n return [\n Log(\n "INFO",\n "Hello World",\n datetime.now().isoformat(),\n )\n ]\n\n def example_value(self) -> Any:\n return [\n Log(\n "INFO",\n "Hello World",\n datetime.now().isoformat(),\n )\n ]\n\n @classmethod\n def run_process(\n cls,\n command: List[str],\n date_format: str = "%Y-%m-%d %H:%M:%S",\n ) -> Generator[List[Log], None, None]:\n process = subprocess.Popen(\n command,\n stdout=subprocess.PIPE,\n stderr=subprocess.STDOUT,\n text=True,\n )\n\n if process.stdout is None:\n raise ValueError("stdout is None")\n\n logs = []\n\n def _log(level: str, message: str):\n log = Log(\n level=level,\n message=message,\n timestamp=datetime.now().strftime(\n date_format\n ),\n )\n logs.append(log)\n return logs\n\n _log("INFO", f"Running {\' \'.join(command)}")\n for line in process.stdout:\n yield _log("INFO", line.strip())\n\n # TODO: what if task is cancelled but process is still running?\n\n process.stdout.close()\n return_code = process.wait()\n if return_code:\n yield _log(\n "ERROR",\n f"Process exited with code {return_code}",\n )\n else:\n yield _log(\n "INFO", "Process exited successfully"\n )\n\n @classmethod\n def run_thread(\n cls,\n fn: Callable,\n log_level: int = logging.INFO,\n logger_name: str | None = None,\n date_format: str = "%Y-%m-%d %H:%M:%S",\n **kwargs,\n ) -> Generator[List[Log], None, None]:\n logs = [\n Log(\n level="INFO",\n message=f"Running {fn.__name__}({\', \'.join(f\'{k}={v}\' for k, v in kwargs.items())})",\n timestamp=datetime.now().strftime(\n date_format\n ),\n )\n ]\n yield logs\n\n thread = Thread(\n target=non_failing_fn(fn), kwargs=kwargs\n )\n\n def _log(record: logging.LogRecord) -> bool:\n if record.thread != thread.ident:\n return False # Skip if not from the thread\n if logger_name and not record.name.startswith(\n logger_name\n ):\n return False # Skip if not from the logger\n if record.levelno < log_level:\n return False # Skip if too verbose\n log = Log(\n level=record.levelname,\n message=record.getMessage(),\n timestamp=datetime.fromtimestamp(\n record.created\n ).strftime(date_format),\n )\n logs.append(log)\n return True\n\n with capture_logging(log_level) as log_queue:\n thread.start()\n\n # Loop to capture and yield logs from the thread\n while thread.is_alive():\n while True:\n try:\n if _log(log_queue.get_nowait()):\n yield logs\n except queue.Empty:\n break\n thread.join(\n timeout=0.1\n ) # adjust the timeout as needed\n\n # After the thread completes, yield any remaining logs\n while True:\n try:\n if _log(log_queue.get_nowait()):\n yield logs\n except queue.Empty:\n break\n\n logs.append(\n Log(\n level="INFO",\n message="Thread completed successfully",\n timestamp=datetime.now().strftime(\n date_format\n ),\n )\n )'}}, 'user_fn_refs': {'LogsView': ['Log', 'LogsView']}}}
|
9 |
|
|
|
124 |
def fn_process():
|
125 |
# Run a process and capture all logs from the process
|
126 |
yield from LogsView.run_process(
|
127 |
+
cmd=["mergekit-yaml", "config.yaml", "merge", "--copy-", "--cuda", "--low-cpu-memory"]
|
128 |
)
|
129 |
|
130 |
with gr.Blocks() as demo:
|
src/pyproject.toml
CHANGED
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
|
|
8 |
|
9 |
[project]
|
10 |
name = "gradio_logsview"
|
11 |
-
version = "0.0.
|
12 |
description = "Visualize logs in your Gradio app"
|
13 |
readme = "README.md"
|
14 |
license = "MIT"
|
|
|
8 |
|
9 |
[project]
|
10 |
name = "gradio_logsview"
|
11 |
+
version = "0.0.3"
|
12 |
description = "Visualize logs in your Gradio app"
|
13 |
readme = "README.md"
|
14 |
license = "MIT"
|