code-puppy 0.0.169__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 +8 -8
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +9 -2
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +48 -9
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +146 -199
- 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 +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +1713 -1
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/json_agent.py +12 -1
- 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 +174 -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 +395 -0
- 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 +233 -627
- 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 +1 -4
- 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 +16 -27
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +3 -3
- 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 +17 -11
- code_puppy/command_line/mcp/start_all_command.py +22 -13
- code_puppy/command_line/mcp/start_command.py +50 -31
- code_puppy/command_line/mcp/status_command.py +6 -7
- code_puppy/command_line/mcp/stop_all_command.py +11 -8
- code_puppy/command_line/mcp/stop_command.py +11 -10
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/utils.py +1 -1
- code_puppy/command_line/mcp/wizard_utils.py +22 -18
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +89 -30
- 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 +626 -75
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +1181 -51
- 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 +220 -104
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -594
- code_puppy/{mcp → mcp_}/__init__.py +17 -0
- code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
- code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
- code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
- code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
- code_puppy/{mcp → mcp_}/dashboard.py +15 -6
- code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
- code_puppy/{mcp → mcp_}/managed_server.py +66 -39
- code_puppy/{mcp → mcp_}/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/{mcp → mcp_}/registry.py +6 -6
- code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
- 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 +33 -5
- code_puppy/messaging/spinner/console_spinner.py +92 -52
- code_puppy/messaging/spinner/spinner_base.py +29 -0
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +686 -80
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +86 -104
- 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 +51 -0
- 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 +10 -15
- code_puppy/session_storage.py +294 -0
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +52 -14
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +139 -6
- code_puppy/tools/agent_tools.py +548 -49
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- 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 +941 -153
- code_puppy/tools/common.py +1146 -6
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +352 -266
- 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.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
- code_puppy/agent.py +0 -231
- code_puppy/agents/agent_orchestrator.json +0 -26
- code_puppy/agents/runtime_manager.py +0 -272
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/command_line/meta_command_handler.py +0 -153
- code_puppy/message_history_processor.py +0 -490
- code_puppy/messaging/spinner/textual_spinner.py +0 -101
- code_puppy/state_management.py +0 -200
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -986
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -550
- 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 -182
- 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 -15
- 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 -290
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
- code_puppy-0.0.169.dist-info/RECORD +0 -112
- /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
- /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
- /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
- /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
- /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
- /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
code_puppy/http_utils.py
CHANGED
|
@@ -4,27 +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 retry_if_exception_type, stop_after_attempt, wait_exponential
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
import requests
|
|
18
|
+
from code_puppy.config import get_http2
|
|
19
|
+
|
|
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")
|
|
52
|
+
)
|
|
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,
|
|
21
81
|
)
|
|
22
|
-
|
|
23
|
-
# Fallback if pydantic_ai.retries is not available
|
|
24
|
-
AsyncTenacityTransport = None
|
|
25
|
-
RetryConfig = None
|
|
26
|
-
TenacityTransport = None
|
|
27
|
-
wait_retry_after = None
|
|
82
|
+
|
|
28
83
|
|
|
29
84
|
try:
|
|
30
85
|
from .reopenable_async_client import ReopenableAsyncClient
|
|
@@ -32,14 +87,104 @@ except ImportError:
|
|
|
32
87
|
ReopenableAsyncClient = None
|
|
33
88
|
|
|
34
89
|
try:
|
|
35
|
-
from .messaging import emit_info
|
|
90
|
+
from .messaging import emit_info, emit_warning
|
|
36
91
|
except ImportError:
|
|
37
92
|
# Fallback if messaging system is not available
|
|
38
93
|
def emit_info(content: str, **metadata):
|
|
39
94
|
pass # No-op if messaging system is not available
|
|
40
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
|
|
41
121
|
|
|
42
|
-
|
|
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:
|
|
43
188
|
# First check if SSL_CERT_FILE environment variable is set
|
|
44
189
|
ssl_cert_file = os.environ.get("SSL_CERT_FILE")
|
|
45
190
|
if ssl_cert_file and os.path.exists(ssl_cert_file):
|
|
@@ -55,31 +200,18 @@ def create_client(
|
|
|
55
200
|
if verify is None:
|
|
56
201
|
verify = get_cert_bundle_path()
|
|
57
202
|
|
|
203
|
+
# Check if HTTP/2 is enabled in config
|
|
204
|
+
http2_enabled = get_http2()
|
|
205
|
+
|
|
58
206
|
# If retry components are available, create a client with retry transport
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
config=RetryConfig(
|
|
68
|
-
retry=lambda e: isinstance(e, httpx.HTTPStatusError) and e.response.status_code in retry_status_codes,
|
|
69
|
-
wait=wait_retry_after(
|
|
70
|
-
fallback_strategy=wait_exponential(multiplier=1, max=60),
|
|
71
|
-
max_wait=300
|
|
72
|
-
),
|
|
73
|
-
stop=stop_after_attempt(10),
|
|
74
|
-
reraise=True
|
|
75
|
-
),
|
|
76
|
-
validate_response=should_retry_status
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
return httpx.Client(transport=transport, verify=verify, headers=headers or {}, timeout=timeout)
|
|
80
|
-
else:
|
|
81
|
-
# Fallback to regular client if retry components are not available
|
|
82
|
-
return httpx.Client(verify=verify, headers=headers or {}, timeout=timeout)
|
|
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
|
+
)
|
|
83
215
|
|
|
84
216
|
|
|
85
217
|
def create_async_client(
|
|
@@ -88,41 +220,36 @@ def create_async_client(
|
|
|
88
220
|
headers: Optional[Dict[str, str]] = None,
|
|
89
221
|
retry_status_codes: tuple = (429, 502, 503, 504),
|
|
90
222
|
) -> httpx.AsyncClient:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
transport = AsyncTenacityTransport(
|
|
103
|
-
config=RetryConfig(
|
|
104
|
-
retry=lambda e: isinstance(e, httpx.HTTPStatusError) and e.response.status_code in retry_status_codes,
|
|
105
|
-
wait=wait_retry_after(
|
|
106
|
-
fallback_strategy=wait_exponential(multiplier=1, max=60),
|
|
107
|
-
max_wait=300
|
|
108
|
-
),
|
|
109
|
-
stop=stop_after_attempt(10),
|
|
110
|
-
reraise=True
|
|
111
|
-
),
|
|
112
|
-
validate_response=should_retry_status
|
|
223
|
+
config = _resolve_proxy_config(verify)
|
|
224
|
+
|
|
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,
|
|
230
|
+
headers=headers or {},
|
|
231
|
+
timeout=timeout,
|
|
232
|
+
http2=config.http2_enabled,
|
|
233
|
+
trust_env=config.trust_env,
|
|
113
234
|
)
|
|
114
|
-
|
|
115
|
-
return httpx.AsyncClient(transport=transport, verify=verify, headers=headers or {}, timeout=timeout)
|
|
116
235
|
else:
|
|
117
|
-
|
|
118
|
-
|
|
236
|
+
return httpx.AsyncClient(
|
|
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,
|
|
243
|
+
)
|
|
119
244
|
|
|
120
245
|
|
|
121
246
|
def create_requests_session(
|
|
122
247
|
timeout: float = 5.0,
|
|
123
248
|
verify: Union[bool, str] = None,
|
|
124
249
|
headers: Optional[Dict[str, str]] = None,
|
|
125
|
-
) -> requests.Session:
|
|
250
|
+
) -> "requests.Session":
|
|
251
|
+
import requests
|
|
252
|
+
|
|
126
253
|
session = requests.Session()
|
|
127
254
|
|
|
128
255
|
if verify is None:
|
|
@@ -164,50 +291,39 @@ def create_reopenable_async_client(
|
|
|
164
291
|
headers: Optional[Dict[str, str]] = None,
|
|
165
292
|
retry_status_codes: tuple = (429, 502, 503, 504),
|
|
166
293
|
) -> Union[ReopenableAsyncClient, httpx.AsyncClient]:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
wait=wait_retry_after(
|
|
182
|
-
fallback_strategy=wait_exponential(multiplier=1, max=60),
|
|
183
|
-
max_wait=300
|
|
184
|
-
),
|
|
185
|
-
stop=stop_after_attempt(10),
|
|
186
|
-
reraise=True
|
|
187
|
-
),
|
|
188
|
-
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
|
|
189
308
|
)
|
|
190
|
-
|
|
191
|
-
if
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
)
|
|
195
|
-
else:
|
|
196
|
-
# Fallback to regular AsyncClient if ReopenableAsyncClient is not available
|
|
197
|
-
return httpx.AsyncClient(transport=transport, verify=verify, headers=headers or {}, timeout=timeout)
|
|
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)
|
|
198
313
|
else:
|
|
199
|
-
# Fallback to
|
|
200
|
-
if
|
|
201
|
-
return
|
|
202
|
-
|
|
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
|
|
203
318
|
)
|
|
204
319
|
else:
|
|
205
|
-
|
|
206
|
-
return httpx.AsyncClient(verify=verify, headers=headers or {}, timeout=timeout)
|
|
320
|
+
return httpx.AsyncClient(**base_kwargs)
|
|
207
321
|
|
|
208
322
|
|
|
209
323
|
def is_cert_bundle_available() -> bool:
|
|
210
324
|
cert_path = get_cert_bundle_path()
|
|
325
|
+
if cert_path is None:
|
|
326
|
+
return False
|
|
211
327
|
return os.path.exists(cert_path) and os.path.isfile(cert_path)
|
|
212
328
|
|
|
213
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()
|