codex-telegram 0.4.7__tar.gz → 0.4.8__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 (90) hide show
  1. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/PKG-INFO +6 -2
  2. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/client.py +24 -8
  3. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex_telegram/__init__.py +1 -1
  4. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex_telegram.egg-info/PKG-INFO +6 -2
  5. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_codex_client.py +41 -0
  6. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/LICENSE +0 -0
  7. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/MANIFEST.in +0 -0
  8. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/README.md +0 -0
  9. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/TELEGRAM.md +0 -0
  10. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/WEB.md +0 -0
  11. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/app_runtime/__init__.py +0 -0
  12. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/app_runtime/bootstrap.py +0 -0
  13. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/app_runtime/telegram_app.py +0 -0
  14. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/app_runtime/web_server.py +0 -0
  15. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/bot/__init__.py +0 -0
  16. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/bot/callbacks.py +0 -0
  17. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/bot/features_ui.py +0 -0
  18. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/bot/handlers.py +0 -0
  19. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/bot/keyboard.py +0 -0
  20. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/bot/projects_ui.py +0 -0
  21. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/bot/skills_ui.py +0 -0
  22. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/bot/thread_ui.py +0 -0
  23. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/__init__.py +0 -0
  24. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/approval_flow.py +0 -0
  25. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/approval_guardian.py +0 -0
  26. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/collaboration_mode.py +0 -0
  27. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/command_router/__init__.py +0 -0
  28. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/command_router/common.py +0 -0
  29. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/command_router/context.py +0 -0
  30. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/command_router/contracts.py +0 -0
  31. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/command_router/core.py +0 -0
  32. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/command_router/projects.py +0 -0
  33. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/command_router/review.py +0 -0
  34. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/command_router/system.py +0 -0
  35. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/command_router/threads.py +0 -0
  36. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/commands.py +0 -0
  37. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/event_forwarding.py +0 -0
  38. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/events.py +0 -0
  39. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex/protocol.py +0 -0
  40. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex_telegram/cli.py +0 -0
  41. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex_telegram.egg-info/SOURCES.txt +0 -0
  42. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex_telegram.egg-info/dependency_links.txt +0 -0
  43. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex_telegram.egg-info/entry_points.txt +0 -0
  44. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex_telegram.egg-info/requires.txt +1 -1
  45. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/codex_telegram.egg-info/top_level.txt +0 -0
  46. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/conf.toml.example +0 -0
  47. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/main.py +0 -0
  48. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/models/__init__.py +0 -0
  49. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/models/state.py +0 -0
  50. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/models/thread.py +0 -0
  51. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/models/user.py +0 -0
  52. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/pyproject.toml +0 -0
  53. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/setup.cfg +0 -0
  54. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_approval_guardian.py +0 -0
  55. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_approval_policy.py +0 -0
  56. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_callbacks_results.py +0 -0
  57. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_cli_entrypoint.py +0 -0
  58. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_command_router.py +0 -0
  59. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_guardian_config.py +0 -0
  60. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_handlers_results.py +0 -0
  61. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_keyboard.py +0 -0
  62. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_main_approval_flow.py +0 -0
  63. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_normalize.py +0 -0
  64. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_pypi_check.py +0 -0
  65. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_single_instance.py +0 -0
  66. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_telegram_bot_config.py +0 -0
  67. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_thread_history.py +0 -0
  68. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_web_runtime.py +0 -0
  69. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/tests/test_web_server_local_command.py +0 -0
  70. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/utils/__init__.py +0 -0
  71. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/utils/approval_policy.py +0 -0
  72. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/utils/config.py +0 -0
  73. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/utils/local_command.py +0 -0
  74. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/utils/logger.py +0 -0
  75. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/utils/normalize.py +0 -0
  76. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/utils/pypi_check.py +0 -0
  77. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/utils/single_instance.py +0 -0
  78. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/utils/web_config.py +0 -0
  79. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/web/__init__.py +0 -0
  80. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/web/dependencies.py +0 -0
  81. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/web/routes.py +0 -0
  82. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/web/runtime.py +0 -0
  83. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/web/server.py +0 -0
  84. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/web/static/dist/assets/index-B6IQl7AZ.js +0 -0
  85. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/web/static/dist/assets/index-Czx9yv3Y.css +0 -0
  86. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/web/static/dist/icons-tab-add.svg +0 -0
  87. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/web/static/dist/icons-tab-close.svg +0 -0
  88. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/web/static/dist/index.html +0 -0
  89. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/web/thread_history.py +0 -0
  90. {codex-telegram-0.4.7 → codex_telegram-0.4.8}/web/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: codex-telegram
