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.
Files changed (25) hide show
  1. {thread_order-1.3.0/thread_order.egg-info → thread_order-1.3.2}/PKG-INFO +16 -18
  2. {thread_order-1.3.0 → thread_order-1.3.2}/README.md +15 -17
  3. {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/__init__.py +5 -1
  4. {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/cli/app.py +15 -13
  5. thread_order-1.3.2/thread_order/logger.py +162 -0
  6. {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/scheduler.py +3 -2
  7. {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/ui/app.py +339 -109
  8. {thread_order-1.3.0 → thread_order-1.3.2/thread_order.egg-info}/PKG-INFO +16 -18
  9. thread_order-1.3.0/thread_order/logger.py +0 -147
  10. {thread_order-1.3.0 → thread_order-1.3.2}/LICENSE +0 -0
  11. {thread_order-1.3.0 → thread_order-1.3.2}/pyproject.toml +0 -0
  12. {thread_order-1.3.0 → thread_order-1.3.2}/setup.cfg +0 -0
  13. {thread_order-1.3.0 → thread_order-1.3.2}/tests/test_graph.py +0 -0
  14. {thread_order-1.3.0 → thread_order-1.3.2}/tests/test_init.py +0 -0
  15. {thread_order-1.3.0 → thread_order-1.3.2}/tests/test_scheduler.py +0 -0
  16. {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/cli/__init__.py +0 -0
  17. {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/graph.py +0 -0
  18. {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/graph_summary.py +0 -0
  19. {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/timer.py +0 -0
  20. {thread_order-1.3.0 → thread_order-1.3.2}/thread_order/ui/__init__.py +0 -0
  21. {thread_order-1.3.0 → thread_order-1.3.2}/thread_order.egg-info/SOURCES.txt +0 -0
  22. {thread_order-1.3.0 → thread_order-1.3.2}/thread_order.egg-info/dependency_links.txt +0 -0
  23. {thread_order-1.3.0 → thread_order-1.3.2}/thread_order.egg-info/entry_points.txt +0 -0
  24. {thread_order-1.3.0 → thread_order-1.3.2}/thread_order.egg-info/requires.txt +0 -0
  25. {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.0
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
- ## Custom Logging
221
+ ## Optional Highlights
222
222
 
223
- If a module defines a `setup_logging()` function, `tdrun` will automatically detect and invoke it during module initialization. This allows the module to configure its own logging behavior in a consistent and structured way while remaining compatible with the execution model.
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
- def setup_logging(
227
- workers, # Effective worker count resolved by CLI
228
- verbose=False, # Enable verbose (debug-level) logging
229
- add_stream_handler=False, # Attach console handler (disabled when progress/viewer active)
230
- add_file_handler=False # Attach file handler when --log is enabled
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
- At runtime, `tdrun` will invoke it as:
238
+ When logging is enabled, these rules are appended to the default highlights and applied to log output during execution.
235
239
 
236
- ```Python
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
- ## Custom Logging
179
+ ## Optional Highlights
180
180
 
181
- If a module defines a `setup_logging()` function, `tdrun` will automatically detect and invoke it during module initialization. This allows the module to configure its own logging behavior in a consistent and structured way while remaining compatible with the execution model.
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
- def setup_logging(
185
- workers, # Effective worker count resolved by CLI
186
- verbose=False, # Enable verbose (debug-level) logging
187
- add_stream_handler=False, # Attach console handler (disabled when progress/viewer active)
188
- add_file_handler=False # Attach file handler when --log is enabled
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
- At runtime, `tdrun` will invoke it as:
196
+ When logging is enabled, these rules are appended to the default highlights and applied to log output during execution.
193
197
 
194
- ```Python
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.0'
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, ThreadProxyLogger, default_workers, load_and_collect_functions, register_functions)
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
- setup_logging_function = getattr(module, 'setup_logging', None)
181
- if callable(setup_logging_function):
182
- setup_logging_function(
183
- args.effective_workers,
184
- verbose=args.verbose,
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):