programgarden 1.29.1__tar.gz → 1.29.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.
- {programgarden-1.29.1 → programgarden-1.29.2}/PKG-INFO +1 -1
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/executor.py +75 -21
- {programgarden-1.29.1 → programgarden-1.29.2}/pyproject.toml +1 -1
- {programgarden-1.29.1 → programgarden-1.29.2}/README.md +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/__init__.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/binding_validator.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/client.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/code_worker.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/context.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/database/__init__.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/database/checkpoint_manager.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/database/query_builder.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/deep_fixtures.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/node_runner.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/plugin/__init__.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/providers/__init__.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/reconnect_handler.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/resolver.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/resource/__init__.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/resource/context.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/resource/limiter.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/resource/monitor.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/resource/throttle.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/semantic_rules.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/tools/__init__.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/tools/event_tools.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/tools/job_tools.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-1.29.1 → programgarden-1.29.2}/programgarden/validation_recommender.py +0 -0
|
@@ -1040,6 +1040,10 @@ _SYMBOL_ITEM_KEYS = ("symbol", "shcode", "code")
|
|
|
1040
1040
|
# 가격 대신 종목코드 목록이 표에 실렸다(비결정 + 잘못된 데이터).
|
|
1041
1041
|
_BRANCH_PAYLOAD_PORTS = ("result", "value", "data", "ohlcv_data")
|
|
1042
1042
|
|
|
1043
|
+
# 실시간 노드가 어떤 분기 스코프(종목)까지 구독을 걸었는지 기록하는 node_state 키.
|
|
1044
|
+
# 종목마다 최초 1회만 실행하고, 틱마다의 분기 재구동에선 건너뛴다(재구독 폭주 방지).
|
|
1045
|
+
_SUBSCRIBED_SCOPES_KEY = "_subscribed_branch_scopes"
|
|
1046
|
+
|
|
1043
1047
|
|
|
1044
1048
|
def _item_symbol(item: Any) -> Optional[str]:
|
|
1045
1049
|
"""Split 아이템에서 종목코드를 뽑는다 ('005930' 또는 {'symbol': '005930'})."""
|
|
@@ -1053,21 +1057,50 @@ def _item_symbol(item: Any) -> Optional[str]:
|
|
|
1053
1057
|
return None
|
|
1054
1058
|
|
|
1055
1059
|
|
|
1060
|
+
def _is_symbol_keyed_payload(value: Any) -> bool:
|
|
1061
|
+
"""`{"005930": [bar], "000660": [bar]}` 처럼 종목코드로 키가 잡힌 시세 payload 인가."""
|
|
1062
|
+
return (
|
|
1063
|
+
isinstance(value, dict)
|
|
1064
|
+
and bool(value)
|
|
1065
|
+
and all(isinstance(v, list) for v in value.values())
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
|
|
1056
1069
|
def _slice_realtime_outputs(outputs: Dict[str, Any], symbol: str) -> Dict[str, Any]:
|
|
1057
|
-
"""실시간 시세 노드의 합쳐진 출력을 한 종목 몫으로 자른다.
|
|
1058
|
-
|
|
1070
|
+
"""실시간 시세 노드의 합쳐진 출력을 한 종목 몫으로 자른다.
|
|
1071
|
+
|
|
1072
|
+
**이 종목의 틱이 아직 없으면 공개 포트를 하나도 내지 않는다**(내부 `_` 포트만 남긴다).
|
|
1073
|
+
빈 값이나 `symbols` 목록만 내보내면 Throttle 이 "흘릴 데이터가 있다"고 착각해 통과시키고,
|
|
1074
|
+
표에 **가격 없는 행**이 낀다. 실제로 HKEX 라이브에서 아직 체결이 없던 2번째 종목이
|
|
1075
|
+
합쳐진 덩어리를 그대로 행으로 실어 이 결함이 드러났다(2026-07-14).
|
|
1076
|
+
"""
|
|
1077
|
+
meta = {
|
|
1078
|
+
k: v for k, v in outputs.items()
|
|
1079
|
+
if isinstance(k, str) and k.startswith("_")
|
|
1080
|
+
}
|
|
1081
|
+
payload: Dict[str, Any] = {}
|
|
1082
|
+
has_payload = False
|
|
1083
|
+
|
|
1059
1084
|
for port, value in outputs.items():
|
|
1085
|
+
if isinstance(port, str) and port.startswith("_"):
|
|
1086
|
+
continue
|
|
1087
|
+
if _is_symbol_keyed_payload(value):
|
|
1088
|
+
if symbol in value:
|
|
1089
|
+
payload[port] = value[symbol]
|
|
1090
|
+
has_payload = True
|
|
1091
|
+
# 이 종목 몫이 없으면 그 포트는 **아예 내지 않는다** (덩어리 통과 금지)
|
|
1092
|
+
continue
|
|
1060
1093
|
if port == "symbol":
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
return
|
|
1094
|
+
payload[port] = symbol
|
|
1095
|
+
continue
|
|
1096
|
+
if port == "symbols" and isinstance(value, list):
|
|
1097
|
+
payload[port] = [s for s in value if _item_symbol(s) == symbol]
|
|
1098
|
+
continue
|
|
1099
|
+
payload[port] = value
|
|
1100
|
+
|
|
1101
|
+
if not has_payload:
|
|
1102
|
+
return meta
|
|
1103
|
+
return {**meta, **payload}
|
|
1071
1104
|
|
|
1072
1105
|
|
|
1073
1106
|
def _realtime_branch_row(public: Dict[str, Any], symbol: Optional[str]) -> Optional[Dict[str, Any]]:
|
|
@@ -18857,12 +18890,22 @@ class WorkflowJob:
|
|
|
18857
18890
|
if not node:
|
|
18858
18891
|
continue
|
|
18859
18892
|
|
|
18860
|
-
|
|
18861
|
-
|
|
18862
|
-
|
|
18863
|
-
|
|
18864
|
-
|
|
18865
|
-
|
|
18893
|
+
is_realtime = node.node_type in REALTIME_NODE_TYPES
|
|
18894
|
+
subscribed: List[str] = []
|
|
18895
|
+
if is_realtime:
|
|
18896
|
+
# 실시간 노드는 **분기 아이템(종목)마다 최초 1회는 반드시 실행**해야 한다
|
|
18897
|
+
# — 그래야 그 종목의 구독이 걸린다. 그 뒤(틱마다의 분기 재구동)엔 실행하지
|
|
18898
|
+
# 않는다: 재실행하면 틱마다 재구독이 걸려 소켓이 폭주한다. 최신 시세는 틱
|
|
18899
|
+
# 콜백이 이 노드의 context 출력에 계속 갱신해 두므로 하류는 그걸 읽으면 된다.
|
|
18900
|
+
#
|
|
18901
|
+
# ⚠️ 예전엔 "출력이 있으면 스킵"으로 판단했는데, 첫 종목이 실행되며 출력을
|
|
18902
|
+
# 남기는 순간 **2번째 이후 종목의 최초 구독까지 건너뛰어** 그 종목엔 시세가
|
|
18903
|
+
# 영영 오지 않았다(HKEX 라이브에서 2번째 종목이 통째로 死, 2026-07-14).
|
|
18904
|
+
subscribed = list(
|
|
18905
|
+
self.context.get_node_state(node_id, _SUBSCRIBED_SCOPES_KEY) or []
|
|
18906
|
+
)
|
|
18907
|
+
if branch_scope in subscribed:
|
|
18908
|
+
continue
|
|
18866
18909
|
|
|
18867
18910
|
# Prepare config
|
|
18868
18911
|
config = dict(node.config)
|
|
@@ -18901,15 +18944,26 @@ class WorkflowJob:
|
|
|
18901
18944
|
for port_name, value in outputs.items():
|
|
18902
18945
|
self.context.set_output(node_id, port_name, value)
|
|
18903
18946
|
|
|
18947
|
+
if is_realtime:
|
|
18948
|
+
# 이 종목 구독 완료 — 다음 재구동부터는 이 노드를 건너뛴다
|
|
18949
|
+
subscribed.append(branch_scope)
|
|
18950
|
+
self.context.set_node_state(node_id, _SUBSCRIBED_SCOPES_KEY, subscribed)
|
|
18951
|
+
|
|
18904
18952
|
# Track last result — only from PUBLIC ports. Internal meta
|
|
18905
18953
|
# (_throttle_stats 등)이 유일 출력일 때 그걸 결과로 집으면 Aggregate/Display
|
|
18906
18954
|
# 가 내부 통계를 데이터로 렌더한다.
|
|
18907
18955
|
public = _public_outputs(outputs) if outputs else {}
|
|
18908
18956
|
last_had_public = bool(public)
|
|
18909
18957
|
if public:
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
18958
|
+
if realtime_branch:
|
|
18959
|
+
row = _realtime_branch_row(public, symbol)
|
|
18960
|
+
if row is None:
|
|
18961
|
+
# 실시간 분기인데 가격 payload 가 없다(예: symbols 목록만 남음)
|
|
18962
|
+
# → 이 아이템은 표에 기여할 실데이터가 없다 → skip.
|
|
18963
|
+
# 그러지 않으면 가격 없는 행이 '실시간 체결가' 표에 낀다.
|
|
18964
|
+
last_had_public = False
|
|
18965
|
+
else:
|
|
18966
|
+
result = row
|
|
18913
18967
|
else:
|
|
18914
18968
|
result = next(
|
|
18915
18969
|
(public[p] for p in _BRANCH_PAYLOAD_PORTS if public.get(p) is not None),
|
|
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.1 → programgarden-1.29.2}/programgarden/database/workflow_position_tracker.py
RENAMED
|
File without changes
|
{programgarden-1.29.1 → programgarden-1.29.2}/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
|