thread-order 1.3.0__tar.gz → 1.3.2__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.
- {thread_order-1.3.0/thread_order.egg-info → thread_order-1.3.2}/PKG-INFO +16 -18
- {thread_order-1.3.0 → thread_order-1.3.2}/README.md +15 -17
- {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/__init__.py +5 -1
- {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/cli/app.py +15 -13
- thread_order-1.3.2/thread_order/logger.py +162 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/scheduler.py +3 -2
- {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/ui/app.py +339 -109
- {thread_order-1.3.0 → thread_order-1.3.2/thread_order.egg-info}/PKG-INFO +16 -18
- thread_order-1.3.0/thread_order/logger.py +0 -147
- {thread_order-1.3.0 → thread_order-1.3.2}/LICENSE +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/pyproject.toml +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/setup.cfg +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/tests/test_graph.py +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/tests/test_init.py +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/tests/test_scheduler.py +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/cli/__init__.py +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/graph.py +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/graph_summary.py +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/timer.py +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/ui/__init__.py +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/thread_order.egg-info/SOURCES.txt +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/thread_order.egg-info/dependency_links.txt +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/thread_order.egg-info/entry_points.txt +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/thread_order.egg-info/requires.txt +0 -0
- {thread_order-1.3.0 → thread_order-1.3.2}/thread_order.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thread-order
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: A lightweight framework for running functions concurrently across multiple threads while maintaining a defined execution order.
|
|
5
5
|
Author-email: Emilio Reyes <soda480@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -218,29 +218,26 @@ These appear in `initial_state` and can be processed in your module’s `setup_s
|
|
|
218
218
|
|
|
219
219
|
This allows your module to compute initial state based on CLI parameters.
|
|
220
220
|
|
|
221
|
-
##
|
|
221
|
+
## Optional Highlights
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
`tdrun` CLI supports customizable log highlighting. In addition to the built-in highlight rules (e.g. PASSED, FAILED, SKIPPED), you may provide additional highlight patterns to emphasize important output.
|
|
224
|
+
|
|
225
|
+
If a module defines an `add_logging_highlights()` function, it should return a list of (compiled_regex, color) pairs:
|
|
224
226
|
|
|
225
227
|
```Python
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
)
|
|
228
|
+
import re
|
|
229
|
+
from colorama import Fore
|
|
230
|
+
|
|
231
|
+
def add_logging_highlights():
|
|
232
|
+
return [
|
|
233
|
+
(re.compile(r'\bCRITICAL\b'), Fore.RED),
|
|
234
|
+
(re.compile(r'Environment:\s+\w+'), Fore.CYAN),
|
|
235
|
+
]
|
|
232
236
|
```
|
|
233
237
|
|
|
234
|
-
|
|
238
|
+
When logging is enabled, these rules are appended to the default highlights and applied to log output during execution.
|
|
235
239
|
|
|
236
|
-
|
|
237
|
-
setup_logging(
|
|
238
|
-
args.workers, # effective workers
|
|
239
|
-
verbose=args.verbose,
|
|
240
|
-
add_stream_handler=not args.progress and not args.viewer,
|
|
241
|
-
add_file_handler=args.log,
|
|
242
|
-
)
|
|
243
|
-
```
|
|
240
|
+
This allows modules or callers to visually emphasize important state, metadata, or runtime signals without modifying the core logging configuration.
|
|
244
241
|
|
|
245
242
|
### DAG Inspection
|
|
246
243
|
|
|
@@ -311,6 +308,7 @@ class Scheduler(
|
|
|
311
308
|
setup_logging=False, # enable built-in logging config
|
|
312
309
|
add_stream_handler=True, # attach stream handler to logger
|
|
313
310
|
add_file_handler=True, # attach file handlers for each thread to logger
|
|
311
|
+
highlights=None, # Optional list of highlight rules applied to log output
|
|
314
312
|
verbose=False, # enable extra debug logging on stream handler
|
|
315
313
|
skip_dependents=False # skip dependents when prerequisites fail
|
|
316
314
|
)
|
|
@@ -176,29 +176,26 @@ These appear in `initial_state` and can be processed in your module’s `setup_s
|
|
|
176
176
|
|
|
177
177
|
This allows your module to compute initial state based on CLI parameters.
|
|
178
178
|
|
|
179
|
-
##
|
|
179
|
+
## Optional Highlights
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
`tdrun` CLI supports customizable log highlighting. In addition to the built-in highlight rules (e.g. PASSED, FAILED, SKIPPED), you may provide additional highlight patterns to emphasize important output.
|
|
182
|
+
|
|
183
|
+
If a module defines an `add_logging_highlights()` function, it should return a list of (compiled_regex, color) pairs:
|
|
182
184
|
|
|
183
185
|
```Python
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
)
|
|
186
|
+
import re
|
|
187
|
+
from colorama import Fore
|
|
188
|
+
|
|
189
|
+
def add_logging_highlights():
|
|
190
|
+
return [
|
|
191
|
+
(re.compile(r'\bCRITICAL\b'), Fore.RED),
|
|
192
|
+
(re.compile(r'Environment:\s+\w+'), Fore.CYAN),
|
|
193
|
+
]
|
|
190
194
|
```
|
|
191
195
|
|
|
192
|
-
|
|
196
|
+
When logging is enabled, these rules are appended to the default highlights and applied to log output during execution.
|
|
193
197
|
|
|
194
|
-
|
|
195
|
-
setup_logging(
|
|
196
|
-
args.workers, # effective workers
|
|
197
|
-
verbose=args.verbose,
|
|
198
|
-
add_stream_handler=not args.progress and not args.viewer,
|
|
199
|
-
add_file_handler=args.log,
|
|
200
|
-
)
|
|
201
|
-
```
|
|
198
|
+
This allows modules or callers to visually emphasize important state, metadata, or runtime signals without modifying the core logging configuration.
|
|
202
199
|
|
|
203
200
|
### DAG Inspection
|
|
204
201
|
|
|
@@ -269,6 +266,7 @@ class Scheduler(
|
|
|
269
266
|
setup_logging=False, # enable built-in logging config
|
|
270
267
|
add_stream_handler=True, # attach stream handler to logger
|
|
271
268
|
add_file_handler=True, # attach file handlers for each thread to logger
|
|
269
|
+
highlights=None, # Optional list of highlight rules applied to log output
|
|
272
270
|
verbose=False, # enable extra debug logging on stream handler
|
|
273
271
|
skip_dependents=False # skip dependents when prerequisites fail
|
|
274
272
|
)
|
|
@@ -12,6 +12,7 @@ __all__ = [
|
|
|
12
12
|
'default_workers',
|
|
13
13
|
'load_and_collect_functions',
|
|
14
14
|
'register_functions',
|
|
15
|
+
'validate_highlights',
|
|
15
16
|
'__version__']
|
|
16
17
|
|
|
17
18
|
def __getattr__(name):
|
|
@@ -42,6 +43,9 @@ def __getattr__(name):
|
|
|
42
43
|
if name == 'register_functions':
|
|
43
44
|
from .scheduler import register_functions
|
|
44
45
|
return register_functions
|
|
46
|
+
if name == 'validate_highlights':
|
|
47
|
+
from .logger import validate_highlights
|
|
48
|
+
return validate_highlights
|
|
45
49
|
# If the requested attribute isn't one of the known top-level symbols,
|
|
46
50
|
# try to lazily import a submodule (e.g. `thread_order.scheduler`) so
|
|
47
51
|
# attribute lookups such as those used by mocking/patching succeed.
|
|
@@ -53,7 +57,7 @@ def __getattr__(name):
|
|
|
53
57
|
try:
|
|
54
58
|
__version__ = _metadata.version(__name__)
|
|
55
59
|
except _metadata.PackageNotFoundError:
|
|
56
|
-
__version__ = '1.3.
|
|
60
|
+
__version__ = '1.3.2'
|
|
57
61
|
|
|
58
62
|
if getenv('DEV'):
|
|
59
63
|
__version__ = f'{__version__}+dev'
|
|
@@ -4,7 +4,12 @@ import json
|
|
|
4
4
|
from contextlib import nullcontext
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from thread_order import (
|
|
7
|
-
Scheduler,
|
|
7
|
+
Scheduler,
|
|
8
|
+
ThreadProxyLogger,
|
|
9
|
+
default_workers,
|
|
10
|
+
load_and_collect_functions,
|
|
11
|
+
register_functions,
|
|
12
|
+
validate_highlights)
|
|
8
13
|
from thread_order.graph_summary import format_graph_summary
|
|
9
14
|
try:
|
|
10
15
|
from progress1bar import ProgressBar
|
|
@@ -177,19 +182,16 @@ def _build_scheduler_kwargs(args, initial_state, clear_results_on_start, module)
|
|
|
177
182
|
'skip_dependents': args.skip_deps
|
|
178
183
|
}
|
|
179
184
|
# prefer module-provided logging hook if available
|
|
180
|
-
|
|
181
|
-
if callable(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
add_stream_handler=not args.progress and not args.viewer,
|
|
186
|
-
add_file_handler=args.log)
|
|
187
|
-
else:
|
|
188
|
-
scheduler_kwargs['setup_logging'] = True
|
|
189
|
-
scheduler_kwargs['verbose'] = args.verbose
|
|
190
|
-
scheduler_kwargs['add_stream_handler'] = not args.progress and not args.viewer
|
|
191
|
-
scheduler_kwargs['add_file_handler'] = args.log
|
|
185
|
+
add_logging_highlights_function = getattr(module, 'add_logging_highlights', None)
|
|
186
|
+
if callable(add_logging_highlights_function):
|
|
187
|
+
highlights = add_logging_highlights_function()
|
|
188
|
+
validated_highlights = validate_highlights(highlights)
|
|
189
|
+
scheduler_kwargs['highlights'] = validated_highlights
|
|
192
190
|
|
|
191
|
+
scheduler_kwargs['setup_logging'] = True
|
|
192
|
+
scheduler_kwargs['verbose'] = args.verbose
|
|
193
|
+
scheduler_kwargs['add_stream_handler'] = not args.progress and not args.viewer
|
|
194
|
+
scheduler_kwargs['add_file_handler'] = args.log
|
|
193
195
|
return scheduler_kwargs
|
|
194
196
|
|
|
195
197
|
def validate_args(args):
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
5
|
+
import logging
|
|
6
|
+
from colorama import init
|
|
7
|
+
from colorama import Fore, Style
|
|
8
|
+
|
|
9
|
+
def validate_highlights(highlights):
|
|
10
|
+
if not isinstance(highlights, (list, tuple)):
|
|
11
|
+
raise TypeError('highlights must be a list/tuple of (regex, color) pairs')
|
|
12
|
+
for item in highlights:
|
|
13
|
+
if not isinstance(item, (list, tuple)) or len(item) != 2:
|
|
14
|
+
raise TypeError('each highlight must be a (regex, color) pair')
|
|
15
|
+
pattern, _ = item
|
|
16
|
+
if not isinstance(pattern, re.Pattern):
|
|
17
|
+
raise TypeError('highlight pattern must be a compiled regex (re.Pattern)')
|
|
18
|
+
return list(highlights)
|
|
19
|
+
|
|
20
|
+
class ColoredFormatter(logging.Formatter):
|
|
21
|
+
LEVEL_COLORS = {
|
|
22
|
+
logging.DEBUG: Style.BRIGHT + Fore.CYAN,
|
|
23
|
+
logging.INFO: Style.BRIGHT + Fore.BLUE,
|
|
24
|
+
logging.WARNING: Style.BRIGHT + Fore.YELLOW,
|
|
25
|
+
logging.ERROR: Style.BRIGHT + Fore.RED,
|
|
26
|
+
logging.CRITICAL: Style.BRIGHT + Fore.RED,
|
|
27
|
+
}
|
|
28
|
+
DEFAULT_HIGHLIGHTS = [
|
|
29
|
+
(re.compile(r'\bPASSED\b', re.IGNORECASE), Fore.GREEN),
|
|
30
|
+
(re.compile(r'\bFAILED\b', re.IGNORECASE), Fore.RED),
|
|
31
|
+
(re.compile(r'\bSKIPPED\b', re.IGNORECASE), Fore.YELLOW),
|
|
32
|
+
(re.compile(r'Scheduler::State:\s*(\{.*?^})', re.DOTALL | re.MULTILINE), Fore.MAGENTA)
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
def __init__(self, workers, *args, **kwargs):
|
|
36
|
+
highlights = kwargs.pop('highlights', [])
|
|
37
|
+
if highlights is not None:
|
|
38
|
+
validated = validate_highlights(highlights)
|
|
39
|
+
self.highlights = [*self.DEFAULT_HIGHLIGHTS, *validated]
|
|
40
|
+
else:
|
|
41
|
+
self.highlights = list(self.DEFAULT_HIGHLIGHTS)
|
|
42
|
+
self.verbose = kwargs.pop('verbose', False)
|
|
43
|
+
super().__init__(*args, **kwargs)
|
|
44
|
+
|
|
45
|
+
def _apply_highlights(self, message):
|
|
46
|
+
out = message
|
|
47
|
+
for pattern, color in self.highlights:
|
|
48
|
+
def replace(m):
|
|
49
|
+
text = m.group(0)
|
|
50
|
+
return f'{color}{text}{Style.RESET_ALL}{Fore.WHITE}'
|
|
51
|
+
out = pattern.sub(replace, out)
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
def format(self, record):
|
|
55
|
+
timestamp = self.formatTime(record, self.datefmt)
|
|
56
|
+
level_color = self.LEVEL_COLORS.get(record.levelno, Fore.WHITE)
|
|
57
|
+
|
|
58
|
+
raw_msg = record.getMessage()
|
|
59
|
+
colored_msg = self._apply_highlights(raw_msg)
|
|
60
|
+
|
|
61
|
+
# only log thread name if it's not MainThread
|
|
62
|
+
# to avoid cluttering the logs with 'MainThread' entries
|
|
63
|
+
if record.threadName == 'MainThread':
|
|
64
|
+
thread_name = ''
|
|
65
|
+
else:
|
|
66
|
+
thread_name = f'[{record.threadName}] '
|
|
67
|
+
|
|
68
|
+
if self.verbose:
|
|
69
|
+
msg = (
|
|
70
|
+
f"{Style.DIM}{timestamp}{Style.RESET_ALL} "
|
|
71
|
+
f"{level_color}{record.levelname:<5}{Style.RESET_ALL} "
|
|
72
|
+
f"{thread_name}"
|
|
73
|
+
f"{Fore.WHITE}{record.funcName}: {colored_msg}{Style.RESET_ALL}"
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
msg = (
|
|
77
|
+
f"{Style.DIM}{timestamp}{Style.RESET_ALL} "
|
|
78
|
+
f"{thread_name}"
|
|
79
|
+
f"{colored_msg}{Style.RESET_ALL}"
|
|
80
|
+
)
|
|
81
|
+
if record.exc_info:
|
|
82
|
+
msg += '\n' + self.formatException(record.exc_info)
|
|
83
|
+
|
|
84
|
+
return msg
|
|
85
|
+
|
|
86
|
+
def _validate_highlights(self, highlights):
|
|
87
|
+
if not isinstance(highlights, (list, tuple)):
|
|
88
|
+
raise TypeError('highlights must be a list/tuple of (regex, color) pairs')
|
|
89
|
+
for item in highlights:
|
|
90
|
+
if not isinstance(item, (list, tuple)) or len(item) != 2:
|
|
91
|
+
raise TypeError('each highlight must be a (regex, color) pair')
|
|
92
|
+
pattern, _ = item
|
|
93
|
+
if not isinstance(pattern, re.Pattern):
|
|
94
|
+
raise TypeError('highlight pattern must be a compiled regex (re.Pattern)')
|
|
95
|
+
return list(highlights)
|
|
96
|
+
|
|
97
|
+
class MainThreadAwareFormatter(logging.Formatter):
|
|
98
|
+
|
|
99
|
+
def __init__(self, main_fmt, thread_fmt, workers, thread_prefix='thread_', *args, **kwargs):
|
|
100
|
+
super().__init__(fmt=main_fmt, *args, **kwargs)
|
|
101
|
+
self.main_fmt = main_fmt
|
|
102
|
+
self.thread_fmt = thread_fmt
|
|
103
|
+
self.thread_prefix = thread_prefix
|
|
104
|
+
|
|
105
|
+
def format(self, record):
|
|
106
|
+
if record.threadName == 'MainThread':
|
|
107
|
+
self._style._fmt = self.main_fmt
|
|
108
|
+
else:
|
|
109
|
+
self._style._fmt = self.thread_fmt
|
|
110
|
+
return super().format(record)
|
|
111
|
+
|
|
112
|
+
class ThreadProxyLogger:
|
|
113
|
+
def __getattr__(self, name):
|
|
114
|
+
return getattr(logging.getLogger(threading.current_thread().name), name)
|
|
115
|
+
|
|
116
|
+
def configure_logging(workers, prefix='thread', add_stream_handler=False, highlights=None,
|
|
117
|
+
verbose=False, add_file_handler=True):
|
|
118
|
+
|
|
119
|
+
root_logger = logging.getLogger()
|
|
120
|
+
if getattr(root_logger, '_logging_initialized', False):
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
root_logger.setLevel(logging.DEBUG)
|
|
124
|
+
root_logger.handlers.clear()
|
|
125
|
+
|
|
126
|
+
# Prevent lastResort output by ensuring *a* handler exists:
|
|
127
|
+
root_logger.addHandler(logging.NullHandler())
|
|
128
|
+
|
|
129
|
+
file_formatter = logging.Formatter(
|
|
130
|
+
'%(asctime)s %(levelname)-5s [%(threadName)s] %(funcName)s: %(message)s')
|
|
131
|
+
|
|
132
|
+
if add_stream_handler:
|
|
133
|
+
init(autoreset=False)
|
|
134
|
+
stream_formatter = ColoredFormatter(workers, highlights=highlights, verbose=verbose)
|
|
135
|
+
stream_handler = logging.StreamHandler(sys.stderr)
|
|
136
|
+
stream_handler.setFormatter(stream_formatter)
|
|
137
|
+
stream_handler.setLevel(logging.DEBUG if verbose else logging.INFO)
|
|
138
|
+
root_logger.addHandler(stream_handler)
|
|
139
|
+
|
|
140
|
+
root_logger._logging_initialized = True
|
|
141
|
+
|
|
142
|
+
base = os.path.splitext(os.path.basename(sys.argv[0]))[0]
|
|
143
|
+
|
|
144
|
+
def add_handler(name):
|
|
145
|
+
filename = f'{base}_{name}.log'
|
|
146
|
+
logger = logging.getLogger(name)
|
|
147
|
+
if not any(
|
|
148
|
+
isinstance(handler, logging.FileHandler)
|
|
149
|
+
and getattr(handler, 'baseFilename', '').endswith(filename)
|
|
150
|
+
for handler in logger.handlers
|
|
151
|
+
):
|
|
152
|
+
fhandler = logging.FileHandler(filename, mode='a', encoding='utf-8')
|
|
153
|
+
fhandler.setLevel(logging.DEBUG)
|
|
154
|
+
fhandler.setFormatter(file_formatter)
|
|
155
|
+
logger.addHandler(fhandler)
|
|
156
|
+
logger.setLevel(logging.DEBUG)
|
|
157
|
+
return logger
|
|
158
|
+
|
|
159
|
+
if add_file_handler:
|
|
160
|
+
add_handler(threading.current_thread().name)
|
|
161
|
+
for index in range(workers):
|
|
162
|
+
add_handler(f'{prefix}_{index}')
|
|
@@ -32,7 +32,7 @@ class Scheduler:
|
|
|
32
32
|
"""
|
|
33
33
|
def __init__(self, workers=None, setup_logging=False, add_stream_handler=True,
|
|
34
34
|
state=None, store_results=True, clear_results_on_start=True, verbose=False,
|
|
35
|
-
skip_dependents=False, add_file_handler=True):
|
|
35
|
+
skip_dependents=False, add_file_handler=True, highlights=None):
|
|
36
36
|
""" initialize scheduler with thread pool size, logging, and callback placeholders
|
|
37
37
|
"""
|
|
38
38
|
# number of concurrent worker threads in the pool
|
|
@@ -83,7 +83,8 @@ class Scheduler:
|
|
|
83
83
|
if setup_logging:
|
|
84
84
|
configure_logging(self._workers, prefix=self._prefix, verbose=verbose,
|
|
85
85
|
add_stream_handler=add_stream_handler,
|
|
86
|
-
add_file_handler=add_file_handler
|
|
86
|
+
add_file_handler=add_file_handler,
|
|
87
|
+
highlights=highlights)
|
|
87
88
|
self._skip_dependents = skip_dependents
|
|
88
89
|
|
|
89
90
|
def register(self, obj, name, after=None, with_state=False):
|