programgarden 1.29.3__tar.gz → 1.29.4__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.
- {programgarden-1.29.3 → programgarden-1.29.4}/PKG-INFO +1 -1
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/executor.py +142 -85
- {programgarden-1.29.3 → programgarden-1.29.4}/pyproject.toml +1 -1
- {programgarden-1.29.3 → programgarden-1.29.4}/README.md +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/__init__.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/binding_validator.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/client.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/code_worker.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/context.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/database/__init__.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/database/query_builder.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/deep_fixtures.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/node_runner.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/plugin/__init__.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/providers/__init__.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/reconnect_handler.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/resolver.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/resource/__init__.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/resource/context.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/resource/limiter.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/resource/monitor.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/resource/throttle.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/semantic_rules.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/tools/__init__.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/tools/event_tools.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/tools/job_tools.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-1.29.3 → programgarden-1.29.4}/programgarden/validation_recommender.py +0 -0
|
@@ -15,6 +15,7 @@ import ast
|
|
|
15
15
|
import re
|
|
16
16
|
import uuid
|
|
17
17
|
import logging
|
|
18
|
+
import sys
|
|
18
19
|
|
|
19
20
|
from programgarden.resolver import WorkflowResolver, ResolvedWorkflow, ValidationResult
|
|
20
21
|
from programgarden_core import (
|
|
@@ -42,6 +43,44 @@ from programgarden_core.nodes.base import BaseMessagingNode
|
|
|
42
43
|
logger = logging.getLogger("programgarden.executor")
|
|
43
44
|
|
|
44
45
|
|
|
46
|
+
def _safe_print(*args: Any, **kwargs: Any) -> None:
|
|
47
|
+
"""Emit console output without ever letting it abort the caller.
|
|
48
|
+
|
|
49
|
+
Console output is a side effect — a workflow must never die because a log
|
|
50
|
+
line could not be rendered. On a console whose encoding cannot represent
|
|
51
|
+
the text (cp949 on Korean Windows is the case we hit), a bare ``print``
|
|
52
|
+
raises ``UnicodeEncodeError``. Two things make that fatal rather than
|
|
53
|
+
cosmetic:
|
|
54
|
+
|
|
55
|
+
1. The progress line at the head of ``_execute_main_flow`` runs before any
|
|
56
|
+
node does, so the whole job dies before it starts.
|
|
57
|
+
2. The same call pattern sits inside ``except`` blocks, so the *reporting*
|
|
58
|
+
path raises too and the original cause is swallowed — the failure never
|
|
59
|
+
reaches ``notify_job_state`` and vanishes silently.
|
|
60
|
+
|
|
61
|
+
Note that ASCII-only literals are not sufficient protection: interpolated
|
|
62
|
+
values (node names, exception messages) can themselves carry characters the
|
|
63
|
+
console cannot encode. So every print in this module goes through here.
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
print(*args, **kwargs)
|
|
67
|
+
except UnicodeEncodeError:
|
|
68
|
+
# Re-render through the stream's own encoding so the line still lands,
|
|
69
|
+
# with unrepresentable characters replaced rather than the job lost.
|
|
70
|
+
try:
|
|
71
|
+
stream = kwargs.get("file") or sys.stdout
|
|
72
|
+
encoding = getattr(stream, "encoding", None) or "ascii"
|
|
73
|
+
sep = kwargs.get("sep", " ")
|
|
74
|
+
end = kwargs.get("end", "\n")
|
|
75
|
+
text = sep.join(str(a) for a in args) + end
|
|
76
|
+
stream.write(text.encode(encoding, errors="replace").decode(encoding, errors="replace"))
|
|
77
|
+
except Exception:
|
|
78
|
+
logger.debug("console output dropped", exc_info=True)
|
|
79
|
+
except Exception:
|
|
80
|
+
# Logging is never load-bearing.
|
|
81
|
+
logger.debug("console output dropped", exc_info=True)
|
|
82
|
+
|
|
83
|
+
|
|
45
84
|
# lib reserved iteration variable roots — valid ONLY mid-iteration.
|
|
46
85
|
# `{{ item.* }}` (auto-iterate per-item) and `{{ row.* }}` (ConditionNode
|
|
47
86
|
# items.extract) only resolve while the executor has an iteration context set;
|
|
@@ -8396,8 +8435,8 @@ class DisplayNodeExecutor(NodeExecutorBase):
|
|
|
8396
8435
|
|
|
8397
8436
|
if chart_type == "summary":
|
|
8398
8437
|
# Raw JSON 표시
|
|
8399
|
-
|
|
8400
|
-
|
|
8438
|
+
_safe_print(f"\n📋 {title or 'Data Summary'} [{now}]")
|
|
8439
|
+
_safe_print("=" * 60)
|
|
8401
8440
|
try:
|
|
8402
8441
|
def serialize(obj):
|
|
8403
8442
|
if hasattr(obj, 'model_dump'):
|
|
@@ -8411,11 +8450,11 @@ class DisplayNodeExecutor(NodeExecutorBase):
|
|
|
8411
8450
|
return obj
|
|
8412
8451
|
|
|
8413
8452
|
serialized = serialize(data)
|
|
8414
|
-
|
|
8453
|
+
_safe_print(json.dumps(serialized, indent=2, ensure_ascii=False, default=str))
|
|
8415
8454
|
except Exception as e:
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8455
|
+
_safe_print(f"(직렬화 실패: {e})")
|
|
8456
|
+
_safe_print(str(data)[:500])
|
|
8457
|
+
_safe_print("=" * 60 + "\n")
|
|
8419
8458
|
|
|
8420
8459
|
output_data["data"] = data
|
|
8421
8460
|
|
|
@@ -8426,8 +8465,8 @@ class DisplayNodeExecutor(NodeExecutorBase):
|
|
|
8426
8465
|
sort_by = config.get("sort_by")
|
|
8427
8466
|
sort_order = config.get("sort_order", "desc")
|
|
8428
8467
|
|
|
8429
|
-
|
|
8430
|
-
|
|
8468
|
+
_safe_print(f"\n📊 {title or 'Table'} [{now}]")
|
|
8469
|
+
_safe_print("=" * 80)
|
|
8431
8470
|
|
|
8432
8471
|
# 데이터 형식 판단
|
|
8433
8472
|
if isinstance(data, list) and len(data) > 0 and isinstance(data[0], dict):
|
|
@@ -8441,13 +8480,13 @@ class DisplayNodeExecutor(NodeExecutorBase):
|
|
|
8441
8480
|
(k, k) for k in list(rows[0].keys())[:8]
|
|
8442
8481
|
]
|
|
8443
8482
|
header = " | ".join(f"{label:<12}" for _, label in cols)
|
|
8444
|
-
|
|
8445
|
-
|
|
8483
|
+
_safe_print(header)
|
|
8484
|
+
_safe_print("-" * 80)
|
|
8446
8485
|
for row in rows:
|
|
8447
8486
|
values = " | ".join(
|
|
8448
8487
|
f"{self._format_value(row.get(key)):<12}" for key, _ in cols
|
|
8449
8488
|
)
|
|
8450
|
-
|
|
8489
|
+
_safe_print(values)
|
|
8451
8490
|
|
|
8452
8491
|
elif isinstance(data, dict):
|
|
8453
8492
|
# dict of dicts (symbol: {field: value}) 또는 flat dict
|
|
@@ -8463,19 +8502,19 @@ class DisplayNodeExecutor(NodeExecutorBase):
|
|
|
8463
8502
|
(k, k) for k in list(items[0][1].keys())[:6]
|
|
8464
8503
|
]
|
|
8465
8504
|
header = f"{'Key':<12} | " + " | ".join(f"{label:<12}" for _, label in cols)
|
|
8466
|
-
|
|
8467
|
-
|
|
8505
|
+
_safe_print(header)
|
|
8506
|
+
_safe_print("-" * 80)
|
|
8468
8507
|
for key, val in items:
|
|
8469
8508
|
values = f"{key:<12} | " + " | ".join(
|
|
8470
8509
|
f"{self._format_value(val.get(col_key)):<12}" for col_key, _ in cols
|
|
8471
8510
|
)
|
|
8472
|
-
|
|
8511
|
+
_safe_print(values)
|
|
8473
8512
|
else:
|
|
8474
8513
|
# flat dict
|
|
8475
8514
|
for k, v in list(data.items())[:limit]:
|
|
8476
|
-
|
|
8515
|
+
_safe_print(f" {k:<25}: {self._format_value(v)}")
|
|
8477
8516
|
|
|
8478
|
-
|
|
8517
|
+
_safe_print("=" * 80 + "\n")
|
|
8479
8518
|
output_data["data"] = data
|
|
8480
8519
|
|
|
8481
8520
|
elif chart_type == "line":
|
|
@@ -8489,36 +8528,36 @@ class DisplayNodeExecutor(NodeExecutorBase):
|
|
|
8489
8528
|
context.log("error", f"line 차트에 x_field, y_field 필수", node_id)
|
|
8490
8529
|
return {"rendered": False, "error": "x_field, y_field 필수"}
|
|
8491
8530
|
|
|
8492
|
-
|
|
8493
|
-
|
|
8531
|
+
_safe_print(f"\n📈 {title or 'Line Chart'} [{now}]")
|
|
8532
|
+
_safe_print("=" * 60)
|
|
8494
8533
|
|
|
8495
8534
|
if isinstance(data, list) and len(data) > 0:
|
|
8496
8535
|
first = data[0]
|
|
8497
8536
|
last = data[-1]
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8537
|
+
_safe_print(f"📊 데이터 포인트: {len(data)}개")
|
|
8538
|
+
_safe_print(f"📅 X축 범위: {first.get(x_field)} ~ {last.get(x_field)}")
|
|
8539
|
+
_safe_print(f"📈 Y축({y_field}) 시작: {self._format_value(first.get(y_field))}")
|
|
8540
|
+
_safe_print(f"📈 Y축({y_field}) 끝: {self._format_value(last.get(y_field))}")
|
|
8502
8541
|
|
|
8503
8542
|
# 최대/최소
|
|
8504
8543
|
values = [d.get(y_field, 0) for d in data if d.get(y_field) is not None]
|
|
8505
8544
|
if values:
|
|
8506
|
-
|
|
8507
|
-
|
|
8545
|
+
_safe_print(f"📈 최대: {self._format_value(max(values))}")
|
|
8546
|
+
_safe_print(f"📉 최소: {self._format_value(min(values))}")
|
|
8508
8547
|
|
|
8509
8548
|
# 시그널 마커 표시
|
|
8510
8549
|
if signal_field:
|
|
8511
8550
|
signals = self._extract_signals(data, x_field, signal_field, side_field)
|
|
8512
8551
|
if signals:
|
|
8513
|
-
|
|
8552
|
+
_safe_print(f"\n🎯 시그널 ({len(signals)}개):")
|
|
8514
8553
|
for sig in signals[:10]: # 최대 10개만 표시
|
|
8515
|
-
|
|
8554
|
+
_safe_print(f" {sig['marker']} {sig['x']} ({sig['signal']}/{sig['side']})")
|
|
8516
8555
|
if len(signals) > 10:
|
|
8517
|
-
|
|
8556
|
+
_safe_print(f" ... 외 {len(signals) - 10}개")
|
|
8518
8557
|
else:
|
|
8519
|
-
|
|
8558
|
+
_safe_print("(데이터 없음)")
|
|
8520
8559
|
|
|
8521
|
-
|
|
8560
|
+
_safe_print("=" * 60 + "\n")
|
|
8522
8561
|
output_data["data"] = data
|
|
8523
8562
|
output_data["x_field"] = x_field
|
|
8524
8563
|
output_data["y_field"] = y_field
|
|
@@ -8542,8 +8581,8 @@ class DisplayNodeExecutor(NodeExecutorBase):
|
|
|
8542
8581
|
context.log("error", f"multi_line 차트에 x_field, y_field, series_key 필수", node_id)
|
|
8543
8582
|
return {"rendered": False, "error": "x_field, y_field, series_key 필수"}
|
|
8544
8583
|
|
|
8545
|
-
|
|
8546
|
-
|
|
8584
|
+
_safe_print(f"\n📈 {title or 'Multi-Line Chart'} [{now}]")
|
|
8585
|
+
_safe_print("=" * 80)
|
|
8547
8586
|
|
|
8548
8587
|
# data가 평탄화된 배열 형식인 경우 [{symbol, date, value, ...}, ...]
|
|
8549
8588
|
if isinstance(data, list):
|
|
@@ -8568,29 +8607,29 @@ class DisplayNodeExecutor(NodeExecutorBase):
|
|
|
8568
8607
|
|
|
8569
8608
|
series_items = series_items[:limit]
|
|
8570
8609
|
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8610
|
+
_safe_print(f"📊 시리즈 수: {len(series_items)}개 (limit: {limit})")
|
|
8611
|
+
_safe_print("-" * 80)
|
|
8612
|
+
_safe_print(f"{'Series':<12} | {'Points':<8} | {f'First {y_field}':<15} | {f'Last {y_field}':<15}")
|
|
8613
|
+
_safe_print("-" * 80)
|
|
8575
8614
|
|
|
8576
8615
|
for symbol, rows in series_items:
|
|
8577
8616
|
if isinstance(rows, list) and len(rows) > 0:
|
|
8578
8617
|
first_val = rows[0].get(y_field, 0)
|
|
8579
8618
|
last_val = rows[-1].get(y_field, 0)
|
|
8580
|
-
|
|
8619
|
+
_safe_print(f"{symbol:<12} | {len(rows):<8} | {self._format_value(first_val):<15} | {self._format_value(last_val):<15}")
|
|
8581
8620
|
|
|
8582
8621
|
# 시그널 마커 표시
|
|
8583
8622
|
if signal_field:
|
|
8584
8623
|
signals = self._extract_signals(data, x_field, signal_field, side_field, series_key)
|
|
8585
8624
|
if signals:
|
|
8586
|
-
|
|
8625
|
+
_safe_print(f"\n🎯 시그널 ({len(signals)}개):")
|
|
8587
8626
|
for sig in signals[:15]: # 최대 15개만 표시
|
|
8588
8627
|
series_info = f" [{sig.get('series', '')}]" if sig.get('series') else ""
|
|
8589
|
-
|
|
8628
|
+
_safe_print(f" {sig['marker']} {sig['x']}{series_info} ({sig['signal']}/{sig['side']})")
|
|
8590
8629
|
if len(signals) > 15:
|
|
8591
|
-
|
|
8630
|
+
_safe_print(f" ... 외 {len(signals) - 15}개")
|
|
8592
8631
|
|
|
8593
|
-
|
|
8632
|
+
_safe_print("=" * 80 + "\n")
|
|
8594
8633
|
output_data["data"] = data
|
|
8595
8634
|
output_data["x_field"] = x_field
|
|
8596
8635
|
output_data["y_field"] = y_field
|
|
@@ -8616,16 +8655,16 @@ class DisplayNodeExecutor(NodeExecutorBase):
|
|
|
8616
8655
|
context.log("error", f"candlestick 차트에 date_field, open_field, high_field, low_field, close_field 필수", node_id)
|
|
8617
8656
|
return {"rendered": False, "error": "OHLC 필드 필수"}
|
|
8618
8657
|
|
|
8619
|
-
|
|
8620
|
-
|
|
8658
|
+
_safe_print(f"\n📊 {title or 'Candlestick Chart'} [{now}]")
|
|
8659
|
+
_safe_print("=" * 80)
|
|
8621
8660
|
|
|
8622
8661
|
if isinstance(data, list) and len(data) > 0:
|
|
8623
8662
|
first = data[0]
|
|
8624
8663
|
last = data[-1]
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8664
|
+
_safe_print(f"📊 캔들 수: {len(data)}개")
|
|
8665
|
+
_safe_print(f"📅 기간: {first.get(date_field)} ~ {last.get(date_field)}")
|
|
8666
|
+
_safe_print(f"💵 시작 종가: {self._format_value(first.get(close_field))}")
|
|
8667
|
+
_safe_print(f"💵 최종 종가: {self._format_value(last.get(close_field))}")
|
|
8629
8668
|
|
|
8630
8669
|
# 수익률
|
|
8631
8670
|
start_price = first.get(close_field, 0)
|
|
@@ -8634,21 +8673,21 @@ class DisplayNodeExecutor(NodeExecutorBase):
|
|
|
8634
8673
|
pct_change = (end_price - start_price) / start_price * 100
|
|
8635
8674
|
sign = "+" if pct_change >= 0 else ""
|
|
8636
8675
|
color = "\033[92m" if pct_change >= 0 else "\033[91m"
|
|
8637
|
-
|
|
8676
|
+
_safe_print(f"📈 변동률: {color}{sign}{pct_change:.2f}%\033[0m")
|
|
8638
8677
|
|
|
8639
8678
|
# 시그널 마커 표시
|
|
8640
8679
|
if signal_field:
|
|
8641
8680
|
signals = self._extract_signals(data, date_field, signal_field, side_field)
|
|
8642
8681
|
if signals:
|
|
8643
|
-
|
|
8682
|
+
_safe_print(f"\n🎯 시그널 ({len(signals)}개):")
|
|
8644
8683
|
for sig in signals[:10]: # 최대 10개만 표시
|
|
8645
|
-
|
|
8684
|
+
_safe_print(f" {sig['marker']} {sig['x']} ({sig['signal']}/{sig['side']})")
|
|
8646
8685
|
if len(signals) > 10:
|
|
8647
|
-
|
|
8686
|
+
_safe_print(f" ... 외 {len(signals) - 10}개")
|
|
8648
8687
|
else:
|
|
8649
|
-
|
|
8688
|
+
_safe_print("(데이터 없음)")
|
|
8650
8689
|
|
|
8651
|
-
|
|
8690
|
+
_safe_print("=" * 80 + "\n")
|
|
8652
8691
|
output_data["data"] = data
|
|
8653
8692
|
output_data["date_field"] = date_field
|
|
8654
8693
|
output_data["open_field"] = open_field
|
|
@@ -8671,23 +8710,23 @@ class DisplayNodeExecutor(NodeExecutorBase):
|
|
|
8671
8710
|
context.log("error", f"bar 차트에 x_field, y_field 필수", node_id)
|
|
8672
8711
|
return {"rendered": False, "error": "x_field, y_field 필수"}
|
|
8673
8712
|
|
|
8674
|
-
|
|
8675
|
-
|
|
8713
|
+
_safe_print(f"\n📊 {title or 'Bar Chart'} [{now}]")
|
|
8714
|
+
_safe_print("=" * 60)
|
|
8676
8715
|
|
|
8677
8716
|
if isinstance(data, list) and len(data) > 0:
|
|
8678
|
-
|
|
8679
|
-
|
|
8717
|
+
_safe_print(f"{'Bar':<20} | {y_field}")
|
|
8718
|
+
_safe_print("-" * 60)
|
|
8680
8719
|
for item in data[:20]:
|
|
8681
8720
|
x_val = item.get(x_field, "")
|
|
8682
8721
|
y_val = item.get(y_field, 0)
|
|
8683
|
-
|
|
8722
|
+
_safe_print(f"{str(x_val):<20} | {self._format_value(y_val)}")
|
|
8684
8723
|
elif isinstance(data, dict):
|
|
8685
|
-
|
|
8686
|
-
|
|
8724
|
+
_safe_print(f"{'Key':<20} | {'Value'}")
|
|
8725
|
+
_safe_print("-" * 60)
|
|
8687
8726
|
for k, v in list(data.items())[:20]:
|
|
8688
|
-
|
|
8727
|
+
_safe_print(f"{str(k):<20} | {self._format_value(v)}")
|
|
8689
8728
|
|
|
8690
|
-
|
|
8729
|
+
_safe_print("=" * 60 + "\n")
|
|
8691
8730
|
output_data["data"] = data
|
|
8692
8731
|
output_data["x_field"] = x_field
|
|
8693
8732
|
output_data["y_field"] = y_field
|
|
@@ -18203,7 +18242,7 @@ class WorkflowJob:
|
|
|
18203
18242
|
except asyncio.CancelledError:
|
|
18204
18243
|
self.status = "cancelled"
|
|
18205
18244
|
logger.info(f"Job {self.job_id} cancelled")
|
|
18206
|
-
|
|
18245
|
+
_safe_print(f"[CANCELLED] Job {self.job_id} cancelled")
|
|
18207
18246
|
await self.context.notify_job_state("cancelled", self.stats)
|
|
18208
18247
|
except Exception as e:
|
|
18209
18248
|
self.status = "failed"
|
|
@@ -18220,9 +18259,17 @@ class WorkflowJob:
|
|
|
18220
18259
|
}
|
|
18221
18260
|
self.context.log("error", error_msg)
|
|
18222
18261
|
logger.exception(f"Job {self.job_id} failed: {e}")
|
|
18223
|
-
|
|
18262
|
+
# ASCII-only marker: this is the failure-reporting path, so it must
|
|
18263
|
+
# survive a console that could not render the text that failed.
|
|
18264
|
+
_safe_print(f"[FAILED] Job {self.job_id} failed: {e}")
|
|
18224
18265
|
import traceback
|
|
18225
|
-
|
|
18266
|
+
try:
|
|
18267
|
+
# print_exc renders the offending source line, which may itself
|
|
18268
|
+
# hold the character that could not be encoded — so it can raise
|
|
18269
|
+
# the very error it is reporting.
|
|
18270
|
+
traceback.print_exc()
|
|
18271
|
+
except Exception:
|
|
18272
|
+
logger.debug("traceback output dropped", exc_info=True)
|
|
18226
18273
|
await self.context.notify_job_state("failed", self.stats)
|
|
18227
18274
|
|
|
18228
18275
|
# WORKFLOW_FAILED notification
|
|
@@ -18296,7 +18343,7 @@ class WorkflowJob:
|
|
|
18296
18343
|
skip_nodes: 복구 모드에서 이미 완료된 노드 ID 집합 (스킵)
|
|
18297
18344
|
"""
|
|
18298
18345
|
skip_nodes = skip_nodes or set()
|
|
18299
|
-
|
|
18346
|
+
_safe_print(f"🔄 Executing main flow: {self.workflow.execution_order}")
|
|
18300
18347
|
|
|
18301
18348
|
# === Item-based execution setup ===
|
|
18302
18349
|
# Find Split/Aggregate pairs and their branch nodes
|
|
@@ -18336,7 +18383,7 @@ class WorkflowJob:
|
|
|
18336
18383
|
|
|
18337
18384
|
# === Checkpoint restore: 이미 완료된 노드 스킵 ===
|
|
18338
18385
|
if node_id in skip_nodes:
|
|
18339
|
-
|
|
18386
|
+
_safe_print(f" ⏭ Skipping restored node: {node_id} ({node.node_type})")
|
|
18340
18387
|
self._completed_node_ids.add(node_id)
|
|
18341
18388
|
await self.context.notify_node_state(
|
|
18342
18389
|
node_id=node_id,
|
|
@@ -18347,7 +18394,7 @@ class WorkflowJob:
|
|
|
18347
18394
|
|
|
18348
18395
|
# === Item-based execution: Skip branch nodes (handled by SplitNode) ===
|
|
18349
18396
|
if node_id in branch_nodes and node.node_type not in ("SplitNode", "AggregateNode"):
|
|
18350
|
-
|
|
18397
|
+
_safe_print(f" ⏭ Skipping branch node: {node_id} (handled by SplitNode)")
|
|
18351
18398
|
continue
|
|
18352
18399
|
|
|
18353
18400
|
# === Item-based execution: Handle SplitNode specially ===
|
|
@@ -18358,12 +18405,12 @@ class WorkflowJob:
|
|
|
18358
18405
|
|
|
18359
18406
|
# === Item-based execution: Skip AggregateNode (already executed by SplitNode) ===
|
|
18360
18407
|
if node.node_type == "AggregateNode" and node_id in split_aggregate_pairs.values():
|
|
18361
|
-
|
|
18408
|
+
_safe_print(f" ⏭ Skipping AggregateNode: {node_id} (already executed by SplitNode)")
|
|
18362
18409
|
continue
|
|
18363
18410
|
|
|
18364
18411
|
# === IfNode: 비활성 브랜치 스킵 ===
|
|
18365
18412
|
if node_id in if_skipped_nodes:
|
|
18366
|
-
|
|
18413
|
+
_safe_print(f" ⏭ Skipping node: {node_id} (IfNode branch not taken)")
|
|
18367
18414
|
await self.context.notify_node_state(
|
|
18368
18415
|
node_id=node_id,
|
|
18369
18416
|
node_type=node.node_type,
|
|
@@ -18371,7 +18418,7 @@ class WorkflowJob:
|
|
|
18371
18418
|
)
|
|
18372
18419
|
continue
|
|
18373
18420
|
|
|
18374
|
-
|
|
18421
|
+
_safe_print(f" ▶ Executing node: {node_id} ({node.node_type})")
|
|
18375
18422
|
|
|
18376
18423
|
# 🆕 노드 실행 시작 알림
|
|
18377
18424
|
start_time = datetime.utcnow()
|
|
@@ -18542,7 +18589,7 @@ class WorkflowJob:
|
|
|
18542
18589
|
new_skips = self._compute_if_skip_nodes(node_id, taken)
|
|
18543
18590
|
if_skipped_nodes.update(new_skips)
|
|
18544
18591
|
if new_skips:
|
|
18545
|
-
|
|
18592
|
+
_safe_print(f" 🔀 IfNode {node_id}: branch={taken}, skipping {new_skips}")
|
|
18546
18593
|
|
|
18547
18594
|
# deep_validate: a node that *returns* a sole-`error` dict (rather
|
|
18548
18595
|
# than raising) would otherwise be stored as a COMPLETED output and
|
|
@@ -18843,7 +18890,7 @@ class WorkflowJob:
|
|
|
18843
18890
|
all_results = []
|
|
18844
18891
|
total = len(items)
|
|
18845
18892
|
|
|
18846
|
-
|
|
18893
|
+
_safe_print(f" 🔄 Auto-iterate: {node_id} ({node.node_type}) - {total} items")
|
|
18847
18894
|
|
|
18848
18895
|
for idx, current_item in enumerate(items):
|
|
18849
18896
|
# === item, index, total을 ExecutionContext에 설정 ===
|
|
@@ -18854,7 +18901,7 @@ class WorkflowJob:
|
|
|
18854
18901
|
|
|
18855
18902
|
# 진행 상황 로그
|
|
18856
18903
|
item_label = current_item.get("symbol", str(current_item)) if isinstance(current_item, dict) else str(current_item)
|
|
18857
|
-
|
|
18904
|
+
_safe_print(f" [{idx+1}/{total}] Processing: {item_label}")
|
|
18858
18905
|
self.context.log("debug", f"Auto-iterate [{idx+1}/{total}]: {item_label}", node_id)
|
|
18859
18906
|
|
|
18860
18907
|
# A-3: per-item spacing for order / external-API nodes
|
|
@@ -18887,7 +18934,7 @@ class WorkflowJob:
|
|
|
18887
18934
|
|
|
18888
18935
|
# 결과 병합: 배열 필드는 병합, 단일 필드는 마지막 값
|
|
18889
18936
|
merged = self._merge_iterate_results(all_results)
|
|
18890
|
-
|
|
18937
|
+
_safe_print(f" ✅ Auto-iterate complete: {len(all_results)} results merged")
|
|
18891
18938
|
|
|
18892
18939
|
return merged
|
|
18893
18940
|
|
|
@@ -19081,7 +19128,7 @@ class WorkflowJob:
|
|
|
19081
19128
|
"""
|
|
19082
19129
|
from programgarden_core.bases.listener import NodeState
|
|
19083
19130
|
|
|
19084
|
-
|
|
19131
|
+
_safe_print(f" 🔀 Executing SplitNode branch: {split_id}")
|
|
19085
19132
|
|
|
19086
19133
|
# Get paired AggregateNode
|
|
19087
19134
|
aggregate_id = split_aggregate_pairs.get(split_id)
|
|
@@ -19170,7 +19217,7 @@ class WorkflowJob:
|
|
|
19170
19217
|
results_by_index: Dict[int, Any] = {}
|
|
19171
19218
|
errors: List[str] = []
|
|
19172
19219
|
|
|
19173
|
-
|
|
19220
|
+
_safe_print(f" 📦 SplitNode: {total} items, parallel={parallel}, delay_ms={delay_ms}")
|
|
19174
19221
|
|
|
19175
19222
|
# === Execute branch for each item ===
|
|
19176
19223
|
if parallel:
|
|
@@ -19417,7 +19464,7 @@ class WorkflowJob:
|
|
|
19417
19464
|
if not node:
|
|
19418
19465
|
return
|
|
19419
19466
|
|
|
19420
|
-
|
|
19467
|
+
_safe_print(f" 🔗 Executing AggregateNode: {aggregate_id}")
|
|
19421
19468
|
|
|
19422
19469
|
start_time = datetime.utcnow()
|
|
19423
19470
|
await self.context.notify_node_state(
|
|
@@ -19884,24 +19931,24 @@ class WorkflowJob:
|
|
|
19884
19931
|
Follows topological order to ensure proper data flow.
|
|
19885
19932
|
"""
|
|
19886
19933
|
from datetime import datetime
|
|
19887
|
-
|
|
19934
|
+
_safe_print(f"[{datetime.now().strftime('%H:%M:%S')}] 🔄 실시간 업데이트 이벤트 수신: trigger_nodes={event.trigger_nodes}")
|
|
19888
19935
|
|
|
19889
19936
|
# trigger_nodes가 비어있으면 source_node_id에서 연결된 하위 노드 찾기
|
|
19890
19937
|
trigger_nodes = event.trigger_nodes
|
|
19891
19938
|
if not trigger_nodes and event.source_node_id:
|
|
19892
19939
|
trigger_nodes = self._find_trigger_nodes(event.source_node_id)
|
|
19893
|
-
|
|
19940
|
+
_safe_print(f" → 자동 탐색된 trigger_nodes: {trigger_nodes}")
|
|
19894
19941
|
|
|
19895
19942
|
if not trigger_nodes:
|
|
19896
|
-
|
|
19943
|
+
_safe_print(f" ⚠️ 트리거할 노드 없음, 스킵")
|
|
19897
19944
|
return
|
|
19898
19945
|
|
|
19899
19946
|
logger.debug(f"Realtime update from {event.source_node_id}, triggering: {trigger_nodes}")
|
|
19900
|
-
|
|
19947
|
+
_safe_print(f" → {event.source_node_id}에서 트리거: {trigger_nodes}")
|
|
19901
19948
|
|
|
19902
19949
|
# 🆕 트리거된 노드들 + 하위 노드들을 모두 찾아서 실행 순서대로 정렬
|
|
19903
19950
|
nodes_to_execute = self._find_downstream_nodes(trigger_nodes)
|
|
19904
|
-
|
|
19951
|
+
_safe_print(f" → 실행할 노드 체인: {nodes_to_execute}")
|
|
19905
19952
|
|
|
19906
19953
|
# Split→Aggregate 분기는 노드를 하나씩 재실행해서는 절대 채워지지 않는다.
|
|
19907
19954
|
# AggregateNode 의 유일한 입력인 node_state["_collected_items"] 를 쓰는 곳은
|
|
@@ -19918,7 +19965,7 @@ class WorkflowJob:
|
|
|
19918
19965
|
split_node = self.workflow.nodes.get(split_id)
|
|
19919
19966
|
if not split_node:
|
|
19920
19967
|
continue
|
|
19921
|
-
|
|
19968
|
+
_safe_print(f" 🔀 Re-driving split branch: {split_id} → {aggregate_id}")
|
|
19922
19969
|
await self._execute_split_branch(split_id, split_node, split_pairs, branch)
|
|
19923
19970
|
branch_driven |= branch | {split_id, aggregate_id}
|
|
19924
19971
|
|
|
@@ -19940,7 +19987,7 @@ class WorkflowJob:
|
|
|
19940
19987
|
|
|
19941
19988
|
# Re-execute the triggered node
|
|
19942
19989
|
try:
|
|
19943
|
-
|
|
19990
|
+
_safe_print(f" ▶ Re-executing: {node_id} ({node.node_type})")
|
|
19944
19991
|
|
|
19945
19992
|
# 소스 노드 ID를 config에 추가하여 최신 데이터 참조 가능하게
|
|
19946
19993
|
config_with_source = dict(node.config)
|
|
@@ -20120,7 +20167,17 @@ class WorkflowJob:
|
|
|
20120
20167
|
await self._task
|
|
20121
20168
|
except asyncio.CancelledError:
|
|
20122
20169
|
pass
|
|
20123
|
-
|
|
20170
|
+
|
|
20171
|
+
# Cleanup persistent trackers / flow-end nodes / broker·JIF 구독.
|
|
20172
|
+
# 여태 stop() 은 이 정리를 빼먹어(cancel() 에만 있었다) 잡이 stop 으로 끝나면
|
|
20173
|
+
# 계좌 추적기의 주기 폴링 루프가 취소되지 않고 무한히 LS 를 계속 조회했다(레이트
|
|
20174
|
+
# 리밋·불필요 호출 유발). cleanup_* 는 끝에서 딕셔너리를 비우므로 _run 의 정상완료
|
|
20175
|
+
# 경로가 먼저 정리했더라도 여기서 다시 불려도 빈 순회 = no-op(멱등).
|
|
20176
|
+
await self._cleanup_broker_fill_subscriptions()
|
|
20177
|
+
await self._cleanup_jif_subscriptions()
|
|
20178
|
+
await self.context.cleanup_persistent_nodes()
|
|
20179
|
+
await self.context.cleanup_flow_end_nodes()
|
|
20180
|
+
|
|
20124
20181
|
# Cleanup RiskTracker flush
|
|
20125
20182
|
if self.context.risk_tracker:
|
|
20126
20183
|
try:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden-1.29.3 → programgarden-1.29.4}/programgarden/database/workflow_position_tracker.py
RENAMED
|
File without changes
|
{programgarden-1.29.3 → programgarden-1.29.4}/programgarden/database/workflow_risk_tracker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|