Add Monty sandbox adapter for workflow Python nodes (wip slice)

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
This commit is contained in:
Devin AI 2026-11-06 02:45:07 +00:00
parent 5085022580
commit 734a136a88
8 changed files with 1327 additions and 0 deletions

View file

@ -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

View file

@ -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)))

View file

@ -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)

View file

@ -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

View file

@ -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'",

View file

@ -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

View file

@ -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"

143
uv.lock generated
View file

@ -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"