horsies 0.1.0a4__tar.gz → 0.1.0a6__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.
- {horsies-0.1.0a4 → horsies-0.1.0a6}/PKG-INFO +1 -1
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/app.py +67 -47
- horsies-0.1.0a6/horsies/core/banner.py +258 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/brokers/postgres.py +315 -288
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/cli.py +7 -2
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/errors.py +3 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/models/app.py +87 -64
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/models/recovery.py +30 -21
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/models/schedule.py +30 -19
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/models/tasks.py +1 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/models/workflow.py +489 -202
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/models/workflow_pg.py +3 -1
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/scheduler/service.py +5 -1
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/scheduler/state.py +39 -27
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/task_decorator.py +138 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/types/status.py +7 -5
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/utils/imports.py +10 -10
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/worker/worker.py +197 -139
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/workflows/engine.py +487 -352
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/workflows/recovery.py +148 -119
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies.egg-info/PKG-INFO +1 -1
- {horsies-0.1.0a4 → horsies-0.1.0a6}/pyproject.toml +1 -1
- horsies-0.1.0a4/horsies/core/banner.py +0 -144
- {horsies-0.1.0a4 → horsies-0.1.0a6}/README.md +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/__init__.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/__init__.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/brokers/__init__.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/brokers/listener.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/codec/serde.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/logging.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/models/__init__.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/models/broker.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/models/queues.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/models/task_pg.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/registry/tasks.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/scheduler/__init__.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/scheduler/calculator.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/utils/loop_runner.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/worker/current.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/workflows/__init__.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/core/workflows/registry.py +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies/py.typed +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies.egg-info/SOURCES.txt +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies.egg-info/dependency_links.txt +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies.egg-info/entry_points.txt +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies.egg-info/requires.txt +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/horsies.egg-info/top_level.txt +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/setup.cfg +0 -0
- {horsies-0.1.0a4 → horsies-0.1.0a6}/tests/test_issue_fixes.py +0 -0
|
@@ -82,7 +82,9 @@ class Horsies:
|
|
|
82
82
|
f'horsies subprocess initialized with {config.queue_mode.name} mode (pid={os.getpid()})'
|
|
83
83
|
)
|
|
84
84
|
else:
|
|
85
|
-
self.logger.info(
|
|
85
|
+
self.logger.info(
|
|
86
|
+
f'horsies initialized as {self._role} with {config.queue_mode.name} mode'
|
|
87
|
+
)
|
|
86
88
|
|
|
87
89
|
def set_role(self, role: str) -> None:
|
|
88
90
|
"""Set the role and log it. Called by CLI after discovery."""
|
|
@@ -192,7 +194,7 @@ class Horsies:
|
|
|
192
194
|
# Register task with this app, passing source for duplicate detection
|
|
193
195
|
# Normalize path with realpath to handle symlinks and relative paths
|
|
194
196
|
source_str = (
|
|
195
|
-
f
|
|
197
|
+
f'{os.path.realpath(fn_location.file)}:{fn_location.line}'
|
|
196
198
|
if fn_location
|
|
197
199
|
else None
|
|
198
200
|
)
|
|
@@ -247,15 +249,19 @@ class Horsies:
|
|
|
247
249
|
if module_path.endswith('.py') or os.path.sep in module_path:
|
|
248
250
|
abs_path = os.path.realpath(module_path)
|
|
249
251
|
if not os.path.exists(abs_path):
|
|
250
|
-
errors.append(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
252
|
+
errors.append(
|
|
253
|
+
_no_location(
|
|
254
|
+
ConfigurationError(
|
|
255
|
+
message=f'task module not found: {module_path}',
|
|
256
|
+
code=ErrorCode.CLI_INVALID_ARGS,
|
|
257
|
+
notes=[f'resolved path: {abs_path}'],
|
|
258
|
+
help_text=(
|
|
259
|
+
'remove it from app.discover_tasks([...]) or fix the path; \n'
|
|
260
|
+
'if using globs, run app.expand_module_globs([...]) first'
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
)
|
|
259
265
|
continue
|
|
260
266
|
import_by_path(abs_path)
|
|
261
267
|
else:
|
|
@@ -265,16 +271,20 @@ class Horsies:
|
|
|
265
271
|
except HorsiesError as exc:
|
|
266
272
|
errors.append(exc)
|
|
267
273
|
except Exception as exc:
|
|
268
|
-
errors.append(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
errors.append(
|
|
275
|
+
_no_location(
|
|
276
|
+
ConfigurationError(
|
|
277
|
+
message=f'failed to import module: {module_path}',
|
|
278
|
+
code=ErrorCode.CLI_INVALID_ARGS,
|
|
279
|
+
notes=[str(exc)],
|
|
280
|
+
help_text=(
|
|
281
|
+
'ensure the module is importable; '
|
|
282
|
+
'for file paths include .py and a valid path, '
|
|
283
|
+
'for dotted paths verify PYTHONPATH or run from the project root'
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
)
|
|
278
288
|
finally:
|
|
279
289
|
self.suppress_sends(prev_suppress)
|
|
280
290
|
return errors
|
|
@@ -283,26 +293,32 @@ class Horsies:
|
|
|
283
293
|
"""Check broker connectivity via SELECT 1."""
|
|
284
294
|
import asyncio
|
|
285
295
|
|
|
296
|
+
from sqlalchemy import text
|
|
297
|
+
|
|
298
|
+
HEALTH_CHECK_SQL = text("""SELECT 1""")
|
|
299
|
+
|
|
286
300
|
errors: list[HorsiesError] = []
|
|
287
301
|
try:
|
|
288
302
|
broker = self.get_broker()
|
|
289
303
|
|
|
290
304
|
async def _test_connection() -> None:
|
|
291
|
-
from sqlalchemy import text
|
|
292
|
-
|
|
293
305
|
async with broker.session_factory() as session:
|
|
294
|
-
await session.execute(
|
|
306
|
+
await session.execute(HEALTH_CHECK_SQL)
|
|
295
307
|
|
|
296
308
|
asyncio.run(_test_connection())
|
|
297
309
|
except HorsiesError as exc:
|
|
298
310
|
errors.append(exc)
|
|
299
311
|
except Exception as exc:
|
|
300
|
-
errors.append(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
312
|
+
errors.append(
|
|
313
|
+
_no_location(
|
|
314
|
+
ConfigurationError(
|
|
315
|
+
message='broker connectivity check failed',
|
|
316
|
+
code=ErrorCode.BROKER_INVALID_URL,
|
|
317
|
+
notes=[str(exc)],
|
|
318
|
+
help_text='check database_url in PostgresConfig',
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
)
|
|
306
322
|
return errors
|
|
307
323
|
|
|
308
324
|
def list_tasks(self) -> list[str]:
|
|
@@ -508,26 +524,30 @@ class Horsies:
|
|
|
508
524
|
if self.config.queue_mode == QueueMode.CUSTOM:
|
|
509
525
|
valid_queues = self.get_valid_queue_names()
|
|
510
526
|
if resolved_queue not in valid_queues:
|
|
511
|
-
report.add(
|
|
512
|
-
|
|
513
|
-
|
|
527
|
+
report.add(
|
|
528
|
+
ConfigurationError(
|
|
529
|
+
message='TaskNode queue not in app config',
|
|
530
|
+
code=ErrorCode.TASK_INVALID_QUEUE,
|
|
531
|
+
notes=[
|
|
532
|
+
f"TaskNode '{task.name}' has queue '{resolved_queue}'",
|
|
533
|
+
f'valid queues: {valid_queues}',
|
|
534
|
+
],
|
|
535
|
+
help_text='use one of the configured queue names or add this queue to app config',
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
queue_valid = False
|
|
539
|
+
elif resolved_queue != 'default':
|
|
540
|
+
report.add(
|
|
541
|
+
ConfigurationError(
|
|
542
|
+
message='TaskNode has non-default queue in DEFAULT mode',
|
|
543
|
+
code=ErrorCode.CONFIG_INVALID_QUEUE_MODE,
|
|
514
544
|
notes=[
|
|
515
545
|
f"TaskNode '{task.name}' has queue '{resolved_queue}'",
|
|
516
|
-
|
|
546
|
+
"app is in DEFAULT mode (only 'default' queue allowed)",
|
|
517
547
|
],
|
|
518
|
-
help_text='
|
|
519
|
-
)
|
|
520
|
-
|
|
521
|
-
elif resolved_queue != 'default':
|
|
522
|
-
report.add(ConfigurationError(
|
|
523
|
-
message='TaskNode has non-default queue in DEFAULT mode',
|
|
524
|
-
code=ErrorCode.CONFIG_INVALID_QUEUE_MODE,
|
|
525
|
-
notes=[
|
|
526
|
-
f"TaskNode '{task.name}' has queue '{resolved_queue}'",
|
|
527
|
-
"app is in DEFAULT mode (only 'default' queue allowed)",
|
|
528
|
-
],
|
|
529
|
-
help_text='either remove queue override or switch to QueueMode.CUSTOM',
|
|
530
|
-
))
|
|
548
|
+
help_text='either remove queue override or switch to QueueMode.CUSTOM',
|
|
549
|
+
)
|
|
550
|
+
)
|
|
531
551
|
queue_valid = False
|
|
532
552
|
|
|
533
553
|
if queue_valid:
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Startup banner for horsies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import TYPE_CHECKING, TextIO
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from horsies.core.app import Horsies
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# ANSI color codes (matching Rust owo-colors output)
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
class Colors:
|
|
17
|
+
"""ANSI escape codes for terminal colors."""
|
|
18
|
+
|
|
19
|
+
RESET = '\033[0m'
|
|
20
|
+
BOLD = '\033[1m'
|
|
21
|
+
DIMMED = '\033[2m'
|
|
22
|
+
|
|
23
|
+
# Standard colors
|
|
24
|
+
WHITE = '\033[37m'
|
|
25
|
+
BRIGHT_WHITE = '\033[97m'
|
|
26
|
+
CYAN = '\033[36m'
|
|
27
|
+
BRIGHT_CYAN = '\033[96m'
|
|
28
|
+
YELLOW = '\033[33m'
|
|
29
|
+
BRIGHT_YELLOW = '\033[93m'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _color(text: str, *codes: str) -> str:
|
|
33
|
+
"""Apply ANSI color codes to text."""
|
|
34
|
+
if not codes:
|
|
35
|
+
return text
|
|
36
|
+
return ''.join(codes) + text + Colors.RESET
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Braille art of galloping horse - converted from the golden horse image
|
|
40
|
+
# Each braille character represents a 2x4 pixel block for higher resolution
|
|
41
|
+
HORSE_BRAILLE = """
|
|
42
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣶⣾⣿⣿⣿⣿⣷⣯⡀⠀⠀⠀⠀⠀⠀
|
|
43
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⣿⣆⠀⠀⠀⠀
|
|
44
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⡟⠹⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀
|
|
45
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣠⣴⣾⣿⣿⣿⣶⣶⣶⣤⣤⣤⣤⣤⣶⣾⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⠈⠉⠛⠻⡿⣿⣿⠂⠀
|
|
46
|
+
⠀⠀⢀⣀⠀⢀⣀⣠⣶⣿⣿⠟⢛⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⡐⠀⠀⠀⠀⠀⠀⠈⠋⡿⠁⠀⠀
|
|
47
|
+
⠀⠀⠀⢹⣿⣿⣿⣿⣿⡿⠁⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
48
|
+
⠀⠀⠀⠀⠛⠻⠿⠛⠉⠀⠀⠀⠈⢯⡻⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⡿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
49
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠻⣷⣿⣿⣿⡟⠀⠙⠻⠿⠿⣿⣿⠃⣿⣿⣿⣿⣿⣿⣿⣿⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
50
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⡿⠁⢀⠀⠀⠀⠀⠀⠂⠀⢿⣿⣿⣿⡍⠈⢁⣙⣿⢦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
51
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⠏⠀⠀⣼⠀⠀⠀⠀⠀⠀⠀⠀⠙⢿⣿⣷⠀⠀⠀⠀⠁⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
52
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣿⣧⣀⢄⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢻⣿⣧⠀⠀⢀⣼⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
53
|
+
⠀⠀⠀⣀⠀⠀⠀⢀⡀⠀⠀⠀⡀⠀⠀⠀⣀⠛⣿⣷⣍⠛⣦⡀⠀⠂⢠⣷⣶⣾⡿⠟⠛⢃⠀⢠⣾⡟⠀⠀⠀⠀⡀⠀⠀⠀⡀⠀⠀⠀
|
|
54
|
+
⠄⡤⠦⠤⠤⢤⡤⠡⠤⠤⢤⠬⠤⠤⠤⢤⠅⠀⠤⣿⣷⠄⠎⢻⣤⡦⠄⠀⠤⢵⠄⠠⠤⠬⣾⠏⠁⠀⠥⡤⠄⠠⠬⢦⡤⠀⠠⠵⢤⠠
|
|
55
|
+
⠚⠒⠒⠒⡶⠚⠒⠒⠒⡰⠓⠒⠒⠒⢲⠓⠒⠒⠒⢻⠿⠀⠀⠚⢿⡷⠐⠒⠒⠚⡆⠒⠒⠒⠚⣖⠒⠒⠒⠚⢶⠒⠒⠒⠚⢶⠂⠐⠒⠛
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# Figlet-style "horsies" text with version and tagline
|
|
59
|
+
LOGO_TEXT = r"""
|
|
60
|
+
__ _
|
|
61
|
+
/ /_ ____ __________(_)__ _____
|
|
62
|
+
/ __ \/ __ \/ ___/ ___/ / _ \/ ___/ {version}
|
|
63
|
+
/ / / / /_/ / / (__ ) / __(__ ) distributed task queue
|
|
64
|
+
/_/ /_/\____/_/ /____/_/\___/____/ and workflow engine
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
# Full banner combining horse and logo
|
|
68
|
+
BANNER = (
|
|
69
|
+
HORSE_BRAILLE
|
|
70
|
+
+ r"""
|
|
71
|
+
__ _
|
|
72
|
+
/ /_ ____ __________(_)__ _____
|
|
73
|
+
/ __ \/ __ \/ ___/ ___/ / _ \/ ___/ {version}
|
|
74
|
+
/ / / / /_/ / / (__ ) / __(__ ) distributed task queue
|
|
75
|
+
/_/ /_/\____/_/ /____/_/\___/____/ and workflow engine
|
|
76
|
+
"""
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_version() -> str:
|
|
81
|
+
"""Get horsies version from package metadata."""
|
|
82
|
+
try:
|
|
83
|
+
from importlib.metadata import version
|
|
84
|
+
return version('horsies')
|
|
85
|
+
except Exception:
|
|
86
|
+
return 'dev'
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def format_banner(version: str | None = None) -> str:
|
|
90
|
+
"""Format the banner string with version."""
|
|
91
|
+
if version is None:
|
|
92
|
+
version = get_version()
|
|
93
|
+
return BANNER.format(version=f'v{version}')
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Formatting helpers (matching Rust banner.rs)
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def _format_ms(ms: int) -> str:
|
|
101
|
+
"""Format milliseconds into a human-readable string."""
|
|
102
|
+
if ms >= 60_000:
|
|
103
|
+
mins = ms // 60_000
|
|
104
|
+
remainder_s = (ms % 60_000) // 1_000
|
|
105
|
+
if remainder_s > 0:
|
|
106
|
+
return f'{mins}m{remainder_s}s'
|
|
107
|
+
return f'{mins}m'
|
|
108
|
+
elif ms >= 1_000:
|
|
109
|
+
secs = ms // 1_000
|
|
110
|
+
remainder_ms = ms % 1_000
|
|
111
|
+
if remainder_ms > 0:
|
|
112
|
+
return f'{secs}.{remainder_ms // 100}s'
|
|
113
|
+
return f'{secs}s'
|
|
114
|
+
return f'{ms}ms'
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _format_bool(val: bool) -> str:
|
|
118
|
+
"""Format a boolean for display."""
|
|
119
|
+
return 'yes' if val else 'no'
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _write_section_header(lines: list[str], name: str) -> None:
|
|
123
|
+
"""Write a colored [section] header line."""
|
|
124
|
+
header = _color(name, Colors.BRIGHT_CYAN, Colors.BOLD)
|
|
125
|
+
lines.append(f'[{header}]')
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _write_kv(lines: list[str], key: str, value: str) -> None:
|
|
129
|
+
"""Write a colored '.> key: value' line with consistent alignment."""
|
|
130
|
+
prefix = _color('.>', Colors.DIMMED)
|
|
131
|
+
# Pad key before coloring to maintain alignment
|
|
132
|
+
padded_key = f'{key}:'.ljust(22)
|
|
133
|
+
colored_key = _color(padded_key, Colors.WHITE, Colors.BOLD)
|
|
134
|
+
colored_value = _color(value, Colors.BRIGHT_WHITE)
|
|
135
|
+
lines.append(f' {prefix} {colored_key} {colored_value}')
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def print_banner(
|
|
139
|
+
app: 'Horsies',
|
|
140
|
+
role: str = 'worker',
|
|
141
|
+
show_tasks: bool = True,
|
|
142
|
+
file: TextIO | None = None,
|
|
143
|
+
) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Print startup banner with configuration and task list.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
app: The Horsies app instance
|
|
149
|
+
role: The role (worker, scheduler, producer)
|
|
150
|
+
show_tasks: Whether to list discovered tasks
|
|
151
|
+
file: Output file (default: sys.stdout)
|
|
152
|
+
"""
|
|
153
|
+
if file is None:
|
|
154
|
+
file = sys.stdout
|
|
155
|
+
|
|
156
|
+
version = get_version()
|
|
157
|
+
|
|
158
|
+
# Build the banner
|
|
159
|
+
lines: list[str] = []
|
|
160
|
+
|
|
161
|
+
# ASCII art header - cyan colored
|
|
162
|
+
colored_horse = _color(HORSE_BRAILLE, Colors.CYAN)
|
|
163
|
+
lines.append(colored_horse)
|
|
164
|
+
|
|
165
|
+
# Logo with version - bright yellow bold
|
|
166
|
+
logo = LOGO_TEXT.replace('{version}', f'v{version}')
|
|
167
|
+
colored_logo = _color(logo, Colors.BRIGHT_YELLOW, Colors.BOLD)
|
|
168
|
+
lines.append(colored_logo)
|
|
169
|
+
|
|
170
|
+
# [config] section
|
|
171
|
+
_write_section_header(lines, 'config')
|
|
172
|
+
_write_kv(lines, 'app', app.__class__.__name__)
|
|
173
|
+
_write_kv(lines, 'role', role)
|
|
174
|
+
_write_kv(lines, 'queue_mode', app.config.queue_mode.name.lower())
|
|
175
|
+
|
|
176
|
+
# Queue info
|
|
177
|
+
if app.config.queue_mode.name == 'CUSTOM' and app.config.custom_queues:
|
|
178
|
+
queues_str = ', '.join(q.name for q in app.config.custom_queues)
|
|
179
|
+
else:
|
|
180
|
+
queues_str = 'default'
|
|
181
|
+
_write_kv(lines, 'queues', queues_str)
|
|
182
|
+
|
|
183
|
+
# Broker info (mask password)
|
|
184
|
+
broker_url = app.config.broker.database_url
|
|
185
|
+
if '@' in broker_url:
|
|
186
|
+
pre, post = broker_url.split('@', 1)
|
|
187
|
+
if ':' in pre:
|
|
188
|
+
scheme_user = pre.rsplit(':', 1)[0]
|
|
189
|
+
broker_url = f'{scheme_user}:****@{post}'
|
|
190
|
+
_write_kv(lines, 'broker', broker_url)
|
|
191
|
+
|
|
192
|
+
# Cluster-wide cap
|
|
193
|
+
if hasattr(app.config, 'cluster_wide_cap') and app.config.cluster_wide_cap:
|
|
194
|
+
_write_kv(lines, 'cluster_cap', f'{app.config.cluster_wide_cap} (cluster-wide)')
|
|
195
|
+
|
|
196
|
+
# Prefetch buffer
|
|
197
|
+
if hasattr(app.config, 'prefetch_buffer') and app.config.prefetch_buffer > 0:
|
|
198
|
+
_write_kv(lines, 'prefetch', str(app.config.prefetch_buffer))
|
|
199
|
+
|
|
200
|
+
lines.append('')
|
|
201
|
+
|
|
202
|
+
# [recovery] section
|
|
203
|
+
if hasattr(app.config, 'recovery') and app.config.recovery:
|
|
204
|
+
recovery = app.config.recovery
|
|
205
|
+
_write_section_header(lines, 'recovery')
|
|
206
|
+
_write_kv(
|
|
207
|
+
lines,
|
|
208
|
+
'requeue_stale_claimed',
|
|
209
|
+
_format_bool(recovery.auto_requeue_stale_claimed),
|
|
210
|
+
)
|
|
211
|
+
_write_kv(
|
|
212
|
+
lines,
|
|
213
|
+
'fail_stale_running',
|
|
214
|
+
_format_bool(recovery.auto_fail_stale_running),
|
|
215
|
+
)
|
|
216
|
+
_write_kv(
|
|
217
|
+
lines,
|
|
218
|
+
'check_interval',
|
|
219
|
+
_format_ms(recovery.check_interval_ms),
|
|
220
|
+
)
|
|
221
|
+
_write_kv(
|
|
222
|
+
lines,
|
|
223
|
+
'heartbeat_interval',
|
|
224
|
+
_format_ms(recovery.runner_heartbeat_interval_ms),
|
|
225
|
+
)
|
|
226
|
+
lines.append('')
|
|
227
|
+
|
|
228
|
+
# [tasks] section
|
|
229
|
+
if show_tasks:
|
|
230
|
+
task_names = app.list_tasks()
|
|
231
|
+
tasks_header = _color('tasks', Colors.BRIGHT_CYAN, Colors.BOLD)
|
|
232
|
+
lines.append(f'[{tasks_header}] ({len(task_names)} registered)')
|
|
233
|
+
for task_name in sorted(task_names):
|
|
234
|
+
bullet = _color('.', Colors.DIMMED)
|
|
235
|
+
name = _color(task_name, Colors.WHITE)
|
|
236
|
+
lines.append(f' {bullet} {name}')
|
|
237
|
+
lines.append('')
|
|
238
|
+
|
|
239
|
+
# Print everything
|
|
240
|
+
output = '\n'.join(lines)
|
|
241
|
+
print(output, file=file)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def print_simple_banner(file: TextIO | None = None) -> None:
|
|
245
|
+
"""Print just the ASCII art banner without config."""
|
|
246
|
+
if file is None:
|
|
247
|
+
file = sys.stdout
|
|
248
|
+
|
|
249
|
+
version = get_version()
|
|
250
|
+
|
|
251
|
+
# Horse art - cyan
|
|
252
|
+
colored_horse = _color(HORSE_BRAILLE, Colors.CYAN)
|
|
253
|
+
print(colored_horse, file=file)
|
|
254
|
+
|
|
255
|
+
# Logo with version - bright yellow bold
|
|
256
|
+
logo = LOGO_TEXT.replace('{version}', f'v{version}')
|
|
257
|
+
colored_logo = _color(logo, Colors.BRIGHT_YELLOW, Colors.BOLD)
|
|
258
|
+
print(colored_logo, file=file)
|