3
- Version: 0.4.7
3
+ Version: 0.4.8
4
4
  Summary: Bridge for controlling the Codex App Server from Telegram and Web UI
5
5
  Author: heungtae
6
6
  License: Apache License
@@ -218,6 +218,10 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
218
218
  Requires-Python: >=3.11
219
219
  Description-Content-Type: text/markdown
220
220
  License-File: LICENSE
221
+ Requires-Dist: python-telegram-bot>=20.0
222
+ Requires-Dist: fastapi>=0.111.0
223
+ Requires-Dist: uvicorn>=0.30.0
224
+ Dynamic: license-file
221
225
 
222
226
  # Codex Telegram + Web
223
227
 
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import inspect
3
+ from dataclasses import dataclass
3
4
  from typing import Any, Callable
4
5
  import logging
5
6
 
@@ -10,6 +11,12 @@ from utils.config import get
10
11
  logger = logging.getLogger("codex-telegram.codex")
11
12
 
12
13
 
14
+ @dataclass(slots=True)
15
+ class _ApprovalWaiter:
16
+ future: asyncio.Future[str]
17
+ loop: asyncio.AbstractEventLoop
18
+
19
+
13
20
  class CodexClient:
14
21
  def __init__(self):
15
22
  self.protocol = Protocol()
@@ -17,7 +24,7 @@ class CodexClient:
17
24
  self._reader_task: asyncio.Task | None = None
18
25
  self._stderr_task: asyncio.Task | None = None
19
26
  self._pending: dict[int, asyncio.Future[JSONRPCResponse]] = {}
20
- self._pending_approvals: dict[int, asyncio.Future[str]] = {}
27
+ self._pending_approvals: dict[int, _ApprovalWaiter] = {}
21
28
  self._event_handlers: dict[str, list[Callable]] = {}
22
29
  self._any_event_handlers: list[Callable] = []
23
30
  self._approval_handlers: list[Callable] = []
@@ -114,10 +121,18 @@ class CodexClient:
114
121
  self._approval_handlers.append(handler)
115
122
 
116
123
  def submit_approval_decision(self, request_id: int, decision: str) -> bool:
117
- future = self._pending_approvals.get(request_id)
118
- if future is None or future.done():
124
+ waiter = self._pending_approvals.get(request_id)
125
+ if waiter is None or waiter.future.done():
119
126
  return False
120
- future.set_result(decision)
127
+
128
+ def _resolve() -> None:
129
+ if not waiter.future.done():
130
+ waiter.future.set_result(decision)
131
+
132
+ if waiter.loop.is_running():
133
+ waiter.loop.call_soon_threadsafe(_resolve)
134
+ else:
135
+ _resolve()
121
136
  return True
122
137
 
123
138
  def _write(self, msg: JSONRPCRequest | JSONRPCNotification | JSONRPCResponse):
@@ -132,8 +147,8 @@ class CodexClient:
132
147
  future.set_exception(exc)
133
148
  self._pending.clear()
134
149
  for future in self._pending_approvals.values():
135
- if not future.done():
136
- future.set_result("deny")
150
+ if not future.future.done():
151
+ future.future.set_result("deny")
137
152
  self._pending_approvals.clear()
138
153
 
139
154
  async def _read_stdout_stream(self):
