libsrg_log2web 1.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- libsrg_log2web-1.0.3/PKG-INFO +15 -0
- libsrg_log2web-1.0.3/libsrg_log2web/Demo.py +70 -0
- libsrg_log2web-1.0.3/libsrg_log2web/FlaskApp.py +32 -0
- libsrg_log2web-1.0.3/libsrg_log2web/Log2Web.py +277 -0
- libsrg_log2web-1.0.3/libsrg_log2web/__init__.py +0 -0
- libsrg_log2web-1.0.3/libsrg_log2web/static/style.css +8 -0
- libsrg_log2web-1.0.3/libsrg_log2web/templates/status.html +29 -0
- libsrg_log2web-1.0.3/libsrg_log2web.egg-info/PKG-INFO +15 -0
- libsrg_log2web-1.0.3/libsrg_log2web.egg-info/SOURCES.txt +12 -0
- libsrg_log2web-1.0.3/libsrg_log2web.egg-info/dependency_links.txt +1 -0
- libsrg_log2web-1.0.3/libsrg_log2web.egg-info/requires.txt +2 -0
- libsrg_log2web-1.0.3/libsrg_log2web.egg-info/top_level.txt +1 -0
- libsrg_log2web-1.0.3/pyproject.toml +47 -0
- libsrg_log2web-1.0.3/setup.cfg +4 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: libsrg_log2web
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: Multithreded logging to webpage
|
|
5
|
+
Author-email: Steve Goncalo <steven@goncalo.us>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Topic :: Utilities
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Requires-Python: >=3.14
|
|
14
|
+
Requires-Dist: flask>=3.1.2
|
|
15
|
+
Requires-Dist: libsrg>=6.0.4
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from time import sleep
|
|
5
|
+
|
|
6
|
+
from libsrg_log2web import FlaskApp, Log2Web
|
|
7
|
+
from libsrg.LoggingAppBase import LoggingAppBase
|
|
8
|
+
|
|
9
|
+
class Main(LoggingAppBase):
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
fmt = "%(asctime)s %(levelname)-8s %(lineno)4d %(name) 20s.%(funcName)-22s %(threadName)-12s-- %(message)s"
|
|
15
|
+
self.file0 = Path(__file__)
|
|
16
|
+
self.project_path = self.file0.parent
|
|
17
|
+
self.node = platform.node()
|
|
18
|
+
self.node0 = self.node.split(".")[0]
|
|
19
|
+
|
|
20
|
+
logfile_path = Path.home() / "Log2Web.log"
|
|
21
|
+
logfile_path.unlink(missing_ok=True)
|
|
22
|
+
self.logfile_name = str(logfile_path)
|
|
23
|
+
super().__init__(logfile=self.logfile_name, format=fmt)
|
|
24
|
+
|
|
25
|
+
self.bridge= Log2Web.Bridge(self.run, FlaskApp.app, title="Log2Web Demo",
|
|
26
|
+
headertext="Log2Web Demo Threads")
|
|
27
|
+
self.bridge.run()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run(self)->None:
|
|
31
|
+
self.logger.info("callback to application")
|
|
32
|
+
|
|
33
|
+
executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="POOL_")
|
|
34
|
+
futures = [executor.submit(self.app_thread_target,i ) for i in range(10)]
|
|
35
|
+
|
|
36
|
+
# sleep(10)
|
|
37
|
+
executor.shutdown(wait=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
self.logger.debug("Bye")
|
|
41
|
+
# """Raises a signal to main thread which is WebGUI to shut down."""
|
|
42
|
+
# signal.raise_signal(2)
|
|
43
|
+
|
|
44
|
+
def app_thread_target(self,n:int)->None:
|
|
45
|
+
|
|
46
|
+
Log2Web.send_log_op(Log2Web.LogOps.NEW_THREAD)
|
|
47
|
+
self.logger.info(f"app {n} starting")
|
|
48
|
+
for i in range(20):
|
|
49
|
+
match i:
|
|
50
|
+
case 3:
|
|
51
|
+
Log2Web.send_log_op(Log2Web.LogOps.BG_COLOR, 'pink')
|
|
52
|
+
Log2Web.send_log_op(Log2Web.LogOps.FG_COLOR, 'blue')
|
|
53
|
+
case 5:
|
|
54
|
+
Log2Web.send_log_op(Log2Web.LogOps.BG_COLOR, 'black')
|
|
55
|
+
Log2Web.send_log_op(Log2Web.LogOps.FG_COLOR, 'white')
|
|
56
|
+
case 7:
|
|
57
|
+
Log2Web.send_log_op(Log2Web.LogOps.BG_COLOR, 'cyan')
|
|
58
|
+
Log2Web.send_log_op(Log2Web.LogOps.FG_COLOR, 'red')
|
|
59
|
+
|
|
60
|
+
if i == n:
|
|
61
|
+
self.logger.warning(f"app {n=} cycle {i=}")
|
|
62
|
+
else:
|
|
63
|
+
self.logger.info(f"app {n=} cycle {i=}")
|
|
64
|
+
|
|
65
|
+
sleep(1+n/10.)
|
|
66
|
+
|
|
67
|
+
Log2Web.send_log_op(Log2Web.LogOps.THREAD_EXIT)
|
|
68
|
+
|
|
69
|
+
if __name__=="__main__":
|
|
70
|
+
main = Main()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from flask import Flask, render_template
|
|
4
|
+
from libsrg_log2web.Log2Web import LogWatcher, Bridge
|
|
5
|
+
|
|
6
|
+
my_file = __file__
|
|
7
|
+
flask_dir = Path(my_file).parent
|
|
8
|
+
app_dir = flask_dir.parent.parent
|
|
9
|
+
template_dir = flask_dir / "templates"
|
|
10
|
+
static_dir = flask_dir / "static"
|
|
11
|
+
app = Flask(__name__,
|
|
12
|
+
template_folder=template_dir,
|
|
13
|
+
static_folder=static_dir)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.route('/hello')
|
|
17
|
+
def hello_world(): # put the application's code here
|
|
18
|
+
return 'Hello World!'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.route('/')
|
|
22
|
+
def display_status(): # put the application's code here
|
|
23
|
+
watchers = LogWatcher.instance.display()
|
|
24
|
+
return render_template('status.html',
|
|
25
|
+
delay=0.1,
|
|
26
|
+
watchers=LogWatcher.instance.display_watchers,
|
|
27
|
+
title=Bridge.instance.title,
|
|
28
|
+
headertext=Bridge.instance.headertext)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == '__main__':
|
|
32
|
+
app.run()
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import signal
|
|
3
|
+
import threading
|
|
4
|
+
import webbrowser
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from threading import Thread
|
|
7
|
+
from time import sleep, time
|
|
8
|
+
from typing import Callable, Optional
|
|
9
|
+
|
|
10
|
+
from flask import Flask
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
This module does not import the user's Flask application module or the user's Main module.
|
|
14
|
+
The user's Flask application can include the Log2Web module.
|
|
15
|
+
The user's Main module must import (and instantiate) the user's FlaskApp module.
|
|
16
|
+
|
|
17
|
+
The User's Main module must connect the FlaskApp module to the Log2Web module via a Bridge object.
|
|
18
|
+
|
|
19
|
+
As general guidance, attempting to log within the Log2Web module may cause infinite recursion.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
LOGOP = "LOGOP %s"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LogOps(Enum):
|
|
26
|
+
NEW_THREAD = 1
|
|
27
|
+
THREAD_EXIT = 2
|
|
28
|
+
FG_COLOR = 3
|
|
29
|
+
BG_COLOR = 4
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def send_log_op(op: LogOps, *args):
|
|
33
|
+
fmt = LOGOP
|
|
34
|
+
fmt += " %s" * len(args)
|
|
35
|
+
# print(f"send op {op=} {args=} {fmt=}")
|
|
36
|
+
logging.getLogger().info(fmt, op, *args)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
ThreadLogWatcher is an object that receives log messages from a given thread and retains information for display.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
status_color = {
|
|
44
|
+
logging.DEBUG: 'green',
|
|
45
|
+
logging.INFO: 'blue',
|
|
46
|
+
logging.WARNING: 'orange',
|
|
47
|
+
logging.ERROR: 'red',
|
|
48
|
+
logging.CRITICAL: 'magenta'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ThreadLogWatcher:
|
|
53
|
+
def __init__(self, thread_id: int):
|
|
54
|
+
self.thread_id: int = thread_id
|
|
55
|
+
self.live = True
|
|
56
|
+
self.time_of_death = None
|
|
57
|
+
self.record: Optional[logging.LogRecord] = logging.LogRecord(
|
|
58
|
+
name="No data yet",
|
|
59
|
+
level=logging.DEBUG,
|
|
60
|
+
pathname="No data yet",
|
|
61
|
+
lineno=0,
|
|
62
|
+
msg="No data yet",
|
|
63
|
+
args=(),
|
|
64
|
+
exc_info=None
|
|
65
|
+
)
|
|
66
|
+
self.fg = 'green'
|
|
67
|
+
self.bg = 'white'
|
|
68
|
+
self.worst = logging.DEBUG
|
|
69
|
+
self.latest = logging.DEBUG
|
|
70
|
+
self.tname = "?"
|
|
71
|
+
self.msg="No message yet"
|
|
72
|
+
|
|
73
|
+
def process_log_record(self, record: logging.LogRecord):
|
|
74
|
+
# print(f"{self.thread_id} {record.getMessage()} {record.message=} {type(record.message)=} {record.args=}")
|
|
75
|
+
self.record = record
|
|
76
|
+
self.msg = record.msg
|
|
77
|
+
self.worst = max(self.worst, record.levelno)
|
|
78
|
+
self.latest = record.levelno
|
|
79
|
+
self.tname = record.threadName
|
|
80
|
+
|
|
81
|
+
def process_log_op(self, op: LogOps,record: logging.LogRecord, *args):
|
|
82
|
+
# print(f"PROC OP {self.thread_id} {op=} {type(op)=} {args=}")
|
|
83
|
+
self.record = record
|
|
84
|
+
match op:
|
|
85
|
+
case LogOps.FG_COLOR:
|
|
86
|
+
self.fg = args[1]
|
|
87
|
+
case LogOps.BG_COLOR:
|
|
88
|
+
self.bg = args[1]
|
|
89
|
+
case LogOps.NEW_THREAD:
|
|
90
|
+
pass # no action, but expected
|
|
91
|
+
case _:
|
|
92
|
+
# print(f"Unknown OP {op=} {args=}")
|
|
93
|
+
pass
|
|
94
|
+
# print(f"PROC OP results {self.thread_id} {self.fg=} {self.bg=}")
|
|
95
|
+
|
|
96
|
+
# noinspection PyUnusedLocal
|
|
97
|
+
def terminated(self, op: LogOps, *args):
|
|
98
|
+
# print(f"TERMINATED {self.thread_id} OP {op.name=} {type(op)=}")
|
|
99
|
+
self.live = False
|
|
100
|
+
self.time_of_death = time()
|
|
101
|
+
|
|
102
|
+
def time_dead(self, now: float) -> float | None:
|
|
103
|
+
if self.live:
|
|
104
|
+
return 0
|
|
105
|
+
return now - self.time_of_death
|
|
106
|
+
|
|
107
|
+
def display_row_start(self) -> str:
|
|
108
|
+
"""HTML lead in"""
|
|
109
|
+
stat_w = status_color.get(self.worst, 'violet')
|
|
110
|
+
stat_l = status_color.get(self.latest, 'violet')
|
|
111
|
+
live_fg = 'green' if self.live else 'black'
|
|
112
|
+
live_bg = 'white' if self.live else 'lightgrey'
|
|
113
|
+
out = [
|
|
114
|
+
f"<td><span style='background-color:{live_bg};color:{live_fg};'>",
|
|
115
|
+
self.tname if self.live else f"<del>{self.tname}</del>",
|
|
116
|
+
f"</span></td>",
|
|
117
|
+
|
|
118
|
+
f"<td><span style='background-color:white;color:{stat_w};'>",
|
|
119
|
+
logging.getLevelName(self.worst),
|
|
120
|
+
f"</span></td>",
|
|
121
|
+
|
|
122
|
+
f"<td><div style='background-color:white;color:{stat_l};'>",
|
|
123
|
+
logging.getLevelName(self.latest),
|
|
124
|
+
f"</div></td>",
|
|
125
|
+
|
|
126
|
+
f"<td><div style='background-color:{self.bg};color:{self.fg};'>",
|
|
127
|
+
# f"{stat_w=} {stat_l=}<br>"
|
|
128
|
+
]
|
|
129
|
+
return '\n'.join(out)
|
|
130
|
+
|
|
131
|
+
def display_row_body(self) -> str:
|
|
132
|
+
return f"{self.msg} {self.record.filename} {self.record.lineno}"
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def display_row_end() -> str:
|
|
136
|
+
return "</div></td>"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
LogWatcher is a logging handler that can be attached to the logging subsystem to observe
|
|
141
|
+
log messages generated by all threads of the user's application.
|
|
142
|
+
It manages a collection of ThreadLogWatcher objects, one for each thread.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class LogWatcher(logging.Handler):
|
|
147
|
+
instance: LogWatcher = None
|
|
148
|
+
|
|
149
|
+
def __init__(self, *args, **kwargs):
|
|
150
|
+
"""
|
|
151
|
+
Constructor.
|
|
152
|
+
:param args: positional arguments passed to logging.Handler
|
|
153
|
+
:param kwargs: keyword arguments passed to logging.Handler
|
|
154
|
+
"""
|
|
155
|
+
super().__init__(*args, **kwargs)
|
|
156
|
+
self.kwargs = kwargs
|
|
157
|
+
logging.getLogger().addHandler(self)
|
|
158
|
+
LogWatcher.instance = self
|
|
159
|
+
self.thread_log_watchers: dict[int, ThreadLogWatcher] = {}
|
|
160
|
+
self.display_watchers: list[ThreadLogWatcher] = []
|
|
161
|
+
self.data_event = threading.Event()
|
|
162
|
+
|
|
163
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Emit a record. (record is recorded to queue)
|
|
166
|
+
:param record:
|
|
167
|
+
:return:
|
|
168
|
+
"""
|
|
169
|
+
if record.name == 'werkzeug':
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
self.data_event.set()
|
|
173
|
+
|
|
174
|
+
thread = record.thread
|
|
175
|
+
# tname = f"{record.funcName=} {record.threadName=} {record.name=} {record.filename=}"
|
|
176
|
+
watcher = self.thread_log_watchers.get(thread, None)
|
|
177
|
+
oflag = record.msg.startswith(LOGOP)
|
|
178
|
+
op = record.args[0] if (len(record.args) > 0) and oflag else None # and (record.message == LOGOP)
|
|
179
|
+
|
|
180
|
+
# print(f"EMIT {thread} {op=} {type(op)=} {record.getMessage()} {watcher=} {tname=} {oflag=}")
|
|
181
|
+
# On termination of a thread, remove the old watcher from the dictionary (but not the list of display watchers).
|
|
182
|
+
if watcher is not None and (op in [LogOps.NEW_THREAD, LogOps.THREAD_EXIT]):
|
|
183
|
+
self.thread_log_watchers.pop(thread)
|
|
184
|
+
watcher.terminated(op)
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
# if there is no watcher for the thread, create one and add to dictionary and list of display watchers.
|
|
188
|
+
if watcher is None:
|
|
189
|
+
watcher = ThreadLogWatcher(thread)
|
|
190
|
+
self.thread_log_watchers[thread] = watcher
|
|
191
|
+
self.display_watchers.append(watcher)
|
|
192
|
+
|
|
193
|
+
if op is None:
|
|
194
|
+
watcher.process_log_record(record)
|
|
195
|
+
else:
|
|
196
|
+
watcher.process_log_op(op, record,*record.args)
|
|
197
|
+
|
|
198
|
+
def display(self) -> list[ThreadLogWatcher]:
|
|
199
|
+
# wait up to 1 second for data to arrive.
|
|
200
|
+
self.data_event.wait(timeout=1)
|
|
201
|
+
self.data_event.clear()
|
|
202
|
+
|
|
203
|
+
now = time()
|
|
204
|
+
dead_watchers = [watcher for watcher in self.display_watchers if watcher.time_dead(now) > 5]
|
|
205
|
+
for watcher in dead_watchers:
|
|
206
|
+
self.display_watchers.remove(watcher)
|
|
207
|
+
|
|
208
|
+
return self.display_watchers.copy()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
The bridge object connects the user application and flask application.
|
|
213
|
+
All three are expected to be singletons.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class Bridge:
|
|
218
|
+
instance: Bridge = None
|
|
219
|
+
|
|
220
|
+
def __init__(self,
|
|
221
|
+
user_callable: Callable[[], None],
|
|
222
|
+
flask_app: Flask,
|
|
223
|
+
browser: bool = True,
|
|
224
|
+
title: str = "Log2Web",
|
|
225
|
+
headertext: str = "Data from threads",
|
|
226
|
+
) -> None:
|
|
227
|
+
self.user_callable = user_callable
|
|
228
|
+
self.flask_app = flask_app
|
|
229
|
+
self.user_thread = Thread(target=self._run_user_in_thread, name="USER_MAIN")
|
|
230
|
+
Bridge.instance = self
|
|
231
|
+
self.log_watcher = LogWatcher()
|
|
232
|
+
self.browser = browser
|
|
233
|
+
|
|
234
|
+
self.title = title
|
|
235
|
+
self.headertext = headertext
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
The run method swaps control of the main thread to the flask application.
|
|
239
|
+
The user application starts in the main thread. Flask must run in the main thread.
|
|
240
|
+
A second thread is created to run the user_callable provided in the constructor.
|
|
241
|
+
This method does not return until the flask application exits.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
def run(self):
|
|
245
|
+
self.user_thread.start()
|
|
246
|
+
self.flask_app.run(host='0.0.0.0', port=8080)
|
|
247
|
+
|
|
248
|
+
def _run_user_in_thread(self):
|
|
249
|
+
"""
|
|
250
|
+
Runs the user callable in a separate thread.
|
|
251
|
+
On completion, raises a signal to the main thread which is WebGUI to shut down.
|
|
252
|
+
:return:
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
if self.browser:
|
|
256
|
+
webbrowser.open("http://localhost:8080")
|
|
257
|
+
logging.getLogger().info("starting user callable")
|
|
258
|
+
self.user_callable()
|
|
259
|
+
logging.getLogger().info("user callable completed")
|
|
260
|
+
sleep(4)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logging.getLogger().exception(e)
|
|
263
|
+
finally:
|
|
264
|
+
logging.getLogger().info("GUI Shutdown")
|
|
265
|
+
sleep(1)
|
|
266
|
+
signal.raise_signal(2)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class LoggerGUIProxy:
|
|
270
|
+
@staticmethod
|
|
271
|
+
def gui_configure(background='white', foreground='black'):
|
|
272
|
+
send_log_op(LogOps.BG_COLOR, background)
|
|
273
|
+
send_log_op(LogOps.FG_COLOR, foreground)
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def gui_new_line():
|
|
277
|
+
send_log_op(LogOps.NEW_THREAD)
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta http-equiv="refresh" content="{{ delay }}">
|
|
6
|
+
<link href="../static/style.css" rel="stylesheet" type="text/css">
|
|
7
|
+
<title>{{title}}</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
{{headertext}}
|
|
11
|
+
<table class="status">
|
|
12
|
+
<tr class="header">
|
|
13
|
+
<th>Thread</th>
|
|
14
|
+
<th>Net Status</th>
|
|
15
|
+
<th>Latest Status</th>
|
|
16
|
+
<th>Message</th>
|
|
17
|
+
</tr>
|
|
18
|
+
{% for watcher in watchers %}
|
|
19
|
+
<tr>
|
|
20
|
+
{{ watcher.display_row_start()|safe }}
|
|
21
|
+
{{ watcher.display_row_body() }}
|
|
22
|
+
{{ watcher.display_row_end()|safe }}
|
|
23
|
+
|
|
24
|
+
</tr>
|
|
25
|
+
{% endfor %}
|
|
26
|
+
</table>
|
|
27
|
+
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: libsrg_log2web
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: Multithreded logging to webpage
|
|
5
|
+
Author-email: Steve Goncalo <steven@goncalo.us>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Topic :: Utilities
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Requires-Python: >=3.14
|
|
14
|
+
Requires-Dist: flask>=3.1.2
|
|
15
|
+
Requires-Dist: libsrg>=6.0.4
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
libsrg_log2web/Demo.py
|
|
3
|
+
libsrg_log2web/FlaskApp.py
|
|
4
|
+
libsrg_log2web/Log2Web.py
|
|
5
|
+
libsrg_log2web/__init__.py
|
|
6
|
+
libsrg_log2web.egg-info/PKG-INFO
|
|
7
|
+
libsrg_log2web.egg-info/SOURCES.txt
|
|
8
|
+
libsrg_log2web.egg-info/dependency_links.txt
|
|
9
|
+
libsrg_log2web.egg-info/requires.txt
|
|
10
|
+
libsrg_log2web.egg-info/top_level.txt
|
|
11
|
+
libsrg_log2web/static/style.css
|
|
12
|
+
libsrg_log2web/templates/status.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
libsrg_log2web
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "libsrg_log2web"
|
|
3
|
+
version = "1.0.3"
|
|
4
|
+
description = "Multithreded logging to webpage"
|
|
5
|
+
requires-python = ">=3.14"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"flask>=3.1.2",
|
|
8
|
+
"libsrg>=6.0.4",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
authors = [{name="Steve Goncalo", email="steven@goncalo.us"}]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3.14",
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
# "License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: POSIX :: Linux",
|
|
18
|
+
"Natural Language :: English",
|
|
19
|
+
"Topic :: Utilities",
|
|
20
|
+
"Intended Audience :: Developers"
|
|
21
|
+
]
|
|
22
|
+
#license = {text="MIT"}
|
|
23
|
+
license = "MIT"
|
|
24
|
+
|
|
25
|
+
[tool.uv]
|
|
26
|
+
"link-mode"="symlink"
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = [
|
|
30
|
+
"build>=1.2.2.post1",
|
|
31
|
+
"twine>=6.1.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["setuptools", "wheel"]
|
|
38
|
+
build-backend = "setuptools.build_meta"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools]
|
|
41
|
+
packages = ["libsrg_log2web"]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.package-data]
|
|
44
|
+
# Include all .txt and .rst files in the 'hello' package
|
|
45
|
+
#"hello" = ["*.txt", "*.rst"]
|
|
46
|
+
# Include all .png files in all packages
|
|
47
|
+
"libsrg_log2web" = ["static/*","templates/*"]
|