From 734a136a883c7758ac3cb68b8f3579661eac52db Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 02:45:07 +0000 Subject: [PATCH] Add Monty sandbox adapter for workflow Python nodes (wip slice) Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com> --- litellm/proxy/workflows/monty/AGENTS.md | 11 + litellm/proxy/workflows/monty/compiler.py | 358 ++++++++++++++++++ litellm/proxy/workflows/monty/models.py | 76 ++++ litellm/proxy/workflows/monty/runtime.py | 255 +++++++++++++ pyproject.toml | 1 + .../integration/sandbox/test_monty_runtime.py | 335 ++++++++++++++++ .../proxy/workflows/monty/test_compiler.py | 148 ++++++++ uv.lock | 143 +++++++ 8 files changed, 1327 insertions(+) create mode 100644 litellm/proxy/workflows/monty/AGENTS.md create mode 100644 litellm/proxy/workflows/monty/compiler.py create mode 100644 litellm/proxy/workflows/monty/models.py create mode 100644 litellm/proxy/workflows/monty/runtime.py create mode 100644 tests/integration/sandbox/test_monty_runtime.py create mode 100644 tests/unit/proxy/workflows/monty/test_compiler.py diff --git a/litellm/proxy/workflows/monty/AGENTS.md b/litellm/proxy/workflows/monty/AGENTS.md new file mode 100644 index 00000000000..8bd696872e0 --- /dev/null +++ b/litellm/proxy/workflows/monty/AGENTS.md @@ -0,0 +1,11 @@ +# Monty security boundary + +Never pass host callbacks, mounts, environment variables, credentials, or live SDK objects into Monty + +Never import, exec, or eval uploaded source on the gateway. Inspect it with AST only + +Never fall back to CPython execution. Missing or unverified workers fail closed + +Monty's max_memory is an allocator budget, not an RSS limit. Apply the worker address-space limit separately + +Language isolation is not an OS security boundary. An interpreter escape requires separate OS-level containment diff --git a/litellm/proxy/workflows/monty/compiler.py b/litellm/proxy/workflows/monty/compiler.py new file mode 100644 index 00000000000..1956f6b6c56 --- /dev/null +++ b/litellm/proxy/workflows/monty/compiler.py @@ -0,0 +1,358 @@ +import ast +import hashlib +import keyword +import re +from typing import Final + +from jsonschema import Draft202012Validator +from jsonschema.exceptions import SchemaError +from pydantic import JsonValue + +from .models import CompiledProgram, JsonSchema, MontyErrorKind, MontyFailure, MontyLimits, PythonProgram + +_FORBIDDEN: Final = frozenset( + { + "eval", + "exec", + "compile", + "open", + "__import__", + "globals", + "locals", + "vars", + "getattr", + "setattr", + "delattr", + "breakpoint", + "input", + "help", + } +) +_PREFIX: Final = "_liteflow_" +_GENERATED_GLOBALS: Final = frozenset({"dataclass", "str", "int", "float", "bool", "list", "dict", "all", "isinstance"}) + + +def escape_diagnostics(value: str) -> str: + return "".join( + f"\\x{ord(char):02x}" if (ord(char) < 32 and char not in "\n\t") or 127 <= ord(char) <= 159 else char + for char in value + ) + + +def _invalid(rule: str, node: ast.AST) -> MontyFailure: + return MontyFailure( + MontyErrorKind.INVALID_PROGRAM, escape_diagnostics(f"{rule} at line {getattr(node, 'lineno', 1)}") + ) + + +def _safe_name(name: str) -> bool: + return name.isidentifier() and not keyword.iskeyword(name) and "__" not in name and not name.startswith(_PREFIX) + + +def _source_error(tree: ast.Module, program: PythonProgram) -> MontyFailure | None: + for statement in tree.body: + if ( + isinstance(statement, ast.Expr) + and isinstance(statement.value, ast.Constant) + and isinstance(statement.value.value, str) + ): + continue + if isinstance(statement, ast.ImportFrom) and statement.module == "__future__" and statement.level == 0: + if all(alias.name == "annotations" and alias.asname is None for alias in statement.names): + continue + if not isinstance(statement, ast.FunctionDef): + return _invalid("module body permits only function definitions and future annotations", statement) + functions: Final = tuple(node for node in tree.body if isinstance(node, ast.FunctionDef)) + if len({node.name for node in functions}) != len(functions): + return _invalid("duplicate function name", tree) + entries: Final = tuple(node for node in functions if node.name == program.function_name) + if len(entries) != 1: + return _invalid("entry function must exist exactly once", tree) + args: Final = entries[0].args + if args.posonlyargs or args.vararg or args.kwarg or args.kwonlyargs or args.defaults or args.kw_defaults: + return _invalid("entry parameters must be positional without defaults or variadics", entries[0]) + if tuple(arg.arg for arg in args.args) != program.parameter_names: + return _invalid("entry parameter order must match parameter_names", entries[0]) + for node in ast.walk(tree): + if isinstance(node, (ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda, ast.Global, ast.Nonlocal)): + return _invalid(f"forbidden {type(node).__name__}", node) + if isinstance(node, ast.FunctionDef): + if node not in functions: + return _invalid("nested functions and closures are forbidden", node) + if node.decorator_list: + return _invalid("decorators are forbidden", node) + if not _safe_name(node.name) or node.name in _GENERATED_GLOBALS: + return _invalid("reserved function name", node) + if node.args.defaults or any(default is not None for default in node.args.kw_defaults): + return _invalid("function defaults are forbidden", node) + if isinstance(node, (ast.Import, ast.ImportFrom)) and node not in tree.body: + return _invalid("imports inside functions are forbidden", node) + if isinstance(node, ast.Name) and (not _safe_name(node.id) or node.id in _FORBIDDEN): + return _invalid("forbidden builtin or reserved name", node) + if isinstance(node, ast.arg) and not _safe_name(node.arg): + return _invalid("reserved parameter name", node) + if isinstance(node, ast.Attribute) and not _safe_name(node.attr): + return _invalid("dunder or reserved attribute access", node) + return None + + +def _mapping(value: JsonValue) -> JsonSchema: + return value if isinstance(value, dict) else {} + + +def _resolve(schema: JsonSchema, root: JsonSchema) -> JsonSchema: + ref: Final = schema.get("$ref") + if not isinstance(ref, str): + return schema + + def descend(value: JsonValue, parts: tuple[str, ...]) -> JsonSchema: + if not parts: + return _mapping(value) + if isinstance(value, dict): + return descend(value.get(parts[0].replace("~1", "/").replace("~0", "~")), parts[1:]) + if isinstance(value, list) and parts[0].isdigit() and int(parts[0]) < len(value): + return descend(value[int(parts[0])], parts[1:]) + return {} + + return descend(root, tuple(ref[2:].split("/"))) + + +def _schema_error(value: JsonValue) -> str | None: + if isinstance(value, list): + return next((error for item in value if (error := _schema_error(item)) is not None), None) + if not isinstance(value, dict): + return None + for key, item in value.items(): + if key in {"$id", "$dynamicRef", "$dynamicAnchor", "$anchor", "$schema"}: + return f"unsupported schema keyword {key}" + if key == "$ref" and (not isinstance(item, str) or not item.startswith("#/")): + return "only local #/ JSON references are supported" + if key not in {"const", "enum", "default", "examples"}: + if (error := _schema_error(item)) is not None: + return error + return None + + +def _record_error(schema: JsonSchema, root: JsonSchema, seen: frozenset[str] = frozenset()) -> str | None: + ref: Final = schema.get("$ref") + if isinstance(ref, str): + if ref in seen: + return "recursive schema references are unsupported" + return _record_error(_resolve(schema, root), root, seen | {ref}) + if schema.get("x-liteflow-record") is True: + if schema.get("type") != "object" or not isinstance(schema.get("properties"), dict): + return "records require fixed object properties" + if schema.get("additionalProperties") is not False: + return "records require additionalProperties false" + if any(not _safe_name(name) for name in _mapping(schema.get("properties"))): + return "record field names must be safe identifiers" + for keyword_name in ("anyOf", "oneOf", "allOf"): + alternatives: Final = schema.get(keyword_name) + if isinstance(alternatives, list): + if any(_contains_record(_mapping(item), root, seen) for item in alternatives): + if keyword_name != "anyOf" or len(alternatives) != 2: + return "records in unions are supported only as Optional X" + if sum(_mapping(item).get("type") == "null" for item in alternatives) != 1: + return "records in unions are supported only as Optional X" + for key, value in schema.items(): + if key in {"properties", "$defs"} and isinstance(value, dict): + for child in value.values(): + if (error := _record_error(_mapping(child), root, seen)) is not None: + return error + if key in {"anyOf", "oneOf", "allOf", "prefixItems"} and isinstance(value, list): + for child_schema in value: + if (error := _record_error(_mapping(child_schema), root, seen)) is not None: + return error + if key in {"items", "additionalProperties"} and isinstance(value, dict): + if (error := _record_error(value, root, seen)) is not None: + return error + return None + + +def _contains_record(schema: JsonSchema, root: JsonSchema, seen: frozenset[str]) -> bool: + ref: Final = schema.get("$ref") + if isinstance(ref, str): + return ref not in seen and _contains_record(_resolve(schema, root), root, seen | {ref}) + if schema.get("x-liteflow-record") is True: + return True + return any(_contains_record(child, root, seen) for child in _children(schema)) + + +def _children(schema: JsonSchema) -> tuple[JsonSchema, ...]: + def children_of(key: str, value: JsonValue) -> tuple[JsonSchema, ...]: + if key in {"properties", "$defs"} and isinstance(value, dict): + return tuple(_mapping(child) for child in value.values()) + if key in {"anyOf", "oneOf", "allOf", "prefixItems"} and isinstance(value, list): + return tuple(_mapping(child) for child in value) + if key in {"items", "additionalProperties"} and isinstance(value, dict): + return (value,) + return () + + return tuple(child for group in (children_of(key, value) for key, value in schema.items()) for child in group) + + +def _records(root: JsonSchema) -> tuple[tuple[str, JsonSchema], ...]: + return tuple( + (name, value) + for name, value in _mapping(root.get("$defs")).items() + if isinstance(value, dict) and value.get("x-liteflow-record") is True + ) + + +def _build(schema: JsonSchema, root: JsonSchema, expression: str, depth: int = 0) -> str: + resolved: Final = _resolve(schema, root) + ref: Final = schema.get("$ref") + if isinstance(ref, str) and "$ref" in resolved: + return _build(resolved, root, expression, depth + 1) + union: Final = resolved.get("anyOf") + if isinstance(union, list) and _contains_record(resolved, root, frozenset()): + other: Final = next(_mapping(item) for item in union if _mapping(item).get("type") != "null") + return f"(None if {expression} is None else {_build(other, root, expression, depth + 1)})" + if resolved.get("x-liteflow-record") is True: + name: Final = next(name for name, candidate in _records(root) if candidate == resolved) + required: Final = resolved.get("required", []) + + def field(name: str, child: JsonValue) -> str: + access: Final = ( + f"{expression}[{name!r}]" + if isinstance(required, list) and name in required + else f"{expression}.get({name!r})" + ) + built: Final = _build(_mapping(child), root, access, depth + 1) + return ( + f"{name}={built}" + if isinstance(required, list) and name in required + else f"{name}=(None if {access} is None else {built})" + ) + + return ( + f"{name}(" + + ", ".join(field(key, child) for key, child in _mapping(resolved.get("properties")).items()) + + ")" + ) + if resolved.get("type") == "array": + variable: Final = f"{_PREFIX}item{depth}" + return f"[{_build(_mapping(resolved.get('items')), root, variable, depth + 1)} for {variable} in {expression}]" + if resolved.get("type") == "object" and isinstance(resolved.get("properties"), dict): + fields: Final = _mapping(resolved.get("properties")) + variable_key: Final = f"{_PREFIX}key{depth}" + variable_value: Final = f"{_PREFIX}value{depth}" + branches: Final = "".join( + f"{_build(_mapping(child), root, variable_value, depth + 1)} if {variable_key} == {key!r} else " + for key, child in fields.items() + ) + fallback: Final = _build(_mapping(resolved.get("additionalProperties")), root, variable_value, depth + 1) + return ( + f"{{{variable_key}: ({branches}{fallback}) for {variable_key}, {variable_value} in {expression}.items()}}" + ) + return expression + + +def _record_class(name: str, schema: JsonSchema) -> str: + required: Final = schema.get("required", []) + fields: Final = _mapping(schema.get("properties")) + mandatory: Final = tuple(key for key in fields if isinstance(required, list) and key in required) + optional: Final = tuple(key for key in fields if key not in mandatory) + return ( + f"@dataclass(frozen=True)\nclass {name}:\n" + + ( + "\n".join(f" {key}: object" for key in mandatory) + + "\n" + + "\n".join(f" {key}: object = None" for key in optional) + if fields + else " pass" + ) + + "\n" + ) + + +def _source(program: PythonProgram) -> str: + records: Final = dict(_records(program.input_schema) + _records(program.output_schema)) + classes: Final = "\n".join(_record_class(name, schema) for name, schema in records.items()) + record_dump: Final = "\n".join( + f" if isinstance(value, {name}):\n return {_PREFIX}dump({{" + + ", ".join(f"{field!r}: value.{field}" for field in _mapping(schema.get("properties"))) + + "})" + for name, schema in records.items() + ) + dumper: Final = f"""def {_PREFIX}dump(value): +{record_dump} + if value is None or isinstance(value, (str, int, float, bool)): + return [True, value] + if isinstance(value, list): + pairs = [{_PREFIX}dump(item) for item in value] + return [all(pair[0] for pair in pairs), [pair[1] for pair in pairs]] + if isinstance(value, dict): + pairs = {{key: {_PREFIX}dump(item) for key, item in value.items()}} + return [all(isinstance(key, str) and pair[0] for key, pair in pairs.items()), {{key: pair[1] for key, pair in pairs.items()}}] + return [False, None] +""" + arguments: Final = ", ".join( + _build( + _mapping(_mapping(program.input_schema.get("properties")).get(name)), + program.input_schema, + f"{_PREFIX}input[{name!r}]", + ) + for name in program.parameter_names + ) + return ( + "from dataclasses import dataclass\n" + + classes + + "\n" + + program.source + + "\n" + + dumper + + f"\n{_PREFIX}dump({program.function_name}({arguments}))" + ) + + +def compile_program(program: PythonProgram, limits: MontyLimits) -> CompiledProgram | MontyFailure: + try: + encoded: Final = program.source.encode("utf-8") + if len(encoded) > limits.max_source_bytes: + return MontyFailure(MontyErrorKind.INVALID_PROGRAM, "source byte limit exceeded at line 1") + if hashlib.sha256(encoded).hexdigest() != program.source_sha256: + return MontyFailure(MontyErrorKind.INVALID_PROGRAM, "source_sha256 mismatch at line 1") + if re.search(r"(?m)^\s*#\s*///\s*script\b", program.source): + return MontyFailure(MontyErrorKind.INVALID_PROGRAM, "PEP 723 script blocks forbidden at line 1") + tree: Final = ast.parse(program.source) + if (failure := _source_error(tree, program)) is not None: + return failure + for schema in (program.input_schema, program.output_schema): + if (error := _schema_error(schema)) is not None: + return MontyFailure(MontyErrorKind.UNSUPPORTED_SCHEMA, escape_diagnostics(error)) + Draft202012Validator.check_schema(schema) + if (record_error := _record_error(schema, schema)) is not None: + return MontyFailure(MontyErrorKind.UNSUPPORTED_SCHEMA, escape_diagnostics(record_error)) + for name, record in _records(schema): + if ( + not _safe_name(name) + or name in _GENERATED_GLOBALS + or name in {node.name for node in tree.body if isinstance(node, ast.FunctionDef)} + ): + return MontyFailure( + MontyErrorKind.UNSUPPORTED_SCHEMA, "record class name must be a distinct safe identifier" + ) + input_records: Final = dict(_records(program.input_schema)) + if any( + name in input_records and input_records[name] != schema for name, schema in _records(program.output_schema) + ): + return MontyFailure(MontyErrorKind.UNSUPPORTED_SCHEMA, "conflicting record definitions") + inputs: Final = program.input_schema + if ( + inputs.get("type") != "object" + or set(_mapping(inputs.get("properties"))) != set(program.parameter_names) + or inputs.get("additionalProperties") is not False + or not isinstance(inputs.get("required"), list) + or set(inputs["required"]) != set(program.parameter_names) + ): + return MontyFailure( + MontyErrorKind.UNSUPPORTED_SCHEMA, "input schema must require exactly the named parameters" + ) + return CompiledProgram(program, _source(program), program.input_schema, program.output_schema) + except SyntaxError as exc: + return MontyFailure( + MontyErrorKind.INVALID_PROGRAM, escape_diagnostics(f"syntax error at line {exc.lineno}: {exc.msg}") + ) + except (SchemaError, ValueError, RecursionError, StopIteration) as exc: + return MontyFailure(MontyErrorKind.UNSUPPORTED_SCHEMA, escape_diagnostics(str(exc))) diff --git a/litellm/proxy/workflows/monty/models.py b/litellm/proxy/workflows/monty/models.py new file mode 100644 index 00000000000..b41a2291719 --- /dev/null +++ b/litellm/proxy/workflows/monty/models.py @@ -0,0 +1,76 @@ +from dataclasses import dataclass +from enum import StrEnum +from typing import Annotated, Literal + +from pydantic import BaseModel, ConfigDict, Field, JsonValue + +JsonSchema = dict[str, JsonValue] + + +class PythonProgram(BaseModel): + model_config = ConfigDict(frozen=True) + + contract_version: Literal["1"] + source: str + function_name: str + parameter_names: Annotated[tuple[str, ...], Field(min_length=1)] + input_schema: JsonSchema + output_schema: JsonSchema + source_sha256: str + + +class MontyErrorKind(StrEnum): + INVALID_PROGRAM = "INVALID_PROGRAM" + UNSUPPORTED_SCHEMA = "UNSUPPORTED_SCHEMA" + INPUT_INVALID = "INPUT_INVALID" + INPUT_TOO_LARGE = "INPUT_TOO_LARGE" + OUTPUT_INVALID = "OUTPUT_INVALID" + OUTPUT_TOO_LARGE = "OUTPUT_TOO_LARGE" + RUNTIME_ERROR = "RUNTIME_ERROR" + TIMEOUT = "TIMEOUT" + MEMORY_LIMIT = "MEMORY_LIMIT" + RECURSION_LIMIT = "RECURSION_LIMIT" + STDOUT_LIMIT = "STDOUT_LIMIT" + WORKER_CRASHED = "WORKER_CRASHED" + QUEUE_FULL = "QUEUE_FULL" + PREDICATE_NOT_BOOL = "PREDICATE_NOT_BOOL" + CANCELLED = "CANCELLED" + RUNTIME_UNAVAILABLE = "RUNTIME_UNAVAILABLE" + + +@dataclass(frozen=True, slots=True) +class MontyFailure: + kind: MontyErrorKind + message: str + + +@dataclass(frozen=True, slots=True) +class MontySuccess: + output: JsonValue + stdout: str + duration_ms: int + + +@dataclass(frozen=True, slots=True) +class CompiledProgram: + program: PythonProgram + sandbox_source: str + input_schema: JsonSchema + output_schema: JsonSchema + + +class MontyLimits(BaseModel): + model_config = ConfigDict(frozen=True) + + max_source_bytes: int = Field(default=64 * 1024, gt=0) + max_input_bytes: int = Field(default=1024 * 1024, gt=0) + max_output_bytes: int = Field(default=1024 * 1024, gt=0) + max_input_depth: int = Field(default=64, gt=0) + max_output_depth: int = Field(default=64, gt=0) + max_memory: int = Field(default=64 * 1024 * 1024, gt=0) + worker_address_space_bytes: int = Field(default=512 * 1024 * 1024, gt=0) + feed_seconds: float = Field(default=5, gt=0) + max_recursion_depth: int = Field(default=200, gt=0) + max_stdout_bytes: int = Field(default=64 * 1024, gt=0) + max_concurrent_workers: int = Field(default=4, gt=0) + max_queued_waiters: int = Field(default=16, ge=0) diff --git a/litellm/proxy/workflows/monty/runtime.py b/litellm/proxy/workflows/monty/runtime.py new file mode 100644 index 00000000000..e4f81c354c4 --- /dev/null +++ b/litellm/proxy/workflows/monty/runtime.py @@ -0,0 +1,255 @@ +from __future__ import annotations + +import asyncio +import base64 +import hashlib +import importlib.metadata +import json +import math +import os +import signal +import sys +import time +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING, Final + +from jsonschema import Draft202012Validator +from jsonschema.exceptions import ValidationError +from pydantic import JsonValue, TypeAdapter + +from .compiler import escape_diagnostics +from .models import CompiledProgram, JsonSchema, MontyErrorKind, MontyFailure, MontyLimits, MontySuccess + +if TYPE_CHECKING: + from pydantic_monty import AsyncMonty, AsyncMontySession + +_JSON: Final = TypeAdapter(JsonValue) + + +@dataclass(frozen=True, slots=True) +class BinaryArtifact: + path: Path + sha256: str + + +def installed_binary() -> BinaryArtifact | MontyFailure: + try: + distribution: Final = importlib.metadata.distribution("pydantic-monty-runtime") + candidates: Final = tuple(file for file in distribution.files or () if file.name in {"monty", "monty.exe"}) + if len(candidates) != 1: + return MontyFailure(MontyErrorKind.RUNTIME_UNAVAILABLE, "runtime binary missing or ambiguous") + file: Final = candidates[0] + if file.hash is None or file.hash.mode != "sha256": + return MontyFailure(MontyErrorKind.RUNTIME_UNAVAILABLE, "runtime binary has no SHA-256 RECORD hash") + digest: Final = base64.urlsafe_b64decode(file.hash.value + "=" * (-len(file.hash.value) % 4)).hex() + return BinaryArtifact(Path(distribution.locate_file(file)).resolve(), digest) + except (importlib.metadata.PackageNotFoundError, OSError, ValueError) as exc: + return MontyFailure(MontyErrorKind.RUNTIME_UNAVAILABLE, escape_diagnostics(str(exc))) + + +def _json_issue(value: object, depth: int, max_depth: int) -> MontyErrorKind | None: + if depth > max_depth: + return MontyErrorKind.INPUT_TOO_LARGE + if value is None or type(value) in (str, int, bool): + return None + if type(value) is float: + return None if math.isfinite(value) else MontyErrorKind.INPUT_INVALID + if type(value) is list: + return next((issue for item in value if (issue := _json_issue(item, depth + 1, max_depth)) is not None), None) + if type(value) is dict: + if any(type(key) is not str for key in value): + return MontyErrorKind.INPUT_INVALID + return next( + (issue for item in value.values() if (issue := _json_issue(item, depth + 1, max_depth)) is not None), None + ) + return MontyErrorKind.INPUT_INVALID + + +def validate_json( + value: object, schema: JsonSchema, *, max_bytes: int, max_depth: int, output: bool = False +) -> JsonValue | MontyFailure: + invalid: Final = MontyErrorKind.OUTPUT_INVALID if output else MontyErrorKind.INPUT_INVALID + too_large: Final = MontyErrorKind.OUTPUT_TOO_LARGE if output else MontyErrorKind.INPUT_TOO_LARGE + issue: Final = _json_issue(value, 0, max_depth) + if issue is not None: + return MontyFailure( + too_large if issue == MontyErrorKind.INPUT_TOO_LARGE else invalid, "JSON depth or type invalid" + ) + try: + encoded: Final = json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(",", ":")).encode("utf-8") + if len(encoded) > max_bytes: + return MontyFailure(too_large, "JSON byte limit exceeded") + parsed: Final = _JSON.validate_python(value, strict=True) + Draft202012Validator(schema).validate(parsed) + return parsed + except (ValidationError, ValueError, OverflowError, RecursionError) as exc: + return MontyFailure(invalid, escape_diagnostics(str(exc))[:4096]) + + +def _kill(session: AsyncMontySession) -> None: + pid: Final = session.worker_pid + if pid is None: + return + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + return + + +class MontyRuntime: + def __init__(self, limits: MontyLimits, *, binary: BinaryArtifact | None = None) -> None: + self.limits = limits + self._binary = binary + self._pool: AsyncMonty | None = None + self._slots = asyncio.Semaphore(limits.max_concurrent_workers) + self._admitted = 0 + + async def start(self) -> MontyFailure | None: + if self._pool is not None: + return None + try: + from pydantic_monty import AsyncMonty + + artifact: Final = self._binary if self._binary is not None else installed_binary() + if isinstance(artifact, MontyFailure): + return artifact + if hashlib.sha256(artifact.path.read_bytes()).hexdigest() != artifact.sha256: + return MontyFailure(MontyErrorKind.RUNTIME_UNAVAILABLE, "runtime binary SHA-256 mismatch") + pool: Final = AsyncMonty( + binary_path=artifact.path, + min_processes=0, + max_processes=self.limits.max_concurrent_workers, + max_checkouts_per_worker=1, + checkout_timeout=self.limits.feed_seconds, + request_timeout=self.limits.feed_seconds + 1, + feed_duration_limit_grace=0.1, + ) + await pool.__aenter__() + self._pool = pool + return None + except (ImportError, OSError, ValueError, RuntimeError) as exc: + return MontyFailure(MontyErrorKind.RUNTIME_UNAVAILABLE, escape_diagnostics(str(exc))) + + async def close(self) -> None: + pool: Final = self._pool + self._pool = None + if pool is not None: + await pool.__aexit__(None, None, None) + + async def run_program( + self, compiled: CompiledProgram, input: object, *, deadline_monotonic: float + ) -> MontySuccess | MontyFailure: + started: Final = time.monotonic() + if started >= deadline_monotonic: + return MontyFailure(MontyErrorKind.TIMEOUT, "node deadline expired") + if self._pool is None: + return MontyFailure(MontyErrorKind.RUNTIME_UNAVAILABLE, "Monty runtime is not started") + if self._admitted >= self.limits.max_concurrent_workers + self.limits.max_queued_waiters: + return MontyFailure(MontyErrorKind.QUEUE_FULL, "Monty execution queue is full") + self._admitted += 1 + try: + async with asyncio.timeout_at(asyncio.get_running_loop().time() + deadline_monotonic - time.monotonic()): + async with self._slots: + validated: Final = validate_json( + input, + compiled.input_schema, + max_bytes=self.limits.max_input_bytes, + max_depth=self.limits.max_input_depth, + ) + if isinstance(validated, MontyFailure): + return validated + return await self._execute(compiled, validated, deadline_monotonic, started) + except TimeoutError: + return MontyFailure(MontyErrorKind.TIMEOUT, "node deadline expired") + finally: + self._admitted -= 1 + + async def _execute( + self, compiled: CompiledProgram, input: JsonValue, deadline: float, started: float + ) -> MontySuccess | MontyFailure: + from pydantic_monty import CollectString, MontyCrashedError, MontyRuntimeError, MontySyntaxError, ResourceLimits + + pool: Final = self._pool + if pool is None: + return MontyFailure(MontyErrorKind.RUNTIME_UNAVAILABLE, "Monty runtime is closed") + remaining: Final = deadline - time.monotonic() + if remaining <= 0: + return MontyFailure(MontyErrorKind.TIMEOUT, "node deadline expired") + collector: Final = CollectString(max_bytes=self.limits.max_stdout_bytes) + session: Final = pool.checkout( + script_name="workflow.py", + limits=ResourceLimits( + max_feed_duration_secs=min(self.limits.feed_seconds, remaining), + max_memory=self.limits.max_memory, + max_recursion_depth=self.limits.max_recursion_depth, + ), + os_policy={"sleep": "zero"}, + ) + try: + async with session: + try: + if sys.platform == "linux": + import resource + + pid: Final = session.worker_pid + if pid is None: + return MontyFailure(MontyErrorKind.RUNTIME_UNAVAILABLE, "worker PID unavailable") + try: + resource.prlimit(pid, resource.RLIMIT_AS, (self.limits.worker_address_space_bytes,) * 2) + except (OSError, ValueError) as exc: + return MontyFailure(MontyErrorKind.RUNTIME_UNAVAILABLE, escape_diagnostics(str(exc))) + result: Final[object] = await session.feed_run( + compiled.sandbox_source, + inputs={"_liteflow_input": input}, + print_callback=collector, + ) + if not isinstance(result, list) or len(result) != 2 or result[0] is not True: + return MontyFailure(MontyErrorKind.OUTPUT_INVALID, "return value is not strict JSON") + output: Final = validate_json( + result[1], + compiled.output_schema, + max_bytes=self.limits.max_output_bytes, + max_depth=self.limits.max_output_depth, + output=True, + ) + if isinstance(output, MontyFailure): + return output + if time.monotonic() >= deadline: + return MontyFailure(MontyErrorKind.TIMEOUT, "node deadline expired during validation") + return MontySuccess( + output, escape_diagnostics(collector.output), int((time.monotonic() - started) * 1000) + ) + except asyncio.CancelledError: + _kill(session) + raise + except MontyRuntimeError as exc: + error: Final = exc.exception() + message: Final = escape_diagnostics(exc.display())[:4096] + if isinstance(error, TimeoutError): + return MontyFailure(MontyErrorKind.TIMEOUT, message) + if isinstance(error, MemoryError): + kind: Final = MontyErrorKind.STDOUT_LIMIT if "bytes >" in str(error) else MontyErrorKind.MEMORY_LIMIT + return MontyFailure(kind, message) + if isinstance(error, RecursionError): + return MontyFailure(MontyErrorKind.RECURSION_LIMIT, message) + return MontyFailure(MontyErrorKind.RUNTIME_ERROR, message) + except MontyCrashedError as exc: + return MontyFailure( + MontyErrorKind.TIMEOUT if exc.timed_out else MontyErrorKind.WORKER_CRASHED, + escape_diagnostics(str(exc))[:4096], + ) + except MontySyntaxError as exc: + return MontyFailure(MontyErrorKind.INVALID_PROGRAM, escape_diagnostics(str(exc))[:4096]) + except (ValueError, OSError) as exc: + return MontyFailure(MontyErrorKind.RUNTIME_ERROR, escape_diagnostics(str(exc))[:4096]) + + async def evaluate_predicate( + self, compiled: CompiledProgram, input: object, *, deadline_monotonic: float + ) -> bool | MontyFailure: + result: Final = await self.run_program(compiled, input, deadline_monotonic=deadline_monotonic) + if isinstance(result, MontyFailure): + return result + if type(result.output) is not bool: + return MontyFailure(MontyErrorKind.PREDICATE_NOT_BOOL, "predicate must return exactly bool") + return result.output diff --git a/pyproject.toml b/pyproject.toml index 0b81618355d..cb3e475b1d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ Documentation = "https://docs.litellm.ai" # supply-chain-pinned to a vetted, aged release. [project.optional-dependencies] proxy = [ + "pydantic-monty==1.4.0", "gunicorn>=23.0.0,<24.0", "uvicorn>=0.33.0,<1.0", "granian>=2.7.4,<3.0; python_version < '3.15'", diff --git a/tests/integration/sandbox/test_monty_runtime.py b/tests/integration/sandbox/test_monty_runtime.py new file mode 100644 index 00000000000..b3a6feef70b --- /dev/null +++ b/tests/integration/sandbox/test_monty_runtime.py @@ -0,0 +1,335 @@ +import asyncio +import hashlib +import os +import signal +import time +from collections.abc import AsyncIterator +from pathlib import Path +from typing import Final + +import pytest +import pytest_asyncio + +from litellm.proxy.workflows.monty.compiler import compile_program +from litellm.proxy.workflows.monty.models import ( + CompiledProgram, + JsonSchema, + MontyErrorKind, + MontyFailure, + MontyLimits, + MontySuccess, + PythonProgram, +) +from litellm.proxy.workflows.monty.runtime import BinaryArtifact, MontyRuntime, installed_binary + +pytest.importorskip("pydantic_monty") + + +def _compile( + source: str, + output: JsonSchema | None = None, + argument: JsonSchema | None = None, + definitions: JsonSchema | None = None, +) -> CompiledProgram: + program: Final = PythonProgram( + contract_version="1", + source=source, + function_name="transform", + parameter_names=("value",), + input_schema={ + "type": "object", + "properties": {"value": argument if argument is not None else {}}, + "required": ["value"], + "additionalProperties": False, + "$defs": definitions or {}, + }, + output_schema={**(output or {}), "$defs": definitions or {}}, + source_sha256=hashlib.sha256(source.encode()).hexdigest(), + ) + result: Final = compile_program(program, MontyLimits()) + assert isinstance(result, CompiledProgram), result + return result + + +@pytest_asyncio.fixture +async def runtime() -> AsyncIterator[MontyRuntime]: + instance: Final = MontyRuntime(MontyLimits(max_concurrent_workers=1, max_queued_waiters=0)) + assert await instance.start() is None + try: + yield instance + finally: + await instance.close() + + +async def _run(runtime: MontyRuntime, compiled: CompiledProgram, value: object) -> MontySuccess | MontyFailure: + return await runtime.run_program(compiled, {"value": value}, deadline_monotonic=time.monotonic() + 10) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "value", + [ + {"__class__": "os.system"}, + {"type": "function", "callable": "os.system"}, + "Bearer sk-test-fake", + "${OPENAI_API_KEY}", + "https://example.invalid/action", + [], + None, + ], +) +async def test_model_output_remains_inert_json(runtime: MontyRuntime, value: object) -> None: + result: Final = await _run(runtime, _compile("def transform(value): return value"), value) + assert isinstance(result, MontySuccess), result + assert result.output == value + + +@pytest.mark.asyncio +async def test_typed_json_transform_and_escaped_stdout(runtime: MontyRuntime) -> None: + compiled: Final = _compile( + "def transform(value):\n print('\\x1b[31m')\n return {'n': value['n'] + 1}", + {"type": "object", "properties": {"n": {"type": "integer"}}, "required": ["n"]}, + ) + result: Final = await _run(runtime, compiled, {"n": 2}) + assert isinstance(result, MontySuccess), result + assert result.output == {"n": 3} + assert result.stdout == "\\x1b[31m\n" + + +_RECORDS: Final[JsonSchema] = { + "Expense": { + "type": "object", + "x-liteflow-record": True, + "additionalProperties": False, + "properties": { + "amount_cents": {"type": "integer", "minimum": 0}, + "currency": {"type": "string"}, + "receipt_present": {"type": "boolean"}, + }, + "required": ["amount_cents", "currency", "receipt_present"], + }, +} +_EXPENSE: Final[JsonSchema] = {"$ref": "#/$defs/Expense"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("receipt,amount,review", [(True, 100, False), (False, 100, True), (True, 10001, True)]) +async def test_expense_normalize_condition_and_both_branches( + runtime: MontyRuntime, receipt: bool, amount: int, review: bool +) -> None: + normalize: Final = _compile( + "def transform(value: Expense) -> Expense:\n return Expense(amount_cents=value.amount_cents, currency=value.currency.strip().upper(), receipt_present=value.receipt_present)", + _EXPENSE, + _EXPENSE, + _RECORDS, + ) + normalized: Final = await _run( + runtime, normalize, {"amount_cents": amount, "currency": " usd ", "receipt_present": receipt} + ) + assert isinstance(normalized, MontySuccess), normalized + assert normalized.output == {"amount_cents": amount, "currency": "USD", "receipt_present": receipt} + condition: Final = _compile( + "def transform(value: Expense) -> bool:\n return not value.receipt_present or value.currency != 'USD' or value.amount_cents > 10000", + {"type": "boolean"}, + _EXPENSE, + _RECORDS, + ) + assert ( + await runtime.evaluate_predicate( + condition, {"value": normalized.output}, deadline_monotonic=time.monotonic() + 10 + ) + is review + ) + status: Final = "review_required" if review else "ready_for_processing" + branch: Final = _compile( + f"def transform(value): return {{'status': {status!r}, 'amount_cents': value.amount_cents, 'currency': value.currency}}", + {"type": "object"}, + _EXPENSE, + _RECORDS, + ) + result: Final = await _run(runtime, branch, normalized.output) + assert isinstance(result, MontySuccess), result + assert result.output == {"status": status, "amount_cents": amount, "currency": "USD"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "source", + [ + "def transform(value): return Expense(amount_cents=1, currency='USD', receipt_present=True)", + "def transform(value): return {'amount_cents':1, 'currency':'USD', 'receipt_present':True}", + ], +) +async def test_record_and_equal_dict_are_equivalent(runtime: MontyRuntime, source: str) -> None: + result: Final = await _run(runtime, _compile(source, _EXPENSE, definitions=_RECORDS), None) + assert isinstance(result, MontySuccess), result + assert result.output == {"amount_cents": 1, "currency": "USD", "receipt_present": True} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("value", [None, {"amount_cents": 7, "currency": "USD", "receipt_present": True}]) +async def test_optional_record(runtime: MontyRuntime, value: object) -> None: + optional: Final[JsonSchema] = {"anyOf": [_EXPENSE, {"type": "null"}]} + result: Final = await _run( + runtime, + _compile( + "def transform(value): return None if value is None else value.amount_cents", + argument=optional, + definitions=_RECORDS, + ), + value, + ) + assert isinstance(result, MontySuccess), result + assert result.output == (None if value is None else 7) + + +@pytest.mark.asyncio +async def test_list_of_records(runtime: MontyRuntime) -> None: + result: Final = await _run( + runtime, + _compile( + "def transform(value): return [item.currency for item in value]", + argument={"type": "array", "items": _EXPENSE}, + definitions=_RECORDS, + ), + [{"amount_cents": 7, "currency": "USD", "receipt_present": True}], + ) + assert isinstance(result, MontySuccess), result + assert result.output == ["USD"] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("value", [1, "true", None]) +async def test_predicate_requires_exact_bool(runtime: MontyRuntime, value: object) -> None: + result: Final = await runtime.evaluate_predicate( + _compile("def transform(value): return value"), {"value": value}, deadline_monotonic=time.monotonic() + 10 + ) + assert isinstance(result, MontyFailure) + assert result.kind == MontyErrorKind.PREDICATE_NOT_BOOL + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "source,schema,kind", + [ + ("def transform(value): return -1", {"type": "integer", "minimum": 0}, MontyErrorKind.OUTPUT_INVALID), + ("def transform(value): return 'wrong'", {"type": "integer"}, MontyErrorKind.OUTPUT_INVALID), + ("def helper(x): return x\ndef transform(value): return helper", {}, MontyErrorKind.OUTPUT_INVALID), + ("def transform(value): return 'x' * 1100000", {}, MontyErrorKind.OUTPUT_TOO_LARGE), + ("def transform(value):\n while True: pass", {}, MontyErrorKind.TIMEOUT), + ("def transform(value): return [0] * 1000000000", {}, MontyErrorKind.MEMORY_LIMIT), + ("def transform(value): return transform(value)", {}, MontyErrorKind.RECURSION_LIMIT), + ("def transform(value): print('x' * 100000)", {}, MontyErrorKind.STDOUT_LIMIT), + ("def transform(value): return 1 / 0", {}, MontyErrorKind.RUNTIME_ERROR), + ], +) +async def test_resource_and_output_failures( + runtime: MontyRuntime, source: str, schema: JsonSchema, kind: MontyErrorKind +) -> None: + result: Final = await _run(runtime, _compile(source, schema), None) + assert isinstance(result, MontyFailure), result + assert result.kind == kind, result + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "value,kind", + [ + ({1: "not JSON"}, MontyErrorKind.INPUT_INVALID), + (float("nan"), MontyErrorKind.INPUT_INVALID), + ("x" * 1100000, MontyErrorKind.INPUT_TOO_LARGE), + ], +) +async def test_input_failures(runtime: MontyRuntime, value: object, kind: MontyErrorKind) -> None: + result: Final = await _run(runtime, _compile("def transform(value): return value"), value) + assert isinstance(result, MontyFailure), result + assert result.kind == kind + + +@pytest.mark.asyncio +async def test_expired_deadline(runtime: MontyRuntime) -> None: + result: Final = await runtime.run_program( + _compile("def transform(value): return value"), {"value": 1}, deadline_monotonic=0 + ) + assert isinstance(result, MontyFailure) + assert result.kind == MontyErrorKind.TIMEOUT + + +@pytest.mark.asyncio +async def test_binary_digest_mismatch_fails_closed(tmp_path: Path) -> None: + artifact: Final = installed_binary() + assert isinstance(artifact, BinaryArtifact) + runtime: Final = MontyRuntime(MontyLimits(), binary=BinaryArtifact(artifact.path, "0" * 64)) + result: Final = await runtime.start() + assert isinstance(result, MontyFailure) + assert result.kind == MontyErrorKind.RUNTIME_UNAVAILABLE + assert "SHA-256" in result.message + + +def _worker_pids() -> frozenset[int]: + children: Final = Path(f"/proc/{os.getpid()}/task/{os.getpid()}/children").read_text().split() + return frozenset(int(pid) for pid in children if Path(f"/proc/{pid}/comm").read_text().strip() == "monty") + + +async def _active_worker(previous: frozenset[int]) -> int: + async with asyncio.timeout(5): + while True: + current: Final = _worker_pids() - previous + if current: + return next(iter(current)) + await asyncio.sleep(0.01) + + +@pytest.mark.asyncio +async def test_cancellation_kills_worker_releases_slot_and_queue_is_bounded(runtime: MontyRuntime) -> None: + previous: Final = _worker_pids() + task: Final = asyncio.create_task(_run(runtime, _compile("def transform(value):\n while True: pass"), None)) + pid: Final = await _active_worker(previous) + full: Final = await _run(runtime, _compile("def transform(value): return value"), 1) + assert isinstance(full, MontyFailure) + assert full.kind == MontyErrorKind.QUEUE_FULL + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + result: Final = await _run(runtime, _compile("def transform(value): return value"), 2) + assert isinstance(result, MontySuccess), result + assert result.output == 2 + assert not Path(f"/proc/{pid}").exists() + + +@pytest.mark.asyncio +async def test_killed_worker_fails_and_next_run_succeeds(runtime: MontyRuntime) -> None: + previous: Final = _worker_pids() + task: Final = asyncio.create_task(_run(runtime, _compile("def transform(value):\n while True: pass"), None)) + pid: Final = await _active_worker(previous) + os.kill(pid, signal.SIGKILL) + result: Final = await task + assert isinstance(result, MontyFailure), result + assert result.kind == MontyErrorKind.WORKER_CRASHED + next_result: Final = await _run(runtime, _compile("def transform(value): return value"), 3) + assert isinstance(next_result, MontySuccess), next_result + assert next_result.output == 3 + + +@pytest.mark.asyncio +async def test_fresh_worker_has_no_parent_canary_and_has_address_space_limit( + runtime: MontyRuntime, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("LITEFLOW_ENV_CANARY", "fake-value-no-credential") + previous: Final = _worker_pids() + task: Final = asyncio.create_task(_run(runtime, _compile("def transform(value):\n while True: pass"), None)) + pid: Final = await _active_worker(previous) + assert b"LITEFLOW_ENV_CANARY" not in Path(f"/proc/{pid}/environ").read_bytes() + async with asyncio.timeout(5): + while str(runtime.limits.worker_address_space_bytes) not in Path(f"/proc/{pid}/limits").read_text(): + await asyncio.sleep(0.01) + assert str(runtime.limits.worker_address_space_bytes) in Path(f"/proc/{pid}/limits").read_text() + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + second: Final = asyncio.create_task(_run(runtime, _compile("def transform(value):\n while True: pass"), None)) + next_pid: Final = await _active_worker(previous | {pid}) + assert next_pid != pid + second.cancel() + with pytest.raises(asyncio.CancelledError): + await second diff --git a/tests/unit/proxy/workflows/monty/test_compiler.py b/tests/unit/proxy/workflows/monty/test_compiler.py new file mode 100644 index 00000000000..e38f2327f50 --- /dev/null +++ b/tests/unit/proxy/workflows/monty/test_compiler.py @@ -0,0 +1,148 @@ +import hashlib +from typing import Final + +import pytest + +from litellm.proxy.workflows.monty.compiler import compile_program, escape_diagnostics +from litellm.proxy.workflows.monty.models import ( + CompiledProgram, + JsonSchema, + MontyErrorKind, + MontyFailure, + MontyLimits, + PythonProgram, +) + + +def _program(source: str, *, output: JsonSchema | None = None) -> PythonProgram: + return PythonProgram( + contract_version="1", + source=source, + function_name="transform", + parameter_names=("value",), + input_schema={ + "type": "object", + "properties": {"value": {}}, + "required": ["value"], + "additionalProperties": False, + }, + output_schema=output if output is not None else {}, + source_sha256=hashlib.sha256(source.encode()).hexdigest(), + ) + + +@pytest.mark.parametrize( + "source", + [ + "import os\ndef transform(value): return value", + "from typing import TypedDict\ndef transform(value): return value", + "x=1\ndef transform(value): return value", + "print('side effect')\ndef transform(value): return value", + "async def transform(value): return value", + "class X: pass\ndef transform(value): return value", + "@print\ndef transform(value): return value", + "def transform(value):\n def closure(): return value\n return closure()", + "def transform(value): return (lambda: value)()", + "def transform(value):\n global x\n return value", + "def transform(value):\n nonlocal x\n return value", + "def transform(value): return value.__class__", + "def transform(value): return __name__", + "# /// script\ndef transform(value): return value", + "def transform(other): return other", + "def transform(value=1): return value", + "def transform(*value): return value", + "def transform(value, **kwargs): return value", + "def transform(*, value): return value", + "def transform(value, /): return value", + "def transform(value):\n import json\n return value", + "def transform(value): return", + "def transform(value): return value\ndef isinstance(x,y): return True", + ], +) +def test_rejects_unsupported_source_without_running_it(source: str) -> None: + program: Final = _program(source) + result: Final = compile_program(program, MontyLimits()) + if source == "def transform(value): return": + assert isinstance(result, CompiledProgram) + return + assert isinstance(result, MontyFailure) + assert result.kind == MontyErrorKind.INVALID_PROGRAM + assert "line" in result.message + + +@pytest.mark.parametrize( + "builtin", + [ + "eval", + "exec", + "compile", + "open", + "__import__", + "globals", + "locals", + "vars", + "getattr", + "setattr", + "delattr", + "breakpoint", + "input", + "help", + ], +) +def test_rejects_dangerous_builtin_references(builtin: str) -> None: + result: Final = compile_program(_program(f"def transform(value): return {builtin}"), MontyLimits()) + assert isinstance(result, MontyFailure) + assert result.kind == MontyErrorKind.INVALID_PROGRAM + + +@pytest.mark.parametrize( + "schema", + [ + {"$ref": "https://example.invalid/schema"}, + {"$ref": "file:///etc/passwd"}, + {"$id": "https://example.invalid/schema"}, + {"$dynamicRef": "#/x"}, + {"$schema": "https://example.invalid/schema"}, + {"type": "not-a-json-type"}, + {"$defs": {"Bad": {"type": "object", "x-liteflow-record": True, "properties": {}}}}, + { + "$defs": { + "Bad-Name": { + "type": "object", + "x-liteflow-record": True, + "properties": {}, + "additionalProperties": False, + } + } + }, + ], +) +def test_rejects_unsupported_schema_without_remote_retrieval(schema: JsonSchema) -> None: + result: Final = compile_program(_program("def transform(value): return value", output=schema), MontyLimits()) + assert isinstance(result, MontyFailure) + assert result.kind == MontyErrorKind.UNSUPPORTED_SCHEMA + + +def test_hash_covers_exact_source_bytes() -> None: + program: Final = _program("def transform(value): return value") + result: Final = compile_program(program.model_copy(update={"source": program.source + "\n"}), MontyLimits()) + assert isinstance(result, MontyFailure) + assert result.kind == MontyErrorKind.INVALID_PROGRAM + assert "source_sha256" in result.message + + +def test_annotations_are_not_evaluated() -> None: + result: Final = compile_program( + _program("def transform(value: Unknown[Foo]) -> Missing: return value"), MontyLimits() + ) + assert isinstance(result, CompiledProgram) + + +def test_source_size_is_bounded() -> None: + result: Final = compile_program(_program("def transform(value): return value"), MontyLimits(max_source_bytes=8)) + assert isinstance(result, MontyFailure) + assert result.kind == MontyErrorKind.INVALID_PROGRAM + + +def test_diagnostics_escape_terminal_controls_but_preserve_layout() -> None: + assert escape_diagnostics("a\x1b[31m\x00\x7f\x9b\n\t") == "a\\x1b[31m\\x00\\x7f\\x9b\n\t" diff --git a/uv.lock b/uv.lock index fb591264fc3..6d4711abd26 100644 --- a/uv.lock +++ b/uv.lock @@ -5090,6 +5090,7 @@ proxy = [ { name = "polars" }, { name = "pydantic", version = "2.13.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.15'" }, { name = "pydantic", version = "2.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.15'" }, + { name = "pydantic-monty" }, { name = "pyjwt" }, { name = "pynacl" }, { name = "pyroscope-io", marker = "sys_platform != 'win32'" }, @@ -5349,6 +5350,7 @@ requires-dist = [ { name = "pydantic", marker = "python_full_version >= '3.15'", specifier = ">=2.14.0,<3.0.0" }, { name = "pydantic", marker = "extra == 'mcp'", specifier = ">=2.12.0,<3" }, { name = "pydantic", marker = "extra == 'proxy'", specifier = ">=2.12.0,<3" }, + { name = "pydantic-monty", marker = "extra == 'proxy'", specifier = "==1.4.0" }, { name = "pydantic-settings", specifier = ">=2.14.1,<3.0" }, { name = "pyjwt", marker = "extra == 'proxy'", specifier = ">=2.13.0,<3.0" }, { name = "pynacl", marker = "extra == 'proxy'", specifier = ">=1.6.2,<2.0" }, @@ -8785,6 +8787,147 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/f3/d15bc0b1fb0c4f1e07b3326ffb7489f7ae70da2b56a0884d22641b0eb47c/pydantic_core-2.50.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7f476456ac2bb0d937f75191494a09c83a30765fea4f70f3b404942fe25f6cdf", size = 2336787, upload-time = "2026-10-11T18:35:42.558Z" }, ] +[[package]] +name = "pydantic-monty" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic-monty-client" }, + { name = "pydantic-monty-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/7c/58467370dd9f2db100ee2627791668f6f21a9fa150ccff9e60efc588ab8f/pydantic_monty-1.4.0.tar.gz", hash = "sha256:db1f1c779f509f7c547e580620a63ab8968cf167fd6a45a4f60b6625187a9848", size = 8025, upload-time = "2026-10-26T12:31:49.864Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/5a/5cb59f15ca5b89df8ea810397523877d0ffba33419a8975f5bde8795b262/pydantic_monty-1.4.0-py3-none-any.whl", hash = "sha256:e398217fdb701e4daaeeb9fd65ad9319627cb114334bc00d6cf2444845c43bca", size = 7552, upload-time = "2026-10-26T12:28:25.388Z" }, +] + +[[package]] +name = "pydantic-monty-client" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.15'" }, + { name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/78/0f94fe1b8b45ba5e9c8bc622117f949820a71f7e96a67a7b691fdda96e3d/pydantic_monty_client-1.4.0.tar.gz", hash = "sha256:2252ca45e3cfd241a63950fde184b2ff06ddce5b3e1c260282c0733d70590acd", size = 2916981, upload-time = "2026-10-26T12:31:51.132Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/9f/c16b213349757ee032946bb29b59a50d9593820765a347cdeec66b2b4fff/pydantic_monty_client-1.4.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6fc2b38d871932b43cf08d6ccd9cc38ca6f9fe386845658ea35f4f0de99e6c4a", size = 4288592, upload-time = "2026-10-26T12:28:26.929Z" }, + { url = "https://files.pythonhosted.org/packages/38/32/97f6b6bd430e32a69f3398e3528ac5c4140d79303b18a0d3d59ede7a02c9/pydantic_monty_client-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4597ffa0664a0f2aac77d95f8d2c8786444b1d6aa7c5411e5538dde2d37ff96", size = 4060552, upload-time = "2026-10-26T12:28:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/34/b1/327f06e728f8d9e0369ae6335775387fd2ee39eb0d6d528878590d316389/pydantic_monty_client-1.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:141422a3af8536a139f7e099e9f7ac3f78307d2381564ec91239b3a91bd13bf7", size = 4122496, upload-time = "2026-10-26T12:28:30.273Z" }, + { url = "https://files.pythonhosted.org/packages/17/a2/77563d71661923383addaae926f8bdb5799e54fe91434b1df41db814d544/pydantic_monty_client-1.4.0-cp310-cp310-manylinux_2_28_armv7l.whl", hash = "sha256:32eaf5482170020b1bf85576c58d297a99dfdb126aecc451b266a6cf12b00bf2", size = 3807830, upload-time = "2026-10-26T12:28:31.926Z" }, + { url = "https://files.pythonhosted.org/packages/9b/4a/9d505fea5afebfcfcfee49374dcf618e0fc4768bfbd66c545e9e429663ff/pydantic_monty_client-1.4.0-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:0ec3c0205f02bc7d327fc8c53e985c68df5991607c8e18ec9b2d3562519a08ff", size = 4049655, upload-time = "2026-10-26T12:28:33.607Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/f0b3300858e34a66d5a0270d7c2ef3d5c4bbca649b0ffcef548e9f122722/pydantic_monty_client-1.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:33b9aa71bd8bf5cd0f65ab9dbf8241684f0cf3d7d70c99eb00bc45a6608b43ae", size = 4260607, upload-time = "2026-10-26T12:28:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/7bab7113a230bb21e28ccec28d2aa8cfd17426528c6a541baa01e8f18d90/pydantic_monty_client-1.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a7e4a7c3d04651cf6c2f6503fec7694b979c02a1a286546949500d750f0210a", size = 4154742, upload-time = "2026-10-26T12:28:37.05Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d1/506e3450bddedc44f80ce40fda1840f65da26b15de4ff8f6c123cb3c8897/pydantic_monty_client-1.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:2204520a4f4c4360527aa957e9b79f0295f14dfd30ea1bc0b7db373e7d8958e5", size = 4411287, upload-time = "2026-10-26T12:28:38.494Z" }, + { url = "https://files.pythonhosted.org/packages/a0/91/59cc51af399c80490231207c54ca2557f36fd48024294a707c28d09392e9/pydantic_monty_client-1.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:309b2fa11943de2711ff9e453a2327cfd265192ac6b5ecad19fafde31671cdb5", size = 4295152, upload-time = "2026-10-26T12:28:39.921Z" }, + { url = "https://files.pythonhosted.org/packages/5a/17/ad54eab3c573c7e186ff270eef3389717c1c4d239b51f8901901daf5be6f/pydantic_monty_client-1.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d45d0351d563dc652f8963339a098c0dd29b026df10a4dc92fdcae1c04f420af", size = 4605594, upload-time = "2026-10-26T12:28:41.769Z" }, + { url = "https://files.pythonhosted.org/packages/50/83/e9207c92d37f445b745cadcf0a5a1b52c3fdaac94b4aa9b04347093666dc/pydantic_monty_client-1.4.0-cp310-cp310-win32.whl", hash = "sha256:959ac1ec194f32d17b6e78fff5b7c7b31815e68b551dd17d0c6ab9b007c020e9", size = 3686561, upload-time = "2026-10-26T12:28:43.27Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e2/28ded8f46e31c635c0fea322fce526ec05df71c5f642373c0595ab96a1f4/pydantic_monty_client-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:959e5e03edd2f6ab719a306cd36f86e5cfb1bcd398f346ee439876dbce35108a", size = 4360024, upload-time = "2026-10-26T12:28:45.302Z" }, + { url = "https://files.pythonhosted.org/packages/91/7b/2798ebabdd90c1258274e4f85541130d31e9d543d330eb3ee98b98eaee2a/pydantic_monty_client-1.4.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e15c2a6f2833806ddb1f4334aa8272cdc9ab0a1c2301824af6354b88e2fb17c", size = 4287529, upload-time = "2026-10-26T12:28:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/62/6c/f1618692da03841fb11a73731b52b6d11e0b861d47034059c877371c0950/pydantic_monty_client-1.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:485981ad29da00f295ad994299db809bb60aeb3b664beff7b6f5c31116c78b59", size = 4060688, upload-time = "2026-10-26T12:28:48.913Z" }, + { url = "https://files.pythonhosted.org/packages/82/f3/7c45f7f38b75d94a3f7164d79cb32f1bcd88ee657ed5646f4ea4c6bc2503/pydantic_monty_client-1.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:063534e65ae971099f2e64c674626d871aa3efcf6df983db5ba520d9085a60a6", size = 4121979, upload-time = "2026-10-26T12:28:50.783Z" }, + { url = "https://files.pythonhosted.org/packages/4f/94/e47425838420e172664868c93e99730cfc3ada642eb467be15deebbde4cc/pydantic_monty_client-1.4.0-cp311-cp311-manylinux_2_28_armv7l.whl", hash = "sha256:fbd9ae34c2d68d0b14774468f87c43369041631b4de469e336ada7a34a57a30f", size = 3806350, upload-time = "2026-10-26T12:28:52.244Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9f/4693140e8e27f2afa1c6d718f2d45ec17f201a57189f3353ed4dcad80577/pydantic_monty_client-1.4.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:814f551fe454c5db082192b53d75ec4f71c1b790ea944d68763528aba714e182", size = 4048270, upload-time = "2026-10-26T12:28:53.976Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c6/69c7747f62b6a5a25d450563c630190cec254ef43e4dab0446329733bcb2/pydantic_monty_client-1.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:69d7a87c60a8a2eea5b488f6c373b49f4c11a61917fbcd45c1247f7daf0a3a45", size = 4259483, upload-time = "2026-10-26T12:28:55.879Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9c/b901f54c53ad9ba2cfb2a0319fb59211304b51512746b4c0be1516f3ca6a/pydantic_monty_client-1.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:94cf89b243dc9dad970f36072849e7607fc4e7573d0b190be4613bd6167d1b9a", size = 4153767, upload-time = "2026-10-26T12:28:57.644Z" }, + { url = "https://files.pythonhosted.org/packages/f8/48/e342373a0164fe5ed71015707de82f66761e107cfe54bb77c2a2f67854fd/pydantic_monty_client-1.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d5676ea4b2fead463c427f0a4536f59156feb978c56bd0862aef5ba24c00aff8", size = 4410295, upload-time = "2026-10-26T12:28:59.915Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b0/d85b45cea1a542251ef229f21b31f5d9368cfc3f24ad7c88b7e7efbfc95a/pydantic_monty_client-1.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:12e35274ddac034f8c4772e1e0931a15d834ba8db84d347372e2d09f349f2168", size = 4293927, upload-time = "2026-10-26T12:29:01.998Z" }, + { url = "https://files.pythonhosted.org/packages/57/5d/4fbfae9af7c350b3672f2684253baf422299cea6e282d69a4648e127e26c/pydantic_monty_client-1.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d28aa3d51cb522861cf49f7b21e3405afa256d0938606c330e72e2089a4a158e", size = 4604347, upload-time = "2026-10-26T12:29:03.604Z" }, + { url = "https://files.pythonhosted.org/packages/b5/de/a167924d35a052a74f94e14d9793cb9c0d3cb2fa159b0b2e48c7c1f4e180/pydantic_monty_client-1.4.0-cp311-cp311-win32.whl", hash = "sha256:ce66f43bea399ae8454c603664ff2bdb9bb3a58034a4e2855fdec85d412c4a08", size = 3685412, upload-time = "2026-10-26T12:29:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7f/24317b4e747aca7aa85dd62d5540f976fdf5a7eaaa40f351e7586c1812b8/pydantic_monty_client-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:25440b0b3c5b7eebf010075729db1b4778cf90e9cdebd988a1fd5714df73f85b", size = 4359255, upload-time = "2026-10-26T12:29:06.95Z" }, + { url = "https://files.pythonhosted.org/packages/64/9f/6605612467001a29734947e3e66eb2f9c03172e524ad89a72ede2c00e2c5/pydantic_monty_client-1.4.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c771ded8705f24fdac9ea119547ffe3cd7126ac4d3f7a1514adf7f3f9961f4c", size = 4317732, upload-time = "2026-10-26T12:29:08.5Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a0/bf17349667f3a3f76054fac14702840bd745a86c4de86cebaa4f50db742b/pydantic_monty_client-1.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0f0677e9a3be73159c772c4bc55292150356b5080cc4623196dd57407ddc5504", size = 4046984, upload-time = "2026-10-26T12:29:09.991Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2e/5dca50a7086efb25fd7850595bd491c76692bc8c83e1331a0ca5dfd71331/pydantic_monty_client-1.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:287614a13d60c590475a39708171b5a037a6dbb6868e0125871cce365009846a", size = 4121512, upload-time = "2026-10-26T12:29:11.416Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c4/b5dd4714b9cb204f1a992e6244066dbb1e880eae74303e526a77f2a1536f/pydantic_monty_client-1.4.0-cp312-cp312-manylinux_2_28_armv7l.whl", hash = "sha256:8bfe5c08eea64423174712f641189dcc5e9fc2caad18c8b243af041bec8402a0", size = 3805501, upload-time = "2026-10-26T12:29:12.912Z" }, + { url = "https://files.pythonhosted.org/packages/12/1a/a54b7346c8b045318d65eebf97d09817dc3d04cf89dd2e678c6f3d2ddc6a/pydantic_monty_client-1.4.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:7471989402d267e181d7d5a528cc48a4674d3815fc56a67aeeb8caaeef324477", size = 4045097, upload-time = "2026-10-26T12:29:14.75Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a4/94d8e46b0ed94c3f1f39c44269d7241363ca071d46351e4f0088064019cc/pydantic_monty_client-1.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:d6f50a54f2a315897c047dc503881282d02bc601a5da04aa464af071ddc97fa0", size = 4257816, upload-time = "2026-10-26T12:29:16.563Z" }, + { url = "https://files.pythonhosted.org/packages/cd/23/80b53e023b05149ccd318f0873c4f00f1cddf53ad7bf4c72cda439ad3982/pydantic_monty_client-1.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:35217394abf732e2df1033f7a5c3b28eb2b78ac4ce468886f88214d1d342e6ff", size = 4159259, upload-time = "2026-10-26T12:29:18.121Z" }, + { url = "https://files.pythonhosted.org/packages/23/3e/827db7c549b4b33cf3d3c66ec3e53920ec40ce86228b85ee3687053150e9/pydantic_monty_client-1.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5f3f32a9cc72aa831ab4c951b4c6e720a1c81cca0db8d8956b70f5c242381ba8", size = 4410442, upload-time = "2026-10-26T12:29:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/b1/94/ea72c9c9f05e859c1275191567274d63b0cfd84a40fd700270b91765ed04/pydantic_monty_client-1.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4c4e54a0f670fe37fc8a6c065286c83061e5269bfb5bb3a9a0ee4f0369868e19", size = 4288907, upload-time = "2026-10-26T12:29:23.365Z" }, + { url = "https://files.pythonhosted.org/packages/08/86/3eec37a86a95418c9dfd36a34216b66aa0da08c94ff908f6745bdca49a0b/pydantic_monty_client-1.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1edfff6112e2757e2e1760d99a6b7d400f6a4f2fe7f77053e030f21e5d807255", size = 4605169, upload-time = "2026-10-26T12:29:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/ac/39/c49055ba96c72eb365faa8abd88f0467f8c750c51a7fa01a0c19622973c5/pydantic_monty_client-1.4.0-cp312-cp312-win32.whl", hash = "sha256:f7ad83cb3ac4939c79ecc78b7cee6b0db8c5f69be5ca3396dc36d6311b3c2261", size = 3679130, upload-time = "2026-10-26T12:29:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/30/bb/9bd7cb6d9975e10a62fb77836137fbc63111998639920c31a43d06e43b8d/pydantic_monty_client-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:be6f88686d3e93f73e006c7d88fee42350986afc7d307a08f4b0cd3f54bbcf49", size = 4346625, upload-time = "2026-10-26T12:29:28.716Z" }, + { url = "https://files.pythonhosted.org/packages/0e/90/7fb99bed2696f847f9a6cda709ae24f3e8eb188d3e9b869a9982a9a8ae76/pydantic_monty_client-1.4.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f3104fab8df6995769c9b241f634e99d46782270e21d5b8b197bbe5874866b2f", size = 4324480, upload-time = "2026-10-26T12:29:31.232Z" }, + { url = "https://files.pythonhosted.org/packages/2f/03/47bf82469d8e7d451b1c5e5bd2fae427d4768346d6ea271d0a0f8fd4bb14/pydantic_monty_client-1.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c900bebb52812a58055a7cb00f8f4e678688aab2e66f8262e9d0733f1ff5874a", size = 4052142, upload-time = "2026-10-26T12:29:32.999Z" }, + { url = "https://files.pythonhosted.org/packages/2b/06/ac16fc6aded0eb73e18a27e4f7835f1810e6f9fa1e8f01fd2f8bcb81670f/pydantic_monty_client-1.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:3dc3e1d518f5732cdf254bdea8f56f54408ac3a5d84818ec992286f6859fd477", size = 4123662, upload-time = "2026-10-26T12:29:38.367Z" }, + { url = "https://files.pythonhosted.org/packages/39/8e/1ab5ef0e71fea32e29305fc7b2890107858008e8e5f6cc1e5b9a169092fb/pydantic_monty_client-1.4.0-cp313-cp313-manylinux_2_28_armv7l.whl", hash = "sha256:6ad41d4809f51523698fb0fdf8dda2506abc120f7e4d409f75205c0bea40f158", size = 3806072, upload-time = "2026-10-26T12:29:39.849Z" }, + { url = "https://files.pythonhosted.org/packages/49/86/477ef5c02ec571b73853d20bf6927f81e04bd975525b168fcbdcdc3a98c9/pydantic_monty_client-1.4.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:aa0c3ecd210818c53fc9965ffd863ec7fa725373fda73e70627311fbbc8c12fc", size = 4044974, upload-time = "2026-10-26T12:29:41.35Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/03aee4671043f25ab2443979dd000486a3f6642ee860413c82febec340ca/pydantic_monty_client-1.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:879b5c2351527d0d4ce7de73b0330bb934184fd62fe04104166849c233827e19", size = 4261162, upload-time = "2026-10-26T12:29:43.154Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b3/ec5ab86e16987df0dd31c0dd3df2ddb0efa89367bc2106e1818ef5703715/pydantic_monty_client-1.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:15e9949499463fdf707953689a210b7c8f7af216877db7474eadd35f25418d52", size = 4161701, upload-time = "2026-10-26T12:29:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/c6e3c5ce48dec3faae4a70690feeeaf4cd989b582f5aec1b305ae0f0fa13/pydantic_monty_client-1.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:15bd85bc75f1e6fafae2fc7f83f88348795d4376f25e7ede8d23b1cc504005a9", size = 4415744, upload-time = "2026-10-26T12:29:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/e8/26/7db5c0400544bd63be3f6c7b4c6855ae4232610be7a81016f9c0cc2e1ba5/pydantic_monty_client-1.4.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:4079c47f62fce1655e1c3004f630676e18cf9fa2b652adff835fad03610b3762", size = 4292858, upload-time = "2026-10-26T12:29:48.405Z" }, + { url = "https://files.pythonhosted.org/packages/50/43/e560d6e2ae6f355e2306a8a1c7149ef49271ed4e349d1ecfcd26aba5a418/pydantic_monty_client-1.4.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:fc5b9e2d2ec0e2e6f38497803c61dff5c800e9b8f37f9b127e6ae339b7aa4f20", size = 4609688, upload-time = "2026-10-26T12:29:49.883Z" }, + { url = "https://files.pythonhosted.org/packages/12/7c/6af90b241f4adada9bfa7c5170b13ec6cfbb0aad319aea4fd6688080cfa1/pydantic_monty_client-1.4.0-cp313-cp313-win32.whl", hash = "sha256:272eb19ae41423448da0673a135bc0ecf3edf4f9d7860efb357b555efcab9563", size = 3679388, upload-time = "2026-10-26T12:29:51.321Z" }, + { url = "https://files.pythonhosted.org/packages/56/46/ffa8a123d9f6aa95984216faa90da223f3657f741f93573dee278e687ea8/pydantic_monty_client-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce9dbbceaa891fbef9edb2428839e636784c1fca10c4b4c72179ad8ae5f85338", size = 4350595, upload-time = "2026-10-26T12:29:52.9Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4e/23788c86158a3ba16246716c6f0d0e331b0271a98ca2ee35a28772bcb09a/pydantic_monty_client-1.4.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d68b922161f7ed637addd58ee9e25082406009832e114838683d6cae77dbe587", size = 4324862, upload-time = "2026-10-26T12:29:54.68Z" }, + { url = "https://files.pythonhosted.org/packages/d7/18/a1b951d4ebfa939f10c54200909f8d667fb58eef1489d07d94aea7de9c46/pydantic_monty_client-1.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f633e1786bc12acd2b31f2a0fd90f955982ef0a06c9a2ef1d2e3d5832987fb11", size = 4053512, upload-time = "2026-10-26T12:29:56.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/297f1580de9ef26b975b73f3020546145cd50f6000abb275a643474d0515/pydantic_monty_client-1.4.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:87260bf6a226f5bbcc03e4a8883ac3a04bebb1c8a3e3da747ee8c464443e6f37", size = 4123580, upload-time = "2026-10-26T12:29:57.975Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/93bd1fa14eabf97c59b983ba020ef951391ff2497c06babb2d6c61635aad/pydantic_monty_client-1.4.0-cp314-cp314-manylinux_2_28_armv7l.whl", hash = "sha256:ffa480dab03bbc0ae7e59d13e894c42c89909837a7a2ee3d8e41967d192213b4", size = 3807832, upload-time = "2026-10-26T12:29:59.388Z" }, + { url = "https://files.pythonhosted.org/packages/9c/3f/4f2fbb788c71f346a73fd08fdb172053aa09cb382c51091e7058868a8c86/pydantic_monty_client-1.4.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:731499cad98261d0e8f1d125349ef268c2db9081b2cb25d8bef68d99c62ecd80", size = 4046624, upload-time = "2026-10-26T12:30:01.16Z" }, + { url = "https://files.pythonhosted.org/packages/56/41/8055a6d78c10399602cdf605a94146245a13d2a6ad519be65903f8ad9406/pydantic_monty_client-1.4.0-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:f50b807e1475e47cf813049c265070d3cdb76e8ef7e5b77b3e34908d15a8a4db", size = 4262907, upload-time = "2026-10-26T12:30:02.785Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c6/e466e7a374fdc54718c8bb664a1bfbec063733315254490f4b21877f0459/pydantic_monty_client-1.4.0-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:586d0f24faeabb6243584d2ce13bb2180a9607ac157a4e9e4edc78e40b2ac32f", size = 4163131, upload-time = "2026-10-26T12:30:04.228Z" }, + { url = "https://files.pythonhosted.org/packages/56/ac/3f868d04d3db7902ffcf0e4455498b5ad5baa73dbd1188fa978efc993750/pydantic_monty_client-1.4.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:41e7bd5b0b6ae8f1ac878f2c2a3c60656319fb3d51602bb638f72d5db0734711", size = 4416533, upload-time = "2026-10-26T12:30:05.78Z" }, + { url = "https://files.pythonhosted.org/packages/b7/27/913d7c73cdffb78ccd2fc516d05b0c392e48f61d42166910aff1fdf18d63/pydantic_monty_client-1.4.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fa992fa81e1d753bd1c637d90f5713a92d7ce25d968c6195e88219fdf8e628c2", size = 4293701, upload-time = "2026-10-26T12:30:07.249Z" }, + { url = "https://files.pythonhosted.org/packages/85/ef/ec082ca4405889d784d0c667a63c17c3755cc1b0c775bf7e801509cd4fc9/pydantic_monty_client-1.4.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:868d269d20b356d513968aeecbd69161adf8f2a59bc506aac463fff30bd73ab1", size = 4610590, upload-time = "2026-10-26T12:30:09.211Z" }, + { url = "https://files.pythonhosted.org/packages/23/18/a8b360113cb09539ba67e012471661325549f314d3634162b7afbdd0b07e/pydantic_monty_client-1.4.0-cp314-cp314-win32.whl", hash = "sha256:878aede0148c7a9b51bf20940157bbcad089991f60ff5bf27ed34f402d31332f", size = 3680393, upload-time = "2026-10-26T12:30:11.07Z" }, + { url = "https://files.pythonhosted.org/packages/91/ac/fd15b7e8e2517b94cf5c2ac9124f1871367a9e94843e89eb3dcc829bf72a/pydantic_monty_client-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:59f038a326bb3ee8716236a62119f54d7fb27c00a3f9e56ac06ae7d468a48284", size = 4349750, upload-time = "2026-10-26T12:30:12.813Z" }, + { url = "https://files.pythonhosted.org/packages/6c/eb/1d513581f8e8d0e5363edc3cad334cce69777f8abfcbbd6883510e8a53c5/pydantic_monty_client-1.4.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:69aec529dd526655cb5861d6080b2df4bca2c5ba2fbdfd46c2b3ddea4050f31d", size = 4312356, upload-time = "2026-10-26T12:30:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/26/6e/2e9187b663f3cb222beb8c6b5722250d36c12b8b65b71642825ad9c5013f/pydantic_monty_client-1.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f1cc47e6fd4a163f5bbb88934aa84fbbfa8ebfb5f76932896467d49dabfaf905", size = 4041090, upload-time = "2026-10-26T12:30:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e3/f144d76a0956fa35487391f090af7cde4ffc48366f40945d2875b799bfd7/pydantic_monty_client-1.4.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:06290ce25c3b4a89941bdb9a08e1c42f2e288274ff98edcbd7fc2dbbbc2730c7", size = 4118219, upload-time = "2026-10-26T12:30:18.067Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/ae06f344a390a3cdb9aaff29cd89c6a28c680a8301f32b6d970873bac73e/pydantic_monty_client-1.4.0-cp314-cp314t-manylinux_2_28_armv7l.whl", hash = "sha256:4f7ed73540db9c17d6a64dcc1e823946444a44574883dac53c68cb93987dba97", size = 3795829, upload-time = "2026-10-26T12:30:19.521Z" }, + { url = "https://files.pythonhosted.org/packages/4c/54/023a9516f211a63f44881c5a4536f8827742541b5ccc50522c67712482b6/pydantic_monty_client-1.4.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:0493a65bc534445123834276bedf38baa4af964ca35b8fef66e440d6d23a1964", size = 4037589, upload-time = "2026-10-26T12:30:21.561Z" }, + { url = "https://files.pythonhosted.org/packages/09/d2/4aab45f23dad94fdea6705a236ff9eabf48577fcc6e4e80df031382cf231/pydantic_monty_client-1.4.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:41305b81fe9d61fcef7e9c6eabc7308053ebaa568a0ef9d21cc4715198392937", size = 4253700, upload-time = "2026-10-26T12:30:23.358Z" }, + { url = "https://files.pythonhosted.org/packages/09/40/fb1c9248efbd4b5e9d2219b63c3185ea3a0113f869415b56a489259ef3fe/pydantic_monty_client-1.4.0-cp314-cp314t-manylinux_2_28_s390x.whl", hash = "sha256:1943eb2505ac1b80e9e676abb92d015d5406a7413fb6a4f6d364d44905a521c9", size = 4147967, upload-time = "2026-10-26T12:30:25.107Z" }, + { url = "https://files.pythonhosted.org/packages/75/79/3b7b33118d03998ba1dee714919bf9f5d8099ae0771aa75495dcad1e9dfe/pydantic_monty_client-1.4.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1d1e9db032a2ff431d44de84fb569b0736c3f21dc58c84ef974985dbc722a34c", size = 4413011, upload-time = "2026-10-26T12:30:28.146Z" }, + { url = "https://files.pythonhosted.org/packages/45/76/40656a25a297112aedbe4d34f7f745e90aeb8f11a3377cbefd3a344cc445/pydantic_monty_client-1.4.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:076b256cc3b8b0301934f8c12a38f43e549fc2f07b945bf9a9251dfaa9d11aeb", size = 4285647, upload-time = "2026-10-26T12:30:29.77Z" }, + { url = "https://files.pythonhosted.org/packages/44/f7/10b71b443fdfd977e2e60913a2928f562cef97eb13119ecec91a6a719084/pydantic_monty_client-1.4.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3d466a34a6d111b07cff2464796d681a043279cb956a4286e7bfb8c730d22ab5", size = 4606819, upload-time = "2026-10-26T12:30:31.382Z" }, + { url = "https://files.pythonhosted.org/packages/63/c3/1a41dfed2d0fe15965a57919a0266e8fed06b20513f5fe773ae7b7aa26af/pydantic_monty_client-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:fd7ccfa999bbcd05bd8b248ac2071c3d9b9a7e44b99e696596ee785e69aed91c", size = 3671748, upload-time = "2026-10-26T12:30:33.1Z" }, + { url = "https://files.pythonhosted.org/packages/01/b2/6a75a83dbc25e0244ac986ea33a822af7b6f0ca363b5c4ee852fd26fe396/pydantic_monty_client-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8ad5d44fec1b0f60f68209eeff600fc8673dc6960d070bb05e307c6aba35c4fa", size = 4344965, upload-time = "2026-10-26T12:30:34.712Z" }, + { url = "https://files.pythonhosted.org/packages/24/86/61e5de22b9fb6963adba6d0b53b7aebd4a89f01a744f63596e9dbe56a5a0/pydantic_monty_client-1.4.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:1a8907baeca0f0bf0d257620bbe37f6867ac6a7ff2cd677591e6ad52b3ba8667", size = 4324609, upload-time = "2026-10-26T12:30:36.236Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/e766f4c65c47e18ed41dec5ceb3c795ec843c2c03f0d9cc0df4b00bdc52a/pydantic_monty_client-1.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7e6423fc6026378c241e20d3cce48e549d1cbff744c1622f0bf625f85b2c3789", size = 4053763, upload-time = "2026-10-26T12:30:37.664Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ed/419fbd3627764e67b9bacf8af72d1e28a214759b6efcbabcb03d3a373aae/pydantic_monty_client-1.4.0-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:f87a316222f38755b904902fe8c3c4e581165af25bcf872da741c354f004dd9d", size = 4123545, upload-time = "2026-10-26T12:30:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8d/40696d7bd4c5af8be86275943a98311beb011dd8aec6805b2a163924b4b4/pydantic_monty_client-1.4.0-cp315-cp315-manylinux_2_28_armv7l.whl", hash = "sha256:7fdffd11c96710c81b85f7e35faa4641bf6edcee092724850344e2045c88ce6d", size = 3808033, upload-time = "2026-10-26T12:30:40.822Z" }, + { url = "https://files.pythonhosted.org/packages/b5/51/9242b4f151db9aa653e974dee222fb7a7f660b5474e17203e1668d3ff2b7/pydantic_monty_client-1.4.0-cp315-cp315-manylinux_2_28_i686.whl", hash = "sha256:b24b44316bbb14dab9088fdcfa76f66ee92107b9e429186d2921aa2142f697e8", size = 4046971, upload-time = "2026-10-26T12:30:42.479Z" }, + { url = "https://files.pythonhosted.org/packages/35/7a/6619c87bb695b9cc0cc727ee427e1617812bec184dffa0fdc6441e0795f2/pydantic_monty_client-1.4.0-cp315-cp315-manylinux_2_28_ppc64le.whl", hash = "sha256:ec085c9c5d66a540067148dabc4d894d160741277947a57ac4179c5607a5bc2c", size = 4264420, upload-time = "2026-10-26T12:30:44.554Z" }, + { url = "https://files.pythonhosted.org/packages/4a/90/b941236f60533f48c077c54b810d9f47d5c75b71f5057c13a1e4e1091905/pydantic_monty_client-1.4.0-cp315-cp315-manylinux_2_28_s390x.whl", hash = "sha256:2d80aed4644d4ff907324582def94e76151ad862198776d544d760db8dc57219", size = 4163434, upload-time = "2026-10-26T12:30:46.765Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a0/31b38b607c9a22dac273f860b21fb21e591b126a1ae251fb4a54bf110d43/pydantic_monty_client-1.4.0-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:e459c3722c5db566b5f4295f5c5862e77eb0d09abb49b1feac9b0e39467c6fae", size = 4416755, upload-time = "2026-10-26T12:30:48.826Z" }, + { url = "https://files.pythonhosted.org/packages/50/c6/e07dccc12c598739fe442881b71d0d41293713f2eb2bd4f8f63270c0a854/pydantic_monty_client-1.4.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:7130cf0b75557301764b57925da46cbaecae68440ea7b9401bc2d2b0e6bf9dd7", size = 4293680, upload-time = "2026-10-26T12:30:50.496Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5f/be7b09bf2e159afcd91932dcd5f8e8878cdbdb2fec863b81824e8d370d3f/pydantic_monty_client-1.4.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:29f76fb33adf72dda3277adeae13b11340178157b1ab0b544f8f13e9d032780c", size = 4610741, upload-time = "2026-10-26T12:30:52.095Z" }, + { url = "https://files.pythonhosted.org/packages/39/d5/bdefd85f079cbf6a6c8aea2b8b086f6a3ebd48d3291866aa45c6c2741bb9/pydantic_monty_client-1.4.0-cp315-cp315-win32.whl", hash = "sha256:7e073a55ac45eb001e2e702dca94b2bed882ee5fe65898642dd6f1e116589f93", size = 3680779, upload-time = "2026-10-26T12:30:53.66Z" }, + { url = "https://files.pythonhosted.org/packages/ab/4e/dc9403b65e914bba1b9507e33a52b243a6c1d83a8b85087306d71b412b7c/pydantic_monty_client-1.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:f928cc867284e2f366c2b2677590baceccd1800ff34aab82221df9a7e3f58336", size = 4349931, upload-time = "2026-10-26T12:30:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/12/4b/a032b0d12627c182b4b52cf9cd6e34a3c5fceaa635a2ce8b66ce3e753205/pydantic_monty_client-1.4.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d0fbba5046db61eb82a41425c30c1317029694c68fe864cec8b05aff5fc8abc", size = 4312356, upload-time = "2026-10-26T12:30:56.765Z" }, + { url = "https://files.pythonhosted.org/packages/4c/90/4bc4258da5889fb7cd958f5e9fc79d254d51c5a9caba732abf48113e5332/pydantic_monty_client-1.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:639f4a7a8fb8f1978f9036734f7b8bc68b0f48074a91f9f2360fe51fb0023b78", size = 4041241, upload-time = "2026-10-26T12:30:58.258Z" }, + { url = "https://files.pythonhosted.org/packages/04/20/c8c385e7b756d284e1a6c77bf9cf2b1d7c41b5eeec033dc01c0bff38ab6f/pydantic_monty_client-1.4.0-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:cc66b78f433dd67ea4d9697fa50b4d5d24d7f6a96e99cff162ca1d46e89998bd", size = 4118063, upload-time = "2026-10-26T12:30:59.943Z" }, + { url = "https://files.pythonhosted.org/packages/61/0d/61bf503046e0954675e746ca468d1115c682125d96fb05e41a821b93f32d/pydantic_monty_client-1.4.0-cp315-cp315t-manylinux_2_28_armv7l.whl", hash = "sha256:74579bd3139c6c6cc707199b6956f97dbdf16e7c7ce4a802e1af64d48e10e68b", size = 3795982, upload-time = "2026-10-26T12:31:01.494Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4a/11deac6408b5bfca8d9e2150802e366b59f80d5cba37b5d4a2bd99f9a545/pydantic_monty_client-1.4.0-cp315-cp315t-manylinux_2_28_i686.whl", hash = "sha256:527a10831d7bb56b728c878c8c80ee0c7cdcfbf1e4c3b82474e8b58d31beeba3", size = 4037807, upload-time = "2026-10-26T12:31:02.884Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5e/16d5ed8a6d55d80847808248734ba0a7f68e5d4f077f46f4c5924344f8aa/pydantic_monty_client-1.4.0-cp315-cp315t-manylinux_2_28_ppc64le.whl", hash = "sha256:59bac93edeeb060b11b0b4d74bfb2afbd7548e8f67d70bc132d7526a4e4177fd", size = 4254709, upload-time = "2026-10-26T12:31:04.463Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/02c8a175b3fdc7098dcc2c25e6e0faff89c8346c86ea6b070ada9ecee2bd/pydantic_monty_client-1.4.0-cp315-cp315t-manylinux_2_28_s390x.whl", hash = "sha256:ee32f3242000c4020e5708abe1e4c36615c0ee4ac5b8d87318079f76a794e109", size = 4148932, upload-time = "2026-10-26T12:31:06.012Z" }, + { url = "https://files.pythonhosted.org/packages/54/32/1e97c4c2b4b35fbdd8c952f756038e6a92d354f9e30eeee2d5226daadad2/pydantic_monty_client-1.4.0-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:8cd489ea1fab085164185284cad43aa8780833d79e9e2f80071dd8494e5c4dd5", size = 4413415, upload-time = "2026-10-26T12:31:07.609Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2f/11644f09adf9ebfe98a9e4a42f5d8583f4a465860754da1fd9926870f27b/pydantic_monty_client-1.4.0-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:02bdae4755e7c90ecc32b15ea68e13a527ea6f9d6574a54a92d0242ec2a2866a", size = 4285838, upload-time = "2026-10-26T12:31:09.182Z" }, + { url = "https://files.pythonhosted.org/packages/a7/32/528b03acf62fdcea145f7f7b2ea9b7a33a6364fd734874558f448f5c07c7/pydantic_monty_client-1.4.0-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:4412a0adc72e4c51bc3f23993500856523498676936f3eeab26d223bce27314b", size = 4607062, upload-time = "2026-10-26T12:31:11.578Z" }, + { url = "https://files.pythonhosted.org/packages/4b/62/72379b965360e953cb45c6aebc61605951f6b2e0e6d703571dcd780d6518/pydantic_monty_client-1.4.0-cp315-cp315t-win32.whl", hash = "sha256:29cdd6fc5c4ffd0e6580455185a138b63b7ffddbc4eda1f468e8ae96f611a4c3", size = 3671769, upload-time = "2026-10-26T12:31:13.115Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/ab795155a741ff3208ddb6f6c6475ccbb5cb65384329f4d0bad079443748/pydantic_monty_client-1.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c7111b4239f745b63910212c3104c9f91a8f5ee1b7c64ebcc18286117d7cf1d6", size = 4344987, upload-time = "2026-10-26T12:31:14.729Z" }, +] + +[[package]] +name = "pydantic-monty-runtime" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/e7/1567009ab17fe091caba156ea9406487520ef382543e73ac7354a4eef88a/pydantic_monty_runtime-1.4.0.tar.gz", hash = "sha256:5275beb451201bd23669a253c8be93b90cad018b449fcdd0a1ad57daeed15187", size = 2581228, upload-time = "2026-10-26T12:31:53.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/6e/654bf4e3867e48564c7f4dabf03023660c860f53a0065915c4fa1e816825/pydantic_monty_runtime-1.4.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ba7ad3163cbf601d7d36e5c9b6edb7b1625f27683a910885682d224edb8c05e", size = 12643236, upload-time = "2026-10-26T12:31:16.785Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9b/0eedee19d738a168cb4fbcefb1be38cb7c729c750d0293379483e38fa71a/pydantic_monty_runtime-1.4.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:317edf6c62ea62c8a0f456ec13be889387cd16a917441fc087953cae042e6d75", size = 13763884, upload-time = "2026-10-26T12:31:19.94Z" }, + { url = "https://files.pythonhosted.org/packages/d6/91/613e52c0483bb6cbc92ad443450b6573b0d55924941e5235c72edfd08eef/pydantic_monty_runtime-1.4.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:fabe933bb157910aeb61b705c19500d8675b4033215bdb93bc7da1ee9a22a6c9", size = 14209091, upload-time = "2026-10-26T12:31:23.568Z" }, + { url = "https://files.pythonhosted.org/packages/35/cd/326b9d2333d3f18a1642ed0c6721f9454a1672a469b316c92942d764363e/pydantic_monty_runtime-1.4.0-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:b33e86f80adeb86f7928c5040602049d9a245d7cf08ff1398b2ee577fbf7d6fb", size = 13279002, upload-time = "2026-10-26T12:31:26.429Z" }, + { url = "https://files.pythonhosted.org/packages/66/d6/b9ab4adaa305b2aaa5624a9ac5b9a21fb65312c44f8dd658d336d2e20356/pydantic_monty_runtime-1.4.0-py3-none-manylinux_2_28_i686.whl", hash = "sha256:be875b3d248df50768ef9d2c420252e14458ec6f99a1ba2f10e34b0a2f7d88c1", size = 13509495, upload-time = "2026-10-26T12:31:28.996Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/d812921782424db588b9772f57558df4ad8e58c65125d5101f511a08180d/pydantic_monty_runtime-1.4.0-py3-none-manylinux_2_28_ppc64le.whl", hash = "sha256:2be251cc1ee73337dbe3a7ea00d657e4a8385f2476523e74c5685d8f1fc2fea4", size = 13887394, upload-time = "2026-10-26T12:31:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/20/60/2ebff28552652f00f4a8b7266570326f8e03383d44a9e06d492501cbaed5/pydantic_monty_runtime-1.4.0-py3-none-manylinux_2_28_s390x.whl", hash = "sha256:7e13192106dea260dc605a371ab44ffd248dc5afe0ab003f873779774c73a7e6", size = 14964165, upload-time = "2026-10-26T12:31:34.635Z" }, + { url = "https://files.pythonhosted.org/packages/d3/4a/fdf0ff01c3ee23f5af7a017e970967fc88d6d263a79db03d3dfce5c028a1/pydantic_monty_runtime-1.4.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:ff6301049a8e2f3407188dd174a749f2f2af6f82a2bc993c707dbebf44dcf651", size = 14726134, upload-time = "2026-10-26T12:31:37.607Z" }, + { url = "https://files.pythonhosted.org/packages/80/85/317a61daca57f3211d53f868977dbd1d491e0212082f0e79cd6df50137de/pydantic_monty_runtime-1.4.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:ae4e68d623b9038f8a41baa95e06e8d32402ad972120b068d14c336bb924eb03", size = 12315943, upload-time = "2026-10-26T12:31:40.22Z" }, + { url = "https://files.pythonhosted.org/packages/50/c4/fc238425268af5ca9642d5ae997cb1101c95b87e7876ceabf8275b42a1d3/pydantic_monty_runtime-1.4.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:af1f5d41c32207a2ecd9a9c4277290911c6e5f37299588097c7ff83240bb665c", size = 13021113, upload-time = "2026-10-26T12:31:42.899Z" }, + { url = "https://files.pythonhosted.org/packages/24/be/2d221328cd4017b3cfefb6c197740e56ab02689ac1300ad883450a15d565/pydantic_monty_runtime-1.4.0-py3-none-win32.whl", hash = "sha256:30c393985843e836b32e1ceb18c251661e54984bb1e992b7a88d5e897679af01", size = 12392040, upload-time = "2026-10-26T12:31:45.422Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b2/5ac23283be20db8b127c42bb1072910d809792c98cb5b626a85e6ffdcc09/pydantic_monty_runtime-1.4.0-py3-none-win_amd64.whl", hash = "sha256:c2f5a7503288087669f7c5c6131dc9b68a26dfabf07d5fb36df33ff475afbe61", size = 13250757, upload-time = "2026-10-26T12:31:48.107Z" }, +] + [[package]] name = "pydantic-settings" version = "2.14.2"