programgarden 1.22.0__tar.gz → 1.22.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.22.0 → programgarden-1.22.2}/PKG-INFO +4 -4
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/context.py +66 -2
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/database/checkpoint_manager.py +68 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/executor.py +435 -12
- programgarden-1.22.2/programgarden/reconnect_handler.py +288 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/resolver.py +68 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/pyproject.toml +9 -4
- programgarden-1.22.0/programgarden/reconnect_handler.py +0 -176
- {programgarden-1.22.0 → programgarden-1.22.2}/README.md +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/__init__.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/binding_validator.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/client.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/database/__init__.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/database/query_builder.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/database/workflow_position_tracker.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/database/workflow_risk_tracker.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/node_runner.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/plugin/__init__.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/plugin/sandbox.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/providers/__init__.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/providers/llm_errors.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/providers/llm_provider.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/resource/__init__.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/resource/context.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/resource/limiter.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/resource/monitor.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/resource/throttle.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/tools/__init__.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/tools/credential_tools.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/tools/definition_tools.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/tools/event_tools.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/tools/job_tools.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/tools/registry_tools.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/tools/sqlite_tools.py +0 -0
- {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/validation_recommender.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: programgarden
|
|
3
|
-
Version: 1.22.
|
|
3
|
+
Version: 1.22.2
|
|
4
4
|
Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
|
|
5
5
|
Author: 프로그램동산
|
|
6
6
|
Author-email: coding@programgarden.com
|
|
@@ -13,9 +13,9 @@ Requires-Dist: aiosqlite (>=0.20.0,<0.21.0)
|
|
|
13
13
|
Requires-Dist: croniter (>=6.0.0,<7.0.0)
|
|
14
14
|
Requires-Dist: litellm (>=1.40.0)
|
|
15
15
|
Requires-Dist: lxml (>=6.0.2,<7.0.0)
|
|
16
|
-
Requires-Dist: programgarden-community (>=1.13.
|
|
17
|
-
Requires-Dist: programgarden-core (>=1.
|
|
18
|
-
Requires-Dist: programgarden-finance (>=1.6.
|
|
16
|
+
Requires-Dist: programgarden-community (>=1.13.7,<2.0.0)
|
|
17
|
+
Requires-Dist: programgarden-core (>=1.14.2,<2.0.0)
|
|
18
|
+
Requires-Dist: programgarden-finance (>=1.6.9,<2.0.0)
|
|
19
19
|
Requires-Dist: psutil (>=6.0.0,<7.0.0)
|
|
20
20
|
Requires-Dist: psycopg2-binary (>=2.9.11,<3.0.0)
|
|
21
21
|
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
|
@@ -1315,15 +1315,79 @@ class ExecutionContext:
|
|
|
1315
1315
|
def set_workflow_job(self, job: Any) -> None:
|
|
1316
1316
|
"""
|
|
1317
1317
|
Set workflow job reference for start datetime access.
|
|
1318
|
-
|
|
1318
|
+
|
|
1319
1319
|
This enables notify_workflow_pnl to include workflow_start_datetime
|
|
1320
1320
|
and workflow_elapsed_days in events.
|
|
1321
|
-
|
|
1321
|
+
|
|
1322
1322
|
Args:
|
|
1323
1323
|
job: WorkflowJob instance
|
|
1324
1324
|
"""
|
|
1325
1325
|
self._workflow_job = job
|
|
1326
1326
|
|
|
1327
|
+
# ============================================================
|
|
1328
|
+
# A-4: 주문 idempotency 위임 (WorkflowJob → ExecutionContext)
|
|
1329
|
+
# 주문 executor가 context만 받으므로 context를 통해 job 메서드에 접근.
|
|
1330
|
+
# ============================================================
|
|
1331
|
+
|
|
1332
|
+
def check_order_already_submitted(
|
|
1333
|
+
self,
|
|
1334
|
+
node_id: str,
|
|
1335
|
+
item: Optional[Any] = None,
|
|
1336
|
+
) -> Optional[Any]:
|
|
1337
|
+
"""이미 제출된 주문인지 확인 (WorkflowJob.check_order_already_submitted 위임).
|
|
1338
|
+
|
|
1339
|
+
WorkflowJob이 없거나 idempotency가 비활성화된 경우 None 반환.
|
|
1340
|
+
|
|
1341
|
+
Args:
|
|
1342
|
+
node_id: 주문 노드 ID
|
|
1343
|
+
item: 주문 정보 dict (symbol, exchange, quantity, price 등)
|
|
1344
|
+
|
|
1345
|
+
Returns:
|
|
1346
|
+
이미 제출된 경우 주문 결과 dict, 미제출/비활성이면 None
|
|
1347
|
+
"""
|
|
1348
|
+
job = self._workflow_job
|
|
1349
|
+
if job is None:
|
|
1350
|
+
return None
|
|
1351
|
+
check_fn = getattr(job, 'check_order_already_submitted', None)
|
|
1352
|
+
if check_fn is None:
|
|
1353
|
+
return None
|
|
1354
|
+
# A-4: ordering 세대(_order_cycle) 사용. flow_executions 는 복구 시 드리프트
|
|
1355
|
+
# 하므로 cycle 로 쓰면 realtime 복구가 동일 주문을 다른 키로 기록해 중복 차단
|
|
1356
|
+
# 실패. _order_cycle 은 entry main flow(초기/복구 재실행) 에서 항상 0.
|
|
1357
|
+
cycle = getattr(job, '_order_cycle', 0)
|
|
1358
|
+
try:
|
|
1359
|
+
return check_fn(node_id=node_id, cycle=cycle, item=item)
|
|
1360
|
+
except Exception:
|
|
1361
|
+
return None
|
|
1362
|
+
|
|
1363
|
+
def record_order_submitted(
|
|
1364
|
+
self,
|
|
1365
|
+
node_id: str,
|
|
1366
|
+
order_result: Any,
|
|
1367
|
+
item: Optional[Any] = None,
|
|
1368
|
+
) -> None:
|
|
1369
|
+
"""주문 제출 성공 후 idempotency 레지스트리에 기록 (WorkflowJob 위임).
|
|
1370
|
+
|
|
1371
|
+
WorkflowJob이 없거나 idempotency가 비활성화된 경우 무시.
|
|
1372
|
+
|
|
1373
|
+
Args:
|
|
1374
|
+
node_id: 주문 노드 ID
|
|
1375
|
+
order_result: 주문 결과 dict (success, order_no 포함)
|
|
1376
|
+
item: 주문 정보 dict
|
|
1377
|
+
"""
|
|
1378
|
+
job = self._workflow_job
|
|
1379
|
+
if job is None:
|
|
1380
|
+
return
|
|
1381
|
+
record_fn = getattr(job, 'record_order_submitted', None)
|
|
1382
|
+
if record_fn is None:
|
|
1383
|
+
return
|
|
1384
|
+
# A-4: check 와 동일한 _order_cycle 사용 (드리프트 방지 — 위 check 주석 참조).
|
|
1385
|
+
cycle = getattr(job, '_order_cycle', 0)
|
|
1386
|
+
try:
|
|
1387
|
+
record_fn(node_id=node_id, cycle=cycle, item=item, order_result=order_result)
|
|
1388
|
+
except Exception:
|
|
1389
|
+
pass
|
|
1390
|
+
|
|
1327
1391
|
async def notify_node_state(
|
|
1328
1392
|
self,
|
|
1329
1393
|
node_id: str,
|
|
@@ -63,6 +63,18 @@ class CheckpointManager:
|
|
|
63
63
|
PRIMARY KEY (job_id, node_id, port_name)
|
|
64
64
|
)
|
|
65
65
|
""")
|
|
66
|
+
# A-4: 주문 idempotency 레지스트리
|
|
67
|
+
# enable_order_idempotency=True 일 때 주문 중복 방지에 사용.
|
|
68
|
+
# idempotency_key = workflow_id:node_id:cycle:item_hash
|
|
69
|
+
conn.execute("""
|
|
70
|
+
CREATE TABLE IF NOT EXISTS order_idempotency (
|
|
71
|
+
job_id TEXT NOT NULL,
|
|
72
|
+
idempotency_key TEXT NOT NULL,
|
|
73
|
+
order_result_json TEXT NOT NULL,
|
|
74
|
+
submitted_at TEXT NOT NULL,
|
|
75
|
+
PRIMARY KEY (job_id, idempotency_key)
|
|
76
|
+
)
|
|
77
|
+
""")
|
|
66
78
|
conn.commit()
|
|
67
79
|
|
|
68
80
|
# ============================================================
|
|
@@ -240,9 +252,65 @@ class CheckpointManager:
|
|
|
240
252
|
with sqlite3.connect(self.db_path) as conn:
|
|
241
253
|
conn.execute("DELETE FROM checkpoint_meta WHERE job_id = ?", (job_id,))
|
|
242
254
|
conn.execute("DELETE FROM checkpoint_outputs WHERE job_id = ?", (job_id,))
|
|
255
|
+
conn.execute("DELETE FROM order_idempotency WHERE job_id = ?", (job_id,))
|
|
243
256
|
conn.commit()
|
|
244
257
|
logger.debug(f"Checkpoint 삭제: job={job_id}")
|
|
245
258
|
|
|
259
|
+
# ============================================================
|
|
260
|
+
# A-4: 주문 idempotency 레지스트리
|
|
261
|
+
# ============================================================
|
|
262
|
+
|
|
263
|
+
def record_order_submission(
|
|
264
|
+
self,
|
|
265
|
+
job_id: str,
|
|
266
|
+
idempotency_key: str,
|
|
267
|
+
order_result: Dict[str, Any],
|
|
268
|
+
) -> None:
|
|
269
|
+
"""주문 제출 결과를 idempotency 레지스트리에 기록.
|
|
270
|
+
|
|
271
|
+
enable_order_idempotency=True 인 워크플로우에서 주문 노드가 LS에
|
|
272
|
+
주문을 제출한 직후 호출됩니다. 동일 idempotency_key가 존재하면
|
|
273
|
+
IGNORE (멱등, 이미 기록된 것을 덮어쓰지 않음).
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
job_id: 워크플로우 job ID
|
|
277
|
+
idempotency_key: 결정적 키 (workflow_id:node_id:cycle:item_hash)
|
|
278
|
+
order_result: 주문 결과 dict (order_no 포함)
|
|
279
|
+
"""
|
|
280
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
281
|
+
result_json = json.dumps(order_result, default=str)
|
|
282
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
283
|
+
conn.execute("""
|
|
284
|
+
INSERT OR IGNORE INTO order_idempotency
|
|
285
|
+
(job_id, idempotency_key, order_result_json, submitted_at)
|
|
286
|
+
VALUES (?, ?, ?, ?)
|
|
287
|
+
""", (job_id, idempotency_key, result_json, now))
|
|
288
|
+
conn.commit()
|
|
289
|
+
logger.debug(
|
|
290
|
+
f"Order idempotency 기록: job={job_id}, key={idempotency_key}, "
|
|
291
|
+
f"order_no={order_result.get('order_no', '?')}"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def get_submitted_order(
|
|
295
|
+
self,
|
|
296
|
+
job_id: str,
|
|
297
|
+
idempotency_key: str,
|
|
298
|
+
) -> Optional[Dict[str, Any]]:
|
|
299
|
+
"""idempotency_key로 이미 제출된 주문 결과를 조회.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
주문 결과 dict (order_no 포함) 또는 None (미제출)
|
|
303
|
+
"""
|
|
304
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
305
|
+
row = conn.execute(
|
|
306
|
+
"SELECT order_result_json FROM order_idempotency "
|
|
307
|
+
"WHERE job_id = ? AND idempotency_key = ?",
|
|
308
|
+
(job_id, idempotency_key),
|
|
309
|
+
).fetchone()
|
|
310
|
+
if row is None:
|
|
311
|
+
return None
|
|
312
|
+
return json.loads(row[0])
|
|
313
|
+
|
|
246
314
|
def has_checkpoint(self, job_id: str) -> bool:
|
|
247
315
|
"""체크포인트 존재 여부."""
|
|
248
316
|
with sqlite3.connect(self.db_path) as conn:
|