python-codex 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl

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 (59) hide show
  1. pycodex/__init__.py +5 -1
  2. pycodex/agent.py +39 -41
  3. pycodex/cli.py +51 -43
  4. pycodex/collaboration.py +6 -7
  5. pycodex/compat.py +99 -0
  6. pycodex/context.py +87 -87
  7. pycodex/doctor.py +40 -40
  8. pycodex/model.py +69 -69
  9. pycodex/portable.py +33 -33
  10. pycodex/portable_server.py +22 -21
  11. pycodex/protocol.py +84 -86
  12. pycodex/runtime.py +36 -35
  13. pycodex/runtime_services.py +72 -69
  14. pycodex/tools/agent_tool_schemas.py +0 -2
  15. pycodex/tools/apply_patch_tool.py +43 -44
  16. pycodex/tools/base_tool.py +35 -36
  17. pycodex/tools/close_agent_tool.py +2 -4
  18. pycodex/tools/code_mode_manager.py +61 -61
  19. pycodex/tools/exec_command_tool.py +5 -6
  20. pycodex/tools/exec_runtime.js +3 -3
  21. pycodex/tools/exec_tool.py +3 -5
  22. pycodex/tools/grep_files_tool.py +10 -11
  23. pycodex/tools/list_dir_tool.py +8 -9
  24. pycodex/tools/read_file_tool.py +13 -14
  25. pycodex/tools/request_permissions_tool.py +2 -4
  26. pycodex/tools/request_user_input_tool.py +13 -14
  27. pycodex/tools/resume_agent_tool.py +2 -4
  28. pycodex/tools/send_input_tool.py +8 -9
  29. pycodex/tools/shell_command_tool.py +5 -6
  30. pycodex/tools/shell_tool.py +5 -6
  31. pycodex/tools/spawn_agent_tool.py +4 -5
  32. pycodex/tools/unified_exec_manager.py +79 -61
  33. pycodex/tools/update_plan_tool.py +4 -5
  34. pycodex/tools/view_image_tool.py +4 -5
  35. pycodex/tools/wait_agent_tool.py +2 -4
  36. pycodex/tools/wait_tool.py +4 -5
  37. pycodex/tools/web_search_tool.py +1 -3
  38. pycodex/tools/write_stdin_tool.py +4 -5
  39. pycodex/utils/dotenv.py +6 -6
  40. pycodex/utils/get_env.py +57 -34
  41. pycodex/utils/random_ids.py +1 -2
  42. pycodex/utils/visualize.py +79 -79
  43. {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/METADATA +15 -9
  44. python_codex-0.1.3.dist-info/RECORD +74 -0
  45. {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/WHEEL +1 -1
  46. responses_server/__init__.py +17 -0
  47. responses_server/__main__.py +5 -0
  48. responses_server/app.py +227 -0
  49. responses_server/config.py +63 -0
  50. responses_server/payload_processors.py +86 -0
  51. responses_server/server.py +63 -0
  52. responses_server/session_store.py +37 -0
  53. responses_server/stream_router.py +784 -0
  54. responses_server/tools/__init__.py +4 -0
  55. responses_server/tools/custom_adapter.py +235 -0
  56. responses_server/tools/web_search.py +263 -0
  57. python_codex-0.1.1.dist-info/RECORD +0 -62
  58. {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/entry_points.txt +0 -0
  59. {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/licenses/LICENSE +0 -0
pycodex/__init__.py CHANGED
@@ -1,3 +1,7 @@
1
+ from .compat import patch_asyncio
2
+
3
+ patch_asyncio()
4
+
1
5
  from .agent import AgentLoop
2
6
  from .context import ContextConfig, ContextManager
3
7
  from .model import (
@@ -60,7 +64,7 @@ from .tools import (
60
64
  WriteStdinTool,
61
65
  )
62
66
 
63
- def debug(stop: bool = False):
67
+ def debug(stop: 'bool' = False):
64
68
 
65
69
  import socket
66
70
 
pycodex/agent.py CHANGED
@@ -1,8 +1,7 @@
1
- from __future__ import annotations
2
1
 
3
2
  import asyncio
4
3
  import json
5
- from collections.abc import Callable
4
+ from typing import Callable
6
5
 
7
6
  from .context import ContextManager
8
7
  from .model import ModelClient
@@ -19,10 +18,11 @@ from .protocol import (
19
18
  )
20
19
  from .tools import ToolContext, ToolRegistry
21
20
  from .utils import uuid7_string
21
+ import typing
22
22
 
23
23
 
24
24
  EventHandler = Callable[[AgentEvent], None]
25
- NOOP_EVENT_HANDLER: EventHandler = lambda _event: None
25
+ NOOP_EVENT_HANDLER: 'EventHandler' = lambda _event: None
26
26
 
27
27
 
28
28
  class TurnInterrupted(RuntimeError):
@@ -40,47 +40,47 @@ class AgentLoop:
40
40
 
41
41
  def __init__(
42
42
  self,
43
- model_client: ModelClient,
44
- tool_registry: ToolRegistry,
45
- context_manager: ContextManager | None = None,
46
- parallel_tool_calls: bool = True,
47
- event_handler: EventHandler = NOOP_EVENT_HANDLER,
48
- initial_history: tuple[ConversationItem, ...] = (),
49
- ) -> None:
43
+ model_client: 'ModelClient',
44
+ tool_registry: 'ToolRegistry',
45
+ context_manager: 'typing.Union[ContextManager, None]' = None,
46
+ parallel_tool_calls: 'bool' = True,
47
+ event_handler: 'EventHandler' = NOOP_EVENT_HANDLER,
48
+ initial_history: 'typing.Tuple[ConversationItem, ...]' = (),
49
+ ) -> 'None':
50
50
  self._model_client = model_client
51
51
  self._tool_registry = tool_registry
52
52
  self._context_manager = context_manager or ContextManager()
53
53
  self._parallel_tool_calls = parallel_tool_calls
54
54
  self._event_handler = event_handler
55
- self._history: list[ConversationItem] = list(initial_history)
55
+ self._history: 'typing.List[ConversationItem]' = list(initial_history)
56
56
  self.interrupt_asap = False
57
57
 
58
58
  @property
59
- def history(self) -> tuple[ConversationItem, ...]:
59
+ def history(self) -> 'typing.Tuple[ConversationItem, ...]':
60
60
  return tuple(self._history)
61
61
 
62
62
  def set_event_handler(
63
- self, event_handler: EventHandler = NOOP_EVENT_HANDLER
64
- ) -> None:
63
+ self, event_handler: 'EventHandler' = NOOP_EVENT_HANDLER
64
+ ) -> 'None':
65
65
  self._event_handler = event_handler
66
66
 
67
67
  def _raise_if_interrupt_requested(
68
68
  self,
69
- turn_id: str,
70
- iteration: int,
71
- output_text: str | None = None,
72
- ) -> None:
69
+ turn_id: 'str',
70
+ iteration: 'int',
71
+ output_text: 'typing.Union[str, None]' = None,
72
+ ) -> 'None':
73
73
  if self.interrupt_asap:
74
74
  self.interrupt_asap = False
75
- payload: dict[str, object] = {"iteration": iteration}
75
+ payload: 'typing.Dict[str, object]' = {"iteration": iteration}
76
76
  if output_text is not None:
77
77
  payload["output_text"] = output_text
78
78
  self._emit("turn_interrupted", turn_id, **payload)
79
79
  raise TurnInterrupted("turn interrupted")
80
80
 
81
81
  async def run_turn(
82
- self, texts: list[str], turn_id: str | None = None
83
- ) -> TurnResult:
82
+ self, texts: 'typing.List[str]', turn_id: 'typing.Union[str, None]' = None
83
+ ) -> 'TurnResult':
84
84
  turn_id = turn_id or uuid7_string()
85
85
  self.interrupt_asap = False
86
86
  for text in texts:
@@ -93,10 +93,8 @@ class AgentLoop:
93
93
  user_texts=list(texts),
94
94
  )
