From 06b4a8cbcb07de849b23db8686ead478b3e57861 Mon Sep 17 00:00:00 2001 From: Tasnim Kabir Sadik Date: Sat, 2 May 2026 21:46:16 +0600 Subject: [PATCH] Merge issues fixed --- .gitignore | 5 +- .pre-commit-config.yaml | 25 ++++++ .woodpecker/check.yml | 2 +- AGENTS.md | 56 +++++++++++++ pyproject.toml | 3 +- src/wrenn/async_capsule.py | 8 +- src/wrenn/capsule.py | 9 +- src/wrenn/client.py | 15 +++- src/wrenn/code_interpreter/async_capsule.py | 2 +- src/wrenn/code_interpreter/capsule.py | 2 +- src/wrenn/commands.py | 19 +++-- uv.lock | 93 ++++++++++++++++++++- 12 files changed, 216 insertions(+), 23 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 AGENTS.md diff --git a/.gitignore b/.gitignore index 155c313..619209d 100644 --- a/.gitignore +++ b/.gitignore @@ -177,4 +177,7 @@ cython_debug/ CODE_EXECUTION.md .opencode/ -.claude/ +# AI +.code-review-graph/ +.claude +.mcp.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8dc6f53 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.10 + hooks: + - id: ruff + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.20.0 + hooks: + - id: mypy + additional_dependencies: + - pydantic>=2.12.5 + - httpx>=0.28.1 + - httpx-ws>=0.9.0 + - email-validator>=2.3.0 + + - repo: local + hooks: + - id: unit-tests + name: unit tests + entry: uv run pytest -m "not integration" -x -q + language: system + pass_filenames: false + always_run: true diff --git a/.woodpecker/check.yml b/.woodpecker/check.yml index 1cc4e69..3b78cc7 100644 --- a/.woodpecker/check.yml +++ b/.woodpecker/check.yml @@ -1,5 +1,5 @@ when: - event: push + event: pull_request branch: - main - dev diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..53b599d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,56 @@ +# AGENTS.md + +## Project + +Wrenn Python SDK — a client library for the Wrenn microVM platform. e2b drop-in replacement. +Package name: `wrenn`. Python 3.13+, managed with [uv](https://docs.astral.sh/uv/). + +## Commands + +```bash +uv sync # install deps +make lint # ruff check + format check (no auto-fix) +make test # unit tests only (tests/test_client.py) +make test-integration # all tests including integration (needs live server) +make generate # regenerate models from OpenAPI spec (fetches from remote) +make check # lint + unit test +``` + +- `make test` only runs `tests/test_client.py`, not all unit tests. To run a specific test file: `uv run pytest tests/test_capsule_features.py -v` +- No typecheck step in Makefile or CI. `mypy` is a dev dependency but not wired up — do not assume it runs. + +## Architecture + +- `src/wrenn/` — the library package + - `capsule.py` / `async_capsule.py` — high-level `Capsule` / `AsyncCapsule` (main user-facing classes) + - `client.py` — low-level `WrennClient` / `AsyncWrennClient` + - `commands.py` — command execution and streaming + - `files.py` — filesystem operations + - `pty.py` — interactive terminal (PTY) over WebSocket + - `exceptions.py` — typed error hierarchy (`WrennError` base) + - `models/_generated.py` — **auto-generated** from OpenAPI spec via `datamodel-codegen` (never edit directly; run `make generate`) + - `sandbox.py` — deprecated `Sandbox` alias for `Capsule` + - `code_interpreter/` — specialized capsule for stateful Jupyter kernel execution +- `tests/` — unit tests use `respx` to mock `httpx`; integration tests are in `tests/integration/` +- `api/openapi.yaml` — downloaded OpenAPI spec used for code generation + +## Key Conventions + +- Generated code lives in `src/wrenn/models/_generated.py`. Never edit it. Run `make generate` to update. +- `Sandbox` is a deprecated alias for `Capsule`. New code should use `Capsule` / `AsyncCapsule`. +- Dual sync/async API: every major class has an `Async` counterpart. +- Uses `httpx` for HTTP, `httpx-ws` for WebSockets, `pydantic` for models. +- `__init__.py` uses `__getattr__` for lazy deprecated aliases (`Sandbox`, `WrennHostHasSandboxesError`). + +## Testing + +- Unit tests mock HTTP via `respx` (httpx mocking library). +- Integration tests require env vars: `WRENN_API_KEY` (or `WRENN_TOKEN`), optionally `WRENN_BASE_URL`. +- Integration test fixtures in `tests/integration/conftest.py` create real capsules and clean them up. +- `pytest` marker: `@pytest.mark.integration` for tests needing a live server. + +## CI + +Woodpecker CI (`.woodpecker/check.yml`) runs on push to `main` and `dev`: +1. `make lint` +2. `make test` (unit tests only — integration tests are not in CI) diff --git a/pyproject.toml b/pyproject.toml index a235194..c8f5d1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "wrenn" -version = "0.1.0" +version = "0.1.1" description = "Python SDK for Wrenn" readme = "README.md" license = "MIT" @@ -36,6 +36,7 @@ build-backend = "hatchling.build" dev = [ "datamodel-code-generator[ruff]>=0.56.0", "mypy>=1.20.0", + "pre-commit>=4.6.0", "pydoc-markdown>=4.8.2", "pytest>=9.0.3", "pytest-asyncio>=1.3.0", diff --git a/src/wrenn/async_capsule.py b/src/wrenn/async_capsule.py index 24bfbe2..1d72408 100644 --- a/src/wrenn/async_capsule.py +++ b/src/wrenn/async_capsule.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio import logging +import builtins import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager @@ -103,6 +104,7 @@ class AsyncCapsule: memory_mb=memory_mb, timeout_sec=timeout, ) + assert info.id is not None capsule = cls( _capsule_id=info.id, _client=client, @@ -287,7 +289,7 @@ class AsyncCapsule: async def pty( self, cmd: str = "/bin/bash", - args: list[str] | None = None, + args: builtins.list[str] | None = None, cols: int = 80, rows: int = 24, envs: dict[str, str] | None = None, @@ -319,7 +321,7 @@ class AsyncCapsule: """ async with httpx_ws.aconnect_ws( f"/v1/capsules/{self._id}/pty", client=self._client.http - ) as ws: + ) as ws: # type: httpx_ws.AsyncWebSocketSession session = AsyncPtySession(ws, self._id) await session._send_start( cmd=cmd, args=args, cols=cols, rows=rows, envs=envs, cwd=cwd @@ -338,7 +340,7 @@ class AsyncCapsule: """ async with httpx_ws.aconnect_ws( f"/v1/capsules/{self._id}/pty", client=self._client.http - ) as ws: + ) as ws: # type: httpx_ws.AsyncWebSocketSession session = AsyncPtySession(ws, self._id) await session._send_connect(tag) yield session diff --git a/src/wrenn/capsule.py b/src/wrenn/capsule.py index 62865a9..29fe52f 100644 --- a/src/wrenn/capsule.py +++ b/src/wrenn/capsule.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import builtins import time from collections.abc import Iterator from contextlib import contextmanager @@ -96,7 +97,7 @@ class Capsule: """ if _capsule_id is not None: assert _client is not None - self._id = _capsule_id + self._id: str = _capsule_id self._client = _client self._info = _info if self._id is None: @@ -370,7 +371,7 @@ class Capsule: def pty( self, cmd: str = "/bin/bash", - args: list[str] | None = None, + args: builtins.list[str] | None = None, cols: int = 80, rows: int = 24, envs: dict[str, str] | None = None, @@ -401,7 +402,7 @@ class Capsule: """ with httpx_ws.connect_ws( f"/v1/capsules/{self._id}/pty", client=self._client.http - ) as ws: + ) as ws: # type: httpx_ws.WebSocketSession session = PtySession(ws, self._id) session._send_start( cmd=cmd, args=args, cols=cols, rows=rows, envs=envs, cwd=cwd @@ -420,7 +421,7 @@ class Capsule: """ with httpx_ws.connect_ws( f"/v1/capsules/{self._id}/pty", client=self._client.http - ) as ws: + ) as ws: # type: httpx_ws.WebSocketSession session = PtySession(ws, self._id) session._send_connect(tag) yield session diff --git a/src/wrenn/client.py b/src/wrenn/client.py index c927396..c51b190 100644 --- a/src/wrenn/client.py +++ b/src/wrenn/client.py @@ -6,6 +6,7 @@ import httpx from wrenn._config import DEFAULT_BASE_URL, ENV_API_KEY, ENV_BASE_URL from wrenn.exceptions import handle_response + from wrenn.models import ( Template, ) @@ -13,6 +14,8 @@ from wrenn.models import ( Capsule as CapsuleModel, ) +_LONG_TIMEOUT = httpx.Timeout(60.0) + def _resolve_api_key(api_key: str | None) -> str: resolved = api_key or os.environ.get(ENV_API_KEY) @@ -108,7 +111,7 @@ class CapsulesResource: Raises: WrennNotFoundError: If no capsule with the given ID exists. """ - resp = self._http.post(f"/v1/capsules/{id}/pause") + resp = self._http.post(f"/v1/capsules/{id}/pause", timeout=_LONG_TIMEOUT) return CapsuleModel.model_validate(handle_response(resp)) def resume(self, id: str) -> CapsuleModel: @@ -224,7 +227,7 @@ class AsyncCapsulesResource: Raises: WrennNotFoundError: If no capsule with the given ID exists. """ - resp = await self._http.post(f"/v1/capsules/{id}/pause") + resp = await self._http.post(f"/v1/capsules/{id}/pause", timeout=_LONG_TIMEOUT) return CapsuleModel.model_validate(handle_response(resp)) async def resume(self, id: str) -> CapsuleModel: @@ -285,7 +288,9 @@ class SnapshotsResource: params: dict = {} if overwrite: params["overwrite"] = "true" - resp = self._http.post("/v1/snapshots", json=payload, params=params) + resp = self._http.post( + "/v1/snapshots", json=payload, params=params, timeout=_LONG_TIMEOUT + ) return Template.model_validate(handle_response(resp)) def list(self, type: str | None = None) -> list[Template]: @@ -347,7 +352,9 @@ class AsyncSnapshotsResource: params: dict = {} if overwrite: params["overwrite"] = "true" - resp = await self._http.post("/v1/snapshots", json=payload, params=params) + resp = await self._http.post( + "/v1/snapshots", json=payload, params=params, timeout=_LONG_TIMEOUT + ) return Template.model_validate(handle_response(resp)) async def list(self, type: str | None = None) -> list[Template]: diff --git a/src/wrenn/code_interpreter/async_capsule.py b/src/wrenn/code_interpreter/async_capsule.py index a9708c0..b328f6b 100644 --- a/src/wrenn/code_interpreter/async_capsule.py +++ b/src/wrenn/code_interpreter/async_capsule.py @@ -229,7 +229,7 @@ class AsyncCapsule(BaseAsyncCapsule): deadline = time.monotonic() + timeout headers = {"X-API-Key": self._client._api_key} - async with httpx_ws.aconnect_ws(ws_url, headers=headers) as ws: + async with httpx_ws.aconnect_ws(ws_url, headers=headers) as ws: # type: httpx_ws.AsyncWebSocketSession await ws.send_text(json.dumps(msg)) while time.monotonic() < deadline: time_left = deadline - time.monotonic() diff --git a/src/wrenn/code_interpreter/capsule.py b/src/wrenn/code_interpreter/capsule.py index 6aebd27..7d70d91 100644 --- a/src/wrenn/code_interpreter/capsule.py +++ b/src/wrenn/code_interpreter/capsule.py @@ -244,7 +244,7 @@ class Capsule(BaseCapsule): deadline = time.monotonic() + timeout headers = {"X-API-Key": self._client._api_key} - with httpx_ws.connect_ws(ws_url, headers=headers) as ws: + with httpx_ws.connect_ws(ws_url, headers=headers) as ws: # type: httpx_ws.WebSocketSession ws.send_text(json.dumps(msg)) while time.monotonic() < deadline: time_left = deadline - time.monotonic() diff --git a/src/wrenn/commands.py b/src/wrenn/commands.py index c8a0380..98b596e 100644 --- a/src/wrenn/commands.py +++ b/src/wrenn/commands.py @@ -1,6 +1,7 @@ from __future__ import annotations import base64 +import builtins import json from collections.abc import AsyncIterator, Iterator from dataclasses import dataclass @@ -207,6 +208,7 @@ class Commands: timeout=http_timeout, ) data = handle_response(resp) + assert isinstance(data, dict) if background: return CommandHandle( @@ -225,6 +227,7 @@ class Commands: """ resp = self._http.get(f"/v1/capsules/{self._capsule_id}/processes") data = handle_response(resp) + assert isinstance(data, dict) return [ ProcessInfo( pid=p.get("pid", 0), @@ -260,7 +263,7 @@ class Commands: with httpx_ws.connect_ws( f"/v1/capsules/{self._capsule_id}/processes/{pid}/stream", self._http, - ) as ws: + ) as ws: # type: httpx_ws.WebSocketSession while True: try: raw = ws.receive_json() @@ -271,7 +274,9 @@ class Commands: except httpx_ws.WebSocketDisconnect: break - def stream(self, cmd: str, args: list[str] | None = None) -> Iterator[StreamEvent]: + def stream( + self, cmd: str, args: builtins.list[str] | None = None + ) -> Iterator[StreamEvent]: """Execute a command via WebSocket, streaming output as events. Args: @@ -288,7 +293,7 @@ class Commands: with httpx_ws.connect_ws( f"/v1/capsules/{self._capsule_id}/exec/stream", self._http, - ) as ws: + ) as ws: # type: httpx_ws.WebSocketSession if args: start_msg: dict = {"type": "start", "cmd": cmd, "args": args} else: @@ -392,6 +397,7 @@ class AsyncCommands: timeout=http_timeout, ) data = handle_response(resp) + assert isinstance(data, dict) if background: return CommandHandle( @@ -410,6 +416,7 @@ class AsyncCommands: """ resp = await self._http.get(f"/v1/capsules/{self._capsule_id}/processes") data = handle_response(resp) + assert isinstance(data, dict) return [ ProcessInfo( pid=p.get("pid", 0), @@ -447,7 +454,7 @@ class AsyncCommands: async with httpx_ws.aconnect_ws( f"/v1/capsules/{self._capsule_id}/processes/{pid}/stream", self._http, - ) as ws: + ) as ws: # type: httpx_ws.AsyncWebSocketSession try: while True: raw = await ws.receive_json() @@ -459,7 +466,7 @@ class AsyncCommands: pass async def stream( - self, cmd: str, args: list[str] | None = None + self, cmd: str, args: builtins.list[str] | None = None ) -> AsyncIterator[StreamEvent]: """Execute a command via WebSocket, streaming output as events. @@ -477,7 +484,7 @@ class AsyncCommands: async with httpx_ws.aconnect_ws( f"/v1/capsules/{self._capsule_id}/exec/stream", self._http, - ) as ws: + ) as ws: # type: httpx_ws.AsyncWebSocketSession if args: start_msg: dict = {"type": "start", "cmd": cmd, "args": args} else: diff --git a/uv.lock b/uv.lock index bc040cd..36aea7d 100644 --- a/uv.lock +++ b/uv.lock @@ -72,6 +72,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.7" @@ -226,6 +235,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, ] +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -282,6 +300,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + [[package]] name = "genson" version = "1.3.0" @@ -343,6 +370,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/f8/a6bc80313a9e93c888fa10534dfce2ad76ff86911b6f485777ce6de6a073/httpx_ws-0.9.0-py3-none-any.whl", hash = "sha256:71640d2fb1bf9a225775015b33cd755cfd4c5f7e21c885192fe3adc4c387b248", size = 15759, upload-time = "2026-03-28T14:11:11.887Z" }, ] +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -548,6 +584,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "nr-date" version = "2.1.0" @@ -615,6 +660,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + [[package]] name = "pydantic" version = "2.12.5" @@ -745,6 +806,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] +[[package]] +name = "python-discovery" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, +] + [[package]] name = "pytokens" version = "0.4.1" @@ -956,6 +1030,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] +[[package]] +name = "virtualenv" +version = "21.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/8b/6331f7a7fe70131c301106ec1e7cf23e2501bf7d4ca3636805801ca191bb/virtualenv-21.3.0.tar.gz", hash = "sha256:733750db978ec95c2d8eb4feadaa57091002bce404cb39ba69899cf7bd28944e", size = 7614069, upload-time = "2026-04-27T17:05:58.927Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl", hash = "sha256:4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7", size = 7594690, upload-time = "2026-04-27T17:05:55.468Z" }, +] + [[package]] name = "watchdog" version = "6.0.0" @@ -1032,7 +1121,7 @@ wheels = [ [[package]] name = "wrenn" -version = "0.1.0" +version = "0.1.1" source = { editable = "." } dependencies = [ { name = "email-validator" }, @@ -1045,6 +1134,7 @@ dependencies = [ dev = [ { name = "datamodel-code-generator", extra = ["ruff"] }, { name = "mypy" }, + { name = "pre-commit" }, { name = "pydoc-markdown" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -1064,6 +1154,7 @@ requires-dist = [ dev = [ { name = "datamodel-code-generator", extras = ["ruff"], specifier = ">=0.56.0" }, { name = "mypy", specifier = ">=1.20.0" }, + { name = "pre-commit", specifier = ">=4.6.0" }, { name = "pydoc-markdown", specifier = ">=4.8.2" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=1.3.0" },