Wauplin HF Staff commited on
Commit
8861b68
·
1 Parent(s): 9cc9603
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
- from gradio_logsview import LogsView
 
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
- yield from LogsView.run_process(["python", "-u", "script.py"])
 
 
30
 
31
 
32
  def fn_process_failing():
33
- yield from LogsView.run_process(["python", "-u", "script.py", "--failing"])
 
 
34
 
35
 
36
  def fn_thread_success():
37
- yield from LogsView.run_thread(random_values, log_level=logging.INFO, failing=False)
 
 
38
 
39
 
40
  def fn_thread_failing():
41
- yield from LogsView.run_thread(random_values, log_level=logging.INFO, failing=True)
 
 
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.1-py3-none-any.whl
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.1-py3-none-any.whl
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
- yield from LogsView.run_thread(my_function, log_level=logging.INFO, arg1="value1")
 
 
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
- yield from LogsView.run_process(
98
- [mergekit-yaml", "config.yaml", "merge", "--copy-", "--cuda", "--low-cpu-memory"]
 
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
- - [ ] stop process if `run_process` gets cancelled (back-end)
114
- - [ ] correctly pass error stacktrace in `run_thread` (back-end)
 
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, Iterable, List, Literal
18
 
19
  from gradio.components.base import Component
20
  from gradio.events import Events
21
 
 
 
22
 
23
  @dataclass
24
  class Log:
25
- level: Literal["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"]
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) -> "LogsView":
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
- return fn(*args, **kwargs)
247
  except Exception as e:
248
- logging.error(f"Error in {fn.__name__}: {e}", stack_info=True)
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
- yield from LogsView.run_process(["python", "-u", "demo/script.py"])
 
 
30
 
31
 
32
  def fn_process_failing():
33
- yield from LogsView.run_process(["python", "-u", "demo/script.py", "--failing"])
 
 
34
 
35
 
36
  def fn_thread_success():
37
- yield from LogsView.run_thread(random_values, log_level=logging.INFO, failing=False)
 
 
38
 
39
 
40
  def fn_thread_failing():
41
- yield from LogsView.run_thread(random_values, log_level=logging.INFO, failing=True)
 
 
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
- yield from LogsView.run_thread(my_function, log_level=logging.INFO, arg1="value1")
 
 
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
- 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:
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.1"
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"