code-puppy 0.0.214__py3-none-any.whl → 0.0.366__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.
- code_puppy/__init__.py +7 -1
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +142 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +176 -738
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -185
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
code_puppy/http_utils.py
CHANGED
|
@@ -4,29 +4,82 @@ HTTP utilities module for code-puppy.
|
|
|
4
4
|
This module provides functions for creating properly configured HTTP clients.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import asyncio
|
|
7
8
|
import os
|
|
8
9
|
import socket
|
|
9
|
-
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
|
10
13
|
|
|
11
14
|
import httpx
|
|
12
|
-
import requests
|
|
13
|
-
from tenacity import stop_after_attempt, wait_exponential
|
|
14
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
import requests
|
|
15
18
|
from code_puppy.config import get_http2
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ProxyConfig:
|
|
23
|
+
"""Configuration for proxy and SSL settings."""
|
|
24
|
+
|
|
25
|
+
verify: Union[bool, str, None]
|
|
26
|
+
trust_env: bool
|
|
27
|
+
proxy_url: str | None
|
|
28
|
+
disable_retry: bool
|
|
29
|
+
http2_enabled: bool
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _resolve_proxy_config(verify: Union[bool, str, None] = None) -> ProxyConfig:
|
|
33
|
+
"""Resolve proxy, SSL, and retry settings from environment.
|
|
34
|
+
|
|
35
|
+
This centralizes the logic for detecting proxies, determining SSL verification,
|
|
36
|
+
and checking if retry transport should be disabled.
|
|
37
|
+
"""
|
|
38
|
+
if verify is None:
|
|
39
|
+
verify = get_cert_bundle_path()
|
|
40
|
+
|
|
41
|
+
http2_enabled = get_http2()
|
|
42
|
+
|
|
43
|
+
disable_retry = os.environ.get(
|
|
44
|
+
"CODE_PUPPY_DISABLE_RETRY_TRANSPORT", ""
|
|
45
|
+
).lower() in ("1", "true", "yes")
|
|
46
|
+
|
|
47
|
+
has_proxy = bool(
|
|
48
|
+
os.environ.get("HTTP_PROXY")
|
|
49
|
+
or os.environ.get("HTTPS_PROXY")
|
|
50
|
+
or os.environ.get("http_proxy")
|
|
51
|
+
or os.environ.get("https_proxy")
|
|
23
52
|
)
|
|
24
|
-
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
53
|
+
|
|
54
|
+
# Determine trust_env and verify based on proxy/retry settings
|
|
55
|
+
if disable_retry:
|
|
56
|
+
# Test mode: disable SSL verification for proxy testing
|
|
57
|
+
verify = False
|
|
58
|
+
trust_env = True
|
|
59
|
+
elif has_proxy:
|
|
60
|
+
# Production proxy: keep SSL verification enabled
|
|
61
|
+
trust_env = True
|
|
62
|
+
else:
|
|
63
|
+
trust_env = False
|
|
64
|
+
|
|
65
|
+
# Extract proxy URL
|
|
66
|
+
proxy_url = None
|
|
67
|
+
if has_proxy:
|
|
68
|
+
proxy_url = (
|
|
69
|
+
os.environ.get("HTTPS_PROXY")
|
|
70
|
+
or os.environ.get("https_proxy")
|
|
71
|
+
or os.environ.get("HTTP_PROXY")
|
|
72
|
+
or os.environ.get("http_proxy")
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return ProxyConfig(
|
|
76
|
+
verify=verify,
|
|
77
|
+
trust_env=trust_env,
|
|
78
|
+
proxy_url=proxy_url,
|
|
79
|
+
disable_retry=disable_retry,
|
|
80
|
+
http2_enabled=http2_enabled,
|
|
81
|
+
)
|
|
82
|
+
|
|
30
83
|
|
|
31
84
|
try:
|
|
32
85
|
from .reopenable_async_client import ReopenableAsyncClient
|
|
@@ -34,14 +87,104 @@ except ImportError:
|
|
|
34
87
|
ReopenableAsyncClient = None
|
|
35
88
|
|
|
36
89
|
try:
|
|
37
|
-
from .messaging import emit_info
|
|
90
|
+
from .messaging import emit_info, emit_warning
|
|
38
91
|
except ImportError:
|
|
39
92
|
# Fallback if messaging system is not available
|
|
40
93
|
def emit_info(content: str, **metadata):
|
|
41
94
|
pass # No-op if messaging system is not available
|
|
42
95
|
|
|
96
|
+
def emit_warning(content: str, **metadata):
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class RetryingAsyncClient(httpx.AsyncClient):
|
|
101
|
+
"""AsyncClient with built-in rate limit handling (429) and retries.
|
|
102
|
+
|
|
103
|
+
This replaces the Tenacity transport with a more direct subclass implementation,
|
|
104
|
+
which plays nicer with proxies and custom transports (like Antigravity).
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
retry_status_codes: tuple = (429, 502, 503, 504),
|
|
110
|
+
max_retries: int = 5,
|
|
111
|
+
**kwargs,
|
|
112
|
+
):
|
|
113
|
+
super().__init__(**kwargs)
|
|
114
|
+
self.retry_status_codes = retry_status_codes
|
|
115
|
+
self.max_retries = max_retries
|
|
116
|
+
|
|
117
|
+
async def send(self, request: httpx.Request, **kwargs: Any) -> httpx.Response:
|
|
118
|
+
"""Send request with automatic retries for rate limits and server errors."""
|
|
119
|
+
last_response = None
|
|
120
|
+
last_exception = None
|
|
43
121
|
|
|
44
|
-
|
|
122
|
+
for attempt in range(self.max_retries + 1):
|
|
123
|
+
try:
|
|
124
|
+
response = await super().send(request, **kwargs)
|
|
125
|
+
last_response = response
|
|
126
|
+
|
|
127
|
+
# Check for retryable status
|
|
128
|
+
if response.status_code not in self.retry_status_codes:
|
|
129
|
+
return response
|
|
130
|
+
|
|
131
|
+
# Close response if we're going to retry
|
|
132
|
+
await response.aclose()
|
|
133
|
+
|
|
134
|
+
# Determine wait time
|
|
135
|
+
wait_time = 1.0 * (
|
|
136
|
+
2**attempt
|
|
137
|
+
) # Default exponential backoff: 1s, 2s, 4s...
|
|
138
|
+
|
|
139
|
+
# Check Retry-After header
|
|
140
|
+
retry_after = response.headers.get("Retry-After")
|
|
141
|
+
if retry_after:
|
|
142
|
+
try:
|
|
143
|
+
wait_time = float(retry_after)
|
|
144
|
+
except ValueError:
|
|
145
|
+
# Try parsing http-date
|
|
146
|
+
from email.utils import parsedate_to_datetime
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
date = parsedate_to_datetime(retry_after)
|
|
150
|
+
wait_time = date.timestamp() - time.time()
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# Cap wait time
|
|
155
|
+
wait_time = max(0.5, min(wait_time, 60.0))
|
|
156
|
+
|
|
157
|
+
if attempt < self.max_retries:
|
|
158
|
+
emit_info(
|
|
159
|
+
f"HTTP retry: {response.status_code} received. Waiting {wait_time:.1f}s (attempt {attempt + 1}/{self.max_retries})"
|
|
160
|
+
)
|
|
161
|
+
await asyncio.sleep(wait_time)
|
|
162
|
+
|
|
163
|
+
except (httpx.ConnectError, httpx.ReadTimeout, httpx.PoolTimeout) as e:
|
|
164
|
+
last_exception = e
|
|
165
|
+
wait_time = 1.0 * (2**attempt)
|
|
166
|
+
if attempt < self.max_retries:
|
|
167
|
+
emit_warning(
|
|
168
|
+
f"HTTP connection error: {e}. Retrying in {wait_time}s..."
|
|
169
|
+
)
|
|
170
|
+
await asyncio.sleep(wait_time)
|
|
171
|
+
else:
|
|
172
|
+
raise
|
|
173
|
+
except Exception:
|
|
174
|
+
raise
|
|
175
|
+
|
|
176
|
+
# Return last response (even if it's an error status)
|
|
177
|
+
if last_response:
|
|
178
|
+
return last_response
|
|
179
|
+
|
|
180
|
+
# Should catch this in loop, but just in case
|
|
181
|
+
if last_exception:
|
|
182
|
+
raise last_exception
|
|
183
|
+
|
|
184
|
+
return last_response
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_cert_bundle_path() -> str | None:
|
|
45
188
|
# First check if SSL_CERT_FILE environment variable is set
|
|
46
189
|
ssl_cert_file = os.environ.get("SSL_CERT_FILE")
|
|
47
190
|
if ssl_cert_file and os.path.exists(ssl_cert_file):
|
|
@@ -61,42 +204,14 @@ def create_client(
|
|
|
61
204
|
http2_enabled = get_http2()
|
|
62
205
|
|
|
63
206
|
# If retry components are available, create a client with retry transport
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return True
|
|
73
|
-
|
|
74
|
-
transport = TenacityTransport(
|
|
75
|
-
config=RetryConfig(
|
|
76
|
-
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
77
|
-
and e.response.status_code in retry_status_codes,
|
|
78
|
-
wait=wait_retry_after(
|
|
79
|
-
fallback_strategy=wait_exponential(multiplier=1, max=60),
|
|
80
|
-
max_wait=300,
|
|
81
|
-
),
|
|
82
|
-
stop=stop_after_attempt(10),
|
|
83
|
-
reraise=True,
|
|
84
|
-
),
|
|
85
|
-
validate_response=should_retry_status,
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
return httpx.Client(
|
|
89
|
-
transport=transport,
|
|
90
|
-
verify=verify,
|
|
91
|
-
headers=headers or {},
|
|
92
|
-
timeout=timeout,
|
|
93
|
-
http2=http2_enabled,
|
|
94
|
-
)
|
|
95
|
-
else:
|
|
96
|
-
# Fallback to regular client if retry components are not available
|
|
97
|
-
return httpx.Client(
|
|
98
|
-
verify=verify, headers=headers or {}, timeout=timeout, http2=http2_enabled
|
|
99
|
-
)
|
|
207
|
+
# Note: TenacityTransport was removed. For now we just return a standard client.
|
|
208
|
+
# Future TODO: Implement RetryingClient(httpx.Client) if needed.
|
|
209
|
+
return httpx.Client(
|
|
210
|
+
verify=verify,
|
|
211
|
+
headers=headers or {},
|
|
212
|
+
timeout=timeout,
|
|
213
|
+
http2=http2_enabled,
|
|
214
|
+
)
|
|
100
215
|
|
|
101
216
|
|
|
102
217
|
def create_async_client(
|
|
@@ -105,45 +220,26 @@ def create_async_client(
|
|
|
105
220
|
headers: Optional[Dict[str, str]] = None,
|
|
106
221
|
retry_status_codes: tuple = (429, 502, 503, 504),
|
|
107
222
|
) -> httpx.AsyncClient:
|
|
108
|
-
|
|
109
|
-
verify = get_cert_bundle_path()
|
|
110
|
-
|
|
111
|
-
# Check if HTTP/2 is enabled in config
|
|
112
|
-
http2_enabled = get_http2()
|
|
223
|
+
config = _resolve_proxy_config(verify)
|
|
113
224
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if response.status_code in retry_status_codes:
|
|
120
|
-
emit_info(
|
|
121
|
-
f"HTTP retry: Retrying request due to status code {response.status_code}"
|
|
122
|
-
)
|
|
123
|
-
return True
|
|
124
|
-
|
|
125
|
-
transport = AsyncTenacityTransport(
|
|
126
|
-
config=RetryConfig(
|
|
127
|
-
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
128
|
-
and e.response.status_code in retry_status_codes,
|
|
129
|
-
wait=wait_retry_after(10),
|
|
130
|
-
stop=stop_after_attempt(10),
|
|
131
|
-
reraise=True,
|
|
132
|
-
),
|
|
133
|
-
validate_response=should_retry_status,
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
return httpx.AsyncClient(
|
|
137
|
-
transport=transport,
|
|
138
|
-
verify=verify,
|
|
225
|
+
if not config.disable_retry:
|
|
226
|
+
return RetryingAsyncClient(
|
|
227
|
+
retry_status_codes=retry_status_codes,
|
|
228
|
+
proxy=config.proxy_url,
|
|
229
|
+
verify=config.verify,
|
|
139
230
|
headers=headers or {},
|
|
140
231
|
timeout=timeout,
|
|
141
|
-
http2=http2_enabled,
|
|
232
|
+
http2=config.http2_enabled,
|
|
233
|
+
trust_env=config.trust_env,
|
|
142
234
|
)
|
|
143
235
|
else:
|
|
144
|
-
# Fallback to regular client if retry components are not available
|
|
145
236
|
return httpx.AsyncClient(
|
|
146
|
-
|
|
237
|
+
proxy=config.proxy_url,
|
|
238
|
+
verify=config.verify,
|
|
239
|
+
headers=headers or {},
|
|
240
|
+
timeout=timeout,
|
|
241
|
+
http2=config.http2_enabled,
|
|
242
|
+
trust_env=config.trust_env,
|
|
147
243
|
)
|
|
148
244
|
|
|
149
245
|
|
|
@@ -151,7 +247,9 @@ def create_requests_session(
|
|
|
151
247
|
timeout: float = 5.0,
|
|
152
248
|
verify: Union[bool, str] = None,
|
|
153
249
|
headers: Optional[Dict[str, str]] = None,
|
|
154
|
-
) -> requests.Session:
|
|
250
|
+
) -> "requests.Session":
|
|
251
|
+
import requests
|
|
252
|
+
|
|
155
253
|
session = requests.Session()
|
|
156
254
|
|
|
157
255
|
if verify is None:
|
|
@@ -193,75 +291,39 @@ def create_reopenable_async_client(
|
|
|
193
291
|
headers: Optional[Dict[str, str]] = None,
|
|
194
292
|
retry_status_codes: tuple = (429, 502, 503, 504),
|
|
195
293
|
) -> Union[ReopenableAsyncClient, httpx.AsyncClient]:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
)
|
|
211
|
-
return True
|
|
212
|
-
|
|
213
|
-
transport = AsyncTenacityTransport(
|
|
214
|
-
config=RetryConfig(
|
|
215
|
-
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
216
|
-
and e.response.status_code in retry_status_codes,
|
|
217
|
-
wait=wait_retry_after(
|
|
218
|
-
fallback_strategy=wait_exponential(multiplier=1, max=60),
|
|
219
|
-
max_wait=300,
|
|
220
|
-
),
|
|
221
|
-
stop=stop_after_attempt(10),
|
|
222
|
-
reraise=True,
|
|
223
|
-
),
|
|
224
|
-
validate_response=should_retry_status,
|
|
294
|
+
config = _resolve_proxy_config(verify)
|
|
295
|
+
|
|
296
|
+
base_kwargs = {
|
|
297
|
+
"proxy": config.proxy_url,
|
|
298
|
+
"verify": config.verify,
|
|
299
|
+
"headers": headers or {},
|
|
300
|
+
"timeout": timeout,
|
|
301
|
+
"http2": config.http2_enabled,
|
|
302
|
+
"trust_env": config.trust_env,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if ReopenableAsyncClient is not None:
|
|
306
|
+
client_class = (
|
|
307
|
+
RetryingAsyncClient if not config.disable_retry else httpx.AsyncClient
|
|
225
308
|
)
|
|
226
|
-
|
|
227
|
-
if
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
verify=verify,
|
|
231
|
-
headers=headers or {},
|
|
232
|
-
timeout=timeout,
|
|
233
|
-
http2=http2_enabled,
|
|
234
|
-
)
|
|
235
|
-
else:
|
|
236
|
-
# Fallback to regular AsyncClient if ReopenableAsyncClient is not available
|
|
237
|
-
return httpx.AsyncClient(
|
|
238
|
-
transport=transport,
|
|
239
|
-
verify=verify,
|
|
240
|
-
headers=headers or {},
|
|
241
|
-
timeout=timeout,
|
|
242
|
-
http2=http2_enabled,
|
|
243
|
-
)
|
|
309
|
+
kwargs = {**base_kwargs, "client_class": client_class}
|
|
310
|
+
if not config.disable_retry:
|
|
311
|
+
kwargs["retry_status_codes"] = retry_status_codes
|
|
312
|
+
return ReopenableAsyncClient(**kwargs)
|
|
244
313
|
else:
|
|
245
|
-
# Fallback to
|
|
246
|
-
if
|
|
247
|
-
return
|
|
248
|
-
|
|
249
|
-
headers=headers or {},
|
|
250
|
-
timeout=timeout,
|
|
251
|
-
http2=http2_enabled,
|
|
314
|
+
# Fallback to RetryingAsyncClient or plain AsyncClient
|
|
315
|
+
if not config.disable_retry:
|
|
316
|
+
return RetryingAsyncClient(
|
|
317
|
+
retry_status_codes=retry_status_codes, **base_kwargs
|
|
252
318
|
)
|
|
253
319
|
else:
|
|
254
|
-
|
|
255
|
-
return httpx.AsyncClient(
|
|
256
|
-
verify=verify,
|
|
257
|
-
headers=headers or {},
|
|
258
|
-
timeout=timeout,
|
|
259
|
-
http2=http2_enabled,
|
|
260
|
-
)
|
|
320
|
+
return httpx.AsyncClient(**base_kwargs)
|
|
261
321
|
|
|
262
322
|
|
|
263
323
|
def is_cert_bundle_available() -> bool:
|
|
264
324
|
cert_path = get_cert_bundle_path()
|
|
325
|
+
if cert_path is None:
|
|
326
|
+
return False
|
|
265
327
|
return os.path.exists(cert_path) and os.path.isfile(cert_path)
|
|
266
328
|
|
|
267
329
|
|
code_puppy/keymap.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Keymap configuration for code-puppy.
|
|
2
|
+
|
|
3
|
+
This module handles configurable keyboard shortcuts, starting with the
|
|
4
|
+
cancel_agent_key feature that allows users to override Ctrl+C with a
|
|
5
|
+
different key for cancelling agent tasks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Character codes for Ctrl+letter combinations (Ctrl+A = 0x01, Ctrl+Z = 0x1A)
|
|
9
|
+
KEY_CODES: dict[str, str] = {
|
|
10
|
+
"ctrl+a": "\x01",
|
|
11
|
+
"ctrl+b": "\x02",
|
|
12
|
+
"ctrl+c": "\x03",
|
|
13
|
+
"ctrl+d": "\x04",
|
|
14
|
+
"ctrl+e": "\x05",
|
|
15
|
+
"ctrl+f": "\x06",
|
|
16
|
+
"ctrl+g": "\x07",
|
|
17
|
+
"ctrl+h": "\x08",
|
|
18
|
+
"ctrl+i": "\x09",
|
|
19
|
+
"ctrl+j": "\x0a",
|
|
20
|
+
"ctrl+k": "\x0b",
|
|
21
|
+
"ctrl+l": "\x0c",
|
|
22
|
+
"ctrl+m": "\x0d",
|
|
23
|
+
"ctrl+n": "\x0e",
|
|
24
|
+
"ctrl+o": "\x0f",
|
|
25
|
+
"ctrl+p": "\x10",
|
|
26
|
+
"ctrl+q": "\x11",
|
|
27
|
+
"ctrl+r": "\x12",
|
|
28
|
+
"ctrl+s": "\x13",
|
|
29
|
+
"ctrl+t": "\x14",
|
|
30
|
+
"ctrl+u": "\x15",
|
|
31
|
+
"ctrl+v": "\x16",
|
|
32
|
+
"ctrl+w": "\x17",
|
|
33
|
+
"ctrl+x": "\x18",
|
|
34
|
+
"ctrl+y": "\x19",
|
|
35
|
+
"ctrl+z": "\x1a",
|
|
36
|
+
"escape": "\x1b",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Valid keys for cancel_agent_key configuration
|
|
40
|
+
# NOTE: "escape" is excluded because it conflicts with ANSI escape sequences
|
|
41
|
+
# (arrow keys, F-keys, etc. all start with \x1b)
|
|
42
|
+
VALID_CANCEL_KEYS: set[str] = {
|
|
43
|
+
"ctrl+c",
|
|
44
|
+
"ctrl+k",
|
|
45
|
+
"ctrl+q",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
DEFAULT_CANCEL_AGENT_KEY: str = "ctrl+c"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class KeymapError(Exception):
|
|
52
|
+
"""Exception raised for keymap configuration errors."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_cancel_agent_key() -> str:
|
|
56
|
+
"""Get the configured cancel agent key from config.
|
|
57
|
+
|
|
58
|
+
On Windows when launched via uvx, this automatically returns "ctrl+k"
|
|
59
|
+
to work around uvx capturing Ctrl+C before it reaches Python.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The key name (e.g., "ctrl+c", "ctrl+k") from config,
|
|
63
|
+
or the default if not configured.
|
|
64
|
+
"""
|
|
65
|
+
from code_puppy.config import get_value
|
|
66
|
+
from code_puppy.uvx_detection import should_use_alternate_cancel_key
|
|
67
|
+
|
|
68
|
+
# On Windows + uvx, force ctrl+k to bypass uvx's SIGINT capture
|
|
69
|
+
if should_use_alternate_cancel_key():
|
|
70
|
+
return "ctrl+k"
|
|
71
|
+
|
|
72
|
+
key = get_value("cancel_agent_key")
|
|
73
|
+
if key is None or key.strip() == "":
|
|
74
|
+
return DEFAULT_CANCEL_AGENT_KEY
|
|
75
|
+
return key.strip().lower()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def validate_cancel_agent_key() -> None:
|
|
79
|
+
"""Validate the configured cancel agent key.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
KeymapError: If the configured key is invalid.
|
|
83
|
+
"""
|
|
84
|
+
key = get_cancel_agent_key()
|
|
85
|
+
if key not in VALID_CANCEL_KEYS:
|
|
86
|
+
valid_keys_str = ", ".join(sorted(VALID_CANCEL_KEYS))
|
|
87
|
+
raise KeymapError(
|
|
88
|
+
f"Invalid cancel_agent_key '{key}' in puppy.cfg. "
|
|
89
|
+
f"Valid options are: {valid_keys_str}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def cancel_agent_uses_signal() -> bool:
|
|
94
|
+
"""Check if the cancel agent key uses SIGINT (Ctrl+C).
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if the cancel key is ctrl+c (uses SIGINT handler),
|
|
98
|
+
False if it uses keyboard listener approach.
|
|
99
|
+
"""
|
|
100
|
+
return get_cancel_agent_key() == "ctrl+c"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_cancel_agent_char_code() -> str:
|
|
104
|
+
"""Get the character code for the cancel agent key.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The character code (e.g., "\x0b" for ctrl+k).
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
KeymapError: If the key is not found in KEY_CODES.
|
|
111
|
+
"""
|
|
112
|
+
key = get_cancel_agent_key()
|
|
113
|
+
if key not in KEY_CODES:
|
|
114
|
+
raise KeymapError(f"Unknown key '{key}' - no character code mapping found.")
|
|
115
|
+
return KEY_CODES[key]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_cancel_agent_display_name() -> str:
|
|
119
|
+
"""Get a human-readable display name for the cancel agent key.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
A formatted display name like "Ctrl+K".
|
|
123
|
+
"""
|
|
124
|
+
key = get_cancel_agent_key()
|
|
125
|
+
if key.startswith("ctrl+"):
|
|
126
|
+
letter = key.split("+")[1].upper()
|
|
127
|
+
return f"Ctrl+{letter}"
|
|
128
|
+
return key.upper()
|