95
95
 
96
- last_assistant_message: str | None = None
97
- final_response_items: tuple[
98
- AssistantMessage | ToolCall | ReasoningItem, ...
99
- ] = ()
96
+ last_assistant_message: 'typing.Union[str, None]' = None
97
+ final_response_items: 'typing.Tuple[\n typing.Union[typing.Union[AssistantMessage, ToolCall], ReasoningItem], ...\n]' = ()
100
98
 
101
99
  iteration = 0
102
100
  try:
@@ -132,7 +130,7 @@ class AgentLoop:
132
130
  item_count=len(response.items),
133
131
  )
134
132
 
135
- tool_calls: list[ToolCall] = []
133
+ tool_calls: 'typing.List[ToolCall]' = []
136
134
  for item in response.items:
137
135
  self._history.append(item)
138
136
  if isinstance(item, AssistantMessage):
@@ -182,11 +180,11 @@ class AgentLoop:
182
180
 
183
181
  async def _execute_tool_batch(
184
182
  self,
185
- turn_id: str,
186
- tool_calls: list[ToolCall],
187
- ) -> list[ToolResult]:
188
- results: list[ToolResult] = []
189
- parallel_batch: list[ToolCall] = []
183
+ turn_id: 'str',
184
+ tool_calls: 'typing.List[ToolCall]',
185
+ ) -> 'typing.List[ToolResult]':
186
+ results: 'typing.List[ToolResult]' = []
187
+ parallel_batch: 'typing.List[ToolCall]' = []
190
188
 
