diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c0d0637..2b775f76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,15 @@ # Changelog ## [Unreleased] +### Added + +- Analysis sandbox supports `open()` and `with` blocks for reading document files, including `.read()`, `.readline()`, and `.readlines()`. + ### Changed - Require `pydantic-ai-slim>=2.18,<3`. +- `pydantic-monty` bumped to `>=0.0.19`; sandbox code now runs in a subprocess worker pool. A crashed or timed-out worker fails one call and the next call gets a replacement session. +- The analysis sandbox gives Monty a duration budget of `analysis.code_timeout * analysis.max_executions` for the session, was `analysis.code_timeout`. - `enable_thinking` maps onto Pydantic AI's unified `thinking` setting for the `anthropic`, `gemini`, `groq` and `bedrock` providers. The Anthropic thinking budget is now Pydantic AI's default of 10000 tokens, was 4096, and `max_tokens` must exceed it on budget-based Claude models; `enable_thinking: false` disables Gemini thinking rather than only hiding thoughts; Groq maps reasoning effort rather than `groq_reasoning_format`; Bedrock Qwen with `enable_thinking: false` no longer sends `reasoning_config`, while Bedrock-served Claude keeps an explicit `thinking: disabled`. The `openai` and `ollama` providers still map to `openai_reasoning_effort`. - `provider: bedrock` with a proprietary OpenAI model such as `openai.o3-mini-v1:0` raises `UserError`: Bedrock Converse serves only the `gpt-oss` family. Use `provider: bedrock-mantle`. - Tool failures raise `pydantic_ai.ToolFailed` instead of returning failure text: search and code-execution limits, sandbox execution errors, and `get_document`/`summarize_document` misses. @@ -15,6 +21,8 @@ - `evaluations run` opens the database read-only outside the population phase, so an embedder identity differing from the stored one warns instead of aborting the run. - `docs/installation.md` documents the `jina`, `s3` and `ingester` extras, names the extras the full package actually pulls, drops the removed MixedBread AI reranker, and no longer lists Anthropic as a built-in provider. - `docs/tuning.md` no longer points at the removed `claim_timeout_s` setting. +- `analysis.code_timeout` is enforced before each document read, bounding a call that reads in a loop. +- `metadata.json` in the document VFS rejects writes, matching `content.txt`, `items.jsonl` and `toc.json`. ### Removed diff --git a/docs/configuration/qa.md b/docs/configuration/qa.md index 601a0165..aa4f4681 100644 --- a/docs/configuration/qa.md +++ b/docs/configuration/qa.md @@ -50,13 +50,13 @@ analysis: provider: anthropic name: claude-sonnet-4-20250514 temperature: 0.0 # Default: 0.0 (deterministic for code generation) - code_timeout: 60.0 # Max seconds for code execution + code_timeout: 60.0 # Max seconds a call may spend reading documents max_output_chars: 50000 # Truncate output after this many chars max_executions: 15 # Max execute_code calls per question ``` - **model**: LLM configuration (see [Providers](providers.md#model-settings)). When unset, falls back to `qa.model`. -- **code_timeout**: Maximum seconds for each code execution (default: 60) +- **code_timeout**: Seconds a single `execute_code` call may spend reading documents (default: 60). The sandbox refuses further reads past this point. Code that computes without reading is killed by the worker watchdog at the same limit. `code_timeout * max_executions` is the cumulative ceiling across all calls in one question. - **max_output_chars**: Truncate code output after this many characters (default: 50000) - **max_executions**: Maximum `execute_code` calls per question before the capability is told to answer from what it has (default: 15) diff --git a/haiku_rag_slim/haiku/rag/capabilities/analysis.py b/haiku_rag_slim/haiku/rag/capabilities/analysis.py index de250785..8c8e133c 100644 --- a/haiku_rag_slim/haiku/rag/capabilities/analysis.py +++ b/haiku_rag_slim/haiku/rag/capabilities/analysis.py @@ -66,7 +66,7 @@ class AnalysisCapability(RAGCapabilityBase[AnalysisState]): async def _close(self) -> None: if self.sandbox is not None: - self.sandbox.close() + await self.sandbox.close() self.sandbox = None await super()._close() diff --git a/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md b/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md index 92cbf702..b16547e4 100644 --- a/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md +++ b/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md @@ -17,7 +17,7 @@ Inside the code, these functions are available (use `await`): - `await list_documents()` → list of dicts with keys: id, title, uri, created_at Available modules: `json`, `re`, `math`, `pathlib` -Not supported: class definitions, generators/yield, match statements, decorators, `with` statements +Not supported: class inheritance and metaclasses, generators/yield, match statements, decorators, `collections`, iterating a file object (`for line in f`) ### analysis_search Search the knowledge base directly (outside code execution). Each result has a `Type:` (paragraph, table, code, list_item, picture). When the Type is `picture`, the corresponding figure may also be attached to the tool response as an image alongside the text — use it directly to answer questions about figures, diagrams, charts, screenshots. @@ -48,7 +48,7 @@ All documents are mounted as a virtual filesystem at `/documents/`: `{document_id}` is an internal identifier, not the user-facing `uri` (filename, URL, etc.). When you only know a document by its URI or title, use `await list_documents()` to enumerate ids and match against `uri` / `title` — that's a single call to the host. Iterating `/documents/` and reading every `metadata.json` works too but is much slower on portal-scale corpora. ### Reading files -Always use `Path.read_text()` — do NOT use `open()` or `with` statements (they are not supported). +Read with `Path.read_text()` or `open()` (including `with` blocks); file objects support `.read()`, `.readline()`, and `.readlines()`. A file object cannot be iterated, so read line-wise with `.readlines()` or `.read().split("\n")` instead of `for line in f`. Files are read-only; writing raises `PermissionError`. ```python from pathlib import Path @@ -63,7 +63,7 @@ for doc_dir in Path('/documents').iterdir(): content = Path(f'/documents/{doc_id}/content.txt').read_text() # Read and parse items -for line in Path(f'/documents/{doc_id}/items.jsonl').read_text().strip().split(chr(10)): +for line in Path(f'/documents/{doc_id}/items.jsonl').read_text().strip().split("\n"): item = json.loads(line) if item['label'] == 'table': print(item['text'][:200]) @@ -112,6 +112,6 @@ You MUST call `analysis_cite` with at least one chunk ID before producing your f - Use `print()` to output results — the output is your only feedback - When you write code, execute it — don't describe what code would do. But not every question needs code; simple lookups are best answered by `analysis_search → analysis_cite`. - Use `await` for all async functions inside `analysis_execute_code` (`search`, `list_documents`) -- Use `Path.read_text()` to read files — do NOT use `open()`, `with` statements, or `collections` module +- Read files with `Path.read_text()` or `open()`/`with`. For lines use `.readlines()` or `.read().split("\n")`, never `for line in f`. The `collections` module is unavailable. - Do NOT include chunk IDs or UUIDs in your answer text — your answer should read naturally. Use the `analysis_cite` tool separately to register citations. `cite{...}` markdown-style inline references do nothing; only an actual `analysis_cite` tool call registers a citation. - **Before you write your final answer, invoke the `analysis_cite` tool with the supporting chunk_ids.** This is the last tool call before answering whenever your answer draws on retrieved evidence. diff --git a/haiku_rag_slim/haiku/rag/sandbox/sandbox.py b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py index 76486690..0812e250 100644 --- a/haiku_rag_slim/haiku/rag/sandbox/sandbox.py +++ b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py @@ -2,13 +2,19 @@ import asyncio import json import os from collections.abc import AsyncIterator, Callable, Coroutine -from contextlib import asynccontextmanager +from contextlib import asynccontextmanager, suppress from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Any, Literal import pydantic_monty -from pydantic_monty import CallbackFile, MemoryFile, MontyRepl, OSAccess +from pydantic_monty import ( + AsyncMonty, + AsyncMontySession, + CallbackFile, + OSAccess, + ResourceLimits, +) from haiku.rag.config.models import AppConfig from haiku.rag.sandbox.dependencies import AnalysisContext @@ -109,24 +115,26 @@ class Sandbox: """Execute code in a sandboxed Python interpreter. Uses pydantic-monty, a minimal secure Python interpreter written in Rust. - External functions (search, list_documents) are called by Monty code + The interpreter runs in a subprocess worker checked out of an ``AsyncMonty`` + pool. External functions (search, list_documents) are called by Monty code using ``await`` and resolved asynchronously on the host. Documents are exposed via a virtual filesystem at ``/documents/{id}/``. - The interpreter uses a REPL session — variables persist across - ``execute()`` calls within the same Sandbox instance. + The session persists across ``execute()`` calls within the same Sandbox + instance — variables carry over. Call ``close()`` to return the worker to + the pool and shut the pool down. sandbox = Sandbox(db_path, config, context) result = await sandbox.execute("x = await search('query')") result = await sandbox.execute("print(x[0]['content'])") # x persists + await sandbox.close() All database access runs on the event loop that drives ``execute()``. Monty's - file callbacks are synchronous and run on the interpreter's worker thread, so - they bridge back to that loop via ``run_coroutine_threadsafe``; the loop is - free during ``feed_run_async`` (the VM runs on the worker thread), so the - bridge does not deadlock. When a ``rag`` connection is supplied it is used - for every read, so an analysis run drives a single connection on a single - loop; otherwise each read opens an ephemeral read-only connection. + file callbacks are synchronous and run off that loop while ``feed_run`` is + awaited, so they bridge back to it via ``run_coroutine_threadsafe`` without + deadlocking. When a ``rag`` connection is supplied it is used for every read, + so an analysis run drives a single connection on a single loop; otherwise + each read opens an ephemeral read-only connection. """ _db_path: Path @@ -139,9 +147,11 @@ class Sandbox: _doc_chunk_index: dict[str, dict[str, list[str]]] _items_jsonl_cache: dict[str, str] _toc_json_cache: dict[str, str] - _repl: MontyRepl | None + _pool: AsyncMonty | None + _session: AsyncMontySession | None _vfs: OSAccess | None _loop: asyncio.AbstractEventLoop | None + _deadline: float | None def __init__( self, @@ -161,9 +171,11 @@ class Sandbox: self._doc_chunk_index = {} self._items_jsonl_cache = {} self._toc_json_cache = {} - self._repl = None + self._pool = None + self._session = None self._vfs = None self._loop = None + self._deadline = None @asynccontextmanager async def _connection(self) -> "AsyncIterator[HaikuRAG]": @@ -185,17 +197,49 @@ class Sandbox: def _run_on_loop(self, coro: Coroutine[Any, Any, Any]) -> Any: """Run a coroutine on the execute() loop from a synchronous callback. - Called from Monty's worker thread while ``feed_run_async`` leaves the - loop free, so scheduling onto it and blocking for the result is safe. + Called off the loop while ``feed_run`` is awaited, so scheduling onto it + and blocking for the result is safe. + + Blocking here suspends the worker, and Monty checks its duration budget + between interpreter steps, so it cannot check while a read is in flight. + Enforce the budget before starting another read, or code that reads in a + loop overruns it by however long the outstanding reads take. Raising from + inside the callback answers the worker's suspension, which keeps the + session usable — cancelling ``feed_run`` from outside does not, and wedges + the protocol. """ assert self._loop is not None, ( "VFS reads happen during execute(); the loop must be captured first." ) + if self._deadline is not None and self._loop.time() > self._deadline: + coro.close() + raise TimeoutError( + "time limit exceeded: no further document reads after " + f"{self._config.analysis.code_timeout}s" + ) return asyncio.run_coroutine_threadsafe(coro, self._loop).result() - def close(self) -> None: - """Retained for API compatibility; the sandbox owns no resources.""" - return + async def _discard_session(self) -> None: + """Drop a session whose worker is gone. + + The session object is unusable once its worker dies: it answers every + later call with ``RuntimeError: this checkout has already been + finished``. Clearing it makes ``_ensure_initialized`` check out a + replacement, at the cost of the variables the dead worker held. + """ + session, self._session = self._session, None + if session is not None: + with suppress(Exception): + await session.__aexit__(None, None, None) + + async def close(self) -> None: + """Return the worker to the pool and shut the pool down. Idempotent.""" + if self._session is not None: + await self._session.__aexit__(None, None, None) + self._session = None + if self._pool is not None: + await self._pool.__aexit__(None, None, None) + self._pool = None def _build_external_functions(self) -> dict[str, Any]: """Build async external functions for the Monty interpreter.""" @@ -255,12 +299,12 @@ class Sandbox: """Build the virtual filesystem with document data. Mounts per-document directories with: - - metadata.json: MemoryFile (eager, small) + - metadata.json: CallbackFile (eager, small) - content.txt: CallbackFile (lazy, can be large) - items.jsonl: CallbackFile (lazy, bulk-cached) - toc.json: CallbackFile (lazy, bulk-cached) """ - files: list[MemoryFile | CallbackFile] = [] + files: list[CallbackFile] = [] def _deny_write(_path: "PurePosixPath", _content: str | bytes) -> None: raise PermissionError(f"Document files are read-only: {_path}") @@ -370,7 +414,16 @@ class Sandbox: }, ensure_ascii=False, ) - files.append(MemoryFile(f"{doc_dir}/metadata.json", metadata)) + + # MemoryFile has no write hook, so metadata.json goes through the + # same read and deny pair as the rest. Its content is already built. + files.append( + CallbackFile( + f"{doc_dir}/metadata.json", + read=lambda _path, text=metadata: text, + write=_deny_write, + ) + ) def _make_content_reader( did: str, @@ -413,52 +466,79 @@ class Sandbox: return OSAccess(files) - async def _ensure_initialized(self) -> tuple[MontyRepl, OSAccess]: - """Initialize the REPL session and VFS on first use.""" - if self._repl is None: + def _session_limits(self) -> ResourceLimits: + """Resource limits for the worker session. + + Monty spends ``max_duration_secs`` across the session's whole life, and + the session is reused so variables persist between calls. Budget it for + the run rather than for one call, or the first slow call starves every + later one. ``code_timeout`` is enforced per call elsewhere: the read + deadline in ``_run_on_loop`` bounds a call that reads, and the pool's + ``request_timeout`` bounds one that computes. + """ + analysis = self._config.analysis + return {"max_duration_secs": analysis.code_timeout * analysis.max_executions} + + async def _ensure_initialized(self) -> tuple[AsyncMontySession, OSAccess]: + """Check out a worker session and build the VFS on first use.""" + if self._vfs is None: self._vfs = await self._build_vfs() - self._repl = MontyRepl( - limits={ - "max_duration_secs": self._config.analysis.code_timeout, - }, - ) - assert self._repl is not None and self._vfs is not None - return self._repl, self._vfs + if self._pool is None: + # The watchdog counts only time the worker spends running code, so a + # read that blocks the worker never trips it. That leaves the two + # limits disjoint: this one bounds a call that computes, and the read + # deadline bounds a call that reads. + pool = AsyncMonty(request_timeout=self._config.analysis.code_timeout) + await pool.__aenter__() + self._pool = pool + if self._session is None: + session = self._pool.checkout(limits=self._session_limits()) + await session.__aenter__() + self._session = session + assert self._session is not None and self._vfs is not None + return self._session, self._vfs async def execute(self, code: str) -> SandboxResult: - """Execute Python code in the Monty REPL. + """Execute Python code in the Monty worker session. Variables persist across calls within the same Sandbox instance. """ # Monty's synchronous file callbacks bridge DB reads back to this loop. self._loop = asyncio.get_running_loop() - repl, vfs = await self._ensure_initialized() + self._deadline = self._loop.time() + self._config.analysis.code_timeout + session, vfs = await self._ensure_initialized() external_fns = self._build_external_functions() stdout_lines: list[str] = [] - def print_callback( # pragma: no cover - runs on Monty's Rust thread - _stream: Literal["stdout"], text: str + def print_callback( # pragma: no cover - runs on Monty's worker thread + _stream: Literal["stdout", "stderr"], text: str ) -> None: stdout_lines.append(text) max_chars = self._config.analysis.max_output_chars try: - output = await repl.feed_run_async( + output = await session.feed_run( code, - external_functions=external_fns, + external_lookup=external_fns, print_callback=print_callback, os=vfs, ) - except ( - pydantic_monty.MontySyntaxError, - pydantic_monty.MontyRuntimeError, - ) as e: + except (pydantic_monty.MontyError, RuntimeError) as e: stdout = "".join(stdout_lines) if len(stdout) > max_chars: stdout = stdout[:max_chars] + "\n... (output truncated)" - return SandboxResult(stdout=stdout, stderr=str(e), success=False) + stderr = str(e) + # A crash kills the worker, and a protocol error leaves it out of + # step. Both poison the session. Bad user code does not. + if isinstance(e, pydantic_monty.MontyCrashedError | RuntimeError): + await self._discard_session() + stderr = ( + f"{stderr}\n\nThe interpreter restarted. Variables from " + "earlier calls are gone." + ) + return SandboxResult(stdout=stdout, stderr=stderr, success=False) stdout = "".join(stdout_lines) if output is not None: diff --git a/haiku_rag_slim/pyproject.toml b/haiku_rag_slim/pyproject.toml index 827ebaf3..c1e87d84 100644 --- a/haiku_rag_slim/pyproject.toml +++ b/haiku_rag_slim/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "pathspec>=1.0.4", "pydantic>=2.12.5", "pydantic-ai-slim[openai,logfire,ag-ui]>=2.18.0,<3.0.0", - "pydantic-monty>=0.0.17", + "pydantic-monty>=0.0.19", "pypdfium2>=5.0", "python-dotenv>=1.2.2", "pyyaml>=6.0.3", diff --git a/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_read.yaml b/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_read.yaml new file mode 100644 index 00000000..67dccd0c --- /dev/null +++ b/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_read.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '99' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - Content about foxes and dogs. + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 8 + total_tokens: 8 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_readlines.yaml b/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_readlines.yaml new file mode 100644 index 00000000..634000ab --- /dev/null +++ b/tests/cassettes/test_sandbox/TestSandboxVFS.test_open_readlines.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '114' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - The quick brown fox jumps over the lazy dog. + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: FRyZuSpwurxU9KI852o6PEjJn7qA2K08UbWLPTuOuTv1AVs8oBz3O7iwGT0rZN07lsMqOwOsdr0pyPQ7pzQfvWj2sz2Ltga94ZOUO7lIpLp/i0O81ueduxlnrLv3Ecq8gSNzu7nwwTyoaLW8XzpbO3A0lzwTkyM7Q2OJvRuIBztj4b08T+afOxYN67qI6qm8op7gO+aBCLwOT4q896X9vKc0J7zsnlm9odG4PN1MpDxpDoq8po3Lu1S/oTqg6wG8VqQhvbRRCb0SKOa5y2hLPGkdmzyNwGC8SyIyvBM/gTwm3se4sFQOvOh18LuMixa8ChA1O5e/iTvMdT2952XwOxhVkbthMGe8e0urPF7Xn7zVy048Y5wSvYvl37y0cYE9Jdh3vNFq6TwoEXK8lrA0vOGLg7zutuc7RASdvORxmTwK5zU8lq7gPNXEkjo4/ZY884D+PJf1KT012348fhmDu4RjuDwYYew7QNc2PKS87zwOSbS8C/GFPEDNvjvnplA8QwCjvMqcmrwHXDS6GR8UPFf7szyT/vu8c2x8vIIKYLwrWSc8S6ZrvLOz37wIWcq7sVPSu/VRUjtjXdw6/1+cPB0VorsBcio8ALpwO7/vorzvfo68tn9ZPewZKzyV8J47K7daOrEdrDzmeXW8MbG9OyE22TyVf7i8fmaBvJ7xDTpMEiy8EGfcvArJ/7irDIy89rcJPEnReDzRhsy88dMcPDvAN7wI/iG8ABEXvMvzXrqIoge7hEGhuzulvzs0Yps7OtLVvBcxKr3W+qC8PtTiOz87mTwBipU8T0gTPVN9kLrRViu7tNwqPSqNtjyzIFo8lw5TvK+nYLvlAp67QhDtOz8sIzwOauY7WyHzPBCLrzy/9LI8f6kkvALjNL3z/Dw89J17O1SmmTvStRw7XzyFvPDxyDvxw9O8s5WTvEr0ezkl5hW9etlfvP8goDv8jw470nToua/z2zsha7u8m/TWOQE2wTxW6RU8/7ivO5iZ7LsL/8o8JU1LPNx8qLwOkLM7AKYnPBHegrvLuqA7sncFvISARjyvUsI8OQu9PNpe5zwlsR089CRWvC5C+rqWhx26WGp9PPaG+TqYk4+8TnV/vPV1KTztxGA7uZ6oPLNQu7yQlQG9JIyAvCAWm7xAsVg8XircvPGmC7vP9a48AsdFPdCRN7dIyKk8CoHqu1tY6rpPQey8125aPAu9fzqBwVc7YhB4vFdN+7uhTBM8d+m3O5NJzrvY8sC8tSixvPvlDT1hbZk8PwU9O9p7U7xTkRi8u2qBvNTOY7ykpNE7aVuaO4R75jxgBne8+wBvPAHEvrzE/Ne7p2r9vE5hVTv7b4I8+O2nuWgdl7yLUZY8IjFxu2Giy7t9zQA8qPzkuwPq4ruj8Ma7yE4aPWls8roJ0mY7v0iAPI0JG72rIHa8De0OvCdTfztMkoc7EAlXPZqcHrynXuk7SWFeOvJwVTyg1m68b5sNPIOmhLyhAj28JS++PI6FFLwaqv47hlQdu2wEtLpf5SK8QdBvvDW2ublonKQ8XuCkvPtsWTxWUJ07qPhTvU2uhDvtvN28qmJJvGv3gDxQF4g8kCuqvB8kZ7sr8QC90SgQPIDaarx5vz66s6QnPT3OqDw7ehI9HXI1uz8gnDtmB6m8pD/JvEKJRbylUMC7q9TSug1Bmru8yjY9zmFIvF5maLzPefE7WxrVvB2HOb198gc8cnrRvEF02jzwQQI7+bdRvRfN2Dtv+qK6FQJavCkSyjx/tdw8ct2PvDiGCj2Ltiu9koiYuxRoNDodY2c8Mwh+O4KcIz0iS5E8f+Z4PDkLsLwwxBk8DJLIPAMZxjyIvTk77lUDPD685DvTLMS77ouCvJjEkr3pWtI67hnCvGvPprwsLKG7c/ijPJ1P4TzNe6E8R7ORO+eBuzu4DDi9B+e8OzEjKDxGxSc8aZQ2PYvrh7yM/u66rg1sO5Zg1zzn3VC8x0q5vLsCLrzeLwi9dXmsPDWlory7W2k8vcxLujShjjwzHBi9u8utvLy5zjsSQu48Fxp1PEvTxLuH1X48autOvGw/kzx1gOC8hagIvDT+fLuWcQM7anCkPM4TpLurzuu7nccDPJ95+rz89C48nqHHO/BEyrxPSEU9ciLrvLdPr7piPj28Z+ZIu0IEs7ySpki7RMGku4oJE72pmwk9JymEvEK3iLw7PT67a8vGO29h0zt+/5O8MugyvH/LwTwjtBg9cuolvZJO17yz+MA7XG6bvMRgHzx6FK63Btqtu0A84zwEoKk7eo7nu4lN5DzWGRu7eRLAu3xnCrz5bYA8sIAGPTwYqzw7nYW8wgjxu5dVvLuBK/S8v9ZJO3eFwjtpNNe7uHWgPCKhvjvWIcs8ZWm1O3HOKzyTGEA8e6lTPB51lTx+T1698A7JvPI/eLsQe4a6u41VvOi3X72xsdc85mOTOwAijLz+wWO85UrxOlkrbr3Jiks8M0m6vGuN3by0ouO8K1k6vQKUDbqDDWW82VEAPSJISzzZRq68/xGAvEX7l7wz0Jy60RmNu0q2+jsP1KI8hktzPLQporw1ArA8qj/9PDbpYzxTqJ68Un/YPJLD6brsjnk8q7zBPCFd4Dxepdm80pQ1u+2dzDycSw09CsN0PAodE72HRNG8bav8PHWJjjxmjuC7jgk7O6cOWLzikw08ybDjPN+5hLw0bkG80VB5vPexijyMVzQ9/WjdvH65Azyowlm6XnJEPPmrUbsQrcc7BkcRvYFwAD0B8RU89E8CuzpLE71vzv87Ms0VvZFgkLxntLK6Bhp2vFGDzrp7JTC93ybtPMXc3zx2A069SnhivPw54LzU3AA6ZzABO//Wu7tf5rm7kC0xvaXtkryn/9485ksPPQZNezx4Z6q7qnz8uwvqJDwwOT+8mOoCvIFjoDwa3Wm8pi9Ru66AMrwip1+727ANPPw1pLzRecg7NoGVPCKitLytk407BNkFPV8YzDwE65y7dKM4vJUAEjzFDT08vRJDvDGDp7tdh3Q8KXG5PEbtCL1Fudu87X/4uoUWIry7LqE6pE9cO7U46zw2XdE8S0nMPFiBd7yt8NE8DEE6PO+5j7yV3Z68S1D9vNBSSjvdObi7oOa9vA4TwrzhAyC8qSeRvFGJ1zul2GI72WK3O3GdpzsAaoC8PrCsPOMYNLzl1pu8k3TnPI1zfbwfG/G7soKWvOCHV7u6R1A8Y/cQPV3o2DuVIz26a1mEPJtz1jv7DUM8CZSxPF+yITz5jte8I0B2PJmYGL2j25K8Kw4BvVEzwjzE42s8eX08va7WQ7yUNVC8rDD9vJXPO7wR94+8PiyBvEwr/LotSY27Wxd8PHDqCb0FHTK9kPpeOyKwI7zmcse7RxjSvJzuHb3q0uw8ELbxPLvjvbxvN+C5VlEfve191TvqkrY8Xcvqu1kDjzx8Gu87pk7TPGKmBD34hXc8NU8yvA3r/LyZl6y8nvAMPHV/yzsgKBG8GTVovKjD27wHxdE88JLPPH89N7xLGBo9x+6DvLyjhjzdxpU80UJkOlyDoDstIGY8+CONPD+3wDpE/Lw8NNsCvR3vjzvKBTg9wFXYu51WlTq0Txu9nXYYvX7JdzzR86w7iktKvJCXlbxx/9s8aVmau1pg+jr3OWa88vSJvIbFRDym1vK8E3d8O56vqrxiJti8Fke2uu+DJ7zSQbY847nXuanPc7xboQK9ELlWPIBpATzsqI252D1dPIP/uDuTAP+8XjgCvSyA27sFx6Y85XERPTx2UjtO8gm75rKBPCyFqDxAR+U7kYktvJMNezzVzgS82VPQvEwBCL34k0G8/QRbPKvBrLztDNK7K3RVvO1JkDwfXbk8oyoUO2rrgjzSrWi8IRsDPK+z4jgA3eK7F9AovDSwvjzTXzO9udVBPb30XzwUx6s773D9vPnpTjzS5XG8Z1YYPKRHYbu3VgM85faCu1kKAb1Cumg7PsBHvHVq27vA2dO7UyAQvd285Dz9mca8fLEUvShV1zzz1s05DNq3PC00Jj1rtIS8HKOZu85thbu+dFk9bNMlPMyzpLu+KOY7hgBQPBcR67zAURe6clkCu2WkHDtzIUy8RiWLPMa0jjz+cIu8KEfMO5AT4bzm+dU8P77EPOv+zTzoM+K7PQ0tPAwRILzq3Qe8FDS9vEXc7zxKuIg8mTSKPO2r8rwleIU8FPWRPLkmhrzfmp68DHofvZ97jbyZ27y8UPCovGBB6zyyBbq7+6uut+9oG7ywc/g8xPidO4UirTw/Vqa83B+yvP1ECL32Zak7x5WBvNnIljx/8Cw9dXM8uxjeUzxmeLq8DOYRvOytVzuF5q08VTf6POgZqrw8x8O7NHGIuLJ9lry14Wy8yYa7PFcfML0FHQI88yE6PLwmCDztHZI8W/wqvEVGtrv3Qs08rCm+PEsDaTxmvZI8ivd9PNSjgbxeh+c7lK/8vOdjWjziN6c8SbumvGtOWrzHmMg8LctrvUcc97z+z5q8GcLwPE3eeDwM4D884Ji9vENxWby75zg8cI6EPG+qSDyK6yy8DaS3u9GMiLyEcy09ZVgGvB3bJLy0t3i8qvKpPLdc+zu1OTi8ZFwcPY4jRbzfncE8QyIgvXYgibzn/SG9LYxJPCO9Nrx3SnU8L62vPMStVjxWU4S88SMEPbx86zxHhO47C/86vABGDLxmNES8qqdXPEJPqLxqKKG8yHJ2vLJZ3Dx9n2y8EmgePcfnRj2xrfi8hj2CPYPdTLz2ZVg8+zCePHVglDw3JhO8KauWubsckLyAxeM5I/h4PArpXTsJGgw9gKMHvZ3b7jtm8S48nfrTvJgUyDzx1jo7h+sDPUduB7sDqgI8OeFlu2PcVLx9Pxe7mFE3N5ShITyu2fq8AYYPPShhFDzfttK8n4wguybuebxx9Bc9qUQYvX5gMb0wBIG7r22KO1hjeLxp2867OY/NPH7317usvTi8oGU+PHPTurzGDYi69ff6vNdyIrr2OZw7whDPOj6gAzwoRKC73n/EvBVwijywYUk9NzxUvO0Yz7w8Psu724ZGPH/1Fz0Ym8q8UUc3vKzk7rruj6e6/LAzPPIiZTmWFaq81MZSvAIcGDvx27A81ImHvJMRlrwfHoW8jTudO7s7hDzu5Xe8yj3NvE0Yj7wqgk07EknpuoiqyTtqzNG7uSzMO9K6cryUxtY8wmjwuyh69bvqhqe8ywQyvITbXzx7q8G8pzb0PDRQvjrFUPY8AkcEPRYpHD0z+Ze8ZXhjO+UwkzqlyQY8UjHxvMAFJ7xCgb48PnQDvU1VjjnH9jY8TYoivNpJEDlv6Z+7ehEbPL2UWzunf6A8Ab8+vVNcDz21EB09HLJuPNuzIbxpIvM7cuiUPMyrp7wuoDw8GraJvAZP+TzWAlk8OBAwPOvXozyKePI87APNu5RChbuXM5Q7jtpbPDV52TvJISG7cpQWvDzFbrwQ+Ok76ymfvDoWOzxc6cg8uuxzPDA2m7w+39a7OuHWPIJDzDxqFiq7bhfuPDVhjjwA4WS8dpZFu56aaDy7cf06ezxQvOCwobwIyEY7TMvWvFRaMzvpzZG89AU/vEYZXru/xg28R2LNvFcCgry0xsY853xSu1HL1bzIWAy8oKCZu4wy+jwpMZE7XUUEvN4CqLx6lX86PQM/PDbvsbtX/cC8KtpVO/roxDwYdnO8G0WiOzExqDwPO6W8B5J1PJQqyDnW8RC8OjZdOxXizzzmtUQ8QduJu1yVMLsIYiM9XCYNObu5yjt0Su48Dc2pu5tEjLvw1QC8ewwHvexJSbsSsue8uUPdvFyAeDxpNxO7LvkePRGoi7yDMsW8WfB7PKPRs7sCKrU7Q1iOPCllQzzowYE9TIGlvPwUh7vxzok8688BPHMSlrzdg2W8mlYwPOtxAbvP+Ya8MpqRvE+DfzzjIE67NjiIPLM7Tjyhc468ILyAvNIBfzynu8g8VRyWOzWkgTvQZq+6cCImPEO8YTx0kOO7JQcWvakhNDzgQ/o8zTKPvMBmL7wfFw29HLVgvBYtODwBRxC8WgBMvWmu5zoTb4a8H9+hPGmamTyroKq8jCOJPMb+Cj3goAO8Cd6ZO/H0Bj3wY7c6xTw3vKSOaDs2AQy96G4AvNBMbbxEACk9SvSZvFfANz0UQGw8ULCWPO0SPb0IV788ogVFPEBINDxypZq7/YYtu1sQHjwEo9I8kUYgvCvKyjwKOty7L67dvDz4ozw3TBa7SodpvLM5BDzELuG8VocVPAprSDyjgRS8dSWxvIwJj7xkaQs8mzRWO+RG2juQlhy7QYThPJqYPruL8T080x8ivQunJz3gAgY8NRA/vBq8Fbzzkog8FrshPfRNFTtc+Rm9ii0wvaxJoLyPAyc9Gf0bvXFdUDl8G4E85PeXvIP8+bxS9cW6HIl6vP4PjjxW+7u8guCEuxK3+byhNec7P7qcOj2ErrtLzuI7DWaZvCHjs7z28sE745D1PKnYVDykoyy8VL6XPCVB4DyYoiy8nx7gvPRFdDycfhc82EIxPRPaBb088G08ZjaNOi/+ArzJtwK99+yEvNs+QD0Vsua74QBjPSb/q7tWBek65jaIOzobYTxd4EC6rJeIPKUzGr25K4U7PZURvGr3+LzjOko8NeyxPFfGbzyFwFW88esNve+AyjwRVvI8nuViuzw6/bzuciI96na3PMJKiLxQDsy8Hn/LvC5/yTzSElM6remuvPNv3bpnSYO8O8IKu+YdczwQvpY7chQivF1rLzxL3hK9Ch8gvQf0jrxoOfM7wd5VPMN+izsr6WY7YxASO9++mTyrVQQ9aLmePJRq5Tw79pW7BCPxvKTN07qYY0C9ViWNOzIkFrztOVM7djJaPLchMzwJvTI7Ch+5PCe6RL3zqP+8oYXjPE0gKzyFXRi9PUNKvMIgJrxRYDS9x+PlvB9dY7tEe/q8nRh6PKa4e7x6//C7mbkbvFrFprvpwC28MjX2PAXryDtRBZu7lIV3u1JACD3impO7/GnqvE3YFjtlPSk8xcmvuzf3m7wAq3Y7YIJhPWocOryf3NG8RRzMvGMaUDw0KqE7jU0PvAfdYbvc/RK8z82uOznb8LoUNKm87vLeO20BebzUpdE8ybaRvPWSfbwHyuk8i+4YPUrZUTw+Beu8LfyFvE2yezxIqyC87ovwPHyL37yi8UC8pKTHuv2jFL2s6gM9+ORuPDgEuDw0KPO5+CadPL03HzzY/IK8URPlO1lxvbsmxQE8zp65PNKWUjq8RNW8YU7Wu2zFgzxh9sC7wjD2OwKgED3HMQK8RHoqvMFo2jw88Ly8B6JKOsSo8TzpNMm7X4fWO5xozjyk3jE8JsR4OX78OLvyLt47j+REO0vC9bqnt5G8TbArPB6exbw9C7e8pqHXO9P/pLxO4QW9fJ8MO7pRFD26M8A85UCmvLH8cjyCvqE88E1+PHNIjryfFD47zkgivdu2gTv/iXy8t9b+u/ddBzx2eu27trzGvCjukDzwUhE9qvYqvTCfurzf6gU9enjaOxZr37oVY8s7J6XhOl8YFb28uMK8d55BvBxp4Dwsh6C88zzyvAc0HryMeMq7xZyCPOg1Erxv/9s6T9zVvH2AYD2z3gK9SoixvHwRNTs+HrM8uV7nvGNy6jtEUqO8wp9EvL1+wzq8dZE7JS3EvIrvr7xaRne8O1RlPKmk4zywsK48DWnWPIuBuTwaRT08cDE4vHr687uHGKQ8V/PEOyYjObu2Zr68jVuxvGI/JryCkD87LNNYvIjFqLzmPc47J8fgugR9mrrXFBY9shd3PNKnALu5EXE8aS2YPJxI8Tl0zn28LAPaPJq8u7qoeyO7hFmAPX32ybuu7Tm9K67kuxo7y7xysUe8XnBdO1mcQz3jsT68RlOvutXdIDs0zKo833NYPLPA9bqJ/Ye8FOeavLvJtTtnBMc885ENukPozjtpOTK9xqQ7PGkO3LwN0XG7NW/Wu3/d/Ttt6Mo8IUZdvOqSJDtoVV883LtAPWJ3Eb0qVwg6FMC4vCxzFbvoVvS8I06IPGghdjwVP9C6mMsAPM92V7xs/Di8udoxO+eAEDu3aHg82LkBvKQchLz8a4G8VF2LuzHq+jyFKSS8WEuQPPO4SjviLQE9j/wPvXoeJrtUwe88flvFu1Fbhbzid0E84qkEPbT9pDsI4KE8dH2fPLb2Orvqt5k8Bl69PLjsXLs95vg73ikwvM1/3LwV6eU86KWXvEJ7FTzkUvY8Q8kwu6YVcjwjx+s7xJ7RvJX70jtfoCE9niGOPLvpdzy7Ews9Qlzhuc6WMrxIvZy7j7zzO6vigjx3+qq8j6VLvTcsCT19Zog88UgZO4VoCTy/tRA91n/BuRUsDT0lkRK9JIC8O7Y/6rr62o+702ZPPDlrljyBkDq8BvDBuwV72LxuY3O7VhCuPHtkBT0dbJc6sPdwPAfxcDyzcgs8oX+Wurq1ijsA6wq8ejsTvd3djrwoUkC862S0vLX7qrwgQK68JMifO1C8jrz54Ci9onfPOwMkdbzfexo7CW0SvJFdAb2RhPS70+OqPF38JzwGdJs80NbJPK2QujzvbL46eR0JPGyIID10mO28N28FvQ/aJDzc+ii887NmvAdUgbtxYKQ8jtryPBdd0zxsVS47rRR5PJCiL7ySxEo8WU9mPCywpjgjhoy8nOP5PEIW7bugAN27cK3CvIAHibuSMhM96FbmvMfmkzsb8Sa9XA6QvKl/SjuLWEk8VpEzPJYFRLzjb7C8w2UmPMoth7zgQCk92N6cvJ9LAb3vJFu71oeBvACpyjs648W8Y7nsu9vFfTxpAZG7hUfVu3iF5jySLbk7oXE0PbhaAbylT5u8ohbmPMEx0DsVURk7lP37u/Lfm7xeMhS9HLMoPX8S8bwq9dm841HdPAP/QTx8/f673jkRujGgDrutkd88gmKEvENGfDyWXIW7UtnPvJkFsDzaj7g5aacDPBhrybyaeEm8Oar8PA6zaLxJNeS8ZlSHuxN8pbxkVJ+8a877PIusN7yPt828xezxO8SLT7ymKzU8+zxmudRJRzurNyE8AZeHvAqzVjzxRRM84MnMPFckrLyeCxc88nOpu22faDtqR/C8qoOZvI76BDw8/M68M8vZPJkFVrmlZWy7mqesvKhZIr30OBI8L7IlvITGkjzoa4o7NWYAvPozIDyXJYc671WxuvTbQTxrZag70+hvPKWMAr28SkM8KaQQvYRtPrvIEW88veB3PCacwjzEYXQ8DBgBPZEb6jyCLfo7CFUEvOOeuDsww+28nzxsPC15A72zs6W6uy48OxzOZTrVt7E84QmmvCw80Tw8/7i8irOgvBoXkDt97eG88AgivA7vgztWXYQ8BMz6O8i0nLuhc6Y8g0T1PH72rLz4Yrk7w7yMPNbDyzxotAG8T+YZO/FUmjvN6Wq607AqvdUA6bxbYeu8ULWgO9GGKrx71YG8t5ATPM2vBLozoSg8H30uPKMZYTzQmb05EH94O4YQcrwA03K8BuxPOz4ePzxUGJO8F3/VvEIkL7yzHIq8ss8IvTG4j7mVCHY8FgkLPIAvzzwMZNa6ftlYvJkZ/LyizYy86PAQPBAIArwetJ+8UvtkPBHhtTvl3q686qeaPDBiCLsUGj09oAO7O3/wGb3f80k8JC3ludcABD2XiQK7xI/vu/jqRTv5BUA7kxN9vB2N3DwaGu06gSAxPNjR67yt/s86+mOku8ihiDpfspo8x7l+vJ0jrzqoZnS8VtrtvCcvQr1w1qO8eJ32u8YPk7zpMHW8IJKjuv3S2bzvm4Q8ImpjvHDEqLvq3ze9w2ogu/VZJD0FLj48Q+2HPFPgBb2ygtI81CbBvOFYxTxLeZq82kFcvNr7RTu9HfC8LN1pPJOj0rxCPHm6uU8EvfT+SzyoVMq6tjk0vLny0rxRDKQ7FUHAujRGtjsnmxw6++7TOxNOIDwWNkY7DUmtu2ASEL3tqMO7FEomvKC4Azg75OG7vmziPMgdi7sPnZW8LlKzvLwr5jur7GS7A54DvNCgv7l5QJs8Es+sOpU/8Tz/Vsk8w48fOkboAL33LHw7umZVPV4SiDyq6/M8qO7VO+lCsrul4KE8q8NPvFkaDDwUnZa85MmfPNaNuTw9dAo9JL3YPLxpMjz6ubs84jDBO9MNgbwk5XS8+iZTOxAwCDvWx7a7StMxvOUchTrVDLy7QfC8vPYXHzwjfrY8JAUhvaCf2bzhEtc7Q3StOR3jZLyCcvO7vmIKPDq7ET26Tf27XQEZO4OHWTxNKkW9fu21PF0KqTysEVA88gCFvI6sHTxOzEM90xIGPP/m8LykpUE8MiKtPFZdaTx2Jqm8SXZXPAIlS7pXb4c8zyihPNGqJT0orTO8UU76OyxpMbzT2+E8SsfaPG9Q07yeKpg7l/79Ox/5nbzWCTc8Cpl4PH/KOD26xa08JvCzvB+Ox7zujQk7/+QrvL/Q3rwsjQw9MP2Du1DwpjzRZhi8HOMtukUCkTvKYo88i9oyvM6f2rxHnBu9ddA4vBRtgDwU5TO86ZO0vBE4PrwPDBG64yIYvM2ez7xKrdS6BtXgPD7QxLotlyW9mntNvBm9gLweIIa9mHMCPCSDNr2xC8q8j4J5PD5jXTtpFD+8fVwsPdQqFLxXPlG43unqPOPMdTrYcpW8OE3UPG8cqjxWy+q8CfUpveFfB7xQYlk804vqvO4IArzOy/K6zFPaPP5QGj3CE2+8vAnSPCKKXbxHMZ68CiuqPNOvY7oQHJ+7/rV3uz1fOr33D5Q88tK2vKJ1Lb3C5X28G0Jful4OLLyqUX87YE6euo7mPruuzrq6TVe/u/1qHjwKuxI8GsQFvWoRIrtbNA+8zwBMPEybGL2lmyo8U2s6uZs9B71ekb48wvFjPPFJRrx/DXO8YMWru5E6yzpg3qa8aUyHufL517xTcFk754ACPbOfYTwWgIC8/W4UPD3Qy7uZanK84oCSu3j7lTwvB908LpSTO6y/sTzirbM7XSWsvAT47DyUunG7xiGPvEahZryZ8u27saDQPAUzkTwG0Tu9mjhJPLYn1TyZmM28X2VBPMKtGj3urTW83bwGvW9gAby3dNG8R58evLQalTyZuFG8S1yNuoWUczw2FPO8OhPzvMALWLytRBI9HHQZO/fEuDyatIW8azlkOzkwKDr8CEY8vCI9vIyp+7qW1H+8onziuz7O6rl5ncu75lqqPI2OIDumnIG8WHJlPPb517paqiG9oR96POV/izxepxq7PbDivOIhBj38UDY8uYcaPRDV6Du9OlG7KDgXvQUWzLxTkAe94fHkOrpqjjzSJH+7E8IHPV/NX7yHqmy8umA9vG0A97yhyrM78GaSu65mkrxdI5U83nY/u58a2rr8IOg8qcnDPHSEG7w6LXq8YlSxu6/qv7wdMxq8YiLHvFOuDDxPSkG5JmwZvCUjsrzODCy8Y9KqPPq7oLwClju7nBgrPEkJsTxAaAc9uEQgO/Rh7DyIe8+8Wr6ZPHiWPTwszbQ86qynO0l18rw85Ui58VeGu06rsbvAlXA8on2FPGBCPby4VqM6gIT9ORTznLxxRn+8PffzvF2buLp9B6g7fjWxPPZvHr3TOVO8DE09vfn0tbw4vym8IbzdO1IQ6Lr4s4E7rj3IvB/PT7yAuhK84/76vDxdkbv+KZK8/PtkPL5u7LxBL0U8QEjnupQDYbwJcJK8WvofOzAkuTw77A496U4UPPp217wQWqw8u/2ZPGXbXDrF+Aq8dgOdPIMRUzyZB3G8zmheO6+N1bxouB681hwQvSYxojw9FV28h1shvYPsWbzgiym9uStSPGxdP7x9vKw75VQXvFL+4bzznhU9Ec7kvIIFdTys32U8U2LAu42XXzwDvj88N7yKPGuLKbqoJbk8JeTNvOcJkjs7abw8wtypPN9Btbmluyu7GG2SPOIGgTy0kQy7HbwSPXugnbvdrue7/oPFPJpJ57yL65m8MBUMPYoFHLz8JFS6U0nJuogznLy9aDi78lKvu22pBDwhUnG8RLQRu6Zga7yzIAA9RxeevALkOjynEMq4fIH4uz8W67sc1Kg8EEDwvEM0Zrtuklo7fI28O6q/irwy4+w7gPOuPKr0dDysLfG8MJqJO4w+MDsluOG8f2qQOSq2Fr2YBZS7nOYIvCJRrrrWu1O9/J6aOwuWBz1seVG8VG+zvDhKajyiFaS7sdkLPakOejzzIMk89B4+vMyHvTvNlx68jnHRvDI+HDzes+I8CceWPE/fOjwUPUU9PIK6O28QwTuKAQW9O+tmO59TMLxAfOU7uAlWuxUplDw1ZTQ77lyKOwkVWTvkAHg7EQqDu6AdCTymGbG7R2OIvGJqVTq6dX27QQtlvLdCU7qu8iE8nkTHO2MfAL0wk4w7qhoHvJAR0zxyVtY8fDxBO4Jxi7z3YKA7Snu3OxKG+DshLlS8PVxrPB+ypzx29NS7Q4iFOyLUvzwCW4W8r8GbvNiQLLyOsKm7D9aOu01NEjqjzho9uX/duWYNQ7uBCQA9vj50vA+CQjqDnWO7ayXavBLqDjxpiNm77WUkO2vAnTy5EJC8KkeZOxOjqbuTBTy6zgKCPN8UwbtkODa8eYSZuprbUzze7v+8+DMPPK6JszxcCMi7aP1WvEIzTTshM0i80iiJvIIfSrtopCG7eUbxPBBxpLxOp6a8YkhrvGrRbjwe4Em8II0iPP0GwLzgU2y8oIntOUKwcTxQO8Q85pSPPDCzNzlLcys9WOuDvJZOFjwLmZ+6tvRCvIVbH7uNbJu8CYXZO4gcRTsXp528E9Pfu7Dr47q09zc8BBdfPMX3vjz38QI9IOOmPJhdYrx8Yaa7yLgfPMP5CboqH+E82ytRvCTFULtEvmA8vljHu4uWMjwQUBC8q+94vC5N0DwmRKA83rNfvCVvCLyFZ1y8JK4WPKLNCTzf5CS7Rgcyu75fmrzAaIm8+zy2vK5FdLze8hA9FezlPP0FOL2+aNw8mjCqO1sXMLzM24u8Ed2su/q667uEhJQ8EEiyPLfaLLzoE7W8RNLcPDVTV7zKWcO77nCQO/KqL7x1CGW8qgrDPIr/s7vv4IW8Qbw8PEt64bv8ZaC7ElmxPFwWojyfnaU6O9c5uXrmXbySpjG7eDVKu9C2ZTxHh6i7TiZFPA01Bbx2+mE89VDhvJPK4rx31dy8dmP4PFbrEz2mxKk7WaiQvIIZoTwWYxk86TOhvGkSgry8aag8cDgHPJrMZjsL94G8JEPMvM9UyDyGTeE7feNvvNhawjvmPoI8pXKfPHywEjxtbF48dbY3OvAjhrybVts7Ka/VvLcNszx9AOE8tGAOOwt9/zr+NkI9QRtdOwYg3Lq4UYo8DMs9PBoHhLx12qm8r3BcPE1aF7zMOvw7KnqUPJkh1LrD72+8BxP2vMCCI7yIQ/K52FSAudWu7zuIA5M7bKHOO7UGFbxzMBS85oixvOmDsjw6eKe7+ReLvCKiVzxB24C8LwVuuxwQlTyh2yy8GQq0PHjI4rxv74488DM7PJeN07yyoWm8nEaXu935ijwiTLw8HAM9O9jZdbxhoZG8z3LEOqKzm7pR5EG8EO6qvBtsErzwm4e7YLVRPNzCzbsRbQS97D2tO9rNGryCu7y8SLFmvPgMQrwYfKi4C48QPN6My7ycmcQ7Pd3eu2S2ozyyvwW8jsX0u6Xgo7x0Z0w8JTQOvA== + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 11 + total_tokens: 11 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/sandbox/test_sandbox.py b/tests/sandbox/test_sandbox.py index b996fe9d..90423a5a 100644 --- a/tests/sandbox/test_sandbox.py +++ b/tests/sandbox/test_sandbox.py @@ -7,6 +7,7 @@ import pytest from haiku.rag.client import HaikuRAG from haiku.rag.config.models import AppConfig from haiku.rag.sandbox import AnalysisContext, Sandbox, SandboxResult +from haiku.rag.store.models.chunk import Chunk @pytest.fixture(scope="module") @@ -383,6 +384,175 @@ class TestSandboxVFS: assert result.success assert result.stdout.count("True") == 6 + @pytest.mark.asyncio + @pytest.mark.vcr() + async def test_open_read(self, temp_db_path): + """open() and a with-block read document files through the VFS.""" + config = AppConfig() + async with HaikuRAG(temp_db_path, create=True) as client: + doc = await client.create_document( + content="Content about foxes and dogs.", + uri="test://doc", + title="Fox Document", + ) + + context = AnalysisContext() + sb = Sandbox(db_path=temp_db_path, config=config, context=context) + result = await sb.execute( + f"with open('/documents/{doc.id}/content.txt') as f:\n" + " data = f.read()\n" + "print('foxes' in data.lower())" + ) + assert result.success + assert "True" in result.stdout + + @pytest.mark.asyncio + @pytest.mark.vcr() + async def test_open_readlines(self, temp_db_path): + """readlines() splits a newline-delimited VFS file into lines.""" + config = AppConfig() + async with HaikuRAG(temp_db_path, create=True) as client: + doc = await client.create_document( + content="The quick brown fox jumps over the lazy dog.", + uri="test://animals", + title="Animals", + ) + + context = AnalysisContext() + sb = Sandbox(db_path=temp_db_path, config=config, context=context) + result = await sb.execute( + f"lines = open('/documents/{doc.id}/items.jsonl').readlines()\n" + "print(len(lines) > 0)\n" + "import json\n" + "print('self_ref' in json.loads(lines[0]))" + ) + assert result.success + assert result.stdout.count("True") == 2 + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "filename", ["content.txt", "items.jsonl", "toc.json", "metadata.json"] + ) + async def test_write_denied_for_every_document_file(self, temp_db_path, filename): + """Every file in the document VFS is read-only, metadata.json included.""" + from docling_core.types.doc.document import DoclingDocument + from docling_core.types.doc.labels import DocItemLabel + + config = AppConfig() + docling = DoclingDocument(name="d") + docling.add_text(label=DocItemLabel.TEXT, text="Foxes and dogs.") + + async with HaikuRAG(temp_db_path, create=True) as client: + doc = await client.import_document( + docling, + [ + Chunk( + content="Foxes and dogs.", + embedding=[0.1] * config.embeddings.model.vector_dim, + order=0, + ) + ], + uri="test://readonly", + ) + + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) + try: + result = await sb.execute( + "from pathlib import Path\n" + "try:\n" + f" Path('/documents/{doc.id}/{filename}').write_text('nope')\n" + " print('WROTE')\n" + "except PermissionError:\n" + " print('DENIED')" + ) + assert result.success, result.stderr + assert "DENIED" in result.stdout + assert "WROTE" not in result.stdout + finally: + await sb.close() + + @pytest.mark.asyncio + async def test_open_for_writing_is_denied(self, temp_db_path): + """`open()` in write mode is refused, not only `Path.write_text`.""" + from docling_core.types.doc.document import DoclingDocument + from docling_core.types.doc.labels import DocItemLabel + + config = AppConfig() + docling = DoclingDocument(name="d") + docling.add_text(label=DocItemLabel.TEXT, text="Foxes and dogs.") + + async with HaikuRAG(temp_db_path, create=True) as client: + doc = await client.import_document( + docling, + [ + Chunk( + content="Foxes and dogs.", + embedding=[0.1] * config.embeddings.model.vector_dim, + order=0, + ) + ], + uri="test://openwrite", + ) + + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) + try: + result = await sb.execute( + "try:\n" + f" with open('/documents/{doc.id}/content.txt', 'w') as f:\n" + " f.write('nope')\n" + " print('WROTE')\n" + "except PermissionError:\n" + " print('DENIED')" + ) + assert result.success, result.stderr + assert "DENIED" in result.stdout + assert "WROTE" not in result.stdout + finally: + await sb.close() + + @pytest.mark.asyncio + async def test_open_file_objects_are_not_iterable(self, temp_db_path): + """Pins the limitation the instructions warn about: pydantic/monty#490. + + A failure here means Monty gained iteration support and the + `for line in f` prohibition in the analysis instructions is now wrong. + """ + from docling_core.types.doc.document import DoclingDocument + from docling_core.types.doc.labels import DocItemLabel + + config = AppConfig() + docling = DoclingDocument(name="d") + docling.add_text(label=DocItemLabel.TEXT, text="one\ntwo") + + async with HaikuRAG(temp_db_path, create=True) as client: + doc = await client.import_document( + docling, + [ + Chunk( + content="one\ntwo", + embedding=[0.1] * config.embeddings.model.vector_dim, + order=0, + ) + ], + uri="test://lines", + ) + + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) + try: + result = await sb.execute( + f"for line in open('/documents/{doc.id}/content.txt'):\n print(line)" + ) + assert result.success is False + assert "not iterable" in result.stderr + + # The documented alternatives do work. + result = await sb.execute( + f"print(len(open('/documents/{doc.id}/content.txt').readlines()))" + ) + assert result.success, result.stderr + finally: + await sb.close() + @pytest.mark.asyncio @pytest.mark.vcr() async def test_context_filter_limits_vfs(self, temp_db_path): @@ -453,7 +623,7 @@ class TestSandboxHeldConnection: assert int(lines[1]) > 0 assert lines[2] == "True" finally: - sb.close() + await sb.close() @pytest.mark.asyncio @pytest.mark.vcr() @@ -557,7 +727,7 @@ class TestSandboxHeldConnection: assert first.stdout == second.stdout assert not any(t.name == "sandbox-vfs" for t in threading.enumerate()) finally: - sb.close() + await sb.close() @pytest.mark.asyncio @pytest.mark.vcr() @@ -584,21 +754,21 @@ class TestSandboxHeldConnection: assert second.success, second.stderr assert int(second.stdout.strip()) > 0 finally: - sb.close() + await sb.close() @pytest.mark.asyncio async def test_close_is_safe_without_vfs_read(self, temp_db_path): - """close() is a no-op (and safe to call twice) when no VFS read happened.""" + """close() is safe and idempotent before any code has run.""" async with HaikuRAG(temp_db_path, create=True): config = AppConfig() sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) - sb.close() - sb.close() + await sb.close() + await sb.close() @pytest.mark.asyncio @pytest.mark.vcr() async def test_close_is_idempotent_after_vfs_read(self, temp_db_path): - """close() is a safe no-op after a VFS read; no background thread lingers.""" + """close() tears down the worker and is idempotent; no thread lingers.""" config = AppConfig() async with HaikuRAG(temp_db_path, create=True) as client: doc = await client.create_document( @@ -616,5 +786,149 @@ class TestSandboxHeldConnection: ) assert result.success assert not any(t.name == "sandbox-vfs" for t in threading.enumerate()) - sb.close() - sb.close() + await sb.close() + await sb.close() + + +class TestSandboxReadDeadline: + """The VFS bridge suspends the worker for the length of a read, so Monty + cannot check its duration budget while one is in flight. The sandbox + enforces the budget itself, before each read.""" + + @pytest.mark.asyncio + async def test_read_after_deadline_raises_without_scheduling(self, sandbox): + """A read attempted past the deadline fails instead of querying.""" + scheduled = False + + async def _never_runs(): + nonlocal scheduled + scheduled = True + + sandbox._loop = asyncio.get_running_loop() + sandbox._deadline = sandbox._loop.time() - 1.0 + + coro = _never_runs() + with pytest.raises(TimeoutError, match="time limit exceeded"): + sandbox._run_on_loop(coro) + + coro.close() + assert scheduled is False + + def test_session_budget_covers_every_permitted_execution(self, temp_db_path): + """Monty spends its duration budget across the session's whole life, so a + per-call value would let the first call starve the rest.""" + config = AppConfig() + config.analysis.code_timeout = 5.0 + config.analysis.max_executions = 3 + + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) + + assert sb._session_limits() == {"max_duration_secs": 15.0} + + @pytest.mark.asyncio + async def test_refused_read_fails_the_execution(self, temp_db_path, monkeypatch): + """The refusal surfaces as a failed result, not a raised exception.""" + from docling_core.types.doc.document import DoclingDocument + from docling_core.types.doc.labels import DocItemLabel + + config = AppConfig() + docling = DoclingDocument(name="d") + docling.add_text(label=DocItemLabel.TEXT, text="Foxes and dogs.") + + async with HaikuRAG(temp_db_path, create=True) as client: + doc = await client.import_document( + docling, + [ + Chunk( + content="Foxes and dogs.", + embedding=[0.1] * config.embeddings.model.vector_dim, + order=0, + ) + ], + uri="test://deadline", + ) + + def _past_deadline(*_args, **_kwargs): + raise TimeoutError( + "time limit exceeded: no further document reads after 60.0s" + ) + + monkeypatch.setattr(Sandbox, "_run_on_loop", _past_deadline) + + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) + try: + result = await sb.execute( + "from pathlib import Path\n" + f"print(Path('/documents/{doc.id}/content.txt').read_text())" + ) + assert result.success is False + assert "no further document reads" in result.stderr + finally: + await sb.close() + + +class TestSandboxWorkerCrash: + """A dead worker must not poison every later call in the run.""" + + @pytest.mark.asyncio + async def test_crashed_worker_is_replaced(self, temp_db_path): + """The crash fails one call. The next call gets a fresh session.""" + import os + import signal + + config = AppConfig() + async with HaikuRAG(temp_db_path, create=True): + pass + + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) + try: + first = await sb.execute("x = 1\nprint(x)") + assert first.success, first.stderr + assert sb._session is not None + pid = sb._session.worker_pid + assert pid is not None + + os.kill(pid, signal.SIGKILL) + + crashed = await sb.execute("print(2)") + assert crashed.success is False + assert "restarted" in crashed.stderr + + # Without the discard every later call fails on the dead session. + recovered = await sb.execute("print(3)") + assert recovered.success, recovered.stderr + assert "3" in recovered.stdout + + # The replacement worker starts empty, which the failure said. + lost = await sb.execute("print(x)") + assert lost.success is False + finally: + await sb.close() + + +class TestSandboxRequestTimeout: + """The pool watchdog bounds a call that never reads.""" + + @pytest.mark.asyncio + async def test_runaway_compute_is_killed_and_the_next_call_recovers( + self, temp_db_path + ): + """Code that never reads escapes the read deadline. The watchdog kills it.""" + config = AppConfig() + config.analysis.code_timeout = 1.0 + async with HaikuRAG(temp_db_path, create=True): + pass + + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) + try: + runaway = await sb.execute( + "x = 0\nfor i in range(500000000):\n x += i\nprint(x)" + ) + assert runaway.success is False + assert "restarted" in runaway.stderr + + recovered = await sb.execute("print('alive')") + assert recovered.success, recovered.stderr + assert "alive" in recovered.stdout + finally: + await sb.close() diff --git a/tests/sandbox/test_sandbox_toc.py b/tests/sandbox/test_sandbox_toc.py index 25ae2a04..2d359212 100644 --- a/tests/sandbox/test_sandbox_toc.py +++ b/tests/sandbox/test_sandbox_toc.py @@ -83,6 +83,23 @@ def _flatten(tree: list[dict]) -> list[dict]: return out +@pytest.mark.asyncio +class TestMetadataJson: + """metadata.json is served by a reader callback, like the other VFS files.""" + + async def test_metadata_reader_returns_document_fields(self, temp_db_path): + async with HaikuRAG(temp_db_path, create=True) as client: + doc_id = await _empty_doc(client, uri="test://meta", title="Meta Doc") + + sandbox = Sandbox(temp_db_path, AppConfig(), AnalysisContext()) + raw = await _read_vfs_text(sandbox, f"/documents/{doc_id}/metadata.json") + meta = json.loads(raw) + + assert meta["id"] == doc_id + assert meta["title"] == "Meta Doc" + assert meta["uri"] == "test://meta" + + @pytest.mark.asyncio class TestTocShape: """toc.json builds a section tree from heading_level + position.""" diff --git a/uv.lock b/uv.lock index 03a7efcd..d2fb894a 100644 --- a/uv.lock +++ b/uv.lock @@ -1770,7 +1770,7 @@ requires-dist = [ { name = "pydantic-ai-slim", extras = ["mistral"], marker = "extra == 'mistral'" }, { name = "pydantic-ai-slim", extras = ["openai", "logfire", "ag-ui"], specifier = ">=2.18.0,<3.0.0" }, { name = "pydantic-ai-slim", extras = ["voyageai"], marker = "extra == 'voyageai'" }, - { name = "pydantic-monty", specifier = ">=0.0.17" }, + { name = "pydantic-monty", specifier = ">=0.0.19" }, { name = "pypdfium2", specifier = ">=5.0" }, { name = "python-dotenv", specifier = ">=1.2.2" }, { name = "pyyaml", specifier = ">=6.0.3" }, @@ -3981,49 +3981,94 @@ wheels = [ [[package]] name = "pydantic-monty" -version = "0.0.17" +version = "0.0.19" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "pydantic-monty-runtime" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/f8/431ba0b79d02922811392c4e3d283d6508f7052ceaa1936cc34878703ecc/pydantic_monty-0.0.17.tar.gz", hash = "sha256:9c4904a8fbc63282793f3afd2d180124494c7fc371783f365e5691c9586360af", size = 1007724, upload-time = "2026-04-22T20:13:48.915Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/f8/c04df414086488834d102ab85cf8172c114108f842fb142ac92a490213fd/pydantic_monty-0.0.19.tar.gz", hash = "sha256:f3f9e256058b4085349dd4ad347795d5203a8310e9eaad9a2bff93890ac5b86d", size = 1484032, upload-time = "2026-07-24T10:00:13.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/31/95827babdb35149f076c5d191b6b1e7a7c58f4bc72432f905e02e4e3231e/pydantic_monty-0.0.17-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27c2254fa7a7b05e969f79578889230d293c62e0b1ee28371ec4f3c54b14426a", size = 7342248, upload-time = "2026-04-22T20:15:18.775Z" }, - { url = "https://files.pythonhosted.org/packages/cb/67/ca9cfc07cd445d22def53e9db86912f9ae3e11ef772ce41c2ff41a47eac5/pydantic_monty-0.0.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:445cc471ce6f5a88ef06741b7ebc7002a2253d182f55a2f47094d4adedaaf497", size = 7311255, upload-time = "2026-04-22T20:15:27.913Z" }, - { url = "https://files.pythonhosted.org/packages/df/96/abc9c4972d91a9673435b84e12b99d038e42d1f99648fc9e5f242e09d00e/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:39121038405911f59da7bf61164251f59bad3fb1b0cd28f43c42c3949eee2c8a", size = 7868109, upload-time = "2026-04-22T20:15:04.779Z" }, - { url = "https://files.pythonhosted.org/packages/75/82/9e4d55529bb99d882b9277a721762537a8bc1345ab1d052bb614a88bd15b/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dafc8ffe57c257002f623afdb7d0e41f73de850179ebd90b42611e4f2b6f9884", size = 7139709, upload-time = "2026-04-22T20:15:25.386Z" }, - { url = "https://files.pythonhosted.org/packages/96/97/f1af6acefb7bb38d73934d6853998bbd327de7418b811372519080d9fd84/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ea00838ef8f37dcd8085defcbdfe89fdd05297a6533f1d3f4cad857d13cedc7b", size = 7450444, upload-time = "2026-04-22T20:13:37.974Z" }, - { url = "https://files.pythonhosted.org/packages/1d/91/af92ef409e1c065345cf1451bbcf19e00f70a250b8372ec65143ca9a9238/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683d18089acf14d0de293245b9e37c7f0ec64e6d266f6773144211931aa3ec97", size = 7967525, upload-time = "2026-04-22T20:13:42.674Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1f/23ecd6e268ef24ce6b0fe4a1e76a314990d2e923ac5791e29d657418243d/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1829993dd50cf497cbed66ea9f6c8ff7d157d22592a05c7399f92fb8a549e3c", size = 8199124, upload-time = "2026-04-22T20:15:00.02Z" }, - { url = "https://files.pythonhosted.org/packages/42/2a/36b694ea0c7e202250a81a57faf00f218738da6c5d070c752f2d81cd34ce/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a7869e3f41a54cc588096c52a8a4de25ecd81e75c867ea4164b14ea1ae1a57f", size = 7739623, upload-time = "2026-04-22T20:14:37.57Z" }, - { url = "https://files.pythonhosted.org/packages/98/e5/090357d7bc0f0751d1afbb71330695fa26554699c88ba56ecaad91657088/pydantic_monty-0.0.17-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9c17663e2c6f07aec5bc54cd7e39a9e20f250a97a9081e1b2b932eb00d0afc5", size = 7317755, upload-time = "2026-04-22T20:14:48.367Z" }, - { url = "https://files.pythonhosted.org/packages/15/63/67200070cf33325ecfda81d4aee3bf312250ce80bd73058103e04e0f3587/pydantic_monty-0.0.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5d2cf98afe2fb124f6ade91d9663d54277478cad417164f83df1854a41ef450c", size = 7769158, upload-time = "2026-04-22T20:14:39.611Z" }, - { url = "https://files.pythonhosted.org/packages/58/ce/9ecfbc2f45406cfb247fafdea4f4a8412db3e559a22c4385eb15266ba2c1/pydantic_monty-0.0.17-cp312-cp312-win32.whl", hash = "sha256:b2185cc4effbbd6793eed4e0f0bcb6a3dbfbb3289ea4d47888708813f0a3dd47", size = 7227917, upload-time = "2026-04-22T20:14:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/d3/80/9be3bef8273817ccc17da25c3ce4ff5d5d45e5629c17eacc90cdef073821/pydantic_monty-0.0.17-cp312-cp312-win_amd64.whl", hash = "sha256:7833daed757ec9b09b627cc3577a4a76b114c5148f779531d7cfdb1095bcf0a9", size = 8043469, upload-time = "2026-04-22T20:13:22.102Z" }, - { url = "https://files.pythonhosted.org/packages/b5/44/0e106b8b27eb93b66e4f3d279486464e05ba5ee31088848e58b5f506f879/pydantic_monty-0.0.17-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0cdac8c3c16477596bc96ee1cec4f2fbaccd089e2daa1e7b9f227cc89f97cb1", size = 7341507, upload-time = "2026-04-22T20:14:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/e5/88/a0315fa08e62e2d1ef00c03d8202d7bef3f1f71543bebfb916fea265c0a2/pydantic_monty-0.0.17-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37290d6a1c35aba5cfb8b490bb31c0d822e8ddca8f3ca9ea068e30930d80dd1e", size = 7311916, upload-time = "2026-04-22T20:13:34.022Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e5/e4da6acb408594cbfbfb8dd3c0491b9b2ee54e9183e7ebc5f584baa07af9/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:49f252b2fb918686d3e8f76cb30245e782d1560a7fa68dbc0f6940d83c12bd41", size = 7867465, upload-time = "2026-04-22T20:13:31.466Z" }, - { url = "https://files.pythonhosted.org/packages/58/d4/64c2f8eb708a743b0944ea8f71dfd51bc655285b4be28d55577dafbb29fa/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2254d25c34463d67069f5f1567157bde9311654cd5371f8933b8ea9815bfa26a", size = 7139262, upload-time = "2026-04-22T20:14:10.715Z" }, - { url = "https://files.pythonhosted.org/packages/0c/67/5d766f9cd304e871a5dfe5f0a85eaa533538ef07e9b2858fdf9f37f83694/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0688a1fa5dc045ac7b7e996d7269b94f74f116d7b1e352c7b5bb5ad53d4fe03", size = 7450119, upload-time = "2026-04-22T20:13:44.515Z" }, - { url = "https://files.pythonhosted.org/packages/33/98/fa16779021d93edb19807e87cdba56bbec6adfad21f10b41a212572ce513/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b35121ac555ed201405c69d531e4cb916da6984b8cd2e15c8a319117349faf6", size = 7967398, upload-time = "2026-04-22T20:13:55.576Z" }, - { url = "https://files.pythonhosted.org/packages/92/64/287a42720bc9e975ab5b52625aa9fc6bcef8298dd821022cc45c6ee1808d/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c97dc44af25d4392b474902fc40f78f09fe8f44ba791364670334fe12abc077", size = 8198835, upload-time = "2026-04-22T20:15:02.072Z" }, - { url = "https://files.pythonhosted.org/packages/97/37/03edb1fd582b79b2b462afc3fea5e1c8fea73afefc4870dc35fc3c7c492e/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ed2fb365ef9ca921de9a17786ecfa2efe06e65678e6ca57be51658a2a880f31", size = 7739241, upload-time = "2026-04-22T20:13:46.925Z" }, - { url = "https://files.pythonhosted.org/packages/11/0b/b765c9ca2ae27def1caa07345aba073ae1239fc2d9cca7a375f3dc2195f7/pydantic_monty-0.0.17-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5bf9f07b38dd12747e3c95b169a5afb3e2e9107622e01e548246c84d19a69c99", size = 7316719, upload-time = "2026-04-22T20:14:13.058Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3b/64fe872cd575ab5262e1ba2959554ead198c939cfaa425f7f8e9b1ad2694/pydantic_monty-0.0.17-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a5e9bafd4b5acbc0a8e12ee8403a3ce37281c3b0fa5909d3f412bee76c69003c", size = 7769150, upload-time = "2026-04-22T20:13:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/76/b4/b6a0bb41f39bac2e11e6a2fd42ca0886893fafa6344fb17c3f0a94e22e83/pydantic_monty-0.0.17-cp313-cp313-win32.whl", hash = "sha256:1c239ae3e610d3f39cd1609285209a4e2d046b465ac1bfed0d4374c615eed0fa", size = 7227705, upload-time = "2026-04-22T20:15:12.262Z" }, - { url = "https://files.pythonhosted.org/packages/86/e7/d8cd62f537f7ab17714ee19ea221a0e341dab407215376ab1c41d79794c9/pydantic_monty-0.0.17-cp313-cp313-win_amd64.whl", hash = "sha256:1886c3590b02f359ae991f1e76691064f167330eda4fbf22762127ce17d0eb48", size = 8043469, upload-time = "2026-04-22T20:14:24.86Z" }, - { url = "https://files.pythonhosted.org/packages/69/c0/8354baf835e1a04c4b9e11d253f82df7d625a9305e6a23a177fb895b1484/pydantic_monty-0.0.17-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:a166bd04d1996f0d144fbb5e1391cd1c0fbdacd4fa3b689dd48931388679fe98", size = 7341303, upload-time = "2026-04-22T20:14:35.653Z" }, - { url = "https://files.pythonhosted.org/packages/00/ac/d58221b5e17915421ca00bb08b805ac121b6accb194785d5422da4a2f5fc/pydantic_monty-0.0.17-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d82f319e3fd79707a7b81bbb68596509d14ac73502d2f0daf4ab5d281efdfbdc", size = 7318912, upload-time = "2026-04-22T20:13:59.966Z" }, - { url = "https://files.pythonhosted.org/packages/81/3f/8eeb8f652f6cd6e06a737aa9f00de2949e37a669b31756bc51b6182457b4/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f38b9875f7ff56fe69538b60b2ecbdcbe2b8b7780407ceb05fe0ee1414bf8d19", size = 7867027, upload-time = "2026-04-22T20:13:51.426Z" }, - { url = "https://files.pythonhosted.org/packages/55/ba/ec6620c27c8b4cada6ce53378c52c245e238e50a20b968cafdc1b8573c4e/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74a778bb5a4dcdbc85b3b9002f9a72a43fb6ffd88635bfdd502e8d3053008337", size = 7137542, upload-time = "2026-04-22T20:13:35.939Z" }, - { url = "https://files.pythonhosted.org/packages/41/78/5419785630511b54b15cfb094871bcb53ec9025ba8d91bd7ab5b22b6c98f/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e2ab074a65738e9c1b4be9a432e7ca1e9987a6706018dc7a5af6d4ce7cecdf3", size = 7450222, upload-time = "2026-04-22T20:13:53.378Z" }, - { url = "https://files.pythonhosted.org/packages/61/04/cec11fa96a47034da3c21af53e73f43d2270f5ce96cd710809859fe9c0b2/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00fd1cd28b4200c9ccd02629868486b366af1bfe1f0584d4c9e513b7a941a868", size = 7967405, upload-time = "2026-04-22T20:14:03.826Z" }, - { url = "https://files.pythonhosted.org/packages/81/e3/f2be0fb975100b6936ca36a8410098f10fab3b26729c0b0d1de2fac59ff3/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ea6684555bfd00cbe9d2df3e73caada3d82200c168c63cc475197b55b88401", size = 8199028, upload-time = "2026-04-22T20:13:26.802Z" }, - { url = "https://files.pythonhosted.org/packages/2c/43/358bdaa9c50d21fe4a25a71d43ce9af2d6796616fa47aca84f807433564e/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f804a03a3bbd0cf0ade1d4ce11b50ca6858e9c4440b27c746faf1d3c0a272954", size = 7749903, upload-time = "2026-04-22T20:15:09.626Z" }, - { url = "https://files.pythonhosted.org/packages/9a/da/8bcd0a78abf13edceca36aaf5c180fad963e4c2e042fd7247b9e96048306/pydantic_monty-0.0.17-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:b5476e6c08b86b0bea554b97ce9b142aac1177447f2d3c5751864b27991fd1b1", size = 7312704, upload-time = "2026-04-22T20:14:33.668Z" }, - { url = "https://files.pythonhosted.org/packages/88/49/5de8bb7f8b82c3ebb8f2485e0b7a40055b072193039d95dcb5d35fcba72c/pydantic_monty-0.0.17-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b5fcdcca45439844bee268686f37226dbf5803ec7a5945f5536c41419f151dac", size = 7768902, upload-time = "2026-04-22T20:14:29.389Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7c/500a002f1a52f17c8b8a989875da3e1590c18ce95c89856aa967e2348a36/pydantic_monty-0.0.17-cp314-cp314-win32.whl", hash = "sha256:4dd3e6e80a415e7272f7a7583a4f8e045096653f6074e181117eb61fc8fe3b45", size = 7227049, upload-time = "2026-04-22T20:14:01.871Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/b8f552f2a863778f49ebb708f18aaf0bfb275480a48965786e06efacf1c4/pydantic_monty-0.0.17-cp314-cp314-win_amd64.whl", hash = "sha256:36a8090a628e8cf91df8f66c721a71050ac8f48473d4992b9afbd9585941a647", size = 8062999, upload-time = "2026-04-22T20:14:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/36/bf/152bbb3315dfa46d4e4aae71779230e50c67a34d859a3470fd75c01b795c/pydantic_monty-0.0.19-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b073e64edfd62cca918d792d6fe559512472f981f949e53a7aec673201f5f554", size = 2492733, upload-time = "2026-07-24T09:56:49.612Z" }, + { url = "https://files.pythonhosted.org/packages/46/16/9d37f1bf94c47c593a06ecb918bb64a2c8a38af24d6fa2b0d0e031938edf/pydantic_monty-0.0.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5825ae6c40270166f0f31b6e62d59b0fe47201d12b1be5842efc22d3b7dadcee", size = 2234610, upload-time = "2026-07-24T09:56:51.257Z" }, + { url = "https://files.pythonhosted.org/packages/82/4f/31732f4b9c2b6574eb564e420593b9be3f80d92440211970fab23e882bc5/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5b6821c0035ff2c02cf0f37823fb905890f7238ce923bbc3ca937740f9554836", size = 2303116, upload-time = "2026-07-24T09:56:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/21/2c/c21e6179361100dd8b9ad410df775d176a29e0b85f2ffc3144fd29840ee9/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_armv7l.whl", hash = "sha256:5fb4514d99a10e304237a82baeb6072e934ce8462dcd5fb5c3fea0ee0ef16aeb", size = 2007947, upload-time = "2026-07-24T09:56:54.006Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f0/ad64f4894334499f689bdce7e5b5dda6b680d98989424092dcbf21666564/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:2b0f154ac2e6450befa337a99a8519e2f1195cc59f88bd73d780067ace0c4c97", size = 2151468, upload-time = "2026-07-24T09:56:55.626Z" }, + { url = "https://files.pythonhosted.org/packages/2a/97/4ff5f9170e4865ec28fb152bc6ebaa8bd1873e695ee98af2103607ba42e8/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:6fcb4e3a312397432f7c5a0aa04a765670d9f37f4e23f37cb580b3faa2f218b1", size = 2300164, upload-time = "2026-07-24T09:56:57.318Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/c9bfb6708bc5d9f6b040db46156c6e3f16de68ab22f07e2de4f25782414b/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:955d308171ba0260f75d2dede18c46d9732647f94b8f7fba3076bb539f155dc8", size = 2164984, upload-time = "2026-07-24T09:56:58.66Z" }, + { url = "https://files.pythonhosted.org/packages/35/2d/8c1492216632f53e229cb2886c7fd09613d5990f4856f93f7ae0364da9bc/pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3d7e1a6cc1977b24e01b5322888a5ba3f05b112a04a1646ba51f2c34c562a3d9", size = 2357823, upload-time = "2026-07-24T09:57:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/9b/cc/ae4adaaf343de00748fc3598402c1523e48db7094a9c1e0fa26177699440/pydantic_monty-0.0.19-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4904785a34f71f59aa1eb6dc081bafd09e4caa2f028cd379aa3aa846b8a1c27d", size = 2497387, upload-time = "2026-07-24T09:57:01.686Z" }, + { url = "https://files.pythonhosted.org/packages/ec/09/eaef87ed6cbffed9c720dd3cb850e748b0aab11f5ec1ecffb56250973f7d/pydantic_monty-0.0.19-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce15a5326e24cf7045ec9c1456ddb92abd09df91708771c3ae5e72d8e6374c81", size = 2738391, upload-time = "2026-07-24T09:57:03.3Z" }, + { url = "https://files.pythonhosted.org/packages/63/af/58be6fd6ea87e27bd57435013ca1f63d645a0c990c2f23708bcdd048af24/pydantic_monty-0.0.19-cp312-cp312-win32.whl", hash = "sha256:600eb259415e8b2dfef4be38d030c945b3fbb4fb85e727cb97131e7329ab017f", size = 1908274, upload-time = "2026-07-24T09:57:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/20/b7/1cb54e43113cb69c40fb765cfee3be1c222d81153b432131f50508569aee/pydantic_monty-0.0.19-cp312-cp312-win_amd64.whl", hash = "sha256:2c98b1c99994f92ab487a762b107067ab70036f64231e05aa3f6d2b16018688e", size = 2111335, upload-time = "2026-07-24T09:57:06.614Z" }, + { url = "https://files.pythonhosted.org/packages/24/17/0926da051f34ccaa45bf528777dc99e5ea611669cdd7b715be1e086c1fe0/pydantic_monty-0.0.19-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:c01fb1162cf87dbf145b875450eabfde6b35b26f27ed63468398cb4c37732064", size = 2496669, upload-time = "2026-07-24T09:57:07.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/c38f79e24971b4d2205ee156df6e5c6ccdcc720152f54a956cc26e7488b0/pydantic_monty-0.0.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e97dc97de32410003fb5517b72ab47799b4546a3ab48a75e60260cf73734da76", size = 2234599, upload-time = "2026-07-24T09:57:09.458Z" }, + { url = "https://files.pythonhosted.org/packages/5a/89/fb2ed1677ad2c3e2801aa119fdc280d1c64f3480e1015811e0942c9a7d20/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:06318e6add780f66259067829ab16062ce7a556dba0884372a881881b4a7c3bf", size = 2305696, upload-time = "2026-07-24T09:57:10.97Z" }, + { url = "https://files.pythonhosted.org/packages/24/ec/e36ff1c2c97f46420d57680199e7ae2e1eb306d2cfda22be2448e53e7484/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_armv7l.whl", hash = "sha256:5a765a36141bbeb074d89b424d5488bed4e358c603c72606cc9d81ee44d83de2", size = 2007600, upload-time = "2026-07-24T09:57:12.333Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/b47670d3e28f99dc2f4c2686dc42233843dce1a607f3d3cc8a387f3b9b74/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:48f5ebc048779c854f993834586ba372df3b65500d1ed7f147023abc29aeb2c4", size = 2151382, upload-time = "2026-07-24T09:57:13.8Z" }, + { url = "https://files.pythonhosted.org/packages/9f/18/559d71fc66a769c22f6b2a8470515aa6e69a141ab617900fe28a806080b7/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2eb7502f93d8bb568d255fccca95e3b9daca05bb674b9c5323fcba14d088932c", size = 2302643, upload-time = "2026-07-24T09:57:15.42Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/171be42e2ec211bd01fe4be7b6de9ab0c346081edc33830f9f9b781341ab/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:608994600a839dd863940ac84810c48d378390a4fea8c77e5145feb84037e610", size = 2169103, upload-time = "2026-07-24T09:57:16.805Z" }, + { url = "https://files.pythonhosted.org/packages/84/ce/8c47253c8f3f528f0fa2d87dde85623dc3f211189a372e2d69cb6ad896b1/pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:13a652f9dffa6d25ea458fec83108f60f29682caf42cecef91955b5b8cb04365", size = 2358155, upload-time = "2026-07-24T09:57:18.51Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/239107b83bae3a20e4f5424f6c6f3f88bae1fd1e734603c298167985f8be/pydantic_monty-0.0.19-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6167c966db7d8d2fe03940f8da596de6dcd184dcf5cf6209f0a1a9af8792550d", size = 2500858, upload-time = "2026-07-24T09:57:19.878Z" }, + { url = "https://files.pythonhosted.org/packages/48/55/02a7059f20e7198e511ac0b88a3957a2c01b8991326359b7c1bb590a09b8/pydantic_monty-0.0.19-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3102b44307d04f41897ae51d4daeab4fc9b5bf84a78c036781b488876ef998c3", size = 2742212, upload-time = "2026-07-24T09:57:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/a8/28/b632fe0e8eeba3f2b4dbec6ebd4c9b569c20e9597007f2d0fbbf04101307/pydantic_monty-0.0.19-cp313-cp313-win32.whl", hash = "sha256:e43da52776796a894f40533a7e5a322e98d9aaf7d8f6fbb7dc21a0de60a93f41", size = 1908553, upload-time = "2026-07-24T09:57:22.765Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d3/b90872f017871339ceb03e70fa4915ef8682128a476a66adffedfff874d8/pydantic_monty-0.0.19-cp313-cp313-win_amd64.whl", hash = "sha256:fd8c195875f8f44d55bc7d1d53c4e43248b184b3ac803d8131c92d4cc05a1aef", size = 2111345, upload-time = "2026-07-24T09:57:24.351Z" }, + { url = "https://files.pythonhosted.org/packages/eb/11/c2aed55502bfc9620837312f0e2fca7a3d4bc959824a66d81b72144d7256/pydantic_monty-0.0.19-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:2692dc4452937cf2200afd257297e9ac3ccff122b80aa7a69935e8275d684193", size = 2497017, upload-time = "2026-07-24T09:57:25.836Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d0/d4b44a81c71308109cfa642a24b803057615ca1609530c5e66c376780efe/pydantic_monty-0.0.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5a4717f829b35c4bc5d9f6f52a52d19b90729bd494bcb69133bd4c0afcab9c76", size = 2247468, upload-time = "2026-07-24T09:57:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/a9/89/52848dce3acbb1d58df34496db3c4718814cc39f6db01a5025bfdc12b530/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:99f7282213ad6ebf7daf149a88d1517e6328afbf6780180512b9de62f30d292b", size = 2306181, upload-time = "2026-07-24T09:57:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/90/05/f7791c79c7be2240c43a9287196ed49034f773e3f4158492f028e486ebc2/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_armv7l.whl", hash = "sha256:978788b1c56fa0c49927e0a35151f0a635ef2f8d947d16915541ff864d2112f5", size = 2008738, upload-time = "2026-07-24T09:57:30.423Z" }, + { url = "https://files.pythonhosted.org/packages/93/a1/d714258eeb2583acaab2035834acc54ac6baa65bac456f897d901bc0c03a/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:8b821cac39deeb1abb2994d5a65f117ce26e55c586d0f570d425ff469ff48e3c", size = 2152275, upload-time = "2026-07-24T09:57:31.725Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/cdb1fa761e992a489b07a17adb80c21bf99eabd1286ca62f9e73acda1f4b/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:b43c4ffa5651f0eca97458dc673d7952064ae5eb5b836d23967a7d41483bb8f4", size = 2303533, upload-time = "2026-07-24T09:57:33.131Z" }, + { url = "https://files.pythonhosted.org/packages/db/9b/e6685cf82521e68e0dcb94e0c97a1a20410b1266fbce7008c0caa3484039/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:4054610601358943a3dd740a8d59a6812cc682e94d6903cb72baadae1ef5d2ac", size = 2169585, upload-time = "2026-07-24T09:57:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/df/f4/6f031a628d3de72d95bedbb18292ccf998d9a422aff58eedf37347a0b367/pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0b2a34c320968a3cef3d933737e99c932f13c55667315128f366afdaeea2be04", size = 2375171, upload-time = "2026-07-24T09:57:35.907Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d3/4367bccdf2c06a977c0d5ddf816190d570a07199f011034df16d51c84723/pydantic_monty-0.0.19-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ffaf950f4284bb193a54f18fa4e3e8c225b5b52265813abffe492074e90c65fb", size = 2501475, upload-time = "2026-07-24T09:57:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/7a0e1d5c016848afc9a8605aa4f16e6960d68806e775865b986aacaf8f88/pydantic_monty-0.0.19-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:bb1e5ee6762f9494bfb0f4cc316ad3e9a8c7917e3c984a23eb2e2322d401e5cb", size = 2742547, upload-time = "2026-07-24T09:57:39.284Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/92f77cf088f1df72a5dbab109c8c0e82f7ddeb18e65835a8dffcf967bb4a/pydantic_monty-0.0.19-cp314-cp314-win32.whl", hash = "sha256:b68ef6503b39f2f014162e3d8e7f48b9722a43ceb7b6d7199dc6d545f4c37b34", size = 1907832, upload-time = "2026-07-24T09:57:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/cc/3e/85e1a914f81659ea25c34916fdef27fcda2b00323dafb76cf2a5321f7c11/pydantic_monty-0.0.19-cp314-cp314-win_amd64.whl", hash = "sha256:14ef37b43c5bf90966ca51bcad0fae892a3c2546cd151fadc039e0a25ca74073", size = 2123187, upload-time = "2026-07-24T09:57:42.352Z" }, +] + +[[package]] +name = "pydantic-monty-runtime" +version = "0.0.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/29/e44460fd934584ddecc6b8a8acddbc0605e86ea9d027910acdbf526c8f14/pydantic_monty_runtime-0.0.19.tar.gz", hash = "sha256:717e11349d7234575750cec881ac390961de02a40bd09f875dc5e097705b896b", size = 1322628, upload-time = "2026-07-24T10:00:14.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/11/a6f4e12982b2232b9036db334fbcfecbacf46b9acaf311f9c3110e431c53/pydantic_monty_runtime-0.0.19-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:be34905548e31237fc683f5a34986489c127727e8f65481e8c87c4ad0b3a4dc2", size = 9449108, upload-time = "2026-07-24T09:58:44.394Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a0/1e720b1bba457c6a1ab4fca20c571ae058238c7df3e1892b5efebad05a8b/pydantic_monty_runtime-0.0.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0f402f20672c1e25d4915de7e97023519461b9ffbedacd08c427d40daa77bdd6", size = 9735875, upload-time = "2026-07-24T09:58:46.952Z" }, + { url = "https://files.pythonhosted.org/packages/51/97/4439b312d9463881630dd9d998d2c8fe2b8ef457b451202c71816e25688c/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9ec0d25dfbe65b9a5dfe77ff86353e95a86774d786a1d496206163f828f072fd", size = 9171496, upload-time = "2026-07-24T09:58:49.504Z" }, + { url = "https://files.pythonhosted.org/packages/1e/67/fa7b99afe1917b89eec3b4b6375f468c76eeea8227dd48361b7f2db90d97/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_armv7l.whl", hash = "sha256:7ecc2f8eeabf6483908db4cc69b7d4c156a95675dcbdda2a7540a22ed904bfb2", size = 9565495, upload-time = "2026-07-24T09:58:51.913Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a6/83a9dceb8d9f5dffd9a082b60590bb61b0ac48dca19aa02847ebbab1ad46/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:fc1e508b9dc2cab64e27004d0f4ce44c7e1d255e85bafba390884fa07d696319", size = 10199598, upload-time = "2026-07-24T09:58:54.394Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5e/bafd9dfa9a9a0d26b9882053aaea2280d791225c1e3b33b4b48f23fd5f92/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:9e3f6e366f62e54d7bd0fb995f007f42d23c26eee560d57c04d75d5adde3b45c", size = 10355698, upload-time = "2026-07-24T09:58:56.713Z" }, + { url = "https://files.pythonhosted.org/packages/0a/00/ef55cdc9f5ade20475622bb8dba617efce00ef9da2b94b36a76172edadce/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:2ddf4f2d1d063f330bb54b2dc00f6d5bcfbdd3defdc5988b856d350f62160e26", size = 10197859, upload-time = "2026-07-24T09:58:59.158Z" }, + { url = "https://files.pythonhosted.org/packages/a8/67/d1c359658458cf97296fda4359bfc71de8c9f968fb3db68eb7ce00136d43/pydantic_monty_runtime-0.0.19-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ce2c9550c30a5c94b63dfe578243d0823511feeecb020723f5140f9737505410", size = 10661715, upload-time = "2026-07-24T09:59:01.576Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1f/9841d93f956bfbd0bbbac75edf42bb3b13b5bad7d25b09d9a221b5e54d71/pydantic_monty_runtime-0.0.19-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:05563e178b6be783de088abe4a25cef9cfbc04811f7c1e1873860252e711edac", size = 9143212, upload-time = "2026-07-24T09:59:04.019Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ce/0cd3643cc5051456b7baad6f16d85fb1d05daefd0556e4c82ea8f22cc9a2/pydantic_monty_runtime-0.0.19-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:de74aa4df47147d104bccdac4b39c1f3fd543091be3fd0156f77eeea3e483bc3", size = 9731590, upload-time = "2026-07-24T09:59:06.323Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5a/69f4eabec2538df364242395ab3ef77b30a124a0e4b461231589651a1e97/pydantic_monty_runtime-0.0.19-cp312-cp312-win32.whl", hash = "sha256:5208056d9e23d951768ba4b94df3caf7fd84bfe951f68ec4a1803eb03377bbeb", size = 9227834, upload-time = "2026-07-24T09:59:08.694Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4b/221a21f477aef0c488cbe1467111b0988658bc4a42cfc6b404201bc432af/pydantic_monty_runtime-0.0.19-cp312-cp312-win_amd64.whl", hash = "sha256:e4bba0c6024a3a8bd8c8a8cba25233a19cf686218e97afb4c059aa0c625a4b8b", size = 10941520, upload-time = "2026-07-24T09:59:10.959Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a8/1ba497c35eca33273f2144b8e78d94832cc33aec5f69d8bef56968a61933/pydantic_monty_runtime-0.0.19-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:84dd041652581335503af7c60ee7362a462d946a62e8c4a44a939984034d0d25", size = 9449108, upload-time = "2026-07-24T09:59:13.497Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6e/08c05c9729a35d6c9831bfd57d535bd1257f601d15b62936c27f1c0cfb47/pydantic_monty_runtime-0.0.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:263ab33dce2008ca6c83b4013b0280a98a1991d4071c55413d00b539940fe8aa", size = 9735874, upload-time = "2026-07-24T09:59:15.918Z" }, + { url = "https://files.pythonhosted.org/packages/c0/64/63fbdb4c069ceb2af6261fe5923864b5be309799087f16a5dccc96141c12/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:64f56d48097d8a158125534b20e8b22003c5e727082a303907da8e61aae0c7e3", size = 9171498, upload-time = "2026-07-24T09:59:18.799Z" }, + { url = "https://files.pythonhosted.org/packages/1d/cf/82390fe7f0e1100662e6cac7a1c0de716ea4bf851a53aa2b0ef324fc4e3e/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_armv7l.whl", hash = "sha256:a4d80fb4598524cd5039a596f8e6900adb2a5a2da66d012a3734a2b441ad06c3", size = 9565494, upload-time = "2026-07-24T09:59:21.232Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/caa8bf32ba0c0e8ce31ef2773b1a1f60d688e0237cd396e48bbef9f7161f/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:1c5cd0b140c765772e0606a7047fbf95cc49d62d0d8b79b4f520dae0e38b3ba7", size = 10199597, upload-time = "2026-07-24T09:59:23.702Z" }, + { url = "https://files.pythonhosted.org/packages/48/25/ae9bb52518b2ce8fe7509d6cad4f4916b4458b748fecb180bef2d78165e9/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:7d80975c6e792088285f49a91e26de483ef95e5bffc4939b02d4c5ea1059749b", size = 10355699, upload-time = "2026-07-24T09:59:26.389Z" }, + { url = "https://files.pythonhosted.org/packages/a7/38/0875b1239b8ea57e4ea6de421cd240fc5a88c02e381f88d858f00439b1e9/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:f826cb911f78fe4fddf09fa3f38518484041908d08de927fa7dd134c002a2c77", size = 10197858, upload-time = "2026-07-24T09:59:28.889Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/9365473fe31e53a6dc4d6fea406d5d8f3f03a9b7d84d1f29a9e6d664c862/pydantic_monty_runtime-0.0.19-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:fdf56c6cd8c6163737d1ce284f9acb00feff77b7e952f041dc281b94336c4fc9", size = 10661715, upload-time = "2026-07-24T09:59:31.498Z" }, + { url = "https://files.pythonhosted.org/packages/20/f9/8d85b8f77d4006a8d80517a0781c49e6ca026f68747f801b63298e64197e/pydantic_monty_runtime-0.0.19-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a675f165773af0a2e0209dd96014aa9e613115cdc6dad297a7bb96cf87f83315", size = 9143210, upload-time = "2026-07-24T09:59:33.843Z" }, + { url = "https://files.pythonhosted.org/packages/58/53/ded73742ee9fa2160c711141c28ea2ee083f073019e89724049c5e67cd84/pydantic_monty_runtime-0.0.19-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9e42ab0287c0497d9d5529e8a53dd2f63f9d1f92f6592170baab894ea9956da6", size = 9731590, upload-time = "2026-07-24T09:59:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/4b/06/67be8320dc592b8c4caa8c4c1544c8ddf2760029d46843d078aa5a4cdb14/pydantic_monty_runtime-0.0.19-cp313-cp313-win32.whl", hash = "sha256:1f5ff1b9585e648304096705045fb6bd90d43b561568b0d373265e8d201b1234", size = 9227833, upload-time = "2026-07-24T09:59:39.022Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/bab34897974d83640f8773f03d2001142bc13e80e517bfce8c8c4a57157e/pydantic_monty_runtime-0.0.19-cp313-cp313-win_amd64.whl", hash = "sha256:7181a2153ff257fe34109685148167b6e8219d4acfe8f346115b58152fd26aa8", size = 10941519, upload-time = "2026-07-24T09:59:41.538Z" }, + { url = "https://files.pythonhosted.org/packages/63/8d/f46ac4778b2ac64183607bc63ecc783f16ca9981de30eb8ec9aa5e7132cd/pydantic_monty_runtime-0.0.19-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4cc92139b476469c0d7e5caa147d38c2929de39bf1724cb22bbab80297823963", size = 9449107, upload-time = "2026-07-24T09:59:44.139Z" }, + { url = "https://files.pythonhosted.org/packages/a5/14/34a7bb4630d1bac0d055049568ecc888a87954c176428bde5764a7ff6ed7/pydantic_monty_runtime-0.0.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a97e00bd46305f34a85f2e2e3ac4c92dbd3340e7af694aafb192ff58c4fb40e", size = 9735874, upload-time = "2026-07-24T09:59:46.546Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/977de0b258c0858f52b23bd32afbfdf8a8c4614daff5d0b7d5c86332ce6e/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:46ad28b1b3c41113c9e89373da18bcc883a24da371d4eeb9b32d3d194286f1a5", size = 9171497, upload-time = "2026-07-24T09:59:48.768Z" }, + { url = "https://files.pythonhosted.org/packages/39/39/4374200ee4b938fc8b5026056f13bb677e15796bca1323a16210738431ad/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_armv7l.whl", hash = "sha256:dc61844187a32c2f9c2b69846c5d55679eb838b4f7e495b4d03509f89fbf41f9", size = 9565495, upload-time = "2026-07-24T09:59:51.149Z" }, + { url = "https://files.pythonhosted.org/packages/04/55/c4ee4b0a10610359e09cad9d327db19095914a3bf427fb3d8164ec2bcae0/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:b23b52a79ee6be0a943e4b8e5d996e6c489a37b23a337838164f22c9e8ed11a7", size = 10199598, upload-time = "2026-07-24T09:59:53.619Z" }, + { url = "https://files.pythonhosted.org/packages/87/62/cc9df084e9f930bbb2873b6dd832b377f76071aec309b1545589d818fd90/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:484496817f5f238c42aaea8a1945b545a17d8ef2a21e9e81799d55e481d25485", size = 10355699, upload-time = "2026-07-24T09:59:56.043Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/bb13e6f655fcaee340032ad6a3cd1524957d0fa471ddc7e27bdb1f4c240b/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:c3e7cbad58ae9bd3402581faa7d54d719f9c140dc1888f5c9439d45bff528ea1", size = 10197859, upload-time = "2026-07-24T09:59:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/c532715987383668ee835337e1485f51585bc8bf189f033370a05eff17f1/pydantic_monty_runtime-0.0.19-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5534067dc7ffdae809293da95d0e95b6d8481f4c88aff59385e19f466ba3c0f0", size = 10661715, upload-time = "2026-07-24T10:00:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/9a0ab28a061efc184398a0b4db34e459572bb2316cf766f0d6ce65b475e3/pydantic_monty_runtime-0.0.19-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c1ae32b06a4456ab223bafa54d29a349066d625d09063c28ba14ee9019f9b7c2", size = 9143211, upload-time = "2026-07-24T10:00:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/23/5f/af5b3e6395834572975d98f4d1a00a57ee8029bf68ca5732347550f32b35/pydantic_monty_runtime-0.0.19-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:497cf8c3f30992f9aafc8707084eaffe2391b7b5dec067d04d5715f9a562c56b", size = 9731591, upload-time = "2026-07-24T10:00:06.351Z" }, + { url = "https://files.pythonhosted.org/packages/1a/98/96797fd269342cfdb49f03c22fd9a05ef91f711089081c4b3861b9c520e2/pydantic_monty_runtime-0.0.19-cp314-cp314-win32.whl", hash = "sha256:942feb948df8edb61ae7ba6ae77dc655e6985be06d72eef886562fe573ba3086", size = 9227832, upload-time = "2026-07-24T10:00:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/39/fc/02d15281c8e00b48df9af8f75a4fe06f3f8f33ef6a910507a45a19f2b61b/pydantic_monty_runtime-0.0.19-cp314-cp314-win_amd64.whl", hash = "sha256:91d93339c70483ed9256b3b15e3375f6597ae65be280f9b89ba9ca0355f95f54", size = 10941519, upload-time = "2026-07-24T10:00:11.226Z" }, ] [[package]]