devcopilot 0.2.0__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.
- api/__init__.py +17 -0
- api/admin_config.py +1303 -0
- api/admin_routes.py +287 -0
- api/admin_static/admin.css +459 -0
- api/admin_static/admin.js +497 -0
- api/admin_static/index.html +77 -0
- api/admin_urls.py +34 -0
- api/app.py +194 -0
- api/command_utils.py +164 -0
- api/dependencies.py +144 -0
- api/detection.py +152 -0
- api/gateway_model_ids.py +54 -0
- api/model_catalog.py +133 -0
- api/model_router.py +125 -0
- api/models/__init__.py +45 -0
- api/models/anthropic.py +234 -0
- api/models/openai_responses.py +28 -0
- api/models/responses.py +60 -0
- api/optimization_handlers.py +154 -0
- api/request_pipeline.py +424 -0
- api/routes.py +156 -0
- api/runtime.py +334 -0
- api/validation_log.py +48 -0
- api/web_server_tools.py +22 -0
- api/web_tools/__init__.py +17 -0
- api/web_tools/constants.py +15 -0
- api/web_tools/egress.py +99 -0
- api/web_tools/outbound.py +278 -0
- api/web_tools/parsers.py +104 -0
- api/web_tools/request.py +87 -0
- api/web_tools/streaming.py +206 -0
- cli/__init__.py +5 -0
- cli/claude_env.py +12 -0
- cli/entrypoints.py +166 -0
- cli/env.example +209 -0
- cli/launchers/__init__.py +1 -0
- cli/launchers/claude.py +84 -0
- cli/launchers/codex.py +204 -0
- cli/launchers/codex_model_catalog.py +186 -0
- cli/launchers/common.py +93 -0
- cli/managed/__init__.py +6 -0
- cli/managed/claude.py +215 -0
- cli/managed/manager.py +157 -0
- cli/managed/session.py +260 -0
- cli/process_registry.py +78 -0
- config/__init__.py +5 -0
- config/constants.py +13 -0
- config/logging_config.py +159 -0
- config/nim.py +118 -0
- config/paths.py +91 -0
- config/provider_catalog.py +259 -0
- config/provider_ids.py +7 -0
- config/settings.py +538 -0
- core/__init__.py +1 -0
- core/anthropic/__init__.py +46 -0
- core/anthropic/content.py +31 -0
- core/anthropic/conversion.py +587 -0
- core/anthropic/emitted_sse_tracker.py +346 -0
- core/anthropic/errors.py +70 -0
- core/anthropic/native_messages_request.py +280 -0
- core/anthropic/native_sse_block_policy.py +313 -0
- core/anthropic/provider_stream_error.py +34 -0
- core/anthropic/server_tool_sse.py +14 -0
- core/anthropic/sse.py +440 -0
- core/anthropic/stream_contracts.py +205 -0
- core/anthropic/stream_recovery.py +346 -0
- core/anthropic/stream_recovery_session.py +133 -0
- core/anthropic/thinking.py +140 -0
- core/anthropic/tokens.py +117 -0
- core/anthropic/tools.py +212 -0
- core/anthropic/utils.py +9 -0
- core/openai_responses/__init__.py +5 -0
- core/openai_responses/adapter.py +31 -0
- core/openai_responses/anthropic_sse.py +59 -0
- core/openai_responses/errors.py +22 -0
- core/openai_responses/events.py +19 -0
- core/openai_responses/ids.py +21 -0
- core/openai_responses/input.py +258 -0
- core/openai_responses/items.py +37 -0
- core/openai_responses/reasoning.py +52 -0
- core/openai_responses/stream.py +25 -0
- core/openai_responses/stream_state.py +654 -0
- core/openai_responses/tools.py +374 -0
- core/openai_responses/usage.py +37 -0
- core/rate_limit.py +60 -0
- core/trace.py +216 -0
- devcopilot-0.2.0.dist-info/METADATA +687 -0
- devcopilot-0.2.0.dist-info/RECORD +189 -0
- devcopilot-0.2.0.dist-info/WHEEL +4 -0
- devcopilot-0.2.0.dist-info/entry_points.txt +6 -0
- devcopilot-0.2.0.dist-info/licenses/LICENSE +21 -0
- messaging/__init__.py +26 -0
- messaging/cli_event_constants.py +67 -0
- messaging/command_context.py +66 -0
- messaging/command_dispatcher.py +37 -0
- messaging/commands.py +275 -0
- messaging/event_parser.py +181 -0
- messaging/limiter.py +300 -0
- messaging/models.py +36 -0
- messaging/node_event_pipeline.py +127 -0
- messaging/node_runner.py +342 -0
- messaging/platforms/__init__.py +15 -0
- messaging/platforms/base.py +228 -0
- messaging/platforms/discord.py +567 -0
- messaging/platforms/factory.py +103 -0
- messaging/platforms/outbox.py +144 -0
- messaging/platforms/telegram.py +688 -0
- messaging/platforms/voice_flow.py +295 -0
- messaging/rendering/__init__.py +3 -0
- messaging/rendering/discord_markdown.py +318 -0
- messaging/rendering/markdown_tables.py +49 -0
- messaging/rendering/profiles.py +55 -0
- messaging/rendering/telegram_markdown.py +327 -0
- messaging/safe_diagnostics.py +17 -0
- messaging/session.py +334 -0
- messaging/transcript.py +581 -0
- messaging/transcription.py +164 -0
- messaging/trees/__init__.py +15 -0
- messaging/trees/data.py +482 -0
- messaging/trees/manager.py +433 -0
- messaging/trees/processor.py +179 -0
- messaging/trees/repository.py +177 -0
- messaging/turn_intake.py +235 -0
- messaging/ui_updates.py +101 -0
- messaging/voice.py +76 -0
- messaging/workflow.py +200 -0
- providers/__init__.py +31 -0
- providers/base.py +152 -0
- providers/cerebras/__init__.py +7 -0
- providers/cerebras/client.py +31 -0
- providers/cerebras/request.py +55 -0
- providers/codestral/__init__.py +7 -0
- providers/codestral/client.py +34 -0
- providers/deepseek/__init__.py +11 -0
- providers/deepseek/client.py +51 -0
- providers/deepseek/request.py +475 -0
- providers/defaults.py +41 -0
- providers/error_mapping.py +309 -0
- providers/exceptions.py +113 -0
- providers/fireworks/__init__.py +5 -0
- providers/fireworks/client.py +45 -0
- providers/fireworks/request.py +48 -0
- providers/gemini/__init__.py +7 -0
- providers/gemini/client.py +49 -0
- providers/gemini/request.py +199 -0
- providers/groq/__init__.py +7 -0
- providers/groq/client.py +31 -0
- providers/groq/request.py +83 -0
- providers/kimi/__init__.py +10 -0
- providers/kimi/client.py +53 -0
- providers/kimi/request.py +42 -0
- providers/llamacpp/__init__.py +3 -0
- providers/llamacpp/client.py +16 -0
- providers/lmstudio/__init__.py +5 -0
- providers/lmstudio/client.py +16 -0
- providers/mistral/__init__.py +7 -0
- providers/mistral/client.py +31 -0
- providers/mistral/request.py +37 -0
- providers/model_listing.py +133 -0
- providers/nvidia_nim/__init__.py +7 -0
- providers/nvidia_nim/client.py +91 -0
- providers/nvidia_nim/request.py +430 -0
- providers/nvidia_nim/voice.py +95 -0
- providers/ollama/__init__.py +7 -0
- providers/ollama/client.py +39 -0
- providers/open_router/__init__.py +7 -0
- providers/open_router/client.py +124 -0
- providers/open_router/request.py +42 -0
- providers/opencode/__init__.py +11 -0
- providers/opencode/client.py +31 -0
- providers/opencode/request.py +35 -0
- providers/rate_limit.py +300 -0
- providers/registry.py +527 -0
- providers/transports/__init__.py +1 -0
- providers/transports/anthropic_messages/__init__.py +5 -0
- providers/transports/anthropic_messages/http.py +118 -0
- providers/transports/anthropic_messages/recovery.py +206 -0
- providers/transports/anthropic_messages/stream.py +295 -0
- providers/transports/anthropic_messages/transport.py +236 -0
- providers/transports/openai_chat/__init__.py +5 -0
- providers/transports/openai_chat/recovery.py +217 -0
- providers/transports/openai_chat/stream.py +384 -0
- providers/transports/openai_chat/tool_calls.py +293 -0
- providers/transports/openai_chat/transport.py +156 -0
- providers/wafer/__init__.py +10 -0
- providers/wafer/client.py +50 -0
- providers/zai/__init__.py +10 -0
- providers/zai/client.py +46 -0
- providers/zai/request.py +42 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Platform rendering profiles for messaging transcripts and status text."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from messaging.rendering.discord_markdown import (
|
|
9
|
+
discord_bold,
|
|
10
|
+
discord_code_inline,
|
|
11
|
+
escape_discord,
|
|
12
|
+
escape_discord_code,
|
|
13
|
+
render_markdown_to_discord,
|
|
14
|
+
)
|
|
15
|
+
from messaging.rendering.discord_markdown import (
|
|
16
|
+
format_status as format_status_discord,
|
|
17
|
+
)
|
|
18
|
+
from messaging.rendering.telegram_markdown import (
|
|
19
|
+
escape_md_v2,
|
|
20
|
+
escape_md_v2_code,
|
|
21
|
+
mdv2_bold,
|
|
22
|
+
mdv2_code_inline,
|
|
23
|
+
render_markdown_to_mdv2,
|
|
24
|
+
)
|
|
25
|
+
from messaging.rendering.telegram_markdown import (
|
|
26
|
+
format_status as format_status_telegram,
|
|
27
|
+
)
|
|
28
|
+
from messaging.transcript import RenderCtx
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class RenderingProfile:
|
|
33
|
+
format_status: Callable[[str, str, str | None], str]
|
|
34
|
+
parse_mode: str | None
|
|
35
|
+
render_ctx: RenderCtx
|
|
36
|
+
limit_chars: int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_rendering_profile(platform_name: str) -> RenderingProfile:
|
|
40
|
+
"""Return rendering rules for a messaging platform."""
|
|
41
|
+
is_discord = platform_name == "discord"
|
|
42
|
+
return RenderingProfile(
|
|
43
|
+
format_status=format_status_discord if is_discord else format_status_telegram,
|
|
44
|
+
parse_mode=None if is_discord else "MarkdownV2",
|
|
45
|
+
render_ctx=RenderCtx(
|
|
46
|
+
bold=discord_bold if is_discord else mdv2_bold,
|
|
47
|
+
code_inline=discord_code_inline if is_discord else mdv2_code_inline,
|
|
48
|
+
escape_code=escape_discord_code if is_discord else escape_md_v2_code,
|
|
49
|
+
escape_text=escape_discord if is_discord else escape_md_v2,
|
|
50
|
+
render_markdown=render_markdown_to_discord
|
|
51
|
+
if is_discord
|
|
52
|
+
else render_markdown_to_mdv2,
|
|
53
|
+
),
|
|
54
|
+
limit_chars=1900 if is_discord else 3900,
|
|
55
|
+
)
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Telegram MarkdownV2 utilities.
|
|
2
|
+
|
|
3
|
+
Renders common Markdown into Telegram MarkdownV2 format.
|
|
4
|
+
Used by the message handler and Telegram platform adapter.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from markdown_it import MarkdownIt
|
|
8
|
+
|
|
9
|
+
from .markdown_tables import normalize_gfm_tables
|
|
10
|
+
|
|
11
|
+
MDV2_SPECIAL_CHARS = set("\\_*[]()~`>#+-=|{}.!")
|
|
12
|
+
MDV2_LINK_ESCAPE = set("\\)")
|
|
13
|
+
|
|
14
|
+
_MD = MarkdownIt("commonmark", {"html": False, "breaks": False})
|
|
15
|
+
_MD.enable("strikethrough")
|
|
16
|
+
_MD.enable("table")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def escape_md_v2(text: str) -> str:
|
|
20
|
+
"""Escape text for Telegram MarkdownV2."""
|
|
21
|
+
return "".join(f"\\{ch}" if ch in MDV2_SPECIAL_CHARS else ch for ch in text)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def escape_md_v2_code(text: str) -> str:
|
|
25
|
+
"""Escape text for Telegram MarkdownV2 code spans/blocks."""
|
|
26
|
+
return text.replace("\\", "\\\\").replace("`", "\\`")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def escape_md_v2_link_url(text: str) -> str:
|
|
30
|
+
"""Escape URL for Telegram MarkdownV2 link destination."""
|
|
31
|
+
return "".join(f"\\{ch}" if ch in MDV2_LINK_ESCAPE else ch for ch in text)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def mdv2_bold(text: str) -> str:
|
|
35
|
+
"""Format text as bold in MarkdownV2."""
|
|
36
|
+
return f"*{escape_md_v2(text)}*"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def mdv2_code_inline(text: str) -> str:
|
|
40
|
+
"""Format text as inline code in MarkdownV2."""
|
|
41
|
+
return f"`{escape_md_v2_code(text)}`"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def format_status(emoji: str, label: str, suffix: str | None = None) -> str:
|
|
45
|
+
"""Format a status message with emoji and optional suffix."""
|
|
46
|
+
base = f"{emoji} {mdv2_bold(label)}"
|
|
47
|
+
if suffix:
|
|
48
|
+
return f"{base} {escape_md_v2(suffix)}"
|
|
49
|
+
return base
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def render_markdown_to_mdv2(text: str) -> str:
|
|
53
|
+
"""Render common Markdown into Telegram MarkdownV2."""
|
|
54
|
+
if not text:
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
text = normalize_gfm_tables(text)
|
|
58
|
+
tokens = _MD.parse(text)
|
|
59
|
+
|
|
60
|
+
def render_inline_table_plain(children) -> str:
|
|
61
|
+
out: list[str] = []
|
|
62
|
+
for tok in children:
|
|
63
|
+
if tok.type == "text" or tok.type == "code_inline":
|
|
64
|
+
out.append(tok.content)
|
|
65
|
+
elif tok.type in {"softbreak", "hardbreak"}:
|
|
66
|
+
out.append(" ")
|
|
67
|
+
elif tok.type == "image" and tok.content:
|
|
68
|
+
out.append(tok.content)
|
|
69
|
+
return "".join(out)
|
|
70
|
+
|
|
71
|
+
def render_inline_plain(children) -> str:
|
|
72
|
+
out: list[str] = []
|
|
73
|
+
for tok in children:
|
|
74
|
+
if tok.type == "text" or tok.type == "code_inline":
|
|
75
|
+
out.append(escape_md_v2(tok.content))
|
|
76
|
+
elif tok.type in {"softbreak", "hardbreak"}:
|
|
77
|
+
out.append("\n")
|
|
78
|
+
return "".join(out)
|
|
79
|
+
|
|
80
|
+
def render_inline(children) -> str:
|
|
81
|
+
out: list[str] = []
|
|
82
|
+
i = 0
|
|
83
|
+
while i < len(children):
|
|
84
|
+
tok = children[i]
|
|
85
|
+
t = tok.type
|
|
86
|
+
if t == "text":
|
|
87
|
+
out.append(escape_md_v2(tok.content))
|
|
88
|
+
elif t in {"softbreak", "hardbreak"}:
|
|
89
|
+
out.append("\n")
|
|
90
|
+
elif t == "em_open" or t == "em_close":
|
|
91
|
+
out.append("_")
|
|
92
|
+
elif t == "strong_open" or t == "strong_close":
|
|
93
|
+
out.append("*")
|
|
94
|
+
elif t == "s_open" or t == "s_close":
|
|
95
|
+
out.append("~")
|
|
96
|
+
elif t == "code_inline":
|
|
97
|
+
out.append(f"`{escape_md_v2_code(tok.content)}`")
|
|
98
|
+
elif t == "link_open":
|
|
99
|
+
href = ""
|
|
100
|
+
if tok.attrs:
|
|
101
|
+
if isinstance(tok.attrs, dict):
|
|
102
|
+
href = tok.attrs.get("href", "")
|
|
103
|
+
else:
|
|
104
|
+
for key, val in tok.attrs:
|
|
105
|
+
if key == "href":
|
|
106
|
+
href = val
|
|
107
|
+
break
|
|
108
|
+
inner_tokens = []
|
|
109
|
+
i += 1
|
|
110
|
+
while i < len(children) and children[i].type != "link_close":
|
|
111
|
+
inner_tokens.append(children[i])
|
|
112
|
+
i += 1
|
|
113
|
+
link_text = ""
|
|
114
|
+
for child in inner_tokens:
|
|
115
|
+
if child.type == "text" or child.type == "code_inline":
|
|
116
|
+
link_text += child.content
|
|
117
|
+
out.append(
|
|
118
|
+
f"[{escape_md_v2(link_text)}]({escape_md_v2_link_url(href)})"
|
|
119
|
+
)
|
|
120
|
+
elif t == "image":
|
|
121
|
+
href = ""
|
|
122
|
+
alt = tok.content or ""
|
|
123
|
+
if tok.attrs:
|
|
124
|
+
if isinstance(tok.attrs, dict):
|
|
125
|
+
href = tok.attrs.get("src", "")
|
|
126
|
+
else:
|
|
127
|
+
for key, val in tok.attrs:
|
|
128
|
+
if key == "src":
|
|
129
|
+
href = val
|
|
130
|
+
break
|
|
131
|
+
if alt:
|
|
132
|
+
out.append(f"{escape_md_v2(alt)} ({escape_md_v2_link_url(href)})")
|
|
133
|
+
else:
|
|
134
|
+
out.append(escape_md_v2_link_url(href))
|
|
135
|
+
else:
|
|
136
|
+
out.append(escape_md_v2(tok.content or ""))
|
|
137
|
+
i += 1
|
|
138
|
+
return "".join(out)
|
|
139
|
+
|
|
140
|
+
out: list[str] = []
|
|
141
|
+
list_stack: list[dict] = []
|
|
142
|
+
pending_prefix: str | None = None
|
|
143
|
+
blockquote_level = 0
|
|
144
|
+
in_heading = False
|
|
145
|
+
|
|
146
|
+
def apply_blockquote(val: str) -> str:
|
|
147
|
+
if blockquote_level <= 0:
|
|
148
|
+
return val
|
|
149
|
+
prefix = "> " * blockquote_level
|
|
150
|
+
return prefix + val.replace("\n", "\n" + prefix)
|
|
151
|
+
|
|
152
|
+
i = 0
|
|
153
|
+
while i < len(tokens):
|
|
154
|
+
tok = tokens[i]
|
|
155
|
+
t = tok.type
|
|
156
|
+
if t == "paragraph_open":
|
|
157
|
+
pass
|
|
158
|
+
elif t == "paragraph_close":
|
|
159
|
+
out.append("\n")
|
|
160
|
+
elif t == "heading_open":
|
|
161
|
+
in_heading = True
|
|
162
|
+
elif t == "heading_close":
|
|
163
|
+
in_heading = False
|
|
164
|
+
out.append("\n")
|
|
165
|
+
elif t == "bullet_list_open":
|
|
166
|
+
list_stack.append({"type": "bullet", "index": 1})
|
|
167
|
+
elif t == "bullet_list_close":
|
|
168
|
+
if list_stack:
|
|
169
|
+
list_stack.pop()
|
|
170
|
+
out.append("\n")
|
|
171
|
+
elif t == "ordered_list_open":
|
|
172
|
+
start = 1
|
|
173
|
+
if tok.attrs:
|
|
174
|
+
if isinstance(tok.attrs, dict):
|
|
175
|
+
val = tok.attrs.get("start")
|
|
176
|
+
if val is not None:
|
|
177
|
+
try:
|
|
178
|
+
start = int(val)
|
|
179
|
+
except TypeError, ValueError:
|
|
180
|
+
start = 1
|
|
181
|
+
else:
|
|
182
|
+
for key, val in tok.attrs:
|
|
183
|
+
if key == "start":
|
|
184
|
+
try:
|
|
185
|
+
start = int(val)
|
|
186
|
+
except TypeError, ValueError:
|
|
187
|
+
start = 1
|
|
188
|
+
break
|
|
189
|
+
list_stack.append({"type": "ordered", "index": start})
|
|
190
|
+
elif t == "ordered_list_close":
|
|
191
|
+
if list_stack:
|
|
192
|
+
list_stack.pop()
|
|
193
|
+
out.append("\n")
|
|
194
|
+
elif t == "list_item_open":
|
|
195
|
+
if list_stack:
|
|
196
|
+
top = list_stack[-1]
|
|
197
|
+
if top["type"] == "bullet":
|
|
198
|
+
pending_prefix = "\\- "
|
|
199
|
+
else:
|
|
200
|
+
pending_prefix = f"{top['index']}\\."
|
|
201
|
+
top["index"] += 1
|
|
202
|
+
pending_prefix += " "
|
|
203
|
+
elif t == "list_item_close":
|
|
204
|
+
out.append("\n")
|
|
205
|
+
elif t == "blockquote_open":
|
|
206
|
+
blockquote_level += 1
|
|
207
|
+
elif t == "blockquote_close":
|
|
208
|
+
blockquote_level = max(0, blockquote_level - 1)
|
|
209
|
+
out.append("\n")
|
|
210
|
+
elif t == "table_open":
|
|
211
|
+
if pending_prefix:
|
|
212
|
+
out.append(apply_blockquote(pending_prefix.rstrip()))
|
|
213
|
+
out.append("\n")
|
|
214
|
+
pending_prefix = None
|
|
215
|
+
|
|
216
|
+
rows: list[list[str]] = []
|
|
217
|
+
row_is_header: list[bool] = []
|
|
218
|
+
|
|
219
|
+
j = i + 1
|
|
220
|
+
in_thead = False
|
|
221
|
+
in_row = False
|
|
222
|
+
current_row: list[str] = []
|
|
223
|
+
current_row_header = False
|
|
224
|
+
|
|
225
|
+
in_cell = False
|
|
226
|
+
cell_parts: list[str] = []
|
|
227
|
+
|
|
228
|
+
while j < len(tokens):
|
|
229
|
+
tt = tokens[j].type
|
|
230
|
+
if tt == "thead_open":
|
|
231
|
+
in_thead = True
|
|
232
|
+
elif tt == "thead_close":
|
|
233
|
+
in_thead = False
|
|
234
|
+
elif tt == "tr_open":
|
|
235
|
+
in_row = True
|
|
236
|
+
current_row = []
|
|
237
|
+
current_row_header = in_thead
|
|
238
|
+
elif tt in {"th_open", "td_open"}:
|
|
239
|
+
in_cell = True
|
|
240
|
+
cell_parts = []
|
|
241
|
+
elif tt == "inline" and in_cell:
|
|
242
|
+
cell_parts.append(
|
|
243
|
+
render_inline_table_plain(tokens[j].children or [])
|
|
244
|
+
)
|
|
245
|
+
elif tt in {"th_close", "td_close"} and in_cell:
|
|
246
|
+
cell = " ".join(cell_parts).strip()
|
|
247
|
+
current_row.append(cell)
|
|
248
|
+
in_cell = False
|
|
249
|
+
cell_parts = []
|
|
250
|
+
elif tt == "tr_close" and in_row:
|
|
251
|
+
rows.append(current_row)
|
|
252
|
+
row_is_header.append(bool(current_row_header))
|
|
253
|
+
in_row = False
|
|
254
|
+
elif tt == "table_close":
|
|
255
|
+
break
|
|
256
|
+
j += 1
|
|
257
|
+
|
|
258
|
+
if rows:
|
|
259
|
+
col_count = max((len(r) for r in rows), default=0)
|
|
260
|
+
norm_rows: list[list[str]] = []
|
|
261
|
+
for r in rows:
|
|
262
|
+
if len(r) < col_count:
|
|
263
|
+
r = r + [""] * (col_count - len(r))
|
|
264
|
+
norm_rows.append(r)
|
|
265
|
+
|
|
266
|
+
widths: list[int] = []
|
|
267
|
+
for c in range(col_count):
|
|
268
|
+
w = max((len(r[c]) for r in norm_rows), default=0)
|
|
269
|
+
widths.append(max(w, 3))
|
|
270
|
+
|
|
271
|
+
def fmt_row(
|
|
272
|
+
r: list[str], _w: list[int] = widths, _c: int = col_count
|
|
273
|
+
) -> str:
|
|
274
|
+
cells = [r[c].ljust(_w[c]) for c in range(_c)]
|
|
275
|
+
return "| " + " | ".join(cells) + " |"
|
|
276
|
+
|
|
277
|
+
def fmt_sep(_w: list[int] = widths, _c: int = col_count) -> str:
|
|
278
|
+
cells = ["-" * _w[c] for c in range(_c)]
|
|
279
|
+
return "| " + " | ".join(cells) + " |"
|
|
280
|
+
|
|
281
|
+
last_header_idx = -1
|
|
282
|
+
for idx, is_h in enumerate(row_is_header):
|
|
283
|
+
if is_h:
|
|
284
|
+
last_header_idx = idx
|
|
285
|
+
|
|
286
|
+
lines: list[str] = []
|
|
287
|
+
for idx, r in enumerate(norm_rows):
|
|
288
|
+
lines.append(fmt_row(r))
|
|
289
|
+
if idx == last_header_idx:
|
|
290
|
+
lines.append(fmt_sep())
|
|
291
|
+
|
|
292
|
+
table_text = "\n".join(lines).rstrip()
|
|
293
|
+
out.append(f"```\n{escape_md_v2_code(table_text)}\n```")
|
|
294
|
+
out.append("\n")
|
|
295
|
+
|
|
296
|
+
i = j + 1
|
|
297
|
+
continue
|
|
298
|
+
elif t in {"code_block", "fence"}:
|
|
299
|
+
code = escape_md_v2_code(tok.content.rstrip("\n"))
|
|
300
|
+
out.append(f"```\n{code}\n```")
|
|
301
|
+
out.append("\n")
|
|
302
|
+
elif t == "inline":
|
|
303
|
+
rendered = render_inline(tok.children or [])
|
|
304
|
+
if in_heading:
|
|
305
|
+
rendered = f"*{render_inline_plain(tok.children or [])}*"
|
|
306
|
+
if pending_prefix:
|
|
307
|
+
rendered = pending_prefix + rendered
|
|
308
|
+
pending_prefix = None
|
|
309
|
+
rendered = apply_blockquote(rendered)
|
|
310
|
+
out.append(rendered)
|
|
311
|
+
else:
|
|
312
|
+
if tok.content:
|
|
313
|
+
out.append(escape_md_v2(tok.content))
|
|
314
|
+
i += 1
|
|
315
|
+
|
|
316
|
+
return "".join(out).rstrip()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
__all__ = [
|
|
320
|
+
"escape_md_v2",
|
|
321
|
+
"escape_md_v2_code",
|
|
322
|
+
"escape_md_v2_link_url",
|
|
323
|
+
"format_status",
|
|
324
|
+
"mdv2_bold",
|
|
325
|
+
"mdv2_code_inline",
|
|
326
|
+
"render_markdown_to_mdv2",
|
|
327
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Helpers for redacting user-derived content from log lines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def format_exception_for_log(exc: BaseException, *, log_full_message: bool) -> str:
|
|
7
|
+
"""Return exception type and optionally ``str(exc)`` for operator diagnostics."""
|
|
8
|
+
if log_full_message:
|
|
9
|
+
return f"{type(exc).__name__}: {exc}"
|
|
10
|
+
return type(exc).__name__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def text_len_hint(text: str | None) -> int:
|
|
14
|
+
"""Length of text for metadata-only logging (0 when missing)."""
|
|
15
|
+
if not text:
|
|
16
|
+
return 0
|
|
17
|
+
return len(text)
|