191
189
  for call in tool_calls:
192
190
  can_run_parallel = (
@@ -224,10 +222,10 @@ class AgentLoop:
224
222
 
225
223
  async def _run_single_tool(
226
224
  self,
227
- turn_id: str,
228
- call: ToolCall,
229
- prior_results: tuple[ToolResult, ...] = (),
230
- ) -> ToolResult:
225
+ turn_id: 'str',
226
+ call: 'ToolCall',
227
+ prior_results: 'typing.Tuple[ToolResult, ...]' = (),
228
+ ) -> 'ToolResult':
231
229
  self._emit("tool_started", turn_id, tool_name=call.name, call_id=call.call_id)
232
230
  result = await self._tool_registry.execute(
233
231
  call,
@@ -237,7 +235,7 @@ class AgentLoop:
237
235
  collaboration_mode=self._context_manager.collaboration_mode,
238
236
  ),
239
237
  )
240
- payload: dict[str, object] = {
238
+ payload: 'typing.Dict[str, object]' = {
241
239
  "tool_name": call.name,
242
240
  "call_id": call.call_id,
243
241
  "is_error": result.is_error,
@@ -247,12 +245,12 @@ class AgentLoop:
247
245
  self._emit("tool_completed", turn_id, **payload)
248
246
  return result
249
247
 
250
- def _emit(self, kind: str, turn_id: str, **payload: object) -> None:
248
+ def _emit(self, kind: 'str', turn_id: 'str', **payload: 'object') -> 'None':
251
249
  self._event_handler(
252
250
  AgentEvent(kind=kind, turn_id=turn_id, payload=dict(payload))
253
251
  )
254
252
 
255
- def _handle_model_stream_event(self, turn_id: str, event: ModelStreamEvent) -> None:
253
+ def _handle_model_stream_event(self, turn_id: 'str', event: 'ModelStreamEvent') -> 'None':
256
254
  if event.kind == "assistant_delta":
257
255
  self._emit("assistant_delta", turn_id, **event.payload)
258
256
  elif event.kind == "tool_call":
@@ -260,9 +258,9 @@ class AgentLoop:
260
258
 
261
259
  def _build_follow_up_messages(
262
260
  self,
263
- tool_results: list[ToolResult],
264
- ) -> list[UserMessage]:
265
- follow_ups: list[UserMessage] = []
261
+ tool_results: 'typing.List[ToolResult]',
262
+ ) -> 'typing.List[UserMessage]':
263
+ follow_ups: 'typing.List[UserMessage]' = []
266
264
  for result in tool_results:
267
265
  statuses = None
268
266
  if (
pycodex/cli.py CHANGED
@@ -1,4 +1,3 @@
1
- from __future__ import annotations
2
1
 
3
2
  import atexit
4
3
  import argparse
@@ -10,10 +9,11 @@ import sys
10
9
  import tempfile
11
10
  from dataclasses import asdict, replace
12
11
  from pathlib import Path
13
- from typing import Literal, Sequence
12
+ from typing import Sequence
14
13
 
15
14
  from .agent import AgentLoop
16
15
  from .collaboration import DEFAULT_COLLABORATION_MODE, CollaborationMode
16
+ from .compat import Literal
17
17
  from .context import ContextManager
18
18
  from .model import ResponsesModelClient, ResponsesProviderConfig
19
19
  from .portable import bootstrap_called_home, upload_codex_home
@@ -21,7 +21,7 @@ from .protocol import AgentEvent
21
21
  from .runtime import AgentRuntime
22
22
  from .runtime_services import RuntimeEnvironment, create_runtime_environment
23
23
  from .utils import CliSessionView, load_codex_dotenv
24
- from responses_server import launch_chat_completion_compat_server
24
+ import typing
25
25
 
26
26
  EXIT_COMMANDS = {"/exit", "/quit"}
27
27
  HISTORY_COMMAND = "/history"
@@ -33,7 +33,15 @@ LOCAL_RESPONSES_SERVER_API_KEY_ENV = "PYCODEX_LOCAL_RESPONSES_SERVER_KEY"
33
33
  CLI_ORIGINATOR = "codex-tui"
34
34
 
35
35
 
36
- def configure_loguru() -> None:
36
+ def launch_chat_completion_compat_server(*args, **kwargs):
37
+ from responses_server import (
38
+ launch_chat_completion_compat_server as launch_compat_server,
39
+ )
40
+
41
+ return launch_compat_server(*args, **kwargs)
42
+
43
+
44
+ def configure_loguru() -> 'None':
37
45
  try:
38
46
  from loguru import logger
39
47
  except ImportError: # pragma: no cover - dependency may be absent in minimal envs
@@ -54,7 +62,7 @@ def configure_loguru() -> None:
54
62
  logger.add(sys.stderr, level="DEBUG")
55
63
 
56
64
 
57
- def build_parser() -> argparse.ArgumentParser:
65
+ def build_parser() -> 'argparse.ArgumentParser':
58
66
  parser = argparse.ArgumentParser(
59
67
  prog="pycodex",
60
68
  description="Minimal Codex-style local CLI backed by ~/.codex/config.toml.",
@@ -124,11 +132,11 @@ def build_parser() -> argparse.ArgumentParser:
124
132
  return parser
125
133
 
126
134
 
127
- def should_run_interactive(prompt_parts: Sequence[str], stdin_is_tty: bool) -> bool:
135
+ def should_run_interactive(prompt_parts: 'Sequence[str]', stdin_is_tty: 'bool') -> 'bool':
128
136
  return not prompt_parts and stdin_is_tty
129
137
 
130
138
 
131
- def resolve_prompt_text(prompt_parts: Sequence[str]) -> str:
139
+ def resolve_prompt_text(prompt_parts: 'Sequence[str]') -> 'str':
132
140
  if prompt_parts:
133
141
  return " ".join(prompt_parts).strip()
134
142
 
@@ -141,8 +149,8 @@ def resolve_prompt_text(prompt_parts: Sequence[str]) -> str:
141
149
 
142
150
 
143
151
  def get_tools(
144
- runtime_environment: RuntimeEnvironment | None = None,
145
- exec_mode: bool = False,
152
+ runtime_environment: 'typing.Union[RuntimeEnvironment, None]' = None,
153
+ exec_mode: 'bool' = False,
146
154
  ):
147
155
  from .tools import (
148
156
  ApplyPatchTool,
@@ -236,7 +244,7 @@ def get_tools(
236
244
  return registry
237
245
 
238
246
 
239
- def get_subagent_tools(runtime_environment: RuntimeEnvironment | None = None):
247
+ def get_subagent_tools(runtime_environment: 'typing.Union[RuntimeEnvironment, None]' = None):
240
248
  from .tools import (
241
249
  ApplyPatchTool,
242
250
  ExecCommandTool,
@@ -261,13 +269,13 @@ def get_subagent_tools(runtime_environment: RuntimeEnvironment | None = None):
261
269
 
262
270
 
263
271
  def build_runtime(
264
- config_path: str,
265
- profile: str | None,
266
- system_prompt: str | None,
272
+ config_path: 'str',
273
+ profile: 'typing.Union[str, None]',
274
+ system_prompt: 'typing.Union[str, None]',
267
275
  client,
268
- session_mode: CliSessionMode = "exec",
269
- collaboration_mode: CollaborationMode = DEFAULT_COLLABORATION_MODE,
270
- ) -> AgentRuntime:
276
+ session_mode: 'CliSessionMode' = "exec",
277
+ collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
278
+ ) -> 'AgentRuntime':
271
279
  use_tui_context = session_mode == "tui"
272
280
  context_manager = ContextManager.from_codex_config(
273
281
  config_path,
@@ -288,11 +296,11 @@ def build_runtime(
288
296
 
289
297
  def make_subagent_runtime_builder(base_client):
290
298
  def build_subagent_runtime(
291
- model_override: str | None,
292
- reasoning_effort_override: str | None,
299
+ model_override: 'typing.Union[str, None]',
300
+ reasoning_effort_override: 'typing.Union[str, None]',
293
301
  initial_history=(),
294
- session_id: str | None = None,
295
- ) -> AgentRuntime:
302
+ session_id: 'typing.Union[str, None]' = None,
303
+ ) -> 'AgentRuntime':
296
304
  nested_client = base_client.with_overrides(
297
305
  model_override,
298
306
  reasoning_effort_override,
@@ -328,19 +336,19 @@ def build_runtime(
328
336
  )
329
337
 
330
338
 
331
- def format_turn_output(result, json_mode: bool) -> str:
339
+ def format_turn_output(result, json_mode: 'bool') -> 'str':
332
340
  if json_mode:
333
341
  return json.dumps(asdict(result), ensure_ascii=False, indent=2)
334
342
  return result.output_text or ""
335
343
 
336
344
 
337
345
  def _build_model_client(
338
- config_path: str,
339
- profile: str | None,
340
- timeout_seconds: float,
341
- managed_responses_base_url: str | None = None,
342
- vllm_endpoint: str | None = None,
343
- use_chat_completion: bool = False,
346
+ config_path: 'str',
347
+ profile: 'typing.Union[str, None]',
348
+ timeout_seconds: 'float',
349
+ managed_responses_base_url: 'typing.Union[str, None]' = None,
350
+ vllm_endpoint: 'typing.Union[str, None]' = None,
351
+ use_chat_completion: 'bool' = False,
344
352
  ):
345
353
  load_codex_dotenv(config_path)
346
354
  provider_config = ResponsesProviderConfig.from_codex_config(
@@ -386,13 +394,13 @@ def _build_model_client(
386
394
 
387
395
 
388
396
  async def prompt_request_user_input(
389
- view: CliSessionView,
390
- payload: dict[str, object],
391
- ) -> dict[str, object] | None:
397
+ view: 'CliSessionView',
398
+ payload: 'typing.Dict[str, object]',
399
+ ) -> 'typing.Union[typing.Dict[str, object], None]':
392
400
  view.finish_stream()
393
401
  view.pause_spinner()
394
402
  view.write_line("[request_user_input] waiting for user response")
395
- answers: dict[str, dict[str, list[str]]] = {}
403
+ answers: 'typing.Dict[str, typing.Dict[str, typing.List[str]]]' = {}
396
404
  try:
397
405
  for question in payload.get("questions", []):
398
406
  if not isinstance(question, dict):
@@ -449,9 +457,9 @@ async def prompt_request_user_input(
449
457
 
450
458
 
451
459
  async def prompt_request_permissions(
452
- view: CliSessionView,
453
- payload: dict[str, object],
454
- ) -> dict[str, object] | None:
460
+ view: 'CliSessionView',
461
+ payload: 'typing.Dict[str, object]',
462
+ ) -> 'typing.Union[typing.Dict[str, object], None]':
455
463
  view.finish_stream()
456
464
  view.pause_spinner()
457
465
  view.write_line("[request_permissions] user approval required")
@@ -488,14 +496,14 @@ async def prompt_request_permissions(
488
496
 
489
497
 
490
498
  async def run_interactive_session(
491
- runtime: AgentRuntime,
492
- json_mode: bool,
493
- ) -> int:
499
+ runtime: 'AgentRuntime',
500
+ json_mode: 'bool',
501
+ ) -> 'int':
494
502
  worker = asyncio.create_task(runtime.run_forever())
495
503
  view = CliSessionView()
496
504
  model_client = runtime._agent_loop._model_client
497
505
  runtime.set_event_handler(view.handle_event)
498
- pending_turn_tasks: set[asyncio.Task[None]] = set()
506
+ pending_turn_tasks: 'typing.Set[asyncio.Task[None]]' = set()
499
507
  runtime_environment = runtime.runtime_environment
500
508
  if runtime_environment is None:
501
509
  runtime_environment = create_runtime_environment()
@@ -510,13 +518,13 @@ async def run_interactive_session(
510
518
  view.write_line("Extra commands: /history, /title, /model")
511
519
  try:
512
520
 
513
- def has_pending_turn_tasks() -> bool:
521
+ def has_pending_turn_tasks() -> 'bool':
514
522
  pending_turn_tasks.difference_update(
515
523
  task for task in tuple(pending_turn_tasks) if task.done()
516
524
  )
517
525
  return bool(pending_turn_tasks)
518
526
 
519
- async def wait_for_turn_result(future) -> None:
527
+ async def wait_for_turn_result(future) -> 'None':
520
528
  try:
521
529
  result = await future
522
530
  except Exception as exc: # pragma: no cover - defensive surface
@@ -611,7 +619,7 @@ async def run_interactive_session(
611
619
  return 0
612
620
 
613
621
 
614
- async def run_cli(args: argparse.Namespace) -> int:
622
+ async def run_cli(args: 'argparse.Namespace') -> 'int':
615
623
  runtime = None
616
624
  worker = None
617
625
  try:
@@ -621,7 +629,7 @@ async def run_cli(args: argparse.Namespace) -> int:
621
629
  raise ValueError("--put does not accept prompt text")
622
630
  configure_loguru()
623
631
  if args.put is not None:
624
- def emit_put_log(message: str) -> None:
632
+ def emit_put_log(message: 'str') -> 'None':
625
633
  print(message, flush=True)
626
634
 
627
635
  call_spec = upload_codex_home(args.put, event_handler=emit_put_log)
@@ -671,7 +679,7 @@ async def run_cli(args: argparse.Namespace) -> int:
671
679
  await worker
672
680
 
673
681
 
674
- def main(argv: Sequence[str] | None = None) -> int:
682
+ def main(argv: 'typing.Union[Sequence[str], None]' = None) -> 'int':
675
683
  raw_args = list(argv) if argv is not None else None
676
684
  if raw_args is None:
677
685
  raw_args = sys.argv[1:]
pycodex/collaboration.py CHANGED
@@ -1,13 +1,13 @@
1
- from __future__ import annotations
2
1
 
3
- from typing import Literal
2
+ from .compat import Literal
3
+ import typing
4
4
 
5
5
  CollaborationMode = Literal["default", "plan", "execute", "pair_programming"]
6
6
 
7
- DEFAULT_COLLABORATION_MODE: CollaborationMode = "default"
8
- PLAN_COLLABORATION_MODE: CollaborationMode = "plan"
7
+ DEFAULT_COLLABORATION_MODE: 'CollaborationMode' = "default"
8
+ PLAN_COLLABORATION_MODE: 'CollaborationMode' = "plan"
9
9
 
10
- _MODE_DISPLAY_NAMES: dict[str, str] = {
10
+ _MODE_DISPLAY_NAMES: 'typing.Dict[str, str]' = {
11
11
  "default": "Default",
12
12
  "plan": "Plan",
13
13
  "execute": "Execute",
@@ -15,7 +15,6 @@ _MODE_DISPLAY_NAMES: dict[str, str] = {
15
15
  }
16
16
 
17
17
 
18
- def collaboration_mode_display_name(mode: str | None) -> str:
18
+ def collaboration_mode_display_name(mode: 'typing.Union[str, None]') -> 'str':
19
19
  normalized = (mode or DEFAULT_COLLABORATION_MODE).strip().lower()
20
20
  return _MODE_DISPLAY_NAMES.get(normalized, normalized.replace("_", " ").title())
21
-
pycodex/compat.py ADDED
@@ -0,0 +1,99 @@
1
+ import asyncio
2
+ import functools
3
+ import shlex
4
+
5
+ try:
6
+ from http.server import ThreadingHTTPServer
7
+ except ImportError: # pragma: no cover - Python 3.6 path
8
+ from http.server import HTTPServer
9
+ from socketserver import ThreadingMixIn
10
+
11
+ class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
12
+ daemon_threads = True
13
+
14
+ try:
15
+ from importlib import metadata as importlib_metadata
16
+ except ImportError: # pragma: no cover - Python 3.6 path
17
+ import importlib_metadata # type: ignore
18
+
19
+ try:
20
+ from typing import Literal, Protocol, TypeAlias
21
+ except ImportError: # pragma: no cover - Python 3.6 path
22
+ from typing_extensions import Literal, Protocol # type: ignore
23
+ try:
24
+ from typing_extensions import TypeAlias # type: ignore
25
+ except ImportError: # pragma: no cover - old typing_extensions
26
+ TypeAlias = object
27
+
28
+
29
+ def patch_asyncio():
30
+ if not hasattr(asyncio, "create_task"):
31
+ asyncio.create_task = asyncio.ensure_future
32
+
33
+ if not hasattr(asyncio, "get_running_loop"):
34
+ def get_running_loop():
35
+ return asyncio.get_event_loop()
36
+
37
+ asyncio.get_running_loop = get_running_loop
38
+
39
+ if not hasattr(asyncio, "to_thread"):
40
+ async def to_thread(func, *args, **kwargs):
41
+ loop = asyncio.get_event_loop()
42
+ call = functools.partial(func, *args, **kwargs)
43
+ return await loop.run_in_executor(None, call)
44
+
45
+ asyncio.to_thread = to_thread
46
+
47
+ if not hasattr(asyncio, "run"):
48
+ def run(main):
49
+ loop = asyncio.new_event_loop()
50
+ try:
51
+ asyncio.set_event_loop(loop)
52
+ return loop.run_until_complete(main)
53
+ finally:
54
+ all_tasks = getattr(asyncio.Task, "all_tasks", None)
55
+ if all_tasks is not None:
56
+ pending = all_tasks(loop=loop)
57
+ else:
58
+ pending = asyncio.all_tasks(loop)
59
+ for task in pending:
60
+ task.cancel()
61
+ if pending:
62
+ loop.run_until_complete(
63
+ asyncio.gather(*pending, return_exceptions=True)
64
+ )
65
+ shutdown_asyncgens = getattr(loop, "shutdown_asyncgens", None)
66
+ if shutdown_asyncgens is not None:
67
+ loop.run_until_complete(shutdown_asyncgens())
68
+ asyncio.set_event_loop(None)
69
+ loop.close()
70
+
71
+ asyncio.run = run
72
+
73
+
74
+ def shlex_join(parts):
75
+ join = getattr(shlex, "join", None)
76
+ if join is not None:
77
+ return join(parts)
78
+ return " ".join(shlex.quote(part) for part in parts)
79
+
80
+
81
+ def stream_writer_is_closing(writer):
82
+ method = getattr(writer, "is_closing", None)
83
+ if callable(method):
84
+ return method()
85
+ transport = getattr(writer, "transport", None)
86
+ if transport is None:
87
+ return False
88
+ transport_is_closing = getattr(transport, "is_closing", None)
89
+ if callable(transport_is_closing):
90
+ return transport_is_closing()
91
+ return False
92
+
93
+
94
+ def is_ascii(text):
95
+ try:
96
+ text.encode("ascii")
97
+ except UnicodeEncodeError:
98
+ return False
99
+ return True