Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
common.py864 linesDownload Raw Back to console
1import enum2import math3import platform4from collections.abc import Iterable5from functools import lru_cache6 7import urwid.util8from publicsuffix2 import get_sld9from publicsuffix2 import get_tld10 11from mitmproxy import dns12from mitmproxy import flow13from mitmproxy.dns import DNSFlow14from mitmproxy.http import HTTPFlow15from mitmproxy.tcp import TCPFlow16from mitmproxy.udp import UDPFlow17from mitmproxy.utils import emoji18from mitmproxy.utils import human19 20# Detect Windows Subsystem for Linux and Windows21IS_WINDOWS_OR_WSL = (22    "Microsoft" in platform.platform() or "Windows" in platform.platform()23)24 25 26def is_keypress(k):27    """28    Is this input event a keypress?29    """30    if isinstance(k, str):31        return True32 33 34def highlight_key(text, key, textattr="text", keyattr="key"):35    lst = []36    parts = text.split(key, 1)37    if parts[0]:38        lst.append((textattr, parts[0]))39    lst.append((keyattr, key))40    if parts[1]:41        lst.append((textattr, parts[1]))42    return lst43 44 45KEY_MAX = 3046 47 48def format_keyvals(49    entries: Iterable[tuple[str, None | str | urwid.Widget]],50    key_format: str = "key",51    value_format: str = "text",52    indent: int = 0,53) -> list[urwid.Columns]:54    """55    Format a list of (key, value) tuples.56 57    Args:58        entries: The list to format. keys must be strings, values can also be None or urwid widgets.59            The latter makes it possible to use the result of format_keyvals() as a value.60        key_format: The display attribute for the key.61        value_format: The display attribute for the value.62        indent: Additional indent to apply.63    """64    max_key_len = max((len(k) for k, v in entries if k is not None), default=0)65    max_key_len = min(max_key_len, KEY_MAX)66 67    if indent > 2:68        indent -= 2  # We use dividechars=2 below, which already adds two empty spaces69 70    ret = []71    for k, v in entries:72        if v is None:73            v = urwid.Text("")74        elif not isinstance(v, urwid.Widget):75            v = urwid.Text([(value_format, v)])76        ret.append(77            urwid.Columns(78                [79                    ("fixed", indent, urwid.Text("")),80                    ("fixed", max_key_len, urwid.Text([(key_format, k)])),81                    v,82                ],83                dividechars=2,84            )85        )86    return ret87 88 89def fcol(s: str, attr: str) -> tuple[str, int, urwid.Text]:90    s = str(s)91    return ("fixed", len(s), urwid.Text([(attr, s)]))92 93 94if urwid.util.detected_encoding:95    SYMBOL_REPLAY = "\u21ba"96    SYMBOL_RETURN = "\u2190"97    SYMBOL_MARK = "\u25cf"98    SYMBOL_UP = "\u21e7"99    SYMBOL_DOWN = "\u21e9"100    SYMBOL_ELLIPSIS = "\u2026"101    SYMBOL_FROM_CLIENT = "\u21d2"102    SYMBOL_TO_CLIENT = "\u21d0"103else:104    SYMBOL_REPLAY = "[r]"105    SYMBOL_RETURN = "<-"106    SYMBOL_MARK = "#"107    SYMBOL_UP = "^"108    SYMBOL_DOWN = " "109    SYMBOL_ELLIPSIS = "~"110    SYMBOL_FROM_CLIENT = "->"111    SYMBOL_TO_CLIENT = "<-"112 113SCHEME_STYLES = {114    "http": "scheme_http",115    "https": "scheme_https",116    "ws": "scheme_ws",117    "wss": "scheme_wss",118    "tcp": "scheme_tcp",119    "udp": "scheme_udp",120    "dns": "scheme_dns",121    "quic": "scheme_quic",122}123HTTP_REQUEST_METHOD_STYLES = {124    "GET": "method_get",125    "POST": "method_post",126    "DELETE": "method_delete",127    "HEAD": "method_head",128    "PUT": "method_put",129}130HTTP_RESPONSE_CODE_STYLE = {131    2: "code_200",132    3: "code_300",133    4: "code_400",134    5: "code_500",135}136 137 138class RenderMode(enum.Enum):139    TABLE = 1140    """The flow list in table format, i.e. one row per flow."""141    LIST = 2142    """The flow list in list format, i.e. potentially multiple rows per flow."""143    DETAILVIEW = 3144    """The top lines in the detail view."""145 146 147def fixlen(s: str, maxlen: int) -> str:148    if len(s) <= maxlen:149        return s.ljust(maxlen)150    else:151        return s[0 : maxlen - len(SYMBOL_ELLIPSIS)] + SYMBOL_ELLIPSIS152 153 154def fixlen_r(s: str, maxlen: int) -> str:155    if len(s) <= maxlen:156        return s.rjust(maxlen)157    else:158        return SYMBOL_ELLIPSIS + s[len(s) - maxlen + len(SYMBOL_ELLIPSIS) :]159 160 161def render_marker(marker: str) -> str:162    rendered = emoji.emoji.get(marker, SYMBOL_MARK)163 164    # The marker can only be one glyph. Some emoji that use zero-width joiners (ZWJ)165    # will not be rendered as a single glyph and instead will show166    # multiple glyphs. Just use the first glyph as a fallback.167    # https://emojipedia.org/emoji-zwj-sequence/168    return rendered[0]169 170 171class TruncatedText(urwid.Widget):172    def __init__(self, text, attr, align="left"):173        self.text = text174        self.attr = attr175        self.align = align176        super().__init__()177 178    def pack(self, size, focus=False):179        return (len(self.text), 1)180 181    def rows(self, size, focus=False):182        return 1183 184    def render(self, size, focus=False):185        text = self.text186        attr = self.attr187        if self.align == "right":188            text = text[::-1]189            attr = attr[::-1]190 191        text_len = urwid.calc_width(text, 0, len(text))192        if size is not None and len(size) > 0:193            width = size[0]194        else:195            width = text_len196 197        if width >= text_len:198            remaining = width - text_len199            if remaining > 0:200                c_text = text + " " * remaining201                c_attr = attr + [("text", remaining)]202            else:203                c_text = text204                c_attr = attr205        else:206            trim = urwid.util.calc_trim_text(text, 0, width - 1, 0, width - 1)207            visible_text = text[0 : trim[1]]208            if trim[3] == 1:209                visible_text += " "210            c_text = visible_text + SYMBOL_ELLIPSIS211            c_attr = urwid.util.rle_subseg(attr, 0, len(visible_text.encode())) + [212                ("focus", len(SYMBOL_ELLIPSIS.encode()))213            ]214 215        if self.align == "right":216            c_text = c_text[::-1]217            c_attr = c_attr[::-1]218 219        return urwid.TextCanvas([c_text.encode()], [c_attr], maxcol=width)220 221 222def truncated_plain(text, attr, align="left"):223    return TruncatedText(text, [(attr, len(text.encode()))], align)224 225 226# Work around https://github.com/urwid/urwid/pull/330227def rle_append_beginning_modify(rle, a_r):228    """229    Append (a, r) (unpacked from *a_r*) to BEGINNING of rle.230    Merge with first run when possible231 232    MODIFIES rle parameter contents. Returns None.233    """234    a, r = a_r235    if not rle:236        rle[:] = [(a, r)]237    else:238        al, run = rle[0]239        if a == al:240            rle[0] = (a, run + r)241        else:242            rle[0:0] = [(a, r)]243 244 245def colorize_host(host: str):246    if not host:247        return []248 249    tld = get_tld(host)250    sld = get_sld(host)251 252    attr: list = []253 254    tld_size = len(tld)255    sld_size = len(sld) - tld_size256 257    for letter in reversed(range(len(host))):258        character = host[letter]259        if tld_size > 0:260            style = "url_domain"261            tld_size -= 1262        elif tld_size == 0:263            style = "text"264            tld_size -= 1265        elif sld_size > 0:266            sld_size -= 1267            style = "url_extension"268        else:269            style = "text"270        rle_append_beginning_modify(attr, (style, len(character.encode())))271    return attr272 273 274def colorize_req(s: str):275    path = s.split("?", 2)[0]276    i_query = len(path)277    i_last_slash = path.rfind("/")278    i_ext = path[i_last_slash + 1 :].rfind(".")279    i_ext = i_last_slash + i_ext if i_ext >= 0 else len(s)280    in_val = False281    attr: list = []282    for i in range(len(s)):283        c = s[i]284        if (285            (i < i_query and c == "/")286            or (i < i_query and i > i_last_slash and c == ".")287            or (i == i_query)288        ):289            a = "url_punctuation"290        elif i > i_query:291            if in_val:292                if c == "&":293                    in_val = False294                    a = "url_punctuation"295                else:296                    a = "url_query_value"297            else:298                if c == "=":299                    in_val = True300                    a = "url_punctuation"301                else:302                    a = "url_query_key"303        elif i > i_ext:304            a = "url_extension"305        elif i > i_last_slash:306            a = "url_filename"307        else:308            a = "text"309        urwid.util.rle_append_modify(attr, (a, len(c.encode())))310    return attr311 312 313def format_http_content_type(content_type: str) -> tuple[str, str]:314    content_type = content_type.split(";")[0]315    if content_type.endswith("/javascript"):316        style = "content_script"317    elif content_type.startswith("text/"):318        style = "content_text"319    elif (320        content_type.startswith("image/")321        or content_type.startswith("video/")322        or content_type.startswith("font/")323        or "/x-font-" in content_type324    ):325        style = "content_media"326    elif content_type.endswith("/json") or content_type.endswith("/xml"):327        style = "content_data"328    elif content_type.startswith("application/"):329        style = "content_raw"330    else:331        style = "content_other"332    return content_type, style333 334 335def format_duration(duration: float) -> tuple[str, str]:336    pretty_duration = human.pretty_duration(duration)337    style = "gradient_%02d" % int(338        99 - 100 * min(math.log2(max(1.0, 1000 * duration)) / 12, 0.99)339    )340    return pretty_duration, style341 342 343def format_size(num_bytes: int) -> tuple[str, str]:344    pretty_size = human.pretty_size(num_bytes)345    style = "gradient_%02d" % int(99 - 100 * min(math.log2(1 + num_bytes) / 20, 0.99))346    return pretty_size, style347 348 349def format_left_indicators(*, focused: bool, intercepted: bool, timestamp: float):350    indicators: list[str | tuple[str, str]] = []351    if focused:352        indicators.append(("focus", ">>"))353    else:354        indicators.append("  ")355    pretty_timestamp = human.format_timestamp(timestamp)[-8:]356    if intercepted:357        indicators.append(("intercept", pretty_timestamp))358    else:359        indicators.append(("text", pretty_timestamp))360    return "fixed", 10, urwid.Text(indicators)361 362 363def format_right_indicators(364    *,365    replay: bool,366    marked: str,367):368    indicators: list[str | tuple[str, str]] = []369    if replay:370        indicators.append(("replay", SYMBOL_REPLAY))371    else:372        indicators.append(" ")373    if bool(marked):374        indicators.append(("mark", render_marker(marked)))375    else:376        indicators.append("  ")377    return "fixed", 3, urwid.Text(indicators)378 379 380@lru_cache(maxsize=800)381def format_http_flow_list(382    *,383    render_mode: RenderMode,384    focused: bool,385    marked: str,386    is_replay: bool,387    request_method: str,388    request_scheme: str,389    request_host: str,390    request_path: str,391    request_url: str,392    request_http_version: str,393    request_timestamp: float,394    request_is_push_promise: bool,395    intercepted: bool,396    response_code: int | None,397    response_reason: str | None,398    response_content_length: int | None,399    response_content_type: str | None,400    duration: float | None,401    error_message: str | None,402) -> urwid.Widget:403    req = []404 405    if render_mode is RenderMode.DETAILVIEW:406        req.append(fcol(human.format_timestamp(request_timestamp), "highlight"))407    else:408        if focused:409            req.append(fcol(">>", "focus"))410        else:411            req.append(fcol("  ", "focus"))412 413    method_style = HTTP_REQUEST_METHOD_STYLES.get(request_method, "method_other")414    req.append(fcol(request_method, method_style))415 416    if request_is_push_promise:417        req.append(fcol("PUSH_PROMISE", "method_http2_push"))418 419    preamble_len = sum(x[1] for x in req) + len(req) - 1420 421    if request_http_version not in ("HTTP/1.0", "HTTP/1.1"):422        request_url += " " + request_http_version423    if intercepted and not response_code:424        url_style = "intercept"425    elif response_code or error_message:426        url_style = "text"427    else:428        url_style = "title"429 430    if render_mode is RenderMode.DETAILVIEW:431        req.append(urwid.Text([(url_style, request_url)]))432    else:433        req.append(truncated_plain(request_url, url_style))434 435    req.append(format_right_indicators(replay=is_replay, marked=marked))436 437    resp = [("fixed", preamble_len, urwid.Text(""))]438    if response_code:439        if intercepted:440            style = "intercept"441        else:442            style = ""443 444        status_style = style or HTTP_RESPONSE_CODE_STYLE.get(445            response_code // 100, "code_other"446        )447        resp.append(fcol(SYMBOL_RETURN, status_style))448        resp.append(fcol(str(response_code), status_style))449        if response_reason and render_mode is RenderMode.DETAILVIEW:450            resp.append(fcol(response_reason, status_style))451 452        if response_content_type:453            ct, ct_style = format_http_content_type(response_content_type)454            resp.append(fcol(ct, style or ct_style))455 456        if response_content_length:457            size, size_style = format_size(response_content_length)458        elif response_content_length == 0:459            size = "[no content]"460            size_style = "text"461        else:462            size = "[content missing]"463            size_style = "text"464        resp.append(fcol(size, style or size_style))465 466        if duration:467            dur, dur_style = format_duration(duration)468            resp.append(fcol(dur, style or dur_style))469    elif error_message:470        resp.append(fcol(SYMBOL_RETURN, "error"))471        resp.append(urwid.Text([("error", error_message)]))472 473    return urwid.Pile(474        [urwid.Columns(req, dividechars=1), urwid.Columns(resp, dividechars=1)]475    )476 477 478@lru_cache(maxsize=800)479def format_http_flow_table(480    *,481    render_mode: RenderMode,482    focused: bool,483    marked: str,484    is_replay: str | None,485    request_method: str,486    request_scheme: str,487    request_host: str,488    request_path: str,489    request_url: str,490    request_http_version: str,491    request_timestamp: float,492    request_is_push_promise: bool,493    intercepted: bool,494    response_code: int | None,495    response_reason: str | None,496    response_content_length: int | None,497    response_content_type: str | None,498    duration: float | None,499    error_message: str | None,500) -> urwid.Widget:501    items = [502        format_left_indicators(503            focused=focused, intercepted=intercepted, timestamp=request_timestamp504        )505    ]506 507    if intercepted and not response_code:508        request_style = "intercept"509    else:510        request_style = ""511 512    scheme_style = request_style or SCHEME_STYLES.get(request_scheme, "scheme_other")513    items.append(fcol(fixlen(request_scheme.upper(), 5), scheme_style))514 515    if request_is_push_promise:516        method_style = "method_http2_push"517    else:518        method_style = request_style or HTTP_REQUEST_METHOD_STYLES.get(519            request_method, "method_other"520        )521    items.append(fcol(fixlen(request_method, 4), method_style))522 523    items.append(524        (525            "weight",526            0.25,527            TruncatedText(request_host, colorize_host(request_host), "right"),528        )529    )530    items.append(531        ("weight", 1.0, TruncatedText(request_path, colorize_req(request_path), "left"))532    )533 534    if intercepted and response_code:535        response_style = "intercept"536    else:537        response_style = ""538 539    if response_code:540        status = str(response_code)541        status_style = response_style or HTTP_RESPONSE_CODE_STYLE.get(542            response_code // 100, "code_other"543        )544 545        if response_content_length and response_content_type:546            content, content_style = format_http_content_type(response_content_type)547            content_style = response_style or content_style548        elif response_content_length:549            content = ""550            content_style = "content_none"551        elif response_content_length == 0:552            content = "[no content]"553            content_style = "content_none"554        else:555            content = "[content missing]"556            content_style = "content_none"557 558    elif error_message:559        status = "err"560        status_style = "error"561        content = error_message562        content_style = "error"563 564    else:565        status = ""566        status_style = "text"567        content = ""568        content_style = ""569 570    items.append(fcol(fixlen(status, 3), status_style))571    items.append(("weight", 0.15, truncated_plain(content, content_style, "right")))572 573    if response_content_length:574        size, size_style = format_size(response_content_length)575        items.append(fcol(fixlen_r(size, 5), response_style or size_style))576    else:577        items.append(("fixed", 5, urwid.Text("")))578 579    if duration:580        duration_pretty, duration_style = format_duration(duration)581        items.append(582            fcol(fixlen_r(duration_pretty, 5), response_style or duration_style)583        )584    else:585        items.append(("fixed", 5, urwid.Text("")))586 587    items.append(588        format_right_indicators(589            replay=bool(is_replay),590            marked=marked,591        )592    )593    return urwid.Columns(items, dividechars=1, min_width=15)594 595 596@lru_cache(maxsize=800)597def format_message_flow(598    *,599    render_mode: RenderMode,600    focused: bool,601    timestamp_start: float,602    marked: str,603    protocol: str,604    client_address,605    server_address,606    total_size: int,607    duration: float | None,608    error_message: str | None,609):610    conn = f"{human.format_address(client_address)} <-> {human.format_address(server_address)}"611 612    items = []613 614    if render_mode in (RenderMode.TABLE, RenderMode.DETAILVIEW):615        items.append(616            format_left_indicators(617                focused=focused, intercepted=False, timestamp=timestamp_start618            )619        )620    else:621        if focused:622            items.append(fcol(">>", "focus"))623        else:624            items.append(fcol("  ", "focus"))625 626    if render_mode is RenderMode.TABLE:627        items.append(fcol(fixlen(protocol.upper(), 5), SCHEME_STYLES[protocol]))628    else:629        items.append(fcol(protocol.upper(), SCHEME_STYLES[protocol]))630 631    items.append(("weight", 1.0, truncated_plain(conn, "text", "left")))632    if error_message:633        items.append(("weight", 1.0, truncated_plain(error_message, "error", "left")))634 635    if total_size:636        size, size_style = format_size(total_size)637        items.append(fcol(fixlen_r(size, 5), size_style))638    else:639        items.append(("fixed", 5, urwid.Text("")))640 641    if duration:642        duration_pretty, duration_style = format_duration(duration)643        items.append(fcol(fixlen_r(duration_pretty, 5), duration_style))644    else:645        items.append(("fixed", 5, urwid.Text("")))646 647    items.append(format_right_indicators(replay=False, marked=marked))648 649    return urwid.Pile([urwid.Columns(items, dividechars=1, min_width=15)])650 651 652@lru_cache(maxsize=800)653def format_dns_flow(654    *,655    render_mode: RenderMode,656    focused: bool,657    intercepted: bool,658    marked: str,659    is_replay: str | None,660    op_code: str,661    request_timestamp: float,662    domain: str,663    type: str,664    response_code: str | None,665    response_code_http_equiv: int,666    answer: str | None,667    error_message: str,668    duration: float | None,669):670    items = []671 672    if render_mode in (RenderMode.TABLE, RenderMode.DETAILVIEW):673        items.append(674            format_left_indicators(675                focused=focused, intercepted=intercepted, timestamp=request_timestamp676            )677        )678    else:679        items.append(fcol(">>" if focused else "  ", "focus"))680 681    scheme_style = "intercepted" if intercepted else SCHEME_STYLES["dns"]682    t = f"DNS {op_code}"683    if render_mode is RenderMode.TABLE:684        t = fixlen(t, 10)685    items.append(fcol(t, scheme_style))686    items.append(("weight", 0.5, TruncatedText(domain, colorize_host(domain), "right")))687    items.append(fcol("(" + fixlen(type, 5)[: len(type)] + ") =", "text"))688 689    items.append(690        (691            "weight",692            1,693            (694                truncated_plain(695                    "..." if answer is None else "?" if not answer else answer, "text"696                )697                if error_message is None698                else truncated_plain(error_message, "error")699            ),700        )701    )702    status_style = (703        "intercepted"704        if intercepted705        else HTTP_RESPONSE_CODE_STYLE.get(response_code_http_equiv // 100, "code_other")706    )707    items.append(708        fcol(fixlen("" if response_code is None else response_code, 9), status_style)709    )710 711    if duration:712        duration_pretty, duration_style = format_duration(duration)713        items.append(fcol(fixlen_r(duration_pretty, 5), duration_style))714    else:715        items.append(("fixed", 5, urwid.Text("")))716 717    items.append(718        format_right_indicators(719            replay=bool(is_replay),720            marked=marked,721        )722    )723    return urwid.Pile([urwid.Columns(items, dividechars=1, min_width=15)])724 725 726def format_flow(727    f: flow.Flow,728    *,729    render_mode: RenderMode,730    hostheader: bool = False,  # pass options directly if we need more stuff from them731    focused: bool = True,732) -> urwid.Widget:733    """734    This functions calls the proper renderer depending on the flow type.735    We also want to cache the renderer output, so we extract all attributes736    relevant for display and call the render with only that. This assures that rows737    are updated if the flow is changed.738    """739    duration: float | None740    error_message: str | None741    if f.error:742        error_message = f.error.msg743    else:744        error_message = None745 746    if isinstance(f, (TCPFlow, UDPFlow)):747        total_size = 0748        for message in f.messages:749            total_size += len(message.content)750        if f.messages:751            duration = f.messages[-1].timestamp - f.client_conn.timestamp_start752        else:753            duration = None754        if f.client_conn.tls_version == "QUICv1":755            protocol = "quic"756        else:757            protocol = f.type758        return format_message_flow(759            render_mode=render_mode,760            focused=focused,761            timestamp_start=f.client_conn.timestamp_start,762            marked=f.marked,763            protocol=protocol,764            client_address=f.client_conn.peername,765            server_address=f.server_conn.address,766            total_size=total_size,767            duration=duration,768            error_message=error_message,769        )770    elif isinstance(f, DNSFlow):771        if f.request.timestamp and f.response and f.response.timestamp:772            duration = f.response.timestamp - f.request.timestamp773        else:774            duration = None775        if f.response:776            response_code_str: str | None = dns.response_codes.to_str(777                f.response.response_code778            )779            response_code_http_equiv = dns.response_codes.http_equiv_status_code(780                f.response.response_code781            )782            answer = ", ".join(str(x) for x in f.response.answers)783        else:784            response_code_str = None785            response_code_http_equiv = 0786            answer = None787        return format_dns_flow(788            render_mode=render_mode,789            focused=focused,790            intercepted=f.intercepted,791            marked=f.marked,792            is_replay=f.is_replay,793            op_code=dns.op_codes.to_str(f.request.op_code),794            request_timestamp=f.request.timestamp,795            domain=f.request.questions[0].name if f.request.questions else "",796            type=dns.types.to_str(f.request.questions[0].type)797            if f.request.questions798            else "",799            response_code=response_code_str,800            response_code_http_equiv=response_code_http_equiv,801            answer=answer,802            error_message=error_message,803            duration=duration,804        )805    elif isinstance(f, HTTPFlow):806        intercepted = f.intercepted807        response_content_length: int | None808        if f.response:809            if f.response.raw_content is not None:810                response_content_length = len(f.response.raw_content)811            else:812                response_content_length = None813            response_code: int | None = f.response.status_code814            response_reason: str | None = f.response.reason815            response_content_type = f.response.headers.get("content-type")816            if f.response.timestamp_end:817                duration = max(818                    [f.response.timestamp_end - f.request.timestamp_start, 0]819                )820            else:821                duration = None822        else:823            response_content_length = None824            response_code = None825            response_reason = None826            response_content_type = None827            duration = None828 829        scheme = f.request.scheme830        if f.websocket is not None:831            if scheme == "https":832                scheme = "wss"833            elif scheme == "http":834                scheme = "ws"835 836        if render_mode in (RenderMode.LIST, RenderMode.DETAILVIEW):837            render_func = format_http_flow_list838        else:839            render_func = format_http_flow_table840        return render_func(841            render_mode=render_mode,842            focused=focused,843            marked=f.marked,844            is_replay=f.is_replay,845            request_method=f.request.method,846            request_scheme=scheme,847            request_host=f.request.pretty_host if hostheader else f.request.host,848            request_path=f.request.path,849            request_url=f.request.pretty_url if hostheader else f.request.url,850            request_http_version=f.request.http_version,851            request_timestamp=f.request.timestamp_start,852            request_is_push_promise="h2-pushed-stream" in f.metadata,853            intercepted=intercepted,854            response_code=response_code,855            response_reason=response_reason,856            response_content_length=response_content_length,857            response_content_type=response_content_type,858            duration=duration,859            error_message=error_message,860        )861 862    else:863        raise NotImplementedError()864 
codekingpro/portable-devtools · Team Ai