programgarden-core 1.12.2__tar.gz → 1.12.3__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_core-1.12.2 → programgarden_core-1.12.3}/PKG-INFO +1 -1
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/__init__.py +15 -1
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/__init__.py +29 -0
- programgarden_core-1.12.3/programgarden_core/models/validation.py +326 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/workflow.py +89 -22
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/symbol.py +54 -1
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/pyproject.toml +1 -1
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/README.md +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/bases/__init__.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/bases/client.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/bases/components.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/bases/listener.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/bases/mixins.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/bases/products.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/bases/sql.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/bases/storage.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/exceptions.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/expression/__init__.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/expression/evaluator.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/i18n/__init__.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/i18n/locales/en.json +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/i18n/locales/ko.json +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/i18n/translator.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/korea_alias.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/connection_rule.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/credential.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/edge.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/event.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/exchange.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/field_binding.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/job.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/plugin_resource.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/resilience.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/resource.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/__init__.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/account_futures.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/account_korea_stock.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/account_stock.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/ai.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/backtest.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/backtest_futures.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/backtest_korea_stock.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/backtest_stock.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/base.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/broker.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/calculation.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/condition.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/data.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/data_futures.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/data_korea_stock.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/data_stock.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/display.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/event.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/fundamental_korea_stock.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/fundamental_stock.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/infra.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/market_external.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/market_status.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/open_orders_futures.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/open_orders_korea_stock.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/open_orders_stock.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/order.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/portfolio.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/realtime_futures.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/realtime_korea_stock.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/realtime_stock.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/risk.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/symbol_futures.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/symbol_korea_stock.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/symbol_stock.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/trigger.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/presets/__init__.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/presets/news_analyst.json +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/presets/risk_manager.json +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/presets/strategist.json +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/presets/technical_analyst.json +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/registry/__init__.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/registry/credential_registry.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/registry/dynamic_node_registry.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/registry/node_registry.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/registry/plugin_registry.py +0 -0
- {programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/retry_executor.py +0 -0
|
@@ -24,7 +24,7 @@ from programgarden_core.exceptions import *
|
|
|
24
24
|
from programgarden_core import korea_alias
|
|
25
25
|
from programgarden_core import bases
|
|
26
26
|
|
|
27
|
-
__version__ = "
|
|
27
|
+
__version__ = "1.12.3"
|
|
28
28
|
__all__ = [
|
|
29
29
|
# Nodes - Base
|
|
30
30
|
"BaseNode",
|
|
@@ -96,6 +96,20 @@ __all__ = [
|
|
|
96
96
|
"JobState",
|
|
97
97
|
"BrokerCredential",
|
|
98
98
|
"Event",
|
|
99
|
+
# Validation (structured errors + recommendations)
|
|
100
|
+
"ErrorSeverity",
|
|
101
|
+
"ErrorCode",
|
|
102
|
+
"ErrorLocation",
|
|
103
|
+
"ErrorInfo",
|
|
104
|
+
"RecommendationCategory",
|
|
105
|
+
"Recommendation",
|
|
106
|
+
"ValidationLimits",
|
|
107
|
+
"ResultSummary",
|
|
108
|
+
"ValidationResult",
|
|
109
|
+
"CASCADE_SUPPRESSION_RULES",
|
|
110
|
+
"default_severity_for",
|
|
111
|
+
"suggest_close_match",
|
|
112
|
+
"build_error",
|
|
99
113
|
# Registry
|
|
100
114
|
"NodeTypeRegistry",
|
|
101
115
|
"PluginRegistry",
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/__init__.py
RENAMED
|
@@ -78,6 +78,21 @@ from programgarden_core.models.connection_rule import (
|
|
|
78
78
|
ConnectionRule,
|
|
79
79
|
RateLimitConfig,
|
|
80
80
|
)
|
|
81
|
+
from programgarden_core.models.validation import (
|
|
82
|
+
ErrorSeverity,
|
|
83
|
+
ErrorCode,
|
|
84
|
+
ErrorLocation,
|
|
85
|
+
ErrorInfo,
|
|
86
|
+
RecommendationCategory,
|
|
87
|
+
Recommendation,
|
|
88
|
+
ValidationLimits,
|
|
89
|
+
ResultSummary,
|
|
90
|
+
ValidationResult,
|
|
91
|
+
CASCADE_SUPPRESSION_RULES,
|
|
92
|
+
default_severity_for,
|
|
93
|
+
suggest_close_match,
|
|
94
|
+
build_error,
|
|
95
|
+
)
|
|
81
96
|
|
|
82
97
|
__all__ = [
|
|
83
98
|
# Edge
|
|
@@ -150,4 +165,18 @@ __all__ = [
|
|
|
150
165
|
"ConnectionSeverity",
|
|
151
166
|
"ConnectionRule",
|
|
152
167
|
"RateLimitConfig",
|
|
168
|
+
# Validation
|
|
169
|
+
"ErrorSeverity",
|
|
170
|
+
"ErrorCode",
|
|
171
|
+
"ErrorLocation",
|
|
172
|
+
"ErrorInfo",
|
|
173
|
+
"RecommendationCategory",
|
|
174
|
+
"Recommendation",
|
|
175
|
+
"ValidationLimits",
|
|
176
|
+
"ResultSummary",
|
|
177
|
+
"ValidationResult",
|
|
178
|
+
"CASCADE_SUPPRESSION_RULES",
|
|
179
|
+
"default_severity_for",
|
|
180
|
+
"suggest_close_match",
|
|
181
|
+
"build_error",
|
|
153
182
|
]
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Structured validation errors for AI-chatbot-friendly workflow validation.
|
|
2
|
+
|
|
3
|
+
ErrorInfo objects let downstream consumers (AI chatbots, IDEs, dashboards)
|
|
4
|
+
make deterministic self-correction decisions based on error codes and
|
|
5
|
+
structured location metadata instead of parsing free-form strings.
|
|
6
|
+
|
|
7
|
+
All user-facing strings in this module (and all consumers) must be English.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from difflib import get_close_matches
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ErrorSeverity(str, Enum):
|
|
19
|
+
ERROR = "error"
|
|
20
|
+
WARNING = "warning"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ErrorCode(str, Enum):
|
|
24
|
+
# Definition / structure
|
|
25
|
+
DEFINITION_PARSE_ERROR = "DEFINITION_PARSE_ERROR"
|
|
26
|
+
DUPLICATE_NODE_ID = "DUPLICATE_NODE_ID"
|
|
27
|
+
RESERVED_NODE_ID = "RESERVED_NODE_ID"
|
|
28
|
+
MISSING_START_NODE = "MISSING_START_NODE"
|
|
29
|
+
MULTIPLE_START_NODES = "MULTIPLE_START_NODES"
|
|
30
|
+
CYCLE_DETECTED = "CYCLE_DETECTED"
|
|
31
|
+
|
|
32
|
+
# Node / plugin registry
|
|
33
|
+
UNKNOWN_NODE_TYPE = "UNKNOWN_NODE_TYPE"
|
|
34
|
+
UNKNOWN_DYNAMIC_NODE_SCHEMA = "UNKNOWN_DYNAMIC_NODE_SCHEMA"
|
|
35
|
+
DYNAMIC_NODE_CREDENTIAL_FORBIDDEN = "DYNAMIC_NODE_CREDENTIAL_FORBIDDEN"
|
|
36
|
+
MISSING_PLUGIN = "MISSING_PLUGIN"
|
|
37
|
+
UNKNOWN_PLUGIN = "UNKNOWN_PLUGIN"
|
|
38
|
+
|
|
39
|
+
# Edges
|
|
40
|
+
INVALID_EDGE_REF = "INVALID_EDGE_REF"
|
|
41
|
+
INVALID_EDGE_PORT = "INVALID_EDGE_PORT"
|
|
42
|
+
|
|
43
|
+
# Expressions / fields
|
|
44
|
+
INVALID_EXPRESSION_REF = "INVALID_EXPRESSION_REF"
|
|
45
|
+
INVALID_EXPRESSION_SYNTAX = "INVALID_EXPRESSION_SYNTAX"
|
|
46
|
+
MISSING_REQUIRED_FIELD = "MISSING_REQUIRED_FIELD"
|
|
47
|
+
INVALID_FIELD_TYPE = "INVALID_FIELD_TYPE"
|
|
48
|
+
INVALID_FIELD_ENUM = "INVALID_FIELD_ENUM"
|
|
49
|
+
|
|
50
|
+
# Credentials
|
|
51
|
+
UNKNOWN_CREDENTIAL = "UNKNOWN_CREDENTIAL"
|
|
52
|
+
|
|
53
|
+
# Broker / connection rules
|
|
54
|
+
DUPLICATE_BROKER_NODE = "DUPLICATE_BROKER_NODE"
|
|
55
|
+
MISSING_REQUIRED_BROKER = "MISSING_REQUIRED_BROKER"
|
|
56
|
+
INCOMPATIBLE_BROKER_PROVIDER = "INCOMPATIBLE_BROKER_PROVIDER"
|
|
57
|
+
CONNECTION_RULE_VIOLATION = "CONNECTION_RULE_VIOLATION"
|
|
58
|
+
|
|
59
|
+
# Runtime (dry_run)
|
|
60
|
+
DRY_RUN_RUNTIME_ERROR = "DRY_RUN_RUNTIME_ERROR"
|
|
61
|
+
DRY_RUN_CREDENTIAL_MISSING = "DRY_RUN_CREDENTIAL_MISSING"
|
|
62
|
+
DRY_RUN_DEPENDENCY_FAILURE = "DRY_RUN_DEPENDENCY_FAILURE"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_DEFAULT_SEVERITY: Dict[ErrorCode, ErrorSeverity] = {
|
|
66
|
+
ErrorCode.UNKNOWN_PLUGIN: ErrorSeverity.WARNING,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def default_severity_for(code: ErrorCode) -> ErrorSeverity:
|
|
71
|
+
return _DEFAULT_SEVERITY.get(code, ErrorSeverity.ERROR)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ErrorLocation(BaseModel):
|
|
75
|
+
"""Where in the workflow definition the error occurred. All fields optional."""
|
|
76
|
+
|
|
77
|
+
node_id: Optional[str] = Field(default=None, description="Node id from definition.nodes[].id")
|
|
78
|
+
node_type: Optional[str] = Field(default=None, description="Node type (e.g. 'OverseasStockNewOrderNode')")
|
|
79
|
+
field_path: Optional[str] = Field(default=None, description="Dot/bracket notation: 'fields.period' or 'symbols[0].symbol'")
|
|
80
|
+
edge_index: Optional[int] = Field(default=None, description="Index into definition.edges[]")
|
|
81
|
+
edge_from: Optional[str] = Field(default=None, description="Edge.from raw value (may include dot port)")
|
|
82
|
+
edge_to: Optional[str] = Field(default=None, description="Edge.to raw value")
|
|
83
|
+
credential_id: Optional[str] = Field(default=None, description="Credential id from definition.credentials[]")
|
|
84
|
+
plugin_id: Optional[str] = Field(default=None, description="Plugin id referenced by ConditionNode etc.")
|
|
85
|
+
expression: Optional[str] = Field(default=None, description="Raw expression string when INVALID_EXPRESSION_*")
|
|
86
|
+
output_port: Optional[str] = Field(default=None, description="Output port name on source node")
|
|
87
|
+
|
|
88
|
+
model_config = ConfigDict(extra="forbid")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class RecommendationCategory(str, Enum):
|
|
92
|
+
DATA_FLOW = "data_flow"
|
|
93
|
+
RESILIENCE = "resilience"
|
|
94
|
+
PERFORMANCE = "performance"
|
|
95
|
+
SAFETY = "safety"
|
|
96
|
+
READABILITY = "readability"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Recommendation(BaseModel):
|
|
100
|
+
"""Non-blocking quality-improvement hint for AI chatbots and users.
|
|
101
|
+
|
|
102
|
+
Recommendations are deterministic, rule-based suggestions — never prescribe
|
|
103
|
+
a single fixed value. Always present options with rationale so the
|
|
104
|
+
consumer (chatbot or human) can pick the right one for their context.
|
|
105
|
+
|
|
106
|
+
`example_snippet` is a partial workflow fragment (one or two
|
|
107
|
+
nodes/edges) showing one applied option — not a full workflow JSON. The
|
|
108
|
+
chatbot is expected to merge it into the user's existing workflow.
|
|
109
|
+
"""
|
|
110
|
+
title: str = Field(description="Short one-line hint (English). Use 'Consider X' tone.")
|
|
111
|
+
rationale: str = Field(description="Why this may improve the workflow — 1~2 sentences.")
|
|
112
|
+
category: RecommendationCategory
|
|
113
|
+
location: Optional[ErrorLocation] = Field(default=None, description="Optional anchor point in the workflow")
|
|
114
|
+
options: List[str] = Field(default_factory=list, description="2+ alternative approaches the user/AI may pick from")
|
|
115
|
+
example_snippet: Optional[Dict[str, Any]] = Field(
|
|
116
|
+
default=None,
|
|
117
|
+
description="Partial workflow JSON showing one applied option (node/edge fragment only — not a full workflow)",
|
|
118
|
+
)
|
|
119
|
+
rule_id: str = Field(description="Deterministic rule identifier for testing / suppression (e.g. 'REC_REALTIME_THROTTLE')")
|
|
120
|
+
|
|
121
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
122
|
+
|
|
123
|
+
def short(self) -> str:
|
|
124
|
+
return f"[{self.rule_id}] {self.title} — {self.rationale}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ErrorInfo(BaseModel):
|
|
128
|
+
code: ErrorCode
|
|
129
|
+
severity: ErrorSeverity = Field(default=ErrorSeverity.ERROR)
|
|
130
|
+
location: ErrorLocation = Field(default_factory=ErrorLocation)
|
|
131
|
+
message: str = Field(description="Human-readable English message. One sentence preferred.")
|
|
132
|
+
suggestion: Optional[str] = Field(default=None, description="Single-line English remediation hint.")
|
|
133
|
+
available_values: Optional[List[str]] = Field(
|
|
134
|
+
default=None,
|
|
135
|
+
description="Closest valid candidates for enum/registry-style errors (suggest_close_match result).",
|
|
136
|
+
)
|
|
137
|
+
docs_url: Optional[str] = Field(default=None, description="Optional anchor link to documentation.")
|
|
138
|
+
details: Dict[str, Any] = Field(default_factory=dict, description="Free-form structured context.")
|
|
139
|
+
recommendations: List[Recommendation] = Field(
|
|
140
|
+
default_factory=list,
|
|
141
|
+
description="Optional improvement hints worth considering while fixing this error.",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
145
|
+
|
|
146
|
+
def short(self) -> str:
|
|
147
|
+
loc_parts: List[str] = []
|
|
148
|
+
if self.location.node_id:
|
|
149
|
+
loc_parts.append(f"node={self.location.node_id}")
|
|
150
|
+
if self.location.field_path:
|
|
151
|
+
loc_parts.append(f"field={self.location.field_path}")
|
|
152
|
+
if self.location.edge_index is not None:
|
|
153
|
+
loc_parts.append(f"edge[{self.location.edge_index}]")
|
|
154
|
+
suffix = f" ({', '.join(loc_parts)})" if loc_parts else ""
|
|
155
|
+
return f"[{self.code}] {self.message}{suffix}"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ValidationLimits(BaseModel):
|
|
159
|
+
"""Output volume limits to prevent overwhelming LLM context."""
|
|
160
|
+
|
|
161
|
+
max_errors: int = Field(default=20, ge=1)
|
|
162
|
+
max_warnings: int = Field(default=10, ge=1)
|
|
163
|
+
max_recommendations_per_channel: int = Field(default=10, ge=1)
|
|
164
|
+
max_per_node: int = Field(default=3, ge=1)
|
|
165
|
+
|
|
166
|
+
model_config = ConfigDict(extra="forbid")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class ResultSummary(BaseModel):
|
|
170
|
+
"""Top-level summary for fast LLM triage of a ValidationResult."""
|
|
171
|
+
|
|
172
|
+
is_valid: bool
|
|
173
|
+
error_count: int = 0
|
|
174
|
+
warning_count: int = 0
|
|
175
|
+
static_recommendation_count: int = 0
|
|
176
|
+
runtime_recommendation_count: int = 0
|
|
177
|
+
critical_codes: List[ErrorCode] = Field(
|
|
178
|
+
default_factory=list,
|
|
179
|
+
description="Up to 3 most-impactful codes (cascade roots first, then frequency).",
|
|
180
|
+
)
|
|
181
|
+
root_cause_node_ids: List[str] = Field(
|
|
182
|
+
default_factory=list,
|
|
183
|
+
description="Node ids that triggered cascade suppression (fix these first).",
|
|
184
|
+
)
|
|
185
|
+
next_action_hint: Optional[str] = Field(
|
|
186
|
+
default=None,
|
|
187
|
+
description="Deterministic English single-line hint for the consumer's next step.",
|
|
188
|
+
)
|
|
189
|
+
truncated: bool = Field(
|
|
190
|
+
default=False,
|
|
191
|
+
description="True if any channel hit ValidationLimits and entries were dropped.",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class ValidationResult(BaseModel):
|
|
198
|
+
"""Structured validation outcome for a workflow definition.
|
|
199
|
+
|
|
200
|
+
Two recommendation channels exist:
|
|
201
|
+
- `static_recommendations`: filled by `executor.validate()` (topology
|
|
202
|
+
analysis only)
|
|
203
|
+
- `runtime_recommendations`: filled by `executor.execute(dry_run=True)`
|
|
204
|
+
based on actual node mock outputs (e.g. REC_EMPTY_SYMBOL_LIST)
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
errors: List[ErrorInfo] = Field(default_factory=list)
|
|
208
|
+
warnings: List[ErrorInfo] = Field(default_factory=list)
|
|
209
|
+
static_recommendations: List[Recommendation] = Field(default_factory=list)
|
|
210
|
+
runtime_recommendations: List[Recommendation] = Field(default_factory=list)
|
|
211
|
+
summary: Optional[ResultSummary] = Field(
|
|
212
|
+
default=None,
|
|
213
|
+
description="Populated after post-processing (cascade suppression + capping). None until finalize() runs.",
|
|
214
|
+
)
|
|
215
|
+
truncated: Dict[str, int] = Field(
|
|
216
|
+
default_factory=dict,
|
|
217
|
+
description="Per-channel count of entries dropped by ValidationLimits (e.g. {'errors': 5}).",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def is_valid(self) -> bool:
|
|
224
|
+
return not self.errors
|
|
225
|
+
|
|
226
|
+
def add(self, info: ErrorInfo) -> None:
|
|
227
|
+
"""Append an ErrorInfo into errors/warnings by severity."""
|
|
228
|
+
if info.severity == ErrorSeverity.WARNING or info.severity == ErrorSeverity.WARNING.value:
|
|
229
|
+
self.warnings.append(info)
|
|
230
|
+
else:
|
|
231
|
+
self.errors.append(info)
|
|
232
|
+
|
|
233
|
+
def add_static_recommendation(self, rec: Recommendation) -> None:
|
|
234
|
+
self.static_recommendations.append(rec)
|
|
235
|
+
|
|
236
|
+
def add_runtime_recommendation(self, rec: Recommendation) -> None:
|
|
237
|
+
self.runtime_recommendations.append(rec)
|
|
238
|
+
|
|
239
|
+
def all_recommendations(self) -> List[Recommendation]:
|
|
240
|
+
"""Merge static + runtime channels (read-only helper for consumers that don't distinguish stages)."""
|
|
241
|
+
return [*self.static_recommendations, *self.runtime_recommendations]
|
|
242
|
+
|
|
243
|
+
def as_strings(self) -> List[str]:
|
|
244
|
+
"""Human-readable one-line per entry. Not a legacy compat shim — English-only."""
|
|
245
|
+
lines: List[str] = []
|
|
246
|
+
lines.extend(e.short() for e in self.errors)
|
|
247
|
+
lines.extend(w.short() for w in self.warnings)
|
|
248
|
+
lines.extend(r.short() for r in self.all_recommendations())
|
|
249
|
+
return lines
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def suggest_close_match(
|
|
253
|
+
value: str,
|
|
254
|
+
candidates: Iterable[str],
|
|
255
|
+
cutoff: float = 0.6,
|
|
256
|
+
n: int = 3,
|
|
257
|
+
) -> Optional[List[str]]:
|
|
258
|
+
"""Return up to `n` closest matches above `cutoff`, or None if there are no matches.
|
|
259
|
+
|
|
260
|
+
Thin wrapper over `difflib.get_close_matches` with standardised defaults
|
|
261
|
+
for ErrorInfo.available_values.
|
|
262
|
+
"""
|
|
263
|
+
matches = get_close_matches(value, list(candidates), n=n, cutoff=cutoff)
|
|
264
|
+
return matches or None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def build_error(
|
|
268
|
+
code: ErrorCode,
|
|
269
|
+
message: str,
|
|
270
|
+
*,
|
|
271
|
+
location: Optional[ErrorLocation] = None,
|
|
272
|
+
suggestion: Optional[str] = None,
|
|
273
|
+
available_values: Optional[List[str]] = None,
|
|
274
|
+
details: Optional[Dict[str, Any]] = None,
|
|
275
|
+
severity: Optional[ErrorSeverity] = None,
|
|
276
|
+
recommendations: Optional[List[Recommendation]] = None,
|
|
277
|
+
) -> ErrorInfo:
|
|
278
|
+
"""DRY helper for resolver.py and dry_run capture sites."""
|
|
279
|
+
return ErrorInfo(
|
|
280
|
+
code=code,
|
|
281
|
+
severity=severity or default_severity_for(code),
|
|
282
|
+
location=location or ErrorLocation(),
|
|
283
|
+
message=message,
|
|
284
|
+
suggestion=suggestion,
|
|
285
|
+
available_values=available_values,
|
|
286
|
+
details=details or {},
|
|
287
|
+
recommendations=recommendations or [],
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# Cascade suppression rule data — actual application lives in Phase 4 post-processing.
|
|
292
|
+
# Each entry documents which subordinate error codes a given root cause hides.
|
|
293
|
+
CASCADE_SUPPRESSION_RULES: Dict[ErrorCode, str] = {
|
|
294
|
+
ErrorCode.UNKNOWN_NODE_TYPE: (
|
|
295
|
+
"Suppresses INVALID_EXPRESSION_REF / INVALID_EDGE_REF / INVALID_EDGE_PORT "
|
|
296
|
+
"where the source/target is the unknown node id."
|
|
297
|
+
),
|
|
298
|
+
ErrorCode.MISSING_REQUIRED_BROKER: (
|
|
299
|
+
"Suppresses additional MISSING_REQUIRED_BROKER entries with the same "
|
|
300
|
+
"product_scope (keeps only the first)."
|
|
301
|
+
),
|
|
302
|
+
ErrorCode.CYCLE_DETECTED: (
|
|
303
|
+
"Suppresses any follow-on validation errors on nodes inside the cycle."
|
|
304
|
+
),
|
|
305
|
+
ErrorCode.DUPLICATE_NODE_ID: (
|
|
306
|
+
"Suppresses INVALID_EDGE_REF / INVALID_EXPRESSION_REF that reference the "
|
|
307
|
+
"duplicated id (since id resolution is ambiguous)."
|
|
308
|
+
),
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
__all__ = [
|
|
313
|
+
"ErrorSeverity",
|
|
314
|
+
"ErrorCode",
|
|
315
|
+
"ErrorLocation",
|
|
316
|
+
"ErrorInfo",
|
|
317
|
+
"RecommendationCategory",
|
|
318
|
+
"Recommendation",
|
|
319
|
+
"ValidationLimits",
|
|
320
|
+
"ResultSummary",
|
|
321
|
+
"ValidationResult",
|
|
322
|
+
"CASCADE_SUPPRESSION_RULES",
|
|
323
|
+
"default_severity_for",
|
|
324
|
+
"suggest_close_match",
|
|
325
|
+
"build_error",
|
|
326
|
+
]
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/workflow.py
RENAMED
|
@@ -206,42 +206,109 @@ class WorkflowDefinition(BaseModel):
|
|
|
206
206
|
if node.get("id") not in nodes_with_input
|
|
207
207
|
]
|
|
208
208
|
|
|
209
|
-
def validate_structure(self) -> List[
|
|
210
|
-
"""
|
|
211
|
-
워크플로우 구조 검증
|
|
209
|
+
def validate_structure(self) -> List["ErrorInfo"]:
|
|
210
|
+
"""Validate the workflow's structural invariants.
|
|
212
211
|
|
|
213
|
-
Returns
|
|
214
|
-
|
|
212
|
+
Returns a list of `ErrorInfo` objects. An empty list means no
|
|
213
|
+
structural problems were found. Callers should append the result
|
|
214
|
+
to a `ValidationResult` via `result.add(...)` for each entry.
|
|
215
215
|
"""
|
|
216
|
-
|
|
216
|
+
from programgarden_core.models.validation import (
|
|
217
|
+
ErrorCode,
|
|
218
|
+
ErrorInfo,
|
|
219
|
+
ErrorLocation,
|
|
220
|
+
build_error,
|
|
221
|
+
)
|
|
217
222
|
|
|
218
|
-
|
|
223
|
+
errors: List[ErrorInfo] = []
|
|
224
|
+
|
|
225
|
+
# 1. Duplicate node IDs
|
|
219
226
|
node_ids = self.get_node_ids()
|
|
220
227
|
if len(node_ids) != len(set(node_ids)):
|
|
221
|
-
seen = set()
|
|
222
|
-
dupes = [
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
228
|
+
seen: set = set()
|
|
229
|
+
dupes: List[str] = []
|
|
230
|
+
for nid in node_ids:
|
|
231
|
+
if nid in seen and nid not in dupes:
|
|
232
|
+
dupes.append(nid)
|
|
233
|
+
seen.add(nid)
|
|
234
|
+
for dup_id in dupes:
|
|
235
|
+
errors.append(
|
|
236
|
+
build_error(
|
|
237
|
+
ErrorCode.DUPLICATE_NODE_ID,
|
|
238
|
+
f"Duplicate node id '{dup_id}'",
|
|
239
|
+
location=ErrorLocation(node_id=dup_id),
|
|
240
|
+
suggestion="Rename one of the duplicated nodes so each id is unique.",
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# 2. Edge node references
|
|
226
245
|
node_id_set = set(node_ids)
|
|
227
|
-
for edge in self.edges:
|
|
246
|
+
for idx, edge in enumerate(self.edges):
|
|
228
247
|
if edge.from_node_id not in node_id_set:
|
|
229
|
-
errors.append(
|
|
248
|
+
errors.append(
|
|
249
|
+
build_error(
|
|
250
|
+
ErrorCode.INVALID_EDGE_REF,
|
|
251
|
+
f"Edge 'from' references non-existent node '{edge.from_node_id}'",
|
|
252
|
+
location=ErrorLocation(
|
|
253
|
+
edge_index=idx,
|
|
254
|
+
edge_from=edge.from_node_id,
|
|
255
|
+
edge_to=edge.to_node_id,
|
|
256
|
+
),
|
|
257
|
+
available_values=sorted(node_id_set),
|
|
258
|
+
)
|
|
259
|
+
)
|
|
230
260
|
if edge.to_node_id not in node_id_set:
|
|
231
|
-
errors.append(
|
|
232
|
-
|
|
233
|
-
|
|
261
|
+
errors.append(
|
|
262
|
+
build_error(
|
|
263
|
+
ErrorCode.INVALID_EDGE_REF,
|
|
264
|
+
f"Edge 'to' references non-existent node '{edge.to_node_id}'",
|
|
265
|
+
location=ErrorLocation(
|
|
266
|
+
edge_index=idx,
|
|
267
|
+
edge_from=edge.from_node_id,
|
|
268
|
+
edge_to=edge.to_node_id,
|
|
269
|
+
),
|
|
270
|
+
available_values=sorted(node_id_set),
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# 3. StartNode invariants
|
|
234
275
|
start_nodes = [n for n in self.nodes if n.get("type") == "StartNode"]
|
|
235
276
|
if not start_nodes:
|
|
236
|
-
errors.append(
|
|
277
|
+
errors.append(
|
|
278
|
+
build_error(
|
|
279
|
+
ErrorCode.MISSING_START_NODE,
|
|
280
|
+
"Workflow must contain exactly one StartNode",
|
|
281
|
+
suggestion="Add a StartNode at the entry of the main flow.",
|
|
282
|
+
)
|
|
283
|
+
)
|
|
237
284
|
elif len(start_nodes) > 1:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
285
|
+
offending_ids = [n.get("id") for n in start_nodes if n.get("id")]
|
|
286
|
+
errors.append(
|
|
287
|
+
build_error(
|
|
288
|
+
ErrorCode.MULTIPLE_START_NODES,
|
|
289
|
+
f"Workflow has {len(start_nodes)} StartNodes; only one is allowed",
|
|
290
|
+
location=ErrorLocation(
|
|
291
|
+
node_id=offending_ids[0] if offending_ids else None,
|
|
292
|
+
node_type="StartNode",
|
|
293
|
+
),
|
|
294
|
+
suggestion="Keep one StartNode and remove the others (use ScheduleNode for cron triggers).",
|
|
295
|
+
details={"start_node_ids": offending_ids},
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# 4. Cycle detection
|
|
241
300
|
cycle = self._detect_cycle()
|
|
242
301
|
if cycle:
|
|
243
302
|
cycle_path = " -> ".join(cycle)
|
|
244
|
-
errors.append(
|
|
303
|
+
errors.append(
|
|
304
|
+
build_error(
|
|
305
|
+
ErrorCode.CYCLE_DETECTED,
|
|
306
|
+
f"Cycle detected in the workflow DAG: {cycle_path}",
|
|
307
|
+
location=ErrorLocation(node_id=cycle[0] if cycle else None),
|
|
308
|
+
suggestion="Remove one of the edges in the cycle so execution order can be resolved.",
|
|
309
|
+
details={"cycle_path": cycle},
|
|
310
|
+
)
|
|
311
|
+
)
|
|
245
312
|
|
|
246
313
|
return errors
|
|
247
314
|
|
|
@@ -400,6 +400,14 @@ class ScreenerNode(BaseNode):
|
|
|
400
400
|
default=None,
|
|
401
401
|
description="최소 평균 거래량 (주). 예: 1000000 = 100만주",
|
|
402
402
|
)
|
|
403
|
+
price_min: Optional[float] = Field(
|
|
404
|
+
default=None,
|
|
405
|
+
description="최소 주가 ($). 예: 1.0 = $1 이상. 동전주 발굴 등 가격대 필터링에 사용",
|
|
406
|
+
)
|
|
407
|
+
price_max: Optional[float] = Field(
|
|
408
|
+
default=None,
|
|
409
|
+
description="최대 주가 ($). 예: 5.0 = $5 이하. 동전주 발굴 등 가격대 필터링에 사용",
|
|
410
|
+
)
|
|
403
411
|
sector: Optional[str] = Field(
|
|
404
412
|
default=None,
|
|
405
413
|
description="섹터 필터 (Technology, Healthcare, Finance 등)",
|
|
@@ -409,10 +417,15 @@ class ScreenerNode(BaseNode):
|
|
|
409
417
|
description="거래소 필터 (NASDAQ, NYSE, AMEX)",
|
|
410
418
|
)
|
|
411
419
|
max_results: int = Field(
|
|
412
|
-
default=100,
|
|
420
|
+
default=100,
|
|
413
421
|
description="최대 결과 수"
|
|
414
422
|
)
|
|
415
423
|
|
|
424
|
+
data_source: Literal["auto", "ls", "yfinance"] = Field(
|
|
425
|
+
default="auto",
|
|
426
|
+
description="자료 가져올 곳. auto=브로커 연결 시 LS증권, 아니면 Yahoo Finance. ls=LS증권 강제. yfinance=Yahoo Finance 강제.",
|
|
427
|
+
)
|
|
428
|
+
|
|
416
429
|
_inputs: List[InputPort] = [
|
|
417
430
|
InputPort(
|
|
418
431
|
name="symbols",
|
|
@@ -582,6 +595,29 @@ class ScreenerNode(BaseNode):
|
|
|
582
595
|
placeholder="예: 1000000 (100만주)",
|
|
583
596
|
expected_type="int",
|
|
584
597
|
),
|
|
598
|
+
# === PARAMETERS: 가격 필터 ===
|
|
599
|
+
"price_min": FieldSchema(
|
|
600
|
+
name="price_min",
|
|
601
|
+
type=FieldType.NUMBER,
|
|
602
|
+
description="최소 주가 (달러). 동전주 발굴이나 저가주 필터링에 사용하세요. 예: 1.0 = $1 이상.",
|
|
603
|
+
required=False,
|
|
604
|
+
category=FieldCategory.PARAMETERS,
|
|
605
|
+
expression_mode=ExpressionMode.FIXED_ONLY,
|
|
606
|
+
example=1.0,
|
|
607
|
+
placeholder="예: 1.0 ($1 이상)",
|
|
608
|
+
expected_type="float",
|
|
609
|
+
),
|
|
610
|
+
"price_max": FieldSchema(
|
|
611
|
+
name="price_max",
|
|
612
|
+
type=FieldType.NUMBER,
|
|
613
|
+
description="최대 주가 (달러). 고가주 제외나 동전주 상한선 설정에 사용하세요. 예: 5.0 = $5 이하.",
|
|
614
|
+
required=False,
|
|
615
|
+
category=FieldCategory.PARAMETERS,
|
|
616
|
+
expression_mode=ExpressionMode.FIXED_ONLY,
|
|
617
|
+
example=5.0,
|
|
618
|
+
placeholder="예: 5.0 ($5 이하)",
|
|
619
|
+
expected_type="float",
|
|
620
|
+
),
|
|
585
621
|
# === PARAMETERS: 섹터/거래소 필터 ===
|
|
586
622
|
"sector": FieldSchema(
|
|
587
623
|
name="sector",
|
|
@@ -625,6 +661,23 @@ class ScreenerNode(BaseNode):
|
|
|
625
661
|
example="NASDAQ",
|
|
626
662
|
expected_type="str",
|
|
627
663
|
),
|
|
664
|
+
# === PARAMETERS: 자료 가져올 곳 ===
|
|
665
|
+
"data_source": FieldSchema(
|
|
666
|
+
name="data_source",
|
|
667
|
+
type=FieldType.ENUM,
|
|
668
|
+
description="가격/시가총액/거래량 정보를 어디에서 받아올지 선택하세요. '자동'을 권장합니다.",
|
|
669
|
+
default="auto",
|
|
670
|
+
enum_values=["auto", "ls", "yfinance"],
|
|
671
|
+
enum_labels={
|
|
672
|
+
"auto": "자동 (브로커 연결 시 LS증권 사용, 없으면 Yahoo Finance)",
|
|
673
|
+
"ls": "LS증권 (빠름, 브로커 연결 필요)",
|
|
674
|
+
"yfinance": "Yahoo Finance (외부 API)",
|
|
675
|
+
},
|
|
676
|
+
category=FieldCategory.PARAMETERS,
|
|
677
|
+
expression_mode=ExpressionMode.FIXED_ONLY,
|
|
678
|
+
example="auto",
|
|
679
|
+
expected_type="str",
|
|
680
|
+
),
|
|
628
681
|
# === SETTINGS: 결과 제한 ===
|
|
629
682
|
"max_results": FieldSchema(
|
|
630
683
|
name="max_results",
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/bases/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/bases/components.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/bases/listener.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/bases/products.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/expression/__init__.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/expression/evaluator.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/i18n/locales/en.json
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/i18n/locales/ko.json
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/i18n/translator.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/connection_rule.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/credential.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/exchange.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/field_binding.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/plugin_resource.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/resilience.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/models/resource.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/__init__.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/account_futures.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/account_stock.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/backtest.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/backtest_futures.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/backtest_stock.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/calculation.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/condition.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/data_futures.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/data_korea_stock.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/data_stock.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/market_external.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/market_status.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/portfolio.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/realtime_futures.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/realtime_stock.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/symbol_futures.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/nodes/symbol_stock.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/presets/__init__.py
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/presets/news_analyst.json
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/presets/risk_manager.json
RENAMED
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/presets/strategist.json
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/registry/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/registry/node_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
{programgarden_core-1.12.2 → programgarden_core-1.12.3}/programgarden_core/retry_executor.py
RENAMED
|
File without changes
|