@@ -499,8 +514,9 @@ class CodexClient:
499
514
  )
500
515
  return
501
516
 
502
- future: asyncio.Future[str] = asyncio.Future()
503
- self._pending_approvals[req_id] = future
517
+ loop = asyncio.get_running_loop()
518
+ future: asyncio.Future[str] = loop.create_future()
519
+ self._pending_approvals[req_id] = _ApprovalWaiter(future=future, loop=loop)
504
520
  await self._emit_approval_request(method, req_id, params)
505
521
 
506
522
  decision_choice = self._default_choice(auto_mode)
@@ -1,3 +1,3 @@
1
1
  """codex-telegram package."""
2
2
 
3
- __version__ = "0.4.7"
3
+ __version__ = "0.4.8"
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: codex-telegram
3
- Version: 0.4.7
3
+ Version: 0.4.8
4
4
  Summary: Bridge for controlling the Codex App Server from Telegram and Web UI
5
5
  Author: heungtae
6
6
  License: Apache License
@@ -218,6 +218,10 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
218
218
  Requires-Python: >=3.11
219
219
  Description-Content-Type: text/markdown
220
220
  License-File: LICENSE
221
+ Requires-Dist: python-telegram-bot>=20.0
222
+ Requires-Dist: fastapi>=0.111.0
223
+ Requires-Dist: uvicorn>=0.30.0
224
+ Dynamic: license-file
221
225
 
222
226
  # Codex Telegram + Web
223
227
 
@@ -1,4 +1,6 @@
1
+ import asyncio
1
2
  import unittest
3
+ import threading
2
4
  from unittest.mock import patch
3
5
  from types import SimpleNamespace
4
6
 
@@ -89,6 +91,45 @@ class CodexClientServerRequestTests(unittest.IsolatedAsyncioTestCase):
89
91
  answers = ((response.get("result") or {}).get("answers") or {})
90
92
  self.assertEqual({"answers": ["Approve this Session"]}, answers.get("q2"))
91
93
 
94
+ async def test_submit_approval_decision_is_thread_safe(self):
95
+ client = CodexClient()
96
+ written = []
97
+ client._write = lambda msg: written.append(msg) # type: ignore[method-assign]
98
+ req = JSONRPCRequest(
99
+ method="item/commandExecution/requestApproval",
100
+ id=42,
101
+ params={"threadId": "thread-1"},
102
+ )
103
+
104
+ with patch(
105
+ "codex.client.get",
106
+ side_effect=lambda key, default=None: "interactive" if key == "approval.mode" else "approve" if key == "approval.auto_response" else default,
107
+ ):
108
+ task = asyncio.create_task(client._handle_server_request(req))
109
+ for _ in range(100):
110
+ if 42 in client._pending_approvals:
111
+ break
112
+ await asyncio.sleep(0.01)
113
+ else:
114
+ self.fail("approval request was not registered")
115
+
116
+ accepted = {}
117
+
118
+ def approve_from_thread():
119
+ accepted["value"] = client.submit_approval_decision(42, "approve")
120
+
121
+ thread = threading.Thread(target=approve_from_thread)
122
+ thread.start()
123
+ thread.join()
124
+
125
+ await asyncio.wait_for(task, timeout=1.0)
126
+
127
+ self.assertTrue(accepted["value"])
128
+ self.assertEqual(1, len(written))
129
+ response = written[0].to_dict()
130
+ self.assertEqual(42, response.get("id"))
131
+ self.assertEqual({"decision": "accept"}, response.get("result"))
132
+
92
133
  async def test_tool_request_user_input_prefers_option_value_when_present(self):
93
134
  client = CodexClient()
94
135
  written = []
File without changes
File without changes
File without changes
@@ -1,3 +1,3 @@
1
- fastapi>=0.111.0
2
1
  python-telegram-bot>=20.0
2
+ fastapi>=0.111.0
3
3
  uvicorn>=0.30.0
File without changes
File without changes