code-puppy 0.0.302__py3-none-any.whl → 0.0.335__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/agents/base_agent.py +343 -35
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/cli_runner.py +898 -0
- code_puppy/command_line/add_model_menu.py +23 -1
- code_puppy/command_line/autosave_menu.py +271 -35
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +8 -2
- code_puppy/command_line/config_commands.py +82 -10
- code_puppy/command_line/core_commands.py +70 -7
- code_puppy/command_line/diff_menu.py +5 -0
- code_puppy/command_line/mcp/custom_server_form.py +4 -0
- code_puppy/command_line/mcp/edit_command.py +3 -1
- code_puppy/command_line/mcp/handler.py +7 -2
- code_puppy/command_line/mcp/install_command.py +8 -3
- code_puppy/command_line/mcp/install_menu.py +5 -1
- code_puppy/command_line/mcp/logs_command.py +173 -64
- code_puppy/command_line/mcp/restart_command.py +7 -2
- code_puppy/command_line/mcp/search_command.py +10 -4
- code_puppy/command_line/mcp/start_all_command.py +16 -6
- code_puppy/command_line/mcp/start_command.py +3 -1
- code_puppy/command_line/mcp/status_command.py +2 -1
- code_puppy/command_line/mcp/stop_all_command.py +5 -1
- code_puppy/command_line/mcp/stop_command.py +3 -1
- code_puppy/command_line/mcp/wizard_utils.py +10 -4
- code_puppy/command_line/model_settings_menu.py +58 -7
- code_puppy/command_line/motd.py +13 -7
- code_puppy/command_line/onboarding_slides.py +180 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/prompt_toolkit_completion.py +16 -2
- code_puppy/command_line/session_commands.py +11 -4
- code_puppy/config.py +106 -17
- code_puppy/http_utils.py +155 -196
- code_puppy/keymap.py +8 -0
- code_puppy/main.py +5 -828
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/blocking_startup.py +61 -32
- code_puppy/mcp_/config_wizard.py +5 -1
- code_puppy/mcp_/managed_server.py +23 -3
- code_puppy/mcp_/manager.py +65 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/messaging/__init__.py +20 -4
- code_puppy/messaging/bus.py +64 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/messages.py +16 -0
- code_puppy/messaging/renderers.py +21 -9
- code_puppy/messaging/rich_renderer.py +113 -67
- code_puppy/messaging/spinner/console_spinner.py +34 -0
- code_puppy/model_factory.py +271 -45
- code_puppy/model_utils.py +57 -48
- code_puppy/models.json +21 -7
- code_puppy/plugins/__init__.py +12 -0
- 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 +612 -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 +595 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/config.py +5 -1
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +5 -3
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +26 -11
- code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +30 -0
- code_puppy/plugins/claude_code_oauth/utils.py +1 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -118
- code_puppy/plugins/shell_safety/register_callbacks.py +44 -3
- 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/terminal_utils.py +291 -0
- code_puppy/tools/agent_tools.py +34 -9
- code_puppy/tools/command_runner.py +344 -27
- code_puppy/tools/file_operations.py +33 -45
- code_puppy/uvx_detection.py +242 -0
- {code_puppy-0.0.302.data → code_puppy-0.0.335.data}/data/code_puppy/models.json +21 -7
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/METADATA +30 -1
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/RECORD +87 -64
- {code_puppy-0.0.302.data → code_puppy-0.0.335.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/licenses/LICENSE +0 -0
code_puppy/config.py
CHANGED
|
@@ -53,6 +53,7 @@ _DEFAULT_SQLITE_FILE = os.path.join(DATA_DIR, "dbos_store.sqlite")
|
|
|
53
53
|
GEMINI_MODELS_FILE = os.path.join(DATA_DIR, "gemini_models.json")
|
|
54
54
|
CHATGPT_MODELS_FILE = os.path.join(DATA_DIR, "chatgpt_models.json")
|
|
55
55
|
CLAUDE_MODELS_FILE = os.path.join(DATA_DIR, "claude_models.json")
|
|
56
|
+
ANTIGRAVITY_MODELS_FILE = os.path.join(DATA_DIR, "antigravity_models.json")
|
|
56
57
|
|
|
57
58
|
# Cache files (XDG_CACHE_HOME)
|
|
58
59
|
AUTOSAVE_DIR = os.path.join(CACHE_DIR, "autosaves")
|
|
@@ -212,6 +213,9 @@ def get_config_keys():
|
|
|
212
213
|
default_keys.append("enable_dbos")
|
|
213
214
|
# Add cancel agent key configuration
|
|
214
215
|
default_keys.append("cancel_agent_key")
|
|
216
|
+
# Add banner color keys
|
|
217
|
+
for banner_name in DEFAULT_BANNER_COLORS:
|
|
218
|
+
default_keys.append(f"banner_color_{banner_name}")
|
|
215
219
|
|
|
216
220
|
config = configparser.ConfigParser()
|
|
217
221
|
config.read(CONFIG_FILE)
|
|
@@ -256,9 +260,8 @@ def load_mcp_server_configs():
|
|
|
256
260
|
def _default_model_from_models_json():
|
|
257
261
|
"""Load the default model name from models.json.
|
|
258
262
|
|
|
259
|
-
|
|
260
|
-
Falls back to
|
|
261
|
-
As a last resort, falls back to ``gpt-5`` if the file cannot be read.
|
|
263
|
+
Returns the first model in models.json as the default.
|
|
264
|
+
Falls back to ``gpt-5`` if the file cannot be read.
|
|
262
265
|
"""
|
|
263
266
|
global _default_model_cache
|
|
264
267
|
|
|
@@ -270,11 +273,7 @@ def _default_model_from_models_json():
|
|
|
270
273
|
|
|
271
274
|
models_config = ModelFactory.load_config()
|
|
272
275
|
if models_config:
|
|
273
|
-
#
|
|
274
|
-
if "synthetic-GLM-4.6" in models_config:
|
|
275
|
-
_default_model_cache = "synthetic-GLM-4.6"
|
|
276
|
-
return "synthetic-GLM-4.6"
|
|
277
|
-
# Fall back to first model if synthetic-GLM-4.6 is not available
|
|
276
|
+
# Use first model in models.json as default
|
|
278
277
|
first_key = next(iter(models_config))
|
|
279
278
|
_default_model_cache = first_key
|
|
280
279
|
return first_key
|
|
@@ -497,8 +496,8 @@ def set_puppy_token(token: str):
|
|
|
497
496
|
|
|
498
497
|
|
|
499
498
|
def get_openai_reasoning_effort() -> str:
|
|
500
|
-
"""Return the configured OpenAI reasoning effort (low, medium, high)."""
|
|
501
|
-
allowed_values = {"low", "medium", "high"}
|
|
499
|
+
"""Return the configured OpenAI reasoning effort (minimal, low, medium, high, xhigh)."""
|
|
500
|
+
allowed_values = {"minimal", "low", "medium", "high", "xhigh"}
|
|
502
501
|
configured = (get_value("openai_reasoning_effort") or "medium").strip().lower()
|
|
503
502
|
if configured not in allowed_values:
|
|
504
503
|
return "medium"
|
|
@@ -507,7 +506,7 @@ def get_openai_reasoning_effort() -> str:
|
|
|
507
506
|
|
|
508
507
|
def set_openai_reasoning_effort(value: str) -> None:
|
|
509
508
|
"""Persist the OpenAI reasoning effort ensuring it remains within allowed values."""
|
|
510
|
-
allowed_values = {"low", "medium", "high"}
|
|
509
|
+
allowed_values = {"minimal", "low", "medium", "high", "xhigh"}
|
|
511
510
|
normalized = (value or "").strip().lower()
|
|
512
511
|
if normalized not in allowed_values:
|
|
513
512
|
raise ValueError(
|
|
@@ -658,10 +657,22 @@ def get_all_model_settings(model_name: str) -> dict:
|
|
|
658
657
|
for key, val in config[DEFAULT_SECTION].items():
|
|
659
658
|
if key.startswith(prefix) and val.strip():
|
|
660
659
|
setting_name = key[len(prefix) :]
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
660
|
+
# Handle different value types
|
|
661
|
+
val_stripped = val.strip()
|
|
662
|
+
# Check for boolean values first
|
|
663
|
+
if val_stripped.lower() in ("true", "false"):
|
|
664
|
+
settings[setting_name] = val_stripped.lower() == "true"
|
|
665
|
+
else:
|
|
666
|
+
# Try to parse as number (int first, then float)
|
|
667
|
+
try:
|
|
668
|
+
# Try int first for cleaner values like budget_tokens
|
|
669
|
+
if "." not in val_stripped:
|
|
670
|
+
settings[setting_name] = int(val_stripped)
|
|
671
|
+
else:
|
|
672
|
+
settings[setting_name] = float(val_stripped)
|
|
673
|
+
except (ValueError, TypeError):
|
|
674
|
+
# Keep as string if not a number
|
|
675
|
+
settings[setting_name] = val_stripped
|
|
665
676
|
|
|
666
677
|
return settings
|
|
667
678
|
|
|
@@ -1041,11 +1052,11 @@ def set_enable_dbos(enabled: bool) -> None:
|
|
|
1041
1052
|
set_config_value("enable_dbos", "true" if enabled else "false")
|
|
1042
1053
|
|
|
1043
1054
|
|
|
1044
|
-
def get_message_limit(default: int =
|
|
1055
|
+
def get_message_limit(default: int = 1000) -> int:
|
|
1045
1056
|
"""
|
|
1046
1057
|
Returns the user-configured message/request limit for the agent.
|
|
1047
1058
|
This controls how many steps/requests the agent can take.
|
|
1048
|
-
Defaults to
|
|
1059
|
+
Defaults to 1000 if unset or misconfigured.
|
|
1049
1060
|
Configurable by 'message_limit' key.
|
|
1050
1061
|
"""
|
|
1051
1062
|
val = get_value("message_limit")
|
|
@@ -1257,6 +1268,84 @@ def set_diff_deletion_color(color: str):
|
|
|
1257
1268
|
set_config_value("highlight_deletion_color", color)
|
|
1258
1269
|
|
|
1259
1270
|
|
|
1271
|
+
# =============================================================================
|
|
1272
|
+
# Banner Color Configuration
|
|
1273
|
+
# =============================================================================
|
|
1274
|
+
|
|
1275
|
+
# Default banner colors (Rich color names)
|
|
1276
|
+
# A beautiful jewel-tone palette with semantic meaning:
|
|
1277
|
+
# - Blues/Teals: Reading & navigation (calm, informational)
|
|
1278
|
+
# - Warm tones: Actions & changes (edits, shell commands)
|
|
1279
|
+
# - Purples: AI thinking & reasoning (the "brain" colors)
|
|
1280
|
+
# - Greens: Completions & success
|
|
1281
|
+
# - Neutrals: Search & listings
|
|
1282
|
+
DEFAULT_BANNER_COLORS = {
|
|
1283
|
+
"thinking": "deep_sky_blue4", # Sapphire - contemplation
|
|
1284
|
+
"agent_response": "medium_purple4", # Amethyst - main AI output
|
|
1285
|
+
"shell_command": "dark_orange3", # Amber - system commands
|
|
1286
|
+
"read_file": "steel_blue", # Steel - reading files
|
|
1287
|
+
"edit_file": "dark_goldenrod", # Gold - modifications
|
|
1288
|
+
"grep": "grey37", # Silver - search results
|
|
1289
|
+
"directory_listing": "dodger_blue2", # Sky - navigation
|
|
1290
|
+
"agent_reasoning": "dark_violet", # Violet - deep thought
|
|
1291
|
+
"invoke_agent": "deep_pink4", # Ruby - agent invocation
|
|
1292
|
+
"subagent_response": "sea_green3", # Emerald - sub-agent success
|
|
1293
|
+
"list_agents": "dark_slate_gray3", # Slate - neutral listing
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
def get_banner_color(banner_name: str) -> str:
|
|
1298
|
+
"""Get the background color for a specific banner.
|
|
1299
|
+
|
|
1300
|
+
Args:
|
|
1301
|
+
banner_name: The banner identifier (e.g., 'thinking', 'agent_response')
|
|
1302
|
+
|
|
1303
|
+
Returns:
|
|
1304
|
+
Rich color name or hex code for the banner background
|
|
1305
|
+
"""
|
|
1306
|
+
config_key = f"banner_color_{banner_name}"
|
|
1307
|
+
val = get_value(config_key)
|
|
1308
|
+
if val:
|
|
1309
|
+
return val
|
|
1310
|
+
return DEFAULT_BANNER_COLORS.get(banner_name, "blue")
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
def set_banner_color(banner_name: str, color: str):
|
|
1314
|
+
"""Set the background color for a specific banner.
|
|
1315
|
+
|
|
1316
|
+
Args:
|
|
1317
|
+
banner_name: The banner identifier (e.g., 'thinking', 'agent_response')
|
|
1318
|
+
color: Rich color name or hex code
|
|
1319
|
+
"""
|
|
1320
|
+
config_key = f"banner_color_{banner_name}"
|
|
1321
|
+
set_config_value(config_key, color)
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
def get_all_banner_colors() -> dict:
|
|
1325
|
+
"""Get all banner colors (configured or default).
|
|
1326
|
+
|
|
1327
|
+
Returns:
|
|
1328
|
+
Dict mapping banner names to their colors
|
|
1329
|
+
"""
|
|
1330
|
+
return {name: get_banner_color(name) for name in DEFAULT_BANNER_COLORS}
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
def reset_banner_color(banner_name: str):
|
|
1334
|
+
"""Reset a banner color to its default.
|
|
1335
|
+
|
|
1336
|
+
Args:
|
|
1337
|
+
banner_name: The banner identifier to reset
|
|
1338
|
+
"""
|
|
1339
|
+
default_color = DEFAULT_BANNER_COLORS.get(banner_name, "blue")
|
|
1340
|
+
set_banner_color(banner_name, default_color)
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
def reset_all_banner_colors():
|
|
1344
|
+
"""Reset all banner colors to their defaults."""
|
|
1345
|
+
for name, color in DEFAULT_BANNER_COLORS.items():
|
|
1346
|
+
set_banner_color(name, color)
|
|
1347
|
+
|
|
1348
|
+
|
|
1260
1349
|
def get_current_autosave_id() -> str:
|
|
1261
1350
|
"""Get or create the current autosave session ID for this process."""
|
|
1262
1351
|
global _CURRENT_AUTOSAVE_ID
|
code_puppy/http_utils.py
CHANGED
|
@@ -4,29 +4,19 @@ 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
|
|
8
|
+
import logging
|
|
7
9
|
import os
|
|
8
10
|
import socket
|
|
9
|
-
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any, Dict, Optional, Union
|
|
10
13
|
|
|
11
14
|
import httpx
|
|
12
15
|
import requests
|
|
13
|
-
from tenacity import stop_after_attempt, wait_exponential
|
|
14
16
|
|
|
15
17
|
from code_puppy.config import get_http2
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
from pydantic_ai.retries import (
|
|
19
|
-
AsyncTenacityTransport,
|
|
20
|
-
RetryConfig,
|
|
21
|
-
TenacityTransport,
|
|
22
|
-
wait_retry_after,
|
|
23
|
-
)
|
|
24
|
-
except ImportError:
|
|
25
|
-
# Fallback if pydantic_ai.retries is not available
|
|
26
|
-
AsyncTenacityTransport = None
|
|
27
|
-
RetryConfig = None
|
|
28
|
-
TenacityTransport = None
|
|
29
|
-
wait_retry_after = None
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
30
20
|
|
|
31
21
|
try:
|
|
32
22
|
from .reopenable_async_client import ReopenableAsyncClient
|
|
@@ -34,12 +24,109 @@ except ImportError:
|
|
|
34
24
|
ReopenableAsyncClient = None
|
|
35
25
|
|
|
36
26
|
try:
|
|
37
|
-
from .messaging import emit_info
|
|
27
|
+
from .messaging import emit_info, emit_warning
|
|
38
28
|
except ImportError:
|
|
39
29
|
# Fallback if messaging system is not available
|
|
40
30
|
def emit_info(content: str, **metadata):
|
|
41
31
|
pass # No-op if messaging system is not available
|
|
42
32
|
|
|
33
|
+
def emit_warning(content: str, **metadata):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RetryingAsyncClient(httpx.AsyncClient):
|
|
38
|
+
"""AsyncClient with built-in rate limit handling (429) and retries.
|
|
39
|
+
|
|
40
|
+
This replaces the Tenacity transport with a more direct subclass implementation,
|
|
41
|
+
which plays nicer with proxies and custom transports (like Antigravity).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
retry_status_codes: tuple = (429, 502, 503, 504),
|
|
47
|
+
max_retries: int = 5,
|
|
48
|
+
**kwargs,
|
|
49
|
+
):
|
|
50
|
+
super().__init__(**kwargs)
|
|
51
|
+
self.retry_status_codes = retry_status_codes
|
|
52
|
+
self.max_retries = max_retries
|
|
53
|
+
|
|
54
|
+
async def send(self, request: httpx.Request, **kwargs: Any) -> httpx.Response:
|
|
55
|
+
"""Send request with automatic retries for rate limits and server errors."""
|
|
56
|
+
last_response = None
|
|
57
|
+
last_exception = None
|
|
58
|
+
|
|
59
|
+
for attempt in range(self.max_retries + 1):
|
|
60
|
+
try:
|
|
61
|
+
# Clone request for retry (streams might be consumed)
|
|
62
|
+
# But only if it's not the first attempt
|
|
63
|
+
req_to_send = request
|
|
64
|
+
if attempt > 0:
|
|
65
|
+
# httpx requests are reusable, but we need to be careful with streams
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
response = await super().send(req_to_send, **kwargs)
|
|
69
|
+
last_response = response
|
|
70
|
+
|
|
71
|
+
# Check for retryable status
|
|
72
|
+
if response.status_code not in self.retry_status_codes:
|
|
73
|
+
return response
|
|
74
|
+
|
|
75
|
+
# Close response if we're going to retry
|
|
76
|
+
await response.aclose()
|
|
77
|
+
|
|
78
|
+
# Determine wait time
|
|
79
|
+
wait_time = 1.0 * (
|
|
80
|
+
2**attempt
|
|
81
|
+
) # Default exponential backoff: 1s, 2s, 4s...
|
|
82
|
+
|
|
83
|
+
# Check Retry-After header
|
|
84
|
+
retry_after = response.headers.get("Retry-After")
|
|
85
|
+
if retry_after:
|
|
86
|
+
try:
|
|
87
|
+
wait_time = float(retry_after)
|
|
88
|
+
except ValueError:
|
|
89
|
+
# Try parsing http-date
|
|
90
|
+
from email.utils import parsedate_to_datetime
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
date = parsedate_to_datetime(retry_after)
|
|
94
|
+
wait_time = date.timestamp() - time.time()
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
# Cap wait time
|
|
99
|
+
wait_time = max(0.5, min(wait_time, 60.0))
|
|
100
|
+
|
|
101
|
+
if attempt < self.max_retries:
|
|
102
|
+
emit_info(
|
|
103
|
+
f"HTTP retry: {response.status_code} received. Waiting {wait_time:.1f}s (attempt {attempt + 1}/{self.max_retries})"
|
|
104
|
+
)
|
|
105
|
+
await asyncio.sleep(wait_time)
|
|
106
|
+
|
|
107
|
+
except (httpx.ConnectError, httpx.ReadTimeout, httpx.PoolTimeout) as e:
|
|
108
|
+
last_exception = e
|
|
109
|
+
wait_time = 1.0 * (2**attempt)
|
|
110
|
+
if attempt < self.max_retries:
|
|
111
|
+
emit_warning(
|
|
112
|
+
f"HTTP connection error: {e}. Retrying in {wait_time}s..."
|
|
113
|
+
)
|
|
114
|
+
await asyncio.sleep(wait_time)
|
|
115
|
+
else:
|
|
116
|
+
raise
|
|
117
|
+
except Exception:
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
# Return last response (even if it's an error status)
|
|
121
|
+
if last_response:
|
|
122
|
+
return last_response
|
|
123
|
+
|
|
124
|
+
# Should catch this in loop, but just in case
|
|
125
|
+
if last_exception:
|
|
126
|
+
raise last_exception
|
|
127
|
+
|
|
128
|
+
return last_response
|
|
129
|
+
|
|
43
130
|
|
|
44
131
|
def get_cert_bundle_path() -> str:
|
|
45
132
|
# First check if SSL_CERT_FILE environment variable is set
|
|
@@ -60,53 +147,15 @@ def create_client(
|
|
|
60
147
|
# Check if HTTP/2 is enabled in config
|
|
61
148
|
http2_enabled = get_http2()
|
|
62
149
|
|
|
63
|
-
# Check if custom retry transport should be disabled (e.g., for integration tests with proxies)
|
|
64
|
-
disable_retry_transport = os.environ.get(
|
|
65
|
-
"CODE_PUPPY_DISABLE_RETRY_TRANSPORT", ""
|
|
66
|
-
).lower() in ("1", "true", "yes")
|
|
67
|
-
|
|
68
150
|
# If retry components are available, create a client with retry transport
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"""Raise exceptions for retryable HTTP status codes."""
|
|
78
|
-
if response.status_code in retry_status_codes:
|
|
79
|
-
emit_info(
|
|
80
|
-
f"HTTP retry: Retrying request due to status code {response.status_code}"
|
|
81
|
-
)
|
|
82
|
-
return True
|
|
83
|
-
|
|
84
|
-
transport = TenacityTransport(
|
|
85
|
-
config=RetryConfig(
|
|
86
|
-
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
87
|
-
and e.response.status_code in retry_status_codes,
|
|
88
|
-
wait=wait_retry_after(
|
|
89
|
-
fallback_strategy=wait_exponential(multiplier=1, max=60),
|
|
90
|
-
max_wait=300,
|
|
91
|
-
),
|
|
92
|
-
stop=stop_after_attempt(10),
|
|
93
|
-
reraise=True,
|
|
94
|
-
),
|
|
95
|
-
validate_response=should_retry_status,
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
return httpx.Client(
|
|
99
|
-
transport=transport,
|
|
100
|
-
verify=verify,
|
|
101
|
-
headers=headers or {},
|
|
102
|
-
timeout=timeout,
|
|
103
|
-
http2=http2_enabled,
|
|
104
|
-
)
|
|
105
|
-
else:
|
|
106
|
-
# Fallback to regular client if retry components are not available
|
|
107
|
-
return httpx.Client(
|
|
108
|
-
verify=verify, headers=headers or {}, timeout=timeout, http2=http2_enabled
|
|
109
|
-
)
|
|
151
|
+
# Note: TenacityTransport was removed. For now we just return a standard client.
|
|
152
|
+
# Future TODO: Implement RetryingClient(httpx.Client) if needed.
|
|
153
|
+
return httpx.Client(
|
|
154
|
+
verify=verify,
|
|
155
|
+
headers=headers or {},
|
|
156
|
+
timeout=timeout,
|
|
157
|
+
http2=http2_enabled,
|
|
158
|
+
)
|
|
110
159
|
|
|
111
160
|
|
|
112
161
|
def create_async_client(
|
|
@@ -145,52 +194,21 @@ def create_async_client(
|
|
|
145
194
|
else:
|
|
146
195
|
trust_env = False
|
|
147
196
|
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
and not has_proxy
|
|
157
|
-
):
|
|
158
|
-
|
|
159
|
-
def should_retry_status(response):
|
|
160
|
-
"""Raise exceptions for retryable HTTP status codes."""
|
|
161
|
-
if response.status_code in retry_status_codes:
|
|
162
|
-
emit_info(
|
|
163
|
-
f"HTTP retry: Retrying request due to status code {response.status_code}"
|
|
164
|
-
)
|
|
165
|
-
return True
|
|
166
|
-
|
|
167
|
-
# Create transport (with or without proxy base)
|
|
168
|
-
if has_proxy:
|
|
169
|
-
# Extract proxy URL from environment
|
|
170
|
-
proxy_url = (
|
|
171
|
-
os.environ.get("HTTPS_PROXY")
|
|
172
|
-
or os.environ.get("https_proxy")
|
|
173
|
-
or os.environ.get("HTTP_PROXY")
|
|
174
|
-
or os.environ.get("http_proxy")
|
|
175
|
-
)
|
|
176
|
-
else:
|
|
177
|
-
proxy_url = None
|
|
178
|
-
|
|
179
|
-
# Create retry transport wrapper
|
|
180
|
-
transport = AsyncTenacityTransport(
|
|
181
|
-
config=RetryConfig(
|
|
182
|
-
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
183
|
-
and e.response.status_code in retry_status_codes,
|
|
184
|
-
wait=wait_retry_after(10),
|
|
185
|
-
stop=stop_after_attempt(10),
|
|
186
|
-
reraise=True,
|
|
187
|
-
),
|
|
188
|
-
validate_response=should_retry_status,
|
|
197
|
+
# Extract proxy URL if needed
|
|
198
|
+
proxy_url = None
|
|
199
|
+
if has_proxy:
|
|
200
|
+
proxy_url = (
|
|
201
|
+
os.environ.get("HTTPS_PROXY")
|
|
202
|
+
or os.environ.get("https_proxy")
|
|
203
|
+
or os.environ.get("HTTP_PROXY")
|
|
204
|
+
or os.environ.get("http_proxy")
|
|
189
205
|
)
|
|
190
206
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
207
|
+
# Use RetryingAsyncClient if retries are enabled
|
|
208
|
+
if not disable_retry_transport:
|
|
209
|
+
return RetryingAsyncClient(
|
|
210
|
+
retry_status_codes=retry_status_codes,
|
|
211
|
+
proxy=proxy_url,
|
|
194
212
|
verify=verify,
|
|
195
213
|
headers=headers or {},
|
|
196
214
|
timeout=timeout,
|
|
@@ -198,19 +216,7 @@ def create_async_client(
|
|
|
198
216
|
trust_env=trust_env,
|
|
199
217
|
)
|
|
200
218
|
else:
|
|
201
|
-
#
|
|
202
|
-
# when retry transport is explicitly disabled, or when proxies are detected
|
|
203
|
-
# Extract proxy URL if needed
|
|
204
|
-
if has_proxy:
|
|
205
|
-
proxy_url = (
|
|
206
|
-
os.environ.get("HTTPS_PROXY")
|
|
207
|
-
or os.environ.get("https_proxy")
|
|
208
|
-
or os.environ.get("HTTP_PROXY")
|
|
209
|
-
or os.environ.get("http_proxy")
|
|
210
|
-
)
|
|
211
|
-
else:
|
|
212
|
-
proxy_url = None
|
|
213
|
-
|
|
219
|
+
# Regular client for testing
|
|
214
220
|
return httpx.AsyncClient(
|
|
215
221
|
proxy=proxy_url,
|
|
216
222
|
verify=verify,
|
|
@@ -295,87 +301,41 @@ def create_reopenable_async_client(
|
|
|
295
301
|
else:
|
|
296
302
|
trust_env = False
|
|
297
303
|
|
|
298
|
-
#
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
):
|
|
304
|
+
# Extract proxy URL if needed
|
|
305
|
+
proxy_url = None
|
|
306
|
+
if has_proxy:
|
|
307
|
+
proxy_url = (
|
|
308
|
+
os.environ.get("HTTPS_PROXY")
|
|
309
|
+
or os.environ.get("https_proxy")
|
|
310
|
+
or os.environ.get("HTTP_PROXY")
|
|
311
|
+
or os.environ.get("http_proxy")
|
|
312
|
+
)
|
|
308
313
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
f"HTTP retry: Retrying request due to status code {response.status_code}"
|
|
314
|
-
)
|
|
315
|
-
return True
|
|
316
|
-
|
|
317
|
-
transport = AsyncTenacityTransport(
|
|
318
|
-
config=RetryConfig(
|
|
319
|
-
retry=lambda e: isinstance(e, httpx.HTTPStatusError)
|
|
320
|
-
and e.response.status_code in retry_status_codes,
|
|
321
|
-
wait=wait_retry_after(
|
|
322
|
-
fallback_strategy=wait_exponential(multiplier=1, max=60),
|
|
323
|
-
max_wait=300,
|
|
324
|
-
),
|
|
325
|
-
stop=stop_after_attempt(10),
|
|
326
|
-
reraise=True,
|
|
327
|
-
),
|
|
328
|
-
validate_response=should_retry_status,
|
|
314
|
+
if ReopenableAsyncClient is not None:
|
|
315
|
+
# Use RetryingAsyncClient if retries are enabled
|
|
316
|
+
client_class = (
|
|
317
|
+
RetryingAsyncClient if not disable_retry_transport else httpx.AsyncClient
|
|
329
318
|
)
|
|
330
319
|
|
|
331
|
-
#
|
|
332
|
-
|
|
333
|
-
proxy_url
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
proxy_url = None
|
|
320
|
+
# Pass retry config only if using RetryingAsyncClient
|
|
321
|
+
kwargs = {
|
|
322
|
+
"proxy": proxy_url,
|
|
323
|
+
"verify": verify,
|
|
324
|
+
"headers": headers or {},
|
|
325
|
+
"timeout": timeout,
|
|
326
|
+
"http2": http2_enabled,
|
|
327
|
+
"trust_env": trust_env,
|
|
328
|
+
}
|
|
341
329
|
|
|
342
|
-
if
|
|
343
|
-
|
|
344
|
-
transport=transport,
|
|
345
|
-
proxy=proxy_url,
|
|
346
|
-
verify=verify,
|
|
347
|
-
headers=headers or {},
|
|
348
|
-
timeout=timeout,
|
|
349
|
-
http2=http2_enabled,
|
|
350
|
-
trust_env=trust_env,
|
|
351
|
-
)
|
|
352
|
-
else:
|
|
353
|
-
# Fallback to regular AsyncClient if ReopenableAsyncClient is not available
|
|
354
|
-
return httpx.AsyncClient(
|
|
355
|
-
transport=transport,
|
|
356
|
-
proxy=proxy_url,
|
|
357
|
-
verify=verify,
|
|
358
|
-
headers=headers or {},
|
|
359
|
-
timeout=timeout,
|
|
360
|
-
http2=http2_enabled,
|
|
361
|
-
trust_env=trust_env,
|
|
362
|
-
)
|
|
363
|
-
else:
|
|
364
|
-
# Fallback to regular clients if retry components are not available
|
|
365
|
-
# or when proxies are detected
|
|
366
|
-
# Extract proxy URL if needed
|
|
367
|
-
if has_proxy:
|
|
368
|
-
proxy_url = (
|
|
369
|
-
os.environ.get("HTTPS_PROXY")
|
|
370
|
-
or os.environ.get("https_proxy")
|
|
371
|
-
or os.environ.get("HTTP_PROXY")
|
|
372
|
-
or os.environ.get("http_proxy")
|
|
373
|
-
)
|
|
374
|
-
else:
|
|
375
|
-
proxy_url = None
|
|
330
|
+
if not disable_retry_transport:
|
|
331
|
+
kwargs["retry_status_codes"] = retry_status_codes
|
|
376
332
|
|
|
377
|
-
|
|
378
|
-
|
|
333
|
+
return ReopenableAsyncClient(client_class=client_class, **kwargs)
|
|
334
|
+
else:
|
|
335
|
+
# Fallback to RetryingAsyncClient
|
|
336
|
+
if not disable_retry_transport:
|
|
337
|
+
return RetryingAsyncClient(
|
|
338
|
+
retry_status_codes=retry_status_codes,
|
|
379
339
|
proxy=proxy_url,
|
|
380
340
|
verify=verify,
|
|
381
341
|
headers=headers or {},
|
|
@@ -384,7 +344,6 @@ def create_reopenable_async_client(
|
|
|
384
344
|
trust_env=trust_env,
|
|
385
345
|
)
|
|
386
346
|
else:
|
|
387
|
-
# Fallback to regular AsyncClient if ReopenableAsyncClient is not available
|
|
388
347
|
return httpx.AsyncClient(
|
|
389
348
|
proxy=proxy_url,
|
|
390
349
|
verify=verify,
|
code_puppy/keymap.py
CHANGED
|
@@ -55,11 +55,19 @@ class KeymapError(Exception):
|
|
|
55
55
|
def get_cancel_agent_key() -> str:
|
|
56
56
|
"""Get the configured cancel agent key from config.
|
|
57
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
|
+
|
|
58
61
|
Returns:
|
|
59
62
|
The key name (e.g., "ctrl+c", "ctrl+k") from config,
|
|
60
63
|
or the default if not configured.
|
|
61
64
|
"""
|
|
62
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"
|
|
63
71
|
|
|
64
72
|
key = get_value("cancel_agent_key")
|
|
65
73
|
if key is None or key.strip() == "":
|