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.
Files changed (35) hide show
  1. {programgarden-1.22.0 → programgarden-1.22.2}/PKG-INFO +4 -4
  2. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/context.py +66 -2
  3. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/database/checkpoint_manager.py +68 -0
  4. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/executor.py +435 -12
  5. programgarden-1.22.2/programgarden/reconnect_handler.py +288 -0
  6. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/resolver.py +68 -0
  7. {programgarden-1.22.0 → programgarden-1.22.2}/pyproject.toml +9 -4
  8. programgarden-1.22.0/programgarden/reconnect_handler.py +0 -176
  9. {programgarden-1.22.0 → programgarden-1.22.2}/README.md +0 -0
  10. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/__init__.py +0 -0
  11. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/binding_validator.py +0 -0
  12. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/client.py +0 -0
  13. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/database/__init__.py +0 -0
  14. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/database/query_builder.py +0 -0
  15. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/database/workflow_position_tracker.py +0 -0
  16. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/database/workflow_risk_tracker.py +0 -0
  17. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/node_runner.py +0 -0
  18. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/plugin/__init__.py +0 -0
  19. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/plugin/sandbox.py +0 -0
  20. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/providers/__init__.py +0 -0
  21. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/providers/llm_errors.py +0 -0
  22. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/providers/llm_provider.py +0 -0
  23. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/resource/__init__.py +0 -0
  24. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/resource/context.py +0 -0
  25. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/resource/limiter.py +0 -0
  26. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/resource/monitor.py +0 -0
  27. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/resource/throttle.py +0 -0
  28. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/tools/__init__.py +0 -0
  29. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/tools/credential_tools.py +0 -0
  30. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/tools/definition_tools.py +0 -0
  31. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/tools/event_tools.py +0 -0
  32. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/tools/job_tools.py +0 -0
  33. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/tools/registry_tools.py +0 -0
  34. {programgarden-1.22.0 → programgarden-1.22.2}/programgarden/tools/sqlite_tools.py +0 -0
  35. {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.0
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.5,<2.0.0)
17
- Requires-Dist: programgarden-core (>=1.13.0,<2.0.0)
18
- Requires-Dist: programgarden-finance (>=1.6.7,<2.0.0)
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: