diff --git a/.bazelignore b/.bazelignore index 92096883..da92b512 100644 --- a/.bazelignore +++ b/.bazelignore @@ -7,9 +7,11 @@ submodules/ node_modules/ rbt/v1alpha1/node_modules/ rbt/std/node_modules/ +reboot/create-ui/node_modules/ reboot/inspect/node_modules/ reboot/nodejs/node_modules/ reboot/react/node_modules/ +reboot/rootpage/node_modules/ reboot/std/node_modules/ reboot/std/react/node_modules/ reboot/web/node_modules/ diff --git a/.gitignore b/.gitignore index 963545cc..b2ad7496 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,7 @@ dist/**/*.tar dist/build_info/source_info.txt # Generated binaries. -bin/ +# TODO: bin/ somehow? /reboot/*.whl # Files inside submodules. This is moot for git itself, since it knows that diff --git a/Dockerfile b/Dockerfile index a302ccb0..ecd1bc03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -431,7 +431,9 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install bash-comple # code. Keep the version of `mypy` in sync with `mypy-requirements.in`. # * `build`, Python's package-builder-frontend. # * `uv`, a fast Python package installer and resolver. -RUN pip install yapf==0.40.2 mypy==1.18.1 isort==5.12.0 ruff==0.1.14 build==1.0.3 uv==0.9.18 +# * `py-spy`, the sampling profiler invoked by `_maybe_run_py_spy()` +# in `reboot/aio/monitoring.py`. +RUN pip install yapf==0.40.2 mypy==1.18.1 isort==5.12.0 ruff==0.1.14 build==1.0.3 uv==0.9.18 py-spy==0.4.2 # Install and setup fish (shell used on codespaces). # NOTE: We do this as it is done here: diff --git a/charts/reboot/Chart.yaml b/charts/reboot/Chart.yaml index afb2e8a5..06e6cbb2 100644 --- a/charts/reboot/Chart.yaml +++ b/charts/reboot/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: 3.3.2 name: reboot -version: "1.0.4" +version: "1.1.0" description: Reboot is a programming framework that enables transactional microservices built with the developer in mind. type: application keywords: @@ -10,4 +10,4 @@ keywords: - scalable - reactive home: https://docs.reboot.dev/ -appVersion: "1.0.4" +appVersion: "1.1.0" diff --git a/documentation/docs/ai_chat_apps/get_started.mdx b/documentation/docs/ai_chat_apps/get_started.mdx index 8f2e73ea..86532313 100644 --- a/documentation/docs/ai_chat_apps/get_started.mdx +++ b/documentation/docs/ai_chat_apps/get_started.mdx @@ -65,9 +65,9 @@ Naming a type \`User\` in your \`API(...)\` is special: - A \`User\` instance is automatically created for each authenticated user that connects to your app. -- During development all users of your app are automatically - "authenticated" with the built-in \`Anonymous\` OAuth provider, so every - user of your app gets a \`User\` instance. +- During development you "sign in" by picking one of the built-in + \`Development\` OAuth provider's fake identities, so every user of + your app gets a \`User\` instance. - \`User\` acts as an entry point: its methods create other state types (like \`Counter\`) whose IDs the AI tracks in its context window. @@ -355,15 +355,31 @@ touch backend/src/main.py ```python # backend/src/main.py import asyncio +from example_prompts import example_prompts from reboot.aio.applications import Application -from reboot.aio.auth.oauth_providers import Anonymous +from reboot.aio.auth.oauth_providers import ( + Development, + OAuthProviderByEnvironment, +) from servicers.counter import CounterServicer, UserServicer async def main() -> None: application = Application( + title="Chat Counter", + description=( + "Lets a chat client create, list, increment, and " + "show counters on your behalf." + ), servicers=[UserServicer, CounterServicer], - oauth=Anonymous(), + oauth=OAuthProviderByEnvironment( + dev=Development(), + # TODO: set a real provider (e.g. `Google(...)`) before + # production; `prod=None` makes a production deployment fail + # to start until one is chosen. + prod=None, + ), + example_prompts=example_prompts, ) await application.run() @@ -412,6 +428,14 @@ touch web/ui/clicker/App.module.css ```css /* web/ui/clicker/App.module.css */ + +/* + * The clicker renders inline inside the MCP host (e.g. Claude.ai), + * so it inherits the host's color scheme. We set every color + * explicitly — never relying on inherited text color — and provide + * a dark variant via `prefers-color-scheme` so the widget stays + * legible in both Claude.ai's light and dark modes. + */ .container { display: flex; align-items: center; @@ -423,6 +447,7 @@ touch web/ui/clicker/App.module.css border: 1px solid #e0e0e0; border-radius: 12px; background: #fafafa; + color: #1a1a1a; } .value { @@ -430,6 +455,7 @@ touch web/ui/clicker/App.module.css font-weight: 600; min-width: 48px; text-align: center; + color: inherit; } .button { @@ -440,11 +466,35 @@ touch web/ui/clicker/App.module.css border-radius: 8px; border: 1px solid #ccc; background: #f5f5f5; + color: #1a1a1a; cursor: pointer; } +.button:hover:not(:disabled) { + background: #ebebeb; +} + .button:disabled { cursor: default; + opacity: 0.5; +} + +@media (prefers-color-scheme: dark) { + .container { + border-color: #3a3a3a; + background: #1e1e1e; + color: #f0f0f0; + } + + .button { + border-color: #4a4a4a; + background: #2d2d2d; + color: #f0f0f0; + } + + .button:hover:not(:disabled) { + background: #3a3a3a; + } } ``` @@ -774,7 +824,7 @@ touch mcp_servers.json ```sh -npx @mcpjam/inspector@2.4.0 --config mcp_servers.json --server counter-server +npx @mcpjam/inspector@2.9.3 --config mcp_servers.json --server counter-server ``` diff --git a/documentation/docs/ai_chat_apps/get_started_claude_code.mdx b/documentation/docs/ai_chat_apps/get_started_claude_code.mdx index 4b23faf0..36604545 100644 --- a/documentation/docs/ai_chat_apps/get_started_claude_code.mdx +++ b/documentation/docs/ai_chat_apps/get_started_claude_code.mdx @@ -1,3 +1,6 @@ +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + # Get Started (with Claude Code) Build an AI Chat App using @@ -15,15 +18,32 @@ installed. ## Install the Reboot skill -Add the Reboot skills marketplace and install the -`reboot-chat-app` skill using Claude Code: + + + +Install the Reboot plugin for Claude Code with a single +command: + +```sh +curl -fsSL https://reboot.dev/install.sh | bash +``` + +This registers the Reboot marketplace, installs the plugin, +and pre-installs its pinned dependencies so your first build +runs without waiting on downloads. + + + + +Add the marketplace and install the plugin yourself with +Claude Code: ``` -claude plugin marketplace add reboot-dev/reboot-skills +claude plugin marketplace add reboot-dev/reboot-plugin ``` ``` -claude plugin install reboot-chat-app@reboot-skills +claude plugin install reboot@reboot-plugin ``` :::note @@ -37,25 +57,28 @@ project's `.claude/settings.json`: ```json { "extraKnownMarketplaces": { - "reboot-skills": { + "reboot-plugin": { "source": { "source": "github", - "repo": "reboot-dev/reboot-skills" + "repo": "reboot-dev/reboot-plugin" } } }, "enabledPlugins": { - "reboot-chat-app@reboot-skills": true + "reboot@reboot-plugin": true } } ``` + + + ## Use the skill Once installed, invoke the skill with a description of your app: ``` -/reboot-chat-app Build a counter app with an interactive clicker UI +/chat-app Build a counter app with an interactive clicker UI ``` The skill enters **plan mode** first. It will: @@ -70,27 +93,30 @@ then builds and runs it. ## What gets created -The skill generates a complete Reboot AI Chat App: +The skill generates a complete Reboot AI Chat App. The most relevant +files are: ``` my-app/ +├── .rbtrc # Reboot CLI config +├── pyproject.toml # Python deps (uv) ├── api/ │ └── my_app/v1/ -│ └── app.py # API definition (Pydantic) +│ └── my_app.py # API definition (Pydantic) ├── backend/ │ └── src/ │ ├── main.py # Application entrypoint │ └── servicers/ -│ └── app.py # Servicer implementation -├── web/ -│ └── ui/ -│ └── my-ui/ -│ ├── index.html -│ ├── main.tsx # RebootClientProvider entry -│ ├── App.tsx # React component -│ └── App.module.css -├── .rbtrc # Reboot CLI config -└── pyproject.toml +│ └── my_app.py # Servicer implementation +└── web/ + ├── package.json + ├── index.css # Styling + └── ui/ + └── my-ui/ + ├── index.html + ├── main.tsx # RebootClientProvider entry + ├── App.tsx # React component + └── App.module.css ``` The API definition uses the same `User` + application type @@ -102,27 +128,18 @@ by the AI. ## Test your app -After the skill finishes building, it starts the app with -`uv run rbt dev run`. You can test it with -[MCPJam Inspector](https://mcpjam.com): - -```sh -npx @mcpjam/inspector@2.4.0 \ - --config mcp_servers.json --server my-app -``` - -See the [hand-written guide](/ai_chat_apps/get_started) for details -on what to try in the inspector. +Once the build succeeds, Claude will run the program for you. You'll see +a link, click it to try your app out! -To connect from other AI clients (VS Code, ChatGPT, Claude, -Goose), see [From an MCP client](/learn_more/call/from_mcp_client). +Claude will also write unit tests for your app, which help to make sure +that your app works - and keeps working. ## Iterate on your app You can also use the skill to modify an existing app: ``` -/reboot-chat-app Add a reset button that sets the counter back to zero +/app Add a reset button that sets the counter back to zero ``` The skill reads your current code, plans the changes, and waits diff --git a/documentation/docs/ai_chat_apps/get_started_codex.mdx b/documentation/docs/ai_chat_apps/get_started_codex.mdx new file mode 100644 index 00000000..dd50bd2b --- /dev/null +++ b/documentation/docs/ai_chat_apps/get_started_codex.mdx @@ -0,0 +1,251 @@ +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +# Get Started (with Codex) + +Build an AI Chat App using [Codex](https://developers.openai.com/codex/) +and the Reboot skill. Codex writes the code for you: describe the app, +approve the proposed design, and it scaffolds the Reboot backend, +interactive UI, tests, and local development setup. + +:::info Prerequisites +Reboot requires Python >= 3.10 and < 3.13, Docker, and +Node.js >= 20.10. You also need +[Codex](https://developers.openai.com/codex/cli) installed and +authenticated. +::: + +## Install the Reboot skill + + + + +Install the Reboot plugin for Codex with a single command: + +```sh +curl -fsSL https://reboot.dev/install.sh | bash +``` + +The installer detects Codex, registers the Reboot plugin marketplace, +installs the plugin, enables Codex hooks, and pre-installs the pinned +tool shims used by the skill. + +:::warning Temporary Codex sandbox opt-out +The installer currently asks to disable Codex's sandbox globally by +writing `sandbox_mode = "danger-full-access"` into +`~/.codex/config.toml`. This is a temporary workaround for an upstream +Codex issue that breaks Python asyncio cross-thread wakeups inside the +sandbox. Reboot's main commands (`rbt generate`, `rbt dev run`, and +related commands) rely on that asyncio behavior. + +If you accept the prompt, the installer writes the setting inside a +managed Reboot block so it is easy to remove later. If you decline, the +Codex install is skipped because the plugin's main commands will not +work reliably under the current sandbox behavior. +::: + +Restart Codex after installation so the new plugin, skills, hooks, and +PATH configuration are loaded. + + + + +Register the Reboot plugin marketplace and install the plugin yourself: + +```sh +codex plugin marketplace add reboot-dev/reboot-plugin +``` + +```sh +codex plugin add reboot@reboot-plugin +``` + +Then add the required Codex settings to `~/.codex/config.toml`. Codex +prints the plugin install path in `codex plugin list`; use it to build +the PATH entry for your machine: + +```sh +REBOOT_PLUGIN_ROOT="$(codex plugin list \ + | awk '$1 == "reboot@reboot-plugin" {print $NF; exit}')" + +printf 'features.hooks = true\n' +printf 'shell_environment_policy.set.PATH = "%s/bin:%s"\n' \ + "$REBOOT_PLUGIN_ROOT" "$PATH" +printf 'sandbox_mode = "danger-full-access"\n' +``` + +Copy the output into `~/.codex/config.toml`. It should look like this, +with your actual plugin path: + +```toml +features.hooks = true +shell_environment_policy.set.PATH = "/path/from/codex/plugin/list/bin:/your/existing/path" +sandbox_mode = "danger-full-access" +``` + +Restart Codex after changing the config. + + + + +## Use the skill + +In Codex, you do not need a slash command. Start from a normal prompt: + +``` +Build a Reboot counter AI chat app with an interactive clicker UI +``` + +Codex selects the Reboot app skill from the prompt and routes to the AI +Chat App builder. The builder plans first. It will: + +1. Analyze your description and propose a state model. +2. Map out the `User` entry point, application types, methods, AI tools, + and UIs. +3. Wait for your approval before writing files. + +Once you approve, Codex scaffolds the full project, runs Reboot code +generation, builds the React UI, writes backend tests, runs the tests, +and starts the local development processes. + +## What gets created + +The skill generates a complete Reboot AI Chat App. The most relevant +files are: + +``` +my-app/ +├── .rbtrc # Reboot CLI config +├── mcp_servers.json # MCPJam connection config +├── pyproject.toml # Python deps (uv) +├── api/ +│ └── my_app/v1/ +│ └── my_app.py # API definition (Pydantic) +├── backend/ +│ ├── src/ +│ │ ├── main.py # Application entrypoint +│ │ └── servicers/ +│ │ └── my_app.py # Servicer implementation +│ └── tests/ +│ └── my_app_test.py # Backend behavior tests +└── web/ + ├── package.json + ├── index.css # Styling + └── ui/ + └── my-ui/ + ├── index.html + ├── main.tsx # RebootClientProvider entry + ├── App.tsx # React component + └── App.module.css +``` + +The API definition uses the same `User` + application type pattern +described in the [hand-written guide](/ai_chat_apps/get_started): +`User` is the auto-constructed entry point whose methods create other +state types, and those types' methods use `mcp=Tool()` to be callable +by the AI. UI methods use `UI()` and render React inside the MCP host. + +## Test your app + +When the build succeeds, Codex starts: + +* the Reboot backend (`rbt dev run`), +* the Vite frontend dev server, and +* the MCPJam Inspector for AI Chat Apps. + +Codex will print the local URLs, including: + +``` +http://127.0.0.1:9991/__/inspect # Reboot state inspector +http://127.0.0.1:6274 # MCPJam Inspector +``` + +Open MCPJam and try a prompt like: + +``` +Create a counter and show me the clicker UI. +``` + +Codex also writes backend tests for the user-facing tool workflows and +runs them before handing off the app. + +## What is running + +After the app is built, Codex starts the local processes needed to use +it: + +* `rbt dev run` runs the Reboot backend and serves the MCP endpoint at + `http://127.0.0.1:9991/mcp`. +* `npm run dev` runs the Vite frontend dev server. Reboot proxies UI + assets through the backend so MCP UIs can hot reload while you edit. +* MCPJam Inspector runs at `http://127.0.0.1:6274` so you can test the + MCP tools and UI from a browser. +* The Reboot plugin starts a `cloudflared` quick tunnel when a Codex + session begins, so remote MCP clients can reach the local Reboot + server when needed. + +## Iterate on your app + +You can use the same skill to modify an existing app: + +``` +Add a reset button that sets the counter back to zero. +``` + +Codex reads the current Reboot project, proposes a plan, waits for your +approval, then updates the API, servicers, React UI, and tests. + +## Run it again later + +:::info Prerequisite +These prompts require the Reboot plugin. If they do not work, reinstall +with `curl -fsSL https://reboot.dev/install.sh | bash` and restart Codex. +::: + +If you close Codex and come back to the project later, ask Codex to run +the Reboot app again: + +``` +Run my Reboot app in ./my-app +``` + +Or, if Codex is already open in the project directory: + +``` +Run this Reboot app +``` + +Codex will use the Reboot run skill to detect that this is an AI Chat +App and start the backend, frontend, and MCPJam Inspector again. The +`cloudflared` tunnel is handled by the plugin when the Codex session +starts. + +## Stop leftover processes + +Codex does not have a session-end hook, so the plugin includes a reaper +script for leftover local processes. Use it if MCPJam is still holding +port `6274` or if you want to stop the `cloudflared` tunnel: + +```sh +REBOOT_PLUGIN_ROOT="$(codex plugin list \ + | awk '$1 == "reboot@reboot-plugin" {print $NF; exit}')" + +bash "$REBOOT_PLUGIN_ROOT/hooks/codex/reap.sh" +``` + +## Next steps + +* **[Get Started (with Claude Code)](/ai_chat_apps/get_started_claude_code)** — + the equivalent agent-driven workflow for Claude Code. +* **[Get Started (hand-written)](/ai_chat_apps/get_started)** — + build the same app step by step to understand every file. +* **[UIs for AI Chat Apps](/learn_more/implement/ui_methods)** — + the full reference for `UI` methods. +* **[Creating tools](/learn_more/define/pydantic#creating-tools-for-the-ai)** — + control which methods are callable by the AI. +* **[AI Chat App Examples](/ai_chat_apps/examples)** — more examples + to explore. +* **[Deploy to Reboot Cloud](/deploy_on_reboot_cloud)** — deploy your + app with `rbt cloud up`. +* **[Join us on Discord](https://discord.gg/cRbdcS94Nr)** — ask + questions and share what you're building! diff --git a/documentation/docs/deploy_on_reboot_cloud.md b/documentation/docs/deploy_on_reboot_cloud.md index 89917eda..3853b9e6 100644 --- a/documentation/docs/deploy_on_reboot_cloud.md +++ b/documentation/docs/deploy_on_reboot_cloud.md @@ -12,7 +12,7 @@ Application starting; your application will be available at: ${unique-id}.prod1.rbt.cloud:9991 ``` -[Join the waitlist](https://cloud.reboot.dev/) today, and [give us your feedback](https://reboot.dev/discord) +[Sign up](https://cloud.reboot.dev/) today, and [give us your feedback](https://reboot.dev/discord) on the shape of the Reboot Cloud! ### Reboot Enterprise diff --git a/documentation/docs/deploy_on_your_own.md b/documentation/docs/deploy_on_your_own.md index c6598215..41f171c6 100644 --- a/documentation/docs/deploy_on_your_own.md +++ b/documentation/docs/deploy_on_your_own.md @@ -60,4 +60,4 @@ See the [comparison](#comparison) below to get a sense of your options. | High availability (failover time) | \~minutes | \~seconds | \~seconds | \~seconds | | Vertical scaling | yes | yes | yes | yes | | Horizontal scaling | no | no | yes | yes | -| Availability | available now | available now | [Join the waitlist](https://cloud.reboot.dev/)! | [Contact Reboot](mailto:team@reboot.dev)! | +| Availability | available now | available now | [available now](https://cloud.reboot.dev/) | [Contact Reboot](mailto:team@reboot.dev)! | diff --git a/documentation/docs/deploy_operate/import_export.md b/documentation/docs/deploy_operate/import_export.md index 0712e5dd..95d43e4b 100644 --- a/documentation/docs/deploy_operate/import_export.md +++ b/documentation/docs/deploy_operate/import_export.md @@ -4,7 +4,7 @@ Reboot supports importing and exporting your application's data via the `rbt imp ## Use cases -Once your application is [deployed to production](/deploy_operate/where), importing and exporting data is critical for creating backups, as well as for exporting data into analytics (OLAP) systems for executing offline queries. +Once your application is [deployed to production](/deploy_on_your_own), importing and exporting data is critical for creating backups, as well as for exporting data into analytics (OLAP) systems for executing offline queries. Because Reboot's support for import/export operates on "logical" snapshots (i.e. with no assumptions about the "physical" storage/filesystem layout that is in use), imports are also a great way to adopt Reboot for a new application, upsize or downsize your hosting, or to create test instances. diff --git a/documentation/docs/deploy_operate/where.md b/documentation/docs/deploy_operate/where.md deleted file mode 100644 index df6fa427..00000000 --- a/documentation/docs/deploy_operate/where.md +++ /dev/null @@ -1,89 +0,0 @@ -# Where to deploy - -There are two ways to deploy a Reboot application: -1. [`rbt serve`](#rbt-serve) -1. [Reboot Cloud](#reboot-cloud) - -## `rbt serve` -`rbt serve` allows you to run your Reboot applications on your own -infrastructure. `rbt serve` is a self-service Reboot deployment, in contrast to -Reboot Cloud which is a fully managed service. - -With `rbt serve` you deploy your application on a single machine, with support -for vertical scaling by using multiple cores. If your application needs to scale -beyond a single machine, [talk to Reboot](mailto:team@reboot.dev) about using -Reboot Cloud. - -### Kubernetes -The recommended deployment model for `rbt serve` on Kubernetes is to store Reboot's state in -either: - -- A replicated block device (Amazon EBS or equivalent). -- An NFSv4 file system (Amazon EFS or equivalent) - this must support file locking. - -As shown in [the comparison](#comparison), these types of persistent storage have -different failover times and replication properties, due to how they are mounted. EFS may -be mounted by multiple machines at a time, and so failover is limited only by Kubernetes' -failover time -- whereas EBS may only be mounted by one machine at a time, and so failover -additionally requires waiting for EBS to release the volume for a new machine to acquire. - -#### Helm chart - -Reboot can be deployed using `rbt serve` atop Kubernetes using a [Helm](https://helm.sh/) chart. - -Start by adding the [`reboot-dev` Helm charts repository](https://reboot-dev.github.io/helm-charts/): - -```bash -helm repo add reboot-dev https://reboot-dev.github.io/helm-charts -helm repo update -``` - -And then deploy Reboot using [your configured values](https://github.com/reboot-dev/helm-charts/blob/main/reboot/values.yaml): - -```bash -helm install my-release reboot-dev/reboot -f my-values.yaml -``` - -### Requirements - -The primary deployment requirement of a `rbt serve` deployment is a -persistent file system for Reboot to store its state (using RocksDB under -the covers). This is the developer's responsibility, and is -accomplished at or below the file system. RocksDB uses file locking to ensure that only one -process is attached to any given database. - -See the [comparison](#comparison) below to get a sense of your options. - -## Reboot Cloud - -To deploy your Reboot app to production, you can use the [Reboot Cloud](https://cloud.reboot.dev/). The Reboot Cloud leverages Reboot's safety guarantees to automatically partition and deploy your application across a cluster of machines, providing automatic scaling and high availability! - -```console -$ rbt cloud up --image-name=... -... -Application starting; your application will be available at: - - ${unique-id}.prod1.rbt.cloud:9991 -``` - -[Join the waitlist](https://cloud.reboot.dev/) today, and [give us your feedback](https://reboot.dev/discord) -on the shape of the Reboot Cloud! - -### Reboot Enterprise - -For users with more stringent compliance requirements, the Reboot Cloud can also -be deployed on your enterprise's Kubernetes cluster, providing all the benefits -of Reboot Cloud on your own hardware. - -[Contact Reboot](mailto:team@reboot.dev) for more information! - -## Comparison - -| | `rbt serve` (EBS or equivalent)| `rbt serve` (EFS or equivalent)| Reboot Cloud | Reboot Cloud Enterprise | -| :-------------------------------- | :----------------------------- | :----------------------------- | :---------------------------------------------- | :---------------------------------------- | -| Physical backups | yes | yes | yes | yes | -| Replication | within an availability zone | within a region | within a region | within a region | -| High availability (failover time) | \~minutes | \~seconds | \~seconds | \~seconds | -| Vertical scaling | yes | yes | yes | yes | -| Horizontal scaling | no | no | yes | yes | -| Availability | available now | available now | [Join the waitlist](https://cloud.reboot.dev/)! | [Contact Reboot](mailto:team@reboot.dev)! | diff --git a/documentation/docs/learn_more/call/from_mcp_client.mdx b/documentation/docs/learn_more/call/from_mcp_client.mdx index c9ff7815..6cc6ed2b 100644 --- a/documentation/docs/learn_more/call/from_mcp_client.mdx +++ b/documentation/docs/learn_more/call/from_mcp_client.mdx @@ -39,7 +39,7 @@ app runs on a different port or host. Then run MCPJam: ```sh -npx @mcpjam/inspector@2.4.0 \ +npx @mcpjam/inspector@2.9.3 \ --config mcp_servers.json --server my-app ``` @@ -152,7 +152,7 @@ Once Developer Mode is on, to connect your deployed Reboot app: running locally behind an `ngrok` tunnel (see [exposing `rbt dev` to the internet](/learn_more/nonlocal#ngrok)) it will be your tunnel's URL with `/mcp` appended. -4. If your app uses auth (`Anonymous` or otherwise), select +4. If your app uses auth (`Development` or otherwise), select **OAuth** from the Authentication dropdown — ChatGPT will run the OAuth handshake against your Reboot app as soon as you add the server. Otherwise, select **No auth**. diff --git a/documentation/docs/learn_more/call/from_outside_your_app.mdx b/documentation/docs/learn_more/call/from_outside_your_app.mdx index 8a162d9b..aa75d1a3 100644 --- a/documentation/docs/learn_more/call/from_outside_your_app.mdx +++ b/documentation/docs/learn_more/call/from_outside_your_app.mdx @@ -118,3 +118,13 @@ async for response in fig.reactively().get_position(context): ``` + +## Making concurrent calls + +When calling multiple Reboot methods externally, you can use the +same concurrency patterns described in +[Making concurrent calls](/learn_more/call/from_within_your_app#making-concurrent-calls). +For Python, the `concurrently` helper provides adaptive +concurrency limits and streaming results — see +[`concurrently` (Python)](/learn_more/call/from_within_your_app#concurrently-python) +for details and examples. diff --git a/documentation/docs/learn_more/call/from_within_your_app.mdx b/documentation/docs/learn_more/call/from_within_your_app.mdx index ff34ceed..24ca2f9c 100644 --- a/documentation/docs/learn_more/call/from_within_your_app.mdx +++ b/documentation/docs/learn_more/call/from_within_your_app.mdx @@ -169,20 +169,14 @@ You can make concurrent calls using - - - ```py -all_customer_balances: list[CustomerAccounts] = await asyncio.gather( +balances = await asyncio.gather( *[ - customer_accounts(entry.value.decode()) - for entry in customer_ids.entries + Account.ref(account_id).balance(context) + for account_id in account_ids ] ) ``` - - + + +```tsx +const map = useOrderedMap({ id: "my-map" }); + +const handleClick = () => { + map.insert({ key: "my-key", value: Value.fromJson("new value") }); +}; + +return ( + +); +``` + + + +Or a reader, such as [`search()`](#search): + + + + +```tsx +const map = useOrderedMap({ id: "my-map" }); + +const { aborted, response } = map.useSearch({ key: "my-key" }); + +if (aborted !== undefined) { + return
Errored while searching.
; +} else if (response === undefined) { + return
Loading...
; +} else if (!response.found) { + return
Not found
; +} else { + return
{response.value?.toJson() as string}
; +} +``` + + + +See [Call your API from +React](/learn_more/call/from_react) to learn more about how to call methods from +React. diff --git a/documentation/docs/library_services/overview.md b/documentation/docs/library_services/overview.md index 7bcb73b4..751712db 100644 --- a/documentation/docs/library_services/overview.md +++ b/documentation/docs/library_services/overview.md @@ -8,6 +8,7 @@ import TabItem from "@theme/TabItem"; The standard library uses Reboot's capabilities to build powerful data structures and APIs for common tasks. The current libraries are: +* [Ciphertext](./ciphertext) - Envelope encryption with a revocable wrapping key: encrypt values at rest and crypto-shred them by destroying a single key. * [PubSub](./pubsub) - A pub/sub implementation that enables you to subscribe and broadcast to topics. * [Queue](./queue) - A durable queue implementation that allows you to enqueue and dequeue items. * [OrderedMap](./ordered_map) - A newer implementation of a collection / dictionary / map that may be larger than memory. We recommend using this. diff --git a/documentation/docs/library_services/pubsub.mdx b/documentation/docs/library_services/pubsub.mdx index 5a41f12a..00fd0c27 100644 --- a/documentation/docs/library_services/pubsub.mdx +++ b/documentation/docs/library_services/pubsub.mdx @@ -30,17 +30,28 @@ To read messages from a subscribed [`Queue`](/library_services/queue), you will + + + ```py from reboot.std.collections.queue.v1.queue import Queue -from reboot.std.item.v1.item import Item from reboot.std.pubsub.v1.pubsub import Topic - ``` +``` + + + + + ```ts import { Queue } from "@reboot-dev/reboot-std/collections/queue/v1"; import { Topic } from "@reboot-dev/reboot-std/pubsub/v1"; ``` + + @@ -50,22 +61,31 @@ Also make sure to include the pub/sub servicers when starting up your `Applicati ```py -from reboot.std.pubsub.v1 import pubsub +from reboot.std.collections.queue.v1.queue import queue_library +from reboot.std.collections.v1.sorted_map import sorted_map_library +from reboot.std.pubsub.v1.pubsub import pubsub_library async def main(): application = Application( - servicers=[MyServicer] + pubsub.servicers(), + servicers=[MyServicer], + libraries=[ + pubsub_library(), + queue_library(), + sorted_map_library(), + ], ) - await application.run() ``` ```ts -import pubsub from "@reboot-dev/reboot-std/pubsub/v1"; +import { queueLibrary } from "@reboot-dev/reboot-std/collections/queue/v1"; +import { sortedMapLibrary } from "@reboot-dev/reboot-std/collections/v1/sorted_map.js"; +import { pubsubLibrary } from "@reboot-dev/reboot-std/pubsub/v1"; new Application({ - servicers: [MyServicer, ...pubsub.servicers()], + servicers: [MyServicer], + libraries: [pubsubLibrary(), queueLibrary(), sortedMapLibrary()], initialize, }).run(); ``` @@ -74,20 +94,34 @@ new Application({ ## Referencing Topics -This creates references to two `Topic`s with IDs `"my-first-topic"` and `"my-second-topic"`. +This creates references to `Topic`s with IDs `"my-first-topic"` and +`"my-second-topic"`. You can then call methods on these references. + + + ```py first_topic = Topic.ref("my-first-topic") second_topic = Topic.ref("my-second-topic") +third_topic = Topic.ref("my-third-topic") ``` + + + + + ```ts const firstTopic = Topic.ref("my-first-topic"); const secondTopic = Topic.ref("my-second-topic"); ``` + + @@ -95,12 +129,18 @@ const secondTopic = Topic.ref("my-second-topic"); ### Publish -Publish a single message as a `Value`, `bytes`, or `Any`. +Publish a single [`Item`](/library_services/item) directly from a `Value`, `bytes`, or `Any`. + +Note: The ability to add an `Item` with an `Any` format is only supported in Python, not TypeScript. + + + ```py -from reboot.protobuf import from_dict +from reboot.protobuf import from_dict, pack await first_topic.publish( context, @@ -109,13 +149,26 @@ await first_topic.publish( await second_topic.publish(context, bytes=b"my-bytes") -await third_topic.publish(context, any=) +await third_topic.publish(context, any=pack(any)) ``` + + + + + ```ts import { Value } from "@bufbuild/protobuf"; +``` + + + + +```ts await firstTopic.publish(context, { value: Value.fromJson({ details: "details-go-here" }), }); @@ -123,18 +176,16 @@ await firstTopic.publish(context, { await secondTopic.publish(context, { bytes: new TextEncoder().encode("my-bytes"), }); - -await thirdTopic.publish(context, { - any: , -}); ``` + + ### Subscribe Subscribe a single [`Queue`](/library_services/queue) as referenced by its ID to a `Topic`. -Any subsequent messages publishes to the `Topic` will be received by +Any subsequent messages published to the `Topic` will be received by [`dequeue`](/library_services/queue#dequeue)ing on the [`Queue`](/library_services/queue). Note: Any messages published to the `Topic` *before* the `Queue` is subscribed @@ -142,18 +193,32 @@ is not guaranteed to be received by the `Queue`. + + + ```py await first_topic.subscribe(context, queue_id="receiving-queue") + response = await Queue.ref("receiving-queue").dequeue(context) ``` + + + + + ```ts await firstTopic.subscribe(context, { queueId: "receiving-queue", }); + const response = await Queue.ref("receiving-queue").dequeue(context); ``` + + @@ -161,15 +226,28 @@ const response = await Queue.ref("receiving-queue").dequeue(context); Publish a list of messages, formatted as [`Item`](/library_services/item)s. -This examples demonstrates how to store different types of data in `Value`, but +This example demonstrates how to store different types of data in `Value`, but in practice, you will want to use a uniform structure for all `Value`s published to a `Topic`. +Note: The ability to add an `Item` with an `Any` format is only supported in Python, not TypeScript. + + + + ```py -from reboot.protobuf import from_bool, from_dict, from_int, from_list, from_str -from reboot.std.collections.queue.v1.queue import Item +from reboot.protobuf import ( + from_bool, + from_dict, + from_int, + from_list, + from_str, + pack, +) +from reboot.std.item.v1.item import Item await first_topic.publish( context, @@ -193,17 +271,29 @@ await second_topic.publish( await third_topic.publish( context, items=[ - Item(any=), - Item(any=), + Item(any=pack(any)), + Item(any=pack(any)), ], ) ``` + + + + + ```ts import { Value } from "@bufbuild/protobuf"; -import { Item } from "@reboot-dev/reboot-std/collections/queue/v1"; +``` + + + + +```ts await firstTopic.publish(context, { items: [ { value: Value.fromJson(null) }, @@ -221,13 +311,8 @@ await secondTopic.publish(context, { { bytes: new TextEncoder().encode("some-more-bytes") }, ], }); - -await thirdTopic.publish(context, { - items: [ - { any: }, - { any: }, - ], -}); ``` + + diff --git a/documentation/docs/library_services/queue.mdx b/documentation/docs/library_services/queue.mdx index c84e1189..2515ca55 100644 --- a/documentation/docs/library_services/queue.mdx +++ b/documentation/docs/library_services/queue.mdx @@ -16,7 +16,7 @@ these formats in a given `Queue`. Read more about [`Item`](/library_services/ite to determine the best format for your data. -## Imports and servicers +## Imports and set up To use a `Queue`, import the library where you would like to use it. @@ -39,35 +39,39 @@ from reboot.std.item.v1.item import Item ```ts -import queue, { Queue } from "@reboot-dev/reboot-std/collections/queue/v1"; +import { Queue } from "@reboot-dev/reboot-std/collections/queue/v1"; ``` -Also make sure to include the `Queue` servicers when starting up your `Application`. -(Note: this import is different from above.) +Also make sure to include the `Queue` library and the dependent [`SortedMap`](./sorted_map) +library when starting up your `Application`. (Note: this import is different +from above.) ```py -from reboot.std.collections.queue.v1 import queue +from reboot.std.collections.queue.v1.queue import queue_library +from reboot.std.collections.v1.sorted_map import sorted_map_library async def main(): application = Application( - servicers=[MyServicer] + queue.servicers(), + servicers=[MyServicer], + libraries=[queue_library(), sorted_map_library()], ) - await application.run() ``` ```ts -import queue from "@reboot-dev/reboot-std/collections/queue/v1"; +import { queueLibrary } from "@reboot-dev/reboot-std/collections/queue/v1"; +import { sortedMapLibrary } from "@reboot-dev/reboot-std/collections/v1/sorted_map.js"; new Application({ - servicers: [MyServicer, ...queue.servicers()], + servicers: [MyServicer], + libraries: [queueLibrary(), sortedMapLibrary()] initialize, }).run(); ``` @@ -82,19 +86,20 @@ and `"my-second-queue"`. You can then call methods on these references. +(CODE:src=../../../tests/reboot/std/collections/queue/v1/queue_tests.py&lines=397-399) --> ```py first_queue = Queue.ref("my-first-queue") second_queue = Queue.ref("my-second-queue") +third_queue = Queue.ref("my-third-queue") ``` +(CODE:src=../../../tests/reboot/std/collections/queue/v1/queue_tests.ts&lines=75-76) --> ```ts @@ -113,12 +118,12 @@ const secondQueue = Queue.ref("my-second-queue"); Enqueues a single [`Item`](/library_services/item) directly from a `Value`, `bytes`, or `Any`. -NOTE: `Any` is only supported in Python, not TypeScript. +NOTE: The ability to add an `Item` with an `Any` format is only supported in Python, not TypeScript. +(CODE:src=../../../tests/reboot/std/collections/queue/v1/queue_tests.py&lines=410-419) --> ```py @@ -147,7 +152,7 @@ import { Value } from "@bufbuild/protobuf"; +(CODE:src=../../../tests/reboot/std/collections/queue/v1/queue_tests.ts&lines=78-84) --> ```ts @@ -164,60 +169,14 @@ await secondQueue.enqueue(context, { -### Dequeue - -Dequeues a single [`Item`](/library_services/item). - -NOTE: `Any` is only supported in Python, not TypeScript. - - - - - - -```py -from reboot.protobuf import as_dict - -item = await first_queue.dequeue(context) -print(as_dict(item.value)["details"]) - -item = await second_queue.dequeue(context) -print(item.bytes) - -item = await third_queue.dequeue(context) -print(item.any) -``` - - - - - - - -```ts -const { value } = await firstQueue.dequeue(context); -console.log(value?.toJson()["details"]); - -const { bytes } = await secondQueue.dequeue(context); -console.log(bytes); -``` - - - - - -### Bulk Enqueue +#### Bulk Enqueue Enqueues a list of [`Item`](/library_services/item)s. -NOTE: `Any` is only supported in Python, not TypeScript. - +(CODE:src=../../../tests/reboot/std/collections/queue/v1/queue_tests.py&lines=464-498) --> ```py @@ -271,7 +230,7 @@ import { Value } from "@bufbuild/protobuf"; +(CODE:src=../../../tests/reboot/std/collections/queue/v1/queue_tests.ts&lines=129-145) --> ```ts @@ -298,16 +257,59 @@ await secondQueue.enqueue(context, { -### Bulk Dequeue +### Dequeue -By specifying `bulk` and `at_most`/`atMost`, you can also dequeue [`Item`](/library_services/item)s in bulk. +Dequeues a single [`Item`](/library_services/item). If there are no +[`Item`](/library_services/item)s in the `Queue`, this will wait until there is +an item to return. For immediate return on an empty queue, see +[`try_dequeue`/`tryDequeue`](#try-dequeue). + + + + + + +```py +from reboot.protobuf import as_dict + +item = await first_queue.dequeue(context) +print(as_dict(item.value)["details"]) + +item = await second_queue.dequeue(context) +print(item.bytes) + +item = await third_queue.dequeue(context) +print(item.any) +``` + + + + + + + +```ts +const { value } = await firstQueue.dequeue(context); +console.log(value?.toJson()["details"]); + +const { bytes } = await secondQueue.dequeue(context); +console.log(bytes); +``` + + + + -NOTE: `Any` is only supported in Python, not TypeScript. +#### Bulk Dequeue + +By specifying `bulk` and `at_most`/`atMost`, you can also dequeue [`Item`](/library_services/item)s in bulk. +(CODE:src=../../../tests/reboot/std/collections/queue/v1/queue_tests.py&lines=500-501) --> ```py @@ -319,7 +321,7 @@ items = await first_queue.dequeue(context, bulk=True, at_most=5) +(CODE:src=../../../tests/reboot/std/collections/queue/v1/queue_tests.ts&lines=152-156) --> ```ts @@ -333,3 +335,80 @@ const { items } = await firstQueue.dequeue(context, { + +#### Try Dequeue + +Dequeues a single [`Item`](/library_services/item). If there are no +[`Item`](/library_services/item)s in the `Queue`, this will return an empty +response immediately. Otherwise, it will return item(s) as [`dequeue`](#dequeue) +does. Like [`dequeue`](#dequeue), passing `bulk` and +`at_most`/`atMost` parameters allows dequeueing multiple +[`Item`](/library_services/item)s at once. + + + + + + +```py +response = await queue.try_dequeue(context) +if response == DequeueResponse(): + print("The queue is empty.") +else: + # Something was in the queue; handle it. +``` + + + + + + + +```ts +const response = await queue.tryDequeue(context, { bulk: true, atMost: 1 }); +if (response.items.length === 0) { + console.log("The queue is empty."); +} else { + // Something was in the queue; handle it. +} +``` + + + + + +### Empty + +Returns `True` if the queue has no items in it, otherwise returns `False`. + + + + + + +```py +response = await queue.empty(context) +if response.empty: + print("The queue is empty.") +``` + + + + + + + +```ts +const response = await queue.empty(context); +if (response.empty) { + console.log("The queue is empty."); +} +``` + + + + diff --git a/documentation/docs/library_services/sorted_map.mdx b/documentation/docs/library_services/sorted_map.mdx index 280da99b..741acf49 100644 --- a/documentation/docs/library_services/sorted_map.mdx +++ b/documentation/docs/library_services/sorted_map.mdx @@ -48,14 +48,11 @@ from reboot.std.collections.v1.sorted_map import SortedMap +(CODE:src=../../../tests/reboot/std/collections/sorted_map_tests.ts&lines=4-4) --> ```ts -import { - SortedMap, - sortedMapLibrary, -} from "@reboot-dev/reboot-std/collections/v1/sorted_map.js"; +import { SortedMap } from "@reboot-dev/reboot-std/collections/v1/sorted_map.js"; ``` @@ -74,12 +71,13 @@ async def main(): application = Application( servicers=[MyServicer], libraries=[sorted_map_library()], - ).run() + ) + await application.run() ``` ```ts -import sortedMap from "@reboot-dev/reboot-std/collections/v1/sorted_map.js"; +import { sortedMapLibrary } from "@reboot-dev/reboot-std/collections/v1/sorted_map.js"; new Application({ servicers: [MyServicer], diff --git a/documentation/sidebars.js b/documentation/sidebars.js index afbd1af9..48d6e5e7 100644 --- a/documentation/sidebars.js +++ b/documentation/sidebars.js @@ -30,6 +30,11 @@ const sidebars = { id: "ai_chat_apps/get_started_claude_code", label: "Get Started (with Claude Code)", }, + { + type: "doc", + id: "ai_chat_apps/get_started_codex", + label: "Get Started (with Codex)", + }, { type: "doc", id: "ai_chat_apps/get_started", @@ -125,6 +130,7 @@ const sidebars = { label: "Standard library", items: [ "library_services/overview", + "library_services/ciphertext", "library_services/mailgun", "library_services/ordered_map", "library_services/pubsub", @@ -152,7 +158,7 @@ const sidebars = { // { // type: "category", // label: "Deploy & operate", - // items: ["deploy_operate/where", "deploy_operate/import_export"], + // items: ["deploy_operate/import_export"], // }, // TODO: #3363. diff --git a/mypy.ini b/mypy.ini index 546bca2b..70589f23 100644 --- a/mypy.ini +++ b/mypy.ini @@ -49,6 +49,8 @@ ignore_missing_imports = True ignore_missing_imports = True [mypy-cffi.*] ignore_missing_imports = True +[mypy-dotenv.*] +ignore_missing_imports = True [mypy-cryptography.*] ignore_missing_imports = True [mypy-envoy.*] @@ -160,6 +162,8 @@ ignore_missing_imports = True ignore_missing_imports = True [mypy-reboot.*] ignore_missing_imports = True +[mypy-rbt.*] +ignore_missing_imports = True # Bazel-generated proto/grpc modules for pydantic-to-proto targets may be diff --git a/package.json b/package.json index 786cd170..8575315c 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "private": true, "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", - "@reboot-dev/reboot-std-api": "1.0.4", - "@reboot-dev/reboot-std-react": "1.0.4", - "@reboot-dev/reboot-std": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", - "@reboot-dev/reboot": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", + "@reboot-dev/reboot-std-api": "1.1.0", + "@reboot-dev/reboot-std-react": "1.1.0", + "@reboot-dev/reboot-std": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", + "@reboot-dev/reboot": "1.1.0", "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", "@bufbuild/protobuf": "1.10.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a045978..d472f3c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,31 +18,31 @@ importers: specifier: 1.10.1 version: 1.10.1 '@modelcontextprotocol/ext-apps': - specifier: ^1.0.1 - version: 1.1.2(@modelcontextprotocol/sdk@1.26.0)(react-dom@19.2.1)(react@19.2.1)(zod@3.25.76) + specifier: 1.5.0 + version: 1.5.0(@modelcontextprotocol/sdk@1.29.0)(react-dom@19.2.1)(react@19.2.1)(zod@3.25.76) '@modelcontextprotocol/sdk': - specifier: ^1.26.0 - version: 1.26.0(zod@3.25.76) + specifier: 1.29.0 + version: 1.29.0(zod@3.25.76) '@reboot-dev/reboot': - specifier: workspace:* + specifier: 1.0.4 version: link:reboot/nodejs '@reboot-dev/reboot-api': - specifier: workspace:* + specifier: 1.0.4 version: link:rbt/v1alpha1 '@reboot-dev/reboot-react': - specifier: workspace:* + specifier: 1.0.4 version: link:reboot/react '@reboot-dev/reboot-std': - specifier: workspace:* + specifier: 1.0.4 version: link:reboot/std '@reboot-dev/reboot-std-api': - specifier: workspace:* + specifier: 1.0.4 version: link:rbt/std '@reboot-dev/reboot-std-react': - specifier: workspace:* + specifier: 1.0.4 version: link:reboot/std/react '@reboot-dev/reboot-web': - specifier: workspace:* + specifier: 1.0.4 version: link:reboot/web '@standard-schema/spec': specifier: 1.0.0 @@ -148,6 +148,18 @@ importers: specifier: ^3.25.51 version: 3.25.76 + reboot/create-ui: + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.11 + esbuild: + specifier: ^0.25.0 + version: 0.25.12 + typescript: + specifier: ^5.9.2 + version: 5.9.3 + reboot/inspect: dependencies: react: @@ -172,7 +184,7 @@ importers: specifier: 1.10.1 version: 1.10.1 '@reboot-dev/reboot-api': - specifier: 0.44.0 + specifier: 1.0.4 version: link:../../rbt/v1alpha1 '@scarf/scarf': specifier: 1.4.0 @@ -233,16 +245,16 @@ importers: specifier: 1.10.1 version: 1.10.1 '@modelcontextprotocol/ext-apps': - specifier: ^1.0.1 - version: 1.1.2(@modelcontextprotocol/sdk@1.26.0)(react-dom@19.2.1)(react@19.2.1)(zod@3.25.76) + specifier: 1.5.0 + version: 1.5.0(@modelcontextprotocol/sdk@1.29.0)(react-dom@19.2.1)(react@19.2.1)(zod@3.25.76) '@modelcontextprotocol/sdk': - specifier: ^1.26.0 - version: 1.26.0(zod@3.25.76) + specifier: 1.29.0 + version: 1.29.0(zod@3.25.76) '@reboot-dev/reboot-api': - specifier: 0.44.0 + specifier: 1.0.4 version: link:../../rbt/v1alpha1 '@reboot-dev/reboot-web': - specifier: 0.44.0 + specifier: 1.0.4 version: link:../web '@scarf/scarf': specifier: 1.4.0 @@ -273,13 +285,22 @@ importers: specifier: ^22.7.4 version: 22.19.11 + reboot/rootpage: + dependencies: + react: + specifier: 19.2.1 + version: 19.2.1 + react-dom: + specifier: 19.2.1 + version: 19.2.1(react@19.2.1) + reboot/std: dependencies: '@reboot-dev/reboot': - specifier: 0.44.0 + specifier: 1.0.4 version: link:../nodejs '@reboot-dev/reboot-std-api': - specifier: 0.44.0 + specifier: 1.0.4 version: link:../../rbt/std '@scarf/scarf': specifier: 1.4.0 @@ -292,16 +313,16 @@ importers: reboot/std/react: dependencies: '@reboot-dev/reboot-api': - specifier: 0.44.0 + specifier: 1.0.4 version: link:../../../rbt/v1alpha1 '@reboot-dev/reboot-react': - specifier: 0.44.0 + specifier: 1.0.4 version: link:../../react '@reboot-dev/reboot-std-api': - specifier: 0.44.0 + specifier: 1.0.4 version: link:../../../rbt/std '@reboot-dev/reboot-web': - specifier: 0.44.0 + specifier: 1.0.4 version: link:../../web '@scarf/scarf': specifier: 1.4.0 @@ -317,7 +338,7 @@ importers: specifier: 1.10.1 version: 1.10.1 '@reboot-dev/reboot-api': - specifier: 0.44.0 + specifier: 1.0.4 version: link:../../rbt/v1alpha1 '@scarf/scarf': specifier: 1.4.0 @@ -626,6 +647,15 @@ packages: dev: false optional: true + /@esbuild/aix-ppc64@0.25.12: + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + /@esbuild/aix-ppc64@0.27.3: resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} @@ -653,6 +683,15 @@ packages: dev: false optional: true + /@esbuild/android-arm64@0.25.12: + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm64@0.27.3: resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} @@ -680,6 +719,15 @@ packages: dev: false optional: true + /@esbuild/android-arm@0.25.12: + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.27.3: resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} @@ -707,6 +755,15 @@ packages: dev: false optional: true + /@esbuild/android-x64@0.25.12: + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.27.3: resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} @@ -734,6 +791,15 @@ packages: dev: false optional: true + /@esbuild/darwin-arm64@0.25.12: + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.27.3: resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} @@ -761,6 +827,15 @@ packages: dev: false optional: true + /@esbuild/darwin-x64@0.25.12: + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.27.3: resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} @@ -788,6 +863,15 @@ packages: dev: false optional: true + /@esbuild/freebsd-arm64@0.25.12: + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.27.3: resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} @@ -815,6 +899,15 @@ packages: dev: false optional: true + /@esbuild/freebsd-x64@0.25.12: + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.27.3: resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} @@ -842,6 +935,15 @@ packages: dev: false optional: true + /@esbuild/linux-arm64@0.25.12: + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.27.3: resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} @@ -869,6 +971,15 @@ packages: dev: false optional: true + /@esbuild/linux-arm@0.25.12: + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.27.3: resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} @@ -896,6 +1007,15 @@ packages: dev: false optional: true + /@esbuild/linux-ia32@0.25.12: + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.27.3: resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} @@ -923,6 +1043,15 @@ packages: dev: false optional: true + /@esbuild/linux-loong64@0.25.12: + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.27.3: resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} @@ -950,6 +1079,15 @@ packages: dev: false optional: true + /@esbuild/linux-mips64el@0.25.12: + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.27.3: resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} @@ -977,6 +1115,15 @@ packages: dev: false optional: true + /@esbuild/linux-ppc64@0.25.12: + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.27.3: resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} @@ -1004,6 +1151,15 @@ packages: dev: false optional: true + /@esbuild/linux-riscv64@0.25.12: + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.27.3: resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} @@ -1031,6 +1187,15 @@ packages: dev: false optional: true + /@esbuild/linux-s390x@0.25.12: + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.27.3: resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} @@ -1058,6 +1223,15 @@ packages: dev: false optional: true + /@esbuild/linux-x64@0.25.12: + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.27.3: resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} @@ -1076,6 +1250,15 @@ packages: dev: false optional: true + /@esbuild/netbsd-arm64@0.25.12: + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-arm64@0.27.3: resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} engines: {node: '>=18'} @@ -1103,6 +1286,15 @@ packages: dev: false optional: true + /@esbuild/netbsd-x64@0.25.12: + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.27.3: resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} @@ -1121,6 +1313,15 @@ packages: dev: false optional: true + /@esbuild/openbsd-arm64@0.25.12: + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-arm64@0.27.3: resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} engines: {node: '>=18'} @@ -1148,6 +1349,15 @@ packages: dev: false optional: true + /@esbuild/openbsd-x64@0.25.12: + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.27.3: resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} @@ -1157,6 +1367,15 @@ packages: dev: false optional: true + /@esbuild/openharmony-arm64@0.25.12: + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + requiresBuild: true + dev: true + optional: true + /@esbuild/openharmony-arm64@0.27.3: resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} engines: {node: '>=18'} @@ -1184,6 +1403,15 @@ packages: dev: false optional: true + /@esbuild/sunos-x64@0.25.12: + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/sunos-x64@0.27.3: resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} @@ -1211,6 +1439,15 @@ packages: dev: false optional: true + /@esbuild/win32-arm64@0.25.12: + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.27.3: resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} @@ -1238,6 +1475,15 @@ packages: dev: false optional: true + /@esbuild/win32-ia32@0.25.12: + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.27.3: resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} @@ -1265,6 +1511,15 @@ packages: dev: false optional: true + /@esbuild/win32-x64@0.25.12: + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.27.3: resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} @@ -1349,11 +1604,11 @@ packages: '@jridgewell/sourcemap-codec': 1.5.4 dev: false - /@modelcontextprotocol/ext-apps@1.1.2(@modelcontextprotocol/sdk@1.26.0)(react-dom@19.2.1)(react@19.2.1)(zod@3.25.76): - resolution: {integrity: sha512-Gx4TEo3/F8yq1Ix6LdgLwMrKqfZqD7++eakZdbMUewrYtHeeJn3nKpeNhgEfO7nYRwonqWYomOAszWZWJS0IbA==} - requiresBuild: true + /@modelcontextprotocol/ext-apps@1.5.0(@modelcontextprotocol/sdk@1.29.0)(react-dom@19.2.1)(react@19.2.1)(zod@3.25.76): + resolution: {integrity: sha512-q4fut89TOoP2LEPHSGfZErIf1K1xOTTzV+41h/bB2BqKw2gKb0uLKbHusOy1UtbY0puS16zBho/vFp3f5XMVbQ==} + engines: {node: '>=20'} peerDependencies: - '@modelcontextprotocol/sdk': ^1.24.0 + '@modelcontextprotocol/sdk': ^1.29.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 zod: ^3.25.0 || ^4.0.0 @@ -1363,32 +1618,14 @@ packages: react-dom: optional: true dependencies: - '@modelcontextprotocol/sdk': 1.26.0(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) react: 19.2.1 react-dom: 19.2.1(react@19.2.1) zod: 3.25.76 - optionalDependencies: - '@oven/bun-darwin-aarch64': 1.3.9 - '@oven/bun-darwin-x64': 1.3.9 - '@oven/bun-darwin-x64-baseline': 1.3.9 - '@oven/bun-linux-aarch64': 1.3.9 - '@oven/bun-linux-aarch64-musl': 1.3.9 - '@oven/bun-linux-x64': 1.3.9 - '@oven/bun-linux-x64-baseline': 1.3.9 - '@oven/bun-linux-x64-musl': 1.3.9 - '@oven/bun-linux-x64-musl-baseline': 1.3.9 - '@oven/bun-windows-x64': 1.3.9 - '@oven/bun-windows-x64-baseline': 1.3.9 - '@rollup/rollup-darwin-arm64': 4.57.1 - '@rollup/rollup-darwin-x64': 4.57.1 - '@rollup/rollup-linux-arm64-gnu': 4.57.1 - '@rollup/rollup-linux-x64-gnu': 4.57.1 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.57.1 - dev: false - - /@modelcontextprotocol/sdk@1.26.0(zod@3.25.76): - resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} + dev: false + + /@modelcontextprotocol/sdk@1.29.0(zod@3.25.76): + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -1438,94 +1675,6 @@ packages: semver: 7.7.4 dev: false - /@oven/bun-darwin-aarch64@1.3.9: - resolution: {integrity: sha512-df7smckMWSUfaT5mzwN9Lfpd3ZGkOqo+vmQ8VV2a32gl14v6uZ/qeeo+1RlANXn8M0uzXPWWCkrKZIWSZUR0qw==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@oven/bun-darwin-x64-baseline@1.3.9: - resolution: {integrity: sha512-XbhsA2XAFzvFr0vPSV6SNqGxab4xHKdPmVTLqoSHAx9tffrSq/012BDptOskulwnD+YNsrJUx2D2Ve1xvfgGcg==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@oven/bun-darwin-x64@1.3.9: - resolution: {integrity: sha512-YiLxfsPzQqaVvT2a+nxH9do0YfUjrlxF3tKP0b1DDgvfgCcVKGsrQH3Wa82qHgL4dnT8h2bqi94JxXESEuPmcA==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@oven/bun-linux-aarch64-musl@1.3.9: - resolution: {integrity: sha512-t8uimCVBTw5f9K2QTZE5wN6UOrFETNrh/Xr7qtXT9nAOzaOnIFvYA+HcHbGfi31fRlCVfTxqm/EiCwJ1gEw9YQ==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@oven/bun-linux-aarch64@1.3.9: - resolution: {integrity: sha512-VaNQTu0Up4gnwZLQ6/Hmho6jAlLxTQ1PwxEth8EsXHf82FOXXPV5OCQ6KC9mmmocjKlmWFaIGebThrOy8DUo4g==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@oven/bun-linux-x64-baseline@1.3.9: - resolution: {integrity: sha512-nZ12g22cy7pEOBwAxz2tp0wVqekaCn9QRKuGTHqOdLlyAqR4SCdErDvDhUWd51bIyHTQoCmj72TegGTgG0WNPw==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@oven/bun-linux-x64-musl-baseline@1.3.9: - resolution: {integrity: sha512-3FXQgtYFsT0YOmAdMcJn56pLM5kzSl6y942rJJIl5l2KummB9Ea3J/vMJMzQk7NCAGhleZGWU/pJSS/uXKGa7w==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@oven/bun-linux-x64-musl@1.3.9: - resolution: {integrity: sha512-4ZjIUgCxEyKwcKXideB5sX0KJpnHTZtu778w73VNq2uNH2fNpMZv98+DBgJyQ9OfFoRhmKn1bmLmSefvnHzI9w==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@oven/bun-linux-x64@1.3.9: - resolution: {integrity: sha512-oQyAW3+ugulvXTZ+XYeUMmNPR94sJeMokfHQoKwPvVwhVkgRuMhcLGV2ZesHCADVu30Oz2MFXbgdC8x4/o9dRg==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@oven/bun-windows-x64-baseline@1.3.9: - resolution: {integrity: sha512-a/+hSrrDpMD7THyXvE2KJy1skxzAD0cnW4K1WjuI/91VqsphjNzvf5t/ZgxEVL4wb6f+hKrSJ5J3aH47zPr61g==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@oven/bun-windows-x64@1.3.9: - resolution: {integrity: sha512-/d6vAmgKvkoYlsGPsRPlPmOK1slPis/F40UG02pYwypTH0wmY0smgzdFqR4YmryxFh17XrW1kITv+U99Oajk9Q==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1561,14 +1710,6 @@ packages: dev: false optional: true - /@rollup/rollup-darwin-arm64@4.57.1: - resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - /@rollup/rollup-darwin-x64@4.44.2: resolution: {integrity: sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==} cpu: [x64] @@ -1577,14 +1718,6 @@ packages: dev: false optional: true - /@rollup/rollup-darwin-x64@4.57.1: - resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - /@rollup/rollup-freebsd-arm64@4.44.2: resolution: {integrity: sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==} cpu: [arm64] @@ -1625,14 +1758,6 @@ packages: dev: false optional: true - /@rollup/rollup-linux-arm64-gnu@4.57.1: - resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@rollup/rollup-linux-arm64-musl@4.44.2: resolution: {integrity: sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==} cpu: [arm64] @@ -1689,14 +1814,6 @@ packages: dev: false optional: true - /@rollup/rollup-linux-x64-gnu@4.57.1: - resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@rollup/rollup-linux-x64-musl@4.44.2: resolution: {integrity: sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==} cpu: [x64] @@ -1713,14 +1830,6 @@ packages: dev: false optional: true - /@rollup/rollup-win32-arm64-msvc@4.59.0: - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@rollup/rollup-win32-ia32-msvc@4.44.2: resolution: {integrity: sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==} cpu: [ia32] @@ -1737,14 +1846,6 @@ packages: dev: false optional: true - /@rollup/rollup-win32-x64-msvc@4.57.1: - resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@scarf/scarf@1.4.0: resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} requiresBuild: true @@ -2485,6 +2586,40 @@ packages: '@esbuild/win32-x64': 0.24.2 dev: false + /esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + dev: true + /esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} @@ -3719,7 +3854,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.4 - debug: 4.3.7 + debug: 4.4.3 socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -3937,6 +4072,12 @@ packages: engines: {node: '>=14.17'} hasBin: true + /typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + /ua-parser-js@1.0.41: resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} hasBin: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9c1369d3..7cdaa8c8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,6 +4,7 @@ packages: - "reboot/inspect/" - "reboot/nodejs/" - "reboot/react/" + - "reboot/rootpage/" - "reboot/std/" - "reboot/std/react/" - "reboot/web/" diff --git a/rbt/std/BUILD.bazel b/rbt/std/BUILD.bazel index 2b1222b4..f2ba5d5e 100644 --- a/rbt/std/BUILD.bazel +++ b/rbt/std/BUILD.bazel @@ -47,14 +47,19 @@ ts_project( }, visibility = ["//visibility:public"], deps = [ + "//rbt/std/ciphertext/v1:ciphertext_js_proto", + "//rbt/std/ciphertext/v1:ciphertext_js_reboot", "//rbt/std/collections/ordered_map/v1:ordered_map_js_proto", "//rbt/std/collections/ordered_map/v1:ordered_map_js_reboot", + "//rbt/std/collections/ordered_map/v1:ordered_map_js_reboot_react", "//rbt/std/collections/queue/v1:queue_js_proto", "//rbt/std/collections/queue/v1:queue_js_reboot", "//rbt/std/collections/v1:sorted_map_js_proto", "//rbt/std/collections/v1:sorted_map_js_reboot", "//rbt/std/item/v1:item_js_proto", "//rbt/std/item/v1:item_js_reboot", + "//rbt/std/oauth/v1:oauth_js_proto", + "//rbt/std/oauth/v1:oauth_js_reboot", "//rbt/std/presence/mouse_tracker/v1:mouse_position_js_proto", "//rbt/std/presence/mouse_tracker/v1:mouse_position_js_reboot", "//rbt/std/presence/mouse_tracker/v1:mouse_position_js_reboot_react", diff --git a/rbt/std/ciphertext/v1/BUILD.bazel b/rbt/std/ciphertext/v1/BUILD.bazel new file mode 100644 index 00000000..6fc1c63a --- /dev/null +++ b/rbt/std/ciphertext/v1/BUILD.bazel @@ -0,0 +1,53 @@ +load( + "@com_github_reboot_dev_reboot//reboot:rules.bzl", + "js_proto_library", + "js_reboot_library", + "py_reboot_library", +) +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") + +proto_library( + name = "ciphertext_proto", + srcs = [ + ":ciphertext.proto", + ], + visibility = ["//visibility:public"], + deps = [ + "@com_github_reboot_dev_reboot//rbt/v1alpha1:options_proto", + "@com_google_protobuf//:descriptor_proto", + ], +) + +py_reboot_library( + name = "ciphertext_py_reboot", + proto = "ciphertext.proto", + proto_library = ":ciphertext_proto", + visibility = ["//visibility:public"], +) + +js_proto_library( + name = "ciphertext_js_proto", + package_json = ":package.json", + proto = "ciphertext.proto", + proto_deps = [ + ":ciphertext_proto", + # ISSUE(https://github.com/reboot-dev/mono/issues/3218): Until we can + # use `create_protoc_plugin_rule` we need to repeat the dependencies of + # the `proto_libraries` here. + "@com_github_reboot_dev_reboot//rbt/v1alpha1:options_proto", + "@com_google_protobuf//:descriptor_proto", + ], + visibility = ["//visibility:public"], +) + +js_reboot_library( + name = "ciphertext_js_reboot", + srcs = [ + ":ciphertext_proto", + ], + proto = "ciphertext.proto", + visibility = ["//visibility:public"], + deps = [ + ":ciphertext_js_proto", + ], +) diff --git a/rbt/std/ciphertext/v1/ciphertext.proto b/rbt/std/ciphertext/v1/ciphertext.proto new file mode 100644 index 00000000..ef6ba6e6 --- /dev/null +++ b/rbt/std/ciphertext/v1/ciphertext.proto @@ -0,0 +1,400 @@ +syntax = "proto3"; + +package rbt.std.ciphertext.v1; + +import "rbt/v1alpha1/options.proto"; + +//////////////////////////////////////////////////////////////////////// +// Errors. + +// Raised when decryption fails because the supplied `associated_data` +// does not match what was provided at encryption time, or the stored +// envelope is corrupt. +message DecryptionFailed {} + +// Raised when the scope protecting this ciphertext has been crypto-shredded +// (see `KeyManager.Shred`). The key that could unwrap it has been +// destroyed, so the plaintext is permanently unrecoverable. +message ScopeShredded {} + +// Raised when `REBOOT_CRYPTO_ROOT_KEYS` does not contain the root key +// version that a `WrappingKey`'s material was wrapped under (e.g. an +// in-use version was dropped from the secret before rotation completed). +message UnknownRootKeyVersion {} + +//////////////////////////////////////////////////////////////////////// +// Ciphertext: a single encrypted value. The "envelope" *is* the state. + +message Ciphertext { + option (rbt.v1alpha1.state) = { + }; + + // The `WrappingKey` that wraps this ciphertext's data encryption key + // (DEK). Revoking that `WrappingKey` crypto-shreds this ciphertext. + // Derived from the caller's `key_manager_id` and `scope`. + string wrapping_key_id = 1; + + // The per-value DEK, encrypted ("wrapped") under the `WrappingKey`. + bytes wrapped_dek = 2; + + // The plaintext, encrypted under the DEK. + bytes data = 3; + + // The `KeyManager` this ciphertext's wrapping key belongs to, remembered + // so `Rescope` can re-wrap within the same manager. + string key_manager_id = 4; +} + +//////////////////////////////////////////////////////////////////////// + +message EncryptRequest { + // The plaintext to encrypt. + bytes plaintext = 1; + + // Additional authenticated data bound to the ciphertext. The identical + // value must be supplied to `Decrypt`. It is authenticated but not + // stored, so it cannot be recovered from the ciphertext and an + // envelope cannot be decrypted in a context it was not sealed for. + bytes associated_data = 2; + + // The crypto-shred scope this value belongs to (e.g. a user id, a + // tenant id). The library derives a `WrappingKey` from it (created + // lazily on first use); shredding the scope destroys every value in it. + // Any string is allowed — it is encoded into a valid state id. + string scope = 3; + + // The `KeyManager` to scope the wrapping key under. Required — pass + // `APP_SHARED_KEY_MANAGER_ID` unless you want a dedicated manager. Lets + // independent consumers partition their keys into separate managers — + // each with its own rotation loop, registry, and shred domain — so two + // consumers can use the same `scope` without sharing (or shredding) each + // other's wrapping key. + string key_manager_id = 4; +} + +message EncryptResponse {} + +//////////////////////////////////////////////////////////////////////// + +message DecryptRequest { + // Must match the `associated_data` supplied to `Encrypt`. + bytes associated_data = 1; +} + +message DecryptResponse { + bytes plaintext = 1; +} + +//////////////////////////////////////////////////////////////////////// + +message RescopeRequest { + // The scope to move this ciphertext into. The bulk `data` is untouched; + // only the (tiny) wrapped DEK is re-encrypted under the new scope's + // wrapping key. + string scope = 1; +} + +message RescopeResponse {} + +//////////////////////////////////////////////////////////////////////// + +service CiphertextMethods { + // Encrypts `plaintext` into this `Ciphertext`. A fresh random DEK + // encrypts the plaintext; the DEK is then wrapped by the `WrappingKey` + // derived from `scope`. + rpc Encrypt(EncryptRequest) returns (EncryptResponse) { + option (rbt.v1alpha1.method) = { + transaction: { constructor: {} }, + }; + } + + // Returns the decrypted plaintext. Requires the same `associated_data` + // used at `Encrypt` time. + rpc Decrypt(DecryptRequest) returns (DecryptResponse) { + option (rbt.v1alpha1.method) = { + reader: {}, + errors: [ "DecryptionFailed", "ScopeShredded", "UnknownRootKeyVersion" ], + }; + } + + // Moves this ciphertext into a different `scope`, re-wrapping its DEK + // under the new scope's wrapping key. Touches only the wrapped DEK. + rpc Rescope(RescopeRequest) returns (RescopeResponse) { + option (rbt.v1alpha1.method) = { + transaction: {}, + errors: [ "ScopeShredded", "UnknownRootKeyVersion" ], + }; + } +} + +//////////////////////////////////////////////////////////////////////// +// WrappingKey: a revocable key that wraps the DEKs of many ciphertexts. +// Its material is stored encrypted ("wrapped") under the root KEK +// derived from `REBOOT_CRYPTO_ROOT_KEYS`. + +message WrappingKey { + option (rbt.v1alpha1.state) = { + }; + + enum Status { + // The key is usable. + ACTIVE = 0; + // The key has been revoked; its material is destroyed and any + // ciphertext it wraps is permanently unrecoverable. + REVOKED = 1; + } + + // The wrapping key material (a random 256-bit key), itself encrypted + // under the root KEK derived from `REBOOT_CRYPTO_ROOT_KEYS`. Unset once + // revoked (and before first use). + optional bytes wrapped_key_material = 1; + + // The `REBOOT_CRYPTO_ROOT_KEYS` version whose root KEK + // `wrapped_key_material` is currently wrapped under. Unset before first + // use. + optional uint32 root_key_version = 2; + + Status status = 3; + + // Id of an `OrderedMap` indexing the `Ciphertext` ids wrapped by this + // key, so they can be enumerated for re-keying. Unset before first use. + optional string ciphertext_index_id = 4; + + // The `KeyManager` this `WrappingKey` is stored in, and the (UUIDv7) key + // under which it is registered in that manager's registry, so the two + // can be cross-referenced. Both unset before first use. + optional string key_manager_id = 6; + optional string key_manager_key = 5; +} + +//////////////////////////////////////////////////////////////////////// + +message WrapRequest { + // The data encryption key to wrap. + bytes dek = 1; + // The `Ciphertext` id to record in this key's index (for re-keying + // enumeration). + string ciphertext_id = 2; + // The `KeyManager` to register this wrapping key with on first use. + string key_manager_id = 3; +} + +message WrapResponse { + bytes wrapped_dek = 1; +} + +//////////////////////////////////////////////////////////////////////// + +message UnwrapRequest { + bytes wrapped_dek = 1; +} + +message UnwrapResponse { + bytes dek = 1; +} + +//////////////////////////////////////////////////////////////////////// + +message RevokeRequest {} + +message RevokeResponse {} + +//////////////////////////////////////////////////////////////////////// + +message RewrapUnderRootKeyRequest {} + +message RewrapUnderRootKeyResponse {} + +//////////////////////////////////////////////////////////////////////// + +message CiphertextsRequest { + // If set, enumeration resumes strictly after this `Ciphertext` id. + optional string start_after = 1; + // Maximum number of ids to return. Must be non-zero. + uint32 limit = 2; +} + +message CiphertextsResponse { + repeated string ciphertext_ids = 1; +} + +//////////////////////////////////////////////////////////////////////// + +service WrappingKeyMethods { + // Wraps `dek` under this key, lazily generating the key's material on + // first use (registering it with the `KeyManager`) and migrating it + // forward to the active root key version. + rpc Wrap(WrapRequest) returns (WrapResponse) { + option (rbt.v1alpha1.method) = { + transaction: {}, + errors: [ "ScopeShredded", "UnknownRootKeyVersion" ], + }; + } + + // Unwraps a previously wrapped DEK. + rpc Unwrap(UnwrapRequest) returns (UnwrapResponse) { + option (rbt.v1alpha1.method) = { + reader: {}, + errors: [ "ScopeShredded", "UnknownRootKeyVersion" ], + }; + } + + // Revokes this key: destroys its material (crypto-shred). Every + // ciphertext wrapped by it becomes permanently unrecoverable. + // Idempotent. + rpc Revoke(RevokeRequest) returns (RevokeResponse) { + option (rbt.v1alpha1.method) = { + writer: {}, + }; + } + + // Re-wraps this key's material under the active root key version. Used + // by `KeyManager.Rotate` so an old `REBOOT_CRYPTO_ROOT_KEYS` version can + // be retired. + rpc RewrapUnderRootKey(RewrapUnderRootKeyRequest) + returns (RewrapUnderRootKeyResponse) { + option (rbt.v1alpha1.method) = { + writer: {}, + errors: [ "UnknownRootKeyVersion" ], + }; + } + + // Enumerates the `Ciphertext` ids wrapped by this key. + rpc Ciphertexts(CiphertextsRequest) returns (CiphertextsResponse) { + option (rbt.v1alpha1.method) = { + reader: {}, + }; + } +} + +//////////////////////////////////////////////////////////////////////// +// KeyManager: registers every `WrappingKey` created under its id and +// continuously rotates them onto the active root key version. An +// application has one manager by default, but consumers may partition +// their keys across several (selected by `EncryptRequest.key_manager_id`), +// each with its own registry, rotation loop, and revocation domain. + +message KeyManager { + option (rbt.v1alpha1.state) = { + }; + + // The `REBOOT_CRYPTO_ROOT_KEYS` versions this manager currently holds + // wrapping keys under (is "consuming"). A version is added when the + // first key under it is registered and removed once rotation has + // migrated every key off it; each change is mirrored to the + // `Application`'s `use_root_key_version` / `disuse_root_key_version` + // markers. More than one version means a rotation is in progress. + repeated uint32 consuming_versions = 1; + + // Cursor (a registry UUIDv7) for an in-progress rotation sweep; unset + // when no sweep is running. + optional string rotation_cursor = 2; + + // Whether the `Watch` control loop has been started. + bool watch_started = 3; +} + +//////////////////////////////////////////////////////////////////////// + +message RegisterRequest { + string wrapping_key_id = 1; + // The root key version the wrapping key's material is wrapped under, so + // the registration can mark it in use atomically (closing the window + // where a key is in use before its `use_root_key_version` marker is + // recorded). + uint32 root_key_version = 2; +} + +message RegisterResponse { + // The UUIDv7 under which the wrapping key was registered. + string uuid = 1; +} + +//////////////////////////////////////////////////////////////////////// + +message WatchRequest {} + +message WatchResponse {} + +//////////////////////////////////////////////////////////////////////// + +message RotateRequest {} + +message RotateResponse {} + +//////////////////////////////////////////////////////////////////////// + +message StatusRequest {} + +message StatusResponse { + // The highest root key version every wrapping key has been rotated to. + // Unset if no rotation has completed yet. + optional uint32 active_version = 1; + // Whether a rotation sweep is currently in progress (i.e. not every + // wrapping key has reached `active_version` yet). + bool rotating = 2; +} + +//////////////////////////////////////////////////////////////////////// + +message ShredRequest { + // The scope to crypto-shred. Shredding destroys the scope's wrapping + // key, so every `Ciphertext` encrypted under it becomes permanently + // unrecoverable. + string scope = 1; +} + +message ShredResponse {} + +//////////////////////////////////////////////////////////////////////// + +service KeyManagerMethods { + // Records a `WrappingKey` in the registry and returns the UUIDv7 it was + // registered under. Called automatically when a `WrappingKey` is first + // generated; also lazily starts the `Watch` control loop. + rpc Register(RegisterRequest) returns (RegisterResponse) { + option (rbt.v1alpha1.method) = { + transaction: {}, + }; + } + + // Crypto-shreds `scope`: destroys its wrapping key so every + // `Ciphertext` encrypted under it becomes permanently unrecoverable. + // Idempotent. This is the supported way to shred a scope; the + // underlying `WrappingKey` is an implementation detail. + rpc Shred(ShredRequest) returns (ShredResponse) { + option (rbt.v1alpha1.method) = { + transaction: {}, + }; + } + + // A long-lived control loop that watches `REBOOT_CRYPTO_ROOT_KEYS` for a + // newer active version and, when one appears (e.g. after an operator + // updates the secret and restarts the application), drives `Rotate` and + // records the version reached in `active_version`. + rpc Watch(WatchRequest) returns (WatchResponse) { + option (rbt.v1alpha1.method) = { + workflow: {}, + }; + } + + // Re-wraps every registered `WrappingKey` onto the active root key + // version. A durable control loop that pages through the registry, so + // it scales to very large keyrings and resumes across restarts. + rpc Rotate(RotateRequest) returns (RotateResponse) { + option (rbt.v1alpha1.method) = { + workflow: {}, + errors: [ "UnknownRootKeyVersion" ], + }; + } + + // Returns the highest root key version every wrapping key has been + // rotated to, and whether a rotation sweep is still in progress. + rpc Status(StatusRequest) returns (StatusResponse) { + option (rbt.v1alpha1.method) = { + reader: {}, + }; + } +} + +//////////////////////////////////////////////////////////////////////// diff --git a/rbt/std/ciphertext/v1/package.json b/rbt/std/ciphertext/v1/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/rbt/std/ciphertext/v1/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/rbt/std/collections/ordered_map/v1/BUILD.bazel b/rbt/std/collections/ordered_map/v1/BUILD.bazel index 04cd1384..ef46a8ea 100644 --- a/rbt/std/collections/ordered_map/v1/BUILD.bazel +++ b/rbt/std/collections/ordered_map/v1/BUILD.bazel @@ -2,6 +2,7 @@ load( "@com_github_reboot_dev_reboot//reboot:rules.bzl", "js_proto_library", "js_reboot_library", + "js_reboot_react_library", "py_reboot_library", ) load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") @@ -13,6 +14,7 @@ proto_library( ], visibility = ["//visibility:public"], deps = [ + "//rbt/std/item/v1:item_proto", "@com_github_reboot_dev_reboot//rbt/v1alpha1:errors_proto", "@com_github_reboot_dev_reboot//rbt/v1alpha1:options_proto", "@com_google_protobuf//:any_proto", @@ -37,6 +39,7 @@ js_proto_library( # ISSUE(https://github.com/reboot-dev/mono/issues/3218): Until we can # use `create_protoc_plugin_rule` we need to repeat the dependencies of # the `proto_libraries` here. + "//rbt/std/item/v1:item_proto", "@com_github_reboot_dev_reboot//rbt/v1alpha1:options_proto", "@com_google_protobuf//:any_proto", "@com_google_protobuf//:struct_proto", @@ -56,3 +59,20 @@ js_reboot_library( ":ordered_map_js_proto", ], ) + +js_reboot_react_library( + name = "ordered_map_js_reboot_react", + srcs = [ + ":ordered_map_js_proto", + ], + proto = "ordered_map.proto", + proto_deps = [ + ":ordered_map_proto", + # ISSUE(https://github.com/reboot-dev/mono/issues/3218): Until we can + # use `create_protoc_plugin_rule` we need to repeat the dependencies of + # the `proto_libraries` here. + "@com_github_reboot_dev_reboot//rbt/v1alpha1:options_proto", + "@com_google_protobuf//:descriptor_proto", + ], + visibility = ["//visibility:public"], +) diff --git a/rbt/std/collections/ordered_map/v1/ordered_map.proto b/rbt/std/collections/ordered_map/v1/ordered_map.proto index f23fc073..9addc045 100644 --- a/rbt/std/collections/ordered_map/v1/ordered_map.proto +++ b/rbt/std/collections/ordered_map/v1/ordered_map.proto @@ -4,6 +4,7 @@ package rbt.std.collections.ordered_map.v1; import "google/protobuf/any.proto"; import "google/protobuf/struct.proto"; +import "rbt/std/item/v1/item.proto"; import "rbt/v1alpha1/options.proto"; //////////////////////////////////////////////////////////////////////// @@ -101,26 +102,39 @@ message NodeSearchResponse { //////////////////////////////////////////////////////////////////////// message NodeInsertRequest { - string key = 1; - // NOTE: this is a message of type `Value`, but for performance - // reasons we don't bother deserializing it except when a user is - // calling into the `OrderedMap` type directly. - bytes value = 2; + reserved 1, 2; + reserved "key", "value"; + // Map of key to serialized `Value` bytes. + map entries = 3; } message NodeInsertResponse { - bool split = 1; - string promoted_key = 2; - string new_child_id = 3; + reserved 1, 2, 3, 4; + reserved "split", "promoted_key", "new_child_id", "inserted"; + // Promoted keys from splits. + repeated string promoted_keys = 5; + // New child IDs from splits. + repeated string new_child_ids = 6; + // Count of newly inserted keys (excludes updates + // of existing keys). + uint32 inserted_count = 7; } //////////////////////////////////////////////////////////////////////// message NodeRemoveRequest { - string key = 1; + reserved 1; + reserved "key"; + // Keys to remove. + repeated string keys = 2; } -message NodeRemoveResponse {} +message NodeRemoveResponse { + reserved 1; + reserved "removed"; + // Count of keys that existed and were removed. + uint32 removed_count = 2; +} //////////////////////////////////////////////////////////////////////// @@ -237,12 +251,24 @@ message OrderedMap { uint32 degree = 1; string root_id = 2; + // Total number of entries. Only maintained when `maintain_size` is + // true. + uint32 size = 3; + bool maintain_size = 4; } //////////////////////////////////////////////////////////////////////// message OrderedMapCreateRequest { - uint32 degree = 1; + // If not set, the default degree will be used (i.e., 128 at the + // time of writing this comment). + optional uint32 degree = 1; + // When true, the map tracks the total number of + // entries. This requires a write to the root on + // every insert/remove, which serializes all + // mutations. When false (default), `total_size` in + // range responses will not be set. + bool maintain_size = 2; } message OrderedMapCreateResponse {} @@ -286,6 +312,14 @@ message OrderedMapInsertRequest { optional google.protobuf.Value value = 2; optional bytes bytes = 3; optional google.protobuf.Any any = 4; + // Bulk insert. If non-empty, single-key fields + // (key, value, bytes, any) must all be unset. + map entries = 5; + // Construction options (used on implicit construction, i.e., first + // `Insert` without a prior `Create`). After construction, if set, + // these are validated to match the existing configuration. + optional uint32 degree = 6; + optional bool maintain_size = 7; } message OrderedMapInsertResponse {} @@ -294,6 +328,8 @@ message OrderedMapInsertResponse {} message OrderedMapRemoveRequest { string key = 1; + // Bulk remove: keys to remove. + repeated string keys = 2; } message OrderedMapRemoveResponse {} @@ -311,6 +347,9 @@ message OrderedMapRangeRequest { message OrderedMapRangeResponse { repeated Entry entries = 1; + // Total entries in map, regardless of size of `entries`. Only set + // when the map was constructed with `maintain_size = true`. + optional uint32 total_size = 2; } //////////////////////////////////////////////////////////////////////// diff --git a/rbt/std/oauth/v1/BUILD.bazel b/rbt/std/oauth/v1/BUILD.bazel new file mode 100644 index 00000000..2cb87007 --- /dev/null +++ b/rbt/std/oauth/v1/BUILD.bazel @@ -0,0 +1,53 @@ +load( + "@com_github_reboot_dev_reboot//reboot:rules.bzl", + "js_proto_library", + "js_reboot_library", + "py_reboot_library", +) +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") + +proto_library( + name = "oauth_proto", + srcs = [ + ":oauth.proto", + ], + visibility = ["//visibility:public"], + deps = [ + "@com_github_reboot_dev_reboot//rbt/v1alpha1:options_proto", + "@com_google_protobuf//:descriptor_proto", + ], +) + +py_reboot_library( + name = "oauth_py_reboot", + proto = "oauth.proto", + proto_library = ":oauth_proto", + visibility = ["//visibility:public"], +) + +js_proto_library( + name = "oauth_js_proto", + package_json = ":package.json", + proto = "oauth.proto", + proto_deps = [ + ":oauth_proto", + # ISSUE(https://github.com/reboot-dev/mono/issues/3218): Until we can + # use `create_protoc_plugin_rule` we need to repeat the dependencies of + # the `proto_libraries` here. + "@com_github_reboot_dev_reboot//rbt/v1alpha1:options_proto", + "@com_google_protobuf//:descriptor_proto", + ], + visibility = ["//visibility:public"], +) + +js_reboot_library( + name = "oauth_js_reboot", + srcs = [ + ":oauth_proto", + ], + proto = "oauth.proto", + visibility = ["//visibility:public"], + deps = [ + ":oauth_js_proto", + ], +) diff --git a/rbt/std/oauth/v1/oauth.proto b/rbt/std/oauth/v1/oauth.proto new file mode 100644 index 00000000..54e7f52b --- /dev/null +++ b/rbt/std/oauth/v1/oauth.proto @@ -0,0 +1,98 @@ +syntax = "proto3"; + +package rbt.std.oauth.v1; + +import "rbt/v1alpha1/options.proto"; + +//////////////////////////////////////////////////////////////////////// +// OAuthTokens: an external identity provider's OAuth tokens, captured +// during the code exchange so the application can later call the +// provider's API on the user's behalf. Stored encrypted (serialized into +// a `Ciphertext`), never at rest in the clear. + +message OAuthTokens { + // The provider's access token (a bearer token for the provider's API). + string access_token = 1; + + // The provider's refresh token, if one was issued. Unset (`HasField` + // false) when the provider returned none. + optional string refresh_token = 2; + + // Absolute expiry of `access_token` as epoch seconds. Unset if the + // provider didn't tell us. + optional int64 expires_at = 3; + + // The scopes the provider actually granted. + repeated string scopes = 4; +} + +//////////////////////////////////////////////////////////////////////// +// OAuthTokenManager: stores the OAuth tokens for one external third-party +// service (e.g. Google, GitHub), keyed by `user_id`. Addressed by a state +// id naming that service (use the `GOOGLE` / `GITHUB` constants, or any +// string for a service without a predefined one). Each manager encrypts +// under its own dedicated `KeyManager`, so every key for a service can be +// shredded together; each user's tokens are scoped to their `user_id`, so +// a single user's tokens can be crypto-shredded. +// +// The state itself is intentionally empty: the `user_id -> Ciphertext id` +// pointers live in a per-manager `OrderedMap` derived from the manager's +// state id (just as `KeyManager` keeps its registry in a derived map). + +message OAuthTokenManager { + option (rbt.v1alpha1.state) = { + }; +} + +//////////////////////////////////////////////////////////////////////// + +message StoreRequest { + // The user whose tokens these are. + string user_id = 1; + + // The provider tokens to encrypt and store. If `refresh_token` is unset + // but one was previously stored for this user, the existing refresh + // token is carried forward (some providers issue a refresh token only on + // the first consent). + OAuthTokens tokens = 2; +} + +message StoreResponse {} + +//////////////////////////////////////////////////////////////////////// + +message FetchRequest { + // The user whose tokens to read. + string user_id = 1; +} + +message FetchResponse { + // Whether tokens are stored (and still recoverable) for the user. + bool found = 1; + + // The stored tokens; meaningful only when `found` is true. + OAuthTokens tokens = 2; +} + +//////////////////////////////////////////////////////////////////////// + +service OAuthTokenManagerMethods { + // Encrypts and persists `tokens` for `user_id`, replacing any previously + // stored tokens. Implicitly constructs the manager on first use. + rpc Store(StoreRequest) returns (StoreResponse) { + option (rbt.v1alpha1.method) = { + transaction: {}, + }; + } + + // Returns the stored tokens for `user_id`, with `found` indicating + // whether any are recoverable (false once a user's tokens have been + // crypto-shredded). + rpc Fetch(FetchRequest) returns (FetchResponse) { + option (rbt.v1alpha1.method) = { + reader: {}, + }; + } +} + +//////////////////////////////////////////////////////////////////////// diff --git a/rbt/std/oauth/v1/package.json b/rbt/std/oauth/v1/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/rbt/std/oauth/v1/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/rbt/std/package.json b/rbt/std/package.json index d3a57b04..0f7dd3f0 100644 --- a/rbt/std/package.json +++ b/rbt/std/package.json @@ -1,6 +1,6 @@ { "name": "@reboot-dev/reboot-std-api", - "version": "1.0.4", + "version": "1.1.0", "description": "Reboot standard library API.", "main": "index.js", "type": "module", diff --git a/rbt/v1alpha1/BUILD.bazel b/rbt/v1alpha1/BUILD.bazel index a3fa03ed..737a8a46 100644 --- a/rbt/v1alpha1/BUILD.bazel +++ b/rbt/v1alpha1/BUILD.bazel @@ -235,6 +235,9 @@ proto_library( name = "transactions_proto", srcs = [":transactions.proto"], visibility = ["//visibility:public"], + deps = [ + "@com_google_protobuf//:timestamp_proto", + ], ) py_proto_library( diff --git a/rbt/v1alpha1/application/BUILD.bazel b/rbt/v1alpha1/application/BUILD.bazel new file mode 100644 index 00000000..9e5fd8ad --- /dev/null +++ b/rbt/v1alpha1/application/BUILD.bazel @@ -0,0 +1,58 @@ +load("@com_github_grpc_grpc//bazel:python_rules.bzl", "py_grpc_library", "py_proto_library") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") +load("//reboot:rules.bzl", "js_proto_library", "js_reboot_react_library", "py_reboot_library") + +proto_library( + name = "application_proto", + srcs = [":application.proto"], + visibility = ["//visibility:public"], + deps = [ + "//rbt/v1alpha1:options_proto", + ], +) + +py_proto_library( + name = "application_py_proto", + visibility = ["//visibility:public"], + deps = [":application_proto"], +) + +py_grpc_library( + name = "application_py_grpc", + srcs = [":application_proto"], + visibility = ["//visibility:public"], + deps = [":application_py_proto"], +) + +py_reboot_library( + name = "application_py_reboot", + proto = "application.proto", + proto_library = ":application_proto", + visibility = ["//visibility:public"], +) + +js_proto_library( + name = "application_js_proto", + package_json = ":package.json", + proto = "application.proto", + proto_deps = [ + ":application_proto", + # ISSUE(https://github.com/reboot-dev/mono/issues/3218): Until + # we can use `create_protoc_plugin_rule` we need to repeat the + # dependencies of the `proto_libraries` here. + "//rbt/v1alpha1:options_proto", + "@com_google_protobuf//:any_proto", + "@com_google_protobuf//:descriptor_proto", + ], + visibility = ["//visibility:public"], +) + +js_reboot_react_library( + name = "application_js_reboot_react", + srcs = [":application_js_proto"], + proto = "application.proto", + proto_deps = [ + ":application_proto", + ], + visibility = ["//visibility:public"], +) diff --git a/rbt/v1alpha1/application/application.proto b/rbt/v1alpha1/application/application.proto new file mode 100644 index 00000000..89606d94 --- /dev/null +++ b/rbt/v1alpha1/application/application.proto @@ -0,0 +1,213 @@ +syntax = "proto3"; + +package rbt.v1alpha1.application; + +import "rbt/v1alpha1/options.proto"; + +//////////////////////////////////////////////////////////////////////// + +// Per-application state that we want to expose, e.g., for web or MCP +// apps. There is exactly one of these per Reboot application, keyed +// by the name used when running the application (e.g., from `rbt dev +// run --name=...`). It is initialized on every boot in all environments +// (dev and production), so libraries can rely on it being present. +message Application { + option (rbt.v1alpha1.state) = { + }; + + // Human-readable name of the application, set by the developer via + // the `title=` kwarg on `reboot.aio.applications.Application`. + string title = 1; + // Human-readable description, set by the developer via the + // `description=` kwarg on `reboot.aio.applications.Application`. + optional string description = 2; + // Public URLs of the cloudflared and ngrok tunnels forwarding to + // this application's port, discovered by polling ngrok's local + // management API and cloudflared's `/quicktunnel` metrics endpoint. + // + // These are unset when they aren't running or have no tunnel for + // our application's port. + // + // We track ngrok and cloudflared in separate fields so the wizard + // can offer whichever is detected (or both — they can run + // side-by-side on different metrics ports). + optional string ngrok_public_url = 3; + optional string cloudflared_public_url = 4; + // Every distinct `(x-forwarded-host, user-agent)` pair the + // framework has observed on an inbound MCP request, grouped by + // host. + repeated HostConnection connections = 5; + // Example chat prompts for trying out the application. + repeated ExamplePrompt example_prompts = 6; + // Whether this application exposes any MCP tools or resources. + bool mcp = 7; + // Local port of the application. + int32 port = 8; + // Whether the `WatchTunnels` workflow has already been scheduled. + bool watch_tunnels_scheduled = 9; + + // Cryptographic root-key versions still in use, by consumer (see + // `reboot.crypto.root_keys`). A consumer records `(consumer, version)` + // while it holds keys derived from that root version and clears it once + // it has migrated off, so an operator can retire a root key version once + // no consumer still uses it. The active version is always in use and is + // never retired. + repeated RootKeyUsage root_key_usages = 10; +} + +//////////////////////////////////////////////////////////////////////// + +// A marker that `consumer` is still using cryptographic root-key +// `version`. There is at most one entry per `(consumer, version)`. +message RootKeyUsage { + string consumer = 1; + uint32 version = 2; +} + +//////////////////////////////////////////////////////////////////////// + +// All distinct `user-agent` values observed against a single +// `x-forwarded-host`. Stored in insertion order — first seen first. +message HostConnection { + string forwarded_host = 1; + repeated string user_agents = 2; +} + +//////////////////////////////////////////////////////////////////////// + +// An "example prompt" is a short, named scenario to provide to users +// of the app. Each example can contain multiple sequenced prompts — +// e.g., "Create a counter for tracking my coffee intake." followed by +// "Now increment my coffee counter." — that together exercise a flow +// through the app's MCP tools and resources. +message ExamplePrompt { + // Human-readable label for the example. Acts as the identity key, + // prompts with the same `title` replace any existing entry. + string title = 1; + // The actual prompt strings, in the order the user should send + // them, one per chat turn. + repeated string prompts = 2; +} + +//////////////////////////////////////////////////////////////////////// + +message InitializeRequest { + string title = 1; + optional string description = 2; + // Local port the application is serving on. + int32 port = 3; + // Whether the application exposes any MCP tools or resources. + bool mcp = 4; + // Current example prompts. + repeated ExamplePrompt example_prompts = 5; +} + +message InitializeResponse {} + +//////////////////////////////////////////////////////////////////////// + +message GetRequest {} + +// Explicitly enumerates which fields of `Application` state are safe +// to expose. We DO NOT return the full `Application` message — that +// way, if the state ever gets a field that's privileged or otherwise +// shouldn't leak, it stays opt-in. +message GetResponse { + string title = 1; + optional string description = 2; + optional string ngrok_public_url = 3; + optional string cloudflared_public_url = 4; + // Every distinct `(x-forwarded-host, user-agent)` pairs the app has + // observed on an inbound MCP request, grouped by host. + repeated HostConnection connections = 5; + repeated ExamplePrompt example_prompts = 6; + // Whether the application exposes any MCP tools or resources. + bool mcp = 7; + // Whether the app is running under `rbt dev`. + bool dev = 8; +} + +//////////////////////////////////////////////////////////////////////// + +message RecordConnectionRequest { + // The `x-forwarded-host` (or `host`) header and the `user-agent` + // header observed on the request. Both are required and must be + // non-empty — a connection is only meaningful if we have both. + string forwarded_host = 1; + string user_agent = 2; +} + +message RecordConnectionResponse {} + +//////////////////////////////////////////////////////////////////////// + +message WatchTunnelsRequest {} + +message WatchTunnelsResponse {} + +//////////////////////////////////////////////////////////////////////// + +message UseRootKeyVersionRequest { + string consumer = 1; + uint32 version = 2; +} + +message UseRootKeyVersionResponse {} + +//////////////////////////////////////////////////////////////////////// + +message DisuseRootKeyVersionRequest { + string consumer = 1; + uint32 version = 2; +} + +message DisuseRootKeyVersionResponse {} + +//////////////////////////////////////////////////////////////////////// + +service ApplicationMethods { + // Store the latest state (`title`, `description`, `mcp`, etc) from + // the started `Application(...)`. The state is implicitly + // default-constructed on this first writer call (we declare no + // explicit constructor), so `Initialize` can be called fresh on + // every boot whether or not the state already exists. + rpc Initialize(InitializeRequest) returns (InitializeResponse) { + option (rbt.v1alpha1.method).writer = { + }; + } + + // Returns a view of the `Application` state. + rpc Get(GetRequest) returns (GetResponse) { + option (rbt.v1alpha1.method).reader = { + }; + } + + // Record a connection — a `(x-forwarded-host, user-agent)` pair + // observed on an inbound MCP request. + rpc RecordConnection(RecordConnectionRequest) + returns (RecordConnectionResponse) { + option (rbt.v1alpha1.method).writer = { + }; + } + + // Long-running workflow that polls the local management APIs of + // every supported tunnel provider every few seconds. + rpc WatchTunnels(WatchTunnelsRequest) returns (WatchTunnelsResponse) { + option (rbt.v1alpha1.method).workflow = { + }; + } + + // Records that `consumer` is using cryptographic root-key `version`. + rpc UseRootKeyVersion(UseRootKeyVersionRequest) + returns (UseRootKeyVersionResponse) { + option (rbt.v1alpha1.method).writer = { + }; + } + + // Records that `consumer` no longer uses root-key `version`. + rpc DisuseRootKeyVersion(DisuseRootKeyVersionRequest) + returns (DisuseRootKeyVersionResponse) { + option (rbt.v1alpha1.method).writer = { + }; + } +} diff --git a/rbt/v1alpha1/application/package.json b/rbt/v1alpha1/application/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/rbt/v1alpha1/application/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/rbt/v1alpha1/database.proto b/rbt/v1alpha1/database.proto index 6d04dc7c..0518139c 100644 --- a/rbt/v1alpha1/database.proto +++ b/rbt/v1alpha1/database.proto @@ -285,6 +285,45 @@ message LoadResponse { // Invariant: actor's 'state' is expected to be set, although it may be empty. repeated Actor actors = 1; repeated Task tasks = 2; + + // Latest timestamp from database clock, for timestamp refresh. + // Optional for explicit backwards compatibility. + optional google.protobuf.Timestamp timestamp = 3; +} + +//////////////////////////////////////////////////////////////////////// + +// Combined preload of state and idempotent mutations in a single +// (server-streaming) RPC to the database. Used to eliminate one +// round-trip on the cold path of a request: a single `Preload` is +// fired early when handling a method call, and `_load()` and +// `check_for_idempotent_mutation()` consume the cached results +// instead of making their own RPCs to the database. +message PreloadRequest { + string state_type = 1; + string state_ref = 2; + // When true, the skip loading the state. Used by servers that + // already have the state cached or being loaded. + bool skip_state = 3; + // When true, the skip recovering idempotent mutations. Used by + // servers that already have the idempotent mutations cached or + // being loaded. + bool skip_idempotent_mutations = 4; +} + +message PreloadResponse { + // Set only in the first response message of the stream. Not set if + // their is no state yet. + optional Actor actor = 1; + + // Idempotent mutations are streamed across multiple response + // messages to handle large sets without exceeding gRPC message size + // limits. + repeated IdempotentMutation idempotent_mutations = 2; + + // Latest timestamp from database clock, for timestamp refresh. Set + // only in the first response message. + optional google.protobuf.Timestamp timestamp = 3; } //////////////////////////////////////////////////////////////////////// @@ -313,7 +352,11 @@ message StoreRequest { bool sync = 7; } -message StoreResponse {} +message StoreResponse { + // Latest timestamp from database clock, for timestamp refresh. + // Optional for explicit backwards compatibility. + optional google.protobuf.Timestamp timestamp = 1; +} //////////////////////////////////////////////////////////////////////// @@ -341,18 +384,20 @@ message RecoverResponse { repeated Task pending_tasks = 1; repeated Transaction participant_transactions = 2; - // Transaction UUID -> PreparedTransactionCoordinator for every + // Transaction UUID -> TransactionCoordinator for every // transaction that a coordinator completed the prepare phase of two // phase commit. // // NOTE: because a map key can not be bytes we pass a stringified UUID // where as other places we pass the UUID in its bytes form. - // clang-format off - map - prepared_transaction_coordinators = 3; - // clang-format on + map transaction_coordinators = 3; repeated IdempotentMutation idempotent_mutations = 4; + + // Latest timestamp from the database's clock. Used as the server's + // recovery timestamp for restart detection. Optional for explicit + // backwards compatibility. + optional google.protobuf.Timestamp timestamp = 7; } //////////////////////////////////////////////////////////////////////// @@ -381,9 +426,32 @@ message RecoverIdempotentMutationsResponse { message TransactionParticipantPrepareRequest { string state_type = 1; string state_ref = 2; + + // Transaction info, provided when using restart detection + // to register the participant at prepare time (instead of + // at join time). + optional Transaction transaction = 3; + + // When using restart detection, all intermediate writes within the + // transaction are kept in memory and sent at prepare time. + + // The final serialized state for this participant. The + // `state_type` and `state_ref` are taken from the `transaction` + // field above. + optional bytes state = 4; + + // Tasks scheduled during the transaction. + repeated Task task_upserts = 5; + + // Idempotent mutations recorded during the transaction. + repeated IdempotentMutation idempotent_mutations = 6; } -message TransactionParticipantPrepareResponse {} +message TransactionParticipantPrepareResponse { + // Latest timestamp from database clock, for timestamp refresh. + // Optional for explicit backwards compatibility. + optional google.protobuf.Timestamp timestamp = 1; +} //////////////////////////////////////////////////////////////////////// @@ -392,7 +460,11 @@ message TransactionParticipantCommitRequest { string state_ref = 2; } -message TransactionParticipantCommitResponse {} +message TransactionParticipantCommitResponse { + // Latest timestamp from database clock, for timestamp refresh. + // Optional for explicit backwards compatibility. + optional google.protobuf.Timestamp timestamp = 1; +} //////////////////////////////////////////////////////////////////////// @@ -405,13 +477,17 @@ message TransactionParticipantAbortResponse {} //////////////////////////////////////////////////////////////////////// -// Message representing a prepared transaction from the coordinator's -// perspective. -message PreparedTransactionCoordinator { +// Message representing a transaction coordinator. +message TransactionCoordinator { // The coordinator's `state_ref`. string state_ref = 1; // The participants in this transaction. Participants participants = 2; + // True while the coordinator is still preparing participants. Once + // all participants are prepared the coordinator sets this back to + // false. Default of false also for backwards compatibility, i.e., + // existing records are treated as fully prepared. + bool preparing = 3; } message TransactionCoordinatorPreparedRequest { @@ -419,10 +495,30 @@ message TransactionCoordinatorPreparedRequest { // Deprecated: Use `prepared_transaction_coordinator` instead. Participants participants = 2 [deprecated = true]; // New field that includes state_ref for shard-aware storage. - PreparedTransactionCoordinator prepared_transaction_coordinator = 3; + TransactionCoordinator transaction_coordinator = 3; +} + +message TransactionCoordinatorPreparedResponse { + // Latest timestamp from database clock, for timestamp refresh. + // Optional for explicit backwards compatibility. + optional google.protobuf.Timestamp timestamp = 1; +} + +//////////////////////////////////////////////////////////////////////// + +// Records the participant list for a transaction that is about to be +// prepared. This is the first phase of the coordinator's database +// interaction and can run concurrently with participant prepares. +message TransactionCoordinatorPrepareRequest { + bytes transaction_id = 1; + TransactionCoordinator transaction_coordinator = 2; } -message TransactionCoordinatorPreparedResponse {} +message TransactionCoordinatorPrepareResponse { + // Latest timestamp from database clock, for timestamp refresh. + // Optional for explicit backwards compatibility. + optional google.protobuf.Timestamp timestamp = 1; +} //////////////////////////////////////////////////////////////////////// @@ -435,6 +531,17 @@ message TransactionCoordinatorCleanupResponse {} //////////////////////////////////////////////////////////////////////// +message RefreshTimestampRequest {} + +message RefreshTimestampResponse { + // Timestamp from the database's clock. Used by servers to source + // timestamps for UUID7 transaction ID generation and restart + // detection. + google.protobuf.Timestamp timestamp = 1; +} + +//////////////////////////////////////////////////////////////////////// + message GetApplicationMetadataRequest {} message GetApplicationMetadataResponse { @@ -490,6 +597,15 @@ service Database { // exists. rpc Load(LoadRequest) returns (LoadResponse); + // Combined preload of state and idempotent mutations in a single + // round-trip. Server-streaming so that large sets of idempotent + // mutations can be sent across multiple response messages. + // + // TODO: consider replacing with a `PreloadBatch` to preload a batch + // of states at a time which will futher reduce requests to the + // database as well as reduce the work on the server. + rpc Preload(PreloadRequest) returns (stream PreloadResponse); + // Stores the specified data. rpc Store(StoreRequest) returns (StoreResponse); @@ -513,6 +629,9 @@ service Database { rpc TransactionCoordinatorPrepared(TransactionCoordinatorPreparedRequest) returns (TransactionCoordinatorPreparedResponse); + rpc TransactionCoordinatorPrepare(TransactionCoordinatorPrepareRequest) + returns (TransactionCoordinatorPrepareResponse); + rpc TransactionCoordinatorCleanup(TransactionCoordinatorCleanupRequest) returns (TransactionCoordinatorCleanupResponse); @@ -532,6 +651,12 @@ service Database { // Stores application metadata to persistent storage. rpc StoreApplicationMetadata(StoreApplicationMetadataRequest) returns (StoreApplicationMetadataResponse); + + // Returns the current timestamp from the database's clock. + // Used by servers to refresh their latest known timestamp + // for UUID7 transaction ID generation. + rpc RefreshTimestamp(RefreshTimestampRequest) + returns (RefreshTimestampResponse); } //////////////////////////////////////////////////////////////////////// diff --git a/rbt/v1alpha1/index.ts b/rbt/v1alpha1/index.ts index c7796a53..25071110 100644 --- a/rbt/v1alpha1/index.ts +++ b/rbt/v1alpha1/index.ts @@ -944,6 +944,12 @@ export type IdempotencyOptions = key: string; perIteration?: undefined; always?: boolean; + } + | { + alias?: undefined; + key?: undefined; + perIteration?: undefined; + always: true; }; export type ScheduleOptions = { when: Date }; diff --git a/rbt/v1alpha1/package.json b/rbt/v1alpha1/package.json index e3eb39a1..fc87458f 100644 --- a/rbt/v1alpha1/package.json +++ b/rbt/v1alpha1/package.json @@ -1,6 +1,6 @@ { "name": "@reboot-dev/reboot-api", - "version": "1.0.4", + "version": "1.1.0", "type": "module", "description": "npm package for Reboot API", "main": "index.js", diff --git a/rbt/v1alpha1/rootpage/rootpage.proto b/rbt/v1alpha1/rootpage/rootpage.proto index 8fedb269..ca9df131 100644 --- a/rbt/v1alpha1/rootpage/rootpage.proto +++ b/rbt/v1alpha1/rootpage/rootpage.proto @@ -12,10 +12,29 @@ service RootPage { rpc RootPage(RootPageRequest) returns (google.api.HttpBody) { option (google.api.http) = { get: "/" + additional_bindings { get: "/__/rootpage/{file=**}" } + }; + } + + // Tiny connectivity endpoint the rootpage frontend uses to verify + // any found tunnels correctly forward to this process. + rpc Probe(ProbeRequest) returns (ProbeResponse) { + option (google.api.http) = { + get: "/__/probe" }; } } //////////////////////////////////////////////////////////////////////// -message RootPageRequest {} +message RootPageRequest { + // Set to the requested file under `/__/rootpage/`; empty when the + // root path `/` is requested. + string file = 1; +} + +//////////////////////////////////////////////////////////////////////// + +message ProbeRequest {} + +message ProbeResponse {} diff --git a/rbt/v1alpha1/transactions.proto b/rbt/v1alpha1/transactions.proto index 2aa1b21d..7160e4ac 100644 --- a/rbt/v1alpha1/transactions.proto +++ b/rbt/v1alpha1/transactions.proto @@ -2,15 +2,37 @@ syntax = "proto3"; package rbt.v1alpha1; +import "google/protobuf/timestamp.proto"; + //////////////////////////////////////////////////////////////////////// message PrepareRequest { bytes transaction_id = 1; + // When true, the participant returns definitive outcomes, i.e., + // whether the transaction should abort or not, whether a restart + // was detected, etc, via fields in `PrepareResponse` rather than + // the legacy semantics of raising an exception causing the RPC to + // abort with a gRPC status code. New coordinators set this; old + // coordinators leave it false (the default) and the participant + // falls back to the legacy semantics. + bool abort_via_response = 2; } //////////////////////////////////////////////////////////////////////// -message PrepareResponse {} +message PrepareResponse { + // True if the participant definitively cannot prepare this + // transaction and the coordinator must abort it. Only set when the + // request had `abort_via_response = true`. + bool abort = 1; + // True if the reason for abort is that the participant restarted + // since the transaction began (restart detection). Only meaningful + // when `abort = true`. + bool restart_detected = 2; + // The participant's recovery timestamp, included when + // `restart_detected = true`. + optional google.protobuf.Timestamp recovery_timestamp = 3; +} //////////////////////////////////////////////////////////////////////// diff --git a/reboot-skills/README.md b/reboot-skills/README.md deleted file mode 100644 index f1cbbd9c..00000000 --- a/reboot-skills/README.md +++ /dev/null @@ -1,85 +0,0 @@ -# Reboot Skills - -Claude Code plugins for building on [Reboot](https://reboot.dev). - -## Available Skills - -| Skill | Description | -| -------------------------------------------- | ------------------------------------------------------------------------------------------ | -| [`reboot-chat-app`](skills/reboot-chat-app/) | Use Reboot to build AI Chat Apps (MCP Apps) for ChatGPT, Claude, VSCode, Goose, and others | - -## Installation - -### From GitHub - -Add the Reboot skills marketplace and install the plugin: - -```bash -# 1. Add the Reboot skills marketplace (one-time). -claude plugin marketplace add reboot-dev/reboot-skills - -# 2. Install a skill. -claude plugin install reboot-chat-app@reboot-skills -``` - -If you install the plugin within `claude` with `/plugin` you need to restart for -the configuration and skill to load correctly. - -To auto-enable for your team, add to your project's -`.claude/settings.json`: - -```json -{ - "extraKnownMarketplaces": { - "reboot-skills": { - "source": { - "source": "github", - "repo": "reboot-dev/reboot-skills" - } - } - }, - "enabledPlugins": { - "reboot-chat-app@reboot-skills": true - } -} -``` - -### Local (repo checked out) - -```bash -git clone https://github.com/reboot-dev/reboot-skills.git -claude --plugin-dir ./reboot-skills -``` - -## Usage - -Once installed, use the skill by name: - -``` -/reboot-chat-app Build a todo list app with drag-to-reorder -``` - -The skill enters plan mode first — it analyzes your description, -proposes a state model and method map, and waits for your approval -before writing any code. - -## Repository Structure - -``` -reboot-skills/ -├── .claude-plugin/ -│ └── marketplace.json -├── README.md -└── skills/ - └── reboot-chat-app/ - └── SKILL.md -``` - -The repository root is the plugin directory. It contains: - -- `.claude-plugin/marketplace.json` — marketplace and plugin definitions -- `skills//SKILL.md` — skill definition with YAML frontmatter - -## License - -Apache-2.0 diff --git a/reboot-skills/skills/reboot-chat-app/SKILL.md b/reboot-skills/skills/reboot-chat-app/SKILL.md deleted file mode 100644 index 7056708f..00000000 --- a/reboot-skills/skills/reboot-chat-app/SKILL.md +++ /dev/null @@ -1,1519 +0,0 @@ ---- -name: reboot-chat-app -description: Use Reboot to build AI Chat Apps (MCP Apps) for ChatGPT, Claude, VSCode, Goose, and others. -argument-hint: [] -# Scaffolding requires file creation, shell commands (uv, npm, -# rbt), and code edits across ~15 files. -allowed-tools: Bash, Read, Write, Glob, Grep, Edit ---- - -# /reboot-chat-app — Build Reboot AI Chat Apps - -Build complete Reboot AI Chat Apps from a user description. - -## Installation - -### From GitHub - -Add the Reboot skills marketplace and install the plugin: - -```bash -# 1. Add the marketplace (one-time). -claude plugin marketplace add reboot-dev/reboot-skills - -# 2. Install the plugin. -claude plugin install reboot-chat-app@reboot-skills -``` - -If you install the plugin within `claude` with `/plugin` you need to restart for -the configuration and skill to load correctly. - -Or add to your project's `.claude/settings.json` so teammates -are automatically offered the plugin on first use: - -```json -{ - "extraKnownMarketplaces": { - "reboot-skills": { - "source": { - "source": "github", - "repo": "reboot-dev/reboot-skills" - } - } - }, - "enabledPlugins": { - "reboot-chat-app@reboot-skills": true - } -} -``` - -### Local (repo checked out) - -If you have the `reboot-skills` repo cloned locally: - -```bash -claude --plugin-dir /path/to/reboot-skills -``` - -## When to Use - -- Building a new Reboot AI Chat App from a description -- Adding features, state, or UI to an existing Reboot AI Chat App -- Modifying state model, methods, or React UI in a Reboot AI Chat App - -## Workflow: Plan First, Then Build - -**Always enter plan mode before writing code.** The state model is -the foundation — getting entities, field types, or method types -wrong means regenerating everything across 12+ files. - -### Plan Phase - -1. Analyze the user's description using the State Model Assessment - below -2. Enter plan mode (`EnterPlanMode`) -3. Present the proposed design: - - User type and its methods (as the MCP front door to the - application types discussed below, creating new ones and - locating existing ones) - - Application types: state shape (fields, types, tags) - - Method map: which operations, which method type (Reader/ - Writer/Transaction/Workflow), which get UI() - - Tool surface: what the AI will see as callable tools -4. Get user approval before writing any files -5. Then execute the Step-by-Step Build Flow - -For updates to existing apps, still plan: read current state, propose -changes, confirm, then modify. - -## State Model Assessment - -Before writing code, analyze the user's request: - -1. **Application types**: What primary things is the user managing? - (counter, inventory, chat thread, etc.) Each becomes - its own `Type` with its own state. -2. **User methods**: How does the AI create instances of - application types? Each gets a `Transaction` on `User` - that calls `.create(context)`. -3. **State shape**: Fields, types — lists, nested objects, - primitives. Each gets `Field(tag=N)`. **Nested Model - subobjects** (preferences, profile, config — owned 1:1 by a - parent state) must be `Optional[X] = Field(tag=N, default=None)` - and hydrated in the parent's factory `create` Writer; nested - Models can't take `default=` or `default_factory=` (Gotcha #21). - For collections, prefer `list[Item]` with `default_factory=list` - over a single-nested wrapper. -4. **Operations**: Map to the right method type: - - `Reader` — read-only queries - - `Writer` — single-state mutations - - `Transaction` — multi-state atomic operations (e.g., - transfer between two accounts, or User creating an - application type instance) - - `Workflow` — long-running control flows with loops, - scheduling, and idempotency helpers -5. **Tool surface**: Which operations need UIs (`UI()`)? - Which need explicit tool exposure (`mcp=Tool()`)? -6. **Identity**: Single default instance vs multiple instances? -7. **Cross-state coordination**: Does any operation touch - multiple state instances? If yes, use `Transaction`. - -## Key Framework Concepts - -### User and Application Types - -Every AI Chat App has a `User` type and one or more application -types: - -- **`User`** is auto-constructed for each authenticated user. Its - state is typically empty. Its methods are `Transaction`s that - create instances of application types, or `Reader`s that find - the IDs of existing application type instances in indexes that - have well-known IDs of their own. -- **Application types** (e.g., `Counter`) hold the - actual application state. They need a `create` Writer with - `factory=True` for construction. - -### Tool Exposure Control - -Every method must explicitly declare its MCP exposure: - -- **`mcp=Tool()`**: Expose the method as an AI-callable tool. - Required on every method — including `User` methods — that - the AI should be able to call. -- **`mcp=None`**: Hide the method from the AI. Use for - human-only actions or to reduce context bloat. -- **`Tool()` options**: `Tool(name="custom_name", title="Title")` - to override the default tool name or add a human-readable title. - -### Method Types - -- **`UI()`**: Opens a React UI in the AI chat interface. Takes - `request=` (config type or `None`), `path=` (web dir relative - to project root), `title=`, `description=`. No servicer - implementation needed — the React app IS the implementation. - When `request=` is a Model type, its fields are passed to the - React component as props. -- **`Writer`**: Single-state mutations. Context: `WriterContext`. - Use `factory=True` on the `create` method of application types. -- **`Reader`**: Read-only queries. Context: `ReaderContext`. -- **`Transaction`**: Multi-state atomic operations. Context: - `TransactionContext`. Use when an operation must modify multiple - state instances atomically, or when User creates application - type instances. -- **`Workflow`**: Long-running control flows. Context: - `WorkflowContext`. Implemented as `@classmethod` (not instance - method). Support `context.loop()` for periodic/reactive loops, - scheduling with `timedelta`, and idempotency helpers. - -### Declarative, Not Decorator - -All MCP surface is defined in the API file. `main.py` is minimal. -No `@mcp.tool()` decorators. - -### State is Durable - -State survives restarts. Set `dev run --application-name=` in `.rbtrc` to -persist across dev restarts. Use `uv run rbt dev expunge --application-name=` to reset. - -## Project Structure - -``` -/ -├── .python-version # "3.10" -├── .rbtrc # Line-based config (NOT YAML!) -├── pyproject.toml # Python deps (uv) -├── api/ -│ └── /v1/ -│ └── .py # API definition -├── backend/ -│ └── src/ -│ ├── main.py # Application entrypoint -│ └── servicers/ -│ └── .py # Servicer implementation -└── web/ - ├── package.json - ├── tsconfig.json - ├── tsconfig.app.json - ├── tsconfig.node.json - ├── vite.config.ts - ├── index.css # Theme variables - └── ui/ - └── / - ├── index.html - ├── main.tsx # RebootClientProvider entry - ├── App.tsx # React component - └── App.module.css -``` - -## Step-by-Step Build Flow - -**Only execute after plan approval. All commands run from the -application directory.** - -1. Create `.python-version`, `pyproject.toml`, `.rbtrc` -2. `uv sync` -3. Write API definition (`api//v1/.py`) -4. `uv run rbt generate` -5. Write servicer (`backend/src/servicers/.py`) -6. Write `main.py` -7. `npm create @reboot-dev/ui` -8. `cd web && npm install` -9. `uv run rbt generate` (React bindings need `node_modules`) -10. Customize React UIs: edit `App.tsx` files in `web/ui/*/` -11. `cd web && npm run build` -12. Create `mcp_servers.json` with - `{"mcpServers":{"":{"url":"http://localhost:9991/mcp","useOAuth":true}}}` -13. **STOP.** Do NOT run the app yourself. Print the - following run instructions exactly, then wait: - - ``` - Project directory: - - To run (each in a separate terminal, from the project directory): - - uv run rbt dev run # start backend - cd web && npm run dev # HMR frontend (separate terminal) - - To test with MCP inspector (separate terminal): - - npx @mcpjam/inspector@2.4.0 --config mcp_servers.json --server - ``` - - Replace `` with the actual server name from - `mcp_servers.json`. Print the real absolute path of the - project directory (not the literal placeholder). Then - suggest a first prompt the user can try in the inspector - (e.g., "Create a new todo list and show it to me"). - -## Inline Patterns - -All patterns below are complete and copy-paste-ready. Replace -``, ``, ``, `` with actual values. - -### `.python-version` - -``` -3.10 -``` - -### `.rbtrc` - -Line-based config. NOT YAML! - -``` -# Find API definitions in 'api/'. -generate api/ - -# Tell `rbt generate` where to put generated files. -generate --python=backend/api/ - -# Generate React bindings for web apps (into "web/api/"). -generate --react=web/api -generate --react-extensions - -# Watch if any source files are modified. -dev run --watch=backend/**/*.py - -# Tell `rbt` that this is a Python application. -dev run --python - -# Save state between restarts. -dev run --application-name= - -# Run the application! -dev run --application=backend/src/main.py - -# Default to HMR mode when no --config is specified. -dev run --default-config=hmr - -# Hot Module Replacement (HMR): Vite dev server proxied through Envoy. -# Run Vite in a separate terminal: cd web && npm run dev -# Envoy routes "/__/web/**" to Vite for HMR support. -dev run:hmr --mcp-frontend-host=http://localhost:4444 - -# Dist mode: serve pre-built artifacts from "web/dist/" (no Vite HMR). -# Usage: uv run rbt dev run --config=dist -# Requires: cd web && npm run build -dev run:dist --mcp-frontend-host="" - -# When expunging, expunge that state we've saved. -dev expunge --application-name= -``` - -### `pyproject.toml` - -```toml -[project] -name = "" -version = "0.1.0" -requires-python = ">= 3.10" -dependencies = [ - "httpx>=0.27,<1.0", - "uuid7>=0.1.0", - "anyio>=4.0.0", - "reboot>=1.0.4", -] - -[tool.rye] -dev-dependencies = [ - "mypy==1.18.1", - "types-protobuf>=4.24.0.20240129", - "reboot>=1.0.4", -] - -virtual = true -managed = true -``` - -### API Definition (`api//v1/.py`) - -Rules: - -- Import only the method types you use from `reboot.api` -- Helper Model types as standalone classes -- State model with `Field(tag=N)` on every field -- `User` type with empty state and `Transaction` methods - that create application type instances -- Application types with their own state and methods -- All methods need explicit `mcp=Tool()` (AI-callable) or - `mcp=None` (hidden from AI) -- Application types need a `create` Writer with `factory=True` -- `api = API(User=Type(...), =Type(...))` - -#### Simple Example (Counter) - -```python -from reboot.api import ( - API, - UI, - Field, - Methods, - Model, - Reader, - Tool, - Transaction, - Type, - Writer, -) - - -# -- User models. -- - - -class CreateCounterResponse(Model): - counter_id: str = Field(tag=1) - - -class UserState(Model): - pass - - -# -- Counter models. -- - - -class CounterState(Model): - value: int = Field(tag=1, default=0) - description: str = Field(tag=2, default="") - - -class ValueResponse(Model): - value: int = Field(tag=1) - - -class AmountRequest(Model): - """Request with an amount parameter.""" - amount: int = Field(tag=1) - - -api = API( - User=Type( - state=UserState, - methods=Methods( - create_counter=Transaction( - request=None, - response=CreateCounterResponse, - description="Create a new Counter. Returns " - "the ID of the new counter. That ID is not " - "human-readable; pass it to future tool " - "calls where needed, but no need to tell " - "the human what it is.", - mcp=Tool(), - ), - ), - ), - Counter=Type( - state=CounterState, - methods=Methods( - show_clicker=UI( - request=None, - path="web/ui/clicker", - title="Counter Clicker", - description="Interactive clicker UI for " - "the counter.", - ), - create=Writer( - request=None, - response=None, - factory=True, - mcp=None, - ), - get=Reader( - request=None, - response=ValueResponse, - description="Get the current counter value.", - mcp=Tool(), - ), - increment=Writer( - request=AmountRequest, - response=None, - description="Increment the counter by the " - "specified amount.", - mcp=Tool(), - ), - decrement=Writer( - request=AmountRequest, - response=None, - description="Decrement the counter by the " - "specified amount.", - mcp=Tool(), - ), - ), - ), -) -``` - -#### Parameterized UI Example - -When the AI should pass parameters to a React UI (e.g., a -personalized message or configuration), use `request=` with a -Model type. The fields become React component props: - -```python -class DashboardConfig(Model): - """Configuration passed by the AI.""" - personalized_message: str = Field(tag=1) - - -# In the application type's Methods(): -show_dashboard=UI( - # The AI provides a DashboardConfig when opening this UI. - # The fields are passed to the React component as props. - request=DashboardConfig, - path="web/ui/dashboard", - title="Counter Dashboard", - description="Dashboard UI. Use `personalized_message` to " - "impart wisdom on the topic of counting things.", -), -``` - -The React component receives the config fields as props: - -```tsx -import { - type DashboardConfig, - useCounter, -} from "@api//v1/_rbt_react"; - -export const DashboardApp: FC = ({ personalizedMessage }) => { - const counter = useCounter(); - const { response } = counter.useGet(); - // personalizedMessage is available as a prop. - return ( -
- {personalizedMessage}: {response?.value ?? 0} -
- ); -}; -``` - -#### mcp=None Example - -Hide a method from the AI (e.g., for human-only actions): - -```python -# In an application type's Methods(): -# Only callable from the React UI, not by the AI. -confirm_dangerous_action=Writer( - request=ConfirmRequest, - response=None, - description="Confirm a dangerous action.", - mcp=None, -), -``` - -#### List State Patterns - -For application types with list-based state (items, entries, -messages, etc.): - -- Define helper Model types as standalone classes (e.g., - `class Item(Model)`) — NOT nested on the application type -- Use `list[Item]` in the state with `default_factory=list` -- Add CRUD Writers: add, remove, toggle, reorder as needed -- Each Writer validates indices before mutating -- The `reorder` pattern uses `pop` + `insert` -- In the servicer, import helpers standalone: - `from .v1. import Item` - -The counter example above shows the full User + application -type pattern. Apply the same structure for any application type, -adding whatever Writers and Readers your app needs. - -#### Nested Model State Patterns - -For application types that own a single nested `Model` -sub-object (preferences blob, profile, config, etc.): - -- Declare the field as `Optional[Sub] = Field(tag=N, default=None)`. - Nested non-Optional `Model` types reject both `default=` and - `default_factory=` (Gotcha #21). -- Hydrate the sub-object in the parent's factory `create` - Writer, so callers never observe the `None`: - -```python -from reboot.api import API, Field, Methods, Model, Transaction, Type, User as RbtUser, Writer -from typing import Optional - -class GuestPreferences(Model): - meal_type: str = Field(tag=1, default="") - calorie_level: str = Field(tag=2, default="") - dietary_restrictions: str = Field(tag=3, default="") - -class Guest(Model): - name: str = Field(tag=1, default="") - # Single nested Model: Optional + default=None, populated - # by the factory `create` below. - preferences: Optional[GuestPreferences] = Field(tag=2, default=None) - -class CreateRequest(Model): - name: str = Field(tag=1) - meal_type: str = Field(tag=2, default="") - calorie_level: str = Field(tag=3, default="") - dietary_restrictions: str = Field(tag=4, default="") - -# Servicer side (in `backend/src/servicers/.py`): -class GuestServicer(Guest.Servicer): - async def create(self, context, *, name, meal_type, calorie_level, dietary_restrictions): - self.state.name = name - self.state.preferences = GuestPreferences( - meal_type=meal_type, - calorie_level=calorie_level, - dietary_restrictions=dietary_restrictions, - ) -``` - -If the prompt suggests _plural_ sub-objects ("each guest's -preferences"), prefer `list[GuestPreferences]` with -`default_factory=list` — lists are exempt from this rule. - -#### Workflow Example (Long-Running) - -Use `Workflow` for periodic or long-running operations: - -```python -from reboot.api import ( - API, - Field, - Methods, - Model, - Tool, - Type, - Workflow, -) - - -class DoPingPeriodicallyRequest(Model): - num_pings: int = Field(tag=1) - period_seconds: float = Field(tag=2) - - -class DoPingPeriodicallyResponse(Model): - num_pings: int = Field(tag=1) - - -# In an application type's Methods(): -do_ping_periodically=Workflow( - request=DoPingPeriodicallyRequest, - response=DoPingPeriodicallyResponse, -) -``` - -### Servicer (`backend/src/servicers/.py`) - -Rules: - -- Import helper types standalone: - `from .v1. import MyItem` -- Import generated classes: - `from .v1._rbt import User, Counter` -- Each type gets its own servicer class - (e.g., `UserServicer`, `CounterServicer`) -- `User.Servicer` / `Counter.Servicer` base -- Context types from `reboot.aio.contexts`: - - `ReaderContext` — read-only - - `WriterContext` — single-state mutation - - `TransactionContext` — multi-state atomic - - `WorkflowContext` — long-running (`@classmethod`) -- Access state via `self.state.` -- Request types: `Counter.XxxRequest`, - response: `Counter.XxxResponse` - -#### Simple Servicer (Counter) - -```python -from ai_chat_counter.v1.counter_rbt import Counter, User -from reboot.aio.contexts import ( - ReaderContext, - TransactionContext, - WriterContext, -) - - -class UserServicer(User.Servicer): - - async def create_counter( - self, - context: TransactionContext, - ) -> User.CreateCounterResponse: - """Create a new Counter and return its ID.""" - # Factory create: pass request fields as keyword args - # directly — do NOT wrap in a Request object. - # No-args: Counter.create(context) - # With args: Counter.create(context, title="...", count=0) - counter, _ = await Counter.create(context) - return User.CreateCounterResponse( - counter_id=counter.state_id, - ) - - -class CounterServicer(Counter.Servicer): - - async def create(self, context) -> None: - # State is initialized with defaults; nothing to do. - pass - - async def increment( - self, - context: WriterContext, - request: Counter.IncrementRequest, - ) -> None: - self.state.value += request.amount - - async def decrement( - self, - context: WriterContext, - request: Counter.DecrementRequest, - ) -> None: - self.state.value -= request.amount - - async def get( - self, - context: ReaderContext, - ) -> Counter.GetResponse: - return Counter.GetResponse(value=self.state.value) -``` - -#### Workflow Servicer - -Workflow methods are `@classmethod` — no `self`, no `self.state`. To -call back into the current instance, use the **state class** -imported from `_rbt` (e.g. `MyType.ref()`), NOT `cls`. Inside a -Workflow, calling `.ref()` with no arguments is special: -it picks up the current `state_id` from `WorkflowContext` -automatically, so `MyType.ref()` resolves to a ref to the running -workflow's own instance. - -```python -from datetime import timedelta -from reboot.aio.contexts import WorkflowContext -# Import the state class — this is what `.ref()` is called on, -# NOT `cls`. `cls` inside the classmethod is `MyTypeBaseServicer`. -from .v1._rbt import MyType - - -class MyTypeServicer(MyType.Servicer): - - @classmethod - async def do_ping_periodically( - cls, - context: WorkflowContext, - request: MyType.DoPingPeriodicallyRequest, - ) -> MyType.DoPingPeriodicallyResponse: - async for iteration in context.loop( - "Ping periodically", - interval=timedelta(seconds=request.period_seconds), - ): - # `MyType.ref()` with no args is Workflow-only magic: - # it reads `state_id` from `WorkflowContext`, returning - # a ref to this workflow's own instance. Do NOT write - # `cls.ref()` or `self.ref()` here — see Critical - # Gotcha #19. - await MyType.ref().do_ping(context) - pings_sent = iteration + 1 # iteration starts at 0. - if pings_sent >= request.num_pings: - break - - # `.read()` is only valid on the workflow's own no-arg - # ref; a foreign-state read like - # `OtherType.ref(id).read(context)` raises a "only - # supported within workflows" RuntimeError. Call a Reader - # method on the foreign type instead — see Gotcha #23. - state = await MyType.ref().read(context) - return MyType.DoPingPeriodicallyResponse( - num_pings=state.num_pings, - ) -``` - -**Use inline writers for workflow-only state changes.** When the -mutation is only ever performed by this workflow, do _not_ add a -separate `store_xxx` Writer to the API just so the workflow can -call it. Pass an `async (state) -> ...` function to -`.idempotently("alias").write(context, fn)`: - -```python -async def increment_count(state): - state.num_pings += 1 - -await MyType.ref().idempotently( - "Increment ping count", -).write(context, increment_count) -``` - -The idempotency alias is a human-readable string that survives -workflow restarts — the inline writer runs at most once per -alias. Anti-pattern: defining a `store_count` Writer in the API -just so the workflow can `await MyType.ref().store_count(context)` -to bump a counter. That adds an unnecessary indirection. -Reserve declared Writers for operations that are also called -from outside the workflow. - -For "run every time" (e.g., re-fetching a remote value on each -loop iteration), use `.always().write(context, fn)` instead of -`.idempotently("...").write(...)`. - -#### Scheduling a Workflow from a Transaction - -A workflow can only be `await`-ed directly from an -`ExternalContext` (e.g. a bootstrap script) or from another -`WorkflowContext`. In **any** other context — most commonly a -`TransactionContext` kicking off a workflow on a state it just -created — the workflow must be **scheduled**, not awaited -directly. Use `.schedule()` to fire-and-forget from a -transaction: - -```python -# In a User's Transaction method that creates a Game and wants -# its autoplay workflow to start running: -class UserServicer(User.Servicer): - - async def create_game( - self, - context: TransactionContext, - request: User.CreateGameRequest, - ) -> User.CreateGameResponse: - game, _ = await Game.create(context, ...) - # GOOD — schedule the workflow from the transaction. - # Request type is empty (`AutoplayRequest`), so just - # pass `context`: - await Game.ref(game.state_id).schedule().autoplay(context) - # If the workflow request had fields (e.g. - # `do_ping_periodically(num_pings, period_seconds)`), - # pass them as keyword args: - await Game.ref(game.state_id).schedule().do_ping_periodically( - context, - num_pings=10, - period_seconds=1.0, - ) - # BAD — wrapping in `request=` raises - # `TypeError: ... got an unexpected keyword argument - # 'request'` (Gotcha #9): - # await Game.ref(game.state_id).schedule().autoplay( - # context, request=Game.AutoplayRequest() - # ) - # BAD — awaiting a workflow directly from a Transaction - # raises `TypeError: ... 'Autoplay' is a workflow and - # must be scheduled from a 'TransactionContext' via - # `await [...].schedule([...]).Autoplay(context, [...])` - # (Gotcha #20): - # await Game.ref(game.state_id).autoplay(context) - return User.CreateGameResponse(game_id=game.state_id) -``` - -`.schedule(when=timedelta(...))` delays the workflow by a -duration. `.schedule()` with no argument starts it as soon as -the transaction commits. - -Same rule from a `WriterContext` or `ReaderContext`: use -`.schedule()`. Only `ExternalContext` and `WorkflowContext` can -await a workflow directly. - -### `main.py` - -Register all servicers (User + application types): - -```python -import asyncio -import logging -from reboot.aio.applications import Application -from servicers. import ( - CounterServicer, - UserServicer, -) - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) - - -async def main() -> None: - application = Application( - servicers=[UserServicer, CounterServicer], - ) - await application.run() - - -if __name__ == "__main__": - asyncio.run(main()) -``` - -### `web/package.json` - -**Use explicit per-UI build scripts as shown below. Do NOT create a -`build.js` or any auto-discovery wrapper — use `npm run build:` -scripts directly.** - -```json -{ - "name": "-web", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build:": "vite build --mode ", - "build:watch:": "vite build --mode --watch", - "build": "tsc --noEmit && npm run build:", - "build:watch": "concurrently \"npm:build:watch:*\"" - }, - "dependencies": { - "@modelcontextprotocol/ext-apps": "1.5.0", - "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-react": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "zod": "^3.25.0" - }, - "devDependencies": { - "@types/react": "^18.2.67", - "@types/react-dom": "^18.2.22", - "@vitejs/plugin-react": "^4.7.0", - "concurrently": "^9.1.2", - "typescript": "^5.9.2", - "vite": "^6.3.5", - "vite-plugin-singlefile": "^2.0.3" - } -} -``` - -For multiple UIs, add `build:` and `build:watch:` entries -for each UI, and update the `build` script to chain them: - -``` -"build": "tsc --noEmit && npm run build:ui1 && npm run build:ui2" -``` - -### `web/vite.config.ts` - -**CRITICAL: Copy this file EXACTLY. Do NOT refactor, generalize, or -add recursive directory scanning. The flat `outDir: "dist"` and -`output: "${name}.html"` pattern is required — nested output paths -will break the MCP server's UI discovery.** - -```typescript -// Vite configuration for Reboot UIs. -import fs from "fs"; -import path from "path"; -import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; -import { viteSingleFile } from "vite-plugin-singlefile"; - -// Auto-discover UIs from ui/ directory. -const uiDir = path.resolve(__dirname, "ui"); -const uis: Record = - Object.fromEntries( - fs - .readdirSync(uiDir) - .filter((d) => fs.existsSync(path.join(uiDir, d, "index.html"))) - .map((name) => [ - name, - { input: `ui/${name}/index.html`, output: `${name}.html` }, - ]) - ); - -export default defineConfig(({ command, mode }) => { - // Path alias for API imports (@api/... -> ./api/...). - const resolve = { - alias: { - "@api": path.resolve(__dirname, "./api"), - }, - dedupe: ["react", "react-dom", "zod"], - }; - - // Dev server configuration. - // - // UIs use a double iframe architecture: - // MCP Host -> srcdoc (origin=null) -> iframe (origin=localhost:9991) - // - // The inner iframe loads from Envoy ("/__/web/**"), which proxies - // to Vite. Because the inner iframe has a real origin, Vite's URLs - // work normally. `base: "/__/web/"` ensures all paths route through - // Envoy. - // - // Hot Module Replacement works automatically: Vite's client connects - // to the page's origin, and Envoy proxies WebSocket upgrades to - // Vite. This also works with tunnels (ngrok) since the tunnel - // points to Envoy. - if (command === "serve") { - const port = parseInt(process.env.RBT_VITE_PORT || "4444", 10); - - return { - plugins: [react()], - root: ".", - resolve, - base: "/__/web/", - server: { - port, - strictPort: true, - // Listen on all interfaces since requests come through - // Envoy (and tunnels). - host: true, - allowedHosts: true, - }, - }; - } - - // Build mode: `vite build --mode ` - const ui = uis[mode]; - if (!ui) { - const valid = Object.keys(uis).join(", "); - throw new Error(`Unknown UI: ${mode}. Use --mode with: ${valid}`); - } - - return { - plugins: [react(), viteSingleFile()], - build: { - outDir: "dist", - emptyOutDir: false, - assetsInlineLimit: 100000000, - cssCodeSplit: false, - rollupOptions: { - input: ui.input, - output: { - inlineDynamicImports: true, - entryFileNames: ui.output.replace(".html", ".js"), - assetFileNames: ui.output.replace(".html", ".[ext]"), - }, - }, - }, - resolve, - }; -}); -``` - -### `web/tsconfig.json` - -```json -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} -``` - -### `web/tsconfig.app.json` - -```json -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2022", - "useDefineForClassFields": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "baseUrl": ".", - "paths": { - "@api/*": ["./api/*"] - } - }, - "include": ["ui"] -} -``` - -### `web/tsconfig.node.json` - -```json -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2023", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["vite.config.ts"] -} -``` - -### `web/index.css` - -```css -:root { - --color-bg: #1a1a2e; - --color-bg-dark: #0f0f1a; - --color-border: #2d2d4a; - --color-text: #e0e0e0; - --color-text-muted: #888899; - --color-green: #4ade80; - --color-blue: #60a5fa; - --color-yellow: #fbbf24; - --color-pink: #f472b6; - --color-purple: #a78bfa; - --color-orange: #fb923c; - --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, - monospace; -} - -[data-theme="light"] { - --color-bg: #f8f9fa; - --color-bg-dark: #e9ecef; - --color-border: #dee2e6; - --color-text: #212529; - --color-text-muted: #6c757d; - --color-green: #16a34a; - --color-blue: #2563eb; - --color-yellow: #ca8a04; - --color-pink: #db2777; - --color-purple: #7c3aed; - --color-orange: #ea580c; -} - -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - font-family: var(--font-mono); - background: var(--color-bg); - color: var(--color-text); -} -``` - -### `web/ui//index.html` - -```html - - - - - - <UI Title> - - -
- - - -``` - -### `web/ui//main.tsx` - -```tsx -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import { RebootClientProvider } from "@reboot-dev/reboot-react"; -import { ClickerApp } from "./App"; -import "../../index.css"; - -createRoot(document.getElementById("root")!).render( - - - - - -); -``` - -### `web/ui//App.tsx` - -Generated hook usage — `use()` returns reader hooks and -mutation functions. Both reads and writes go directly to the -Reboot backend: - -```tsx -import { useCounter } from "@api//v1/_rbt_react"; - -// useCounter() connects to the Counter state instance. -const counter = useCounter(); - -// Reader (WebSocket subscription, auto-updates): -const { response, isLoading } = counter.useGet(); -const value = response?.value ?? 0; - -// Writer (direct call to Reboot backend): -await counter.increment({ amount: 1 }); -``` - -For list state, the same pattern applies — use the generated -hook for the application type and call its methods: - -```tsx -// Python from_index -> TypeScript fromIndex (camelCase) -await myType.reorderItem({ fromIndex: 0, toIndex: 1 }); -await myType.addItem({ text: "New item" }); -``` - -#### Full Counter App.tsx Example - -```tsx -import { useEffect, useRef, useState, type FC } from "react"; -import { useCounter } from "@api/ai_chat_counter/v1/counter_rbt_react"; -import css from "./App.module.css"; - -export const ClickerApp: FC = () => { - const [isPending, setIsPending] = useState(false); - const counter = useCounter(); - const { response, isLoading } = counter.useGet(); - - const prevValueRef = useRef(null); - const [trend, setTrend] = useState<"up" | "down" | "same" | null>(null); - - const value = response?.value ?? 0; - - useEffect(() => { - if (response?.value !== undefined) { - if (prevValueRef.current !== null) { - if (response.value > prevValueRef.current) { - setTrend("up"); - } else if (response.value < prevValueRef.current) { - setTrend("down"); - } else { - setTrend("same"); - } - } - prevValueRef.current = response.value; - } - }, [response?.value]); - - const handleIncrement = async () => { - setIsPending(true); - try { - await counter.increment({ amount: 1 }); - } finally { - setIsPending(false); - } - }; - - const handleDecrement = async () => { - setIsPending(true); - try { - await counter.decrement({ amount: 1 }); - } finally { - setIsPending(false); - } - }; - - const trendIcon = trend === "up" ? "↑" : trend === "down" ? "↓" : "→"; - const trendClass = - trend === "up" ? css.trendUp : trend === "down" ? css.trendDown : ""; - - if (isLoading && response === undefined) { - return ( -
-
loading...
-
- ); - } - - return ( -
-
- -
-
- {value} -
- {trend && ( - {trendIcon} - )} -
- -
- - syncing... - -
- ); -}; -``` - -### `web/ui//App.module.css` - -```css -.container { - background: var(--color-bg); - color: var(--color-text); - font-family: var(--font-mono); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 24px 20px 16px; - gap: 12px; -} - -.row { - display: flex; - align-items: center; - gap: 12px; -} - -.valueGroup { - display: flex; - align-items: baseline; - gap: 4px; - min-width: 80px; - justify-content: center; -} - -.counter { - font-size: 36px; - font-weight: bold; - color: var(--color-text); - transition: color 0.15s ease, opacity 0.15s ease; -} - -.counter.pending { - opacity: 0.7; -} - -.counter.trendUp { - color: var(--color-green); - text-shadow: 0 0 12px rgba(74, 222, 128, 0.25); -} - -.counter.trendDown { - color: var(--color-pink); - text-shadow: 0 0 12px rgba(244, 114, 182, 0.25); -} - -.trend { - font-size: 18px; - font-weight: bold; - transition: color 0.15s ease; -} - -.trendUp { - color: var(--color-green); -} - -.trendDown { - color: var(--color-pink); -} - -.button { - width: 40px; - height: 40px; - font-size: 20px; - font-family: var(--font-mono); - border: none; - border-radius: 6px; - cursor: pointer; - transition: all 0.15s ease; - display: flex; - align-items: center; - justify-content: center; -} - -.button:disabled { - cursor: not-allowed; - opacity: 0.6; -} - -.buttonIncrement { - composes: button; - background: var(--color-green); - color: var(--color-bg-dark); -} - -.buttonDecrement { - composes: button; - background: var(--color-pink); - color: var(--color-bg-dark); -} - -.syncStatus { - color: var(--color-yellow); - font-size: 11px; - height: 14px; - opacity: 0; - transition: opacity 0.15s ease; -} - -.syncStatus.visible { - opacity: 1; -} - -.loading { - color: var(--color-text-muted); - font-size: 12px; -} -``` - -Adapt the CSS module to your app's needs. The CSS variables from -`index.css` provide consistent theming. - -## Critical Gotchas - -1. **`.rbtrc` is line-based, NOT YAML.** Each line is a command with - flags. Comments start with `#`. -2. **No `__init__.py` in `api/` directories.** The generator scans - all `.py` files; `__init__.py` causes conflicts. -3. **`Field(tag=N)` required on every field.** Tags must be unique - within each Model class. Start at 1. - **Defaults:** State fields must use proto3 zero-value defaults: - `default=0` for int, `default=""` for str, `default=False` for - bool, `default_factory=list` for lists. Non-zero defaults like - `default="red"` or `default=1` are NOT supported (protobuf - limitation). Set initial values in a servicer method marked as - a factory instead. Request/response fields need no default. -4. **Helper Model types are standalone imports:** - `from .v1. import MyItem` — - NOT `Counter.MyItem` (that doesn't exist). -5. **Generated class only has:** `.State`, `.Servicer`, - `.XxxRequest`, `.XxxResponse`. -6. **React bindings use camelCase:** Python `from_index` becomes - TypeScript `fromIndex`. -7. **Every method requires explicit `mcp=`.** Use `mcp=Tool()` - to expose a method as an AI-callable tool (required on all - types, including `User`). Use `mcp=None` to hide it from - the AI. -8. **Application types need `factory=True`** on their `create` - Writer method. -9. **Method call signatures: pass request fields as kwargs, - never wrap in a Request object.** This applies to every - call shape — factory constructors, regular Writers/Readers/ - Transactions, and `.schedule().method()` for workflows. - Right shapes: - - - `Type.constructor_method(context, field=val, ...)` - - `Type.ref(id).some_method(context, field=val, ...)` - - `Type.ref(id).schedule().my_workflow(context, field=val, ...)` - - For empty request types, omit fields entirely: - `Type.ref(id).schedule().autoplay(context)`. - - Wrong: - - - `Type.constructor_method(context, request=Type.ConstructorMethodRequest(...))` - - `Type.ref(id).some_method(context, request=Type.SomeMethodRequest(...))` - - The `request=` kwarg raises `TypeError: ... got an unexpected keyword argument 'request'` at runtime. - -10. **`npm install` before second `rbt generate`** — React bindings - need `node_modules` to exist. -11. **Generated React hook:** `use()` — e.g., - `useCounter()`, `useInventory()`, etc. -12. **Generated React import path:** - `@api//v1/_rbt_react` -13. **Generated Python import path:** - `from .v1._rbt import User, Counter` -14. **Use `--default-config=hmr`** in `.rbtrc` (not `--default=hmr`). -15. **`UI(path="web/ui/")`** — path is relative to project root. -16. **`UI(request=)`** passes config as React component - props. `UI(request=None)` passes no props. -17. **Register all servicers** in `main.py`: - `Application(servicers=[UserServicer, CounterServicer])`. -18. The requests and responses on the frontend are always Zod types - generated from the Python Models. -19. **Inside a Workflow classmethod, `cls` is the servicer, not the - state class.** To call methods on the running instance, use the - state class imported from `_rbt`: - `await MyType.ref().some_method(context)`. A no-arg `.ref()` - inside a Workflow picks up `state_id` from `WorkflowContext` - automatically. **Do NOT write `cls.ref()`** — it fails with - `TypeError: BaseServicer.ref() missing 1 required positional argument: 'self'`, because `ref` on the BaseServicer - is an instance method, not the state-class factory. `self.ref()` - is also wrong because there is no `self` in a classmethod. -20. **Workflows must be scheduled, not awaited, from a - `TransactionContext`/`WriterContext`/`ReaderContext`.** Only - `ExternalContext` and `WorkflowContext` can `await` a workflow - directly. From a transaction that kicks off a workflow on a - state it just created, use `.schedule()`: - `await MyType.ref(id).schedule().autoplay(context)`. - Writing `await MyType.ref(id).autoplay(context)` from a - transaction raises `TypeError: ... '' is a workflow and must be scheduled from a 'TransactionContext' via `await [...].schedule([...]).(context, [...])``. - See the "Scheduling a Workflow from a Transaction" example - in the Workflow Servicer section. -21. **Nested `Model` fields can't take `default_factory` or - `default`.** Two related rules — both raise `UserPydanticError` - at startup, not at field-construction time, so they look like - runtime errors but are static schema problems: - - - `default_factory=` is only supported for `list` and `dict`. - `Field(tag=N, default_factory=MyModel)` raises - `Field in model uses default_factory which is not supported for type . Only list, dict types can have a default_factory currently.` - - A non-Optional `Model`-typed field also can't take - `default=`, even with an instance: - `Field in model is a non-optional Model type and cannot have a default value. Use Optional for Model types with empty default.` - - The fix is to declare the field optional and construct lazily, - e.g. `preferences: Optional[UserPreferences] = Field(tag=N, default=None)`, - then materialize it inside the servicer (or in a factory - `create` method) when the parent state is first written. - -22. **`.per_workflow()` is implicit; don't write it.** Inside a - workflow, `MyType.ref().read(context)` and - `MyType.ref().write(context, fn)` already pick the right - semantics: `.always()` inside an `until` block, - `.per_iteration()` inside a `context.loop`, and - `.per_workflow()` everywhere else. Only reach for an explicit - `.per_iteration()` (override the default to per-iteration when - _not_ inside a loop) or `.always()` (re-run every time). A - plain `MyType.ref().per_workflow().some_method(context)` adds - nothing beyond `MyType.ref().some_method(context)`. -23. **`.read(context)` only works on the workflow's own - no-argument `MyType.ref()`.** Inside a workflow, - `MyType.ref().read(context)` reads the workflow's own state - via the no-argument `ref()` (picks up `state_id` from - `WorkflowContext`). A foreign read like - `OtherType.ref(other_id).read(context)` raises - `RuntimeError: read() is currently only supported within workflows` — the constraint isn't actually "must be inside - a workflow" (you are) but "must be the workflow's own - no-argument ref." For cross-state reads, call a Reader - method on the target type. The same rule applies to inline - `.write(context, fn)`. - - ```python - # GOOD — workflow's own state. - state = await MyType.ref().read(context) - - # GOOD — cross-state read via a Reader method. - response = await User.ref(user_id).get_history(context) - - # BAD — raises the "only supported within workflows" - # RuntimeError despite being inside one. Use a Reader. - # user_state = await User.ref(user_id).read(context) - ``` - -## Update Flow - -When modifying an existing app: - -1. Read `.rbtrc`, API definition, servicer, `main.py` -2. Assess state model changes -3. Update API definition -> re-run `uv run rbt generate` -4. Update servicer methods -5. Update React components -6. `cd web && npm run build` diff --git a/reboot/BUILD.bazel b/reboot/BUILD.bazel index 495c14b0..23ddd731 100644 --- a/reboot/BUILD.bazel +++ b/reboot/BUILD.bazel @@ -447,6 +447,13 @@ py_library( ], ) +py_library( + name = "wait_for_tasks_py", + srcs = ["wait_for_tasks.py"], + srcs_version = "PY3", + visibility = ["//visibility:public"], +) + # PEP 561 marker file; tells tools that type information is available. genrule( name = "py_typed", @@ -466,6 +473,7 @@ compile_pip_requirements( py_library( name = "python_std", deps = [ + "//reboot/std/ciphertext/v1:ciphertext_py", "//reboot/std/collections/ordered_map/v1:ordered_map_py", "//reboot/std/collections/queue/v1:queue_py", "//reboot/std/collections/v1:sorted_map_py", @@ -586,6 +594,7 @@ sh_library( "stage_and_publish_local.sh", ], data = [ + "retag_wheel.py", ":reboot.dev", "//rbt/v1alpha1:reboot-dev-reboot-api", "//reboot/create-ui:reboot-dev-create-ui", @@ -603,6 +612,7 @@ sh_binary( "stage_and_publish_local.sh", ], data = [ + "retag_wheel.py", ":reboot.dev", "//rbt/v1alpha1:reboot-dev-reboot-api", "//reboot/create-ui:reboot-dev-create-ui", diff --git a/reboot/admin/BUILD.bazel b/reboot/admin/BUILD.bazel index e9b88024..c7cd5485 100644 --- a/reboot/admin/BUILD.bazel +++ b/reboot/admin/BUILD.bazel @@ -37,6 +37,7 @@ py_library( visibility = ["//visibility:public"], deps = [ requirement("aiofiles"), + "//reboot/aio:concurrently_py", "//reboot/aio:headers_py", "//reboot/aio:types_py", "@com_github_reboot_dev_reboot//rbt/v1alpha1/admin:export_import_py_grpc", diff --git a/reboot/admin/export_import_client.py b/reboot/admin/export_import_client.py index e2bfb149..ac9d1385 100644 --- a/reboot/admin/export_import_client.py +++ b/reboot/admin/export_import_client.py @@ -1,5 +1,4 @@ import aiofiles -import asyncio import grpc import json import sys @@ -11,6 +10,7 @@ ExportRequest, ListServersRequest, ) +from reboot.aio.concurrently import concurrently from reboot.aio.headers import AUTHORIZATION_HEADER from reboot.aio.types import ServerId from typing import AsyncIterator @@ -22,24 +22,77 @@ def _with_admin_auth_metadata(token: str) -> tuple[tuple[str, str]]: return ((AUTHORIZATION_HEADER, f'Bearer {token}'),) +class _MultilineProgress: + """Maintains one fixed terminal line per label, updated in place. + + On construction, prints one line per label to reserve vertical + space. `update()` uses ANSI cursor movement to overwrite the + correct line without scrolling. `finalize()` prints a trailing + newline so subsequent output starts cleanly. + """ + + def __init__(self, labels: list[str]) -> None: + self._labels_count = len(labels) + for label in labels: + print(f" {label}") + + def update(self, index: int, message: str) -> None: + if self._labels_count == 0: + return + lines_up = self._labels_count - index + sys.stdout.write(f"\033[{lines_up}A\r{message}\033[{lines_up}B") + sys.stdout.flush() + + def finalize(self) -> None: + if self._labels_count > 0: + print() + + async def _export( export_import: export_import_pb2_grpc.ExportImportStub, server_id: ServerId, dest_path: Path, *, admin_token: str, + progress: _MultilineProgress, + progress_index: int, ) -> None: while True: try: async with aiofiles.open(dest_path, 'w') as output: + # We buffer items in `write_buffer` and flush in + # batches to reduce the number of `aiofiles` + # event loop calls. + write_buffer: list[str] = [] + total_exported = 0 + + async def _maybe_write_batch(item_json: str) -> None: + nonlocal total_exported + write_buffer.append(item_json) + # The maximum size of item is the maximum size of a + # Protobuf message, which is 4 MiB by default. + # So a batch of 100 items should be at most ~400 MiB + # in the worst case, however in practice we expect it + # to be much smaller. + if len(write_buffer) >= 100: + total_exported += len(write_buffer) + await output.write('\n'.join(write_buffer) + '\n') + write_buffer.clear() + progress.update( + progress_index, + f" {server_id}: {total_exported} items" + f" exported...", + ) + async for item in export_import.Export( ExportRequest(server_id=server_id), metadata=_with_admin_auth_metadata(admin_token), ): - await output.write( - # NOTE: We use `MessageToDict` followed by `json.dumps` - # because protobuf's JSON encoding always inserts - # newlines, even when `indent=0`. + await _maybe_write_batch( + # NOTE: We use `MessageToDict` followed by + # `json.dumps` because protobuf's JSON + # encoding always inserts newlines, even + # when `indent=0`. json.dumps( json_format.MessageToDict( item, @@ -47,7 +100,15 @@ async def _export( ) ) ) - await output.write("\n") + + # Flush any remaining items in the buffer. + if write_buffer: + total_exported += len(write_buffer) + await output.write('\n'.join(write_buffer) + '\n') + progress.update( + progress_index, + f" {server_id}: {total_exported} items exported.", + ) return except grpc.RpcError as e: if e.code() == grpc.StatusCode.NOT_FOUND: @@ -70,17 +131,19 @@ async def do_export( ListServersRequest(), metadata=_with_admin_auth_metadata(admin_token), ) - # TODO: Throttle the total number of outstanding requests. - await asyncio.gather( - *( - _export( - export_import, - server_id, - dest_directory / f"{server_id}.json", - admin_token=admin_token, - ) for server_id in response.server_ids - ) + server_ids = list(response.server_ids) + progress = _MultilineProgress(server_ids) + await concurrently( + _export( + export_import, + server_id, + dest_directory / f"{server_id}.json", + admin_token=admin_token, + progress=progress, + progress_index=i, + ) for i, server_id in enumerate(server_ids) ) + progress.finalize() async def _import( @@ -88,9 +151,14 @@ async def _import( src_path: Path, *, admin_token: str, + progress: _MultilineProgress, + progress_index: int, ) -> None: + total_imported = 0 + async def import_stream() -> AsyncIterator[ExportImportItem]: + nonlocal total_imported async with aiofiles.open(src_path, 'r') as infile: line_number = 0 async for line in infile: @@ -100,14 +168,22 @@ async def import_stream() -> AsyncIterator[ExportImportItem]: line, ExportImportItem(), ) + total_imported = line_number + if line_number % 100 == 0: + progress.update( + progress_index, + f" {src_path.name}: {line_number} items" + f" sent...", + ) except BaseException as e: - # This exception will NOT bubble up nicely, because gRPC - # swallows exceptions in its RPC streaming request - # generators - users will see a `CancelledError` instead. - # So print some debug information now. + # This exception will NOT bubble up nicely, because + # gRPC swallows exceptions in its RPC streaming + # request generators - users will see a + # `CancelledError` instead. So print some debug + # information now. print( - f"IMPORT ERROR: failed to parse line {line_number} in " - f"'{src_path}': {e}", + f"IMPORT ERROR: failed to parse line" + f" {line_number} in '{src_path}': {e}", file=sys.stderr, ) raise @@ -116,6 +192,10 @@ async def import_stream() -> AsyncIterator[ExportImportItem]: import_stream(), metadata=_with_admin_auth_metadata(admin_token), ) + progress.update( + progress_index, + f" {src_path.name}: {total_imported} items imported.", + ) async def do_import( @@ -125,11 +205,17 @@ async def do_import( admin_token: str, ) -> None: """Import all JSON-lines files in the given directory to the server.""" - # TODO: Throttle the number of requests and/or make a best effort to - # direct items at the "correct" nodes using a placement client. - await asyncio.gather( - *( - _import(export_import, path, admin_token=admin_token) - for path in src_directory.iterdir() - ) + # TODO: Make a best effort to direct items at the "correct" + # nodes using a placement client. + paths = list(src_directory.iterdir()) + progress = _MultilineProgress([path.name for path in paths]) + await concurrently( + _import( + export_import, + path, + admin_token=admin_token, + progress=progress, + progress_index=i, + ) for i, path in enumerate(paths) ) + progress.finalize() diff --git a/reboot/admin/export_import_servicer.py b/reboot/admin/export_import_servicer.py index beaaec78..042a77cb 100644 --- a/reboot/admin/export_import_servicer.py +++ b/reboot/admin/export_import_servicer.py @@ -1,3 +1,4 @@ +import asyncio import grpc from google.protobuf import json_format, struct_pb2 from google.protobuf.message import Message @@ -15,6 +16,8 @@ AdminAuthMixin, auth_metadata_from_metadata, ) +from reboot.aio.concurrently import concurrently +from reboot.aio.headers import SERVER_ID_HEADER from reboot.aio.internals.channel_manager import _ChannelManager from reboot.aio.internals.middleware import Middleware from reboot.aio.placement import PlacementClient @@ -24,7 +27,46 @@ SORTED_MAP_ENTRY_TYPE_NAME, SORTED_MAP_TYPE_NAME, ) -from typing import AsyncIterator +from reboot.wait_for_tasks import wait_for_tasks +from typing import AsyncGenerator, AsyncIterator, Optional + +# Bound for the queues used by `Import()`: both the server-wide local +# queue and the per-remote forwarding queues. A bounded queue makes the +# routing loop block when a consumer falls behind, propagating back- +# pressure through gRPC's HTTP/2 flow control window to the client. +_QUEUE_MAX = 100 + + +def _maybe_parse_if_backwards_compatible( + struct: struct_pb2.Struct, + message: Message, +) -> Message: + """Parse `struct` into `message`, raising a clearer error when the + imported types are not backwards compatible.""" + try: + return json_format.ParseDict( + json_format.MessageToDict( + struct, + preserving_proto_field_name=True, + ), + message, + ) + except json_format.ParseError as error: + raise ValueError( + "Failed to parse import item, is it possible the types " + f"being imported are not backwards compatible? ({error})" + ) + + +async def _drain_until_none( + queue: asyncio.Queue, +) -> AsyncGenerator[ExportImportItem, None]: + """Yield items from `queue` until a `None` item is reached.""" + while True: + item = await queue.get() + if item is None: + return + yield item class ExportImportServicer( @@ -51,6 +93,17 @@ def __init__( self._channel_manager = channel_manager self._serializers = serializers self._middleware_by_state_type_name = middleware_by_state_type_name + # Concurrent `Import()` RPCs on this server do not each start + # their own processing loop; instead they all feed the single + # `_local_queue`, drained by the one loop running in + # `_local_processor`. That loop is created by the first active + # `Import()` RPC and torn down — via a `None` item, which will + # be set as the last element in the queue by the last active + # `Import()` RPC — when the last one finishes. + # `_active_imports` counts how many are in flight. + self._local_queue: Optional[asyncio.Queue] = None + self._local_processor: Optional[asyncio.Task[None]] = None + self._active_imports = 0 def add_to_server(self, server: grpc.aio.Server) -> None: export_import_pb2_grpc.add_ExportImportServicer_to_server(self, server) @@ -137,6 +190,93 @@ async def Export( ), ) + async def _handle_local_item(self, item: ExportImportItem) -> None: + """Apply an import item that belongs to this server directly.""" + state_type_name = StateTypeName(item.state_type) + state_ref = StateRef(item.state_ref) + + active_field_name = item.WhichOneof("item") + if active_field_name == "state": + await self._state_manager.import_actor( + state_type_name, + state_ref, + self._serializers.state_from_struct( + item.state, + state_type_name, + ), + # While importing it is safe to allow concurrent writes + # without additional DB synchronization, in case of + # error, the import should be re-run. + sync=False, + ) + elif active_field_name == "sorted_map_entry": + await self._state_manager.import_sorted_map_entry( + state_type_name, + state_ref, + item.sorted_map_entry, + self._serializers, + # While importing it is safe to allow concurrent writes + # without additional DB synchronization, in case of + # error, the import should be re-run. + sync=False, + ) + elif active_field_name == "task": + middleware = self._middleware_by_state_type_name.get( + state_type_name + ) + if middleware is None: + raise ValueError( + f"Unrecognized state type: {item.state_type!r}" + ) + + await self._state_manager.import_task( + state_type_name, + state_ref, + _maybe_parse_if_backwards_compatible( + item.task, + database_pb2.Task(), + ), + middleware, + # While importing it is safe to allow concurrent writes + # without additional DB synchronization, in case of + # error, the import should be re-run. + sync=False, + ) + else: + assert active_field_name == "idempotent_mutation" + await self._state_manager.import_idempotent_mutation( + state_type_name, + state_ref, + _maybe_parse_if_backwards_compatible( + item.idempotent_mutation, + database_pb2.IdempotentMutation(), + ), + # While importing it is safe to allow concurrent writes + # without additional DB synchronization, in case of + # error, the import should be re-run. + sync=False, + ) + + async def _process_local(self, queue: asyncio.Queue) -> None: + """Apply local import items from `queue`. + + This is the single point at which this server applies imported + state: concurrent `Import()` RPCs all feed `queue`, and this one + loop drains it. It finishes once the `None` item — put by + the last active `Import()` RPC — is dequeued. + """ + await concurrently( + self._handle_local_item, + for_each=_drain_until_none(queue), + # The import path sets `sync=False` so writes are non- + # blocking and items can be processed concurrently. + # `limit=50` was chosen empirically: higher values caused + # write contention on DB writes, lower values left + # throughput on the table. + limit=50, + adaptive=False, + ) + async def Import( self, requests: AsyncIterator[ExportImportItem], @@ -144,109 +284,93 @@ async def Import( ) -> ImportResponse: await self.ensure_admin_auth_or_fail(grpc_context) - async def _remote_iterator( - item: ExportImportItem, - ) -> AsyncIterator[ExportImportItem]: - yield item - - def _maybe_parse_if_backwards_compatible( - struct: struct_pb2.Struct, - message: Message, - ): - try: - return json_format.ParseDict( - json_format.MessageToDict( - struct, - preserving_proto_field_name=True, - ), - message, - ) - except json_format.ParseError as error: - raise ValueError( - "Failed to parse import item, is it possible the types " - f"being imported are not backwards compatible? ({error})" - ) - - async def _handle_item( + async def _forward( server_id: ServerId, - item: ExportImportItem, + queue: asyncio.Queue, ) -> None: - if server_id != self._server_id: - channel = self._channel_manager.get_channel_to( - self._placement_client.address_for_server(server_id) - ) - export_import = export_import_pb2_grpc.ExportImportStub( - channel - ) - await export_import.Import( - _remote_iterator(item), - metadata=auth_metadata_from_metadata(grpc_context), - ) - return - - # This item belongs to this server, so we handle it - # directly. - assert server_id == self._server_id + """Stream all queued items to `server_id` in one RPC.""" + channel = self._channel_manager.get_channel_to( + self._placement_client.address_for_server(server_id) + ) + stub = export_import_pb2_grpc.ExportImportStub(channel) + metadata = auth_metadata_from_metadata(grpc_context) + ( + (SERVER_ID_HEADER, server_id), + ) + await stub.Import( + _drain_until_none(queue), + metadata=metadata, + ) - state_type_name = StateTypeName(item.state_type) - state_ref = StateRef(item.state_ref) + # Join this server's single local-import processing loop, + # starting it if this is the first concurrent `Import()` RPC. We + # capture the queue and task in locals because the shared + # references are cleared by whichever RPC finishes last. + if self._local_queue is None: + self._local_queue = asyncio.Queue(maxsize=_QUEUE_MAX) + self._local_processor = asyncio.create_task( + self._process_local(self._local_queue) + ) + local_queue = self._local_queue + local_processor = self._local_processor + assert local_processor is not None + self._active_imports += 1 - active_field_name = item.WhichOneof("item") - if active_field_name == "state": - await self._state_manager.import_actor( - state_type_name, - state_ref, - self._serializers.state_from_struct( - item.state, - state_type_name, - ), - ) - elif active_field_name == "sorted_map_entry": - await self._state_manager.import_sorted_map_entry( - state_type_name, - state_ref, - item.sorted_map_entry, - self._serializers, + remote_queues: dict[ServerId, asyncio.Queue] = {} + remote_tasks: list[asyncio.Task[None]] = [] + interrupted = True + try: + async for request in requests: + server_id = self._placement_client.server_for_actor( + self._application_id, + StateRef(request.state_ref), ) - elif active_field_name == "task": - middleware = self._middleware_by_state_type_name.get( - state_type_name - ) - if middleware is None: - raise ValueError( - "Unrecognized state type: {item.state_type!r}" - ) + if server_id == self._server_id: + await local_queue.put(request) + else: + if server_id not in remote_queues: + queue: asyncio.Queue = asyncio.Queue( + maxsize=_QUEUE_MAX + ) + remote_queues[server_id] = queue + remote_tasks.append( + asyncio.create_task(_forward(server_id, queue)) + ) + await remote_queues[server_id].put(request) - await self._state_manager.import_task( - state_type_name, - state_ref, - _maybe_parse_if_backwards_compatible( - item.task, - database_pb2.Task(), - ), - middleware, - ) + # Tell this RPC's forwarders no more items are coming. + for queue in remote_queues.values(): + await queue.put(None) + + interrupted = False + finally: + # Always release our slot, even on error or cancellation, so + # a failed `Import()` can't wedge future ones. The last + # active import tears down the shared loop; we clear the + # shared refs FIRST so the next `Import()` starts a fresh + # loop no matter what happens to the old processor. + self._active_imports -= 1 + last = self._active_imports == 0 + if last: + self._local_queue = None + self._local_processor = None + + if interrupted: + # Failed or cancelled: abandon our own forwarders, whose + # streams are incomplete, and if we were the last active + # import stop the now-orphaned processor. The failed + # import is expected to be re-run. + await wait_for_tasks(remote_tasks, cancel=True) + if last: + # Cancel the _this_ server processor to stop it + # immediately. + await wait_for_tasks([local_processor], cancel=True) else: - assert active_field_name == "idempotent_mutation" - await self._state_manager.import_idempotent_mutation( - state_type_name, - state_ref, - _maybe_parse_if_backwards_compatible( - item.idempotent_mutation, - database_pb2.IdempotentMutation(), - ), - ) + # Healthy path. Let the processor drain what we queued. + if last: + await local_queue.put(None) - # Handle each request/item which may require sending it to the - # appropriate server. - # - # TODO: optimize this later if it turns out that we need to - # run this concurrently. - async for request in requests: - server_id = self._placement_client.server_for_actor( - self._application_id, - StateRef(request.state_ref), - ) - await _handle_item(server_id, request) + # Wait for the shared loop to finish applying every item and + # for all forwarding RPCs to complete. + await asyncio.gather(local_processor, *remote_tasks) return ImportResponse() diff --git a/reboot/aio/BUILD.bazel b/reboot/aio/BUILD.bazel index 3c0ee8b3..0b5895ba 100644 --- a/reboot/aio/BUILD.bazel +++ b/reboot/aio/BUILD.bazel @@ -32,11 +32,13 @@ py_library( ":servicers_py", ":tracing_py", ":workflows_py", + "//rbt/v1alpha1/application:application_py_reboot", "//reboot:run_environments_py", "//reboot/aio/auth:oauth_providers_py", "//reboot/aio/auth:oauth_server_py", "//reboot/aio/auth:token_verifiers_py", "//reboot/aio/internals:contextvars_py", + "//reboot/application:application_py", "//reboot/cli:terminal_py", "//reboot/controller:server_managers_py", "//reboot/inspect:servicer_py", @@ -88,6 +90,13 @@ py_library( ], ) +py_library( + name = "concurrently_py", + srcs = ["concurrently.py"], + srcs_version = "PY3", + visibility = ["//visibility:public"], +) + py_library( name = "contexts_py", srcs = ["contexts.py"], @@ -102,6 +111,8 @@ py_library( ":tasks_py", ":types_py", "//reboot:time_py", + "//reboot:uuidv7_py", + "//reboot:wait_for_tasks_py", "//reboot/aio/auth:__init___py", "//reboot/aio/backoff:python", "//reboot/aio/internals:channel_manager_py", @@ -165,6 +176,7 @@ py_library( deps = [ requirement("grpcio"), requirement("grpcio-health-checking"), + ":concurrently_py", ":headers_py", ":types_py", "//reboot/aio/backoff:python", @@ -180,8 +192,10 @@ py_library( deps = [ requirement("fastapi"), requirement("uvicorn"), + ":caller_id_py", ":external_py", ":types_py", + "//reboot:wait_for_tasks_py", "//reboot/aio/internals:channel_manager_py", ], ) @@ -287,6 +301,7 @@ py_library( ":placement_py", ":types_py", "//reboot:settings_py", + "//reboot:wait_for_tasks_py", "//reboot/aio/internals:contextvars_py", "//reboot/aio/internals:middleware_py", "//reboot/nodejs:python_py", @@ -419,6 +434,7 @@ py_library( srcs_version = "PY3", visibility = ["//visibility:public"], deps = [ + ":concurrently_py", ":contexts_py", ":once_py", ":placement_py", @@ -427,6 +443,8 @@ py_library( requirement("bitarray"), ":exceptions_py", "//reboot:time_py", + "//reboot:uuidv7_py", + "//reboot:wait_for_tasks_py", "//reboot/admin:export_import_converters_py", "//reboot/aio/internals:channel_manager_py", "//reboot/aio/internals:middleware_py", @@ -490,6 +508,8 @@ py_library( ":libraries_py", ":reboot_py", ":servicers_py", + "//reboot/aio/auth:oauth_providers_py", + "//reboot/aio/auth:oauth_server_py", ], ) @@ -506,6 +526,7 @@ py_library( ":headers_py", ":once_py", ":signals_py", + "//reboot:run_environments_py", "//reboot:settings_py", "@com_github_reboot_dev_reboot//log:log_py", ], @@ -540,6 +561,7 @@ py_library( ":call_py", ":caller_id_auth_py", ":caller_id_py", + ":concurrently_py", ":contexts_py", ":directories_py", ":exceptions_py", diff --git a/reboot/aio/applications.py b/reboot/aio/applications.py index b6bf0b64..77fc083f 100644 --- a/reboot/aio/applications.py +++ b/reboot/aio/applications.py @@ -5,12 +5,17 @@ import os import reboot.aio.memoize import reboot.aio.workflows +import reboot.application import sys import traceback from log.log import get_logger from mcp.server.fastmcp import FastMCP from pathlib import Path -from reboot.aio.auth.oauth_providers import Anonymous, OAuthProvider +from rbt.v1alpha1.application.application_pb2 import ExamplePrompt +from reboot.aio.auth.oauth_providers import ( + OAuthProviderByEnvironment, + OAuthProviderSelector, +) from reboot.aio.auth.oauth_server import OAuthServer from reboot.aio.auth.token_verifiers import TokenVerifier from reboot.aio.exceptions import InputError @@ -34,6 +39,7 @@ from reboot.run_environments import ( InvalidRunEnvironment, RunEnvironment, + application_name, detect_run_environment, running_rbt_dev, within_nodejs_server, @@ -46,12 +52,12 @@ ENVVAR_RBT_SERVERS, ENVVAR_RBT_STATE_DIRECTORY, ENVVAR_REBOOT_CLOUD_DATABASE_ADDRESS, + ENVVAR_REBOOT_CRYPTO_ROOT_KEYS, ENVVAR_REBOOT_LOCAL_ENVOY, ENVVAR_REBOOT_LOCAL_ENVOY_PORT, - ENVVAR_REBOOT_OAUTH_SIGNING_SECRET, RBT_APPLICATION_EXIT_CODE_BACKWARDS_INCOMPATIBILITY, ) -from typing import Awaitable, Callable, NoReturn, Optional +from typing import Any, Awaitable, Callable, NoReturn, Optional logger = get_logger(__name__) @@ -181,7 +187,10 @@ def __init__( Awaitable[None]]] = None, initialize_bearer_token: Optional[str] = None, token_verifier: Optional[TokenVerifier] = None, - oauth: Optional[OAuthProvider] = None, + oauth: Optional[OAuthProviderSelector] = None, + title: Optional[str] = None, + description: Optional[str] = None, + example_prompts: Optional[list[ExamplePrompt]] = None, ): """ :param servicers: the types of Reboot-powered servicers that @@ -203,10 +212,20 @@ def __init__( :param token_verifier: a TokenVerifier that will be used to verify authorization bearer tokens passed to the application. - :param oauth: an OAuth provider (e.g. `Google` or `GitHub`) for - authenticating MCP clients. When set, the Application - automatically creates a `TokenVerifier` for the minted JWTs. - Mutually exclusive with `token_verifier`. + :param oauth: an `OAuthProviderSelector` (e.g. + `OAuthProviderByEnvironment(dev=Development(), + prod=Google(...))`) that chooses the OAuth provider for + authenticating MCP clients. It is resolved (and a + `TokenVerifier` created automatically) only when the + application has a `User`-typed auto-construct servicer. `None` + is equivalent to `OAuthProviderByEnvironment(dev=None, + prod=None)`. Mutually exclusive with `token_verifier`. + :param title: a human-readable name for the application. + Defaults to `application_name()` if unset. + :param description: a human-readable description of the + application. + :param example_prompts: a list of `ExamplePrompt` instances + for using this application in a chat client. TODO(benh): update the initialize function to be run in a transaction and ensure that the transaction has finished before @@ -221,17 +240,12 @@ def __init__( "`TokenVerifier` is created automatically." ) - # Default to Anonymous OAuth in environments that also have a - # default signing secret (i.e. `rbt dev` and unit tests; these - # are the environments where true security isn't needed yet). - # This lets MCP clients connect without explicit config. In `rbt - # dev`, `_is_dev_default` triggers a warning nudging the - # developer to configure a real provider for production. - if oauth is None and token_verifier is None: - if running_rbt_dev(): - oauth = Anonymous(_is_dev_default=True) - elif os.environ.get(ENVVAR_REBOOT_OAUTH_SIGNING_SECRET): - oauth = Anonymous() + # NOTE: `oauth` is an `OAuthProviderSelector`, resolved lazily in + # `_mount_mcp` only when the app needs a provider to identify + # users (it has a `User`-typed auto-construct servicer and no + # `token_verifier`). `oauth=None` is treated as + # `OAuthProviderByEnvironment(dev=None, prod=None)` there, so an + # app that needs OAuth but never configured one fails to start. # Get all libraries including required dependent libraries. if libraries is not None: @@ -254,7 +268,7 @@ def __init__( if len(needed_requirements) > 0: raise ValueError( "Missing required libraries: " - f"{', '.join(needed_requirements)}" + f"{', '.join(needed_requirements)}. " "Please add these libraries and pass them to the " "`libraries` parameter." ) @@ -324,17 +338,14 @@ def __init__( self._token_verifier = token_verifier self._initialize_bearer_token = initialize_bearer_token self._oauth = oauth + self._title = title + self._description = description + self._example_prompts = example_prompts or [] # Validate MCP configuration eagerly at construction time so # errors are raised consistently regardless of run environment. seen_tools: dict[str, str] = {} for servicer_cls in self._servicers or []: - if servicer_cls._is_auto_construct and oauth is None: - raise ValueError( - "Application includes a `User` auto-constructed state " - "type, which requires OAuth to identify the user. Pass " - "`Application(oauth=...)` - e.g. `oauth=Anonymous()`." - ) for tool_name in servicer_cls._mcp_tool_names(): if tool_name in seen_tools: raise ValueError( @@ -388,8 +399,6 @@ def __init__( # cluster! return - self._name: Optional[str] = os.environ.get(ENVVAR_RBT_NAME) - state_directory: Optional[str] = os.environ.get( ENVVAR_RBT_STATE_DIRECTORY ) @@ -399,8 +408,16 @@ def __init__( # NOTE: we construct a 'Reboot' instance here so that it can # perform any process wide initialization as early as possible. + # + # We pass the raw `ENVVAR_RBT_NAME` value (which may be `None`) + # rather than `application_name()`: `Reboot` relies on a `None` + # name to know that this run is unnamed and therefore may use a + # throwaway temporary state directory. `application_name()` + # falls back to a non-`None` default, which would violate that + # invariant. The human-facing default name is applied to + # `title` below instead. self._rbt = Reboot( - application_name=self._name, + application_name=os.environ.get(ENVVAR_RBT_NAME), state_directory=self._state_directory, # Don't initialize tracing for the 'Reboot' instance, if # we're starting a server. @@ -415,8 +432,12 @@ def libraries(self): @property def servicers(self): - # Always include `memoize` servicers. - return (self._servicers or []) + reboot.aio.memoize.servicers() + # Always include `memoize` servicers and `ApplicationServicer` + # for storing runtime information about the application. + return ( + (self._servicers or []) + reboot.aio.memoize.servicers() + + reboot.application.servicers() + ) @property def legacy_grpc_servicers(self): @@ -459,7 +480,14 @@ def _mount_mcp( connections and explain what's missing. """ auto_construct_state_type_full_names: list[StateTypeName] = [] - new_session_hooks = [] + new_session_hooks: list[Callable[ + [ExternalContext, Optional[str]], + Awaitable[None], + ]] = [] + per_request_hooks: list[Callable[ + [ExternalContext, Optional[str], Any], + Awaitable[None], + ]] = [] # Wire up auto-construction of states for every authenticated user. for servicer_cls in servicers: @@ -482,6 +510,46 @@ async def maybe_auto_construct_based_on_user_id( new_session_hooks.append(maybe_auto_construct_based_on_user_id) + # Record MCP connections made into this application — but only + # in dev mode. + if running_rbt_dev(): + # We dedupe so we can skip the RPC entirely for the + # overwhelmingly-common case of repeated calls on an + # established connection. + seen_connections: set[tuple[str, str]] = set() + + async def record_connection( + context: ExternalContext, + user_id: Optional[str], + request, + ) -> None: + forwarded_host = ( + request.headers.get("x-forwarded-host") or + request.headers.get("host") or None + ) + user_agent = request.headers.get("user-agent") or None + if forwarded_host is None or user_agent is None: + # A connection is only meaningful if we have both + # `forwarded_host` and `user_agent`; skip if + # either is absent. + return + connection = (forwarded_host, user_agent) + if connection in seen_connections: + return + seen_connections.add(connection) + await reboot.application.ref().record_connection( + context, + forwarded_host=forwarded_host, + user_agent=user_agent, + ) + + # NOTE: even though we have session hooks we want to check + # for connections per-request because (a) MCP is dropping + # sessions and (b) after an expunge we will still want to + # track which clients have connected and the only way to + # do that is by looking at each request. + per_request_hooks.append(record_connection) + # Create MCP server and register all servicers' tools/resources. server = FastMCP(name="reboot-mcp") for servicer_cls in servicers: @@ -495,9 +563,29 @@ async def maybe_auto_construct_based_on_user_id( # produce the 401 error code needed to trigger a token refresh. mcp_sdk_token_verifier = None oauth_server = None - if self._oauth is not None: + # Resolve an OAuth provider only when the app needs one to + # identify users — it has a `User`-typed auto-construct servicer + # and isn't already authenticating via a `token_verifier`. The + # selector's `get()` raises if no provider is configured for the + # current environment (`oauth=None` is treated as a selector with + # no provider for any environment). + if ( + auto_construct_state_type_full_names and + self._token_verifier is None + ): + selector = self._oauth or OAuthProviderByEnvironment( + dev=None, + prod=None, + ) + provider = selector.get() + # A provider that stores the identity provider's tokens + # (`store_tokens=True`) persists them via the `oauth` library + # (which encrypts them via `ciphertext`); require the developer + # to have mounted them. + if provider.stores_tokens: + self._require_oauth_libraries() oauth_server = OAuthServer( - provider=self._oauth, + provider=provider, protected_resources=[_MCP_PATH], ) self._token_verifier = oauth_server.token_verifier @@ -513,10 +601,37 @@ async def maybe_auto_construct_based_on_user_id( factory=create_mcp_factory( # type: ignore[arg-type] server=server, new_session_hooks=new_session_hooks, + per_request_hooks=per_request_hooks, token_verifier=mcp_sdk_token_verifier, ), ) + def _require_oauth_libraries(self) -> None: + """Fail fast if an OAuth provider with `store_tokens=True` is used + without the `oauth` (and its `ciphertext`) library mounted — they + encrypt and persist the identity provider's tokens. + """ + # Imported lazily: both libraries import `reboot.aio.applications`, + # so a module-level import would be circular. + from reboot.std.ciphertext.v1.ciphertext import CIPHERTEXT_LIBRARY_NAME + from reboot.std.oauth.v1.oauth import OAUTH_LIBRARY_NAME + names = {library.name for library in (self._libraries or [])} + if OAUTH_LIBRARY_NAME in names and CIPHERTEXT_LIBRARY_NAME in names: + return + raise InputError( + reason=( + "An OAuth provider with `store_tokens=True` needs the " + "`oauth` and `ciphertext` libraries to encrypt and persist " + "the identity provider's tokens, but they aren't all " + "mounted. Add them to your `Application`, e.g. " + "`Application(..., libraries=[oauth_library(), " + "ciphertext_library(), ordered_map_library()])` (import " + "`oauth_library` from `reboot.std.oauth.v1.oauth` and " + "`ciphertext_library` from " + "`reboot.std.ciphertext.v1.ciphertext`)." + ), + ) + @function_span() async def _rbt_start_and_up_and_initialize(self) -> None: assert self._rbt is not None @@ -541,13 +656,34 @@ async def _rbt_start_and_up_and_initialize(self) -> None: 'Expecting server count from `rbt dev` or `rbt serve`' ) + async def initialize(context: InitializeContext) -> None: + # Construct/refresh the `Application` singleton on every + # boot so we get latest values (`title`, `description`, + # etc.), hence `.always()`. An `InitializeContext` is + # app-internal, which is what `Application.initialize`'s + # authorizer requires. + await reboot.application.ref().always().initialize( + context, + title=self._title or application_name(), + description=self._description, + port=local_envoy_port, + mcp=any( + servicer._mcp_tool_names() + for servicer in (self._servicers or []) + ), + example_prompts=self._example_prompts, + ) + + if self._initialize is not None: + await self._initialize(context) + await self._rbt.up( servicers=self.servicers, libraries=self.libraries, legacy_grpc_servicers=self.legacy_grpc_servicers, web_framework=self.web_framework, token_verifier=self.token_verifier, - initialize=self._initialize, + initialize=initialize, initialize_bearer_token=self._initialize_bearer_token, local_envoy=local_envoy, local_envoy_port=local_envoy_port, @@ -573,6 +709,25 @@ def _get_serviceables(self) -> list[Serviceable]: return serviceables + def _require_crypto_root_keys(self) -> None: + """Fail fast (clean exit, no stack trace) on a serving path if the + Reboot-managed cryptographic root keys aren't set. Every serving + Reboot application needs them: libraries derive keys from them (the + MCP OAuth server its JWT signing key, `reboot.std.ciphertext` its + key-encryption key, ...), and they must be identical across all + server processes. Config pods (`REBOOT_MODE_CONFIG`) only validate + configuration and never serve, so they never call this. + """ + if not os.environ.get(ENVVAR_REBOOT_CRYPTO_ROOT_KEYS): + terminal.fail( + f"The '{ENVVAR_REBOOT_CRYPTO_ROOT_KEYS}' environment " + "variable is not set. Every Reboot application needs " + "cryptographic root keys, shared across all of its " + "servers. Under `rbt dev` and on Reboot Cloud they are " + "set automatically; for `rbt serve` you must set an " + "environment variable yourself" + ) + async def run(self) -> NoReturn: """Runs the application, and does not return unless the application fails. @@ -585,7 +740,9 @@ async def run(self) -> NoReturn: colorama.init() if within_python_server(): - # We're running as a Python server subprocess. + # We're running as a Python server subprocess — a serving + # path, so it requires the OAuth signing secret. + self._require_crypto_root_keys() try: await run_python_server_process( serviceables=self._get_serviceables(), @@ -662,7 +819,9 @@ async def run(self) -> NoReturn: sys.exit(1) # We're going to run a normal Reboot application with one or - # more serving servers. + # more serving servers — a serving path, so it requires the + # OAuth signing secret. + self._require_crypto_root_keys() try: await self._rbt_start_and_up_and_initialize() except InputError as e: diff --git a/reboot/aio/auth/BUILD.bazel b/reboot/aio/auth/BUILD.bazel index a5f373fe..31c2a3b3 100644 --- a/reboot/aio/auth/BUILD.bazel +++ b/reboot/aio/auth/BUILD.bazel @@ -53,11 +53,18 @@ py_library( py_library( name = "oauth_providers_py", srcs = ["oauth_providers.py"], + data = ["development_login_page.html.j2"], srcs_version = "PY3", visibility = ["//visibility:public"], deps = [ + "//reboot:run_environments_py", + "//reboot/aio:exceptions_py", + "//reboot/aio:http_py", + "//reboot/crypto:root_keys_py", "@com_github_reboot_dev_reboot//log:log_py", + "@com_github_reboot_dev_reboot//rbt/std/oauth/v1:oauth_py_reboot", requirement("aiohttp"), + requirement("jinja2"), requirement("pyjwt"), requirement("python-ulid"), ], @@ -73,6 +80,9 @@ py_library( ":oauth_providers_py", ":token_verifiers_py", "//reboot:settings_py", + "//reboot/aio:http_py", + "//reboot/crypto:root_keys_py", + "@com_github_reboot_dev_reboot//rbt/std/oauth/v1:oauth_py_reboot", requirement("pyjwt"), requirement("starlette"), ], diff --git a/reboot/aio/auth/__init__.py b/reboot/aio/auth/__init__.py index 8f607d6f..f3e821b3 100644 --- a/reboot/aio/auth/__init__.py +++ b/reboot/aio/auth/__init__.py @@ -3,7 +3,31 @@ from dataclasses import dataclass, field from google.protobuf import json_format, struct_pb2 from rbt.v1alpha1 import auth_pb2 -from typing import Any, Optional +from typing import TYPE_CHECKING, Any, Optional + +if TYPE_CHECKING: + # For type checkers only; the runtime re-export is lazy (see + # `__getattr__` below) to avoid an import cycle — `OAuthTokenManager` + # pulls in the `oauth` library, which imports `reboot.aio.applications`, + # which imports this package. + from rbt.std.oauth.v1.oauth_rbt import OAuthTokenManager + from reboot.aio.auth.oauth_providers import OAuthTokens + +__all__ = ["Auth", "OAuthTokens", "OAuthTokenManager"] + + +def __getattr__(name: str) -> Any: + """Lazily re-export the OAuth token public API so + `from reboot.aio.auth import OAuthTokenManager` works without importing + the (heavy, cycle-prone) reboot machinery at package import time. + """ + if name == "OAuthTokenManager": + from rbt.std.oauth.v1.oauth_rbt import OAuthTokenManager + return OAuthTokenManager + if name == "OAuthTokens": + from reboot.aio.auth.oauth_providers import OAuthTokens + return OAuthTokens + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") @dataclass(frozen=True, kw_only=True) diff --git a/reboot/aio/auth/development_login_page.html.j2 b/reboot/aio/auth/development_login_page.html.j2 new file mode 100644 index 00000000..a8a91f8c --- /dev/null +++ b/reboot/aio/auth/development_login_page.html.j2 @@ -0,0 +1,133 @@ + + + + + +Who do you want to be? + + + + + + +
+ +

Who do you want to be?

+

pick an identity to log in as

+ {% for account in accounts %} + + {% endfor %} + +
+ + diff --git a/reboot/aio/auth/oauth_providers.py b/reboot/aio/auth/oauth_providers.py index 784ac9ed..e05613e2 100644 --- a/reboot/aio/auth/oauth_providers.py +++ b/reboot/aio/auth/oauth_providers.py @@ -3,12 +3,25 @@ from __future__ import annotations import aiohttp +import hashlib +import hmac import jwt +import os from abc import ABC, abstractmethod +from dataclasses import dataclass +from datetime import datetime, timezone +from jinja2 import Template from log.log import get_logger, log_at_most_once_per -from typing import NewType +from rbt.std.oauth.v1.oauth_rbt import OAuthTokens +from reboot.aio.exceptions import InputError +from reboot.aio.http import PythonWebFramework +from reboot.crypto import root_keys +from reboot.run_environments import running_rbt_dev +from starlette.requests import Request +from starlette.responses import HTMLResponse +from typing import Any, NewType, Optional from ulid import ULID -from urllib.parse import urlencode +from urllib.parse import urlencode, urlparse, urlunparse logger = get_logger(__name__) @@ -17,6 +30,58 @@ _DEFAULT_ACCESS_TOKEN_TTL_SECONDS = 24 * 60 * 60 # 24 hours. +@dataclass(frozen=True) +class ExchangeResult: + """The result of exchanging an identity provider authorization code: + the resolved user ID and, when the provider captures them, the + provider's tokens. + """ + user_id: UserId + # The provider's tokens, present only when the provider was + # constructed with `store_tokens=True`; `None` otherwise, so apps + # that don't opt in never carry these secrets around. + tokens: Optional[OAuthTokens] + + +def _expires_at_from_expires_in(expires_in: Any) -> Optional[int]: + """Convert a token endpoint's `expires_in` (seconds from now) into an + absolute epoch-seconds expiry, or `None` if absent or unparsable. + """ + try: + now = int(datetime.now(timezone.utc).timestamp()) + return now + int(expires_in) + except (TypeError, ValueError): + return None + + +def _scopes_from_response(granted: Any, requested: list[str]) -> list[str]: + """Parse the granted `scope` string from a token response, falling + back to the scopes we requested when the provider doesn't echo + them. + """ + if isinstance(granted, str) and granted.strip(): + return granted.split() + return requested + + +def origin_from_request(request: Request) -> str: + """ + Derive the server's origin (`scheme://host`) from request headers. + + Respects `X-Forwarded-Proto` and `X-Forwarded-Host` when behind a + reverse proxy. + """ + scheme = request.headers.get( + "x-forwarded-proto", + request.url.scheme, + ) + host = request.headers.get( + "x-forwarded-host", + request.headers.get("host", "localhost"), + ) + return f"{scheme}://{host}" + + class OAuthProvider(ABC): """ Base class for identity providers. @@ -54,9 +119,10 @@ async def exchange_code( self, code: str, redirect_uri: str, - ) -> UserId: - """Exchange an identity provider authorization code for - a user ID. + ) -> ExchangeResult: + """Exchange an identity provider authorization code for a user + ID (and, for providers that capture them, the provider's + tokens). Args: code: The authorization code received from the @@ -67,28 +133,109 @@ async def exchange_code( """ raise NotImplementedError() + @property + def stores_tokens(self) -> bool: + """Whether this provider captures and persists the identity + provider's tokens. `False` by default; providers that support it + flip this on when constructed with `store_tokens=True`. + """ + return False + + @property + def token_service_id(self) -> str: + """The `OAuthTokenManager` state id this provider's tokens are + stored under — names the external third-party service (e.g. + "google.com"), so application code can read them back via + `OAuthTokenManager.ref(...)`. Providers that set `stores_tokens` + must override this. + """ + raise NotImplementedError() + + def mount_routes(self, http: PythonWebFramework.HTTP) -> None: + """ + Optional hook to register provider-specific HTTP routes. + + Default: no extra routes. + """ + + def validate(self) -> None: + """ + Optional hook to verify the provider's configuration. + + Called when this provider will be used in this process (i.e. + it's been selected). Subclasses override to fail fast on missing + or invalid configuration; the default is a no-op. + """ + class RegisteredOAuthProvider(OAuthProvider): """ Base class for providers that use pre-registered client credentials. - + These providers require developers to go through some manual registration flow once, which produces a `client_id` and `client_secret` that we need to know. """ - def __init__(self, *, client_id: str, client_secret: str): + # The scope this provider always requests, on top of any the + # developer adds, because identity resolution depends on it (e.g. + # `openid` for Google, `read:user` for GitHub). Subclasses set this. + _REQUIRED_SCOPE: str = "" + + def __init__( + self, + *, + # `client_id`/`client_secret` are typed `Optional` so a + # production-only provider can still be constructed in dev + # processes where it is present but won't be used. + client_id: Optional[str], + client_secret: Optional[str], + # Extra OAuth scopes to request on top of `_REQUIRED_SCOPE`, so + # the app can call the provider's API on the user's behalf. + scopes: Optional[list[str]] = None, + # When `True`, capture the provider's access/refresh tokens + # during the code exchange and persist them (encrypted) so the + # app can use them later to call the provider's API. Off by + # default — opting in requires the `ciphertext` library and + # `REBOOT_CRYPTO_ROOT_KEYS`. + store_tokens: bool = False, + ): + super().__init__() - if not client_id: - raise ValueError( - f"{type(self).__name__} requires a non-empty `client_id`." - ) - if not client_secret: - raise ValueError( - f"{type(self).__name__} requires a non-empty `client_secret`." - ) self._client_id = client_id self._client_secret = client_secret + self._extra_scopes = scopes or [] + self._store_tokens = store_tokens + + @property + def stores_tokens(self) -> bool: + return self._store_tokens + + def _requested_scopes(self) -> list[str]: + """The full scope list: the required base scope plus the + developer's extras (deduplicated, base scope first). + """ + scopes = [self._REQUIRED_SCOPE] + for scope in self._extra_scopes: + if scope not in scopes: + scopes.append(scope) + return scopes + + def validate(self) -> None: + if not self._client_id: + raise InputError( + reason=( + f"{type(self).__name__} requires a non-empty " + "`client_id`." + ), + ) + if not self._client_secret: + raise InputError( + reason=( + f"{type(self).__name__} requires a non-empty " + "`client_secret`." + ), + ) class Google(RegisteredOAuthProvider): @@ -101,6 +248,15 @@ class Google(RegisteredOAuthProvider): _AUTHORIZATION_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth" _TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token" + # `openid` is the minimum OIDC scope; gives us an ID token with the + # `sub` claim (the user's ID). + _REQUIRED_SCOPE = "openid" + + @property + def token_service_id(self) -> str: + # Mirrors `reboot.std.oauth.v1.oauth.GOOGLE`. + return "google.com" + def authorization_url( self, state: str, @@ -110,10 +266,13 @@ def authorization_url( "client_id": self._client_id, "redirect_uri": redirect_uri, "response_type": "code", - # `openid` is the minimum OIDC scope; gives us an ID token - # with the `sub` claim (the user's ID). - "scope": "openid", + "scope": " ".join(self._requested_scopes()), "state": state, + # `offline` asks Google for a refresh token. Google only + # returns one on the *first* consent (not on later sign-ins), + # so the token store carries the existing refresh token + # forward rather than us forcing the consent screen every + # time with `prompt=consent` (see `OAuthServer._store_oauth_tokens`). "access_type": "offline", } return f"{self._AUTHORIZATION_ENDPOINT}?{urlencode(params)}" @@ -122,9 +281,9 @@ async def exchange_code( self, code: str, redirect_uri: str, - ) -> UserId: + ) -> ExchangeResult: """ - Exchange the Google auth code for user ID. + Exchange the Google auth code for a user ID (and tokens). """ data = { "code": code, @@ -171,18 +330,72 @@ async def exchange_code( user_id = decoded.get("sub") if user_id is None: raise ValueError("Google ID token did not contain a 'sub' claim.") - return UserId(user_id) + return ExchangeResult( + user_id=UserId(user_id), + tokens=self._tokens_from_response(token_response), + ) + + def _tokens_from_response( + self, + token_response: dict, + ) -> Optional[OAuthTokens]: + """Build an `OAuthTokens` from a Google token endpoint response, + or `None` when token storage isn't enabled or no access token came + back. + """ + if not self._store_tokens: + return None + access_token = token_response.get("access_token") + if access_token is None: + return None + return OAuthTokens( + access_token=access_token, + refresh_token=token_response.get("refresh_token"), + expires_at=_expires_at_from_expires_in( + token_response.get("expires_in") + ), + scopes=_scopes_from_response( + token_response.get("scope"), + self._requested_scopes(), + ), + ) class GitHub(RegisteredOAuthProvider): """ GitHub OAuth provider (plain OAuth 2.0). + + Note on refresh tokens (relevant only with `store_tokens=True`): + whether GitHub issues a `refresh_token` is entirely a property of how + the credentials were created, not anything we can request: + + - A classic **OAuth App** never issues a refresh token. Its access + token simply does not expire (so there is nothing to refresh, and + `OAuthTokens.refresh_token` / `expires_at` come back unset). + - A **GitHub App** issues a `refresh_token` (and an expiring access + token) only when "Expire user authorization tokens" is enabled in + the app's settings. With that opt-out left disabled, its + user-to-server token behaves like the OAuth App's — no refresh + token. + + So to get refresh tokens, register a GitHub App and enable expiring + user tokens; `client_id`/`client_secret` are then that app's + credentials. We store whatever the token endpoint returns either way. """ _AUTHORIZATION_ENDPOINT = "https://github.com/login/oauth/authorize" _TOKEN_ENDPOINT = "https://github.com/login/oauth/access_token" _USER_API = "https://api.github.com/user" + # `read:user`: minimum scope needed to call `GET /user` and obtain + # the numeric user ID. + _REQUIRED_SCOPE = "read:user" + + @property + def token_service_id(self) -> str: + # Mirrors `reboot.std.oauth.v1.oauth.GITHUB`. + return "github.com" + def authorization_url( self, state: str, @@ -191,9 +404,7 @@ def authorization_url( params = { "client_id": self._client_id, "redirect_uri": redirect_uri, - # `read:user`: minimum scope needed to call `GET /user` and - # obtain the numeric user ID. - "scope": "read:user", + "scope": " ".join(self._requested_scopes()), "state": state, } return f"{self._AUTHORIZATION_ENDPOINT}?{urlencode(params)}" @@ -202,9 +413,9 @@ async def exchange_code( self, code: str, redirect_uri: str, - ) -> UserId: + ) -> ExchangeResult: """ - Exchange the GitHub auth code for user ID. + Exchange the GitHub auth code for a user ID (and tokens). """ # Since GitHub isn't an OpenID provider (only plain OAuth), we # must first POST to the token endpoint for an access token, @@ -248,7 +459,249 @@ async def exchange_code( user_id = user_info.get("id") if user_id is None: raise ValueError("GitHub user API did not return an 'id' field.") - return UserId(str(user_id)) + return ExchangeResult( + user_id=UserId(str(user_id)), + tokens=self._tokens_from_response(token_response), + ) + + def _tokens_from_response( + self, + token_response: dict, + ) -> Optional[OAuthTokens]: + """Build an `OAuthTokens` from a GitHub token endpoint response, + or `None` when token storage isn't enabled. Classic GitHub OAuth + App tokens don't expire and carry no refresh token; GitHub Apps + issue expiring tokens with a `refresh_token` and `expires_in`. + """ + if not self._store_tokens: + return None + access_token = token_response.get("access_token") + if access_token is None: + return None + return OAuthTokens( + access_token=access_token, + refresh_token=token_response.get("refresh_token"), + expires_at=_expires_at_from_expires_in( + token_response.get("expires_in") + ), + scopes=_scopes_from_response( + token_response.get("scope"), + self._requested_scopes(), + ), + ) + + +class Auth0(RegisteredOAuthProvider): + """ + Auth0 OAuth provider (OpenID Connect). + + Auth0 is an identity broker: a single Auth0 application can let + users sign in through any of the connections you enable in the + Auth0 dashboard (Google, GitHub, username/password, enterprise + SSO, …). To the app it's one OIDC provider — the per-user choice + of login method happens upstream, in Auth0, not here. + + All endpoints live under your tenant domain (e.g. + `your-tenant.us.auth0.com`), so unlike `Google`/`GitHub` this + provider takes a `domain`. Register a "Regular Web Application" in + Auth0 and add Reboot's callback (`/__/oauth/callback`) to its + allowed callback URLs; the app's client ID/secret are the + `client_id`/`client_secret` here. + + Obtains the user ID from the OIDC ID token's `sub` claim (shaped + like `|`, e.g. `google-oauth2|108…`), falling back + to the `/userinfo` endpoint if no ID token is returned. + """ + + # `openid` is the minimum OIDC scope; yields an ID token with the + # `sub` claim (the user's Auth0 ID). Add `profile`/`email` via + # `scopes=` if the app needs them. + _REQUIRED_SCOPE = "openid" + + # Auth0 grants a refresh token only when `offline_access` is + # requested (the analogue of Google's `access_type=offline`); see + # `authorization_url`. + _OFFLINE_ACCESS_SCOPE = "offline_access" + + def __init__( + self, + *, + # The Auth0 tenant domain, e.g. `your-tenant.us.auth0.com`. A + # full `https://...` URL or a trailing slash is tolerated. + domain: Optional[str], + client_id: Optional[str], + client_secret: Optional[str], + scopes: Optional[list[str]] = None, + store_tokens: bool = False, + ): + super().__init__( + client_id=client_id, + client_secret=client_secret, + scopes=scopes, + store_tokens=store_tokens, + ) + self._domain = self._normalize_domain(domain) + + @staticmethod + def _normalize_domain(domain: Optional[str]) -> str: + """Strip a leading scheme and any trailing slash from an Auth0 + `domain` so both `your-tenant.auth0.com` and the full + `https://your-tenant.auth0.com/` form work; returns `""` for a + missing domain (caught later by `validate()`). + """ + if not domain: + return "" + parsed = urlparse(domain) + # `urlparse("tenant.auth0.com")` puts everything in `path` (no + # scheme), whereas `urlparse("https://tenant.auth0.com")` puts it + # in `netloc`; take whichever is populated. + host = parsed.netloc or parsed.path + return host.strip("/") + + @staticmethod + def _sub_from_id_token(id_token: Optional[str]) -> Optional[str]: + """Decode an OIDC ID token and return its `sub` claim, or `None` + if there's no token or no `sub`. + + Decodes without verifying the signature: we received this token + directly from the provider's token endpoint over TLS, so the + transport guarantees authenticity (the same reasoning `Google` + applies). We don't restrict `algorithms` because the signature + isn't checked and Auth0 tenants may sign with RS256 or HS256. + """ + if not id_token: + return None + decoded = jwt.decode(id_token, options={"verify_signature": False}) + return decoded.get("sub") + + @property + def _authorization_endpoint(self) -> str: + return f"https://{self._domain}/authorize" + + @property + def _token_endpoint(self) -> str: + return f"https://{self._domain}/oauth/token" + + @property + def _userinfo_endpoint(self) -> str: + return f"https://{self._domain}/userinfo" + + @property + def token_service_id(self) -> str: + # Auth0 is a per-tenant identity broker, so the tenant domain + # (e.g. "your-tenant.us.auth0.com") names the service. + return self._domain + + def validate(self) -> None: + # Validate the credentials the base class knows about, then the + # `domain` that's unique to Auth0 — same fail-fast contract, so + # a missing `AUTH0_DOMAIN` is caught at selection time, not + # mid-sign-in. + super().validate() + if not self._domain: + raise InputError( + reason="Auth0 requires a non-empty `domain`.", + ) + + def authorization_url( + self, + state: str, + redirect_uri: str, + ) -> str: + scopes = self._requested_scopes() + # Auth0 asks for refresh tokens via the `offline_access` scope + # (where Google uses `access_type=offline`). Only request it + # when we're actually capturing tokens, to avoid prompting the + # user for offline access we'd never use. + if self._store_tokens and self._OFFLINE_ACCESS_SCOPE not in scopes: + scopes = scopes + [self._OFFLINE_ACCESS_SCOPE] + params = { + "client_id": self._client_id, + "redirect_uri": redirect_uri, + "response_type": "code", + "scope": " ".join(scopes), + "state": state, + } + return f"{self._authorization_endpoint}?{urlencode(params)}" + + async def exchange_code( + self, + code: str, + redirect_uri: str, + ) -> ExchangeResult: + """ + Exchange the Auth0 auth code for a user ID (and tokens). + """ + data = { + "grant_type": "authorization_code", + "client_id": self._client_id, + "client_secret": self._client_secret, + "code": code, + "redirect_uri": redirect_uri, + } + async with aiohttp.ClientSession() as session: + async with session.post( + self._token_endpoint, + data=data, + ) as response: + response.raise_for_status() + token_response = await response.json() + + user_id = self._sub_from_id_token(token_response.get("id_token")) + if user_id is None: + # No ID token (shouldn't happen with the `openid` + # scope, but be defensive): fall back to `/userinfo`, + # which returns the same `sub` for the access token. + access_token = token_response.get("access_token") + if access_token is None: + raise ValueError( + "Auth0 token response contained neither an " + "'id_token' nor an 'access_token'; cannot " + "resolve the user's identity." + ) + async with session.get( + self._userinfo_endpoint, + headers={"Authorization": f"Bearer {access_token}"}, + ) as response: + response.raise_for_status() + user_info = await response.json() + user_id = user_info.get("sub") + + if user_id is None: + raise ValueError( + "Auth0 did not return a 'sub' claim; cannot resolve " + "the user's identity." + ) + return ExchangeResult( + user_id=UserId(user_id), + tokens=self._tokens_from_response(token_response), + ) + + def _tokens_from_response( + self, + token_response: dict, + ) -> Optional[OAuthTokens]: + """Build an `OAuthTokens` from an Auth0 token endpoint response, + or `None` when token storage isn't enabled or no access token came + back. Auth0 only returns a `refresh_token` when `offline_access` + was requested (see `authorization_url`). + """ + if not self._store_tokens: + return None + access_token = token_response.get("access_token") + if access_token is None: + return None + return OAuthTokens( + access_token=access_token, + refresh_token=token_response.get("refresh_token"), + expires_at=_expires_at_from_expires_in( + token_response.get("expires_in") + ), + scopes=_scopes_from_response( + token_response.get("scope"), + self._requested_scopes(), + ), + ) class Anonymous(OAuthProvider): @@ -284,7 +737,7 @@ async def exchange_code( self, code: str, redirect_uri: str, - ) -> UserId: + ) -> ExchangeResult: """Generate a fresh anonymous user ID.""" if self._is_dev_default: log_at_most_once_per( @@ -300,4 +753,278 @@ async def exchange_code( # base32 encoding is shorter and easier on the human eye — # relevant here because anonymous user IDs are likely to be seen # by humans. - return UserId(f"anon-{ULID()}") + return ExchangeResult(user_id=UserId(f"anon-{ULID()}"), tokens=None) + + +# The five fake identities offered by the `Development` login page. We +# deliberately keep this a small, hardcoded set: it's for local +# development only. +_DEVELOPMENT_IDENTITIES: tuple[str, ...] = ( + "Alice", + "Ben", + "Carlos", + "Dani", + "Esi", +) + +# HKDF `info` (domain separator) for the key that derives opaque +# development user ids. Distinct from the OAuth signing key: this is a +# separate purpose, so it gets its own root-derived key. +_DEV_USER_ID_INFO = b"reboot.oauth.dev-user-id" + +# Avatar colors for the login page, drawn from the Reboot brand palette. +# Cycled per identity so each account is visually distinct. +_DEVELOPMENT_AVATAR_COLORS: tuple[str, ...] = ( + "#103761", + "#266AB2", + "#3285DE", + "#007367", + "#A39382", +) + +# Path of the `Development` login page's HTTP route. Prefixed with +# `__/oauth/` to match the other OAuth endpoints and to avoid colliding +# with developer-specified routes. +_DEVELOPMENT_LOGIN_PATH = "/__/oauth/dev-login" + +_DEVELOPMENT_LOGIN_PAGE_TEMPLATE_PATH = os.path.join( + os.path.dirname(__file__), + "development_login_page.html.j2", +) + + +class Development(OAuthProvider): + """ + Development provider — a fake "pick an account" login page. + + Shows the developer a Google-style account picker with a handful of + hardcoded fake identities (no passwords); whichever they pick + becomes their identity. Unlike `Anonymous` the developer can pick + the same identity twice, logging them in under the *same* user ID, + so multi-user, multi-client, and returning-user behavior can be + exercised locally. + + For local development only; do not use in production. + """ + + def __init__( + self, + *, + access_token_ttl_seconds: int = _DEFAULT_ACCESS_TOKEN_TTL_SECONDS, + ): + super().__init__(access_token_ttl_seconds=access_token_ttl_seconds) + self._login_page_template: Optional[Template] = None + + def mount_routes(self, http: PythonWebFramework.HTTP) -> None: + # Read and compile the login page template now, rather than at + # module import or `__init__`, so only apps that actually use + # `Development()` pay for it. + with open(_DEVELOPMENT_LOGIN_PAGE_TEMPLATE_PATH) as template_file: + self._login_page_template = Template( + template_file.read(), + # Make interpolated values (notably the per-identity + # links built from the `state` JWT and callback URI) + # safe in HTML/attribute context. + autoescape=True, + ) + + http.get(_DEVELOPMENT_LOGIN_PATH)(self._dev_login) + + def authorization_url( + self, + state: str, + redirect_uri: str, + ) -> str: + # Redirect the browser to our own login page, carrying the OAuth + # `state` and the server's callback URI (passed in as + # `redirect_uri`) so the page can build links straight to the + # callback. We build an absolute URL by reusing the scheme and + # host of the callback URI, so we stay correct behind a reverse + # proxy without needing to know the callback path here. + parsed = urlparse(redirect_uri) + return urlunparse( + parsed._replace( + path=_DEVELOPMENT_LOGIN_PATH, + query=urlencode( + { + "state": state, + "redirect_uri": redirect_uri, + } + ), + ) + ) + + async def _dev_login(self, request: Request) -> HTMLResponse: + """Render the fake account-picker login page.""" + log_at_most_once_per( + seconds=60, + log_method=logger.warning, + message=( + "*** Using Development OAuth. *** This shows a fake " + "account picker for local development only; do not use " + "`Development()` in production." + ), + ) + # The callback URI (where each identity link points) and the + # signed `state` JWT to echo back to it. + callback_uri = request.query_params.get("redirect_uri", "") + state = request.query_params.get("state", "") + + # `callback_uri` is reflected into every per-identity `href` + # below. In the normal flow it's this server's own OAuth + # callback, but the endpoint is reachable directly, so a crafted + # `redirect_uri` could turn this page into an XSS (e.g. a + # `javascript:` scheme — HTML autoescaping does NOT neutralize a + # dangerous URL scheme) or open-redirect gadget. Require it to be + # same-origin over http(s); reject anything else with a 400. The + # legit callback is always built from `origin_from_request`, so + # the legitimate flow always passes. + parsed_callback = urlparse(callback_uri) + if ( + parsed_callback.scheme not in ("http", "https") or + f"{parsed_callback.scheme}://{parsed_callback.netloc}" + != origin_from_request(request) + ): + return HTMLResponse( + "Invalid `redirect_uri`: must be same-origin.", + status_code=400, + ) + + accounts: list[dict[str, str]] = [] + for index, name in enumerate(_DEVELOPMENT_IDENTITIES): + # Build the link straight to the OAuth callback. Use + # `urlencode` (never hand-concatenate the JWT `state`); the + # template autoescapes it into the `href` attribute. + query = urlencode({"code": name, "state": state}) + color = _DEVELOPMENT_AVATAR_COLORS[index % + len(_DEVELOPMENT_AVATAR_COLORS)] + accounts.append( + { + "name": name, + "email": f"{name.lower()}@example.com", + "initial": name[0], + "href": f"{callback_uri}?{query}", + "color": color, + } + ) + # Invariant: `mount_routes` is inherently called before any HTTP + # request is served, so the login page template has + # been loaded. + assert self._login_page_template is not None + return HTMLResponse( + self._login_page_template.render(accounts=accounts) + ) + + async def exchange_code( + self, + code: str, + redirect_uri: str, + ) -> ExchangeResult: + """Derive a stable, per-app user ID for the chosen identity.""" + if code not in _DEVELOPMENT_IDENTITIES: + # Only our own login page produces these codes; anything else + # is bogus. The OAuth server turns this into a graceful + # `access_denied` redirect. + raise ValueError(f"Unknown development identity: {code!r}") + # Key the HMAC by a per-app key derived from the root keys so + # the ID is opaque and differs per app — deliberately NOT + # `dev-{name}`, so nobody hardcodes user IDs into authorization + # logic or tests and is instead pushed toward looking up the + # authenticated user ID at runtime. + # + # We derive from the active root key version, so this ID + # changes if the root keys rotate. That is fine since this is + # dev-only (in production the user subject comes from the IdP, + # not this HMAC), and currently a "rotation" only occurs from + # an expunge, which means all of the user's data has also been + # deleted, so getting a new ID is just fine. + digest = hmac.new( + root_keys.derive_key( + info=_DEV_USER_ID_INFO, + version=root_keys.active_version(), + ), + code.encode(), + hashlib.sha256, + ).hexdigest() + return ExchangeResult( + user_id=UserId(f"dev-{digest[:16]}"), + tokens=None, + ) + + +# Message raised when a selector has no provider for the current +# environment. +_NO_PROVIDER_REASON = ( + "No OAuth provider is configured for this environment. Pass " + "`Application(oauth=OAuthProviderByEnvironment(dev=..., prod=...))` " + "with a provider for the current environment — e.g. `Development()` " + "for local `rbt dev`, and `Google(...)` / `GitHub(...)` for " + "production." +) + + +class OAuthProviderSelector(ABC): + """ + Chooses the `OAuthProvider` an `Application` should use. + + The selection is resolved lazily, via `get()`, so that the choice + can be made based on the environment, and only when the application + actually needs a provider to identify users (i.e. it has a + `User`-typed auto-construct servicer). + """ + + def get(self) -> OAuthProvider: + """ + Return the `OAuthProvider` to use, or raise if none is + configured for the current environment. + """ + provider = self._select() + # The provider has been selected; validate it before anyone + # tries to actually use it. + provider.validate() + return provider + + @abstractmethod + def _select(self) -> OAuthProvider: + """ + Choose the provider for the current environment. + """ + raise NotImplementedError() + + +class OAuthProviderByEnvironment(OAuthProviderSelector): + """ + Selects an OAuth provider based on the run environment. + + `get()` returns the `dev` arm only under `rbt dev run`, and the + `prod` arm everywhere else — `rbt serve`, Reboot Cloud, and any + environment we can't classify (which defaults to the more secure + `prod`, never the local-dev arm). Unit tests don't use this + selector: the test `Application` (`reboot.aio.tests.Application`) + wires a concrete provider via `OAuthProviderForTest` instead. + + Both `dev` and `prod` must be passed explicitly (so the choice for + each environment is deliberate), but either may be `None`. If + `get()` is reached and the selected arm is `None`, it raises — so an + application that never chose a provider for the current environment + fails to start with a clear message rather than shipping without + sensible auth. A typical production app uses + `OAuthProviderByEnvironment(dev=Development(), prod=Google(...))`; + under `rbt dev` that transparently uses `Development` without needing + the real provider's credentials. + """ + + def __init__( + self, + *, + dev: Optional[OAuthProvider], + prod: Optional[OAuthProvider], + ): + self._dev = dev + self._prod = prod + + def _select(self) -> OAuthProvider: + provider = self._dev if running_rbt_dev() else self._prod + if provider is None: + raise InputError(reason=_NO_PROVIDER_REASON) + return provider diff --git a/reboot/aio/auth/oauth_server.py b/reboot/aio/auth/oauth_server.py index 122c746e..7c88591b 100644 --- a/reboot/aio/auth/oauth_server.py +++ b/reboot/aio/auth/oauth_server.py @@ -12,15 +12,20 @@ import json import jwt import logging -import os import rbt.v1alpha1.errors_pb2 import time from mcp.server.auth.provider import AccessToken from reboot.aio.auth import Auth -from reboot.aio.auth.oauth_providers import OAuthProvider, UserId +from reboot.aio.auth.oauth_providers import ( + OAuthProvider, + OAuthTokens, + UserId, + origin_from_request, +) from reboot.aio.auth.token_verifiers import TokenVerifier, VerifyTokenResult from reboot.aio.contexts import ReaderContext -from reboot.settings import ENVVAR_REBOOT_OAUTH_SIGNING_SECRET +from reboot.aio.http import PythonWebFramework, external_context +from reboot.crypto import root_keys from starlette.requests import Request from starlette.responses import JSONResponse, RedirectResponse from typing import Any, Optional @@ -61,23 +66,27 @@ "Access-Control-Allow-Headers": "Content-Type, MCP-Protocol-Version", } +# HKDF `info` (domain separator) for the OAuth server's token-signing key. +_SIGNING_INFO = b"reboot.oauth.signing" -def _base_url(request: Request) -> str: - """ - Derive the server's base URL from the request headers. - Respects `X-Forwarded-Proto` and `X-Forwarded-Host` when behind a - reverse proxy. +def signing_secret() -> bytes: + """The MCP OAuth server's HS256 token-signing key, derived from + Reboot's managed cryptographic root keys (see `reboot.crypto.root_keys`). + + Signs and verifies the access/refresh/authorization-code JWTs this + server issues to MCP clients, so it is exercised on every authenticated + MCP request — including in production behind an external IdP, where the + IdP authenticates the user but this key protects the session token the + app then issues. + + Rotating the root keys changes this value, invalidating outstanding + tokens so clients re-authenticate. That is acceptable: it carries no + persistent identity (production user subjects come from the IdP). """ - scheme = request.headers.get( - "x-forwarded-proto", - request.url.scheme, + return root_keys.derive_key( + info=_SIGNING_INFO, version=root_keys.active_version() ) - host = request.headers.get( - "x-forwarded-host", - request.headers.get("host", "localhost"), - ) - return f"{scheme}://{host}" def _oauth_error( @@ -104,7 +113,7 @@ class OAuthTokenVerifier(TokenVerifier): Returns `Auth(user_id=...)` on success, `None` otherwise. """ - def __init__(self, signing_secret: str): + def __init__(self, signing_secret: bytes): self._signing_secret = signing_secret async def verify_token( @@ -151,7 +160,7 @@ class MCPSDKOAuthTokenVerifier: signature, audience, and type. """ - def __init__(self, signing_secret: str): + def __init__(self, signing_secret: bytes): self._signing_secret = signing_secret async def verify_token( # type: ignore[override] @@ -229,10 +238,10 @@ class OAuthServer: 6. **Refresh.** When the access token expires, the client `POST /token` with `grant_type=refresh_token` to get a new - access/refresh token pair. This OAuth server does NOT currently - re-authorize with the identity provider, since doing so would - require storing the identity provider's sensitive refresh and - access tokens. + access/refresh token pair. This OAuth server does NOT re-authorize + with the identity provider on refresh: token storage + (`store_tokens=True`) exists for the application to call the + provider's API, not to drive re-authorization here. """ def __init__( @@ -244,18 +253,10 @@ def __init__( self._provider = provider self._protected_resources = protected_resources self._access_token_ttl_seconds = provider.access_token_ttl_seconds - self._signing_secret = os.environ.get( - ENVVAR_REBOOT_OAUTH_SIGNING_SECRET - ) - if self._signing_secret is None: - raise ValueError( - f"The '{ENVVAR_REBOOT_OAUTH_SIGNING_SECRET}' environment " - "variable must be set when using `oauth`. " - "For local development with `rbt dev`, this is " - "set automatically. For production, set it to a " - "strong random secret shared across all server " - "processes." - ) + # Derived from the Reboot-managed cryptographic root keys; raises + # if `REBOOT_CRYPTO_ROOT_KEYS` is unset/malformed (fail fast at + # construction rather than per-request). + self._signing_secret = signing_secret() self._token_verifier = OAuthTokenVerifier(self._signing_secret) @property @@ -269,15 +270,13 @@ def token_verifier(self) -> OAuthTokenVerifier: def mcp_sdk_token_verifier(self) -> MCPSDKOAuthTokenVerifier: """ Return something implementing the MCP SDK's `TokenVerifier` - + For use with the SDK's `BearerAuthBackend` / `RequireAuthMiddleware`. """ - # `__init__` raises if `_signing_secret` is None. - assert self._signing_secret is not None return MCPSDKOAuthTokenVerifier(self._signing_secret) - def mount_routes(self, http) -> None: + def mount_routes(self, http: PythonWebFramework.HTTP) -> None: """Register all OAuth endpoints on `http`.""" # RFC 9728: Protected Resource Metadata. MCP # clients discover auth servers through this. @@ -300,10 +299,20 @@ def mount_routes(self, http) -> None: # Authorization and token endpoints. http.get(_AUTHORIZE_PATH)(self.authorize) - http.get(_CALLBACK_PATH)(self.callback) + # The callback persists the provider's tokens (when + # `store_tokens=True`) via the app-internal-only `Ciphertext` / + # `OrderedMap` servicers, so it opts in to an app-internal context + # with `app_internal=True`. That's safe here because the callback + # runs only after the identity provider has redirected back with an + # authorization code that we exchange and validate before doing any + # app-internal work. + http.get(_CALLBACK_PATH, app_internal=True)(self.callback) http.post(_TOKEN_PATH)(self.token) http.options(_TOKEN_PATH)(self.cors_preflight) + # Let the provider register any additional routes it needs. + self._provider.mount_routes(http) + # ---- Helpers ---- def _make_jwt(self, payload: dict[str, Any], ttl_seconds: int) -> str: @@ -345,6 +354,60 @@ def _verify_jwt( except jwt.exceptions.PyJWTError: return None + async def _store_oauth_tokens( + self, + request: Request, + user_id: UserId, + tokens: OAuthTokens, + ) -> None: + """Persist the provider's tokens (encrypted) for `user_id`, under + the `OAuthTokenManager` for this provider's external service. + + Best-effort: a storage failure (e.g. a misconfigured ciphertext + key) is logged but does not block the sign-in, so auth never + breaks because the token vault is misconfigured. + """ + from rbt.std.oauth.v1.oauth_rbt import OAuthTokenManager + service = self._provider.token_service_id + context = external_context(request) + try: + # Carry an existing refresh token forward when this sign-in + # didn't return one (some providers, e.g. Google, issue a + # refresh token only on the first consent, so a later sign-in + # would otherwise drop it). `fetch` and `store` are separate + # transactions, so this can't live inside `store`, which writes + # the same per-user index it would have to read. + if not tokens.HasField("refresh_token"): + try: + existing = await OAuthTokenManager.ref(service).fetch( + context, user_id=user_id + ) + except OAuthTokenManager.FetchAborted: + # Nothing stored for this service yet (the manager is + # constructed lazily by the first store). + existing = None + if ( + existing is not None and existing.found and + existing.tokens.HasField("refresh_token") + ): + merged = OAuthTokens() + merged.CopyFrom(tokens) + merged.refresh_token = existing.tokens.refresh_token + tokens = merged + await OAuthTokenManager.ref(service).store( + context, + user_id=user_id, + tokens=tokens, + ) + except Exception as e: + logger.error( + "Failed to store identity provider tokens for user " + "'%s': %s", + user_id, + e, + exc_info=True, + ) + # ---- Route handlers ---- async def cors_preflight( @@ -367,7 +430,7 @@ async def protected_resource_metadata( Returns RFC 9728 OAuth 2.0 Protected Resource Metadata. Tells MCP clients which authorization server to use. """ - base = _base_url(request) + base = origin_from_request(request) # Derive the resource path from the request URL: # "/.well-known/oauth-protected-resource/mcp" → "/mcp". prefix = "/.well-known/oauth-protected-resource" @@ -386,7 +449,7 @@ async def metadata(self, request: Request) -> JSONResponse: Returns RFC 8414 OAuth Authorization Server Metadata. """ - base = _base_url(request) + base = origin_from_request(request) return JSONResponse( { "issuer": base, @@ -548,7 +611,7 @@ async def authorize(self, request: Request): ) # Our own callback URL. - callback_uri = f"{_base_url(request)}{_CALLBACK_PATH}" + callback_uri = f"{origin_from_request(request)}{_CALLBACK_PATH}" idp_url = self._provider.authorization_url( state=pending, @@ -616,10 +679,11 @@ async def callback(self, request: Request): status_code=400, ) - # Exchange identity provider code for a user ID. - callback_uri = f"{_base_url(request)}{_CALLBACK_PATH}" + # Exchange identity provider code for a user ID (and, for + # providers that capture them, the provider's tokens). + callback_uri = f"{origin_from_request(request)}{_CALLBACK_PATH}" try: - user_id = await self._provider.exchange_code( + result = await self._provider.exchange_code( code=idp_code, redirect_uri=callback_uri, ) @@ -645,6 +709,14 @@ async def callback(self, request: Request): status_code=302, ) + user_id = result.user_id + + # If the provider captured the identity provider's tokens + # (`store_tokens=True`), persist them, encrypted, so the app can + # use them to call the provider's API. + if result.tokens is not None: + await self._store_oauth_tokens(request, user_id, result.tokens) + # Mint the auth code JWT. auth_code = self._make_jwt( { @@ -754,7 +826,7 @@ async def _token_authorization_code( ) user_id: UserId = UserId(code_data["sub"]) - base = _base_url(request) + base = origin_from_request(request) # Mint access token. access_token = self._make_jwt( @@ -794,15 +866,13 @@ async def _token_refresh( ) -> JSONResponse: """ Handle `grant_type=refresh_token`. - - TODO: this flow (currently) doesn't consult the identity - provider; it's therefore not possible to revoke a leaked refresh - token. In the future, when we add the capability to securely - store the identity provider's access token and refresh token, we - should use those to refresh our own access token at the identity - provider before refreshing the caller's access token. It is then - possible for the identity provider to revoke the caller's - refresh token by revoking the refresh token it has issued to us. + + This mints a fresh access/refresh token pair from the (still + valid) refresh token. It does NOT re-authorize at the identity + provider, even when `store_tokens=True`: token storage exists so + the application can call the provider's API, not to drive + re-authorization here. An app that needs a fresh provider token + refreshes on demand using the stored `refresh_token`. """ refresh_token_str = form.get("refresh_token") if refresh_token_str is None: @@ -830,7 +900,7 @@ async def _token_refresh( ) user_id: UserId = UserId(refresh_data["sub"]) - base = _base_url(request) + base = origin_from_request(request) # Mint new access token. access_token = self._make_jwt( diff --git a/reboot/aio/concurrently.py b/reboot/aio/concurrently.py new file mode 100644 index 00000000..04bb704f --- /dev/null +++ b/reboot/aio/concurrently.py @@ -0,0 +1,951 @@ +from __future__ import annotations + +import asyncio +import logging +import time +from dataclasses import dataclass +from typing import ( + Any, + AsyncGenerator, + Awaitable, + Callable, + Generator, + Generic, + Iterable, + List, + Literal, + Optional, + Protocol, + Tuple, + TypeVar, + Union, + overload, +) + +ConcurrentlyResultT = TypeVar("ConcurrentlyResultT") + +# Type variable for the element type in `for_each` mode. +ElementT = TypeVar("ElementT") + +# Generic type variable for the items yielded by the async +# generator and collected by `await`. +YieldT = TypeVar("YieldT") + +# `for_each` accepts either a plain iterable or an async +# generator. Async generators allow lazy, streaming sources +# (e.g., database cursors, paginated APIs) without +# materializing all elements into memory. +ForEach = Union[Iterable[ElementT], AsyncGenerator[ElementT, None]] + +# The first positional argument to `concurrently()` is one of: +# - An async generator of awaitables (lazy streaming). +# - A plain iterable of awaitables (e.g., list or generator +# expression). +# - A callable that maps an element to an awaitable (used +# with `for_each`). +# The overloads carry the precise type variables; this alias +# is used in the implementation signatures where all variants +# must be accepted. +CallableOrAwaitables = Union[ + AsyncGenerator[Awaitable[Any], None], + Iterable[Awaitable[Any]], + Callable[..., Awaitable[Any]], +] + + +class OnWindowClose(Protocol): + """Callback protocol for adaptive limiter window close + events. Invoked each time a sample window closes and the + concurrency limit is recalculated.""" + + def __call__( + self, + *, + limit: int, + long_rtt: float, + short_rtt: float, + gradient: float, + ) -> None: + ... + + +class AdaptiveConcurrencyLimiter: + """Discovers the optimal concurrency level using the Gradient2 + algorithm, adapted from Netflix's concurrency-limits library + (https://github.com/Netflix/concurrency-limits). + + Rather than requiring callers to guess the right concurrency + limit, this class measures operation latency and adjusts the + limit dynamically. It starts at a conservative + `initial_limit` and ramps up when latency is stable, or + backs off when latency increases (indicating overload such as + backend queuing, event loop saturation, or network + contention). + + Algorithm choice + ================ + + We evaluated three algorithms from Netflix's library: + + - **Vegas** (TCP Vegas-inspired): Estimates queue depth as + `limit * (1 - minRTT / currentRTT)` and adjusts when + the estimate crosses alpha/beta thresholds. Rejected + because it depends on finding a stable minimum RTT, which + is unreliable during short batches where the first + operations may have genuinely different latency (cold + caches, connection setup). + + - **AIMD** (Additive Increase, Multiplicative Decrease): + Grows by +1 on success, shrinks by a backoff ratio on + failure. Rejected because it requires binary drop/success + signals. In asyncio there are no explicit rejections -- + overload manifests only as increased latency, which AIMD + cannot detect. + + - **Gradient2** (chosen): Compares short-term RTT against + long-term RTT using a gradient ratio. When latency rises + relative to the baseline, the limit decreases + multiplicatively; when latency is stable, it grows + via a proportional growth term. This provides continuous + latency-based feedback without needing a stable minimum + RTT or explicit drop signals. + + How it works + ============ + + Completed operations report their round-trip time (RTT) via + `on_sample()`. Samples are accumulated into windows of + `min_window_samples` completions. When a window closes: + + 1. **Short-term RTT**: The average RTT of the current window. + + 2. **Long-term RTT**: An exponentially weighted moving average + (EWMA) across all samples, with decay factor + `2 / (long_window + 1)`. This tracks the sustained + baseline latency and adapts gradually. + + 3. **Gradient**: `rtt_tolerance * long_rtt / short_rtt`, + clamped to [0.5, 1.0]. + + - When `short_rtt ≈ long_rtt`, gradient ≈ + `rtt_tolerance` (1.5), which gets clamped to 1.0 + -- the system is healthy and the limit grows. + - When `short_rtt > long_rtt * rtt_tolerance`, gradient + < 1.0 -- latency has increased beyond the tolerance + band and the limit shrinks multiplicatively. + - The 0.5 floor prevents the limit from dropping by more + than half in a single window. + + 4. **New limit**: + `gradient * current_limit * (1 + growth_factor)`, + where growth is applied inside the gradient + multiplication so that backoff dominates when latency + increases (see "Growth strategy" below). The result is + smoothed via `(1 - smoothing) * old + smoothing * new` + and clamped to [min_limit, max_limit]. + + Growth strategy + =============== + + Netflix's Gradient2 uses a fixed additive `queue_size` + (default 4) for growth. This produces linear ramp-up: + +0.8 per window with their default smoothing of 0.2. For + long-lived server processes handling thousands of RPS, + linear growth is fine -- the limiter converges within + seconds and then operates in steady state for hours. + + Our workloads are different: short-lived batches of 10s to + 1000s of operations -- e.g., bulk RPC fan-out where an + application issues many calls in parallel (such as bulk + `OrderedMap` inserts/removes) and the limiter is created + per-batch. Linear growth from 5 to 100 would take ~119 + windows (~595 completions) -- most batches would finish + before the limiter discovered the optimal level. + + Instead, we use **proportional growth** applied inside the + gradient multiplication: + `new_limit = gradient * limit * (1 + growth_factor)`. + This produces exponential ramp-up when healthy and ensures + backoff still works when overloaded (similar to TCP cubic's + convex growth phase): + + - gradient=1.0 (healthy): new_limit = 1.5 * limit. After + smoothing (0.5): +25% per window. + - gradient=0.5 (overloaded): new_limit = 0.75 * limit. + After smoothing: -12.5% per window. + - Starting at 5, reaching 100 takes ~14 windows (~70 + completions) under stable latency. + + Backoff is intentionally slower than growth (-12.5% vs + +25%): short-batch workloads benefit from aggressive + ramp-up, and a single noisy window (cold cache, GC pause, + transient queueing) shouldn't kneecap concurrency for the + rest of the batch. `rtt_tolerance` (1.5) already absorbs + most natural variance, and the gradient's 0.5 floor caps + single-window backoff -- but sustained overload still + drives the limit down via repeated -12.5% windows. + + Placing growth inside the gradient is critical: an additive + growth term (Netflix's `queue_size`) or an additive + proportional term (`gradient * limit + growth_factor * + limit`) would fully compensate the gradient reduction at + the 0.5 floor, preventing the limit from ever decreasing. + + This fast ramp-up is appropriate because Python is single- + threaded: we start conservatively (initial_limit=5) to + avoid overwhelming a single event loop, but ramp up quickly + because the backend typically consists of multiple Python + processes that can absorb higher concurrency. If those + processes are scaled down dynamically, the resulting latency + increase triggers a fast backoff via the gradient mechanism. + + Windowing strategy + ================== + + Netflix's library uses 1-second time windows by default. + This makes sense for servers handling thousands of RPS -- + a 1-second window captures hundreds of samples, providing + a stable signal while preventing too-frequent limit + changes. The 1-second floor is a low-pass filter on update + frequency, not a statement about operation latency (their + operations are also in the millisecond range). + + Our workloads involve 10s to 1000s of operations where each + operation takes milliseconds to 100s of milliseconds, so a + 1-second window might span an entire batch, giving the + algorithm no chance to adapt. We use **sample-count-based + windows** instead (default: 5 samples per window). With + 128 operations and 5-sample windows, the algorithm gets ~25 + limit updates -- enough for meaningful adaptation. Smaller + windows also mean faster detection when backend capacity + changes (e.g., processes being added or removed + dynamically). The EWMA on `long_rtt` and the + `rtt_tolerance` buffer provide sufficient noise reduction + even with small windows. + """ + + # Minimum samples before the algorithm updates the limit. + # Smaller windows mean faster adaptation when backend + # capacity changes (e.g., processes added or removed + # dynamically), at the cost of noisier signal. Netflix + # uses 10, but with our small batches we want more frequent + # updates. With 5 samples per window, a batch of 128 + # operations gives ~25 updates, and the first real limit + # change happens after just 10 completions. + DEFAULT_MIN_WINDOW_SAMPLES = 5 + + # Conservative default initial concurrency. We tie this to + # `DEFAULT_MIN_WINDOW_SAMPLES` so the baseline window and + # the first comparison window operate at the same effective + # concurrency level: any higher and the baseline consists + # of the fastest completions among a larger initial batch, + # biasing `long_rtt` low and making the first gradient + # comparison look artificially overloaded. Starting low is + # fine for Python anyway, since a single event loop is + # easily overwhelmed and we rely on proportional growth to + # ramp up quickly. (Netflix defaults to 20, but they target + # multi-threaded Java servers.) For batches smaller than + # `2 * DEFAULT_MIN_WINDOW_SAMPLES`, the limiter effectively + # acts as a fixed limit -- which is fine since small + # batches don't need adaptive behavior. + DEFAULT_INITIAL_LIMIT = DEFAULT_MIN_WINDOW_SAMPLES + + # Hard ceiling on concurrency. Callers typically override + # this via the `limit` parameter on `concurrently()`. The + # high default avoids being an accidental bottleneck when + # no explicit limit is given. + DEFAULT_MAX_LIMIT = 1000 + + # Default parameter values are documented below with + # rationale for each choice. + + def __init__( + self, + *, + initial_limit: int = DEFAULT_INITIAL_LIMIT, + # Upper bound on concurrency. When `None`, uses + # `DEFAULT_MAX_LIMIT`. + max_limit: Optional[int] = None, + # Floor of 1 ensures we always make progress. + min_limit: int = 1, + # Allow latency to increase by up to 50% before + # reducing concurrency. This matches Netflix's default + # and provides a reasonable tolerance band for the + # natural variance of RPC latencies. + rtt_tolerance: float = 1.5, + # Smoothing factor for limit updates. 0.5 means each + # window's calculated limit contributes 50% to the new + # value. Netflix defaults to 0.2, but that's too + # conservative for short-lived batches where we need to + # ramp up (and back off) quickly. The EWMA on + # `long_rtt` already smooths the input signal, so heavy + # output smoothing would be double-dampening. + smoothing: float = 0.5, + # Proportional growth factor. Expected range: + # `0 < growth_factor <= 1`. When the gradient is 1.0 + # (healthy), the growth term is + # `growth_factor * current_limit`, giving exponential + # ramp-up. With smoothing=0.5 and growth_factor=0.5, + # the limit grows by ~25% per window when healthy. + # Netflix uses a fixed additive `queue_size=4` instead, + # which produces linear growth -- too slow for short + # batches where we need to discover capacity quickly. + growth_factor: float = 0.5, + # Number of samples over which the long-term EWMA + # averages. Decay factor = 2 / (long_window + 1) ≈ + # 0.02, giving a half-life of ~34 samples. Netflix + # uses 600, but our batches are short-lived -- 600 + # samples would rarely be reached. 100 provides a + # stable baseline achievable within a single + # moderately-sized batch. + long_window: int = 100, + min_window_samples: int = DEFAULT_MIN_WINDOW_SAMPLES, + # Optional callback invoked each time a window closes + # and the limit is recalculated. Used by `concurrently` + # to wire up per-window logging without coupling logging + # concerns into the limiter itself. + on_window_close: Optional[OnWindowClose] = None, + ): + max_limit = max_limit or self.DEFAULT_MAX_LIMIT + + assert initial_limit > 0 + assert max_limit > 0 + assert min_limit >= 1 + assert rtt_tolerance > 0 + assert 0 < smoothing <= 1 + assert 0 < growth_factor <= 1 + assert long_window > 0 + assert min_window_samples > 0 + + # Don't start above the caller's upper bound. + initial_limit = min(initial_limit, max_limit) + initial_limit = max(initial_limit, min_limit) + + self._limit: float = float(initial_limit) + self._max_limit = max_limit + self._min_limit = min_limit + self._rtt_tolerance = rtt_tolerance + self._smoothing = smoothing + self._growth_factor = growth_factor + self._decay = 2.0 / (long_window + 1) + self._min_window_samples = min_window_samples + self._on_window_close = on_window_close + + # Long-term RTT baseline (EWMA). None until the first + # window closes. + self._long_rtt: Optional[float] = None + + # Current window's accumulated samples. + self._window_samples: list[float] = [] + + @property + def limit(self) -> int: + """Current effective concurrency limit, rounded to the + nearest integer and clamped to [min_limit, max_limit].""" + return max(self._min_limit, min( + self._max_limit, + round(self._limit), + )) + + def on_sample(self, rtt: float) -> None: + """Record a completed operation's round-trip time. + + Samples are accumulated into the current window. When + the window reaches `min_window_samples`, the limit is + recalculated and the window resets. + """ + self._window_samples.append(rtt) + + if len(self._window_samples) >= self._min_window_samples: + self._update_limit() + self._window_samples.clear() + + def _update_limit(self) -> None: + """Recalculate the concurrency limit from the current + window's samples using the Gradient2 formula.""" + # Short-term RTT: average of the current window. + short_rtt = sum(self._window_samples) / len(self._window_samples) + + if self._long_rtt is None: + # First window: establish the long-term baseline. + self._long_rtt = short_rtt + return + + # Update long-term RTT with EWMA. + previous_long_rtt = self._long_rtt + self._long_rtt += (short_rtt - self._long_rtt) * self._decay + + # Gradient, clamped to [0.5, 1.0]; see "How it works" + # in the class docstring. + gradient = max( + 0.5, + min( + 1.0, + self._rtt_tolerance * previous_long_rtt / short_rtt, + ), + ) + + # Proportional growth applied inside the gradient; see + # "Growth strategy" in the class docstring. + new_limit = gradient * self._limit * (1.0 + self._growth_factor) + + # Smooth the transition to dampen oscillation. + self._limit = ( + (1 - self._smoothing) * self._limit + self._smoothing * new_limit + ) + + # Clamp to bounds. + self._limit = max( + float(self._min_limit), + min(float(self._max_limit), self._limit), + ) + + if self._on_window_close is not None: + self._on_window_close( + limit=self.limit, + long_rtt=self._long_rtt, + short_rtt=short_rtt, + gradient=gradient, + ) + + +# Default module-level logger, used when a `Log` instance does +# not specify a custom logger. +_default_logger = logging.getLogger(__name__) + + +@dataclass(frozen=True, kw_only=True) +class Log: + """Logging configuration for `concurrently()`. All fields are + optional.""" + + # Human-readable description of the operation. If provided, + # the generator's `__qualname__` is appended in parens. If + # omitted, just `__qualname__` is used (or "concurrently" as + # a last resort). + label: Optional[str] = None + # Override the default module-level logger. + logger: Optional[logging.Logger] = None + # Override the default log level (DEBUG). + level: Optional[int] = None + # When True, emit per-window messages (limit, RTT, + # gradient) during execution. When False, only the final + # summary is logged. + verbose: bool = False + + +# The `log` parameter accepts either `True` (all defaults) or a +# `Log` instance for customization. +LogOption = Union[bool, Log] + + +class Concurrently(Generic[YieldT]): + """Wrapper returned by `concurrently()` that supports both + `async for` and `await`. + + `async for` yields items one at a time in completion + order. `await` collects all items into a list. Both + produce the same item type. In `for_each` mode, items + are `(element, result)` tuples. + """ + + def __init__( + self, + callable_or_awaitables: CallableOrAwaitables, + *, + for_each: Optional[ForEach[Any]] = None, + return_exceptions: bool, + limit: Optional[int], + adaptive: bool, + log: Optional[LogOption], + ): + self._callable_or_awaitables = callable_or_awaitables + self._for_each = for_each + self._return_exceptions = return_exceptions + self._limit = limit + self._adaptive = adaptive + self._log = log + + def __aiter__(self) -> AsyncGenerator[YieldT, None]: + return _concurrently( + self._callable_or_awaitables, + for_each=self._for_each, + return_exceptions=self._return_exceptions, + limit=self._limit, + adaptive=self._adaptive, + log=self._log, + ) + + def __await__(self) -> Generator[Any, None, List[YieldT]]: + + async def _collect() -> List[YieldT]: + results: List[YieldT] = [] + async for result in self: + results.append(result) + return results + + return _collect().__await__() + + +# --- `AsyncGenerator` mode overloads --- + + +@overload +def concurrently( + awaitables: AsyncGenerator[Awaitable[ConcurrentlyResultT], None], + *, + return_exceptions: Literal[True], + limit: Optional[int] = ..., + adaptive: bool = ..., + log: Optional[LogOption] = ..., +) -> Concurrently[Union[ConcurrentlyResultT, BaseException]]: + ... + + +@overload +def concurrently( + awaitables: AsyncGenerator[Awaitable[ConcurrentlyResultT], None], + *, + return_exceptions: Literal[False] = ..., + limit: Optional[int] = ..., + adaptive: bool = ..., + log: Optional[LogOption] = ..., +) -> Concurrently[ConcurrentlyResultT]: + ... + + +# --- `Iterable` mode overloads --- + + +@overload +def concurrently( + awaitables: Iterable[Awaitable[ConcurrentlyResultT]], + *, + return_exceptions: Literal[True], + limit: Optional[int] = ..., + adaptive: bool = ..., + log: Optional[LogOption] = ..., +) -> Concurrently[Union[ConcurrentlyResultT, BaseException]]: + ... + + +@overload +def concurrently( + awaitables: Iterable[Awaitable[ConcurrentlyResultT]], + *, + return_exceptions: Literal[False] = ..., + limit: Optional[int] = ..., + adaptive: bool = ..., + log: Optional[LogOption] = ..., +) -> Concurrently[ConcurrentlyResultT]: + ... + + +# --- `Callable` + `for_each` mode overloads --- + + +@overload +def concurrently( + callable: Callable[[ElementT], Awaitable[ConcurrentlyResultT]], + *, + for_each: ForEach[ElementT], + return_exceptions: Literal[True], + limit: Optional[int] = ..., + adaptive: bool = ..., + log: Optional[LogOption] = ..., +) -> Concurrently[ + Tuple[ + ElementT, + Union[ConcurrentlyResultT, BaseException], + ], +]: + ... + + +@overload +def concurrently( + callable: Callable[[ElementT], Awaitable[ConcurrentlyResultT]], + *, + for_each: ForEach[ElementT], + return_exceptions: Literal[False] = ..., + limit: Optional[int] = ..., + adaptive: bool = ..., + log: Optional[LogOption] = ..., +) -> Concurrently[Tuple[ElementT, ConcurrentlyResultT]]: + ... + + +# NOTE: mypy can't prove that the `Any`-based implementation +# signature accepts all `TypeVar` bounded overload signatures. +# This is standard for overloaded implementations — the +# overloads provide type safety for callers but we need +# `ignore[misc]` for the implementation. +def concurrently( # type: ignore[misc] + callable_or_awaitables: CallableOrAwaitables, + *, + for_each: Optional[ForEach[Any]] = None, + return_exceptions: bool = False, + limit: Optional[int] = None, + adaptive: bool = True, + log: Optional[LogOption] = None, +) -> Concurrently[Any]: + """Concurrently execute awaitables with adaptive concurrency + control. + + Two modes of operation: + + **AsyncGenerator mode** — pass an async generator of + awaitables as the first argument:: + + async for result in concurrently( + generator(), + ): + ... + + **Callable + `for_each` mode** — pass a callable and an + iterable. The callable is applied to each element, and + results are yielded as `(element, result)` tuples:: + + async for user_id, response in concurrently( + lambda user_id: User.ref(user_id).get(context), + for_each=user_ids, + ): + ... + + Both modes support `async for` and `await`. `await` + collects all results into a list. + + By default, exceptions are raised immediately (like + `asyncio.gather`). Pass `return_exceptions=True` to + return exceptions as values instead: each item becomes + `Union[result, BaseException]`, enabling mypy's + `isinstance` narrowing for type-safe error handling. + In `for_each` mode, items are + `(element, Union[result, BaseException])` tuples. + + Pass `log=True` to enable logging with defaults, or a + `Log` instance to customize. + """ + return Concurrently( + callable_or_awaitables, + for_each=for_each, + return_exceptions=return_exceptions, + limit=limit, + adaptive=adaptive, + log=log, + ) + + +async def _iterable_to_async_generator( + iterable: Iterable[Awaitable[Any]], +) -> AsyncGenerator[Awaitable[Any], None]: + """Wrap a plain iterable of awaitables into an async + generator so that `_concurrently` can consume it uniformly + via `__anext__()`.""" + for item in iterable: + yield item + + +async def _callable_to_async_generator( + callable: Callable, + for_each: Any, +) -> AsyncGenerator[Tuple[Any, Awaitable], None]: + """Wrap a callable + iterable/async generator into an async + generator that yields `(element, awaitable)` tuples. Used + by `_concurrently` in `for_each` mode. + + Accepts both plain iterables (`for element in ...`) and + async generators (`async for element in ...`). This allows + lazy, streaming sources like database cursors or paginated + APIs to be consumed on demand without materializing all + elements into memory.""" + if hasattr(for_each, "__anext__"): + async for element in for_each: + yield element, callable(element) + else: + for element in for_each: + yield element, callable(element) + + +async def _concurrently( + callable_or_awaitables: CallableOrAwaitables, + *, + for_each: Optional[ForEach[Any]], + return_exceptions: bool, + limit: Optional[int], + adaptive: bool, + log: Optional[LogOption], +) -> AsyncGenerator: + """Internal async generator that does the actual concurrent + execution. See `concurrently()` for the public API.""" + # Determine the mode and build the async generator that + # `fill()` will pull from. + if for_each is not None: + # Callable + `for_each` mode: `callable_or_awaitables` is a + # callable, `for_each` is an iterable or async generator. We + # wrap them into an async generator of (element, awaitable) + # tuples and track elements alongside tasks. + assert callable(callable_or_awaitables), ( + "When `for_each` is provided, the first argument " + "must be a callable." + ) + awaitables: AsyncGenerator = _callable_to_async_generator( + callable_or_awaitables, + for_each, + ) + # Extract qualname from the callable for log labels. + qualname = getattr(callable_or_awaitables, "__qualname__", None) + else: + # AsyncGenerator or iterable mode. + # Extract qualname before potentially wrapping. + qualname = getattr(callable_or_awaitables, "__qualname__", None) + if hasattr(callable_or_awaitables, "__anext__"): + # Already an async generator. + awaitables = callable_or_awaitables # type: ignore[assignment] + else: + # Plain iterable (e.g., list or generator + # expression). Wrap it into an async generator. + awaitables = _iterable_to_async_generator( + callable_or_awaitables, # type: ignore[arg-type] + ) + + # In `for_each` mode, map from task to its corresponding + # element from the iterable so we can yield + # (element, result) tuples. + elements: dict[asyncio.Task, Any] = {} + + # Normalize `log=True` to a default `Log` instance. + if log is True: + log = Log() + + # Unpack the logging config. All fields are optional; + # `logger` and `level` default to the module-level logger + # and DEBUG respectively. + if log is not None and log is not False: + label = log.label + # Build the log label: include the user's label and + # the generator's `__qualname__` when available. The + # qualname is always appended in parens for + # traceability. + if label is not None and qualname is not None: + log_label = f"{label} ({qualname})" + elif label is not None: + log_label = label + elif qualname is not None: + log_label = qualname + else: + log_label = "concurrently" + logger = log.logger or _default_logger + log_level = log.level if log.level is not None else logging.DEBUG + verbose = log.verbose + else: + log_label = None + logger = None + log_level = None + verbose = False + + # Wire up per-window logging callback for the adaptive + # limiter. Only enabled when `verbose=True`; otherwise + # only the final summary is logged. + on_window_close = None + if logger is not None and verbose: + + def on_window_close( + *, + limit: int, + long_rtt: float, + short_rtt: float, + gradient: float, + ) -> None: + assert logger is not None + assert log_level is not None + logger.log( + log_level, + "[%s] window: limit=%d long_rtt=%.1fms " + "short_rtt=%.1fms gradient=%.2f", + log_label, + limit, + long_rtt * 1000, + short_rtt * 1000, + gradient, + ) + + if adaptive: + if limit is not None: + assert limit > 0, f"`limit` must be positive, got {limit}" + limiter = AdaptiveConcurrencyLimiter( + max_limit=limit, + on_window_close=on_window_close, + ) + else: + assert limit is not None, "`limit` is required when `adaptive=False`" + assert limit > 0, f"`limit` must be positive, got {limit}" + limiter = None + + def effective_limit() -> int: + if limiter is not None: + return limiter.limit + assert limit is not None + return limit + + pending: set[asyncio.Task[Any]] = set() + # Map from task to its creation timestamp, used to measure RTT for + # the adaptive limiter. We use `time.perf_counter` which on Linux + # has nanosecond resolution (with some precision lost to float + # representation) and uses a monotonically increasing clock — + # important because we only care about elapsed duration, not + # wall-clock time, and we never want a system clock adjustment to + # produce a negative RTT. + start_times: dict[asyncio.Task[Any], float] = {} + exhausted = False + + # Statistics for the final log summary. + total_count = 0 + error_count = 0 + rtt_min = float("inf") + rtt_max = 0.0 + rtt_sum = 0.0 + initial_start_time = time.perf_counter() + + # Wrap the awaitable in a coroutine so we can pass it to + # `asyncio.create_task`. We don't use `asyncio.ensure_future` + # because according to the robot it only accepts coroutines + # and `Future` objects, not arbitrary `Awaitable`s. + async def run(awaitable: Awaitable[Any]) -> Any: + return await awaitable + + async def fill() -> None: + nonlocal exhausted + while len(pending) < effective_limit() and not exhausted: + try: + item = await awaitables.__anext__() + except StopAsyncIteration: + exhausted = True + break + + if for_each is not None: + # In `for_each` mode, the generator yields + # (element, awaitable) tuples. + element, awaitable = item + else: + element = None + awaitable = item + + task = asyncio.create_task(run(awaitable)) + + pending.add(task) + + if for_each is not None: + elements[task] = element + + if limiter is not None: + start_times[task] = time.perf_counter() + + try: + await fill() + + while len(pending) > 0: + done, _ = await asyncio.wait( + pending, + return_when=asyncio.FIRST_COMPLETED, + ) + for task in done: + pending.discard(task) + + total_count += 1 + + # Record RTT for the adaptive limiter. + if limiter is not None: + start_time = start_times.pop(task) + rtt = time.perf_counter() - start_time + limiter.on_sample(rtt) + if logger is not None: + rtt_min = min(rtt_min, rtt) + rtt_max = max(rtt_max, rtt) + rtt_sum += rtt + + # Retrieve the element for `for_each` mode. + element = elements.pop(task) if for_each is not None else None + + exception = task.exception() + if exception is not None: + error_count += 1 + + if return_exceptions: + # Yield result or exception directly (matching + # `asyncio.gather`). In `for_each` mode, pair with + # the element. This enables mypy's `isinstance` + # narrowing: after `if isinstance(result, + # BaseException)` the else branch knows `result` + # is the result type. + if for_each is not None: + yield element, exception or task.result() + else: + yield exception or task.result() + else: + if exception is not None: + raise exception + if for_each is not None: + yield element, task.result() + else: + yield task.result() + + # NOTE: this implementation deliberately waits to call + # `fill()` until _after_ control has been returned + # from the caller so that if their loop body is very + # slow we will not potentially grow the `pending` + # indefinitely. + # + # With the adaptive limiter, `fill()` re-reads + # `effective_limit()` each time, so limit changes from + # `on_sample()` above take effect immediately, but not + # until after the loop body has yielded control. + # + # TODO: consider adding the ability for the caller to + # pass a setting to override this behavior, i.e., to + # support filling immediately after a task has + # completed rather than after the loop body has run. + await fill() + finally: + # Cancel outstanding tasks if the caller stops iterating + # early, e.g., due to a `break` or raising an exception. + if len(pending) > 0: + for task in pending: + task.cancel() + + # Use `return_exceptions=True` so that + # `CancelledError` from the cancelled tasks is + # swallowed rather than propagated to the caller. + await asyncio.gather(*pending, return_exceptions=True) + + # Log a summary. + if logger is not None: + assert log_level is not None + elapsed = time.perf_counter() - initial_start_time + if limiter is not None and total_count > 0: + avg_rtt = rtt_sum / total_count + logger.log( + log_level, + "[%s] done: %d completed, %d errors, " + "%.1fms elapsed, avg_rtt=%.1fms, " + "min_rtt=%.1fms, max_rtt=%.1fms, " + "final_limit=%d", + log_label, + total_count, + error_count, + elapsed * 1000, + avg_rtt * 1000, + rtt_min * 1000, + rtt_max * 1000, + limiter.limit, + ) + else: + logger.log( + log_level, + "[%s] done: %d completed, %d errors, " + "%.1fms elapsed", + log_label, + total_count, + error_count, + elapsed * 1000, + ) diff --git a/reboot/aio/contexts.py b/reboot/aio/contexts.py index ef455320..fcb372be 100644 --- a/reboot/aio/contexts.py +++ b/reboot/aio/contexts.py @@ -46,6 +46,8 @@ StateTypeName, ) from reboot.time import DateTimeWithTimeZone +from reboot.uuidv7 import uuid7 +from reboot.wait_for_tasks import wait_for_tasks from typing import ( TYPE_CHECKING, Any, @@ -212,7 +214,7 @@ def update( ): # NOTE: manually looping through and calling `self.add()` # instead of just using `update()` on `self._participants` and - # ``self._participants_to_abort` to assert invariant check + # `self._participants_to_abort` to assert invariant check # that participants to commit and abort are mutually # exclusive. for state_ref in state_refs: @@ -511,23 +513,23 @@ async def cancel_unused(self): if task in used_tasks: self._tasks[serialized_request] = task else: - task.cancel() unused_tasks.add(task) - for task in unused_tasks: - try: - await task - except: - pass - # Remove all references to the task object and its - # associated gRPC call so they can be garbage-collected. - # Using `pop` instead of `del`, since it could be a case - # when the task is cancelled before it makes a gRPC call - # and thus doesn't have an entry in `self._calls`, - # `self._responses` and `self._used_response`. - self._calls.pop(task, None) - self._responses.pop(task, None) - self._used_response.pop(task, None) + try: + await wait_for_tasks(unused_tasks, cancel=True) + finally: + for task in unused_tasks: + # Remove all references to the task object and its + # associated gRPC call so they can be + # garbage-collected. Using `pop` instead of `del`, + # since it could be a case when the task is + # cancelled before it makes a gRPC call and thus + # doesn't have an entry in `self._calls`, + # `self._responses` and `self._used_response`. + self._calls.pop(task, None) + self._responses.pop(task, None) + self._used_response.pop(task, None) + unused_tasks.clear() _channel_manager: _ChannelManager _queriers: dict[str, Querier] @@ -910,6 +912,13 @@ def _state_ref(self) -> StateRef: def cookie(self) -> Optional[str]: return self._headers.cookie + @property + def internal_call(self) -> bool: + """Return whether this call originated within a Reboot + method (as opposed to externally, e.g., using `ExternalContext`). + """ + return self._headers.internal_call + @property def app_internal(self) -> bool: """Returns true if this context is for an application internal call, @@ -1117,18 +1126,26 @@ def __init__( method: str, effect_validation: EffectValidation, task: Optional[TaskEffect] = None, + database_timestamp_ms: Optional[int] = None, ): assert ( headers.transaction_ids is None or len(headers.transaction_ids) > 0 ) + # Use UUIDv7 with database-sourced timestamp when available + # (for restart detection), otherwise fall back to UUIDv4. + if database_timestamp_ms is not None: + transaction_id = uuid7(timestamp_ms=database_timestamp_ms) + else: + transaction_id = uuid.uuid4() + if headers.transaction_ids is None: headers = dataclasses.replace( headers, - transaction_ids=[uuid.uuid4()], - # The state servicing the request to executing a - # method of kind transaction acts as the transaction - # coordinator. + transaction_ids=[transaction_id], + # The state servicing the request to executing + # a method of kind transaction acts as the + # transaction coordinator. transaction_coordinator_state_type=state_type_name, transaction_coordinator_state_ref=headers.state_ref, ) @@ -1140,7 +1157,7 @@ def __init__( headers = dataclasses.replace( headers, - transaction_ids=headers.transaction_ids + [uuid.uuid4()], + transaction_ids=headers.transaction_ids + [transaction_id], ) super().__init__( diff --git a/reboot/aio/headers.py b/reboot/aio/headers.py index 436ed98c..be89da46 100644 --- a/reboot/aio/headers.py +++ b/reboot/aio/headers.py @@ -89,6 +89,14 @@ TRACEPARENT_HEADER = 'traceparent' TRACESTATE_HEADER = 'tracestate' +# Header indicating this call originates from within a Reboot method +# (reader, writer, transaction, or workflow) rather than using an +# `ExternalContext`. Note that this is different from the +# `CALLER_ID_HEADER`; the latter is set on any call by a Reboot +# application, even if that call comes from an `ExternalContext` +# (e.g., a legacy gRPC servicer). +INTERNAL_CALL_HEADER = 'x-reboot-internal-call' + @dataclass(kw_only=True, frozen=True) class Headers: @@ -135,6 +143,10 @@ class Headers: traceparent: Optional[str] = None tracestate: Optional[str] = None + # Whether this call originates from within a Reboot method, i.e., + # not an `ExternalContext`. + internal_call: bool = False + def __post_init__(self): validate_ascii( self.bearer_token, @@ -302,6 +314,11 @@ def extract( traceparent: Optional[str] = extract_maybe(TRACEPARENT_HEADER) tracestate: Optional[str] = extract_maybe(TRACESTATE_HEADER) + internal_call: bool = extract_maybe( + INTERNAL_CALL_HEADER, + convert=lambda value: value == 'true', + ) or False + return cls( application_id=application_id, server_id=server_id, @@ -319,6 +336,7 @@ def extract( caller_id=caller_id, traceparent=traceparent, tracestate=tracestate, + internal_call=internal_call, ) @property @@ -408,11 +426,17 @@ def maybe_add_caller_id_header() -> GrpcMetadata | tuple[()]: return ((CALLER_ID_HEADER, str(self.caller_id)),) return () + def maybe_add_internal_call_header() -> GrpcMetadata | tuple[()]: + if self.internal_call: + return ((INTERNAL_CALL_HEADER, 'true'),) + return () + return ( ((STATE_REF_HEADER, self.state_ref.to_str()),) + maybe_add_application_id_header() + maybe_add_server_id_header() + maybe_add_authorization_header() + maybe_add_cookie_header() + maybe_add_transaction_headers() + maybe_add_workflow_headers() + maybe_add_idempotency_key_header() + - maybe_add_opentelemetry_headers() + maybe_add_caller_id_header() + maybe_add_opentelemetry_headers() + maybe_add_caller_id_header() + + maybe_add_internal_call_header() ) diff --git a/reboot/aio/health_check.py b/reboot/aio/health_check.py index 95f4bd84..2bc819f0 100644 --- a/reboot/aio/health_check.py +++ b/reboot/aio/health_check.py @@ -1,8 +1,8 @@ -import asyncio import grpc.aio import time from grpc_health.v1 import health_pb2, health_pb2_grpc from reboot.aio.backoff import Backoff +from reboot.aio.concurrently import concurrently from reboot.aio.headers import STATE_REF_HEADER from reboot.aio.types import StateRef from reboot.ssl.localhost import LOCALHOST_CRT_DATA @@ -85,8 +85,8 @@ async def single_health_check( # First check passed. If `state_refs` are provided, send one health # check per state ref to target specific replicas. if state_refs is not None and len(state_refs) > 0: - results = await asyncio.gather( - *[single_health_check(state_ref) for state_ref in state_refs] + results = await concurrently( + single_health_check(state_ref) for state_ref in state_refs ) if not all(results): log_function( diff --git a/reboot/aio/http.py b/reboot/aio/http.py index 7d9cb006..e4d91d2e 100644 --- a/reboot/aio/http.py +++ b/reboot/aio/http.py @@ -4,9 +4,11 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from fastapi import Depends, FastAPI # type: ignore[import] +from reboot.aio.caller_id import CallerID from reboot.aio.external import ExternalContext from reboot.aio.internals.channel_manager import _ChannelManager -from reboot.aio.types import ServerId +from reboot.aio.types import ApplicationId, ServerId +from reboot.wait_for_tasks import wait_for_tasks from starlette.requests import Request # type: ignore[import] from starlette.types import Receive, Scope, Send # type: ignore[import] from typing import ( @@ -29,6 +31,7 @@ async def start( server_id: ServerId, port: Optional[int], channel_manager: _ChannelManager, + application_id: ApplicationId, ) -> int: raise NotImplementedError() @@ -85,8 +88,30 @@ class HTTP: def __init__(self): self._api_routes: list[PythonWebFramework.APIRoute] = [] self._mounts: list[PythonWebFramework.Mount] = [] + # Exact request paths whose handlers receive an *app-internal* + # context (one that can call app-internal-only servicers) + # instead of the usual external one, because they opted in via + # `app_internal=True`. See the DANGER note in `_api_route`. + self._app_internal_paths: set[str] = set() def _api_route(self, path: str, **kwargs): + # `app_internal` is our own kwarg, not one of FastAPI's, so we + # pop it before storing the rest. When set, this route's + # handler is given an *app-internal* `ExternalContext` (one + # carrying the application's `caller_id`, able to call + # app-internal-only servicers) instead of the external one. + # + # DANGER: an app-internal context bypasses authorizers, so a + # route that gets one can make trusted in-app calls on behalf + # of arbitrary, unauthenticated callers. Only set + # `app_internal=True` on a route you are completely certain + # serves trusted traffic — e.g. an OAuth callback that runs + # only after the authorization code has been exchanged and + # validated. Never set it on a route that acts on unvalidated + # request input. + if kwargs.pop("app_internal", False): + self._app_internal_paths.add(path) + # TODO: add type annotations for `endpoint` so that what # we take in is exactly what we return. def decorator(endpoint): @@ -196,6 +221,7 @@ async def start( server_id: ServerId, port: Optional[int], channel_manager: _ChannelManager, + application_id: ApplicationId, ) -> int: assert server_id not in self._servers @@ -232,15 +258,39 @@ def external_context_from_request(request: Request) -> ExternalContext: # end-user auth when they enter their Reboot code. ) + def app_internal_external_context_from_request( + request: Request, + ) -> ExternalContext: + # An app-internal context (carries the application's + # `caller_id`) so HTTP handlers can call app-internal-only + # servicers (e.g. the OAuth server persisting encrypted + # identity-provider tokens). + return ExternalContext( + name=f"HTTP {request.method} '{request.url.path}'", + channel_manager=channel_manager, + caller_id=CallerID(application_id=application_id), + ) + fastapi = FastAPI() @fastapi.middleware("http") async def external_context_middleware(request: Request, call_next): - # Namespace this so that any other middleware doesn't - # clash on `request.state`. - request.state.reboot_external_context = external_context_from_request( - request - ) + # Most routes get an *external* context (no `caller_id`): an + # HTTP handler serves untrusted external traffic, so handing it + # a caller that bypasses authorizers would let external + # requests escalate to trusted in-app calls. Those routes must + # do their own end-user auth. Only routes that opted in via + # `app_internal=True` get an *app-internal* context instead — + # see the DANGER note on `HTTP._api_route`. We namespace this + # on `request.state` so other middleware doesn't clash. + if request.url.path in self._http._app_internal_paths: + request.state.reboot_external_context = ( + app_internal_external_context_from_request(request) + ) + else: + request.state.reboot_external_context = ( + external_context_from_request(request) + ) return await call_next(request) @@ -326,10 +376,13 @@ async def stop(self, server_id: ServerId): # gross stack traces. server.should_exit = True try: - await uvicorn_run_task - except: - pass - del self._servers[server_id] + # The task stops on its own once `should_exit` is set, + # so pass `cancel=False`. Any `CancelledError` raised + # here is this task's own cancellation and is + # propagated. + await wait_for_tasks([uvicorn_run_task], cancel=False) + finally: + del self._servers[server_id] class NodeWebFramework(WebFramework): @@ -351,7 +404,11 @@ async def start( server_id: ServerId, port: Optional[int], channel_manager: _ChannelManager, + application_id: ApplicationId, ) -> int: + # `application_id` is only needed to build app-internal contexts + # for Python-served framework routes (e.g. the OAuth callback); + # the Node web framework doesn't serve those, so it's ignored. return await self._start(server_id, port, channel_manager) async def stop(self, server_id: ServerId): diff --git a/reboot/aio/idempotency.py b/reboot/aio/idempotency.py index 7b213975..1930d2fb 100644 --- a/reboot/aio/idempotency.py +++ b/reboot/aio/idempotency.py @@ -404,6 +404,13 @@ def idempotently( """Ensures that either all RPCs are performed idempotently or raises in the face of uncertainty about a mutation to avoid a possible undesired mutation.""" + # `.always()` explicitly opts out of idempotency — skip + # key generation, `_rpcs` tracking, and uncertainty + # tracking entirely. + if idempotency is not None and idempotency.always: + yield None + return + if idempotency is None: if self._required: assert self._required_reason is not None diff --git a/reboot/aio/internals/BUILD.bazel b/reboot/aio/internals/BUILD.bazel index 70ff5370..b34ab80c 100644 --- a/reboot/aio/internals/BUILD.bazel +++ b/reboot/aio/internals/BUILD.bazel @@ -107,6 +107,7 @@ py_library( visibility = ["//visibility:public"], deps = [ ":tasks_cache_py", + "//reboot/aio:concurrently_py", "//reboot/aio:state_managers_py", "//reboot/aio:tasks_py", "//reboot/aio/auth:admin_auth_py", diff --git a/reboot/aio/internals/middleware.py b/reboot/aio/internals/middleware.py index ae2a1d7c..d330a71d 100644 --- a/reboot/aio/internals/middleware.py +++ b/reboot/aio/internals/middleware.py @@ -15,6 +15,7 @@ ContextT, EffectValidation, EffectValidationRetry, + ReaderContext, TransactionContext, WorkflowContext, _log_message_for_effect_validation, @@ -105,6 +106,7 @@ def __init__( placement_client: PlacementClient, channel_manager: _ChannelManager, effect_validation: EffectValidation, + get_database_timestamp_ms: Callable[[], Optional[int]], ): self._application_id = application_id self._server_id = server_id @@ -112,6 +114,7 @@ def __init__( assert len(service_names) > 0 self._service_names = service_names self._effect_validation = effect_validation + self._get_database_timestamp_ms = get_database_timestamp_ms self.placement_client = placement_client self.channel_manager = channel_manager @@ -175,6 +178,8 @@ def create_context( ) kwargs['reactively_state_manager'] = reactively_state_manager kwargs['reactively_state_type'] = reactively_state_type + if context_type == TransactionContext: + kwargs['database_timestamp_ms'] = self._get_database_timestamp_ms() context = context_type(**kwargs) # Now toggle 'servicing' to indicate that we are servicing the @@ -247,7 +252,11 @@ def maybe_raise_effect_validation_retry( ( not isinstance(context, WorkflowContext) and context.workflow_id is not None - ) + ) or + # Reader called from within a Reboot method. The outermost + # method's effect-validation re-run will re-invoke this + # reader, so skip its own validation. + (isinstance(context, ReaderContext) and context.internal_call) ) if is_non_root_in_nested: # When a method is nested (transactions/workflows), retry occurs diff --git a/reboot/aio/internals/tasks_servicer.py b/reboot/aio/internals/tasks_servicer.py index 2cfcbab7..ab968f1f 100644 --- a/reboot/aio/internals/tasks_servicer.py +++ b/reboot/aio/internals/tasks_servicer.py @@ -5,6 +5,7 @@ AdminAuthMixin, auth_metadata_from_metadata, ) +from reboot.aio.concurrently import concurrently from reboot.aio.headers import SERVER_ID_HEADER, Headers from reboot.aio.internals.channel_manager import _ChannelManager from reboot.aio.internals.middleware import Middleware @@ -131,12 +132,8 @@ async def _aggregate_all_tasks( async def call_other_server( server_id: ServerId, - list_pending_tasks_responses: list[tasks_pb2.ListTasksResponse], - ): - """ - Calls 'ListTasks' on the given server and appends the - response to 'list_pending_tasks_responses'. - """ + ) -> tasks_pb2.ListTasksResponse: + """Call `ListTasks` on the given server.""" channel = self._channel_manager.get_channel_to( self._placement_client.address_for_server(server_id) ) @@ -144,31 +141,21 @@ async def call_other_server( metadata = auth_metadata_from_metadata(grpc_context) + ( (SERVER_ID_HEADER, server_id), ) - response = await stub.ListTasks( + return await stub.ListTasks( tasks_pb2.ListTasksRequest(only_server_id=server_id), metadata=metadata, ) - list_pending_tasks_responses.append(response) - - list_pending_tasks_responses: list[tasks_pb2.ListTasksResponse] = [] server_ids = self._placement_client.known_servers(self._application_id) - await asyncio.gather( - *( - call_other_server( - server_id, - list_pending_tasks_responses, - ) for server_id in server_ids - ) + return tasks_pb2.ListTasksResponse( + tasks=[ + task async for response in concurrently( + call_other_server(server_id) for server_id in server_ids + ) for task in response.tasks + ] ) - result = tasks_pb2.ListTasksResponse() - for response in list_pending_tasks_responses: - result.tasks.extend(response.tasks) - - return result - async def ListTasks( self, request: tasks_pb2.ListTasksRequest, diff --git a/reboot/aio/memoize.py b/reboot/aio/memoize.py index ea878d6f..896f0b5a 100644 --- a/reboot/aio/memoize.py +++ b/reboot/aio/memoize.py @@ -1,6 +1,5 @@ import log.log import pickle -import uuid from reboot.aio.auth.authorizers import allow_if, is_app_internal from reboot.aio.contexts import ( EffectValidation, @@ -82,6 +81,21 @@ async def memoize( assert context.task_id is not None + # `ALWAYS` means "run every time, don't memoize." Skip all + # `Memoize` state machinery (Reset, Status, Store) since the + # result would be written under a unique UUID that is never + # read again. + # + # TODO: we may still want to record `ALWAYS` results for + # observability (e.g., tracking what the workflow is doing) + # but that should be a separate, lighter-weight mechanism + # than full memoization. + if ( + not isinstance(idempotency_alias_or_tuple, str) and + idempotency_alias_or_tuple[1] == ALWAYS + ): + return await callable() + # Use `Context.idempotency()` to properly create an `Idempotency` # that handles `PER_ITERATION` correctly. idempotency = context.idempotency( @@ -89,12 +103,8 @@ async def memoize( how=PER_ITERATION if context.within_loop() else None, ) if isinstance(idempotency_alias_or_tuple, str) else ( context.idempotency( - alias=f"{idempotency_alias_or_tuple[0]} - {uuid.uuid4()}", - ) if idempotency_alias_or_tuple[1] == ALWAYS else ( - context.idempotency( - alias=idempotency_alias_or_tuple[0], - how=idempotency_alias_or_tuple[1], - ) + alias=idempotency_alias_or_tuple[0], + how=idempotency_alias_or_tuple[1], ) ) diff --git a/reboot/aio/monitoring.py b/reboot/aio/monitoring.py index 8dd40609..66a93e41 100644 --- a/reboot/aio/monitoring.py +++ b/reboot/aio/monitoring.py @@ -1,11 +1,18 @@ import asyncio +import gc import os +import subprocess import threading import time from log.log import get_logger from reboot.settings import ( + ENVVAR_REBOOT_ASYNCIO_SLOW_CALLBACK_DURATION_SECONDS, + ENVVAR_REBOOT_DISABLE_CYCLIC_GC, ENVVAR_REBOOT_ENABLE_EVENT_LOOP_BLOCKED_WATCHDOG, ENVVAR_REBOOT_ENABLE_EVENT_LOOP_LAG_MONITORING, + ENVVAR_REBOOT_PY_SPY_DURATION_SECONDS, + ENVVAR_REBOOT_PY_SPY_OUTPUT, + ENVVAR_REBOOT_PY_SPY_RATE, ) from typing import Optional @@ -107,11 +114,184 @@ async def _monitor_event_loop_lag( ) +def _maybe_enable_slow_callback_warnings( + loop: asyncio.AbstractEventLoop, +) -> None: + """If `REBOOT_ASYNCIO_SLOW_CALLBACK_DURATION_SECONDS` is set, + enable asyncio debug mode on the running loop and configure + `slow_callback_duration` so asyncio logs a warning (with the + offending callback's source location) whenever a single callback + hogs the loop for longer than the given threshold. + + Useful for tracking down coroutines that block the loop. + """ + slow_callback_duration_seconds = os.environ.get( + ENVVAR_REBOOT_ASYNCIO_SLOW_CALLBACK_DURATION_SECONDS + ) + if slow_callback_duration_seconds is None: + return + + loop.set_debug(True) + loop.slow_callback_duration = float(slow_callback_duration_seconds) + logger.warning( + "asyncio debug mode is ENABLED with " + f"`slow_callback_duration={slow_callback_duration_seconds}s` " + f"(via the '{ENVVAR_REBOOT_ASYNCIO_SLOW_CALLBACK_DURATION_SECONDS}' " + "environment variable); this adds non-trivial overhead and " + "should only be used for debugging. NOTE: consider also " + "disabling the cyclic GC so as to avoid false positive " + "warnings that are actually due to GC pauses. " + "Do not use in production!" + ) + + +def _maybe_disable_cyclic_gc() -> None: + """If `REBOOT_DISABLE_CYCLIC_GC` is set to "true", disable + CPython's cyclic garbage collector entirely. + + Reference counting still frees most objects normally; only cycle + collection is suppressed. With cyclic GC off, any remaining + slow-callback warnings are guaranteed to be real Python work + (not GC pauses masquerading as long handles), which makes A/B + testing the GC contribution to event-loop stalls trivial. + Memory will grow over time as cycles accumulate — never enable + in production. + """ + if os.environ.get( + ENVVAR_REBOOT_DISABLE_CYCLIC_GC, + "false", + ).lower() != "true": + return + + gc.disable() + + logger.warning( + "CPython cyclic garbage collector is DISABLED (via the " + f"'{ENVVAR_REBOOT_DISABLE_CYCLIC_GC}' environment variable). " + "This is a diagnostic-only setting: memory will grow over " + "time as cycles accumulate. Do not use in production!" + ) + + +def _enable_ptrace_attach() -> None: + """Allow any process owned by the same user to `PTRACE_ATTACH` to + us via `prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY)`. + + Without this, on systems with `kernel.yama.ptrace_scope=1` (e.g., + the Ubuntu default, which GitHub Codespaces inherits), py-spy + cannot attach to its parent because the Yama LSM requires the + *target* to be a descendant of the *caller* — and our py-spy is a + *child* of the target, not vice versa. The `PR_SET_PTRACER_ANY` + prctl explicitly grants ptrace permission and unblocks the attach. + + Failure here is non-fatal: on platforms without `prctl` (e.g., + macOS) or without libc loadable via `ctypes`, the caller still + tries to launch py-spy and lets it fail with a clear error. + """ + try: + import ctypes + libc = ctypes.CDLL("libc.so.6", use_errno=True) + # Constants from : + # PR_SET_PTRACER = 0x59616d61 + # PR_SET_PTRACER_ANY = (unsigned long)-1 + PR_SET_PTRACER = 0x59616d61 + PR_SET_PTRACER_ANY = ctypes.c_ulong(-1).value + result = libc.prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0) + if result != 0: + logger.warning( + "PR_SET_PTRACER prctl failed with errno " + f"{ctypes.get_errno()}; py-spy may be unable " + "to attach if `kernel.yama.ptrace_scope` >= 1" + ) + except (OSError, AttributeError) as exception: + logger.warning( + "Could not call PR_SET_PTRACER (likely not on " + f"Linux): {exception}. py-spy may be unable to attach if " + "`kernel.yama.ptrace_scope` >= 1" + ) + + +def _maybe_run_py_spy() -> None: + """If `REBOOT_PY_SPY_OUTPUT` is set, launch py-spy as a subprocess + targeting the current process and have it record a flame graph for + a fixed duration. + + py-spy is a sampling profiler that runs out-of-process via + `ptrace`, so it does not significantly perturb the running + process. Fire-and-forget; the subprocess writes its output and + exits on its own. Requires py-spy to be installed and `ptrace` + permissions on the host (`CAP_SYS_PTRACE`, root, or + `kernel.yama.ptrace_scope=0`). Diagnostic only, do not use in + production! + """ + py_spy_output = os.environ.get(ENVVAR_REBOOT_PY_SPY_OUTPUT) + if py_spy_output is None: + return + + py_spy_duration = os.environ.get( + ENVVAR_REBOOT_PY_SPY_DURATION_SECONDS, "60" + ) + py_spy_rate = os.environ.get(ENVVAR_REBOOT_PY_SPY_RATE, "100") + + _enable_ptrace_attach() + + try: + subprocess.Popen( + [ + "py-spy", + "record", + "--pid", + str(os.getpid()), + "--output", + py_spy_output, + "--duration", + py_spy_duration, + "--rate", + py_spy_rate, + # Include all threads (not just the asyncio loop + # thread) so background threads like OpenTelemetry's + # exporter and gRPC's worker threads show up in the + # flame graph too. + "--threads", + # Include idle samples so we see time spent waiting on + # I/O, not just CPU-bound work. + "--idle", + ] + ) + logger.warning( + f"py-spy is RECORDING this process for {py_spy_duration} " + f"seconds at {py_spy_rate}Hz to '{py_spy_output}' " + f"(via the '{ENVVAR_REBOOT_PY_SPY_OUTPUT}' environment variable). " + "py-spy stdout/stderr inherits this process's, so any error " + "appears in this process's log output. The SVG is only " + f"written after {py_spy_duration} seconds has elapsed." + ) + except FileNotFoundError: + logger.warning( + "Could not start py-spy: binary not found on " + "PATH. Install it, e.g., with `pip install py-spy`, and " + "ensure it is on PATH for this process." + ) + + async def monitor_event_loop( server_id: Optional[str] = None, ) -> None: loop = asyncio.get_running_loop() + # Start/enable opt-in event loop monitoring mechansism to help + # debug performance issues. Each mechanism is gated by its own + # environment variable and if none of them are set these are all + # no-ops and add no overhead - the mechanisms are all independent + # of each other and a user might want any subset of them. + + _maybe_enable_slow_callback_warnings(loop) + _maybe_disable_cyclic_gc() + _maybe_run_py_spy() + + # TODO: move watchdog and lag mechanisms that we start below into + # own functions like above to keep this function cleaner. + watchdog_enabled = os.environ.get( ENVVAR_REBOOT_ENABLE_EVENT_LOOP_BLOCKED_WATCHDOG, 'false', diff --git a/reboot/aio/placement.py b/reboot/aio/placement.py index 17c748a1..e5c604e2 100644 --- a/reboot/aio/placement.py +++ b/reboot/aio/placement.py @@ -1,4 +1,5 @@ import asyncio +import bisect import grpc.aio import hashlib import traceback @@ -266,16 +267,20 @@ def shard_for_actor( state_ref_hash: bytes = hashlib.sha1( state_ref.components()[0].to_str().encode("utf-8"), ).digest() - for entry in reversed(shard_list): - if entry.first_key <= state_ref_hash: - return entry.shard_id - - # This cannot happen; the first entry in the shard list has been - # asserted to be `b""`, which is always <= any other key. - raise RuntimeError( - "Unexpectedly reached end of shard list while looking for key " - f"'{state_ref_hash.hex()}'." + # `shard_list` is sorted ascending by `first_key`. Find the + # rightmost entry whose `first_key` is `<= state_ref_hash`. + index = bisect.bisect_right( + shard_list, + state_ref_hash, + key=lambda entry: entry.first_key, ) + # `bisect_right` returns the index _after_ any existing + # matches, so we need `index - 1`. The first entry in the + # shard list has been asserted to be `b""`, which is always <= + # any other key, so `index >= 1` and thus we can always safely + # do `index - 1`. + assert index >= 1 + return shard_list[index - 1].shard_id def server_for_shard(self, shard_id: ShardId) -> ServerId: return self._server_by_shard[shard_id] diff --git a/reboot/aio/react.py b/reboot/aio/react.py index db5c6a10..d1039e98 100644 --- a/reboot/aio/react.py +++ b/reboot/aio/react.py @@ -25,11 +25,28 @@ ) from reboot.nodejs.python import should_print_stacktrace from reboot.settings import EVERY_LOCAL_NETWORK_ADDRESS +from reboot.wait_for_tasks import wait_for_tasks from typing import AsyncIterable, Optional logger = get_logger(__name__) +class _SuppressInvalidHandshakeFilter(logging.Filter): + """Drop spurious `opening handshake failed` logs from non-WebSocket + probes (e.g., HEAD requests or half-open TCP connections from a + devcontainer/Codespaces port forwarder) so they don't appear during + normal operation or shutdown. The `websockets` library logs every + failed handshake at `ERROR` with a full stack trace; legitimate + clients never produce `InvalidMessage`, so it's safe to filter.""" + + def filter(self, record: logging.LogRecord) -> bool: + if record.exc_info is not None: + exception = record.exc_info[1] + if isinstance(exception, websockets.exceptions.InvalidMessage): + return False + return True + + class ReactServicer(react_pb2_grpc.ReactServicer): """System service for serving requests from our code generated react readers. @@ -75,6 +92,11 @@ async def websockets_serve(): # We set the log level to `ERROR` on this logger because websockets is chatty! logger.setLevel(logging.ERROR) + # Also filter out `opening handshake failed` errors from + # clients that aren't speaking WebSocket (e.g., HEAD probes + # or half-open connections from a port forwarder). + logger.addFilter(_SuppressInvalidHandshakeFilter()) + async with websockets.serve( self.serve, EVERY_LOCAL_NETWORK_ADDRESS, @@ -98,12 +120,10 @@ async def websockets_serve(): async def stop(self): self._stop_websockets_serve.set() - try: - await self._websockets_serve_task - except: - # We're trying to stop so no need to propagate any - # exceptions. - pass + # The task stops on its own once the event above is set, so + # pass `cancel=False`. Any `CancelledError` raised here is + # this task's own cancellation and is propagated. + await wait_for_tasks([self._websockets_serve_task], cancel=False) async def serve(self, websocket): # Actually serve within an asyncio task while also creating diff --git a/reboot/aio/reboot.py b/reboot/aio/reboot.py index 6afd4222..00939407 100644 --- a/reboot/aio/reboot.py +++ b/reboot/aio/reboot.py @@ -223,7 +223,7 @@ def create_external_context( idempotency_required_reason: Optional[str] = None, app_internal: bool = False, ) -> ExternalContext: - """ Create an `ExternalContext` for use in tests. + """ Create an `ExternalContext`. app_internal: When true, the context is being used to represent a client internal to the application: essentially, to mock @@ -254,13 +254,7 @@ def create_initialize_context( bearer_token: Optional[str] = None, idempotency_seed: Optional[uuid.UUID] = None, ) -> InitializeContext: - """ Create an `ExternalContext` for use in tests. - - app_internal: When true, the context is being used to - represent a client internal to the application: essentially, to mock - traffic from another servicer in the same application. A - per-application secret will be passed as a header in the call. - """ + """ Create an `InitializeContext`.""" # Initializers are application-internal code, and should # identify themselves as such. caller_id = CallerID(application_id=self._application_id) diff --git a/reboot/aio/servers.py b/reboot/aio/servers.py index bf19191a..d32bd90a 100644 --- a/reboot/aio/servers.py +++ b/reboot/aio/servers.py @@ -178,7 +178,16 @@ async def run_application_initializer( traceback.print_exc() exception_str = f"{exception}" - logger.warning( + + # Use `logger.debug` for retryable errors, e.g., + # UNAVAILABLE, so we don't spam the users for behavior + # that is expected during startup, and + # `logger.warning` for everything else. + ( + logger.debug if + reboot.aio.aborted.is_grpc_retryable_exception(exception) + else logger.warning + )( f"{context.name} for application '{application_id}' failed " f"with {type(exception).__name__}{': ' + exception_str if len(exception_str) > 0 else ''}; " "will retry after backoff ..." @@ -214,11 +223,18 @@ def __init__( grpc_server = grpc.aio.server( options=GRPC_SERVER_OPTIONS, - interceptors=(interceptors or []) + aio_server_interceptors( + # The OpenTelemetry gRPC server interceptors must come first + # (outermost) so they create the per-RPC server span before + # any of the Reboot interceptors run. Otherwise the Reboot + # interceptors' `with span(...)` blocks pick up whatever + # OpenTelemetry "current span" was inherited from the task + # that started the gRPC server (typically + # `ServiceServer.start()`) instead of the actual RPC span. + interceptors=aio_server_interceptors( # We run a lot of health checks; they're not interesting to # trace and they'll spam our output. Suppress them. filter_=filters.negate(filters.health_check()) - ) + ) + (interceptors or []) ) try: @@ -578,11 +594,15 @@ async def start(self): # Once recovery is complete, we can start serving traffic. await super().start() - # And also start serving web framework traffic if requested. + # And also start serving web framework traffic if requested. Pass + # the application ID so its HTTP handlers can build app-internal + # contexts (e.g. the OAuth server persisting encrypted + # identity-provider tokens). self._http_port = await self._web_framework.start( self._server_id, self._http_port, self._channel_manager, + self._application_id, ) # And also start serving React traffic. diff --git a/reboot/aio/state_managers.py b/reboot/aio/state_managers.py index 670a16ca..0b4ede2c 100644 --- a/reboot/aio/state_managers.py +++ b/reboot/aio/state_managers.py @@ -5,6 +5,7 @@ import grpc import hashlib import inspect +import itertools import log.log import logging import math @@ -20,6 +21,7 @@ from datetime import datetime, timedelta, timezone from google.protobuf import any_pb2 from google.protobuf.message import Message +from google.protobuf.timestamp_pb2 import Timestamp from grpc.aio import AioRpcError from log.log import log_at_most_once_per from rbt.v1alpha1 import ( @@ -34,11 +36,13 @@ StateNotConstructed, TransactionParticipantFailedToCommit, TransactionParticipantFailedToPrepare, + Unavailable, ) from reboot.admin.export_import_converters import ExportImportItemConverters from reboot.aio import tracing from reboot.aio.aborted import Aborted, SystemAborted from reboot.aio.backoff import Backoff +from reboot.aio.concurrently import concurrently from reboot.aio.contexts import ( Context, Participants, @@ -75,6 +79,8 @@ DatabaseClient, ) from reboot.time import DateTimeWithTimeZone +from reboot.uuidv7 import uuid7_timestamp_ms +from reboot.wait_for_tasks import wait_for_tasks from struct import pack, unpack from typing import ( Any, @@ -110,10 +116,7 @@ def check_idempotency_key_not_expired(idempotency_key: uuid.UUID) -> None: if idempotency_key.version != 7: return - # Extract the 48-bit timestamp from the first 6 bytes of the UUID. - # UUIDv7 stores the Unix timestamp in milliseconds in big-endian format. - uuid_bytes = idempotency_key.bytes - timestamp_ms = int.from_bytes(uuid_bytes[:6], byteorder='big') + timestamp_ms = uuid7_timestamp_ms(uuid.UUID(bytes=idempotency_key.bytes)) now_ms = int(time.time() * 1000) @@ -212,6 +215,8 @@ def from_context( cls, context: Context, tasks_dispatcher: TasksDispatcher, + *, + using_restart_detection: bool = False, ) -> StateManager.Transaction: assert context.transaction_ids is not None assert context.transaction_coordinator_state_type is not None @@ -226,6 +231,7 @@ def from_context( state_ref=context._state_ref, tasks_dispatcher=tasks_dispatcher, idempotency_key=context.idempotency_key, + using_restart_detection=using_restart_detection, ) @classmethod @@ -283,6 +289,7 @@ def __init__( idempotency_key: Optional[uuid.UUID] = None, idempotent_mutations: Optional[dict[ uuid.UUID, database_pb2.IdempotentMutation]] = None, + using_restart_detection: bool = False, ) -> None: self._ids = transaction_ids @@ -307,6 +314,10 @@ def __init__( # `self.stored` to access this safely. self._stored = stored + # Whether restart detection is being used for this + # transaction. + self.using_restart_detection = using_restart_detection + # Whether or not we should abort this transaction when # asked by the coordinator. self.must_abort = False @@ -436,6 +447,21 @@ def finished(self) -> bool: """Returns whether or not the transaction is finished.""" return self._committed.done() + @property + def latest_timestamp_ms(self) -> Optional[int]: + """Return the latest known database timestamp (ms), e.g., for + UUID7 transaction ID generation. + """ + raise NotImplementedError() + + @abstractmethod + def preload( + self, + state_type_name: StateTypeName, + state_ref: StateRef, + ) -> None: + raise NotImplementedError() + @abstractmethod def add_to_server(self, server: grpc.aio.Server) -> None: """Hook for adding the state manager to a gRPC server.""" @@ -465,6 +491,8 @@ async def import_task( state_ref: StateRef, task: database_pb2.Task, middleware: Middleware, + *, + sync: bool = True, ) -> None: """Imports state for an actor, overwriting on collision.""" raise NotImplementedError @@ -475,6 +503,8 @@ async def import_actor( state_type: StateTypeName, state_ref: StateRef, state: Message, + *, + sync: bool = True, ) -> None: """Imports state for an actor, overwriting on collision.""" raise NotImplementedError @@ -486,6 +516,8 @@ async def import_sorted_map_entry( state_ref: StateRef, state: bytes, actor_converters: ExportImportItemConverters, + *, + sync: bool = True, ) -> None: """Imports state for an actor, overwriting on collision.""" raise NotImplementedError @@ -496,6 +528,8 @@ async def import_idempotent_mutation( state_type: StateTypeName, state_ref: StateRef, idempotent_mutation: database_pb2.IdempotentMutation, + *, + sync: bool = True, ) -> None: """Imports state for an actor, overwriting on collision.""" raise NotImplementedError @@ -1343,6 +1377,49 @@ def __init__( dict[StateRef, asyncio.Event]] = defaultdict(dict) + # Futures for state preloads. `preload()` creates a future + # that resolves with `Optional[bytes]` (raw data, or `None` if + # the state doesn't exist). `_load()` pops the future and + # awaits it before falling back to its own call into the + # database. + # + # NOTE: we've kept the original `_loads` events separate from + # `_state_preloads` futures to simplify the addition of the + # preload feature. + self._state_preloads: defaultdict[ + StateTypeName, + dict[StateRef, asyncio.Future[Optional[bytes]]], + ] = defaultdict(dict) + + # Futures for calls to the database to recovery idempotent + # mutations. `preload()` creates a future that resolves with + # the idempotent mutations; `check_for_idempotent_mutation()` + # also creates a future so concurrent callers await it instead + # of doing their own recovery but they resolve with `None` to + # signal to concurrent callers that nothing else needs to be + # done. + # + # Unlike for state loading, for idempotent mutations we + # combined load and preload into a single future because there + # was no pre-existing mechanism like there was for state + # loading (although arguably that was a bug). + self._idempotent_mutations_loads: defaultdict[ + StateTypeName, + dict[ + StateRef, + asyncio.Future[Optional[list[database_pb2. + IdempotentMutation]]], + ], + ] = defaultdict(dict) + + # Strong references to in-flight preload tasks. The asyncio + # event loop only keeps weak references to tasks, so a task + # created via `asyncio.create_task()` without a strong + # reference may be garbage collected mid-flight. We hold the + # tasks here and discard them via `add_done_callback` once + # they complete. + self._preload_tasks: set[asyncio.Task] = set() + # Any streaming readers. We're explicitly using a 'dict' as # the value instead of a 'defaultdict' because we don't want # to construct memory every time we "check" if there is a @@ -1357,6 +1434,113 @@ def __init__( self._idempotent_mutations: defaultdict[StateTypeName, dict[ StateRef, IdempotentMutations]] = defaultdict(dict) + # Recovery timestamp from the database, set during + # `recover()`. Represents the database's clock reading + # when this server recovered. Used for restart detection. + self._recovery_timestamp_ms: Optional[int] = None + + # Latest timestamp from the database, updated by + # piggybacked responses and periodic refresh. Used for + # UUID7 transaction ID generation. + self._latest_timestamp_ms: Optional[int] = None + + # Event used to cancel and restart the periodic refresh + # timer when a piggybacked timestamp is received. + self._timestamp_refresh_event = asyncio.Event() + + # Task for the periodic timestamp refresh loop. + self._timestamp_refresh_task: Optional[asyncio.Task] = None + + @property + def latest_timestamp_ms(self) -> Optional[int]: + return self._latest_timestamp_ms + + def _can_use_restart_detection( + self, + root_transaction_id: uuid.UUID, + state_type: StateTypeName, + ) -> bool: + """Return whether restart detection can be used for this transaction + to optimize performance. + + Restart detection requires all of: + + 1. A recovery timestamp from the database (i.e., the database + supports timestamps). + + 2. A UUIDv7 root transaction ID (which embeds a + timestamp). UUIDv4 transactions (from older coordinators + that don't yet generate UUIDv7 IDs) don't carry a + timestamp, so we can't compare against the recovery + timestamp. + + 3. A state type that is not `SortedMap`. `SortedMap` + operations use `colocated_[reverse_]range()` which reads + from RocksDB within the transaction, requiring the RocksDB + transaction to have been started at join time. See + https://github.com/reboot-dev/mono/issues/4019. + """ + return ( + self._recovery_timestamp_ms is not None and + root_transaction_id.version == 7 and + state_type != SORTED_MAP_TYPE_NAME + ) + + def _update_latest_timestamp( + self, + timestamp: Optional[Timestamp], + *, + from_refresh: bool = False, + ) -> None: + """Update the latest database timestamp if newer. Also + resets the periodic refresh countdown (unless this update + is from the refresh loop itself). + """ + if timestamp is None: + return + timestamp_ms = timestamp.ToMilliseconds() + if ( + self._latest_timestamp_ms is None or + timestamp_ms > self._latest_timestamp_ms + ): + self._latest_timestamp_ms = timestamp_ms + # Reset the periodic refresh countdown since we just + # got a fresh timestamp (unless this call IS from the + # refresh loop itself). + if not from_refresh: + self._timestamp_refresh_event.set() + + async def _refresh_timestamp_loop( + self, + interval_seconds: float = 2.0, + ) -> None: + """Periodically fetch the latest timestamp from the database. The + timer resets each time a piggybacked timestamp is received, so + the periodic RPC only fires after `interval_seconds` of + complete idleness. + """ + while not self._shutting_down: + self._timestamp_refresh_event.clear() + try: + # Wait `interval_seconds`, but can be cancelled early + # by `_update_latest_timestamp()` setting the event. + await asyncio.wait_for( + self._timestamp_refresh_event.wait(), + timeout=interval_seconds, + ) + # Event was set, i.e., we got a piggybacked timestamp + # so restart the countdown (no RPC needed). + continue + except asyncio.TimeoutError: + # `interval_seconds` have elapsed. Do the refresh RPC. + pass + try: + timestamp = await self._database_client.refresh_timestamp() + self._update_latest_timestamp(timestamp, from_refresh=True) + except Exception: + # Best effort; retry next interval. + pass + async def shutdown(self) -> None: """Shuts down this state manager, which includes cancellation of background tasks. @@ -1365,6 +1549,11 @@ async def shutdown(self) -> None: # we're working on shutting down that we are, in fact, shutting down. # They will get cancelled immediately after creation. self._shutting_down = True + + # Cancel the periodic timestamp refresh task. + if self._timestamp_refresh_task is not None: + self._timestamp_refresh_task.cancel() + # Cancel all previously created coordinator commit control loops. for task in self._coordinator_commit_control_loop_tasks.values(): task.cancel() @@ -1447,6 +1636,8 @@ async def import_task( state_ref: StateRef, task: database_pb2.Task, middleware: Middleware, + *, + sync: bool = True, ) -> None: async with self._mutator_locks[state_type][state_ref]: pending_task_effect = ( @@ -1461,6 +1652,7 @@ async def import_task( state_type=state_type, state_ref=state_ref, task=task, + sync=sync, ) if pending_task_effect is not None: middleware.tasks_dispatcher.dispatch([pending_task_effect]) @@ -1470,12 +1662,15 @@ async def import_actor( state_type: StateTypeName, state_ref: StateRef, state: Message, + *, + sync: bool = True, ) -> None: async with self._mutator_locks[state_type][state_ref]: await self._store( state_type=state_type, state_ref=state_ref, effects=Effects(state=state), + sync=sync, ) async def import_sorted_map_entry( @@ -1484,6 +1679,8 @@ async def import_sorted_map_entry( state_ref: StateRef, state: bytes, actor_converters: ExportImportItemConverters, + *, + sync: bool = True, ) -> None: # TODO: We do not support directly inserting a # SORTED_MAP_ENTRY_TYPE_NAME as a Message, so it must be stored @@ -1521,6 +1718,7 @@ async def import_sorted_map_entry( state_type=state_type, state_ref=state_ref, effects=effects, + sync=sync, ) async def import_idempotent_mutation( @@ -1528,12 +1726,17 @@ async def import_idempotent_mutation( state_type: StateTypeName, state_ref: StateRef, idempotent_mutation: database_pb2.IdempotentMutation, + *, + sync: bool = True, ) -> None: async with self._mutator_locks[state_type][state_ref]: # Recover idempotent mutations before trying to import any # since there is an invariant that we won't do a store on # a state before recovering its idempotent mutations. if state_ref not in self._idempotent_mutations[state_type]: + # NOTE: we assume that an import is done before any + # other concurrent calls and thus we don't bother with + # `self._idempotent_mutations_loads` machinery here. self._idempotent_mutations[state_type][state_ref] = ( await IdempotentMutations.recover( state_type, @@ -1546,6 +1749,7 @@ async def import_idempotent_mutation( state_type=state_type, state_ref=state_ref, idempotent_mutation=idempotent_mutation, + sync=sync, ) @function_span() @@ -1565,6 +1769,126 @@ async def _maybe_authorize( await authorize(state) + def preload( + self, + state_type_name: StateTypeName, + state_ref: StateRef, + ) -> None: + """Fire a `Preload` RPC to load both state and idempotent + mutations in a single round-trip to the database. This is a + fire-and-forget optimization that returns immediately; the + actual RPC runs as a background task. + + Results are delivered via futures in `_state_preloads` and + `_idempotent_mutations_loads`. `_load()` and + `check_for_idempotent_mutation()` await these futures before + falling back to their own RPCs. + + If state or idempotent mutations are already cached or already + being loaded, this call is a no-op. + + Unconsumed futures are auto-cleaned after 60 seconds. The + cleanup uses future identity so a stale timer never evicts a + newer preload's data. + """ + # Only try and load state if it's not already cached, not + # already being loaded, or not already preloaded. + state_future: Optional[asyncio.Future[Optional[bytes]]] = None + if ( + state_ref not in self._states[state_type_name] and + state_ref not in self._loads[state_type_name] and + state_ref not in self._state_preloads[state_type_name] + ): + state_future = asyncio.get_event_loop().create_future() + self._state_preloads[state_type_name][state_ref] = state_future + + # Only try and load idempotent mutations similarly. + idempotent_mutations_future: Optional[asyncio.Future[Optional[list[ + database_pb2.IdempotentMutation]]]] = None + if ( + state_ref not in self._idempotent_mutations[state_type_name] and + state_ref not in self._idempotent_mutations_loads[state_type_name] + ): + idempotent_mutations_future = ( + asyncio.get_event_loop().create_future() + ) + self._idempotent_mutations_loads[state_type_name][ + state_ref] = idempotent_mutations_future + + if state_future is None and idempotent_mutations_future is None: + # Nothing to preload! + return + + async def _preload(): + try: + response = await self._database_client.preload( + state_type_name, + state_ref, + skip_state=state_future is None, + skip_idempotent_mutations=( + idempotent_mutations_future is None + ), + ) + if response.HasField("timestamp"): + self._update_latest_timestamp(response.timestamp) + if state_future is not None: + state_future.set_result( + response.actor.state if response. + HasField("actor") else None + ) + if idempotent_mutations_future is not None: + idempotent_mutations_future.set_result( + response.idempotent_mutations + ) + # Wait 60 seconds for any consumer to come along and + # pop the futures, otherwise we'll pop them ourselves + # in the `finally`. This allows a method call that + # starts a preload but then gets cancelled, e.g., due + # to a network issue, can retry and consume the + # preload it previously started. + await asyncio.sleep(60) + except asyncio.CancelledError: + # Don't swallow cancellations, so the parent + # task can be cancelled properly. + raise + except: + # Preloading is best-effort, so just set an exception + # to signal to any waiters that they should try and + # load themselves. We use `set_exception()` (not + # `cancel()`) so consumers can distinguish when they + # are cancelled. + error = RuntimeError("Preload failed") + if state_future is not None and not state_future.done(): + state_future.set_exception(error) + if ( + idempotent_mutations_future is not None and + not idempotent_mutations_future.done() + ): + idempotent_mutations_future.set_exception(error) + finally: + # Clean up any unconsumed futures. Identity check + # ensures we only pop our own future, not a newer + # preload's since we might have waited for 60 seconds. + if ( + state_future is not None and + self._state_preloads[state_type_name].get(state_ref) + is state_future + ): + del self._state_preloads[state_type_name][state_ref] + if ( + idempotent_mutations_future is not None and + self._idempotent_mutations_loads[state_type_name]. + get(state_ref) is idempotent_mutations_future + ): + del self._idempotent_mutations_loads[state_type_name][ + state_ref] + + # Save a strong reference to the task so the GC doesn't + # collect it mid-flight; discard once it completes. + preload_task = asyncio.create_task(_preload()) + self._preload_tasks.add(preload_task) + preload_task.add_done_callback(self._preload_tasks.discard) + @function_span() async def _load( self, @@ -1619,6 +1943,10 @@ async def _load( # need to wait for it to complete. try: await ongoing_transaction + except asyncio.CancelledError: + # Don't swallow cancellations, so the parent + # task can be cancelled properly. + raise except: # Any errors coming from the transaction do not concern us # here. @@ -1693,11 +2021,44 @@ async def _load( span_name="reboot.aio.state_managers._load() " "- load_actor_state" ): - data: Optional[ - bytes - ] = await self._database_client.load_actor_state( - state_type_name, state_ref + data: Optional[bytes] = None + + # Check for a future from `preload()` before + # calling into the database ourselves. + # + # NOTE: we `pop()` the preload here beacuse + # any other concurrent callers and any other + # calls to `preload()` will see the event in + # `_loads` and wait on that. + preload = self._state_preloads[state_type_name].pop( + state_ref, None ) + if preload is not None: + try: + data = await preload + except asyncio.CancelledError: + # Propagate any cancellations, e.g., + # if the RPC that instigated this load + # was cancelled. Note that `preload` + # is a future not a task so it won't + # get cancelled and another caller can + # still await it. + raise + except Exception: + # Preload failed; need to make a call + # to the database which we signal by + # setting `preload` back to `None`. + preload = None + + # Only try and load if we didn't preload (or + # preload failed). Otherwise, if `data` is + # `None` it means we preloaded and the state + # does not yet exist. + if preload is None: + data = await self._database_client.load_actor_state( + state_type_name, state_ref + ) + if data is not None: with span( state_name=state_type_name, @@ -1956,48 +2317,75 @@ async def _store( ), ) - if transaction is not None and not transaction._stored: - # Regardless of what a dev may have specified, we want to - # ensure the write related to beginning a transaction - # survives machine failures. - # - # As has been suggested in other places in the code, in - # the future we can actually keep all writes after the - # first one in memory to improve performance further. But - # the first one needs to be persisted because it - # demarcates that this state is part of a transaction. - sync = True + # When using restart detection, we defer ALL writes to prepare + # time. Only prepared transactions can be recovered after a + # crash, so intermediate writes within a transaction don't + # need to be persisted — we can keep them in memory on the + # `Transaction` object (via `transaction.state`, + # `transaction.tasks`, and `transaction.idempotent_mutations`) + # and send everything in one batch at prepare time. This + # eliminates a sidecar RPC on every writer/transaction + # `complete()` call. + # + # The in-memory state updates below (updating + # `transaction.state`, streaming readers, idempotent + # mutations, etc.) still run so that readers within the + # transaction see the latest state, and so that the data is + # available at prepare time. + if transaction is not None and transaction.using_restart_detection: + pass # Skip the database call below. + else: + if transaction is not None and not transaction._stored: + # Regardless of what a dev may have specified, we want + # to ensure the write related to beginning a + # transaction survives machine failures. + sync = True - if constructor: - # Regardless of what a dev may have specified, we want to - # ensure that constructors can survive machine failures. - sync = True + if constructor: + # Regardless of what a dev may have specified, we want + # to ensure that constructors can survive machine + # failures. + sync = True - # Ensure that when creating a `SortedMap`, the `SortedMapEntry` - # state type's column family has also been created. - if state_type == SORTED_MAP_TYPE_NAME: - ensure_state_types_created.append(SORTED_MAP_ENTRY_TYPE_NAME) - - # NOTE: we _must_ store the data in the sidecar _before_ - # updating in memory state in case the store fails. - await self._database_client.store( - actor_upserts, - task_upserts, - colocated_upserts, - ensure_state_types_created, - database_pb2.Transaction( - state_type=state_type, - state_ref=state_ref.to_str(), - transaction_ids=[ - transaction_id.bytes for transaction_id in transaction.ids - ], - coordinator_state_type=transaction.coordinator_state_type, - coordinator_state_ref=transaction.coordinator_state_ref.to_str( - ), - ) if transaction is not None else None, - idempotent_mutation, - sync=sync, - ) + # Ensure that when creating a `SortedMap`, the + # `SortedMapEntry` state type's column family has also + # been created. + if state_type == SORTED_MAP_TYPE_NAME: + ensure_state_types_created.append( + SORTED_MAP_ENTRY_TYPE_NAME + ) + + # NOTE: we _must_ store the data in the database _before_ + # updating in memory state in case the store fails. + timestamp = await self._database_client.store( + actor_upserts, + task_upserts, + colocated_upserts, + ensure_state_types_created, + database_pb2.Transaction( + state_type=state_type, + state_ref=state_ref.to_str(), + transaction_ids=[ + transaction_id.bytes + for transaction_id in transaction.ids + ], + coordinator_state_type=transaction.coordinator_state_type, + coordinator_state_ref=transaction.coordinator_state_ref. + to_str(), + ) if transaction is not None else None, + idempotent_mutation, + sync=sync, + ) + + # Update latest timestamp from piggybacked response. + self._update_latest_timestamp(timestamp) + + # The database call actually happened, so mark the + # transaction as stored. (When using restart detection + # we took the `pass` branch above and writes are + # deferred to prepare, so the flag stays unset.) + if transaction is not None: + transaction.stored = True # Now that everything is stored we can update in memory # state. First we update state related to effects (if any). @@ -2063,9 +2451,12 @@ def validate_transaction_participant( ) -> None: """Helper to validate that a transaction participant is conforming to the requirements of being a participant.""" - # We *MUST* have persisted the transaction before returning any - # value back to the caller. Check that this is the case. - assert transaction._stored, 'Transaction not persisted' + # We *MUST* have persisted the transaction before returning + # any value back to the caller -- unless restart detection is + # active, in which case the store is deferred to prepare time. + assert transaction._stored or transaction.using_restart_detection, ( + 'Transaction not persisted' + ) # All outstanding RPCs must complete before returning (and # it should always be > 0 otherwise we have a bug). @@ -2192,6 +2583,46 @@ async def transactionally( "to the maintainers and tell us about it!" ) else: + # If we can use restart detection for this transaction, + # ensure that the last restart was before the start of the + # transaction to ensure consistency. + root_transaction_id = context.transaction_ids[0] + + using_restart_detection = self._can_use_restart_detection( + root_transaction_id, state_type + ) + + if using_restart_detection: + transaction_timestamp_ms = uuid7_timestamp_ms( + root_transaction_id + ) + if ( + self._recovery_timestamp_ms is not None and + self._recovery_timestamp_ms > transaction_timestamp_ms + ): + # Server recovered after this transaction started, + # which means it may have already participated and + # lost in-memory state when it restarted. Return + # UNAVAILABLE so the caller retries with a fresh + # transaction. + transaction_time = datetime.fromtimestamp( + transaction_timestamp_ms / 1000, + tz=timezone.utc, + ).isoformat() + recovery_time = datetime.fromtimestamp( + self._recovery_timestamp_ms / 1000, + tz=timezone.utc, + ).isoformat() + raise SystemAborted( + Unavailable(), + message=( + f"Transaction {root_transaction_id} was " + f"created at {transaction_time} but this " + f"server recovered at {recovery_time}; retry " + "required to ensure consistency." + ), + ) + # Acquire semaphore which might mean we queue until our turn. await self._participant_transactions_semaphore[state_type][ state_ref].acquire() @@ -2201,7 +2632,9 @@ async def transactionally( ) is None transaction = StateManager.Transaction.from_context( - context, tasks_dispatcher + context, + tasks_dispatcher, + using_restart_detection=using_restart_detection, ) self._participant_transactions[state_type][state_ref] = transaction @@ -2211,39 +2644,10 @@ async def transactionally( assert transaction is not None - # Make sure we've stored this transaction. - # - # We need the transaction to be stored _before_ sending a - # response to the user, otherwise they may read two different - # states if we crash. - # - # We also need to store the transaction _before_ making any - # calls which might call into RocksDB. In particular, - # `StateManager.colocated_read()`, which may be called from a - # reader, will read from RocksDB, and will want to use the - # transaction. See - # https://github.com/reboot-dev/mono/issues/4019 for more - # details. - # - # TODO: do this concurrently with yielding the transaction and - # calling into the developers method for better performance and - # then only wait for the store to complete before things like - # `StateManager.colocated_read()` or before we return (or if - # this `transactionally()` wraps a `transaction` method type - # then we make sure that we've stored before we run 2PC). - # - # Other optimizations to consider: - # - # Doing this store now will also allow us to skip actually - # doing any stores from a `writer` or a `transaction`, those - # could just update memory until we are asked to prepare. We - # can also look at optimistically performing a prepare after - # returning to the caller by introducing a mechanism that - # tracks not only the participants but also the number of - # times they've been used within the transaction and thus not - # having to do the prepare when requested for the common case - # that a state is only involved in a transaction once. - await self.transaction_participant_store(transaction) + # When restart detection is not being used, store the + # transaction to disk at join time (the legacy behavior). + if not transaction.using_restart_detection: + await self.transaction_participant_store(transaction) try: yield transaction @@ -2526,6 +2930,10 @@ async def watch_state(): assert context.react is not None context.react.event.set() + except asyncio.CancelledError: + # Don't swallow cancellations, so the parent + # task can be cancelled properly. + raise except BaseException as exception: state_or_exception.exception = exception @@ -2565,38 +2973,25 @@ async def iterator( try: yield states finally: - watch_state_task.cancel() + # Tear down in order: fully stop the `watch_state()` + # before cancelling `React.Query` calls, so + # the two don't race on `context.react`'s queriers. try: - await watch_state_task - except asyncio.CancelledError: - pass - except: - print( - 'Failed to cancel "watch state" task', - file=sys.stderr, - ) - pass - - # Make sure we stop all transitive `React.Query` calls. - # Use `asyncio.shield()` so that if this task is being - # cancelled (e.g., the gRPC client disconnected), the - # cleanup still runs to completion. - context_react_cancel_task = asyncio.ensure_future( - context.react.cancel(), - ) - try: - await asyncio.shield(context_react_cancel_task) - except asyncio.CancelledError: - # Wait for `react.cancel()`` to actually finish. - await context_react_cancel_task - # Propagate so the outer task actually cancels. - raise + await wait_for_tasks([watch_state_task], cancel=True) finally: - # We've already invalidated the `context.react` as part - # of `context.react.cancel()`, so now it is safe to set - # it to `None` to allow the GC to clean up any resources - # associated with it. - context.react = None + # Still execute the `React.Query` cancellation and + # cleanup even if we were cancelled while waiting for + # `watch_state()`. + react_cancel_task = asyncio.ensure_future( + context.react.cancel() + ) + try: + await wait_for_tasks( + [react_cancel_task], + cancel=False, + ) + finally: + context.react = None @asynccontextmanager_span( # We expect an `EffectValidationRetry` exception; that's not an error. @@ -2683,7 +3078,6 @@ async def complete(effects: Effects) -> None: workflow_iteration=context.workflow_iteration, constructor=context.constructor, ) - transaction.stored = True if effects.tasks is not None: assert all( task.task_id.state_type == @@ -3012,7 +3406,6 @@ async def complete(effects: Effects) -> None: workflow_iteration=context.workflow_iteration, constructor=context.constructor, ) - transaction.stored = True if not context.nested: # We're starting a new transaction here, so we should have @@ -3081,22 +3474,35 @@ async def complete(effects: Effects) -> None: if context.transaction_must_abort or transaction.must_abort: raise RuntimeError('Transaction must abort') - # Two phase commit: (1) prepare - await self._transaction_coordinator_prepare( - application_id=context.application_id, - channel_manager=context.channel_manager, - transaction_id=transaction.root_id, - participants=context.participants, + # Two phase commit: (1) prepare. + # + # Concurrently write the participant list to the database + # ("coordinator prepare") and send `Prepare` RPCs to all + # participants ("participants prepare"). + # + # In addition to the participant list the coordinator also + # writes `preparing=True` to the database so it can + # "re-prepare" if the server crashes and recovers. Once + # the coordinator determines that the transaction is + # prepared it will update the database with + # `preparing=False` so that it doesn't need to keep + # re-preparing if the server keeps crashing. + coordinator_prepare_timestamp, _ = await asyncio.gather( + self._database_client.transaction_coordinator_prepare( + transaction_id=transaction.root_id, + transaction_coordinator_state_ref=state_ref, + participants=context.participants, + ), + self._transaction_coordinator_prepare( + application_id=context.application_id, + channel_manager=context.channel_manager, + transaction_id=transaction.root_id, + participants=context.participants, + ), ) - # TODO(benh): if all of the participants were able to - # prepare successfully consider retrying the sidecar call - # more than once! - await self._database_client.transaction_coordinator_prepared( - transaction_id=transaction.root_id, - transaction_coordinator_state_ref=state_ref, - participants=context.participants - ) + if coordinator_prepare_timestamp is not None: + self._update_latest_timestamp(coordinator_prepare_timestamp) participants = self._coordinator_participants[transaction.root_id] @@ -3106,39 +3512,23 @@ async def complete(effects: Effects) -> None: participants.set_result(context.participants) except: - # Mark all the participants as need to be aborted. - context.participants.abort() - if context.nested: - # Raise the exception up to the outermost transaction, - # so the coordinator can handle the failure there. + # Mark all the participants as needing to be aborted, + # then raise the exception up to the outermost + # transaction so the coordinator can handle the + # failure (including participants abort, database + # coordinator record cleanup, etc). + context.participants.abort() raise - # Best effort try and tell the participants that the - # transaction was aborted; if they don't hear from us now - # they'll find out when they watch on their own. await self._transaction_coordinator_abort( application_id=context.application_id, channel_manager=context.channel_manager, transaction_id=transaction.root_id, participants=context.participants, + coordinator_state_ref=state_ref, ) - # To indicate that the transaction has aborted we set the - # participants, which will all be in - # `participants.should_abort`. - participants = self._coordinator_participants[transaction.root_id] - - # Not expecting `participants` to ever be cancelled, see: - # https://github.com/reboot-dev/mono/issues/3241 - assert not participants.cancelled() - - participants.set_result(context.participants) - - # Remove transaction so that participants "watch control - # loop" will determine that the transaction has aborted! - del self._coordinator_participants[transaction.root_id] - raise else: # Two phase commit: (2) commit @@ -3153,6 +3543,7 @@ async def complete(effects: Effects) -> None: transaction_id=transaction.root_id, participants=context.participants, coordinator_state_ref=state_ref, + durably_prepared=False, ), name= f'self._transaction_coordinator_commit_control_loop(...) in {__name__}', @@ -3216,13 +3607,94 @@ async def check_for_idempotent_mutation( # all data structures that are storing state via some LRU like # mechanism. if state_ref not in self._idempotent_mutations[state_type_name]: - self._idempotent_mutations[state_type_name][state_ref] = ( - await IdempotentMutations.recover( - state_type_name, - state_ref, - self._database_client, - ) + # Check for an in progress call to the database (from + # another concurrent caller or `preload()`) and await it + # if it exists. + # + # NOTE: we don't `pop()` here because we want any other + # concurrent callers or calls to `preload()` to not start + # redundant calls into the database. If the future + # resolves and it came from `preload()` (i.e., the future + # has a list of idempotent mutations) then we'll handle + # removing the future from + # `self._idempotent_mutations_loads`. + load = self._idempotent_mutations_loads[state_type_name].get( + state_ref ) + if load is not None: + idempotent_mutations: Optional[list[ + database_pb2.IdempotentMutation]] = None + try: + idempotent_mutations = await load + except asyncio.CancelledError: + # Propagate any cancellations, e.g., if the RPC + # that instigated this call to + # `check_for_idempotent_mutation()` was cancelled. + # + # NOTE: this doesn't cancel `load` since it is a + # future not a task. + raise + except Exception: + # Fall back to our own RPC. + pass + + # If `idempotent_mutations` is not `None` then + # `preload()` must have completed and we need to store + # the idempotent mutations, unless another waiter + # already took care of that and `state_ref` is now in + # `self._idempotent_mutations`. + if ( + idempotent_mutations is not None and state_ref + not in self._idempotent_mutations[state_type_name] + ): + self._idempotent_mutations[state_type_name][ + state_ref] = IdempotentMutations( + state_type_name=state_type_name, + state_ref=state_ref, + database_client=self._database_client, + idempotent_mutations=idempotent_mutations, + ) + # Also need to remove future that was created by + # `preload()`. + assert ( + self._idempotent_mutations_loads[state_type_name]. + get(state_ref) is load + ) + del self._idempotent_mutations_loads[state_type_name][ + state_ref] + + # Need to do our own loading, i.e., either no concurrent + # caller or no `preload()` or `preload()` failed. + if state_ref not in self._idempotent_mutations[state_type_name]: + # Create a future so concurrent callers and + # `preload()` don't also try. + load = asyncio.get_event_loop().create_future() + self._idempotent_mutations_loads[state_type_name][state_ref + ] = load + try: + self._idempotent_mutations[state_type_name][state_ref] = ( + await IdempotentMutations.recover( + state_type_name, + state_ref, + self._database_client, + ) + ) + load.set_result(None) + except: + # Loading failed, signal to any waiters that they + # should try themselves. We use `set_exception()` + # (not `cancel()`) so consumers can distinguish + # when they are cancelled. + load.set_exception(RuntimeError("Loading failed")) + raise + finally: + # We are responsible for cleaning up our future. + assert ( + self._idempotent_mutations_loads[state_type_name]. + get(state_ref) is load + ) + del self._idempotent_mutations_loads[state_type_name][ + state_ref] idempotent_mutation: Optional[database_pb2.IdempotentMutation] = ( await self._idempotent_mutations[state_type_name][state_ref].get( @@ -3306,47 +3778,130 @@ async def _transaction_coordinator_prepare( transaction_id: uuid.UUID, participants: Participants, ): - """Helper for a transaction coordinator performing the prepare step of - two phase commit.""" - try: - # TODO(benh): do in parallel! - for (state_type, state_ref) in participants.should_prepare(): - # Getting a channel to the actor should succeed, since its - # participation in a transaction that is in the prepare step - # indicates that (1) it is up and running and (2) we should be - # able to reach it. If either of those are untrue, that's - # sufficient reason to fail the transaction. - # TODO(rjh, benh): consider being more generous with temporarily - # unavailable participants. - channel = channel_manager.get_channel_to_state( - state_type, - state_ref, - # Since this is a Reboot-internal process that the user - # may not be aware is running in the background, logging - # user-visible errors is unhelpful. - unresolvable_state_log_level=logging.DEBUG, - ) - - stub = transactions_pb2_grpc.ParticipantStub(channel) + """Sends `Prepare` RPCs to all participants concurrently, retrying + each participant individually until getting a definitive + response. + + Passes `abort_via_response=True` in the request so that new + participants use definitive responses via fields in + `PrepareResponse` (`abort`, `restart_detected`) rather than + the legacy semantics of raising an exception causing the RPC + to abort with a gRPC status code. This eliminates ambiguity + with proxy/middleware errors: any RPC-level failure is always + non-definitive and retried. + + Old participants that don't understand + `abort_via_response=True` will ignore the field and abort the + RPC as before. The coordinator treats any such RPC error as + non-definitive and retries until the participant gets + updated. In practice this should only happen once during the + upgrade and be very short lived. + + Raises `SystemAborted` ONLY for definitive participant + responses: + + - `SystemAborted(TransactionParticipantFailedToPrepare())` + if the participant set `abort=True` (no such transaction, + different transaction, or `must_abort`). + + - `SystemAborted(Unavailable())` if the participant set + `abort=True` and `restart_detected=True` (the participant + restarted since this transaction began but it can be + retried). + + Any other exception is retried internally with backoff; this + method only returns or raises once every participant has + reached a definitive state. This narrow contract is what + `_transaction_coordinator_reprepare` relies on when it treats + any `SystemAborted` as "safe to abort". + """ + async def prepare(state_type: StateTypeName, state_ref: StateRef): + # We retry indefinitely on any non-definitive outcome: we + # cannot report the transaction as aborted to the caller + # unless we have a definitive "never prepared" answer from + # the participant, because this participant may in fact + # prepare successfully on retry and a future recovery + # could then commit the whole transaction. + backoff = Backoff() + while True: try: - await stub.Prepare( + # Getting a channel to the actor should succeed, + # since its participation in a transaction that is + # in the prepare step indicates that (1) it is up + # and running and (2) we should be able to reach + # it. If either is temporarily untrue, we'll retry + # below. + channel = channel_manager.get_channel_to_state( + state_type, + state_ref, + # Since this is a Reboot-internal process that + # the user may not be aware is running in the + # background, logging user-visible errors is + # unhelpful. + unresolvable_state_log_level=logging.DEBUG, + ) + + stub = transactions_pb2_grpc.ParticipantStub(channel) + + response = await stub.Prepare( transactions_pb2.PrepareRequest( - transaction_id=transaction_id.bytes + transaction_id=transaction_id.bytes, + abort_via_response=True, ), metadata=Headers( application_id=application_id, state_ref=state_ref, ).to_grpc_metadata(), ) - except AioRpcError as error: + except asyncio.CancelledError: + raise + except BaseException: + # Any RPC or network error is non-definitive + # (could be a proxy failure, an old server that + # doesn't understand `abort_via_response=True`, + # etc). Retry after backoff. + await backoff() + continue + + # RPC succeeded — definitive outcomes are conveyed via + # response fields, never via gRPC status codes. This + # eliminates ambiguity with proxy/middleware errors. + if not response.abort: + return + + if response.restart_detected: + transaction_time = ( + datetime.fromtimestamp( + uuid7_timestamp_ms(transaction_id) / 1000, + tz=timezone.utc, + ).isoformat() + ) + recovery_time = ( + response.recovery_timestamp.ToDatetime( + tzinfo=timezone.utc + ).isoformat() + ) if response.HasField( + "recovery_timestamp" + ) else "unknown" raise SystemAborted( - TransactionParticipantFailedToPrepare(), - message=error.details(), + Unavailable(), + message= + f"No pending transaction for state type '{state_type}' " + f"state '{state_ref}': the server must have " + f"restarted since transaction {transaction_id} began " + f"(transaction created at {transaction_time}, server " + f"recovered at {recovery_time}).", ) from None - except: - # TODO(benh): handle failed transaction! - raise + + raise SystemAborted( + TransactionParticipantFailedToPrepare(), + ) from None + + await concurrently( + prepare(state_type, state_ref) + for (state_type, state_ref) in participants.should_prepare() + ) async def _transaction_coordinator_commit( self, @@ -3358,9 +3913,8 @@ async def _transaction_coordinator_commit( ): """Helper for a transaction coordinator performing the commit step of two phase commit.""" - # TODO(benh): do this for loop and the one below all in parallel! - for (state_type, state_ref) in participants.should_commit(): + async def commit(state_type: StateTypeName, state_ref: StateRef): # Do our best to tell the participant that the transaction has # committed. If we fail (e.g. because we can't get a channel), no # big deal; the caller will retry this at a later point. @@ -3391,8 +3945,7 @@ async def _transaction_coordinator_commit( message=error.details(), ) from None - # Need to abort all of the participants that should be aborted. - for (state_type, state_ref) in participants.should_abort(): + async def abort(state_type: StateTypeName, state_ref: StateRef): # Do our best to tell the participant that, from their # perspective, the transaction has aborted. If we fail # (e.g. because we can't get a channel), no big deal; the @@ -3424,6 +3977,19 @@ async def _transaction_coordinator_commit( message=error.details(), ) from None + await concurrently( + itertools.chain( + ( + commit(state_type, state_ref) + for (state_type, state_ref) in participants.should_commit() + ), + ( + abort(state_type, state_ref) + for (state_type, state_ref) in participants.should_abort() + ), + ) + ) + async def _transaction_coordinator_abort( self, *, @@ -3431,10 +3997,42 @@ async def _transaction_coordinator_abort( channel_manager: _ChannelManager, transaction_id: uuid.UUID, participants: Participants, + coordinator_state_ref: Optional[StateRef], ): - for (state_type, state_ref) in participants.should_prepare(): - # TODO(benh): do in parallel! + """Aborts a transaction for which we are the coordinator: + (1) marks all participants as "to abort"; (2) cleans up + the "preparing" record that may be in the database; (3) + best-effort sends `Abort` RPCs to all participants; and + (4) publishes the aborted participants to the + `_coordinator_participants` future so any participant + "watch control loops" observe the abort, then deletes + the future entry.""" + # Mark all the participants as need to be aborted. + participants.abort() + + # Clean up the "preparing" record that may be in the database + # when the coordinator stored the transaction participants + # (and `preparing=True`). The database write may or may not + # have completed (it could have been cancelled), but the + # database supports deleting missing keys. + # + # If we crash before doing this we'll recover the transaction + # in the database and our commit control loop will try to + # re-prepare only to re-discover the transaction should abort, + # which will then properly abort any other participants and + # clean up the database. If this gets cleaned up before we + # have succesfully aborted any other participants then their + # watch should signal that the transaction doesn't exist and + # they will abort themselves. + await self._database_client.transaction_coordinator_cleanup( + transaction_id=transaction_id, + coordinator_state_ref=coordinator_state_ref, + ) + # Best effort try and tell the participants that the + # transaction was aborted; if they don't hear from us now + # they'll find out when they watch on their own. + async def abort(state_type: StateTypeName, state_ref: StateRef): try: # Getting a channel to the participant may fail; notably this # can happen after the coordinator restarts if it tries to abort @@ -3460,12 +4058,34 @@ async def _transaction_coordinator_abort( state_ref=state_ref, ).to_grpc_metadata(), ) + except asyncio.CancelledError: + # Don't swallow cancellations, so the parent task can + # be cancelled properly. + raise except: # NOTE: aborting is best effort abort, each # participant is responsible for checking back in with # the coordinator as well in case we were unable to # send an abort to them. - continue + pass + + await concurrently( + abort(state_type, state_ref) + for (state_type, state_ref) in participants.should_prepare() + ) + + # To indicate that the transaction has aborted we set the + # participants, which will all be in + # `participants.should_abort`. + # + # Not expecting `participants` to ever be cancelled, see: + # https://github.com/reboot-dev/mono/issues/3241 + assert not self._coordinator_participants[transaction_id].cancelled() + self._coordinator_participants[transaction_id].set_result(participants) + + # Remove transaction so that participants "watch control loop" + # will determine that the transaction has aborted! + del self._coordinator_participants[transaction_id] async def _transaction_participant_watch( self, @@ -3608,13 +4228,68 @@ async def Prepare( transaction_id = uuid.UUID(bytes=request.transaction_id) transaction = self._participant_transactions[state_type].get(state_ref) if transaction is None: + if ( + self._can_use_restart_detection(transaction_id, state_type) and + # `_can_use_restart_detection` ensures that + # `self._recovery_timestamp_ms` is not `None`. + self._recovery_timestamp_ms # type: ignore[operator] + > uuid7_timestamp_ms(transaction_id) + ): + assert self._recovery_timestamp_ms is not None # for mypy + if request.abort_via_response: + recovery_timestamp = Timestamp() + recovery_timestamp.FromMilliseconds( + self._recovery_timestamp_ms, + ) + return transactions_pb2.PrepareResponse( + abort=True, + restart_detected=True, + recovery_timestamp=recovery_timestamp, + ) + transaction_time = datetime.fromtimestamp( + uuid7_timestamp_ms(transaction_id) / 1000, + tz=timezone.utc, + ).isoformat() + recovery_time = datetime.fromtimestamp( + self._recovery_timestamp_ms / 1000, + tz=timezone.utc, + ).isoformat() + # Legacy path for old coordinators. + raise RuntimeError( + f"No pending transaction for state type '{state_type}' " + f"state '{state_ref.id}': the server must have " + f"restarted since transaction {transaction_id} began " + f"(transaction created at {transaction_time}, server " + f"recovered at {recovery_time})." + ) + if request.abort_via_response: + logger.warning( + f"Failed to prepare transaction '{transaction_id}': " + f"No pending transaction for state type '{state_type}' " + f"state '{state_ref.id}'" + ) + return transactions_pb2.PrepareResponse(abort=True) + # Legacy path for old coordinators. raise RuntimeError( f"No pending transaction for state type '{state_type}' " f"state '{state_ref.id}'" ) if transaction.root_id != transaction_id: + if request.abort_via_response: + logger.warning( + f"Failed to prepare transaction '{transaction_id}': " + "Pending transaction id differs" + ) + return transactions_pb2.PrepareResponse(abort=True) + # Legacy path for old coordinators. raise RuntimeError('Pending transaction id differs') else: + # If the transaction is already prepared (e.g., because we + # recovered it from the database after a restart and the + # coordinator is re-preparing), just return success. + if transaction.prepared(): + return transactions_pb2.PrepareResponse() + # All RPCs that are part of this transaction should # have completed which means all streaming readers # should have completed! @@ -3623,19 +4298,16 @@ async def Prepare( await transaction.tasks_dispatcher.validate(transaction.tasks) if transaction.must_abort: + if request.abort_via_response: + logger.warning( + f"Failed to prepare transaction '{transaction_id}': " + "Transaction must abort" + ) + return transactions_pb2.PrepareResponse(abort=True) + # Legacy path for old coordinators. raise RuntimeError('Transaction must abort') - # TODO(benh): take advantage of the fact that only - # prepared transactions can be recovered and instead of - # storing state in the sidecar after each write, instead - # store the state in memory and pass the updated state and - # tasks to the sidecar here. - - await self._database_client.transaction_participant_prepare( - state_type, state_ref - ) - - transaction.prepare() + await self.transaction_participant_prepare(transaction) return transactions_pb2.PrepareResponse() @@ -3734,7 +4406,67 @@ async def transaction_participant_store( state_ref=transaction.state_ref, transaction=transaction, ) - transaction.stored = True + + async def transaction_participant_prepare( + self, + transaction: StateManager.Transaction, + ): + state_type = transaction.state_type + state_ref = transaction.state_ref + + async with transaction.lock: + if not transaction.finished(): + # When using restart detection, all intermediate + # writes were deferred (no database calls). Now at + # prepare time we derive what we need from the + # transaction's in-memory state and send it all at + # once to the database. This is the only database + # write for the entire transaction. + # + # When NOT using restart detection, the writes were + # already applied to the RocksDB transaction during + # intermediate `_store()` calls, so we just need to + # call prepare on the existing transaction. + if transaction.using_restart_detection: + timestamp = await self._database_client.transaction_participant_prepare( + state_type, + state_ref, + transaction=( + database_pb2.Transaction( + state_type=state_type, + state_ref=state_ref.to_str(), + transaction_ids=[ + transaction_id.bytes + for transaction_id in transaction.ids + ], + coordinator_state_type=( + transaction.coordinator_state_type + ), + coordinator_state_ref=( + transaction.coordinator_state_ref.to_str() + ), + ) + ), + state=( + transaction.state.SerializeToString() + if transaction.state is not None else None + ), + task_upserts=[ + task.to_sidecar_task() + for task in transaction.tasks + ], + idempotent_mutations=( + list(transaction.idempotent_mutations.values()) if + len(transaction.idempotent_mutations) > 0 else None + ), + ) + else: + timestamp = await self._database_client.transaction_participant_prepare( + state_type, state_ref + ) + + self._update_latest_timestamp(timestamp) + transaction.prepare() async def transaction_participant_commit( self, transaction: StateManager.Transaction @@ -3749,9 +4481,10 @@ async def transaction_participant_commit( async with transaction.lock: if not transaction.finished(): # TODO(benh): add test case for sidecar failure! - await self._database_client.transaction_participant_commit( + timestamp = await self._database_client.transaction_participant_commit( state_type, state_ref ) + self._update_latest_timestamp(timestamp) # NOTE: invariant here is everything after this line # SHOULD NOT RAISE! @@ -3893,11 +4626,13 @@ async def transaction_participant_abort( # check if the transaction has finished. async with transaction.lock: if not transaction.finished(): - - # We only reach out to the sidecar to abort in case we have - # persisted anything. - if transaction.stored: - # TODO(benh): add test case for sidecar failure! + # If the prepare was cancelled before ever reaching + # the database when using restart detection we won't + # have anything to abort but the database tolerates + # "no transaction found" as a no-op. If the prepare is + # still in progress, the database ensures it is + # serialized with this abort. + if transaction.stored or transaction.using_restart_detection: await self._database_client.transaction_participant_abort( state_type, state_ref ) @@ -4077,6 +4812,12 @@ async def recover( shard_ids=[shard.shard_id for shard in self._shards], ) + # Extract recovery timestamp for restart detection. + if recover_response.HasField("timestamp"): + recovery_timestamp_ms = recover_response.timestamp.ToMilliseconds() + self._recovery_timestamp_ms = recovery_timestamp_ms + self._latest_timestamp_ms = recovery_timestamp_ms + # Need to declare this up here as it is used in multiple scopes. middleware: Optional[Middleware] = None @@ -4151,17 +4892,22 @@ async def recover( for (transaction_id, coordinator) in [ (uuid.UUID(transaction_id), coordinator) for (transaction_id, coordinator - ) in recover_response.prepared_transaction_coordinators.items() + ) in recover_response.transaction_coordinators.items() ]: - participants = coordinator.participants + participants = Participants.from_sidecar(coordinator.participants) coordinator_state_ref = StateRef( coordinator.state_ref ) if coordinator.state_ref else None self._coordinator_participants[transaction_id] = asyncio.Future() - self._coordinator_participants[transaction_id].set_result( - participants - ) + + if not coordinator.preparing: + # Fully prepared: the commit control loop can proceed + # directly to commit, so publish the participants + # future now. + self._coordinator_participants[transaction_id].set_result( + participants + ) self._coordinator_commit_control_loop_tasks[ transaction_id @@ -4170,13 +4916,29 @@ async def recover( application_id=application_id, channel_manager=channel_manager, transaction_id=transaction_id, - participants=Participants.from_sidecar(participants), + participants=participants, coordinator_state_ref=coordinator_state_ref, + # If `coordinator.preparing` is true the + # coordinator recorded participants but crashed + # before confirming all were prepared; the commit + # control loop will re-prepare them before + # continuing. + durably_prepared=not coordinator.preparing, + needs_reprepare=coordinator.preparing, ), name= f'self._transaction_coordinator_commit_control_loop(...) in {__name__}', ) + # Start the periodic timestamp refresh loop only if the + # database supports timestamps which it must if we have a + # recovery timestamp. + if self._recovery_timestamp_ms is not None: + self._timestamp_refresh_task = asyncio.create_task( + self._refresh_timestamp_loop(), + name=f"self._refresh_timestamp_loop() in {__name__}", + ) + def _get_task_effect_from_sidecar_task( self, middleware: Middleware, @@ -4202,6 +4964,75 @@ def _get_task_effect_from_sidecar_task( # methods. raise RuntimeError('Error parsing task request') from e + async def _transaction_coordinator_reprepare( + self, + *, + application_id: ApplicationId, + channel_manager: _ChannelManager, + transaction_id: uuid.UUID, + participants: Participants, + coordinator_state_ref: Optional[StateRef], + ) -> bool: + """Recovery for coordinators that stored participants but didn't + confirm all were prepared. Re-prepares all participants. + Returns `True` if all participants re-prepared successfully, + or `False` if the transaction was aborted (and cleaned up) + because a participant gave a definitive response proving it + never durably prepared this transaction. + """ + prepared = False + backoff = Backoff() + while True: + try: + await self._transaction_coordinator_prepare( + application_id=application_id, + channel_manager=channel_manager, + transaction_id=transaction_id, + participants=participants, + ) + prepared = True + break + except asyncio.CancelledError: + raise + except SystemAborted: + # Per `_transaction_coordinator_prepare`'s contract, + # any `SystemAborted` it raises means some participant + # has definitively no durable prepared state for this + # transaction. If *any* participant never durably + # prepared, the original prepare cannot have fully + # succeeded, so the coordinator never reached the path + # where it returned success to the caller. Therefore + # it is safe to abort. + break + except BaseException as exception: + # `_transaction_coordinator_prepare` is supposed + # to have already retried any non-definitive + # errors. If we end up here something unexpected + # happened; log and retry with backoff. + logger.warning( + f"Failed to re-prepare transaction '{transaction_id}': " + f"{exception}" + ) + await backoff() + continue + + if not prepared: + # A participant gave a definitive "never prepared" + # response. Abort. + await self._transaction_coordinator_abort( + application_id=application_id, + channel_manager=channel_manager, + transaction_id=transaction_id, + participants=participants, + coordinator_state_ref=coordinator_state_ref, + ) + return False + + # All participants re-prepared successfully. Set the + # participants future so watches work correctly. + self._coordinator_participants[transaction_id].set_result(participants) + return True + async def _transaction_coordinator_commit_control_loop( self, *, @@ -4210,7 +5041,56 @@ async def _transaction_coordinator_commit_control_loop( transaction_id: uuid.UUID, participants: Participants, coordinator_state_ref: Optional[StateRef], + durably_prepared: bool, + needs_reprepare: bool = False, ) -> None: + if needs_reprepare: + assert not durably_prepared, ( + "A coordinator transaction that is durably prepared " + "should never need to re-prepare!" + ) + # On recovery, the coordinator recorded participants but + # crashed before confirming all were prepared. Re-prepare + # all participants before continuing with the commit. + if not await self._transaction_coordinator_reprepare( + application_id=application_id, + channel_manager=channel_manager, + transaction_id=transaction_id, + participants=participants, + coordinator_state_ref=coordinator_state_ref, + ): + # The transaction was aborted, and cleanup already + # happened in `_transaction_coordinator_reprepare`. + del self._coordinator_commit_control_loop_tasks[transaction_id] + return + + # If this coordinator transaction is not durably prepared, + # i.e., the database still has "preparing=True" we must now + # overwrite that with `preparing=False` to mark the + # transaction as fully prepared. + # + # This MUST complete _before_ committing so that all + # participants remain valid in the event we crash (again) and + # need to re-prepare (again). + if not durably_prepared: + assert coordinator_state_ref is not None + backoff = Backoff() + while True: + try: + timestamp = await self._database_client.transaction_coordinator_prepared( + transaction_id=transaction_id, + transaction_coordinator_state_ref=coordinator_state_ref, + participants=participants, + ) + if timestamp is not None: + self._update_latest_timestamp(timestamp) + break + except asyncio.CancelledError: + raise + except BaseException: + await backoff() + continue + backoff = Backoff() is_retry = False while True: diff --git a/reboot/aio/stubs.py b/reboot/aio/stubs.py index 176d7382..131ca6ec 100644 --- a/reboot/aio/stubs.py +++ b/reboot/aio/stubs.py @@ -215,6 +215,7 @@ def __init__( transaction_coordinator_state_ref=transaction_coordinator_state_ref, bearer_token=bearer_token, caller_id=caller_id, + internal_call=context is not None, ) def _should_call_retry_unavailable( @@ -277,6 +278,7 @@ async def _call( metadata: Optional[GrpcMetadata] = None, idempotency_key: Optional[uuid.UUID] = None, per_iteration: bool = False, + always: bool = False, bearer_token: Optional[str] = None, ) -> AsyncIterator[Awaitable[ResponseT] | AsyncIterable[ResponseT]]: """Helper for making an RPC, handling any user-defined errors, and @@ -298,10 +300,7 @@ async def _call( # and there isn't a user-provided idempotency key, add one so # that we can retry safely. if not reader and self._context is None and idempotency_key is None: - assert ( - isinstance(self._idempotency_manager, ExternalContext) and - not isinstance(self._idempotency_manager, InitializeContext) - ), "Calls with `InitializeContext` should already have idempotency key" + assert isinstance(self._idempotency_manager, ExternalContext) # We may perform transparent retries on this call. That # means it needs to be idempotent, and for non-readers that @@ -310,13 +309,22 @@ async def _call( # generate one now. Keys generated here don't need to be # reproducible, they just need to be unique, and it should # expire after 7 days. - idempotency_key = make_expiring_idempotency_key() + # + # Although, for `InitializeContext`, we don't generate a + # retry-safety idempotency key so that we don't pay for + # idempotent mutations. Retryable errors (e.g., + # UNAVAILABLE) will still be properly handled by the outer + # `initialize` retry handler in `servers.py` which re-runs + # the entire `initialize` function. + if not isinstance(self._idempotency_manager, InitializeContext): + idempotency_key = make_expiring_idempotency_key() # If we're a `WorkflowContext` all non-readers should have an - # idempotency key already. + # idempotency key already (unless `.always()` which intentionally + # has no key). assert ( not isinstance(self._context, WorkflowContext) or - (reader or idempotency_key is not None) + (reader or idempotency_key is not None or always) ) if idempotency_key is not None: diff --git a/reboot/aio/tests.py b/reboot/aio/tests.py index db964c2a..771d17a6 100644 --- a/reboot/aio/tests.py +++ b/reboot/aio/tests.py @@ -4,6 +4,11 @@ import time import unittest from reboot.aio.applications import Application +from reboot.aio.auth.oauth_providers import ( + OAuthProvider, + OAuthProviderSelector, +) +from reboot.aio.auth.oauth_server import signing_secret from reboot.aio.auth.token_verifiers import TokenVerifier from reboot.aio.contexts import EffectValidation from reboot.aio.external import InitializeContext @@ -13,15 +18,28 @@ from reboot.aio.servicers import Servicer from reboot.run_environments import in_nodejs from reboot.settings import ( + ENVVAR_REBOOT_CRYPTO_ROOT_KEYS, ENVVAR_REBOOT_ENABLE_EVENT_LOOP_BLOCKED_WATCHDOG, - ENVVAR_REBOOT_OAUTH_SIGNING_SECRET, ) from typing import Any, Awaitable, Callable, Optional, Sequence, overload from unittest import mock -# Hardcoded signing secret for unit tests. Not a real -# secret — only used in-process for test JWT minting. -_TEST_OAUTH_SIGNING_SECRET = "reboot-test-signing-secret" +# Hardcoded cryptographic root keys for unit tests. Not real secrets — +# only used in-process (libraries derive their keys from these, e.g., +# the OAuth signing key for test JWT minting). +_TEST_CRYPTO_ROOT_KEYS = "v1:reboot-test-root-key" + + +class OAuthProviderForTest(OAuthProviderSelector): + """`OAuthProviderSelector` that always returns the given provider, + regardless of environment. Used by the test `Application`, since + tests run in a single, known environment.""" + + def __init__(self, provider: OAuthProvider): + self._provider = provider + + def _select(self) -> OAuthProvider: + return self._provider def assert_called_twice_with( @@ -68,11 +86,12 @@ def __init__(self) -> None: # must be set before `start()` which is where # `monitor_event_loop()` reads the env var. os.environ[ENVVAR_REBOOT_ENABLE_EVENT_LOOP_BLOCKED_WATCHDOG] = 'true' - # Set a signing secret so that `OAuthServer` can be used in - # tests without manual env patching. Mirrors what `rbt dev` does - # for local development. - os.environ[ENVVAR_REBOOT_OAUTH_SIGNING_SECRET] = ( - _TEST_OAUTH_SIGNING_SECRET + # Set cryptographic root keys so that `OAuthServer` (and any other + # key-deriving library) can be used in tests without manual env + # patching. Mirrors what `rbt dev` does for local development. + os.environ.setdefault( + ENVVAR_REBOOT_CRYPTO_ROOT_KEYS, + _TEST_CRYPTO_ROOT_KEYS, ) def make_valid_oauth_access_token( @@ -113,7 +132,7 @@ def make_jwt(self, **claims: Any) -> str: """ return jwt.encode( claims, - _TEST_OAUTH_SIGNING_SECRET, + signing_secret(), algorithm="HS256", ) diff --git a/reboot/aio/tracing.py b/reboot/aio/tracing.py index d7b7d299..a6e561f9 100644 --- a/reboot/aio/tracing.py +++ b/reboot/aio/tracing.py @@ -31,11 +31,8 @@ from reboot.aio.headers import TRACEPARENT_HEADER, TRACESTATE_HEADER, Headers from reboot.aio.once import Once from reboot.aio.signals import install_cleanup -from reboot.settings import ( - ENVVAR_RBT_NAME, - ENVVAR_REBOOT_NODEJS, - ENVVAR_REBOOT_TRACE_LEVEL, -) +from reboot.run_environments import application_name +from reboot.settings import ENVVAR_REBOOT_NODEJS, ENVVAR_REBOOT_TRACE_LEVEL from typing import Any, AsyncIterator, Callable, Optional logger = get_logger(__name__) @@ -158,14 +155,7 @@ def _start(process_name: str): def start(process_name: Optional[str] = None, server_id: Optional[str] = None): if process_name is None: - # TODO(rjh): make sure all paths in the Cloud set this - # environment variable (not just customer containers - # using the `rbt serve` CLI, but also the bazel-built - # images), and make sure that it is obeyed (i.e. the - # CLI considers it an alternative to the `--name` - # argument). Then replace the fallback below with an - # `assert`. - process_name = os.environ.get(ENVVAR_RBT_NAME, "Reboot Application") + process_name = application_name() if server_id is not None: # Server IDs have an application ID prefix; that's redundant diff --git a/reboot/api.py b/reboot/api.py index 0b40ff18..b113abeb 100644 --- a/reboot/api.py +++ b/reboot/api.py @@ -428,10 +428,21 @@ def _pydantic_to_proto( assert len(list_args) == 1 list_item_type = list_args[0] + list_item_origin = get_origin(list_item_type) + for list_item in input: - # For 'RepeatedScalarContainer' there is not 'add' method, - # so we need to handle primitive type containers separately. - if list_item_type not in (int, float, str, bool): + # For 'RepeatedScalarContainer' there is no 'add' method, so + # we need to handle scalar containers separately. + if list_item_type in (int, float, str, bool): + # Primitive type, we can append directly. + output.items.append(list_item) + elif list_item_origin is Literal: + # 'Literal' items become a 'repeated' Protobuf 'enum', so + # append the literal's index like a plain scalar. + output.items.append( + _pydantic_to_proto(list_item, list_item_type, int) + ) + else: output_item = output.items.add() nested_output = _pydantic_to_proto( list_item, @@ -440,9 +451,6 @@ def _pydantic_to_proto( ) assert isinstance(nested_output, Message) output_item.CopyFrom(nested_output) - else: - # Primitive type, we can append directly. - output.items.append(list_item) return output elif input_type_or_origin is dict: # Ensure 'output_type' is a class (not a Union of primitive @@ -462,11 +470,22 @@ def _pydantic_to_proto( assert len(dict_args) == 2 dict_value_type = dict_args[1] + dict_value_origin = get_origin(dict_value_type) + for key, value in input.items(): # For map fields, we use dictionary-style assignment instead # of 'add()' or 'append()'. Scalar map values use direct # assignment while message map values should use 'CopyFrom()'. - if dict_value_type not in (int, float, str, bool): + if dict_value_type in (int, float, str, bool): + # Primitive type, we can assign directly. + output.record[key] = value + elif dict_value_origin is Literal: + # 'Literal' values become a 'map' with a Protobuf + # 'enum' value type; assign the literal's index. + output.record[key] = _pydantic_to_proto( + value, dict_value_type, int + ) + else: output_value = output.record[key] nested_output = _pydantic_to_proto( value, @@ -475,9 +494,6 @@ def _pydantic_to_proto( ) assert isinstance(nested_output, Message) output_value.CopyFrom(nested_output) - else: - # Primitive type, we can assign directly. - output.record[key] = value return output elif issubclass(input_type_or_origin, Model): assert isinstance(output_type, type(Message)) diff --git a/reboot/application/BUILD.bazel b/reboot/application/BUILD.bazel new file mode 100644 index 00000000..b4dc579d --- /dev/null +++ b/reboot/application/BUILD.bazel @@ -0,0 +1,22 @@ +load("@rbt_pypi//:requirements.bzl", "requirement") +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "application_py", + srcs = [ + "__init__.py", + "servicer.py", + ], + srcs_version = "PY3", + visibility = ["//visibility:public"], + deps = [ + requirement("aiohttp"), + "//rbt/v1alpha1/application:application_py_proto", + "//rbt/v1alpha1/application:application_py_reboot", + "//reboot:run_environments_py", + "//reboot/aio:contexts_py", + "//reboot/aio:workflows_py", + "//reboot/aio/auth:authorizers_py", + "@com_github_reboot_dev_reboot//log:log_py", + ], +) diff --git a/reboot/application/__init__.py b/reboot/application/__init__.py new file mode 100644 index 00000000..9fb88ec8 --- /dev/null +++ b/reboot/application/__init__.py @@ -0,0 +1,22 @@ +from rbt.v1alpha1.application.application_rbt import Application, ExamplePrompt +from reboot.application.servicer import ApplicationServicer +from reboot.run_environments import application_name + +__all__ = [ + # Re-exporting `ExamplePrompt` so developers don't have to reach + # into the generated protobuf package. + "ExamplePrompt", + "ref", + "servicers", +] + + +def ref() -> Application.WeakReference: + """Returns a reference to the "singleton" `Application` keyed on the + application name. + """ + return Application.ref(application_name()) + + +def servicers(): + return [ApplicationServicer] diff --git a/reboot/application/servicer.py b/reboot/application/servicer.py new file mode 100644 index 00000000..28d3e299 --- /dev/null +++ b/reboot/application/servicer.py @@ -0,0 +1,287 @@ +import aiohttp +import asyncio +import rbt.v1alpha1.errors_pb2 +from log.log import get_logger +from rbt.v1alpha1.application.application_rbt import ( + Application, + DisuseRootKeyVersionRequest, + DisuseRootKeyVersionResponse, + GetRequest, + GetResponse, + InitializeRequest, + InitializeResponse, + RecordConnectionRequest, + RecordConnectionResponse, + UseRootKeyVersionRequest, + UseRootKeyVersionResponse, + WatchTunnelsRequest, + WatchTunnelsResponse, +) +from reboot.aio.auth.authorizers import allow, allow_if, is_app_internal +from reboot.aio.contexts import ReaderContext, WorkflowContext, WriterContext +from reboot.run_environments import running_rbt_dev +from typing import Optional + +logger = get_logger(__name__) + +# ngrok's local management API will run on the default port 4040. We +# only ever read from `/api/tunnels`. +_NGROK_TUNNELS_URL = "http://localhost:4040/api/tunnels" + +# cloudflared's local management endpoint, exposed when the user +# passes `--metrics localhost:4040`. For quick tunnels (`--url ...`) +# it returns `{"hostname": ".trycloudflare.com"}`. +_CLOUDFLARED_QUICKTUNNEL_URL = "http://localhost:4040/quicktunnel" + +# How long to wait between polling for tunnels, in seconds. Tight +# enough that the wizard feels alive when the user starts a tunnel, +# but loose enough that we're not pounding the local APIs while the +# app is otherwise idle. +_TUNNEL_POLL_INTERVAL_SECONDS = 5 + +# Per-request timeout for tunnel-provider probes. Short because both +# providers run locally; if the socket doesn't accept in 2s the +# provider probably isn't there. +_TUNNEL_FETCH_TIMEOUT = aiohttp.ClientTimeout(total=2) + + +async def _fetch_ngrok_public_url(port: int) -> Optional[str]: + """Ask ngrok which tunnel (if any) is forwarding to our local + port and return its `public_url`. Returns `None` on any failure + (ngrok not running, request error, no matching tunnel, JSON + didn't have the expected shape, etc.). + """ + addr = f"http://localhost:{port}" + try: + async with aiohttp.ClientSession( + timeout=_TUNNEL_FETCH_TIMEOUT + ) as session: + async with session.get(_NGROK_TUNNELS_URL) as response: + if response.status != 200: + return None + body = await response.json() + except (aiohttp.ClientError, asyncio.TimeoutError, ValueError): + return None + tunnels = body.get("tunnels") if isinstance(body, dict) else None + if not isinstance(tunnels, list): + return None + for tunnel in tunnels: + if not isinstance(tunnel, dict): + continue + config = tunnel.get("config", {}) + if isinstance(config, dict) and config.get("addr") == addr: + public_url = tunnel.get("public_url") + return public_url if isinstance(public_url, str) else None + return None + + +async def _fetch_cloudflared_public_url() -> Optional[str]: + """Ask cloudflared's metrics endpoint for the current quick + tunnel's hostname and return it as `https://`. Returns + `None` on any failure (cloudflared not running with `--metrics`, + not a quick tunnel, request error, etc.). Unlike the ngrok probe + we can't filter by local port — cloudflared's `/quicktunnel` + only exposes the public hostname, not the forwarded address — so + we trust whatever it reports. + """ + try: + async with aiohttp.ClientSession( + timeout=_TUNNEL_FETCH_TIMEOUT + ) as session: + async with session.get(_CLOUDFLARED_QUICKTUNNEL_URL) as response: + if response.status != 200: + return None + # `content_type=None` skips aiohttp's strict + # `application/json` check; cloudflared serves the + # response as `text/plain`, which would otherwise + # raise `ContentTypeError`. + body = await response.json(content_type=None) + except (aiohttp.ClientError, asyncio.TimeoutError, ValueError): + return None + hostname = body.get("hostname") if isinstance(body, dict) else None + if not isinstance(hostname, str) or not hostname: + return None + return f"https://{hostname}" + + +def _is_running_rbt_dev(*, context, **kwargs): + """Authorizer callable that allows only when the process is being + served by `rbt dev`. + """ + if running_rbt_dev(): + return rbt.v1alpha1.errors_pb2.Ok() + return rbt.v1alpha1.errors_pb2.PermissionDenied() + + +class ApplicationServicer(Application.Servicer): + + def authorizer(self): + return Application.Authorizer( + # `get` is public so the application's root page can be + # served without authentication. + get=allow(), + # `initialize` is called by the application's `initialize` + # which is an app-internal context (in dev *and* prod), so + # gate it on `is_app_internal`. + initialize=allow_if(all=[is_app_internal]), + # Record connection is only done in dev. + record_connection=allow_if(all=[_is_running_rbt_dev]), + # Root-key usage markers are written by consumers + # app-internally. + use_root_key_version=allow_if(all=[is_app_internal]), + disuse_root_key_version=allow_if(all=[is_app_internal]), + ) + + async def initialize( + self, + context: WriterContext, + request: InitializeRequest, + ) -> InitializeResponse: + self.state.title = request.title + # Mirror presence: a developer who didn't pass `description=` + # leaves it unset rather than storing an empty string. + if request.HasField("description"): + self.state.description = request.description + else: + self.state.ClearField("description") + self.state.mcp = request.mcp + # Replace the example prompts wholesale so we have the each + # latest on each boot. + del self.state.example_prompts[:] + self.state.example_prompts.extend(request.example_prompts) + # Store the port so `watch_tunnels` can read the latest (it + # may change across re-inits, e.g. a different `rbt dev` + # port). + self.state.port = request.port + # Schedule the tunnel-watcher exactly once, and only in dev — + # there are only local ngrok/cloudflared providers to poll in + # dev. `initialize` is called on every restart to synchronize + # the latest application's values, so we have a flag to make + # sure we only schedule it once. + if running_rbt_dev() and not self.state.watch_tunnels_scheduled: + await self.ref().schedule().watch_tunnels(context) + self.state.watch_tunnels_scheduled = True + return InitializeResponse() + + async def get( + self, + context: ReaderContext, + request: GetRequest, + ) -> GetResponse: + # Return only the the fields that are safe to expose to an + # unauthenticated caller. + dev = running_rbt_dev() + return GetResponse( + title=self.state.title, + description=( + self.state.description + if self.state.HasField("description") else None + ), + ngrok_public_url=( + self.state.ngrok_public_url + if dev and self.state.HasField("ngrok_public_url") else None + ), + cloudflared_public_url=( + self.state.cloudflared_public_url if dev and + self.state.HasField("cloudflared_public_url") else None + ), + connections=self.state.connections if dev else [], + example_prompts=self.state.example_prompts, + mcp=self.state.mcp, + dev=dev, + ) + + async def record_connection( + self, + context: WriterContext, + request: RecordConnectionRequest, + ) -> RecordConnectionResponse: + # `forwarded_host` and `user_agent` are both required — the + # caller only records a complete (host, user-agent) pair. + # Find (or create) the entry for this host, then dedupe- + # append the user-agent. The list stays insertion-ordered; we + # don't expect many distinct user agents per host in practice + # (one per chat client that's ever called through), so a + # linear scan is fine. + for connection in self.state.connections: + if connection.forwarded_host == request.forwarded_host: + if request.user_agent not in connection.user_agents: + connection.user_agents.append(request.user_agent) + return RecordConnectionResponse() + connection = self.state.connections.add( + forwarded_host=request.forwarded_host, + ) + connection.user_agents.append(request.user_agent) + return RecordConnectionResponse() + + async def use_root_key_version( + self, + context: WriterContext, + request: UseRootKeyVersionRequest, + ) -> UseRootKeyVersionResponse: + # Idempotent: at most one marker per `(consumer, version)`. + for usage in self.state.root_key_usages: + if ( + usage.consumer == request.consumer and + usage.version == request.version + ): + return UseRootKeyVersionResponse() + self.state.root_key_usages.add( + consumer=request.consumer, + version=request.version, + ) + return UseRootKeyVersionResponse() + + async def disuse_root_key_version( + self, + context: WriterContext, + request: DisuseRootKeyVersionRequest, + ) -> DisuseRootKeyVersionResponse: + # Idempotent: remove the marker if present. + remaining = [ + usage for usage in self.state.root_key_usages if not ( + usage.consumer == request.consumer and + usage.version == request.version + ) + ] + del self.state.root_key_usages[:] + self.state.root_key_usages.extend(remaining) + return DisuseRootKeyVersionResponse() + + @classmethod + async def watch_tunnels( + cls, + context: WorkflowContext, + request: WatchTunnelsRequest, + ) -> WatchTunnelsResponse: + # NOTE: we deliberately don't use `context.loop` or memoize + # the fetches: there's nothing here we need to replay + # deterministically — we just want the latest tunnel URLs in + # the state — so a `while`/`sleep` is clearer. + while True: + # Read the port fresh each iteration (`.always()`, so we + # don't reuse a memoized value) since a re-`Initialize` + # may have changed it. + state = await Application.ref().always().read(context) + port = state.port + + ngrok_public_url = await _fetch_ngrok_public_url(port) + cloudflared_public_url = await _fetch_cloudflared_public_url() + + async def write(state: Application.State) -> None: + # Always record the latest values, mirroring presence + # (an unset URL clears a provider that lost its + # tunnel). + if ngrok_public_url is not None: + state.ngrok_public_url = ngrok_public_url + else: + state.ClearField("ngrok_public_url") + if cloudflared_public_url is not None: + state.cloudflared_public_url = cloudflared_public_url + else: + state.ClearField("cloudflared_public_url") + + # We always want to record the latest values. + await Application.ref().always().write(context, write) + + await asyncio.sleep(_TUNNEL_POLL_INTERVAL_SECONDS) diff --git a/reboot/benchmarks/construct/package-lock.json b/reboot/benchmarks/construct/package-lock.json index aa1394b2..42cb8e7d 100644 --- a/reboot/benchmarks/construct/package-lock.json +++ b/reboot/benchmarks/construct/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "@bufbuild/protobuf": "1.10.1", - "@reboot-dev/reboot": "1.0.4", + "@reboot-dev/reboot": "1.1.0", "@supercharge/promise-pool": "^3.2.0", "parse-duration": "2.1.3", "uuid": "11.1.0" @@ -507,15 +507,15 @@ } }, "node_modules/@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -544,9 +544,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -3092,13 +3092,13 @@ "optional": true }, "@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "requires": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -3115,9 +3115,9 @@ } }, "@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "requires": { "@scarf/scarf": "1.4.0", "typescript": "5.4.5", diff --git a/reboot/benchmarks/construct/package.json b/reboot/benchmarks/construct/package.json index ff91c65d..dfe355c7 100644 --- a/reboot/benchmarks/construct/package.json +++ b/reboot/benchmarks/construct/package.json @@ -11,7 +11,7 @@ "type": "module", "dependencies": { "@bufbuild/protobuf": "1.10.1", - "@reboot-dev/reboot": "1.0.4", + "@reboot-dev/reboot": "1.1.0", "@supercharge/promise-pool": "^3.2.0", "uuid": "11.1.0", "parse-duration": "2.1.3" diff --git a/reboot/cli/BUILD.bazel b/reboot/cli/BUILD.bazel index cb6af589..d2c86ae7 100644 --- a/reboot/cli/BUILD.bazel +++ b/reboot/cli/BUILD.bazel @@ -43,6 +43,7 @@ py_library( requirement("aiofiles"), requirement("pyprctl"), requirement("cryptography"), + requirement("python-dotenv"), ":directories_py", ":generate_py", ":monkeys_py", diff --git a/reboot/cli/dev.py b/reboot/cli/dev.py index a25195b1..6f283281 100644 --- a/reboot/cli/dev.py +++ b/reboot/cli/dev.py @@ -6,6 +6,7 @@ import os import random import reboot.aio.tracing +import secrets import shutil import signal import sys @@ -14,6 +15,7 @@ from colorama import Fore from cryptography import x509 from cryptography.hazmat.backends import default_backend +from dotenv import dotenv_values from enum import Enum from grpc_health.v1 import health_pb2, health_pb2_grpc from opentelemetry.sdk.environment_variables import ( @@ -61,6 +63,7 @@ ENVVAR_RBT_NODEJS, ENVVAR_RBT_SERVERS, ENVVAR_RBT_STATE_DIRECTORY, + ENVVAR_REBOOT_CRYPTO_ROOT_KEYS, ENVVAR_REBOOT_LOCAL_ENVOY, ENVVAR_REBOOT_LOCAL_ENVOY_PORT, ENVVAR_REBOOT_OAUTH_SIGNING_SECRET, @@ -96,6 +99,15 @@ def transform(self, value: str): return value.split('=', 1) +def _load_env_file(path: str) -> dict[str, str]: + """Read a `--env-file` into a dict of environment variables. + """ + return { + key: value if value is not None else '' + for key, value in dotenv_values(path).items() + } + + def add_application_options(subcommand: SubcommandParser) -> None: """Helper that adds options used to run Reboot applications.""" subcommand.add_argument( @@ -147,6 +159,17 @@ def _register_dev_run(parser: ArgumentParser): add_application_options(parser.subcommand('dev run')) + parser.subcommand('dev run').add_argument( + '--env-file', + type=str, + help=( + "path to a '.env' file whose 'KEY=VALUE' lines are set " + "as environment variables before running the " + "application; standard '.env' syntax is supported." + ), + non_empty_string=True, + ) + parser.subcommand('dev run').add_argument( '--application-name', type=str, @@ -660,6 +683,12 @@ async def _run_jaeger(): 'receivers.otlp.protocols.http.endpoint=0.0.0.0:4318', '--set', 'receivers.otlp.protocols.grpc.endpoint=0.0.0.0:4317', + # Disable clock skew adjustments, this is all running on the + # same host so even though it is across processes we shouldn't + # expect any clock skew and without this Jaeger adjusts some + # spans to completely incorrect and misleading places. + '--set', + 'extensions.jaeger_query.max_clock_skew_adjust=0s', # Suppress all output from Jaeger; its logs are not # developer-facing. stdout=asyncio.subprocess.DEVNULL, @@ -731,22 +760,39 @@ async def _read_until( When the function is done reading, it should set the result of the future to complete. """ - fd = file_handle.fileno() + future: asyncio.Future[ResultT] = asyncio.Future() + + # The file handle may be unpollable: closed, or redirected from + # a regular file (Linux `epoll` rejects those with + # `PermissionError`). In that case, block forever; callers run + # us inside `_wait_for_first_completed` and will cancel us when + # a sibling task fires. + try: + fd = file_handle.fileno() + except (OSError, ValueError): + return await future + loop = asyncio.get_running_loop() def read(future: asyncio.Future[ResultT]): - value = file_handle.read(1) + try: + value = file_handle.read(1) + except (OSError, ValueError): + loop.remove_reader(fd) + return if not value: - # The input is closed, and will never be readable. Remove our reader - # rather than continuing to poll it. + # The input is closed, and will never be readable. + # Remove our reader rather than continuing to poll it. loop.remove_reader(fd) f(value, future) - future: asyncio.Future[ResultT] = asyncio.Future() - # Add an async file descriptor reader to our running event # loop so that we know when a key has been pressed. - loop.add_reader(fd, read, future) + try: + loop.add_reader(fd, read, future) + except OSError: + return await future + try: return await future finally: @@ -1325,30 +1371,80 @@ async def __dev_run( EffectValidation, ).name - # Set a signing secret for the MCP OAuth server. For local - # dev we use the application name (insecure, but convenient). - # Fall back to a fixed string when `--name` wasn't provided. - if ENVVAR_REBOOT_OAUTH_SIGNING_SECRET not in env: - env[ENVVAR_REBOOT_OAUTH_SIGNING_SECRET] = ( - args.application_name or "reboot-dev" - ) + if ENVVAR_REBOOT_CRYPTO_ROOT_KEYS not in env: + if args.application_name is not None: + # Reboot's managed cryptographic root keys, from which + # libraries derive their own keys (the MCP OAuth signing key, + # the `reboot.std.ciphertext` key-encryption key, ...). Persist + # a random value in the application's state directory so it is + # stable across restarts but wiped by `rbt dev expunge` (which + # also deletes signed-in users' state, so derived OAuth tokens + # should become invalid and push clients back through the OAuth + # flow). The `v1:` prefix carries the version inline so the + # value can later grow to a comma-separated, newest-first list + # during rotation. + root_keys_path = ( + dot_rbt_dev_directory(args, parser) / args.application_name / + "crypto-root-keys" + ) + if root_keys_path.exists(): + root_keys = root_keys_path.read_text() + else: + root_keys = f"v1:{secrets.token_urlsafe(32)}" + root_keys_path.parent.mkdir(parents=True, exist_ok=True) + root_keys_path.write_text(root_keys) + env[ENVVAR_REBOOT_CRYPTO_ROOT_KEYS] = root_keys + else: + # No application name means no per-app state directory to + # persist into; fall back to a fixed value. + env[ENVVAR_REBOOT_CRYPTO_ROOT_KEYS] = "v1:reboot-dev" + + # Backwards compatibility: old application images do an unconditional + # fail-fast check for the legacy OAuth signing secret on startup. Keep + # setting it (to the same value as the root keys) so those apps still + # boot; current code reads `ENVVAR_REBOOT_CRYPTO_ROOT_KEYS` instead. + env.setdefault( + ENVVAR_REBOOT_OAUTH_SIGNING_SECRET, + env[ENVVAR_REBOOT_CRYPTO_ROOT_KEYS], + ) if tracing == Tracing.JAEGER: # TODO: dynamic port. See comment in `_run_jaeger()`. env[OTEL_EXPORTER_OTLP_TRACES_ENDPOINT] = "localhost:4317" env[OTEL_EXPORTER_OTLP_TRACES_INSECURE] = "true" - # Also include all environment variables from '--env='. - for (key, value) in args.env or []: - env[key] = value + # Compose the environment for an application run. Rebuilding from + # the frozen base `env` ensures that keys removed from the file are + # removed from the result. + def compose_env() -> dict[str, str]: + composed = env.copy() + + # Include environment variables from `--env-file=`. The + # `--env=` values below override anything also set here. A + # missing file is ignored here. + if args.env_file is not None and os.path.isfile(args.env_file): + composed.update(_load_env_file(args.env_file)) + + # Also include all environment variables from '--env='. + for (key, value) in args.env or []: + composed[key] = value + + # If 'PYTHONPATH' is not explicitly set, we'll set it to the + # specified generated code directory. + if ( + 'PYTHONPATH' not in composed and + generate_python_directory is not None + ): + pythonpath = generate_python_directory + for proto_directory in generate_proto_directories or []: + pythonpath = pythonpath + os.pathsep + proto_directory + composed['PYTHONPATH'] = pythonpath + + return composed - # If 'PYTHONPATH' is not explicitly set, we'll set it to the - # specified generated code directory. - if 'PYTHONPATH' not in env and generate_python_directory is not None: - pythonpath = generate_python_directory - for proto_directory in generate_proto_directories or []: - pythonpath = pythonpath + os.pathsep + proto_directory - env['PYTHONPATH'] = pythonpath + # Warn once, at startup, if `--env-file` points at a missing file. + if args.env_file is not None and not os.path.isfile(args.env_file): + terminal.warn(f"'--env-file' '{args.env_file}' does not exist.") if not args.chaos: # When running with '--terminate-after-health-check', we @@ -1428,7 +1524,14 @@ async def __dev_run( # NOTE: we don't want to watch `application` yet as it # might be getting generated if we're using `transpile`. - async with watcher.watch(args.watch or []) as watch_event_task: + # + # We also watch the `--env-file` (if any) as its own event + # task so that editing it triggers an application restart. + async with watcher.watch( + args.watch or [] + ) as watch_event_task, watcher.watch( + [args.env_file] if args.env_file is not None else [] + ) as env_file_event_task: # Transpile TypeScript if requested. if args.transpile is not None: @@ -1448,6 +1551,7 @@ async def __dev_run( ) completed = await _wait_for_first_completed( watch_event_task, + env_file_event_task, protos_event_task, rc_file_event_task, ) @@ -1507,6 +1611,7 @@ async def __dev_run( completed = await _wait_for_first_completed( application_event_task, watch_event_task, + env_file_event_task, protos_event_task, rc_file_event_task, ) @@ -1594,7 +1699,7 @@ def have_rc_file_or_protos_or_watch_event(): [application] if not auto_transpilation else ts_input_paths ) as application_event_task, _run( application if not auto_transpilation else str(bundle), - env=env, + env=compose_env(), launcher=launcher, subprocesses=subprocesses, application_started_event=application_started_event, @@ -1619,6 +1724,7 @@ def have_rc_file_or_protos_or_watch_event(): completed = await _wait_for_first_completed( application_event_task, watch_event_task, + env_file_event_task, protos_event_task, process_wait_task, induce_chaos_task, @@ -1675,6 +1781,7 @@ def have_rc_file_or_protos_or_watch_event(): completed = await _wait_for_first_completed( application_event_task, watch_event_task, + env_file_event_task, protos_event_task, rc_file_event_task, expunge_task, @@ -1721,6 +1828,11 @@ def ask_for_confirmation(question: str) -> bool: ): terminal.fail("Expunge cancelled") + # Removing this directory also removes the persisted + # `crypto-root-keys` written by `rbt dev run`, so the next run + # generates fresh root keys and anything derived from them changes — + # e.g. previously minted OAuth tokens become invalid, forcing + # connected clients back through the OAuth flow. application_directory = dot_rbt_dev / args.application_name if not application_directory.exists(): terminal.warn( diff --git a/reboot/cli/export_import.py b/reboot/cli/export_import.py index a053214e..e9f74f1d 100644 --- a/reboot/cli/export_import.py +++ b/reboot/cli/export_import.py @@ -1,4 +1,5 @@ import argparse +import time import traceback from pathlib import Path from rbt.v1alpha1.admin import export_import_pb2_grpc @@ -69,6 +70,7 @@ async def do_export(args: argparse.Namespace) -> int: terminal.fail(f"Destination directory `{dest_dir}` must be empty.\n\n") export_import = _export_import_stub(args) + export_start_time = time.monotonic() try: await export_import_client.do_export( export_import, dest_dir, admin_token=args.admin_credential @@ -78,7 +80,9 @@ async def do_export(args: argparse.Namespace) -> int: f"Failed to export: {e}\n\nPlease report this issue to the maintainers." ) - terminal.info(f"Exported to: `{dest_dir}`") + terminal.info( + f"Exported to: `{dest_dir}` in {time.monotonic() - export_start_time:.2f} seconds" + ) return 0 @@ -90,6 +94,7 @@ async def do_import(args: argparse.Namespace) -> int: terminal.fail(f"Source directory `{src_dir}` must be non-empty.\n\n") export_import = _export_import_stub(args) + import_start_time = time.monotonic() try: await export_import_client.do_import( export_import, src_dir, admin_token=args.admin_credential @@ -100,7 +105,9 @@ async def do_import(args: argparse.Namespace) -> int: f"Failed to import: {type(e)}: {e}\n\nPlease report this issue to the maintainers." ) - terminal.info(f"Imported from: `{src_dir}`") + terminal.info( + f"Imported from: `{src_dir}` in {time.monotonic() - import_start_time:.2f} seconds" + ) return 0 diff --git a/reboot/cli/init/templates/backend_package.json.j2 b/reboot/cli/init/templates/backend_package.json.j2 index ce0ffc26..2786f83e 100644 --- a/reboot/cli/init/templates/backend_package.json.j2 +++ b/reboot/cli/init/templates/backend_package.json.j2 @@ -4,7 +4,7 @@ "private": true, "type": "module", "dependencies": { - "@reboot-dev/reboot": "1.0.4", + "@reboot-dev/reboot": "1.1.0", "typescript": "^5.5.2" } } diff --git a/reboot/cli/init/templates/package.json.j2 b/reboot/cli/init/templates/package.json.j2 index b9b7f945..c1c7eca1 100644 --- a/reboot/cli/init/templates/package.json.j2 +++ b/reboot/cli/init/templates/package.json.j2 @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", "@types/jest": "^27.5.2", "@types/node": "^20.11.5", "@types/react": "^19.2.1", diff --git a/reboot/cli/serve.py b/reboot/cli/serve.py index bd1c239a..d53346e2 100644 --- a/reboot/cli/serve.py +++ b/reboot/cli/serve.py @@ -344,13 +344,20 @@ async def serve_run( env[key] = value # If 'PYTHONPATH' is not explicitly set, we'll set it to the - # specified generated code directory. We want to get the directory from - # 'rbt generate' flags, which user might have specified in '.rbtrc'. + # specified generated code directory plus each proto directory. + # We want to get the directories from 'rbt generate' flags, + # which user might have specified in '.rbtrc'. Including the + # proto directories matches the behavior of `rbt dev run`, so + # that schemas defined as pydantic Models in `api/` are + # importable from servicer code under `rbt serve run` too. if 'PYTHONPATH' not in env and parser.dot_rc is not None: generate_parser = parser_factory(['rbt', 'generate']) generate_args, _ = generate_parser.parse_args() if generate_args.python is not None: - env['PYTHONPATH'] = generate_args.python + pythonpath = generate_args.python + for proto_directory in (generate_args.proto_directories or []): + pythonpath = pythonpath + os.pathsep + proto_directory + env['PYTHONPATH'] = pythonpath if not await aiofiles.os.path.isfile(application): terminal.fail(f"Missing application at '{application}'") diff --git a/reboot/create-ui/package.json b/reboot/create-ui/package.json index ea345dad..a3d8e85d 100644 --- a/reboot/create-ui/package.json +++ b/reboot/create-ui/package.json @@ -1,6 +1,6 @@ { "name": "@reboot-dev/create-ui", - "version": "1.0.4", + "version": "1.1.0", "description": "Scaffold React implementation for Reboot AI Chat App UIs", "type": "commonjs", "bin": { diff --git a/reboot/create-ui/src/templates.ts b/reboot/create-ui/src/templates.ts index 23badce2..570f5df9 100644 --- a/reboot/create-ui/src/templates.ts +++ b/reboot/create-ui/src/templates.ts @@ -70,8 +70,8 @@ export function packageJson( dependencies: { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-react": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", react: "^18.2.0", "react-dom": "^18.2.0", zod: "^3.25.0", diff --git a/reboot/create-ui/templates/vite.config.ts.tmpl b/reboot/create-ui/templates/vite.config.ts.tmpl index b300a4a2..c5d2f538 100644 --- a/reboot/create-ui/templates/vite.config.ts.tmpl +++ b/reboot/create-ui/templates/vite.config.ts.tmpl @@ -29,18 +29,12 @@ export default defineConfig(({ command, mode }) => { // Dev server configuration. // - // UIs use a double iframe architecture: - // MCP Host -> srcdoc (origin=null) -> iframe (origin=localhost:9991) - // - // The inner iframe loads from Envoy ("/__/web/**"), which proxies - // to Vite. Because the inner iframe has a real origin, Vite's URLs - // work normally. `base: "/__/web/"` ensures all paths route through - // Envoy. - // - // Hot Module Replacement works automatically: Vite's client connects - // to the page's origin, and Envoy proxies WebSocket upgrades to - // Vite. This also works with tunnels (ngrok) since the tunnel - // points to Envoy. + // In some cases the Reboot server acts as a relay for this server, + // fetching the HTML from Vite and serving it to the MCP Client. In + // that setup it rewrites any paths to be absolute, since MCP UIs are + // rendered in a `srcdoc` iframe that has no base. The following + // config is chosen to keep Hot Module Replacement (HMR) working in + // that setup. if (command === "serve") { const port = parseInt(process.env.RBT_VITE_PORT || "4444", 10); @@ -55,8 +49,24 @@ export default defineConfig(({ command, mode }) => { // Listen on all interfaces since requests come through // Envoy (and tunnels). host: true, + // Disable the host-allowlist check, so requests forwarded by + // ngrok-style tunnels are accepted regardless of their `Host` + // header. allowedHosts: true, + // Reflect whatever `Origin` the browser sent into + // `Access-Control-Allow-Origin`. We need this because the + // document hosting our scripts lives at the MCP sandbox-proxy + // origin (e.g. `claudemcpcontent.com`) while the scripts + // themselves are served from the Reboot origin, so every fetch + // is cross-origin. Vite 6's `cors: true` shortcut only allows + // loopback origins (post-CVE-2025-30208 hardening), so we have + // to be explicit. + cors: { origin: true }, }, + // We leave `server.hmr` at its defaults: @vite/client derives the + // websocket URL from `import.meta.url`, which post-rewrite points + // at the Reboot server — so the WS hits Envoy with no explicit + // host/port override. }; } diff --git a/reboot/crypto/BUILD.bazel b/reboot/crypto/BUILD.bazel new file mode 100644 index 00000000..32e0b0ca --- /dev/null +++ b/reboot/crypto/BUILD.bazel @@ -0,0 +1,14 @@ +load("@rbt_pypi//:requirements.bzl", "requirement") +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "root_keys_py", + srcs = [ + "root_keys.py", + ], + visibility = ["//visibility:public"], + deps = [ + "//reboot:settings_py", + requirement("cryptography"), + ], +) diff --git a/reboot/crypto/root_keys.py b/reboot/crypto/root_keys.py new file mode 100644 index 00000000..af20ab99 --- /dev/null +++ b/reboot/crypto/root_keys.py @@ -0,0 +1,92 @@ +"""Reboot-managed cryptographic root keys. + +Reboot provisions and rotates a versioned set of root keys in the +`REBOOT_CRYPTO_ROOT_KEYS` environment variable (a comma-separated list of +`vN:key` entries; the highest version is "active"). Libraries derive +their own purpose-specific keys from these roots via HKDF-SHA256, passing a +distinct `info` label per use so that derived keys are independent (domain +separation) — e.g., the ciphertext library derives a key-encryption key and +the MCP OAuth server derives its JWT signing key. + +This module deliberately has no Reboot dependencies so it can be used from +anywhere (including the framework's auth path) and unit tested in isolation. +""" + +import os +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from reboot.settings import ENVVAR_REBOOT_CRYPTO_ROOT_KEYS + + +class MissingRootKeys(Exception): + """Raised when `REBOOT_CRYPTO_ROOT_KEYS` is unset or empty.""" + + +class MalformedRootKeys(Exception): + """Raised when `REBOOT_CRYPTO_ROOT_KEYS` is not a `vN:key` list.""" + + +class UnknownRootKeyVersion(Exception): + """Raised when a requested version is not in `REBOOT_CRYPTO_ROOT_KEYS` + (e.g., it was retired before everything derived from it stopped being + used).""" + + +def derive_key(*, info: bytes, version: int, length: int = 32) -> bytes: + """Derives a `length`-byte key from root key `version` via HKDF-SHA256, + using `info` as the HKDF `info`. + + `info` is a domain separator: distinct `info` labels derive independent + keys from the same root, so each library can derive its own keys + without collision. Raises `UnknownRootKeyVersion` if `version` is not + configured. + """ + key = _parse().get(version) + if key is None: + raise UnknownRootKeyVersion( + f"root key version v{version} is not in " + f"'{ENVVAR_REBOOT_CRYPTO_ROOT_KEYS}'" + ) + return HKDF( + algorithm=hashes.SHA256(), + length=length, + salt=None, + info=info, + ).derive(key.encode()) + + +def active_version() -> int: + """Returns the highest configured root key version — the one new keys + should be derived from.""" + return max(_parse()) + + +def available_versions() -> list[int]: + """Returns all configured root key versions, ascending.""" + return sorted(_parse()) + + +def _parse() -> dict[int, str]: + """Parses `REBOOT_CRYPTO_ROOT_KEYS` into `{version: key}`.""" + value = os.environ.get(ENVVAR_REBOOT_CRYPTO_ROOT_KEYS) + if not value: + raise MissingRootKeys(f"'{ENVVAR_REBOOT_CRYPTO_ROOT_KEYS}' is not set") + + keys: dict[int, str] = {} + for entry in value.split(","): + entry = entry.strip() + prefix, separator, key = entry.partition(":") + if separator != ":" or not prefix.startswith("v") or not key: + raise MalformedRootKeys( + f"'{ENVVAR_REBOOT_CRYPTO_ROOT_KEYS}' entry '{entry}' is not of " + "the form 'vN:key'" + ) + try: + version = int(prefix[1:]) + except ValueError: + raise MalformedRootKeys( + f"'{ENVVAR_REBOOT_CRYPTO_ROOT_KEYS}' entry '{entry}' has a " + "non-integer version" + ) + keys[version] = key + return keys diff --git a/reboot/demos/fig/package-lock.json b/reboot/demos/fig/package-lock.json index 931c1931..1273fcb9 100644 --- a/reboot/demos/fig/package-lock.json +++ b/reboot/demos/fig/package-lock.json @@ -10,11 +10,11 @@ "dependencies": { "@bufbuild/protobuf": "1.10.1", "@radix-ui/react-icons": "^1.3.0", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-std": "1.0.4", - "@reboot-dev/reboot-std-api": "1.0.4", - "@reboot-dev/reboot-std-react": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-std": "1.1.0", + "@reboot-dev/reboot-std-api": "1.1.0", + "@reboot-dev/reboot-std-react": "1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-draggable": "^4.4.6", @@ -1356,15 +1356,15 @@ } }, "node_modules/@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -1393,9 +1393,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -1419,15 +1419,15 @@ } }, "node_modules/@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -1455,45 +1455,45 @@ } }, "node_modules/@reboot-dev/reboot-std": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.0.4.tgz", - "integrity": "sha512-S0TPhVZqPtYDwsiz/4RoRqcnMGD/vtGlbAqM8jmPi5Wslh0uR7n3f3EEjh0ZLjTnIWqy/IuDaUK6ZLGVvQYc3A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.1.0.tgz", + "integrity": "sha512-JMQzyJTwpPNeGoz9/orY3Dea3/7AFULYQGx9ge3FhxoGCvD1h/CXsuQYUiHeJaOmdC/fNRE+hg07B7xRRaVZzA==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-std-api": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-std-api": "1.1.0", "@scarf/scarf": "1.4.0" } }, "node_modules/@reboot-dev/reboot-std-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.0.4.tgz", - "integrity": "sha512-Qg9GKZDGjEB/XWtqrunh7EMAmsIMpqC+nf1j96RslSp3GuWT8ZavDPoBPvq637Aw7wLCKIIMUb9KSXBVVDJkFA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.1.0.tgz", + "integrity": "sha512-IZioMk6LG+kJK4fpFmgAXYzF4dBeZCojL/5KpUqb7roRdQZa1gi5l/FaNnWtP6s5TNOLBKYfkN4CRkz8nfdjUA==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0" } }, "node_modules/@reboot-dev/reboot-std-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-react/-/reboot-std-react-1.0.4.tgz", - "integrity": "sha512-1bnMyqqkGoz1muKkxCPqMl8w9djK2A0rjUhNx+2PytOYSmgNJvlTZWJG+xVk1F4xLT42tnuxY9VbtpvV1aGDMQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-react/-/reboot-std-react-1.1.0.tgz", + "integrity": "sha512-YJTIhV7wifJe/z72+dO/cPjbCxV3+uQvlMfkuqMagrTmICjDTqzoGls+gUkGTddjQRvPmb00pGqdpgL4Xd35eg==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", - "@reboot-dev/reboot-std-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", + "@reboot-dev/reboot-std-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0" } }, "node_modules/@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", @@ -3560,9 +3560,9 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", - "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -3617,12 +3617,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz", - "integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -3635,9 +3635,9 @@ } }, "node_modules/express-rate-limit/node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -3694,9 +3694,9 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", @@ -4086,9 +4086,9 @@ } }, "node_modules/hono": { - "version": "4.12.15", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz", - "integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==", + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -8000,13 +8000,13 @@ "requires": {} }, "@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "requires": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -8194,9 +8194,9 @@ } }, "@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "requires": { "@scarf/scarf": "1.4.0", "typescript": "5.4.5", @@ -8211,14 +8211,14 @@ } }, "@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "requires": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -8235,41 +8235,41 @@ } }, "@reboot-dev/reboot-std": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.0.4.tgz", - "integrity": "sha512-S0TPhVZqPtYDwsiz/4RoRqcnMGD/vtGlbAqM8jmPi5Wslh0uR7n3f3EEjh0ZLjTnIWqy/IuDaUK6ZLGVvQYc3A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.1.0.tgz", + "integrity": "sha512-JMQzyJTwpPNeGoz9/orY3Dea3/7AFULYQGx9ge3FhxoGCvD1h/CXsuQYUiHeJaOmdC/fNRE+hg07B7xRRaVZzA==", "requires": { - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-std-api": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-std-api": "1.1.0", "@scarf/scarf": "1.4.0" } }, "@reboot-dev/reboot-std-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.0.4.tgz", - "integrity": "sha512-Qg9GKZDGjEB/XWtqrunh7EMAmsIMpqC+nf1j96RslSp3GuWT8ZavDPoBPvq637Aw7wLCKIIMUb9KSXBVVDJkFA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.1.0.tgz", + "integrity": "sha512-IZioMk6LG+kJK4fpFmgAXYzF4dBeZCojL/5KpUqb7roRdQZa1gi5l/FaNnWtP6s5TNOLBKYfkN4CRkz8nfdjUA==", "requires": { "@scarf/scarf": "1.4.0" } }, "@reboot-dev/reboot-std-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-react/-/reboot-std-react-1.0.4.tgz", - "integrity": "sha512-1bnMyqqkGoz1muKkxCPqMl8w9djK2A0rjUhNx+2PytOYSmgNJvlTZWJG+xVk1F4xLT42tnuxY9VbtpvV1aGDMQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-react/-/reboot-std-react-1.1.0.tgz", + "integrity": "sha512-YJTIhV7wifJe/z72+dO/cPjbCxV3+uQvlMfkuqMagrTmICjDTqzoGls+gUkGTddjQRvPmb00pGqdpgL4Xd35eg==", "requires": { - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", - "@reboot-dev/reboot-std-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", + "@reboot-dev/reboot-std-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0" } }, "@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "requires": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", @@ -9412,9 +9412,9 @@ } }, "eventsource-parser": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", - "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==" }, "exponential-backoff": { "version": "3.1.1", @@ -9457,17 +9457,17 @@ } }, "express-rate-limit": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz", - "integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "requires": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "dependencies": { "ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==" + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==" } } }, @@ -9518,9 +9518,9 @@ "dev": true }, "fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==" }, "fastq": { "version": "1.17.1", @@ -9792,9 +9792,9 @@ } }, "hono": { - "version": "4.12.15", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz", - "integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==" + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==" }, "http-cache-semantics": { "version": "4.1.1", diff --git a/reboot/demos/fig/package.json b/reboot/demos/fig/package.json index 2910c47b..c48e647e 100644 --- a/reboot/demos/fig/package.json +++ b/reboot/demos/fig/package.json @@ -11,11 +11,11 @@ "dependencies": { "@bufbuild/protobuf": "1.10.1", "@radix-ui/react-icons": "^1.3.0", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-std": "1.0.4", - "@reboot-dev/reboot-std-api": "1.0.4", - "@reboot-dev/reboot-std-react": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-std": "1.1.0", + "@reboot-dev/reboot-std-api": "1.1.0", + "@reboot-dev/reboot-std-react": "1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-draggable": "^4.4.6", diff --git a/reboot/examples/agent-wiki/.claude/settings.json b/reboot/examples/agent-wiki/.claude/settings.json new file mode 100644 index 00000000..d522b476 --- /dev/null +++ b/reboot/examples/agent-wiki/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "reboot-plugin": { + "source": { + "source": "github", + "repo": "reboot-dev/reboot-plugin" + } + } + }, + "enabledPlugins": { + "reboot@reboot-plugin": true + } +} diff --git a/reboot/examples/agent-wiki/Dockerfile b/reboot/examples/agent-wiki/Dockerfile index 3dcdbb44..8e51c36d 100644 --- a/reboot/examples/agent-wiki/Dockerfile +++ b/reboot/examples/agent-wiki/Dockerfile @@ -4,7 +4,7 @@ # locally before `docker build` so that `web/dist/` contains the # bundled UIs. This image copies that prebuilt bundle rather # than installing Node and rebuilding it here. -FROM ghcr.io/reboot-dev/reboot-base:1.0.4 +FROM ghcr.io/reboot-dev/reboot-base:1.1.0 WORKDIR /app diff --git a/reboot/examples/agent-wiki/README.md b/reboot/examples/agent-wiki/README.md index a65282e7..62bfd1ff 100644 --- a/reboot/examples/agent-wiki/README.md +++ b/reboot/examples/agent-wiki/README.md @@ -143,7 +143,7 @@ pytest backend/ `mcp_servers.json` is pre-configured. In another terminal: ```bash -npx @mcpjam/inspector@v2.4.0 --config mcp_servers.json --server agent-wiki +npx @mcpjam/inspector@2.9.3 --config mcp_servers.json --server agent-wiki ``` Try these prompts to exercise each capability. The librarian diff --git a/reboot/examples/agent-wiki/backend/src/main.py b/reboot/examples/agent-wiki/backend/src/main.py index ed907c2e..a8a5e3f2 100644 --- a/reboot/examples/agent-wiki/backend/src/main.py +++ b/reboot/examples/agent-wiki/backend/src/main.py @@ -1,7 +1,10 @@ import asyncio import logging from reboot.aio.applications import Application -from reboot.aio.auth.oauth_providers import Anonymous +from reboot.aio.auth.oauth_providers import ( + Development, + OAuthProviderByEnvironment, +) from servicers.wiki import ( PageServicer, TranscriptServicer, @@ -25,7 +28,13 @@ async def main() -> None: ], # `User` is an auto-constructed state type, so Reboot # needs an OAuth provider to identify the caller. - oauth=Anonymous(), + oauth=OAuthProviderByEnvironment( + dev=Development(), + # TODO: set a real provider (e.g. `Google(...)`) before + # production; `prod=None` makes a production deployment fail + # to start until one is chosen. + prod=None, + ), ) await application.run() diff --git a/reboot/examples/agent-wiki/backend/tests/wiki_test.py b/reboot/examples/agent-wiki/backend/tests/wiki_test.py index ed77bec4..89655bef 100644 --- a/reboot/examples/agent-wiki/backend/tests/wiki_test.py +++ b/reboot/examples/agent-wiki/backend/tests/wiki_test.py @@ -19,7 +19,8 @@ ) from pydantic_ai.models.function import AgentInfo, FunctionModel from reboot.aio.applications import Application -from reboot.aio.tests import Reboot +from reboot.aio.auth.oauth_providers import Anonymous +from reboot.aio.tests import OAuthProviderForTest, Reboot from servicers import wiki as wiki_module from servicers.wiki import ( PageServicer, @@ -54,21 +55,42 @@ def _refuse( return FunctionModel(_refuse) -class ServicerTest(unittest.IsolatedAsyncioTestCase): - """Unit tests for each servicer's CRUD methods. These - tests never add a transcript, so the librarian workflow - never actually runs — but we still replace the agent's - model as a belt-and-braces guard against accidental - Anthropic calls from this suite.""" +def _simple_librarian_model() -> FunctionModel: + """Return a `FunctionModel` that always returns the same + response, used by tests that want to trigger the + librarian but don't care about its behavior.""" + + def _respond( + messages: list[ModelMessage], + info: AgentInfo, + ) -> ModelResponse: + return ModelResponse(parts=[TextPart(content="Librarian response")]) + + return FunctionModel(_respond) + + +class _WikiTestBase(unittest.IsolatedAsyncioTestCase): + """Base class that wires up Reboot, creates an `alice` user + context, and swaps the librarian model for the duration of + each test. Subclasses override `_make_librarian_model` to + choose which stand-in model to install.""" + + def _make_librarian_model(self) -> FunctionModel: + raise NotImplementedError async def asyncSetUp(self) -> None: self._original_model = wiki_module.librarian.wrapped.model - wiki_module.librarian.wrapped.model = _null_librarian_model() + # Overwrite the librarian's model within the test, so any calls + # to LLM become deterministic. + wiki_module.librarian.wrapped.model = self._make_librarian_model() self.rbt = Reboot() await self.rbt.start() await self.rbt.up( - Application(servicers=APPLICATION_SERVICERS), + Application( + servicers=APPLICATION_SERVICERS, + oauth=OAuthProviderForTest(Anonymous()), + ), ) self.user_id = "alice" self.context = self.rbt.create_external_context( @@ -90,6 +112,18 @@ async def asyncTearDown(self) -> None: await self.rbt.stop() wiki_module.librarian.wrapped.model = self._original_model + +class ServicerTest(_WikiTestBase): + """Unit tests for each servicer's CRUD methods. These + tests never add a transcript, so the librarian workflow + never actually runs — but we still replace the agent's + model as a belt-and-braces guard against accidental + Anthropic calls from this suite.""" + + def _make_librarian_model(self) -> FunctionModel: + # The tests should never trigger the librarian. + return _null_librarian_model() + async def test_user_create_and_list_wikis(self) -> None: """A user can create a wiki and then see it in their list, keyed by the user-supplied name.""" @@ -180,6 +214,14 @@ async def test_transcript_crud(self) -> None: self.assertEqual(len(got.messages), 1) self.assertEqual(got.messages[0].content, "Goodbye") + +class ServicerWithSimpleLibrarianTest(_WikiTestBase): + + def _make_librarian_model(self) -> FunctionModel: + # Depending on the timing, that test might trigger the librarian + # when the transcription is added an consumed by `until`. + return _simple_librarian_model() + async def test_add_transcript_creates_transcript( self, ) -> None: @@ -228,8 +270,9 @@ class ScriptedLibrarian: def __init__(self) -> None: self.page_id: str | None = None + self.done = asyncio.Event() - def step( + async def step( self, messages: list[ModelMessage], info: AgentInfo, @@ -286,42 +329,25 @@ def step( ), ] ) + + # Signal done the moment we emit the final response, which means + # the librarian has already executed `update_wiki` and the + # wiki's content is updated by the time any test code waiting on + # `done` wakes up. + self.done.set() return ModelResponse(parts=[TextPart(content="Done.")]) -class IngestWorkflowTest(unittest.IsolatedAsyncioTestCase): +class IngestWorkflowTest(_WikiTestBase): """End-to-end test of the `Wiki.ingest` librarian workflow with the LLM replaced by a `FunctionModel`.""" - async def asyncSetUp(self) -> None: - self.script = ScriptedLibrarian() - self._original_model = wiki_module.librarian.wrapped.model - wiki_module.librarian.wrapped.model = FunctionModel(self.script.step) - - self.rbt = Reboot() - await self.rbt.start() - await self.rbt.up( - Application(servicers=APPLICATION_SERVICERS), - ) - self.user_id = "alice" - self.context = self.rbt.create_external_context( - name=f"test-{self.id()}", - bearer_token=self.rbt.make_valid_oauth_access_token( - user_id=self.user_id, - ), - ) - # `User` is an auto-constructed state type: in - # production the MCP session's "new session" hook - # calls `_auto_construct` for the authenticated user. - # Tests don't go through that hook, so we do it here. - await UserServicer._auto_construct( - self.context, - state_id=self.user_id, - ) + script = ScriptedLibrarian() - async def asyncTearDown(self) -> None: - await self.rbt.stop() - wiki_module.librarian.wrapped.model = self._original_model + def _make_librarian_model(self) -> FunctionModel: + # Scripted model that drives the librarian through a fixed + # sequence of tool calls. + return FunctionModel(self.script.step) async def test_ingest_creates_page_and_updates_wiki( self, @@ -351,20 +377,14 @@ async def test_ingest_creates_page_and_updates_wiki( ], ) - # Poll the wiki's markdown body until the scripted - # `update_wiki` call lands. `Wiki.get` is the only - # externally observable signal — `transcripts` lives - # on the internal state, not on the `get` response. - for _ in range(100): # 10 s at 100 ms steps. - state = await wiki.get(self.context) - if state.content.startswith("# Table of contents"): - break - await asyncio.sleep(0.1) - else: - self.fail( - "Timed out waiting for librarian to rewrite " - "Wiki.content" - ) + # Block until the scripted librarian signals it is + # done. `done` is set the moment `step()` emits its + # final `TextPart("Done.")`, at which point + # `update_wiki` has already executed and + # `Wiki.content` is already updated. + await self.script.done.wait() + + state = await wiki.get(self.context) # The scripted librarian should have created exactly # one page and referenced it from the wiki's diff --git a/reboot/examples/agent-wiki/pyproject.toml b/reboot/examples/agent-wiki/pyproject.toml index 222d05b3..a23c9ccc 100644 --- a/reboot/examples/agent-wiki/pyproject.toml +++ b/reboot/examples/agent-wiki/pyproject.toml @@ -7,13 +7,13 @@ dependencies = [ "uuid7>=0.1.0", "anyio>=4.0.0", "pydantic-ai-slim[anthropic]>=1.0.0", - "reboot==1.0.4", + "reboot==1.1.0", ] [tool.rye] dev-dependencies = [ "pytest>=7.4", - "reboot==1.0.4", + "reboot==1.1.0", ] # This project only uses `rye` to provide `python` and its dependencies. diff --git a/reboot/examples/agent-wiki/requirements-dev.lock b/reboot/examples/agent-wiki/requirements-dev.lock index c2e1f9e1..a3e75439 100644 --- a/reboot/examples/agent-wiki/requirements-dev.lock +++ b/reboot/examples/agent-wiki/requirements-dev.lock @@ -220,8 +220,9 @@ pyprctl==0.1.3 pytest==9.0.3 python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.27 # via mcp python-ulid==3.1.0 @@ -229,7 +230,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/agent-wiki/requirements.lock b/reboot/examples/agent-wiki/requirements.lock index a70e1a1a..3cbf52ed 100644 --- a/reboot/examples/agent-wiki/requirements.lock +++ b/reboot/examples/agent-wiki/requirements.lock @@ -211,8 +211,9 @@ pyprctl==0.1.3 # via reboot python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.27 # via mcp python-ulid==3.1.0 @@ -220,7 +221,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/agent-wiki/web/package-lock.json b/reboot/examples/agent-wiki/web/package-lock.json index 9465bdc2..842f2f14 100644 --- a/reboot/examples/agent-wiki/web/package-lock.json +++ b/reboot/examples/agent-wiki/web/package-lock.json @@ -10,8 +10,8 @@ "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^10.1.0", @@ -888,9 +888,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -915,15 +915,15 @@ } }, "node_modules/@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -951,12 +951,12 @@ } }, "node_modules/@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", diff --git a/reboot/examples/agent-wiki/web/package.json b/reboot/examples/agent-wiki/web/package.json index 74c916fe..05b49b96 100644 --- a/reboot/examples/agent-wiki/web/package.json +++ b/reboot/examples/agent-wiki/web/package.json @@ -17,8 +17,8 @@ "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^10.1.0", diff --git a/reboot/examples/ai-chat-counter-dashboard/README.md b/reboot/examples/ai-chat-counter-dashboard/README.md index 0b65d96d..9dcb7a8a 100644 --- a/reboot/examples/ai-chat-counter-dashboard/README.md +++ b/reboot/examples/ai-chat-counter-dashboard/README.md @@ -29,7 +29,7 @@ The project includes `mcp_servers.json` for testing with MCPJam Inspector: ```bash # In another terminal, run MCPJam: -npx @mcpjam/inspector@2.4.0 --config mcp_servers.json --server counter-server +npx @mcpjam/inspector@2.9.3 --config mcp_servers.json --server counter-server ``` This opens a browser-based inspector where you can test tools. diff --git a/reboot/examples/ai-chat-counter-dashboard/backend/src/main.py b/reboot/examples/ai-chat-counter-dashboard/backend/src/main.py index bc98a692..77f5be46 100644 --- a/reboot/examples/ai-chat-counter-dashboard/backend/src/main.py +++ b/reboot/examples/ai-chat-counter-dashboard/backend/src/main.py @@ -1,13 +1,22 @@ import asyncio from reboot.aio.applications import Application -from reboot.aio.auth.oauth_providers import Anonymous +from reboot.aio.auth.oauth_providers import ( + Development, + OAuthProviderByEnvironment, +) from servicers.counter import CounterServicer, UserServicer async def main() -> None: application = Application( servicers=[UserServicer, CounterServicer], - oauth=Anonymous(), + oauth=OAuthProviderByEnvironment( + dev=Development(), + # TODO: set a real provider (e.g. `Google(...)`) before + # production; `prod=None` makes a production deployment fail + # to start until one is chosen. + prod=None, + ), ) await application.run() diff --git a/reboot/examples/ai-chat-counter-dashboard/pyproject.toml b/reboot/examples/ai-chat-counter-dashboard/pyproject.toml index 675cc6eb..3d9d4a20 100644 --- a/reboot/examples/ai-chat-counter-dashboard/pyproject.toml +++ b/reboot/examples/ai-chat-counter-dashboard/pyproject.toml @@ -6,14 +6,14 @@ dependencies = [ "httpx>=0.27,<1.0", "uuid7>=0.1.0", "anyio>=4.0.0", - "reboot==1.0.4", + "reboot==1.1.0", ] [tool.rye] dev-dependencies = [ "mypy==1.18.1", "types-protobuf>=4.24.0.20240129", - "reboot==1.0.4", + "reboot==1.1.0", ] # This project only uses `rye` to provide `python` and its dependencies. diff --git a/reboot/examples/ai-chat-counter-dashboard/requirements-dev.lock b/reboot/examples/ai-chat-counter-dashboard/requirements-dev.lock index 877f5775..66497f47 100644 --- a/reboot/examples/ai-chat-counter-dashboard/requirements-dev.lock +++ b/reboot/examples/ai-chat-counter-dashboard/requirements-dev.lock @@ -206,6 +206,7 @@ python-dateutil==2.9.0.post0 # via kubernetes-asyncio python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.21 # via mcp python-ulid==3.1.0 @@ -213,7 +214,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/ai-chat-counter-dashboard/requirements.lock b/reboot/examples/ai-chat-counter-dashboard/requirements.lock index 98e906f0..a450956a 100644 --- a/reboot/examples/ai-chat-counter-dashboard/requirements.lock +++ b/reboot/examples/ai-chat-counter-dashboard/requirements.lock @@ -200,8 +200,9 @@ pyprctl==0.1.3 # via reboot python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.22 # via mcp python-ulid==3.1.0 @@ -209,7 +210,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/ai-chat-counter-dashboard/web/package-lock.json b/reboot/examples/ai-chat-counter-dashboard/web/package-lock.json index cfab7ca3..b12d9ebe 100644 --- a/reboot/examples/ai-chat-counter-dashboard/web/package-lock.json +++ b/reboot/examples/ai-chat-counter-dashboard/web/package-lock.json @@ -10,8 +10,8 @@ "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "zod": "^3.25.0" @@ -886,9 +886,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -913,15 +913,15 @@ } }, "node_modules/@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -949,12 +949,12 @@ } }, "node_modules/@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", diff --git a/reboot/examples/ai-chat-counter-dashboard/web/package.json b/reboot/examples/ai-chat-counter-dashboard/web/package.json index 7e4170da..4e2ab28e 100644 --- a/reboot/examples/ai-chat-counter-dashboard/web/package.json +++ b/reboot/examples/ai-chat-counter-dashboard/web/package.json @@ -15,8 +15,8 @@ "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-react": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "zod": "^3.25.0" diff --git a/reboot/examples/ai-chat-counter/.rbtrc b/reboot/examples/ai-chat-counter/.rbtrc index a1da3e4d..b67c4c63 100644 --- a/reboot/examples/ai-chat-counter/.rbtrc +++ b/reboot/examples/ai-chat-counter/.rbtrc @@ -43,16 +43,6 @@ dev expunge --application-name=ai-chat-counter serve run --python serve run --application=backend/src/main.py -# Make both the generated code directory (`backend/api/`) and the -# schema source directory (`api/`) importable. `rbt serve run` only -# auto-adds the `--python=` directory to `PYTHONPATH`; for apps that -# define their schemas in Pydantic modules under `api/` (and import -# from them directly, e.g. `from ai_chat_counter.v1.counter import -# CreateCounterRequest`), `api/` needs to be on the path too. Setting -# `--env=PYTHONPATH=...` overrides the auto-set, so include both. -# (`rbt dev run` does this automatically; `rbt serve run` does not.) -serve run --env=PYTHONPATH=backend/api:api - # Reboot Cloud terminates TLS at the external load balancer, so the app # itself only exposes a non-TLS port. serve run --tls=external diff --git a/reboot/examples/ai-chat-counter/Dockerfile b/reboot/examples/ai-chat-counter/Dockerfile index 8b3b8e16..04815955 100644 --- a/reboot/examples/ai-chat-counter/Dockerfile +++ b/reboot/examples/ai-chat-counter/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/reboot-dev/reboot-base:1.0.4 +FROM ghcr.io/reboot-dev/reboot-base:1.1.0 WORKDIR /app diff --git a/reboot/examples/ai-chat-counter/README.md b/reboot/examples/ai-chat-counter/README.md index 373299ff..34813b1e 100644 --- a/reboot/examples/ai-chat-counter/README.md +++ b/reboot/examples/ai-chat-counter/README.md @@ -28,7 +28,7 @@ The project includes `mcp_servers.json` for testing with MCPJam Inspector: ```bash # In another terminal, run MCPJam: -npx @mcpjam/inspector@2.4.0 --config mcp_servers.json --server counter-server +npx @mcpjam/inspector@2.9.3 --config mcp_servers.json --server counter-server ``` This opens a browser-based inspector where you can test tools. diff --git a/reboot/examples/ai-chat-counter/backend/src/example_prompts.py b/reboot/examples/ai-chat-counter/backend/src/example_prompts.py new file mode 100644 index 00000000..0393aad2 --- /dev/null +++ b/reboot/examples/ai-chat-counter/backend/src/example_prompts.py @@ -0,0 +1,33 @@ +from reboot.application import ExamplePrompt + +example_prompts = [ + ExamplePrompt( + title="Track your morning coffee", + prompts=[ + "Create a new counter for tracking how many cups of " + "coffee I drink today, and show me the counter.", + "I just finished a cup — increment my coffee counter " + "and show it to me again.", + ], + ), + ExamplePrompt( + title="Build a habit tracker", + prompts=[ + "Create a counter for morning runs this month and one " + "for evening walks.", + "Show me the morning runs counter.", + "I just finished a morning run — bump it and show me " + "the counter again.", + ], + ), + ExamplePrompt( + title="Run a quick scoreboard", + prompts=[ + "Create two counters, 'wins' and 'losses', for my " + "board-game nights.", + "I just won a game — show me the wins counter so I " + "can bump it.", + "List all my counters and their current values.", + ], + ), +] diff --git a/reboot/examples/ai-chat-counter/backend/src/main.py b/reboot/examples/ai-chat-counter/backend/src/main.py index 5467ed80..c2cad5ad 100644 --- a/reboot/examples/ai-chat-counter/backend/src/main.py +++ b/reboot/examples/ai-chat-counter/backend/src/main.py @@ -1,14 +1,30 @@ # backend/src/main.py import asyncio +from example_prompts import example_prompts from reboot.aio.applications import Application -from reboot.aio.auth.oauth_providers import Anonymous +from reboot.aio.auth.oauth_providers import ( + Development, + OAuthProviderByEnvironment, +) from servicers.counter import CounterServicer, UserServicer async def main() -> None: application = Application( + title="Chat Counter", + description=( + "Lets a chat client create, list, increment, and " + "show counters on your behalf." + ), servicers=[UserServicer, CounterServicer], - oauth=Anonymous(), + oauth=OAuthProviderByEnvironment( + dev=Development(), + # TODO: set a real provider (e.g. `Google(...)`) before + # production; `prod=None` makes a production deployment fail + # to start until one is chosen. + prod=None, + ), + example_prompts=example_prompts, ) await application.run() diff --git a/reboot/examples/ai-chat-counter/pyproject.toml b/reboot/examples/ai-chat-counter/pyproject.toml index 67170504..bc2a260b 100644 --- a/reboot/examples/ai-chat-counter/pyproject.toml +++ b/reboot/examples/ai-chat-counter/pyproject.toml @@ -6,14 +6,14 @@ dependencies = [ "httpx>=0.27,<1.0", "uuid7>=0.1.0", "anyio>=4.0.0", - "reboot==1.0.4", + "reboot==1.1.0", ] [tool.rye] dev-dependencies = [ "mypy==1.18.1", "types-protobuf>=4.24.0.20240129", - "reboot==1.0.4", + "reboot==1.1.0", ] # This project only uses `rye` to provide `python` and its dependencies. diff --git a/reboot/examples/ai-chat-counter/requirements-dev.lock b/reboot/examples/ai-chat-counter/requirements-dev.lock index f781c149..f9671d0d 100644 --- a/reboot/examples/ai-chat-counter/requirements-dev.lock +++ b/reboot/examples/ai-chat-counter/requirements-dev.lock @@ -204,8 +204,9 @@ pyprctl==0.1.3 # via reboot python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.27 # via mcp python-ulid==3.1.0 @@ -213,7 +214,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/ai-chat-counter/requirements.lock b/reboot/examples/ai-chat-counter/requirements.lock index caadf6f5..07d5ec35 100644 --- a/reboot/examples/ai-chat-counter/requirements.lock +++ b/reboot/examples/ai-chat-counter/requirements.lock @@ -200,8 +200,9 @@ pyprctl==0.1.3 # via reboot python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.22 # via mcp python-ulid==3.1.0 @@ -209,7 +210,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/ai-chat-counter/web/package-lock.json b/reboot/examples/ai-chat-counter/web/package-lock.json index ed9bf8d5..ae7c96fb 100644 --- a/reboot/examples/ai-chat-counter/web/package-lock.json +++ b/reboot/examples/ai-chat-counter/web/package-lock.json @@ -10,8 +10,8 @@ "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "zod": "^3.25.0" @@ -403,9 +403,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -428,15 +428,15 @@ } }, "node_modules/@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -462,12 +462,12 @@ } }, "node_modules/@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", diff --git a/reboot/examples/ai-chat-counter/web/package.json b/reboot/examples/ai-chat-counter/web/package.json index fdafb231..3e38d601 100644 --- a/reboot/examples/ai-chat-counter/web/package.json +++ b/reboot/examples/ai-chat-counter/web/package.json @@ -13,8 +13,8 @@ "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "zod": "^3.25.0" diff --git a/reboot/examples/ai-chat-counter/web/ui/clicker/App.module.css b/reboot/examples/ai-chat-counter/web/ui/clicker/App.module.css index cf12885d..36a56eac 100644 --- a/reboot/examples/ai-chat-counter/web/ui/clicker/App.module.css +++ b/reboot/examples/ai-chat-counter/web/ui/clicker/App.module.css @@ -1,4 +1,12 @@ /* web/ui/clicker/App.module.css */ + +/* + * The clicker renders inline inside the MCP host (e.g. Claude.ai), + * so it inherits the host's color scheme. We set every color + * explicitly — never relying on inherited text color — and provide + * a dark variant via `prefers-color-scheme` so the widget stays + * legible in both Claude.ai's light and dark modes. + */ .container { display: flex; align-items: center; @@ -10,6 +18,7 @@ border: 1px solid #e0e0e0; border-radius: 12px; background: #fafafa; + color: #1a1a1a; } .value { @@ -17,6 +26,7 @@ font-weight: 600; min-width: 48px; text-align: center; + color: inherit; } .button { @@ -27,9 +37,33 @@ border-radius: 8px; border: 1px solid #ccc; background: #f5f5f5; + color: #1a1a1a; cursor: pointer; } +.button:hover:not(:disabled) { + background: #ebebeb; +} + .button:disabled { cursor: default; + opacity: 0.5; +} + +@media (prefers-color-scheme: dark) { + .container { + border-color: #3a3a3a; + background: #1e1e1e; + color: #f0f0f0; + } + + .button { + border-color: #4a4a4a; + background: #2d2d2d; + color: #f0f0f0; + } + + .button:hover:not(:disabled) { + background: #3a3a3a; + } } diff --git a/reboot/examples/all_tests.sh b/reboot/examples/all_tests.sh index 9f45ffde..d4da33ed 100755 --- a/reboot/examples/all_tests.sh +++ b/reboot/examples/all_tests.sh @@ -10,7 +10,7 @@ set -x # Echo executed commands to help debug failures. # Check that this script has been invoked with the right working directory, by # checking that the expected subdirectories exist. -ls -l agent-wiki/ bank-pydantic/ bank-zod/ chick-potle/ docubot/ bank-nodejs/ boutique/ chat-room/ chat-room-nodejs/ monorepo/ 2> /dev/null > /dev/null || { +ls -l agent-wiki/ bank-pydantic/ bank-zod/ chick-potle/ docubot/ bank-nodejs/ boutique/ chat-room/ chat-room-nodejs/ monorepo/ reboot-swag-store/ 2> /dev/null > /dev/null || { echo "ERROR: this script must be invoked from the 'reboot/examples' directory." echo "Current working directory is '$(pwd)'." ls @@ -25,6 +25,7 @@ sh -c 'cd agent-wiki && ./.tests/test.sh' sh -c 'cd bank-pydantic && ./.tests/test.sh' sh -c 'cd bank-zod && ./.tests/test.sh' sh -c 'cd chick-potle && ./.tests/test.sh' +sh -c 'cd reboot-swag-store && ./.tests/test.sh' sh -c 'cd docubot && ./.tests/test.sh' sh -c 'cd bank-nodejs && ./.tests/test.sh' sh -c 'cd boutique && ./.tests/test.sh' diff --git a/reboot/examples/bank-nodejs/package-lock.json b/reboot/examples/bank-nodejs/package-lock.json index 2ef58118..60f5d348 100644 --- a/reboot/examples/bank-nodejs/package-lock.json +++ b/reboot/examples/bank-nodejs/package-lock.json @@ -9,8 +9,8 @@ "version": "0.1.0", "dependencies": { "@bufbuild/protobuf": "1.10.1", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", "@types/node": "20.11.5", "typescript": "5.4.5" }, @@ -510,15 +510,15 @@ } }, "node_modules/@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -547,9 +547,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -3119,13 +3119,13 @@ "optional": true }, "@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "requires": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -3142,9 +3142,9 @@ } }, "@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "requires": { "@scarf/scarf": "1.4.0", "typescript": "5.4.5", diff --git a/reboot/examples/bank-nodejs/package.json b/reboot/examples/bank-nodejs/package.json index fca75a57..c7bdf873 100644 --- a/reboot/examples/bank-nodejs/package.json +++ b/reboot/examples/bank-nodejs/package.json @@ -5,8 +5,8 @@ "type": "module", "dependencies": { "@bufbuild/protobuf": "1.10.1", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", "typescript": "5.4.5", "@types/node": "20.11.5" }, diff --git a/reboot/examples/bank-pydantic/.claude/settings.json b/reboot/examples/bank-pydantic/.claude/settings.json new file mode 100644 index 00000000..d522b476 --- /dev/null +++ b/reboot/examples/bank-pydantic/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "reboot-plugin": { + "source": { + "source": "github", + "repo": "reboot-dev/reboot-plugin" + } + } + }, + "enabledPlugins": { + "reboot@reboot-plugin": true + } +} diff --git a/reboot/examples/bank-pydantic/pyproject.toml b/reboot/examples/bank-pydantic/pyproject.toml index f43f508c..5326581e 100644 --- a/reboot/examples/bank-pydantic/pyproject.toml +++ b/reboot/examples/bank-pydantic/pyproject.toml @@ -1,7 +1,7 @@ [project] requires-python = ">= 3.10" dependencies = [ - "reboot==1.0.4", + "reboot==1.1.0", ] [tool.rye] @@ -9,7 +9,7 @@ dev-dependencies = [ "mypy==1.18.1", "pytest>=7.4.2", "types-protobuf>=4.24.0.20240129", - "reboot==1.0.4", + "reboot==1.1.0", ] # This project only uses `rye` to provide `python` and its dependencies, so diff --git a/reboot/examples/bank-pydantic/requirements-dev.lock b/reboot/examples/bank-pydantic/requirements-dev.lock index 7945b184..24968900 100644 --- a/reboot/examples/bank-pydantic/requirements-dev.lock +++ b/reboot/examples/bank-pydantic/requirements-dev.lock @@ -213,8 +213,9 @@ pyprctl==0.1.3 pytest==8.4.2 python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.22 # via mcp python-ulid==3.1.0 @@ -222,7 +223,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/bank-pydantic/requirements.lock b/reboot/examples/bank-pydantic/requirements.lock index 67c452ff..afc77107 100644 --- a/reboot/examples/bank-pydantic/requirements.lock +++ b/reboot/examples/bank-pydantic/requirements.lock @@ -200,8 +200,9 @@ pyprctl==0.1.3 # via reboot python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.22 # via mcp python-ulid==3.1.0 @@ -209,7 +210,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/bank-pydantic/web/package-lock.json b/reboot/examples/bank-pydantic/web/package-lock.json index 2ca1e8a2..a05cbe07 100644 --- a/reboot/examples/bank-pydantic/web/package-lock.json +++ b/reboot/examples/bank-pydantic/web/package-lock.json @@ -9,9 +9,9 @@ "version": "0.1.0", "dependencies": { "@eslint/js": "^9.34.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", - "@reboot-dev/reboot-std": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", + "@reboot-dev/reboot-std": "1.1.0", "@tailwindcss/vite": "^4.1.11", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -1200,15 +1200,15 @@ } }, "node_modules/@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -1237,9 +1237,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -1263,15 +1263,15 @@ } }, "node_modules/@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -1298,32 +1298,32 @@ } }, "node_modules/@reboot-dev/reboot-std": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.0.4.tgz", - "integrity": "sha512-S0TPhVZqPtYDwsiz/4RoRqcnMGD/vtGlbAqM8jmPi5Wslh0uR7n3f3EEjh0ZLjTnIWqy/IuDaUK6ZLGVvQYc3A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.1.0.tgz", + "integrity": "sha512-JMQzyJTwpPNeGoz9/orY3Dea3/7AFULYQGx9ge3FhxoGCvD1h/CXsuQYUiHeJaOmdC/fNRE+hg07B7xRRaVZzA==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-std-api": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-std-api": "1.1.0", "@scarf/scarf": "1.4.0" } }, "node_modules/@reboot-dev/reboot-std-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.0.4.tgz", - "integrity": "sha512-Qg9GKZDGjEB/XWtqrunh7EMAmsIMpqC+nf1j96RslSp3GuWT8ZavDPoBPvq637Aw7wLCKIIMUb9KSXBVVDJkFA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.1.0.tgz", + "integrity": "sha512-IZioMk6LG+kJK4fpFmgAXYzF4dBeZCojL/5KpUqb7roRdQZa1gi5l/FaNnWtP6s5TNOLBKYfkN4CRkz8nfdjUA==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0" } }, "node_modules/@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", @@ -5415,9 +5415,9 @@ } }, "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6613,9 +6613,9 @@ } }, "node_modules/undici": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", - "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", + "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", "license": "MIT", "engines": { "node": ">=18.17" @@ -7758,13 +7758,13 @@ } }, "@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "requires": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -7965,9 +7965,9 @@ } }, "@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "requires": { "@scarf/scarf": "1.4.0", "typescript": "5.4.5", @@ -7982,14 +7982,14 @@ } }, "@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "requires": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -8006,29 +8006,29 @@ } }, "@reboot-dev/reboot-std": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.0.4.tgz", - "integrity": "sha512-S0TPhVZqPtYDwsiz/4RoRqcnMGD/vtGlbAqM8jmPi5Wslh0uR7n3f3EEjh0ZLjTnIWqy/IuDaUK6ZLGVvQYc3A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.1.0.tgz", + "integrity": "sha512-JMQzyJTwpPNeGoz9/orY3Dea3/7AFULYQGx9ge3FhxoGCvD1h/CXsuQYUiHeJaOmdC/fNRE+hg07B7xRRaVZzA==", "requires": { - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-std-api": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-std-api": "1.1.0", "@scarf/scarf": "1.4.0" } }, "@reboot-dev/reboot-std-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.0.4.tgz", - "integrity": "sha512-Qg9GKZDGjEB/XWtqrunh7EMAmsIMpqC+nf1j96RslSp3GuWT8ZavDPoBPvq637Aw7wLCKIIMUb9KSXBVVDJkFA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.1.0.tgz", + "integrity": "sha512-IZioMk6LG+kJK4fpFmgAXYzF4dBeZCojL/5KpUqb7roRdQZa1gi5l/FaNnWtP6s5TNOLBKYfkN4CRkz8nfdjUA==", "requires": { "@scarf/scarf": "1.4.0" } }, "@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "requires": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", @@ -10345,9 +10345,9 @@ "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==" }, "semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==" + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==" }, "which": { "version": "6.0.1", @@ -11128,9 +11128,9 @@ } }, "undici": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", - "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==" + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", + "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==" }, "unpipe": { "version": "1.0.0", diff --git a/reboot/examples/bank-pydantic/web/package.json b/reboot/examples/bank-pydantic/web/package.json index 65755a25..295a97e0 100644 --- a/reboot/examples/bank-pydantic/web/package.json +++ b/reboot/examples/bank-pydantic/web/package.json @@ -5,9 +5,9 @@ "type": "module", "dependencies": { "@eslint/js": "^9.34.0", - "@reboot-dev/reboot-react": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-std": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-std": "1.1.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/eslint__js": "^8.42.3", diff --git a/reboot/examples/bank-zod/.claude/settings.json b/reboot/examples/bank-zod/.claude/settings.json new file mode 100644 index 00000000..d522b476 --- /dev/null +++ b/reboot/examples/bank-zod/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "reboot-plugin": { + "source": { + "source": "github", + "repo": "reboot-dev/reboot-plugin" + } + } + }, + "enabledPlugins": { + "reboot@reboot-plugin": true + } +} diff --git a/reboot/examples/bank-zod/package-lock.json b/reboot/examples/bank-zod/package-lock.json index 976218c2..bbd3f634 100644 --- a/reboot/examples/bank-zod/package-lock.json +++ b/reboot/examples/bank-zod/package-lock.json @@ -9,10 +9,10 @@ "version": "0.1.0", "dependencies": { "@bufbuild/protobuf": "1.10.1", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", - "@reboot-dev/reboot-std": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", + "@reboot-dev/reboot-std": "1.1.0", "@tailwindcss/vite": "^4.1.11", "@types/node": "20.11.5", "lucide-react": "^0.525.0", @@ -1199,15 +1199,15 @@ } }, "node_modules/@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -1236,9 +1236,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -1262,15 +1262,15 @@ } }, "node_modules/@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -1297,32 +1297,32 @@ } }, "node_modules/@reboot-dev/reboot-std": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.0.4.tgz", - "integrity": "sha512-S0TPhVZqPtYDwsiz/4RoRqcnMGD/vtGlbAqM8jmPi5Wslh0uR7n3f3EEjh0ZLjTnIWqy/IuDaUK6ZLGVvQYc3A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.1.0.tgz", + "integrity": "sha512-JMQzyJTwpPNeGoz9/orY3Dea3/7AFULYQGx9ge3FhxoGCvD1h/CXsuQYUiHeJaOmdC/fNRE+hg07B7xRRaVZzA==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-std-api": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-std-api": "1.1.0", "@scarf/scarf": "1.4.0" } }, "node_modules/@reboot-dev/reboot-std-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.0.4.tgz", - "integrity": "sha512-Qg9GKZDGjEB/XWtqrunh7EMAmsIMpqC+nf1j96RslSp3GuWT8ZavDPoBPvq637Aw7wLCKIIMUb9KSXBVVDJkFA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.1.0.tgz", + "integrity": "sha512-IZioMk6LG+kJK4fpFmgAXYzF4dBeZCojL/5KpUqb7roRdQZa1gi5l/FaNnWtP6s5TNOLBKYfkN4CRkz8nfdjUA==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0" } }, "node_modules/@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", @@ -7382,13 +7382,13 @@ "optional": true }, "@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "requires": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -7405,9 +7405,9 @@ } }, "@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "requires": { "@scarf/scarf": "1.4.0", "typescript": "5.4.5", @@ -7422,14 +7422,14 @@ } }, "@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "requires": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -7446,29 +7446,29 @@ } }, "@reboot-dev/reboot-std": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.0.4.tgz", - "integrity": "sha512-S0TPhVZqPtYDwsiz/4RoRqcnMGD/vtGlbAqM8jmPi5Wslh0uR7n3f3EEjh0ZLjTnIWqy/IuDaUK6ZLGVvQYc3A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std/-/reboot-std-1.1.0.tgz", + "integrity": "sha512-JMQzyJTwpPNeGoz9/orY3Dea3/7AFULYQGx9ge3FhxoGCvD1h/CXsuQYUiHeJaOmdC/fNRE+hg07B7xRRaVZzA==", "requires": { - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-std-api": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-std-api": "1.1.0", "@scarf/scarf": "1.4.0" } }, "@reboot-dev/reboot-std-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.0.4.tgz", - "integrity": "sha512-Qg9GKZDGjEB/XWtqrunh7EMAmsIMpqC+nf1j96RslSp3GuWT8ZavDPoBPvq637Aw7wLCKIIMUb9KSXBVVDJkFA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.1.0.tgz", + "integrity": "sha512-IZioMk6LG+kJK4fpFmgAXYzF4dBeZCojL/5KpUqb7roRdQZa1gi5l/FaNnWtP6s5TNOLBKYfkN4CRkz8nfdjUA==", "requires": { "@scarf/scarf": "1.4.0" } }, "@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "requires": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", diff --git a/reboot/examples/bank-zod/package.json b/reboot/examples/bank-zod/package.json index 20774e05..20cab3d3 100644 --- a/reboot/examples/bank-zod/package.json +++ b/reboot/examples/bank-zod/package.json @@ -9,10 +9,10 @@ }, "dependencies": { "@bufbuild/protobuf": "1.10.1", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-std": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-std": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", "@tailwindcss/vite": "^4.1.11", "@types/node": "20.11.5", "lucide-react": "^0.525.0", diff --git a/reboot/examples/bank/.claude/settings.json b/reboot/examples/bank/.claude/settings.json new file mode 100644 index 00000000..d522b476 --- /dev/null +++ b/reboot/examples/bank/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "reboot-plugin": { + "source": { + "source": "github", + "repo": "reboot-dev/reboot-plugin" + } + } + }, + "enabledPlugins": { + "reboot@reboot-plugin": true + } +} diff --git a/reboot/examples/bank/pyproject.toml b/reboot/examples/bank/pyproject.toml index 409819c1..e93866b8 100644 --- a/reboot/examples/bank/pyproject.toml +++ b/reboot/examples/bank/pyproject.toml @@ -1,14 +1,14 @@ [project] requires-python = ">= 3.10" dependencies = [ - "reboot==1.0.4", + "reboot==1.1.0", ] [tool.rye] dev-dependencies = [ "mypy==1.18.1", "types-protobuf>=4.24.0.20240129", - "reboot==1.0.4", + "reboot==1.1.0", ] # This project only uses `rye` to provide `python` and its dependencies, so diff --git a/reboot/examples/bank/requirements-dev.lock b/reboot/examples/bank/requirements-dev.lock index 604b117b..7fb70611 100644 --- a/reboot/examples/bank/requirements-dev.lock +++ b/reboot/examples/bank/requirements-dev.lock @@ -204,8 +204,9 @@ pyprctl==0.1.3 # via reboot python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.22 # via mcp python-ulid==3.1.0 @@ -213,7 +214,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/bank/requirements.lock b/reboot/examples/bank/requirements.lock index b85b011a..635fca50 100644 --- a/reboot/examples/bank/requirements.lock +++ b/reboot/examples/bank/requirements.lock @@ -200,8 +200,9 @@ pyprctl==0.1.3 # via reboot python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.22 # via mcp python-ulid==3.1.0 @@ -209,7 +210,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/bank/web/package-lock.json b/reboot/examples/bank/web/package-lock.json index c1f76a7e..1cbfd3ee 100644 --- a/reboot/examples/bank/web/package-lock.json +++ b/reboot/examples/bank/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@bufbuild/protobuf": "1.10.1", "@eslint/js": "^9.34.0", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/eslint__js": "^8.42.3", @@ -1227,9 +1227,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -1263,15 +1263,15 @@ } }, "node_modules/@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -1298,12 +1298,12 @@ } }, "node_modules/@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", @@ -6572,9 +6572,9 @@ } }, "@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "requires": { "@scarf/scarf": "1.4.0", "typescript": "5.4.5", @@ -6594,14 +6594,14 @@ } }, "@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "requires": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -6618,11 +6618,11 @@ } }, "@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "requires": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", diff --git a/reboot/examples/bank/web/package.json b/reboot/examples/bank/web/package.json index 31359a62..d579d77d 100644 --- a/reboot/examples/bank/web/package.json +++ b/reboot/examples/bank/web/package.json @@ -6,7 +6,7 @@ "dependencies": { "@bufbuild/protobuf": "1.10.1", "@eslint/js": "^9.34.0", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/eslint__js": "^8.42.3", diff --git a/reboot/examples/boutique/.claude/settings.json b/reboot/examples/boutique/.claude/settings.json new file mode 100644 index 00000000..d522b476 --- /dev/null +++ b/reboot/examples/boutique/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "reboot-plugin": { + "source": { + "source": "github", + "repo": "reboot-dev/reboot-plugin" + } + } + }, + "enabledPlugins": { + "reboot@reboot-plugin": true + } +} diff --git a/reboot/examples/boutique/Dockerfile b/reboot/examples/boutique/Dockerfile index 58e3fa00..668d4c0b 100644 --- a/reboot/examples/boutique/Dockerfile +++ b/reboot/examples/boutique/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/reboot-dev/reboot-base:1.0.4 +FROM ghcr.io/reboot-dev/reboot-base:1.1.0 WORKDIR /app diff --git a/reboot/examples/boutique/pyproject.toml b/reboot/examples/boutique/pyproject.toml index f43f508c..5326581e 100644 --- a/reboot/examples/boutique/pyproject.toml +++ b/reboot/examples/boutique/pyproject.toml @@ -1,7 +1,7 @@ [project] requires-python = ">= 3.10" dependencies = [ - "reboot==1.0.4", + "reboot==1.1.0", ] [tool.rye] @@ -9,7 +9,7 @@ dev-dependencies = [ "mypy==1.18.1", "pytest>=7.4.2", "types-protobuf>=4.24.0.20240129", - "reboot==1.0.4", + "reboot==1.1.0", ] # This project only uses `rye` to provide `python` and its dependencies, so diff --git a/reboot/examples/boutique/requirements-dev.lock b/reboot/examples/boutique/requirements-dev.lock index b4636b3f..3ce516d3 100644 --- a/reboot/examples/boutique/requirements-dev.lock +++ b/reboot/examples/boutique/requirements-dev.lock @@ -211,8 +211,9 @@ pyprctl==0.1.3 pytest==8.3.3 python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.22 # via mcp python-ulid==3.1.0 @@ -220,7 +221,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/boutique/requirements.lock b/reboot/examples/boutique/requirements.lock index b85b011a..635fca50 100644 --- a/reboot/examples/boutique/requirements.lock +++ b/reboot/examples/boutique/requirements.lock @@ -200,8 +200,9 @@ pyprctl==0.1.3 # via reboot python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.22 # via mcp python-ulid==3.1.0 @@ -209,7 +210,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/boutique/web/package-lock.json b/reboot/examples/boutique/web/package-lock.json index 4c872535..e3e376e7 100644 --- a/reboot/examples/boutique/web/package-lock.json +++ b/reboot/examples/boutique/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@bufbuild/protobuf": "1.10.1", "@eslint/js": "^9.34.0", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/eslint__js": "^8.42.3", @@ -1227,9 +1227,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -1263,15 +1263,15 @@ } }, "node_modules/@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -1298,12 +1298,12 @@ } }, "node_modules/@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", @@ -6652,9 +6652,9 @@ } }, "@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "requires": { "@scarf/scarf": "1.4.0", "typescript": "5.4.5", @@ -6674,14 +6674,14 @@ } }, "@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "requires": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -6698,11 +6698,11 @@ } }, "@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "requires": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", diff --git a/reboot/examples/boutique/web/package.json b/reboot/examples/boutique/web/package.json index 1599295f..473fff86 100644 --- a/reboot/examples/boutique/web/package.json +++ b/reboot/examples/boutique/web/package.json @@ -6,7 +6,7 @@ "dependencies": { "@bufbuild/protobuf": "1.10.1", "@eslint/js": "^9.34.0", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/eslint__js": "^8.42.3", diff --git a/reboot/examples/chat-room-nodejs/.tests/serve_test.sh b/reboot/examples/chat-room-nodejs/.tests/serve_test.sh index 99fe54b5..e2270d17 100755 --- a/reboot/examples/chat-room-nodejs/.tests/serve_test.sh +++ b/reboot/examples/chat-room-nodejs/.tests/serve_test.sh @@ -102,6 +102,7 @@ if command -v docker &> /dev/null; then --env=PORT=8787 \ --env=RBT_STATE_DIRECTORY=/app/state/ \ --env=RBT_SERVERS=2 \ + --env=REBOOT_CRYPTO_ROOT_KEYS=v1:reboot-chat-room-nodejs-serve-test \ -p8787:8787 \ --detach \ $image_name \ diff --git a/reboot/examples/chat-room-nodejs/Dockerfile b/reboot/examples/chat-room-nodejs/Dockerfile index 3a3587f5..7e34672f 100644 --- a/reboot/examples/chat-room-nodejs/Dockerfile +++ b/reboot/examples/chat-room-nodejs/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/reboot-dev/reboot-base:1.0.4 +FROM ghcr.io/reboot-dev/reboot-base:1.1.0 WORKDIR /app diff --git a/reboot/examples/chat-room-nodejs/package-lock.json b/reboot/examples/chat-room-nodejs/package-lock.json index 0c79be7b..be55c253 100644 --- a/reboot/examples/chat-room-nodejs/package-lock.json +++ b/reboot/examples/chat-room-nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@bufbuild/protobuf": "1.10.1", - "@reboot-dev/reboot": "1.0.4", + "@reboot-dev/reboot": "1.1.0", "@types/node": "20.11.5", "typescript": "5.4.5" }, @@ -509,15 +509,15 @@ } }, "node_modules/@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -546,9 +546,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -3118,13 +3118,13 @@ "optional": true }, "@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "requires": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -3141,9 +3141,9 @@ } }, "@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "requires": { "@scarf/scarf": "1.4.0", "typescript": "5.4.5", diff --git a/reboot/examples/chat-room-nodejs/package.json b/reboot/examples/chat-room-nodejs/package.json index d1a31f17..5a34f59d 100644 --- a/reboot/examples/chat-room-nodejs/package.json +++ b/reboot/examples/chat-room-nodejs/package.json @@ -5,7 +5,7 @@ "type": "module", "dependencies": { "@bufbuild/protobuf": "1.10.1", - "@reboot-dev/reboot": "1.0.4", + "@reboot-dev/reboot": "1.1.0", "typescript": "5.4.5", "@types/node": "20.11.5" }, diff --git a/reboot/examples/chat-room/.claude/settings.json b/reboot/examples/chat-room/.claude/settings.json new file mode 100644 index 00000000..d522b476 --- /dev/null +++ b/reboot/examples/chat-room/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "reboot-plugin": { + "source": { + "source": "github", + "repo": "reboot-dev/reboot-plugin" + } + } + }, + "enabledPlugins": { + "reboot@reboot-plugin": true + } +} diff --git a/reboot/examples/chat-room/.tests/serve_test.sh b/reboot/examples/chat-room/.tests/serve_test.sh index 64a5a40e..1085bfa0 100755 --- a/reboot/examples/chat-room/.tests/serve_test.sh +++ b/reboot/examples/chat-room/.tests/serve_test.sh @@ -103,6 +103,7 @@ if command -v docker &> /dev/null; then --env=PORT=8787 \ --env=RBT_STATE_DIRECTORY=/app/state/ \ --env=RBT_SERVERS=2 \ + --env=REBOOT_CRYPTO_ROOT_KEYS=v1:reboot-chat-room-serve-test \ -p8787:8787 \ --detach \ $image_name \ diff --git a/reboot/examples/chat-room/Dockerfile b/reboot/examples/chat-room/Dockerfile index 5f2d4c3d..feff1c24 100644 --- a/reboot/examples/chat-room/Dockerfile +++ b/reboot/examples/chat-room/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/reboot-dev/reboot-base:1.0.4 +FROM ghcr.io/reboot-dev/reboot-base:1.1.0 WORKDIR /app diff --git a/reboot/examples/chat-room/pyproject.toml b/reboot/examples/chat-room/pyproject.toml index f43f508c..5326581e 100644 --- a/reboot/examples/chat-room/pyproject.toml +++ b/reboot/examples/chat-room/pyproject.toml @@ -1,7 +1,7 @@ [project] requires-python = ">= 3.10" dependencies = [ - "reboot==1.0.4", + "reboot==1.1.0", ] [tool.rye] @@ -9,7 +9,7 @@ dev-dependencies = [ "mypy==1.18.1", "pytest>=7.4.2", "types-protobuf>=4.24.0.20240129", - "reboot==1.0.4", + "reboot==1.1.0", ] # This project only uses `rye` to provide `python` and its dependencies, so diff --git a/reboot/examples/chat-room/reboot-non-react-web/package-lock.json b/reboot/examples/chat-room/reboot-non-react-web/package-lock.json index 6e71af2c..677e7883 100644 --- a/reboot/examples/chat-room/reboot-non-react-web/package-lock.json +++ b/reboot/examples/chat-room/reboot-non-react-web/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "@bufbuild/protobuf": "1.10.1", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-web": "1.1.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", @@ -71,9 +71,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -98,12 +98,12 @@ } }, "node_modules/@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", diff --git a/reboot/examples/chat-room/reboot-non-react-web/package.json b/reboot/examples/chat-room/reboot-non-react-web/package.json index 70b0c903..f4864a43 100644 --- a/reboot/examples/chat-room/reboot-non-react-web/package.json +++ b/reboot/examples/chat-room/reboot-non-react-web/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@bufbuild/protobuf": "1.10.1", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-web": "1.1.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", diff --git a/reboot/examples/chat-room/requirements-dev.lock b/reboot/examples/chat-room/requirements-dev.lock index b4636b3f..3ce516d3 100644 --- a/reboot/examples/chat-room/requirements-dev.lock +++ b/reboot/examples/chat-room/requirements-dev.lock @@ -211,8 +211,9 @@ pyprctl==0.1.3 pytest==8.3.3 python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.22 # via mcp python-ulid==3.1.0 @@ -220,7 +221,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/chat-room/requirements.lock b/reboot/examples/chat-room/requirements.lock index b85b011a..635fca50 100644 --- a/reboot/examples/chat-room/requirements.lock +++ b/reboot/examples/chat-room/requirements.lock @@ -200,8 +200,9 @@ pyprctl==0.1.3 # via reboot python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.22 # via mcp python-ulid==3.1.0 @@ -209,7 +210,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/chat-room/web/package-lock.json b/reboot/examples/chat-room/web/package-lock.json index 32c8452e..50445e1d 100644 --- a/reboot/examples/chat-room/web/package-lock.json +++ b/reboot/examples/chat-room/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@bufbuild/protobuf": "1.10.1", "@eslint/js": "^9.34.0", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/eslint__js": "^8.42.3", @@ -1226,9 +1226,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -1262,15 +1262,15 @@ } }, "node_modules/@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -1296,12 +1296,12 @@ } }, "node_modules/@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", @@ -6512,9 +6512,9 @@ } }, "@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "requires": { "@scarf/scarf": "1.4.0", "typescript": "5.4.5", @@ -6534,14 +6534,14 @@ } }, "@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "requires": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -6556,11 +6556,11 @@ } }, "@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "requires": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", diff --git a/reboot/examples/chat-room/web/package.json b/reboot/examples/chat-room/web/package.json index 6fa9968f..6f43d513 100644 --- a/reboot/examples/chat-room/web/package.json +++ b/reboot/examples/chat-room/web/package.json @@ -6,7 +6,7 @@ "dependencies": { "@bufbuild/protobuf": "1.10.1", "@eslint/js": "^9.34.0", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/eslint__js": "^8.42.3", diff --git a/reboot/examples/chick-potle/.claude/settings.json b/reboot/examples/chick-potle/.claude/settings.json new file mode 100644 index 00000000..d522b476 --- /dev/null +++ b/reboot/examples/chick-potle/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "reboot-plugin": { + "source": { + "source": "github", + "repo": "reboot-dev/reboot-plugin" + } + } + }, + "enabledPlugins": { + "reboot@reboot-plugin": true + } +} diff --git a/reboot/examples/chick-potle/Dockerfile b/reboot/examples/chick-potle/Dockerfile index 8ae400d0..4281b57b 100644 --- a/reboot/examples/chick-potle/Dockerfile +++ b/reboot/examples/chick-potle/Dockerfile @@ -4,7 +4,7 @@ # locally before `docker build` so that `web/dist/` contains the # bundled UIs. This image copies that prebuilt bundle rather # than installing Node and rebuilding it here. -FROM ghcr.io/reboot-dev/reboot-base:1.0.4 +FROM ghcr.io/reboot-dev/reboot-base:1.1.0 WORKDIR /app diff --git a/reboot/examples/chick-potle/README.md b/reboot/examples/chick-potle/README.md index 9cdd2f88..bf6f2d8d 100644 --- a/reboot/examples/chick-potle/README.md +++ b/reboot/examples/chick-potle/README.md @@ -85,7 +85,7 @@ pytest backend/ `mcp_servers.json` is pre-configured. In another terminal: ```bash -npx @mcpjam/inspector@v2.4.0 --config mcp_servers.json --server chick-potle +npx @mcpjam/inspector@2.9.3 --config mcp_servers.json --server chick-potle ``` Try these prompts to exercise each capability: diff --git a/reboot/examples/chick-potle/backend/src/main.py b/reboot/examples/chick-potle/backend/src/main.py index 727ddae5..081986ee 100644 --- a/reboot/examples/chick-potle/backend/src/main.py +++ b/reboot/examples/chick-potle/backend/src/main.py @@ -1,7 +1,10 @@ import asyncio import logging from reboot.aio.applications import Application -from reboot.aio.auth.oauth_providers import Anonymous +from reboot.aio.auth.oauth_providers import ( + Development, + OAuthProviderByEnvironment, +) from servicers.food import FoodOrderServicer, UserServicer logging.basicConfig( @@ -15,7 +18,13 @@ async def main() -> None: servicers=[UserServicer, FoodOrderServicer], # `User` is an auto-constructed state type, so Reboot # needs an OAuth provider to identify the caller. - oauth=Anonymous(), + oauth=OAuthProviderByEnvironment( + dev=Development(), + # TODO: set a real provider (e.g. `Google(...)`) before + # production; `prod=None` makes a production deployment fail + # to start until one is chosen. + prod=None, + ), ) await application.run() diff --git a/reboot/examples/chick-potle/backend/tests/food_test.py b/reboot/examples/chick-potle/backend/tests/food_test.py index 024acc03..818fb6d9 100644 --- a/reboot/examples/chick-potle/backend/tests/food_test.py +++ b/reboot/examples/chick-potle/backend/tests/food_test.py @@ -6,7 +6,8 @@ from ai_chat_food.v1.food_rbt import FoodOrder, User from reboot.aio.applications import Application from reboot.aio.auth.authorizers import allow -from reboot.aio.tests import Reboot +from reboot.aio.auth.oauth_providers import Anonymous +from reboot.aio.tests import OAuthProviderForTest, Reboot from servicers.food import FoodOrderServicer, UserServicer # Production servicers intentionally don't define an @@ -43,7 +44,10 @@ async def asyncSetUp(self) -> None: self.rbt = Reboot() await self.rbt.start() await self.rbt.up( - Application(servicers=APPLICATION_SERVICERS), + Application( + servicers=APPLICATION_SERVICERS, + oauth=OAuthProviderForTest(Anonymous()), + ), ) self.user_id = "alice" self.context = self.rbt.create_external_context( diff --git a/reboot/examples/chick-potle/pyproject.toml b/reboot/examples/chick-potle/pyproject.toml index 0a62c256..a6310e03 100644 --- a/reboot/examples/chick-potle/pyproject.toml +++ b/reboot/examples/chick-potle/pyproject.toml @@ -6,13 +6,13 @@ dependencies = [ "httpx>=0.27,<1.0", "uuid7>=0.1.0", "anyio>=4.0.0", - "reboot==1.0.4", + "reboot==1.1.0", ] [tool.rye] dev-dependencies = [ "pytest>=7.4", - "reboot==1.0.4", + "reboot==1.1.0", ] # This project only uses `rye` to provide `python` and its dependencies. diff --git a/reboot/examples/chick-potle/requirements-dev.lock b/reboot/examples/chick-potle/requirements-dev.lock index c9cd62d7..191d783b 100644 --- a/reboot/examples/chick-potle/requirements-dev.lock +++ b/reboot/examples/chick-potle/requirements-dev.lock @@ -209,8 +209,9 @@ pyprctl==0.1.3 pytest==9.0.3 python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.27 # via mcp python-ulid==3.1.0 @@ -218,7 +219,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/chick-potle/requirements.lock b/reboot/examples/chick-potle/requirements.lock index 571edf03..67feabca 100644 --- a/reboot/examples/chick-potle/requirements.lock +++ b/reboot/examples/chick-potle/requirements.lock @@ -200,8 +200,9 @@ pyprctl==0.1.3 # via reboot python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.27 # via mcp python-ulid==3.1.0 @@ -209,7 +210,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/chick-potle/web/package-lock.json b/reboot/examples/chick-potle/web/package-lock.json index 8c48cd43..a723ad89 100644 --- a/reboot/examples/chick-potle/web/package-lock.json +++ b/reboot/examples/chick-potle/web/package-lock.json @@ -10,8 +10,8 @@ "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "zod": "^3.25.0" @@ -886,9 +886,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -913,15 +913,15 @@ } }, "node_modules/@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -949,12 +949,12 @@ } }, "node_modules/@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", diff --git a/reboot/examples/chick-potle/web/package.json b/reboot/examples/chick-potle/web/package.json index 0975b36d..48c06bdc 100644 --- a/reboot/examples/chick-potle/web/package.json +++ b/reboot/examples/chick-potle/web/package.json @@ -15,8 +15,8 @@ "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-react": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "zod": "^3.25.0" diff --git a/reboot/examples/counter/.claude/settings.json b/reboot/examples/counter/.claude/settings.json new file mode 100644 index 00000000..d522b476 --- /dev/null +++ b/reboot/examples/counter/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "reboot-plugin": { + "source": { + "source": "github", + "repo": "reboot-dev/reboot-plugin" + } + } + }, + "enabledPlugins": { + "reboot@reboot-plugin": true + } +} diff --git a/reboot/examples/counter/package-lock.json b/reboot/examples/counter/package-lock.json index e734b380..adcbc139 100644 --- a/reboot/examples/counter/package-lock.json +++ b/reboot/examples/counter/package-lock.json @@ -9,8 +9,8 @@ "version": "0.0.0", "dependencies": { "@bufbuild/protobuf": "1.10.1", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", "next": "14.2.13", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -1346,15 +1346,15 @@ } }, "node_modules/@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -1383,9 +1383,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -1410,15 +1410,15 @@ } }, "node_modules/@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -1445,12 +1445,12 @@ } }, "node_modules/@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", @@ -9625,13 +9625,13 @@ "optional": true }, "@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "requires": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -9648,9 +9648,9 @@ } }, "@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "requires": { "@scarf/scarf": "1.4.0", "typescript": "5.4.5", @@ -9665,14 +9665,14 @@ } }, "@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "requires": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -9689,11 +9689,11 @@ } }, "@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "requires": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", diff --git a/reboot/examples/counter/package.json b/reboot/examples/counter/package.json index d18e6bbd..bdfb310f 100644 --- a/reboot/examples/counter/package.json +++ b/reboot/examples/counter/package.json @@ -11,8 +11,8 @@ }, "dependencies": { "@bufbuild/protobuf": "1.10.1", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", "next": "14.2.13", "react": "^18.3.1", "react-dom": "^18.3.1" diff --git a/reboot/examples/docubot/.claude/settings.json b/reboot/examples/docubot/.claude/settings.json new file mode 100644 index 00000000..d522b476 --- /dev/null +++ b/reboot/examples/docubot/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "reboot-plugin": { + "source": { + "source": "github", + "repo": "reboot-dev/reboot-plugin" + } + } + }, + "enabledPlugins": { + "reboot@reboot-plugin": true + } +} diff --git a/reboot/examples/docubot/api/package.json b/reboot/examples/docubot/api/package.json index 62ed7ecd..221f6766 100644 --- a/reboot/examples/docubot/api/package.json +++ b/reboot/examples/docubot/api/package.json @@ -7,7 +7,7 @@ "prepack": "rbt generate && tsc" }, "dependencies": { - "@reboot-dev/reboot": "1.0.4", + "@reboot-dev/reboot": "1.1.0", "typescript": "^5.2.2" }, "files": [ diff --git a/reboot/examples/docubot/docubot/package.json b/reboot/examples/docubot/docubot/package.json index 62694719..6b9b27b8 100644 --- a/reboot/examples/docubot/docubot/package.json +++ b/reboot/examples/docubot/docubot/package.json @@ -8,8 +8,8 @@ }, "dependencies": { "@reboot-dev/docubot-api": "0.1.0", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", "create-temp-directory": "^2.4.0", "openai": "^4.52.7", "puppeteer": "^22.14.0", diff --git a/reboot/examples/docubot/package-lock.json b/reboot/examples/docubot/package-lock.json index 1a0c15b9..7a66a36a 100644 --- a/reboot/examples/docubot/package-lock.json +++ b/reboot/examples/docubot/package-lock.json @@ -18,9 +18,9 @@ "@radix-ui/react-slot": "^1.0.2", "@reboot-dev/docubot": "workspace:*", "@reboot-dev/docubot-api": "workspace:*", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "framer-motion": "^11.0.24", @@ -54,7 +54,7 @@ "name": "@reboot-dev/docubot-api", "version": "0.1.0", "dependencies": { - "@reboot-dev/reboot": "1.0.4", + "@reboot-dev/reboot": "1.1.0", "typescript": "^5.2.2" } }, @@ -63,8 +63,8 @@ "version": "0.1.0", "dependencies": { "@reboot-dev/docubot-api": "0.1.0", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", "create-temp-directory": "^2.4.0", "openai": "^4.52.7", "puppeteer": "^22.14.0", @@ -1651,15 +1651,15 @@ "link": true }, "node_modules/@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -1688,9 +1688,9 @@ } }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -1722,15 +1722,15 @@ } }, "node_modules/@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -1793,12 +1793,12 @@ } }, "node_modules/@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", @@ -11365,8 +11365,8 @@ "version": "file:docubot", "requires": { "@reboot-dev/docubot-api": "0.1.0", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", "@types/node": "^20.12.4", "create-temp-directory": "^2.4.0", "openai": "^4.52.7", @@ -11386,18 +11386,18 @@ "@reboot-dev/docubot-api": { "version": "file:api", "requires": { - "@reboot-dev/reboot": "1.0.4", + "@reboot-dev/reboot": "1.1.0", "typescript": "^5.2.2" } }, "@reboot-dev/reboot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.0.4.tgz", - "integrity": "sha512-ZWprGzt7sx1rN0D6ZKUFx7mNw7Q7Zpp0LER7DB7CpbJV26fj0HnNlQvDBJvSNvoUUPBSRktMDaH3ZCypHSuQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot/-/reboot-1.1.0.tgz", + "integrity": "sha512-NY07xw1x+ziXksCqRNgxUGnw/vaawUWQsw+9yy9drUC+gd/06qQDrGFBcQmIYAw7jTvEZiTvfNaoa2bEuEu0oQ==", "requires": { "@bufbuild/protoc-gen-es": "1.10.1", "@bufbuild/protoplugin": "1.10.1", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "@standard-schema/spec": "1.0.0", "chalk": "^4.1.2", @@ -11421,9 +11421,9 @@ } }, "@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "requires": { "@scarf/scarf": "1.4.0", "typescript": "5.4.5", @@ -11443,14 +11443,14 @@ } }, "@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "requires": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -11479,11 +11479,11 @@ } }, "@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "requires": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", diff --git a/reboot/examples/docubot/package.json b/reboot/examples/docubot/package.json index c72016b1..8135f8e0 100644 --- a/reboot/examples/docubot/package.json +++ b/reboot/examples/docubot/package.json @@ -19,9 +19,9 @@ "@radix-ui/react-slot": "^1.0.2", "@reboot-dev/docubot": "workspace:*", "@reboot-dev/docubot-api": "workspace:*", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "framer-motion": "^11.0.24", diff --git a/reboot/examples/kcdc-2025/backend/src/servicers/channel.py b/reboot/examples/kcdc-2025/backend/src/servicers/channel.py index 5ab7709f..0290d766 100644 --- a/reboot/examples/kcdc-2025/backend/src/servicers/channel.py +++ b/reboot/examples/kcdc-2025/backend/src/servicers/channel.py @@ -10,6 +10,7 @@ ) from chat.v1.message_rbt import Message from reboot.aio.auth.authorizers import allow +from reboot.aio.concurrently import concurrently from reboot.aio.contexts import ReaderContext, TransactionContext from reboot.protobuf import as_str, from_str from reboot.std.index.v1.index import Index @@ -76,15 +77,17 @@ async def messages( limit=request.limit, ) - timestamps = [entry.key for entry in response.entries] - message_ids = [as_str(entry.value) for entry in response.entries] - - responses = await Message.forall(message_ids).get(context) - return MessagesResponse( messages={ - timestamps[i]: response.details - for i, response in enumerate(responses) + timestamp: response.details + async for (timestamp, _), response in concurrently( + lambda _, + message_id: Message.ref(message_id).get(context), + for_each=[ + (entry.key, as_str(entry.value)) + for entry in response.entries + ], + ) } ) diff --git a/reboot/examples/kcdc-2025/backend/src/servicers/chatbot.py b/reboot/examples/kcdc-2025/backend/src/servicers/chatbot.py index 4c4679d8..b656705b 100644 --- a/reboot/examples/kcdc-2025/backend/src/servicers/chatbot.py +++ b/reboot/examples/kcdc-2025/backend/src/servicers/chatbot.py @@ -18,6 +18,7 @@ from langchain.chat_models import init_chat_model from pydantic import BaseModel from reboot.aio.auth.authorizers import allow +from reboot.aio.concurrently import concurrently from reboot.aio.contexts import ( ReaderContext, TransactionContext, @@ -145,16 +146,13 @@ async def control_loop( # Expecting a bunch of message IDs. message_ids = [as_str(item.value) for item in dequeue.items] - # Get each message's "details". + # Get each message's details and filter out messages from + # us. messages = [ - get.details - for get in await Message.forall(message_ids).get(context) - ] - - # Filter out messages from us. - messages = [ - message for message in messages - if message.author != request.name + response.details async for response in concurrently( + Message.ref(message_id).get(context) + for message_id in message_ids + ) if response.details.author != request.name ] # Check if there are any messages excluding those from us. diff --git a/reboot/examples/kcdc-2025/pyproject.toml b/reboot/examples/kcdc-2025/pyproject.toml index a21cbc5a..34e42b26 100644 --- a/reboot/examples/kcdc-2025/pyproject.toml +++ b/reboot/examples/kcdc-2025/pyproject.toml @@ -5,13 +5,13 @@ requires-python = ">= 3.10" dependencies = [ "langchain>=0.3.27", "langchain-anthropic>=0.3.18", - "reboot==1.0.4", + "reboot==1.1.0", ] [tool.rye] dev-dependencies = [ "mypy==1.18.1", "types-protobuf>=4.24.0.20240129", - "reboot==1.0.4", + "reboot==1.1.0", ] # This project only uses `rye` to provide `python` and its dependencies, so diff --git a/reboot/examples/kcdc-2025/requirements-dev.lock b/reboot/examples/kcdc-2025/requirements-dev.lock index f03aa99a..f57e2ce5 100644 --- a/reboot/examples/kcdc-2025/requirements-dev.lock +++ b/reboot/examples/kcdc-2025/requirements-dev.lock @@ -49,7 +49,6 @@ charset-normalizer==3.4.3 click==8.2.1 # via uvicorn colorama==0.4.6 - # via griffecli # via reboot cryptography==44.0.0 # via pyjwt @@ -76,13 +75,8 @@ googleapis-common-protos==1.65.0 # via reboot greenlet==3.2.4 # via sqlalchemy -griffe==2.0.2 - # via pydantic-ai-slim -griffecli==2.0.2 - # via griffe griffelib==2.0.2 - # via griffe - # via griffecli + # via pydantic-ai-slim grpc-interceptor==0.15.4 # via reboot grpcio==1.64.3 @@ -222,7 +216,7 @@ psutil==6.0.0 # via reboot pycparser==2.22 # via cffi -pydantic==2.11.7 +pydantic==2.13.4 # via anthropic # via fastapi # via genai-prices @@ -235,11 +229,11 @@ pydantic==2.11.7 # via pydantic-graph # via pydantic-settings # via reboot -pydantic-ai-slim==1.60.0 +pydantic-ai-slim==1.87.0 # via reboot -pydantic-core==2.33.2 +pydantic-core==2.46.4 # via pydantic -pydantic-graph==1.60.0 +pydantic-graph==1.87.0 # via pydantic-ai-slim pydantic-settings==2.13.1 # via mcp @@ -250,8 +244,9 @@ pyprctl==0.1.3 # via reboot python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.22 # via mcp python-ulid==3.1.0 @@ -261,7 +256,7 @@ pyyaml==6.0.2 # via langchain # via langchain-core # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications @@ -316,7 +311,7 @@ typing-extensions==4.15.0 # via sqlalchemy # via typing-inspection # via uvicorn -typing-inspection==0.4.1 +typing-inspection==0.4.2 # via mcp # via pydantic # via pydantic-ai-slim diff --git a/reboot/examples/kcdc-2025/requirements.lock b/reboot/examples/kcdc-2025/requirements.lock index 2e265dba..80534d4b 100644 --- a/reboot/examples/kcdc-2025/requirements.lock +++ b/reboot/examples/kcdc-2025/requirements.lock @@ -49,7 +49,6 @@ charset-normalizer==3.4.3 click==8.2.1 # via uvicorn colorama==0.4.6 - # via griffecli # via reboot cryptography==44.0.0 # via pyjwt @@ -76,13 +75,8 @@ googleapis-common-protos==1.65.0 # via reboot greenlet==3.2.4 # via sqlalchemy -griffe==2.0.2 - # via pydantic-ai-slim -griffecli==2.0.2 - # via griffe griffelib==2.0.2 - # via griffe - # via griffecli + # via pydantic-ai-slim grpc-interceptor==0.15.4 # via reboot grpcio==1.64.3 @@ -218,7 +212,7 @@ psutil==6.0.0 # via reboot pycparser==2.22 # via cffi -pydantic==2.11.7 +pydantic==2.13.4 # via anthropic # via fastapi # via genai-prices @@ -231,11 +225,11 @@ pydantic==2.11.7 # via pydantic-graph # via pydantic-settings # via reboot -pydantic-ai-slim==1.60.0 +pydantic-ai-slim==1.87.0 # via reboot -pydantic-core==2.33.2 +pydantic-core==2.46.4 # via pydantic -pydantic-graph==1.60.0 +pydantic-graph==1.87.0 # via pydantic-ai-slim pydantic-settings==2.13.1 # via mcp @@ -246,8 +240,9 @@ pyprctl==0.1.3 # via reboot python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.22 # via mcp python-ulid==3.1.0 @@ -257,7 +252,7 @@ pyyaml==6.0.2 # via langchain # via langchain-core # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications @@ -309,7 +304,7 @@ typing-extensions==4.15.0 # via sqlalchemy # via typing-inspection # via uvicorn -typing-inspection==0.4.1 +typing-inspection==0.4.2 # via mcp # via pydantic # via pydantic-ai-slim diff --git a/reboot/examples/kcdc-2025/uv.lock b/reboot/examples/kcdc-2025/uv.lock index 87c7118c..a09e17c4 100644 --- a/reboot/examples/kcdc-2025/uv.lock +++ b/reboot/examples/kcdc-2025/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] [[package]] name = "aiofiles" @@ -22,7 +26,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.10.5" +version = "3.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -31,70 +35,113 @@ dependencies = [ { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, + { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/28/ca549838018140b92a19001a8628578b0f2a3b38c16826212cc6f706e6d4/aiohttp-3.10.5.tar.gz", hash = "sha256:f071854b47d39591ce9a17981c46790acb30518e2f83dfca8db2dfa091178691", size = 7524360, upload-time = "2024-08-19T20:10:48.406Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/4a/b27dd9b88fe22dde88742b341fd10251746a6ffcfe1c0b8b15b4a8cbd7c1/aiohttp-3.10.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3", size = 587010, upload-time = "2024-08-19T20:08:05.444Z" }, - { url = "https://files.pythonhosted.org/packages/de/a9/0f7e2b71549c9d641086c423526ae7a10de3b88d03ba104a3df153574d0d/aiohttp-3.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6", size = 397698, upload-time = "2024-08-19T20:08:07.525Z" }, - { url = "https://files.pythonhosted.org/packages/3b/52/26baa486e811c25b0cd16a494038260795459055568713f841e78f016481/aiohttp-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f1f1c75c395991ce9c94d3e4aa96e5c59c8356a15b1c9231e783865e2772699", size = 389052, upload-time = "2024-08-19T20:08:08.931Z" }, - { url = "https://files.pythonhosted.org/packages/33/df/71ba374a3e925539cb2f6e6d4f5326e7b6b200fabbe1b3cc5e6368f07ce7/aiohttp-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7acae3cf1a2a2361ec4c8e787eaaa86a94171d2417aae53c0cca6ca3118ff6", size = 1248615, upload-time = "2024-08-19T20:08:10.625Z" }, - { url = "https://files.pythonhosted.org/packages/67/02/bb89c1eba08a27fc844933bee505d63d480caf8e2816c06961d2941cd128/aiohttp-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94c4381ffba9cc508b37d2e536b418d5ea9cfdc2848b9a7fea6aebad4ec6aac1", size = 1282930, upload-time = "2024-08-19T20:08:12.483Z" }, - { url = "https://files.pythonhosted.org/packages/db/36/07d8cfcc37f39c039f93a4210cc71dadacca003609946c63af23659ba656/aiohttp-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c31ad0c0c507894e3eaa843415841995bf8de4d6b2d24c6e33099f4bc9fc0d4f", size = 1317250, upload-time = "2024-08-19T20:08:14.383Z" }, - { url = "https://files.pythonhosted.org/packages/9a/44/cabeac994bef8ba521b552ae996928afc6ee1975a411385a07409811b01f/aiohttp-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0912b8a8fadeb32ff67a3ed44249448c20148397c1ed905d5dac185b4ca547bb", size = 1243212, upload-time = "2024-08-19T20:08:16.161Z" }, - { url = "https://files.pythonhosted.org/packages/5a/11/23f1e31f5885ac72be52fd205981951dd2e4c87c5b1487cf82fde5bbd46c/aiohttp-3.10.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d93400c18596b7dc4794d48a63fb361b01a0d8eb39f28800dc900c8fbdaca91", size = 1213401, upload-time = "2024-08-19T20:08:17.977Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e7/6e69a0b0d896fbaf1192d492db4c21688e6c0d327486da610b0e8195bcc9/aiohttp-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3c5e0d764a5c9aa5a62d99728c56d455310bcc288a79cab10157b3af426f", size = 1212450, upload-time = "2024-08-19T20:08:19.519Z" }, - { url = "https://files.pythonhosted.org/packages/a9/7f/a42f51074c723ea848254946aec118f1e59914a639dc8ba20b0c9247c195/aiohttp-3.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d742c36ed44f2798c8d3f4bc511f479b9ceef2b93f348671184139e7d708042c", size = 1211324, upload-time = "2024-08-19T20:08:21.465Z" }, - { url = "https://files.pythonhosted.org/packages/d5/43/c2f9d2f588ccef8f028f0a0c999b5ceafecbda50b943313faee7e91f3e03/aiohttp-3.10.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:814375093edae5f1cb31e3407997cf3eacefb9010f96df10d64829362ae2df69", size = 1266838, upload-time = "2024-08-19T20:08:23.513Z" }, - { url = "https://files.pythonhosted.org/packages/c1/a7/ff9f067ecb06896d859e4f2661667aee4bd9c616689599ff034b63cbd9d7/aiohttp-3.10.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8224f98be68a84b19f48e0bdc14224b5a71339aff3a27df69989fa47d01296f3", size = 1285301, upload-time = "2024-08-19T20:08:25.074Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/dd56bb4c67d216046ce61d98dec0f3023043f1de48f561df1bf93dd47aea/aiohttp-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9a487ef090aea982d748b1b0d74fe7c3950b109df967630a20584f9a99c0683", size = 1235806, upload-time = "2024-08-19T20:08:26.712Z" }, - { url = "https://files.pythonhosted.org/packages/a7/64/90dcd42ac21927a49ba4140b2e4d50e1847379427ef6c43eb338ef9960e3/aiohttp-3.10.5-cp310-cp310-win32.whl", hash = "sha256:d9ef084e3dc690ad50137cc05831c52b6ca428096e6deb3c43e95827f531d5ef", size = 360162, upload-time = "2024-08-19T20:08:28.372Z" }, - { url = "https://files.pythonhosted.org/packages/f3/45/145d8b4853fc92c0c8509277642767e7726a085e390ce04353dc68b0f5b5/aiohttp-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:66bf9234e08fe561dccd62083bf67400bdbf1c67ba9efdc3dac03650e97c6088", size = 379173, upload-time = "2024-08-19T20:08:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/f1/90/54ccb1e4eadfb6c95deff695582453f6208584431d69bf572782e9ae542b/aiohttp-3.10.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c6a4e5e40156d72a40241a25cc226051c0a8d816610097a8e8f517aeacd59a2", size = 586455, upload-time = "2024-08-19T20:08:31.409Z" }, - { url = "https://files.pythonhosted.org/packages/c3/7a/95e88c02756e7e718f054e1bb3ec6ad5d0ee4a2ca2bb1768c5844b3de30a/aiohttp-3.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c634a3207a5445be65536d38c13791904fda0748b9eabf908d3fe86a52941cf", size = 397255, upload-time = "2024-08-19T20:08:32.659Z" }, - { url = "https://files.pythonhosted.org/packages/07/4f/767387b39990e1ee9aba8ce642abcc286d84d06e068dc167dab983898f18/aiohttp-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4aff049b5e629ef9b3e9e617fa6e2dfeda1bf87e01bcfecaf3949af9e210105e", size = 388973, upload-time = "2024-08-19T20:08:33.9Z" }, - { url = "https://files.pythonhosted.org/packages/61/46/0df41170a4d228c07b661b1ba9d87101d99a79339dc93b8b1183d8b20545/aiohttp-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1942244f00baaacaa8155eca94dbd9e8cc7017deb69b75ef67c78e89fdad3c77", size = 1326126, upload-time = "2024-08-19T20:08:35.606Z" }, - { url = "https://files.pythonhosted.org/packages/af/20/da0d65e07ce49d79173fed41598f487a0a722e87cfbaa8bb7e078a7c1d39/aiohttp-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04a1f2a65ad2f93aa20f9ff9f1b672bf912413e5547f60749fa2ef8a644e061", size = 1364538, upload-time = "2024-08-19T20:08:37.754Z" }, - { url = "https://files.pythonhosted.org/packages/aa/20/b59728405114e57541ba9d5b96033e69d004e811ded299537f74237629ca/aiohttp-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f2bfc0032a00405d4af2ba27f3c429e851d04fad1e5ceee4080a1c570476697", size = 1399896, upload-time = "2024-08-19T20:08:39.635Z" }, - { url = "https://files.pythonhosted.org/packages/2a/92/006690c31b830acbae09d2618e41308fe4c81c0679b3b33a3af859e0b7bf/aiohttp-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:424ae21498790e12eb759040bbb504e5e280cab64693d14775c54269fd1d2bb7", size = 1312914, upload-time = "2024-08-19T20:08:41.417Z" }, - { url = "https://files.pythonhosted.org/packages/d4/71/1a253ca215b6c867adbd503f1e142117527ea8775e65962bc09b2fad1d2c/aiohttp-3.10.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:975218eee0e6d24eb336d0328c768ebc5d617609affaca5dbbd6dd1984f16ed0", size = 1271301, upload-time = "2024-08-19T20:08:43.39Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ab/5d1d9ff9ce6cce8fa54774d0364e64a0f3cd50e512ff09082ced8e5217a1/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4120d7fefa1e2d8fb6f650b11489710091788de554e2b6f8347c7a20ceb003f5", size = 1291652, upload-time = "2024-08-19T20:08:45.335Z" }, - { url = "https://files.pythonhosted.org/packages/75/5f/f90510ea954b9ae6e7a53d2995b97a3e5c181110fdcf469bc9238445871d/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b90078989ef3fc45cf9221d3859acd1108af7560c52397ff4ace8ad7052a132e", size = 1286289, upload-time = "2024-08-19T20:08:47.546Z" }, - { url = "https://files.pythonhosted.org/packages/be/9e/1f523414237798660921817c82b9225a363af436458caf584d2fa6a2eb4a/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ba5a8b74c2a8af7d862399cdedce1533642fa727def0b8c3e3e02fcb52dca1b1", size = 1341848, upload-time = "2024-08-19T20:08:49.698Z" }, - { url = "https://files.pythonhosted.org/packages/f6/36/443472ddaa85d7d80321fda541d9535b23ecefe0bf5792cc3955ea635190/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:02594361128f780eecc2a29939d9dfc870e17b45178a867bf61a11b2a4367277", size = 1361619, upload-time = "2024-08-19T20:08:51.942Z" }, - { url = "https://files.pythonhosted.org/packages/19/f6/3ecbac0bc4359c7d7ba9e85c6b10f57e20edaf1f97751ad2f892db231ad0/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4fc029e135859f533025bc82047334e24b0d489e75513144f25408ecaf058", size = 1320869, upload-time = "2024-08-19T20:08:53.708Z" }, - { url = "https://files.pythonhosted.org/packages/34/7e/ed74ffb36e3a0cdec1b05d8fbaa29cb532371d5a20058b3a8052fc90fe7c/aiohttp-3.10.5-cp311-cp311-win32.whl", hash = "sha256:e1ca1ef5ba129718a8fc827b0867f6aa4e893c56eb00003b7367f8a733a9b072", size = 359271, upload-time = "2024-08-19T20:08:55.186Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/718901f04bc8c886a742be9e83babb7b93facabf7c475cc95e2b3ab80b4d/aiohttp-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:349ef8a73a7c5665cca65c88ab24abe75447e28aa3bc4c93ea5093474dfdf0ff", size = 379143, upload-time = "2024-08-19T20:08:56.604Z" }, - { url = "https://files.pythonhosted.org/packages/d9/1c/74f9dad4a2fc4107e73456896283d915937f48177b99867b63381fadac6e/aiohttp-3.10.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:305be5ff2081fa1d283a76113b8df7a14c10d75602a38d9f012935df20731487", size = 583468, upload-time = "2024-08-19T20:08:58.17Z" }, - { url = "https://files.pythonhosted.org/packages/12/29/68d090551f2b58ce76c2b436ced8dd2dfd32115d41299bf0b0c308a5483c/aiohttp-3.10.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3a1c32a19ee6bbde02f1cb189e13a71b321256cc1d431196a9f824050b160d5a", size = 394066, upload-time = "2024-08-19T20:08:59.569Z" }, - { url = "https://files.pythonhosted.org/packages/8f/f7/971f88b4cdcaaa4622925ba7d86de47b48ec02a9040a143514b382f78da4/aiohttp-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61645818edd40cc6f455b851277a21bf420ce347baa0b86eaa41d51ef58ba23d", size = 389098, upload-time = "2024-08-19T20:09:00.965Z" }, - { url = "https://files.pythonhosted.org/packages/f1/5a/fe3742efdce551667b2ddf1158b27c5b8eb1edc13d5e14e996e52e301025/aiohttp-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c225286f2b13bab5987425558baa5cbdb2bc925b2998038fa028245ef421e75", size = 1332742, upload-time = "2024-08-19T20:09:02.29Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/a25c0334a1845eb4967dff279151b67ca32a948145a5812ed660ed900868/aiohttp-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ba01ebc6175e1e6b7275c907a3a36be48a2d487549b656aa90c8a910d9f3178", size = 1372134, upload-time = "2024-08-19T20:09:03.829Z" }, - { url = "https://files.pythonhosted.org/packages/96/3d/33c1d8efc2d8ec36bff9a8eca2df9fdf8a45269c6e24a88e74f2aa4f16bd/aiohttp-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eaf44ccbc4e35762683078b72bf293f476561d8b68ec8a64f98cf32811c323e", size = 1414413, upload-time = "2024-08-19T20:09:05.74Z" }, - { url = "https://files.pythonhosted.org/packages/64/74/0f1ddaa5f0caba1d946f0dd0c31f5744116e4a029beec454ec3726d3311f/aiohttp-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c43eb1ab7cbf411b8e387dc169acb31f0ca0d8c09ba63f9eac67829585b44f", size = 1328107, upload-time = "2024-08-19T20:09:07.476Z" }, - { url = "https://files.pythonhosted.org/packages/0a/32/c10118f0ad50e4093227234f71fd0abec6982c29367f65f32ee74ed652c4/aiohttp-3.10.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7a5299827253023c55ea549444e058c0eb496931fa05d693b95140a947cb73", size = 1280126, upload-time = "2024-08-19T20:09:09.061Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c9/77e3d648d97c03a42acfe843d03e97be3c5ef1b4d9de52e5bd2d28eed8e7/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4790f0e15f00058f7599dab2b206d3049d7ac464dc2e5eae0e93fa18aee9e7bf", size = 1292660, upload-time = "2024-08-19T20:09:10.853Z" }, - { url = "https://files.pythonhosted.org/packages/7e/5d/99c71f8e5c8b64295be421b4c42d472766b263a1fe32e91b64bf77005bf2/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44b324a6b8376a23e6ba25d368726ee3bc281e6ab306db80b5819999c737d820", size = 1300988, upload-time = "2024-08-19T20:09:12.341Z" }, - { url = "https://files.pythonhosted.org/packages/8f/2c/76d2377dd947f52fbe8afb19b18a3b816d66c7966755c04030f93b1f7b2d/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0d277cfb304118079e7044aad0b76685d30ecb86f83a0711fc5fb257ffe832ca", size = 1339268, upload-time = "2024-08-19T20:09:14.198Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e6/3d9d935cc705d57ed524d82ec5d6b678a53ac1552720ae41282caa273584/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:54d9ddea424cd19d3ff6128601a4a4d23d54a421f9b4c0fff740505813739a91", size = 1366993, upload-time = "2024-08-19T20:09:15.725Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c2/f7eed4d602f3f224600d03ab2e1a7734999b0901b1c49b94dc5891340433/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f1c9866ccf48a6df2b06823e6ae80573529f2af3a0992ec4fe75b1a510df8a6", size = 1329459, upload-time = "2024-08-19T20:09:17.556Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8f/27f205b76531fc592abe29e1ad265a16bf934a9f609509c02d765e6a8055/aiohttp-3.10.5-cp312-cp312-win32.whl", hash = "sha256:dc4826823121783dccc0871e3f405417ac116055bf184ac04c36f98b75aacd12", size = 356968, upload-time = "2024-08-19T20:09:19.14Z" }, - { url = "https://files.pythonhosted.org/packages/39/8c/4f6c0b2b3629f6be6c81ab84d9d577590f74f01d4412bfc4067958eaa1e1/aiohttp-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:22c0a23a3b3138a6bf76fc553789cb1a703836da86b0f306b6f0dc1617398abc", size = 377650, upload-time = "2024-08-19T20:09:20.502Z" }, - { url = "https://files.pythonhosted.org/packages/7b/b9/03b4327897a5b5d29338fa9b514f1c2f66a3e4fc88a4e40fad478739314d/aiohttp-3.10.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7f6b639c36734eaa80a6c152a238242bedcee9b953f23bb887e9102976343092", size = 576994, upload-time = "2024-08-19T20:09:22.439Z" }, - { url = "https://files.pythonhosted.org/packages/67/1b/20c2e159cd07b8ed6dde71c2258233902fdf415b2fe6174bd2364ba63107/aiohttp-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29930bc2921cef955ba39a3ff87d2c4398a0394ae217f41cb02d5c26c8b1b77", size = 390684, upload-time = "2024-08-19T20:09:23.844Z" }, - { url = "https://files.pythonhosted.org/packages/4d/6b/ff83b34f157e370431d8081c5d1741963f4fb12f9aaddb2cacbf50305225/aiohttp-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f489a2c9e6455d87eabf907ac0b7d230a9786be43fbe884ad184ddf9e9c1e385", size = 386176, upload-time = "2024-08-19T20:09:25.259Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/6e92817eb657de287560962df4959b7ddd22859c4b23a0309e2d3de12538/aiohttp-3.10.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:123dd5b16b75b2962d0fff566effb7a065e33cd4538c1692fb31c3bda2bfb972", size = 1303310, upload-time = "2024-08-19T20:09:27.098Z" }, - { url = "https://files.pythonhosted.org/packages/04/29/200518dc7a39c30ae6d5bc232d7207446536e93d3d9299b8e95db6e79c54/aiohttp-3.10.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b98e698dc34966e5976e10bbca6d26d6724e6bdea853c7c10162a3235aba6e16", size = 1340445, upload-time = "2024-08-19T20:09:29.228Z" }, - { url = "https://files.pythonhosted.org/packages/8e/20/53f7bba841ba7b5bb5dea580fea01c65524879ba39cb917d08c845524717/aiohttp-3.10.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3b9162bab7e42f21243effc822652dc5bb5e8ff42a4eb62fe7782bcbcdfacf6", size = 1385121, upload-time = "2024-08-19T20:09:30.85Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b4/d99354ad614c48dd38fb1ee880a1a54bd9ab2c3bcad3013048d4a1797d3a/aiohttp-3.10.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1923a5c44061bffd5eebeef58cecf68096e35003907d8201a4d0d6f6e387ccaa", size = 1299669, upload-time = "2024-08-19T20:09:32.383Z" }, - { url = "https://files.pythonhosted.org/packages/51/39/ca1de675f2a5729c71c327e52ac6344e63f036bd37281686ae5c3fb13bfb/aiohttp-3.10.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d55f011da0a843c3d3df2c2cf4e537b8070a419f891c930245f05d329c4b0689", size = 1252638, upload-time = "2024-08-19T20:09:34.287Z" }, - { url = "https://files.pythonhosted.org/packages/54/cf/a3ae7ff43138422d477348e309ef8275779701bf305ff6054831ef98b782/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:afe16a84498441d05e9189a15900640a2d2b5e76cf4efe8cbb088ab4f112ee57", size = 1266889, upload-time = "2024-08-19T20:09:36.427Z" }, - { url = "https://files.pythonhosted.org/packages/6e/7a/c6027ad70d9fb23cf254a26144de2723821dade1a624446aa22cd0b6d012/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8112fb501b1e0567a1251a2fd0747baae60a4ab325a871e975b7bb67e59221f", size = 1266249, upload-time = "2024-08-19T20:09:38.545Z" }, - { url = "https://files.pythonhosted.org/packages/64/fd/ed136d46bc2c7e3342fed24662b4827771d55ceb5a7687847aae977bfc17/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e72589da4c90337837fdfe2026ae1952c0f4a6e793adbbfbdd40efed7c63599", size = 1311036, upload-time = "2024-08-19T20:09:40.501Z" }, - { url = "https://files.pythonhosted.org/packages/76/9a/43eeb0166f1119256d6f43468f900db1aed7fbe32069d2a71c82f987db4d/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4d46c7b4173415d8e583045fbc4daa48b40e31b19ce595b8d92cf639396c15d5", size = 1338756, upload-time = "2024-08-19T20:09:42.57Z" }, - { url = "https://files.pythonhosted.org/packages/d5/bc/d01ff0810b3f5e26896f76d44225ed78b088ddd33079b85cd1a23514318b/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33e6bc4bab477c772a541f76cd91e11ccb6d2efa2b8d7d7883591dfb523e5987", size = 1299976, upload-time = "2024-08-19T20:09:44.422Z" }, - { url = "https://files.pythonhosted.org/packages/3e/c9/50a297c4f7ab57a949f4add2d3eafe5f3e68bb42f739e933f8b32a092bda/aiohttp-3.10.5-cp313-cp313-win32.whl", hash = "sha256:c58c6837a2c2a7cf3133983e64173aec11f9c2cd8e87ec2fdc16ce727bcf1a04", size = 355609, upload-time = "2024-08-19T20:09:45.95Z" }, - { url = "https://files.pythonhosted.org/packages/65/28/aee9d04fb0b3b1f90622c338a08e54af5198e704a910e20947c473298fd0/aiohttp-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:38172a70005252b6893088c0f5e8a47d173df7cc2b2bd88650957eb84fcf5022", size = 375697, upload-time = "2024-08-19T20:09:47.63Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/34/939730e66b716b76046dedfe0842995842fa906ccc4964bba414ff69e429/aiohttp-3.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155", size = 736471, upload-time = "2025-10-28T20:55:27.924Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/dcbdf2df7f6ca72b0bb4c0b4509701f2d8942cf54e29ca197389c214c07f/aiohttp-3.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c", size = 493985, upload-time = "2025-10-28T20:55:29.456Z" }, + { url = "https://files.pythonhosted.org/packages/9d/87/71c8867e0a1d0882dcbc94af767784c3cb381c1c4db0943ab4aae4fed65e/aiohttp-3.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636", size = 489274, upload-time = "2025-10-28T20:55:31.134Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/46c24e8dae237295eaadd113edd56dee96ef6462adf19b88592d44891dc5/aiohttp-3.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da", size = 1668171, upload-time = "2025-10-28T20:55:36.065Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c6/4cdfb4440d0e28483681a48f69841fa5e39366347d66ef808cbdadddb20e/aiohttp-3.13.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725", size = 1636036, upload-time = "2025-10-28T20:55:37.576Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/8708cf678628216fb678ab327a4e1711c576d6673998f4f43e86e9ae90dd/aiohttp-3.13.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5", size = 1727975, upload-time = "2025-10-28T20:55:39.457Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2e/3ebfe12fdcb9b5f66e8a0a42dffcd7636844c8a018f261efb2419f68220b/aiohttp-3.13.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3", size = 1815823, upload-time = "2025-10-28T20:55:40.958Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/ca2ef819488cbb41844c6cf92ca6dd15b9441e6207c58e5ae0e0fc8d70ad/aiohttp-3.13.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802", size = 1669374, upload-time = "2025-10-28T20:55:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/1fe2e1179a0d91ce09c99069684aab619bf2ccde9b20bd6ca44f8837203e/aiohttp-3.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a", size = 1555315, upload-time = "2025-10-28T20:55:44.264Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2b/f3781899b81c45d7cbc7140cddb8a3481c195e7cbff8e36374759d2ab5a5/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204", size = 1639140, upload-time = "2025-10-28T20:55:46.626Z" }, + { url = "https://files.pythonhosted.org/packages/72/27/c37e85cd3ece6f6c772e549bd5a253d0c122557b25855fb274224811e4f2/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22", size = 1645496, upload-time = "2025-10-28T20:55:48.933Z" }, + { url = "https://files.pythonhosted.org/packages/66/20/3af1ab663151bd3780b123e907761cdb86ec2c4e44b2d9b195ebc91fbe37/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d", size = 1697625, upload-time = "2025-10-28T20:55:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/95/eb/ae5cab15efa365e13d56b31b0d085a62600298bf398a7986f8388f73b598/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f", size = 1542025, upload-time = "2025-10-28T20:55:51.861Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2d/1683e8d67ec72d911397fe4e575688d2a9b8f6a6e03c8fdc9f3fd3d4c03f/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f", size = 1714918, upload-time = "2025-10-28T20:55:53.515Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ffe8e0e1c57c5e542d47ffa1fcf95ef2b3ea573bf7c4d2ee877252431efc/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6", size = 1656113, upload-time = "2025-10-28T20:55:55.438Z" }, + { url = "https://files.pythonhosted.org/packages/0d/42/d511aff5c3a2b06c09d7d214f508a4ad8ac7799817f7c3d23e7336b5e896/aiohttp-3.13.2-cp310-cp310-win32.whl", hash = "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251", size = 432290, upload-time = "2025-10-28T20:55:56.96Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ea/1c2eb7098b5bad4532994f2b7a8228d27674035c9b3234fe02c37469ef14/aiohttp-3.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514", size = 455075, upload-time = "2025-10-28T20:55:58.373Z" }, + { url = "https://files.pythonhosted.org/packages/35/74/b321e7d7ca762638cdf8cdeceb39755d9c745aff7a64c8789be96ddf6e96/aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", size = 743409, upload-time = "2025-10-28T20:56:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/99/3d/91524b905ec473beaf35158d17f82ef5a38033e5809fe8742e3657cdbb97/aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", size = 497006, upload-time = "2025-10-28T20:56:01.85Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d3/7f68bc02a67716fe80f063e19adbd80a642e30682ce74071269e17d2dba1/aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", size = 493195, upload-time = "2025-10-28T20:56:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/913f774a4708775433b7375c4f867d58ba58ead833af96c8af3621a0d243/aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", size = 1747759, upload-time = "2025-10-28T20:56:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/04efe156f4326f31c7c4a97144f82132c3bb21859b7bb84748d452ccc17c/aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", size = 1704456, upload-time = "2025-10-28T20:56:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/4e16154d8e0a9cf4ae76f692941fd52543bbb148f02f098ca73cab9b1c1b/aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", size = 1807572, upload-time = "2025-10-28T20:56:08.558Z" }, + { url = "https://files.pythonhosted.org/packages/34/58/b0583defb38689e7f06798f0285b1ffb3a6fb371f38363ce5fd772112724/aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a", size = 1895954, upload-time = "2025-10-28T20:56:10.545Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/083907ee3437425b4e376aa58b2c915eb1a33703ec0dc30040f7ae3368c6/aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", size = 1747092, upload-time = "2025-10-28T20:56:12.118Z" }, + { url = "https://files.pythonhosted.org/packages/ac/61/98a47319b4e425cc134e05e5f3fc512bf9a04bf65aafd9fdcda5d57ec693/aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", size = 1606815, upload-time = "2025-10-28T20:56:14.191Z" }, + { url = "https://files.pythonhosted.org/packages/97/4b/e78b854d82f66bb974189135d31fce265dee0f5344f64dd0d345158a5973/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", size = 1723789, upload-time = "2025-10-28T20:56:16.101Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fc/9d2ccc794fc9b9acd1379d625c3a8c64a45508b5091c546dea273a41929e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", size = 1718104, upload-time = "2025-10-28T20:56:17.655Z" }, + { url = "https://files.pythonhosted.org/packages/66/65/34564b8765ea5c7d79d23c9113135d1dd3609173da13084830f1507d56cf/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", size = 1785584, upload-time = "2025-10-28T20:56:19.238Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/f6a7a426e02fc82781afd62016417b3948e2207426d90a0e478790d1c8a4/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", size = 1595126, upload-time = "2025-10-28T20:56:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c7/8e22d5d28f94f67d2af496f14a83b3c155d915d1fe53d94b66d425ec5b42/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", size = 1800665, upload-time = "2025-10-28T20:56:22.922Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/91133c8b68b1da9fc16555706aa7276fdf781ae2bb0876c838dd86b8116e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", size = 1739532, upload-time = "2025-10-28T20:56:25.924Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/3747644d26a998774b21a616016620293ddefa4d63af6286f389aedac844/aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", size = 431876, upload-time = "2025-10-28T20:56:27.524Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/688462108c1a00eb9f05765331c107f95ae86f6b197b865d29e930b7e462/aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", size = 456205, upload-time = "2025-10-28T20:56:29.062Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623, upload-time = "2025-10-28T20:56:30.797Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664, upload-time = "2025-10-28T20:56:32.708Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808, upload-time = "2025-10-28T20:56:34.57Z" }, + { url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863, upload-time = "2025-10-28T20:56:36.377Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586, upload-time = "2025-10-28T20:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625, upload-time = "2025-10-28T20:56:39.75Z" }, + { url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281, upload-time = "2025-10-28T20:56:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431, upload-time = "2025-10-28T20:56:43.162Z" }, + { url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846, upload-time = "2025-10-28T20:56:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606, upload-time = "2025-10-28T20:56:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663, upload-time = "2025-10-28T20:56:48.528Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939, upload-time = "2025-10-28T20:56:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132, upload-time = "2025-10-28T20:56:52.568Z" }, + { url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802, upload-time = "2025-10-28T20:56:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512, upload-time = "2025-10-28T20:56:56.428Z" }, + { url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690, upload-time = "2025-10-28T20:56:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465, upload-time = "2025-10-28T20:57:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" }, + { url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" }, + { url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387, upload-time = "2025-10-28T20:57:08.685Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314, upload-time = "2025-10-28T20:57:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317, upload-time = "2025-10-28T20:57:12.563Z" }, + { url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539, upload-time = "2025-10-28T20:57:14.623Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597, upload-time = "2025-10-28T20:57:16.399Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006, upload-time = "2025-10-28T20:57:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220, upload-time = "2025-10-28T20:57:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570, upload-time = "2025-10-28T20:57:22.253Z" }, + { url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407, upload-time = "2025-10-28T20:57:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093, upload-time = "2025-10-28T20:57:26.257Z" }, + { url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084, upload-time = "2025-10-28T20:57:28.349Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987, upload-time = "2025-10-28T20:57:30.233Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859, upload-time = "2025-10-28T20:57:32.105Z" }, + { url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192, upload-time = "2025-10-28T20:57:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234, upload-time = "2025-10-28T20:57:36.415Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733, upload-time = "2025-10-28T20:57:38.205Z" }, + { url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303, upload-time = "2025-10-28T20:57:40.122Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965, upload-time = "2025-10-28T20:57:42.28Z" }, + { url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221, upload-time = "2025-10-28T20:57:44.869Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178, upload-time = "2025-10-28T20:57:47.216Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001, upload-time = "2025-10-28T20:57:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325, upload-time = "2025-10-28T20:57:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978, upload-time = "2025-10-28T20:57:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042, upload-time = "2025-10-28T20:57:55.617Z" }, + { url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085, upload-time = "2025-10-28T20:57:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238, upload-time = "2025-10-28T20:57:59.525Z" }, + { url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395, upload-time = "2025-10-28T20:58:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965, upload-time = "2025-10-28T20:58:03.972Z" }, + { url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585, upload-time = "2025-10-28T20:58:06.189Z" }, + { url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621, upload-time = "2025-10-28T20:58:08.636Z" }, + { url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627, upload-time = "2025-10-28T20:58:11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360, upload-time = "2025-10-28T20:58:13.358Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616, upload-time = "2025-10-28T20:58:15.339Z" }, + { url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131, upload-time = "2025-10-28T20:58:17.693Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168, upload-time = "2025-10-28T20:58:20.113Z" }, + { url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200, upload-time = "2025-10-28T20:58:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497, upload-time = "2025-10-28T20:58:24.672Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703, upload-time = "2025-10-28T20:58:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738, upload-time = "2025-10-28T20:58:29.787Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061, upload-time = "2025-10-28T20:58:32.529Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201, upload-time = "2025-10-28T20:58:34.618Z" }, + { url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868, upload-time = "2025-10-28T20:58:38.835Z" }, + { url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660, upload-time = "2025-10-28T20:58:41.507Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548, upload-time = "2025-10-28T20:58:43.674Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240, upload-time = "2025-10-28T20:58:45.787Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334, upload-time = "2025-10-28T20:58:47.936Z" }, + { url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685, upload-time = "2025-10-28T20:58:50.642Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093, upload-time = "2025-10-28T20:58:52.782Z" }, ] [[package]] @@ -170,6 +217,92 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] +[[package]] +name = "bitarray" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/06/92fdc84448d324ab8434b78e65caf4fb4c6c90b4f8ad9bdd4c8021bfaf1e/bitarray-3.8.0.tar.gz", hash = "sha256:3eae38daffd77c9621ae80c16932eea3fb3a4af141fb7cc724d4ad93eff9210d", size = 151991, upload-time = "2025-11-02T21:41:15.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/b9/8a645fd36fc4c01ee223f97eccd4699c2f2e91681ccb33c0e963881c8e58/bitarray-3.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f08342dc8d19214faa7ef99574dea6c37a2790d6d04a9793ef8fa76c188dc08d", size = 148504, upload-time = "2025-11-02T21:38:54.596Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f4/11b562e13ff732bd0674376f367f0a272034ebc28b8efbafbeb924552d21/bitarray-3.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:792462abfeeca6cc8c6c1e6d27e14319682f0182f6b0ba37befe911af794db70", size = 145481, upload-time = "2025-11-02T21:38:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7c/5a2487da579491b38abab3b437e01d3b05be6e16e69cc5eb304040dcebd5/bitarray-3.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0df69d26f21a9d2f1b20266f6737fa43f08aa5015c99900fb69f255fbe4dabb4", size = 322760, upload-time = "2025-11-02T21:38:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/8d/59/f0ef82d6a878d4af1b4961d208a716317929aa172fc0dfa5f4115319a873/bitarray-3.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b4f10d3f304be7183fac79bf2cd997f82e16aa9a9f37343d76c026c6e435a8a8", size = 350332, upload-time = "2025-11-02T21:38:58.238Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ec/d444b22fce853327d4a8adec1de9987e11b28fcc2d7204dcbc544e196ed9/bitarray-3.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fc98ff43abad61f00515ad9a06213b7716699146e46eabd256cdfe7cb522bd97", size = 360787, upload-time = "2025-11-02T21:38:59.239Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9e/60b205f52ea9ff155e9f12249090475159c909039daa29e47cd95e115dd5/bitarray-3.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81c6b4a6c1af800d52a6fa32389ef8f4281583f4f99dc1a40f2bb47667281541", size = 329050, upload-time = "2025-11-02T21:39:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/2ce373b423bc85a0eb93ee1cba3977971259a92a116932632f417b1b04d2/bitarray-3.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f3fd8df63c41ff6a676d031956aebf68ebbc687b47c507da25501eb22eec341f", size = 320507, upload-time = "2025-11-02T21:39:01.714Z" }, + { url = "https://files.pythonhosted.org/packages/2a/88/437408a2674b8bdb02063dd1535969b9c73cb8fdd197485de431e506c50e/bitarray-3.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0ce9d9e07c75da8027c62b4c9f45771d1d8aae7dc9ad7fb606c6a5aedbe9741", size = 348449, upload-time = "2025-11-02T21:39:03.124Z" }, + { url = "https://files.pythonhosted.org/packages/97/46/d799e7e731c778b6dcb4627bafd395102065e5ab15a4a31f4222a3e20706/bitarray-3.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8a9c962c64a4c08def58b9799333e33af94ec53038cf151d36edacdb41f81646", size = 344776, upload-time = "2025-11-02T21:39:04.147Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9a/129fff56d22d316b1c848c6e13e64191485756b5cd6ceb08e640edb80020/bitarray-3.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1a54d7e7999735faacdcbe8128e30207abc2caf9f9fd7102d180b32f1b78bfce", size = 325899, upload-time = "2025-11-02T21:39:05.118Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/4b01e99452ecc39f4abccf9bf83fe0f01c390e9794dad2d04b2c8b893c5f/bitarray-3.8.0-cp310-cp310-win32.whl", hash = "sha256:3ea52df96566457735314794422274bd1962066bfb609e7eea9113d70cf04ffe", size = 142756, upload-time = "2025-11-02T21:39:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/18/3f/c83635a67d90f45f88012468566c233eed1e9e9a9184fa882ba4039fadb3/bitarray-3.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:82a07de83dce09b4fa1bccbdc8bde8f188b131666af0dc9048ba0a0e448d8a3b", size = 149527, upload-time = "2025-11-02T21:39:07.377Z" }, + { url = "https://files.pythonhosted.org/packages/33/46/391b3902a523d4555313640746460b19d317c6233d9379e150af97fa1554/bitarray-3.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5ba07e58fd98c9782201e79eb8dd4225733d212a5a3700f9a84d329bd0463a6", size = 146453, upload-time = "2025-11-02T21:39:08.624Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7d/63558f1d0eb09217a3d30c1c847890879973e224a728fcff9391fab999b8/bitarray-3.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:25b9cff6c9856bc396232e2f609ea0c5ec1a8a24c500cee4cca96ba8a3cd50b6", size = 148502, upload-time = "2025-11-02T21:39:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7b/f957ad211cb0172965b5f0881b67b99e2b6d41512af0a1001f44a44ddf4a/bitarray-3.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d9984017314da772f5f7460add7a0301a4ffc06c72c2998bb16c300a6253607", size = 145484, upload-time = "2025-11-02T21:39:10.904Z" }, + { url = "https://files.pythonhosted.org/packages/9f/dc/897973734f14f91467a3a795a4624752238053ecffaec7c8bbda1e363fda/bitarray-3.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbbbfbb7d039b20d289ce56b1beb46138d65769d04af50c199c6ac4cb6054d52", size = 330909, upload-time = "2025-11-02T21:39:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/67/be/24b4b792426d92de289e73e09682915d567c2e69d47e8857586cbdc865d0/bitarray-3.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1f723e260c35e1c7c57a09d3a6ebe681bd56c83e1208ae3ce1869b7c0d10d4f", size = 358469, upload-time = "2025-11-02T21:39:13.766Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0e/2eda69a7a59a6998df8fb57cc9d1e0e62888c599fb5237b0a8b479a01afb/bitarray-3.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cbd1660fb48827381ce3a621a4fdc237959e1cd4e98b098952a8f624a0726425", size = 369131, upload-time = "2025-11-02T21:39:15.041Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7b/8a372d6635a6b2622477b2f96a569b2cd0318a62bc95a4a2144c7942c987/bitarray-3.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df6d7bf3e15b7e6e202a16ff4948a51759354016026deb04ab9b5acbbe35e096", size = 337089, upload-time = "2025-11-02T21:39:16.124Z" }, + { url = "https://files.pythonhosted.org/packages/93/f0/8eca934dbe5dee47a0e5ef44eeb72e85acacc8097c27cd164337bc4ec5d3/bitarray-3.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d5c931ec1c03111718cabf85f6012bb2815fa0ce578175567fa8d6f2cc15d3b4", size = 328504, upload-time = "2025-11-02T21:39:17.321Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/928b8e23a9950f8a8bfc42bc1e7de41f4e27f57de01a716308be5f683c2b/bitarray-3.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:41b53711f89008ba2de62e4c2d2260a8b357072fd4f18e1351b28955db2719dc", size = 356461, upload-time = "2025-11-02T21:39:18.396Z" }, + { url = "https://files.pythonhosted.org/packages/a9/93/4fb58417aff47fa2fe1874a39c9346b589a1d78c93a9cb24cccede5dc737/bitarray-3.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4f298daaaea58d45e245a132d6d2bdfb6f856da50dc03d75ebb761439fb626cf", size = 353008, upload-time = "2025-11-02T21:39:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/da/54/aa04e4a7b45aa5913f08ee377d43319b0979925e3c0407882eb29df3be66/bitarray-3.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:30989a2451b693c3f9359d91098a744992b5431a0be4858f1fdf0ec76b457125", size = 334048, upload-time = "2025-11-02T21:39:20.924Z" }, + { url = "https://files.pythonhosted.org/packages/da/52/e851f41076df014c05d6ac1ce34fbf7db5fa31241da3e2f09bb2be9e283d/bitarray-3.8.0-cp311-cp311-win32.whl", hash = "sha256:e5aed4754895942ae15ffa48c52d181e1c1463236fda68d2dba29c03aa61786b", size = 142907, upload-time = "2025-11-02T21:39:22.312Z" }, + { url = "https://files.pythonhosted.org/packages/28/01/db0006148b1dd13b4ac2686df8fa57d12f5887df313a506e939af0cb0997/bitarray-3.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:22c540ed20167d3dbb1e2d868ca935180247d620c40eace90efa774504a40e3b", size = 149670, upload-time = "2025-11-02T21:39:23.341Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ea/b7d55ee269b1426f758a535c9ec2a07c056f20f403fa981685c3c8b4798c/bitarray-3.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:84b52b2cf77bb7f703d16c4007b021078dbbe6cf8ffb57abe81a7bacfc175ef2", size = 146709, upload-time = "2025-11-02T21:39:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/0c41d893eda756315491adfdbf9bc928aee3d377a7f97a8834d453aa5de1/bitarray-3.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2fcbe9b3a5996b417e030aa33a562e7e20dfc86271e53d7e841fc5df16268b8", size = 148575, upload-time = "2025-11-02T21:39:25.718Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/12ab2f4a4429bd844b419c37877caba93d676d18be71354fbbeb21d9f4cc/bitarray-3.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd761d158f67e288fd0ebe00c3b158095ce80a4bc7c32b60c7121224003ba70d", size = 145454, upload-time = "2025-11-02T21:39:26.695Z" }, + { url = "https://files.pythonhosted.org/packages/26/58/314b3e3f219533464e120f0c51ac5123e7b1c1b91f725a4073fb70c5a858/bitarray-3.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c394a3f055b49f92626f83c1a0b6d6cd2c628f1ccd72481c3e3c6aa4695f3b20", size = 332949, upload-time = "2025-11-02T21:39:27.801Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ce/ca8c706bd8341c7a22dd92d2a528af71f7e5f4726085d93f81fd768cb03b/bitarray-3.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:969fd67de8c42affdb47b38b80f1eaa79ac0ef17d65407cdd931db1675315af1", size = 360599, upload-time = "2025-11-02T21:39:28.964Z" }, + { url = "https://files.pythonhosted.org/packages/ef/dc/aa181df85f933052d962804906b282acb433cb9318b08ec2aceb4ee34faf/bitarray-3.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:99d25aff3745c54e61ab340b98400c52ebec04290a62078155e0d7eb30380220", size = 371972, upload-time = "2025-11-02T21:39:30.228Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d9/b805bfa158c7bcf4df0ac19b1be581b47e1ddb792c11023aed80a7058e78/bitarray-3.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e645b4c365d6f1f9e0799380ad6395268f3c3b898244a650aaeb8d9d27b74c35", size = 340303, upload-time = "2025-11-02T21:39:31.342Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/5308cc97ea929e30727292617a3a88293470166851e13c9e3f16f395da55/bitarray-3.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2fa23fdb3beab313950bbb49674e8a161e61449332d3997089fe3944953f1b77", size = 330494, upload-time = "2025-11-02T21:39:32.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/89/64f1596cb80433323efdbc8dcd0d6e57c40dfbe6ea3341623f34ec397edd/bitarray-3.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:165052a0e61c880f7093808a0c524ce1b3555bfa114c0dfb5c809cd07918a60d", size = 358123, upload-time = "2025-11-02T21:39:34.331Z" }, + { url = "https://files.pythonhosted.org/packages/27/fd/f3d49c5443b57087f888b5e118c8dd78bb7c8e8cfeeed250f8e92128a05f/bitarray-3.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:337c8cd46a4c6568d367ed676cbf2d7de16f890bb31dbb54c44c1d6bb6d4a1de", size = 356046, upload-time = "2025-11-02T21:39:35.449Z" }, + { url = "https://files.pythonhosted.org/packages/aa/db/1fd0b402bd2b47142e958b6930dbb9445235d03fa703c9a24caa6e576ae2/bitarray-3.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21ca6a47bf20db9e7ad74ca04b3d479e4d76109b68333eb23535553d2705339e", size = 336872, upload-time = "2025-11-02T21:39:36.891Z" }, + { url = "https://files.pythonhosted.org/packages/58/73/680b47718f1313b4538af479c4732eaca0aeda34d93fc5b869f87932d57d/bitarray-3.8.0-cp312-cp312-win32.whl", hash = "sha256:178c5a4c7fdfb5cd79e372ae7f675390e670f3732e5bc68d327e01a5b3ff8d55", size = 143025, upload-time = "2025-11-02T21:39:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/f8/11/7792587c19c79a8283e8838f44709fa4338a8f7d2a3091dfd81c07ae89c7/bitarray-3.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:75a3b6e9c695a6570ea488db75b84bb592ff70a944957efa1c655867c575018b", size = 149969, upload-time = "2025-11-02T21:39:39.715Z" }, + { url = "https://files.pythonhosted.org/packages/9a/00/9df64b5d8a84e8e9ec392f6f9ce93f50626a5b301cb6c6b3fe3406454d66/bitarray-3.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:5591daf81313096909d973fb2612fccd87528fdfdd39f6478bdce54543178954", size = 146907, upload-time = "2025-11-02T21:39:40.815Z" }, + { url = "https://files.pythonhosted.org/packages/3e/35/480364d4baf1e34c79076750914664373f561c58abb5c31c35b3fae613ff/bitarray-3.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18214bac86341f1cc413772e66447d6cca10981e2880b70ecaf4e826c04f95e9", size = 148582, upload-time = "2025-11-02T21:39:42.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a8/718b95524c803937f4edbaaf6480f39c80f6ed189d61357b345e8361ffb6/bitarray-3.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:01c5f0dc080b0ebb432f7a68ee1e88a76bd34f6d89c9568fcec65fb16ed71f0e", size = 145433, upload-time = "2025-11-02T21:39:43.552Z" }, + { url = "https://files.pythonhosted.org/packages/03/66/4a10f30dc9e2e01e3b4ecd44a511219f98e63c86b0e0f704c90fac24059b/bitarray-3.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86685fa04067f7175f9718489ae755f6acde03593a1a9ca89305554af40e14fd", size = 332986, upload-time = "2025-11-02T21:39:44.656Z" }, + { url = "https://files.pythonhosted.org/packages/53/25/4c08774d847f80a1166e4c704b4e0f1c417c0afe6306eae0bc5e70d35faa/bitarray-3.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56896ceeffe25946c4010320629e2d858ca763cd8ded273c81672a5edbcb1e0a", size = 360634, upload-time = "2025-11-02T21:39:45.798Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/bf8ad26169ebd0b2746d5c7564db734453ca467f8aab87e9d43b0a794383/bitarray-3.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9858dcbc23ba7eaadcd319786b982278a1a2b2020720b19db43e309579ff76fb", size = 371992, upload-time = "2025-11-02T21:39:46.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/16/ce166754e7c9d10650e02914552fa637cf3b2591f7ed16632bbf6b783312/bitarray-3.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa7dec53c25f1949513457ef8b0ea1fb40e76c672cc4d2daa8ad3c8d6b73491a", size = 340315, upload-time = "2025-11-02T21:39:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/de/2a/fbba3a106ddd260e84b9a624f730257c32ba51a8a029565248dfedfdf6f2/bitarray-3.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15a2eff91f54d2b1f573cca8ca6fb58763ce8fea80e7899ab028f3987ef71cd5", size = 330473, upload-time = "2025-11-02T21:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/68/97/56cf3c70196e7307ad32318a9d6ed969dbdc6a4534bbe429112fa7dfe42e/bitarray-3.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b1572ee0eb1967e71787af636bb7d1eb9c6735d5337762c450650e7f51844594", size = 358129, upload-time = "2025-11-02T21:39:51.189Z" }, + { url = "https://files.pythonhosted.org/packages/fd/be/afd391a5c0896d3339613321b2f94af853f29afc8bd3fbc327431244c642/bitarray-3.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5bfac7f236ba1a4d402644bdce47fb9db02a7cf3214a1f637d3a88390f9e5428", size = 356005, upload-time = "2025-11-02T21:39:52.355Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a8e1a371babba29bad3378bb3a2cdca2b012170711e7fe1f22031a6b7b95/bitarray-3.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f0a55cf02d2cdd739b40ce10c09bbdd520e141217696add7a48b56e67bdfdfe6", size = 336862, upload-time = "2025-11-02T21:39:54.345Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/6dc1d0fdc06991c8dc3b1fcfe1ae49fbaced42064cd1b5f24278e73fe05f/bitarray-3.8.0-cp313-cp313-win32.whl", hash = "sha256:a2ba92f59e30ce915e9e79af37649432e3a212ddddf416d4d686b1b4825bcdb2", size = 143018, upload-time = "2025-11-02T21:39:56.361Z" }, + { url = "https://files.pythonhosted.org/packages/2e/72/76e13f5cd23b8b9071747909663ce3b02da24a5e7e22c35146338625db35/bitarray-3.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f2a5d8006db5a555e06f9437e76bf52537d3dfd130cb8ae2b30866aca32c9", size = 149977, upload-time = "2025-11-02T21:39:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/01/37/60f336c32336cc3ec03b0c61076f16ea2f05d5371c8a56e802161d218b77/bitarray-3.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:50ddbe3a7b4b6ab96812f5a4d570f401a2cdb95642fd04c062f98939610bbeee", size = 146930, upload-time = "2025-11-02T21:39:59.308Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b0/411327a6c7f6b2bead64bb06fe60b92e0344957ec1ab0645d5ccc25fdafe/bitarray-3.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8cbd4bfc933b33b85c43ef4c1f4d5e3e9d91975ea6368acf5fbac02bac06ea89", size = 148563, upload-time = "2025-11-02T21:40:01.006Z" }, + { url = "https://files.pythonhosted.org/packages/2a/bc/ff80d97c627d774f879da0ea93223adb1267feab7e07d5c17580ffe6d632/bitarray-3.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9d35d8f8a1c9ed4e2b08187b513f8a3c71958600129db3aa26d85ea3abfd1310", size = 145422, upload-time = "2025-11-02T21:40:02.535Z" }, + { url = "https://files.pythonhosted.org/packages/66/e7/b4cb6c5689aacd0a32f3aa8a507155eaa33528c63de2f182b60843fbf700/bitarray-3.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f55e14e7c56f4fafe1343480c32b110ef03836c21ff7c48bae7add6818f77c", size = 332852, upload-time = "2025-11-02T21:40:03.645Z" }, + { url = "https://files.pythonhosted.org/packages/e7/91/fbd1b047e3e2f4b65590f289c8151df1d203d75b005f5aae4e072fe77d76/bitarray-3.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dfbe2aa45b273f49e715c5345d94874cb65a28482bf231af408891c260601b8d", size = 360801, upload-time = "2025-11-02T21:40:04.827Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4a/63064c593627bac8754fdafcb5343999c93ab2aeb27bcd9d270a010abea5/bitarray-3.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64af877116edf051375b45f0bda648143176a017b13803ec7b3a3111dc05f4c5", size = 371408, upload-time = "2025-11-02T21:40:05.985Z" }, + { url = "https://files.pythonhosted.org/packages/46/97/ddc07723767bdafd170f2ff6e173c940fa874192783ee464aa3c1dedf07d/bitarray-3.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cdfbb27f2c46bb5bbdcee147530cbc5ca8ab858d7693924e88e30ada21b2c5e2", size = 340033, upload-time = "2025-11-02T21:40:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1e/e1ea9f1146fd4af032817069ff118918d73e5de519854ce3860e2ed560ff/bitarray-3.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4d73d4948dcc5591d880db8933004e01f1dd2296df9de815354d53469beb26fe", size = 330774, upload-time = "2025-11-02T21:40:08.496Z" }, + { url = "https://files.pythonhosted.org/packages/cf/9f/8242296c124a48d1eab471fd0838aeb7ea9c6fd720302d99ab7855d3e6d3/bitarray-3.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:28a85b056c0eb7f5d864c0ceef07034117e8ebfca756f50648c71950a568ba11", size = 358337, upload-time = "2025-11-02T21:40:10.035Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6b/9095d75264c67d479f298c80802422464ce18c3cdd893252eeccf4997611/bitarray-3.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:79ec4498a545733ecace48d780d22407411b07403a2e08b9a4d7596c0b97ebd7", size = 355639, upload-time = "2025-11-02T21:40:11.485Z" }, + { url = "https://files.pythonhosted.org/packages/a0/af/c93c0ae5ef824136e90ac7ddf6cceccb1232f34240b2f55a922f874da9b4/bitarray-3.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:33af25c4ff7723363cb8404dfc2eefeab4110b654f6c98d26aba8a08c745d860", size = 336999, upload-time = "2025-11-02T21:40:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/81/0f/72c951f5997b2876355d5e671f78dd2362493254876675cf22dbd24389ae/bitarray-3.8.0-cp314-cp314-win32.whl", hash = "sha256:2c3bb96b6026643ce24677650889b09073f60b9860a71765f843c99f9ab38b25", size = 142169, upload-time = "2025-11-02T21:40:14.031Z" }, + { url = "https://files.pythonhosted.org/packages/8a/55/ef1b4de8107bf13823da8756c20e1fbc9452228b4e837f46f6d9ddba3eb3/bitarray-3.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:847c7f61964225fc489fe1d49eda7e0e0d253e98862c012cecf845f9ad45cdf4", size = 148737, upload-time = "2025-11-02T21:40:15.436Z" }, + { url = "https://files.pythonhosted.org/packages/5f/26/bc0784136775024ac56cc67c0d6f9aa77a7770de7f82c3a7c9be11c217cd/bitarray-3.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:a2cb35a6efaa0e3623d8272471371a12c7e07b51a33e5efce9b58f655d864b4e", size = 146083, upload-time = "2025-11-02T21:40:17.135Z" }, + { url = "https://files.pythonhosted.org/packages/6e/64/57984e64264bf43d93a1809e645972771566a2d0345f4896b041ce20b000/bitarray-3.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:15e8d0597cc6e8496de6f4dea2a6880c57e1251502a7072f5631108a1aa28521", size = 149455, upload-time = "2025-11-02T21:40:18.558Z" }, + { url = "https://files.pythonhosted.org/packages/81/c0/0d5f2eaef1867f462f764bdb07d1e116c33a1bf052ea21889aefe4282f5b/bitarray-3.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8ffe660e963ae711cb9e2b8d8461c9b1ad6167823837fc17d59d5e539fb898fa", size = 146491, upload-time = "2025-11-02T21:40:19.665Z" }, + { url = "https://files.pythonhosted.org/packages/65/c6/bc1261f7a8862c0c59220a484464739e52235fd1e2afcb24d7f7d3fb5702/bitarray-3.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4779f356083c62e29b4198d290b7b17a39a69702d150678b7efff0fdddf494a8", size = 339721, upload-time = "2025-11-02T21:40:21.277Z" }, + { url = "https://files.pythonhosted.org/packages/81/d8/289ca55dd2939ea17b1108dc53bffc0fdc5160ba44f77502dfaae35d08c6/bitarray-3.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:025d133bf4ca8cf75f904eeb8ea946228d7c043231866143f31946a6f4dd0bf3", size = 367823, upload-time = "2025-11-02T21:40:22.463Z" }, + { url = "https://files.pythonhosted.org/packages/91/a2/61e7461ca9ac0fcb70f327a2e84b006996d2a840898e69037a39c87c6d06/bitarray-3.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:451f9958850ea98440d542278368c8d1e1ea821e2494b204570ba34a340759df", size = 377341, upload-time = "2025-11-02T21:40:23.789Z" }, + { url = "https://files.pythonhosted.org/packages/6c/87/4a0c9c8bdb13916d443e04d8f8542eef9190f31425da3c17c3478c40173f/bitarray-3.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d79f659965290af60d6acc8e2716341865fe74609a7ede2a33c2f86ad893b8f", size = 344985, upload-time = "2025-11-02T21:40:25.261Z" }, + { url = "https://files.pythonhosted.org/packages/17/4c/ff9259b916efe53695b631772e5213699c738efc2471b5ffe273f4000994/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fbf05678c2ae0064fb1b8de7e9e8f0fc30621b73c8477786dd0fb3868044a8c8", size = 336796, upload-time = "2025-11-02T21:40:26.942Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4b/51b2468bbddbade5e2f3b8d5db08282c5b309e8687b0f02f75a8b5ff559c/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:c396358023b876cff547ce87f4e8ff8a2280598873a137e8cc69e115262260b8", size = 365085, upload-time = "2025-11-02T21:40:28.224Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/53473bfc2e052c6dbb628cdc1b156be621c77aaeb715918358b01574be55/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed3493a369fe849cce98542d7405c88030b355e4d2e113887cb7ecc86c205773", size = 361012, upload-time = "2025-11-02T21:40:29.635Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/242bf2e44bfc69e73fa2b954b425d761a8e632f78ea31008f1c3cfad0854/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c764fb167411d5afaef88138542a4bfa28bd5e5ded5e8e42df87cef965efd6e9", size = 340644, upload-time = "2025-11-02T21:40:31.089Z" }, + { url = "https://files.pythonhosted.org/packages/cf/01/12e5ecf30a5de28a32485f226cad4b8a546845f65f755ce0365057ab1e92/bitarray-3.8.0-cp314-cp314t-win32.whl", hash = "sha256:e12769d3adcc419e65860de946df8d2ed274932177ac1cdb05186e498aaa9149", size = 143630, upload-time = "2025-11-02T21:40:32.351Z" }, + { url = "https://files.pythonhosted.org/packages/b6/92/6b6ade587b08024a8a890b07724775d29da9cf7497be5c3cbe226185e463/bitarray-3.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0ca70ccf789446a6dfde40b482ec21d28067172cd1f8efd50d5548159fccad9e", size = 150250, upload-time = "2025-11-02T21:40:33.596Z" }, + { url = "https://files.pythonhosted.org/packages/ed/40/be3858ffed004e47e48a2cefecdbf9b950d41098b780f9dc3aa609a88351/bitarray-3.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2a3d1b05ffdd3e95687942ae7b13c63689f85d3f15c39b33329e3cb9ce6c015f", size = 147015, upload-time = "2025-11-02T21:40:35.064Z" }, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -496,6 +629,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, ] +[[package]] +name = "genai-prices" +version = "0.0.62" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx2" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/8e/ed322d1f22b57fd455749bdbe2f285d310e1c1ebe921cb3d5c0b920de648/genai_prices-0.0.62.tar.gz", hash = "sha256:baf1ffa64be0d15577878216464d6a2d04244db5fbdf78d56bde43809e7aef44", size = 67611, upload-time = "2026-05-25T18:47:16.306Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/35/ce64112dcc6f406b3e290dcf57a97acfa2b7d3d0391979219cb9d4a9db6d/genai_prices-0.0.62-py3-none-any.whl", hash = "sha256:5d9ab0d9e5d81e035f88bf591fb6a8dde527922786acf1ee2737358f7bbe0167", size = 70333, upload-time = "2026-05-25T18:47:17.642Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.65.0" @@ -559,6 +705,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" }, ] +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, +] + [[package]] name = "grpc-interceptor" version = "0.15.4" @@ -695,6 +850,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -708,6 +885,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/34/18f1c596e677962f040284246f393b10a1f8ce440b3a7e69c637d0f1c7ad/httpcore2-2.3.0.tar.gz", hash = "sha256:07327e251560960eea8e969d92d4c6a325feb13cca39e25340731336c3baf924", size = 64300, upload-time = "2026-06-01T13:15:02.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/dd/3357218c69360d1cecc196c230c9a1d5c9afd5dba362056e23e60a5e64e5/httpcore2-2.3.0-py3-none-any.whl", hash = "sha256:477e9e334f74e5240dcac002e890580f36a57d40ff0fb14cc9655731d23b8415", size = 80024, upload-time = "2026-06-01T13:15:00.001Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -723,6 +913,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "httpx2" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpcore2" }, + { name = "idna" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/9a/cca0b9145f13d8ae34b885ae28d403a1469a433abc78e0f94f4ce94e650b/httpx2-2.3.0.tar.gz", hash = "sha256:227e7c41d95a76d4077a52640564132777215fc3394e07b66a3116c33d668fa9", size = 81115, upload-time = "2026-06-01T13:15:04.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/ce/ae2911859847f9ba1d6b23027e53481cbeb50b93234f355a968d300ca2cb/httpx2-2.3.0-py3-none-any.whl", hash = "sha256:6f393663bdf6dbe7fe90118e3eb5b2bd024a675cae0390ac08cec9198812d8b7", size = 74538, upload-time = "2026-06-01T13:15:01.566Z" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -861,6 +1084,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, ] +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + [[package]] name = "kcdc-2025" version = "0.1.0" @@ -875,7 +1126,7 @@ dependencies = [ requires-dist = [ { name = "langchain", specifier = ">=0.3.27" }, { name = "langchain-anthropic", specifier = ">=0.3.18" }, - { name = "reboot", specifier = "==0.33.0" }, + { name = "reboot", specifier = "==1.1.0" }, ] [[package]] @@ -976,6 +1227,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/65/e9/1fcade4e6ed39c2679b6cc6c0456f81ec91f0c0ce36007cedb3d86e2fe20/langsmith-0.4.11-py3-none-any.whl", hash = "sha256:ce3a52809d37854bcb129affd8cd883025985891b82a46f2ae597c511283360d", size = 372354, upload-time = "2025-08-05T00:12:39.523Z" }, ] +[[package]] +name = "logfire-api" +version = "4.35.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/25/016b7d5e0433ae28d8a8bcb18681c48da2a0cdbf0ca8f7b2acdac2f16f4a/logfire_api-4.35.0.tar.gz", hash = "sha256:dcc073c7e337b0005f63075cf89951bacf00944b7c7420c2422b18133c8d2605", size = 83091, upload-time = "2026-06-02T14:55:58.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/3a/861f2040b251aa12b653b104c314b7d140cb44a6ab19cb141535aa72beb9/logfire_api-4.35.0-py3-none-any.whl", hash = "sha256:c8eb8f49c261c09b3d815b22ecba1c5224e8ba9aa9b546b0afcdb13a89fa6bfe", size = 131026, upload-time = "2026-06-02T14:55:55.252Z" }, +] + [[package]] name = "markupsafe" version = "3.0.2" @@ -1034,6 +1294,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] +[[package]] +name = "mcp" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, +] + [[package]] name = "multidict" version = "6.6.3" @@ -1138,15 +1423,15 @@ wheels = [ [[package]] name = "mypy-protobuf" -version = "3.5.0" +version = "3.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, { name = "types-protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/10/585b0a5494c8bc6e4fe33a92eadc4f750ab192ecbdfd1d9f7fc4545c2985/mypy-protobuf-3.5.0.tar.gz", hash = "sha256:21f270da0a9792a9dac76b0df463c027e561664ab6973c59be4e4d064dfe67dc", size = 24418, upload-time = "2023-07-29T00:23:23.567Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/282d64d66bf48ce60e38a6560753f784e0f88ab245ac2fb5e93f701a36cd/mypy-protobuf-3.6.0.tar.gz", hash = "sha256:02f242eb3409f66889f2b1a3aa58356ec4d909cdd0f93115622e9e70366eca3c", size = 24445, upload-time = "2024-04-01T20:24:42.837Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/b8/10a141d38da112507c633eee498aa9939df34dfb417c93f0a246c4931eb7/mypy_protobuf-3.5.0-py3-none-any.whl", hash = "sha256:0d0548c6b9a6faf14ce1a9ce2831c403a5c1f2a9363e85b1e2c51d5d57aa8393", size = 16435, upload-time = "2023-07-29T00:23:21.753Z" }, + { url = "https://files.pythonhosted.org/packages/e8/73/d6b999782ae22f16971cc05378b3b33f6a89ede3b9619e8366aa23484bca/mypy_protobuf-3.6.0-py3-none-any.whl", hash = "sha256:56176e4d569070e7350ea620262478b49b7efceba4103d468448f1d21492fd6c", size = 16434, upload-time = "2024-04-01T20:24:40.583Z" }, ] [[package]] @@ -1485,7 +1770,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.7" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1493,105 +1778,187 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "1.87.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "genai-prices" }, + { name = "griffelib" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/f9/76c7943f208b09b320dc65a000689929df6a5d3b143d56b48deade6db486/pydantic_ai_slim-1.87.0.tar.gz", hash = "sha256:25822985ca21d6f2995310da915080fc3f75763aec82e815a3388257b06d6b84", size = 573802, upload-time = "2026-04-25T01:09:21.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, + { url = "https://files.pythonhosted.org/packages/d1/90/810dbc478bf063677cf56babc4404f2f0c59794017a5022ac93345a26d75/pydantic_ai_slim-1.87.0-py3-none-any.whl", hash = "sha256:6a9b4f9bcac3709ef47f3b3cda70446c002eb55901038a50d6224ee6743fe31a", size = 732159, upload-time = "2026-04-25T01:09:13.025Z" }, ] [[package]] name = "pydantic-core" -version = "2.33.2" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, - { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, - { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, - { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, - { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, - { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, - { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, - { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, - { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, - { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, - { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, - { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, - { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, - { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, - { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, - { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, - { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, - { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, - { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, - { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, - { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, - { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, - { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, - { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, - { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, - { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, - { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "1.87.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/fa/b2306c6dbb06e4dfe6ce6b7c5a28b82bee536d965e1dd1800b49c386b389/pydantic_graph-1.87.0.tar.gz", hash = "sha256:0f44848f8e83908ce372491c32ef349dfaf05e29f39fade0bae9309ab4f015cd", size = 59251, upload-time = "2026-04-25T01:09:23.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/9a/e99edfa37527ee04c14db7fc4b8da3f8e9c913f91c541a4b2d08438b461e/pydantic_graph-1.87.0-py3-none-any.whl", hash = "sha256:fd39e4e852808e36163474fe2af48e88a046b5e5e00596730f33c17d2429b7d2", size = 73063, upload-time = "2026-04-25T01:09:16.493Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, ] [[package]] name = "pyjwt" -version = "2.8.0" +version = "2.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/72/8259b2bccfe4673330cea843ab23f86858a419d8f1493f66d413a76c7e3b/PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", size = 78313, upload-time = "2023-07-18T20:02:22.594Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/4f/e04a8067c7c96c364cef7ef73906504e2f40d690811c021e1a1901473a19/PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320", size = 22591, upload-time = "2023-07-18T20:02:21.561Z" }, + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, ] [[package]] @@ -1615,6 +1982,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + +[[package]] +name = "python-ulid" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/7e/0d6c82b5ccc71e7c833aed43d9e8468e1f2ff0be1b3f657a6fcafbb8433d/python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636", size = 93175, upload-time = "2025-08-18T16:09:26.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/a0/4ed6632b70a52de845df056654162acdebaf97c20e3212c559ac43e7216e/python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619", size = 11577, upload-time = "2025-08-18T16:09:25.047Z" }, +] + +[[package]] +name = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/1b/9cfdeac80ee45bebbbcb31f1b7b99a0d81a1c72de48d837be984e0e88b1d/pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e", size = 6361387, upload-time = "2026-06-04T07:49:14.329Z" }, + { url = "https://files.pythonhosted.org/packages/33/b1/7afc96d041d982c27bc2df6f853d43f01fd273e3d39d04be3647ddeb533d/pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db", size = 6926780, upload-time = "2026-06-04T07:49:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/4140da9ad54108e517f4a16b2d83da3033e08662144623e1239587cb7db6/pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd", size = 4307203, upload-time = "2026-06-04T07:49:18.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f5/10a6e845a00fc5e7afd0a988b744f403d4d57162a28d160a093c4d9322f0/pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c", size = 6362659, upload-time = "2026-06-04T07:49:21.349Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/dcd2d62b5944b6d5db53413a5899016ccd57ffcb7278f3f81655d25d2027/pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a", size = 6928825, upload-time = "2026-06-04T07:49:23.934Z" }, + { url = "https://files.pythonhosted.org/packages/b7/56/3cbb433fe4501cdba2eb9040f56a4e1a8243faa4186b25295564d1a7a79d/pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47", size = 6721875, upload-time = "2026-06-04T07:49:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -1661,11 +2080,13 @@ wheels = [ [[package]] name = "reboot" -version = "0.33.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, { name = "aiohttp" }, + { name = "anyio" }, + { name = "bitarray" }, { name = "cffi" }, { name = "colorama" }, { name = "cryptography" }, @@ -1677,9 +2098,11 @@ dependencies = [ { name = "grpcio-reflection" }, { name = "grpcio-status" }, { name = "grpcio-tools" }, + { name = "h2" }, { name = "jinja2" }, { name = "jinja2-strcase" }, { name = "kubernetes-asyncio" }, + { name = "mcp" }, { name = "mypy-protobuf" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-grpc" }, @@ -1690,10 +2113,13 @@ dependencies = [ { name = "protobuf" }, { name = "psutil" }, { name = "pydantic" }, + { name = "pydantic-ai-slim" }, { name = "pyjwt" }, { name = "pyprctl" }, + { name = "python-dotenv" }, + { name = "python-ulid" }, { name = "pyyaml" }, - { name = "sortedcontainers" }, + { name = "starlette" }, { name = "tabulate" }, { name = "typing-extensions" }, { name = "tzlocal" }, @@ -1704,10 +2130,24 @@ dependencies = [ { name = "websockets" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/a2/99f002a85aa588b4cb93553b4f8fb396115a2022659080c7f5f10060e583/reboot-0.33.0-py3-none-macosx_13_0_arm64.whl", hash = "sha256:88c37878c4b09db4dbe4f7ac6cb39e24a4afddc80a2ad6a871f1809a65657e90", size = 12356289, upload-time = "2025-08-12T19:28:23.286Z" }, - { url = "https://files.pythonhosted.org/packages/08/7b/911ff71e46eb49dba64b4602ff1a1e4740e7c63ba05158a63dfb6efe3606/reboot-0.33.0-py3-none-macosx_13_0_x86_64.whl", hash = "sha256:6d8945a48a13dd8dd10b63d157015f78fcc7d3adee8622b93a5a7cba935f8475", size = 12277764, upload-time = "2025-08-12T20:00:01.158Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d8/55722056ca66c9cfe4bbf50e2d4d0e3d322d219ae28d7795c1ff2b6979eb/reboot-0.33.0-py3-none-manylinux_2_31_aarch64.whl", hash = "sha256:85e718517591d5afaa399d524285d09e72df91f08632577c78347eb7e6356f30", size = 14710822, upload-time = "2025-08-12T19:22:22.651Z" }, - { url = "https://files.pythonhosted.org/packages/d0/b4/da3accc85ed736e492ece379949c69b034272fc1183638092c7a33f99ee6/reboot-0.33.0-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:4e087a710f477f52f6116171a2c3f5169fcc1708a40e8d444ea4380e1f8c165e", size = 14657196, upload-time = "2025-08-12T19:20:49.586Z" }, + { url = "https://files.pythonhosted.org/packages/5c/3a/3656c155ba7daf8a5b4b8de71345c82b11af5875ef8c727d804e6188b48e/reboot-1.1.0-py3-none-macosx_13_0_arm64.whl", hash = "sha256:458c18a0fd897870d37cec3a396d13cc97e1d0ce7c604de5c94ff2fe363c0b05", size = 20695822, upload-time = "2026-06-04T19:03:56.637Z" }, + { url = "https://files.pythonhosted.org/packages/66/a5/2784d4b02a8b9b5a27d710ac1de9e89b84e17c7954fc354b2735ee46ac02/reboot-1.1.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:f3a55ab0b70703b0c07a128cc8c0b35a619627ec28dda427904cf4104a624fe9", size = 24093172, upload-time = "2026-06-04T18:03:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/0a/50/47016a92fbd9078b86e2e08f2e669f57fc51059989e477266915c9bb0043/reboot-1.1.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:ef64a530c25e84be8f8992706050086dfaf9ff213eed958925e2f6ad9209307e", size = 24221768, upload-time = "2026-06-04T18:02:07.117Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] @@ -1737,6 +2177,271 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/a0/acf8b6fc20bfdcd3a45bd3f57680fb198e157b7e997b9123b10763798bd2/rpds_py-2026.5.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3397a5ed7174dc2786bb214030232fc36fe8e5584fec43a9952cc542b1a12036", size = 355609, upload-time = "2026-05-28T11:58:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/b6/95/f8203fd997484b1690a6869cd0e503b6c3c6be55b0ecc36d1a491fe742f0/rpds_py-2026.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99ab6ba7bfa2cb0f96a04e3652355bf04e3f51aceb1e943b8541dab7ba4828cc", size = 348460, upload-time = "2026-05-28T11:58:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/33/8c/b47326ad2f0be545a5e5c1a55937a12afaea7d392ba2837bb9680f57e6c9/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0efbe45632665e53e3db8fe1e5692db58fc5cb9bab4459d570b83efefe11164", size = 381031, upload-time = "2026-05-28T11:58:53.775Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/e83bbd97ffac6f6389b605cd4e1c8ac5761dc7e977769c9255d8c5adb7bd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead", size = 387121, upload-time = "2026-05-28T11:58:55.243Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0e/d285d1bc8864245919c61e1ca82263e4a66d337759c3a4cef72766ff9afc/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7559f72b94ae52659086c595dfa017cde03155f7832071d30959049052cb3ece", size = 501026, upload-time = "2026-05-28T11:58:56.788Z" }, + { url = "https://files.pythonhosted.org/packages/86/06/ccb2109a1e543437b5e43816f2b43b9554cc6783145528a4e3711e05c011/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e25b7088f9ccbfc0dfcaa52bf969300ca229e10ecf758974ebcbb080a4b37bb", size = 391865, upload-time = "2026-05-28T11:58:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/3d/33/237173db1cfef10105b3839a24de00eb8d2a523711add4632447cdf0aedd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613fc4ee9eaef26dc5840666214dd6fbcebcf32f46e76f4abc473059f4e13dda", size = 378012, upload-time = "2026-05-28T11:58:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/97/64/1eae54e34d5161f9969295e80bd6b62a55f2b6ac5f2a5b60d02c2140e758/rpds_py-2026.5.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:85264a90ff4c05c1568dd65f5921c837614b67c60358fb4c17df3b7f2e90690a", size = 391111, upload-time = "2026-05-28T11:59:01.104Z" }, + { url = "https://files.pythonhosted.org/packages/d8/34/5bb334a5a0f65d77869217c4654f34c78a7d11b93938a3c076a2edeafc52/rpds_py-2026.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe71bca7d547acb17027c7fd1624ff8aae623499c498d3e7011182c4de5c25e0", size = 409225, upload-time = "2026-05-28T11:59:02.433Z" }, + { url = "https://files.pythonhosted.org/packages/16/0f/007ec21283b5b040b4ec3bd95e0402591e22bfa7d5c93dfe01c465c2d2d7/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05fa4f41f37ec97c9c260441a940450a192f78d774d2b097eee1379f1e1246a", size = 556487, upload-time = "2026-05-28T11:59:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/ff/10/5437c94508169b6b22d8418fef7a66e9ffb5f3b9e9c94460f2eedafe06ff/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df1d2a1996755b24b9ecee92cb4d36c28f86f464a6a173349c26bab41e94b8c2", size = 620798, upload-time = "2026-05-28T11:59:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d5/9937dce4d6bda74157b954e7d1460db05a22f5929dccfeeba1ed27a93df0/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8895840ac4809e5f60c88fd07617cd71326e73d6e5a8aa783c5c0f7c24985de2", size = 584053, upload-time = "2026-05-28T11:59:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/6c/31/750617dd0ae1752471bf43f9e41d263398fae7cde7849d23b8574a70e617/rpds_py-2026.5.1-cp311-cp311-win32.whl", hash = "sha256:3684a59b158a7683aaeb8e25352e9a9dd2122cec78f2d8530266e4f91b4c7b3f", size = 214390, upload-time = "2026-05-28T11:59:08.402Z" }, + { url = "https://files.pythonhosted.org/packages/3c/bb/3dcab0e1d9516303f2eb672a5d6f62eca5a69e2886301e9c8c54b520c39b/rpds_py-2026.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:7bd530e6a530bb3ea892f194fafa455f3516ac25ecf7143fd33c09be62b0470a", size = 231097, upload-time = "2026-05-28T11:59:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/49/d6/c6bbf5cb1cf12b9732df8074b57f6ef8341ba884c95d40632ae8bddb44e4/rpds_py-2026.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:0a5ae4dbe43c1076983b72616496919872ae7bbe7a1e21cc48336bc3154d130b", size = 226361, upload-time = "2026-05-28T11:59:11.079Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e7/a78582dc57caa592dcc7d4fb69b61390561e908eb3d2f5df5928a8e354c0/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d", size = 353040, upload-time = "2026-05-28T11:59:12.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c", size = 345775, upload-time = "2026-05-28T11:59:13.827Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08", size = 376329, upload-time = "2026-05-28T11:59:15.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb", size = 383539, upload-time = "2026-05-28T11:59:16.665Z" }, + { url = "https://files.pythonhosted.org/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1", size = 494674, upload-time = "2026-05-28T11:59:17.991Z" }, + { url = "https://files.pythonhosted.org/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5", size = 389268, upload-time = "2026-05-28T11:59:19.434Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644", size = 376280, upload-time = "2026-05-28T11:59:21Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4", size = 387233, upload-time = "2026-05-28T11:59:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/bcec1005c4f4a234f92a29078631fee49206c7265ccae966f18fd332e80e/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6", size = 405009, upload-time = "2026-05-28T11:59:23.845Z" }, + { url = "https://files.pythonhosted.org/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4", size = 553113, upload-time = "2026-05-28T11:59:25.184Z" }, + { url = "https://files.pythonhosted.org/packages/d4/25/2ee807bdb3e1f0b7eddf7782acd5665a8b5205a331a7d7244a52c4812fd9/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24", size = 618838, upload-time = "2026-05-28T11:59:26.749Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732", size = 582436, upload-time = "2026-05-28T11:59:28.11Z" }, + { url = "https://files.pythonhosted.org/packages/04/1d/9d12b0a337bab46f4769f8857f4007e3b2d639e14f9a44a0efe157696e64/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed", size = 212734, upload-time = "2026-05-28T11:59:29.689Z" }, + { url = "https://files.pythonhosted.org/packages/c5/93/e4116f2de7f56bc7406a76033dc501811ddeb22b7f056b92d632871ebb0c/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870", size = 229045, upload-time = "2026-05-28T11:59:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/53/6c3419d85eb2ec5938a37627c585b42d76a63bb731d6e42ed4b079ebf486/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473", size = 223967, upload-time = "2026-05-28T11:59:32.318Z" }, + { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" }, + { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" }, + { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" }, + { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" }, + { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" }, + { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" }, + { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" }, + { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" }, + { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" }, + { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" }, + { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" }, + { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" }, + { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" }, + { url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" }, + { url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" }, + { url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" }, + { url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" }, + { url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" }, + { url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" }, + { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" }, + { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" }, + { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" }, + { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" }, + { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" }, + { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" }, + { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/42/56/3fe0fb34820ff667be791b3a3c22b85e8bcba54e9c832f47438c191fa7be/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:edf2765d84e42447f112ad877af8fe1db0089aaec5b28e88d6eab45e7fe99cea", size = 357151, upload-time = "2026-05-28T12:01:53.43Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/3eb9ccdb9f143b8c9b003978898cb497f942a324c077401e6b8834238e63/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad3773236e95f7f33991eb125224b7da66f206504d032a253a02da7e134519fb", size = 350195, upload-time = "2026-05-28T12:01:54.901Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/dbda232bc4f3ed732120692ab0d2c8402cb020516556d8bee622dcef2413/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04df86b3f0fade39ec8fd0e0aab089b1da9fbd2b48df778a57ef96f5e7d38df", size = 381850, upload-time = "2026-05-28T12:01:56.601Z" }, + { url = "https://files.pythonhosted.org/packages/40/30/32e769839a358f78810c234f160f2cc21d1e4e47e1c0e0e0d535be5a0219/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6142dbd80c4df62a5d899f0d616d417f84e0bc8d32526c8e5589019d75d028a7", size = 387899, upload-time = "2026-05-28T12:01:58.212Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/ec84d243aadb3b34b71dd26a010d0930b2d284ff5fc9a69fec53810ee6fd/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b35217adefe87f2fe4db7e9766cabe84744bfe9616d9667be18988928c7f2dc", size = 501618, upload-time = "2026-05-28T12:01:59.888Z" }, + { url = "https://files.pythonhosted.org/packages/74/25/b60e52686bbff777a64f9e4f4d3dd57980dc846913777177a2c92e4937aa/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b95d5e11fc712b752081183a55a244c03cd00570489edd7014d8899f8ceb8162", size = 394003, upload-time = "2026-05-28T12:02:01.482Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c7/b3a6a588cc2219510ef3f42e207483a93950bedd1e3a0fd4015c95cff9e5/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141c9498daf2ace9eda35d2b0e376f9ea8b058d84f2aef4f96fccfd449a2f251", size = 379778, upload-time = "2026-05-28T12:02:03.197Z" }, + { url = "https://files.pythonhosted.org/packages/31/00/c7dba3fc8a3da8cb3f6db1eb3386be4d79c2e97c6890d20eb9ac66ae8c43/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:6f249f8b860a200ad35193af961183ebe9132710484e6f6ce0cf89fd83c63a9a", size = 392359, upload-time = "2026-05-28T12:02:04.817Z" }, + { url = "https://files.pythonhosted.org/packages/93/dd/472ba494c70753f93745992c99855bee0636daf74e6984e5e003f150316f/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4abbf391a70be864920858bf360f4fb380577c9a0f732438a1996726e2c195b", size = 412820, upload-time = "2026-05-28T12:02:06.401Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6f/93831a3bfe789542ed0c1d0d74b78b440f055d6dc3ea4640eba2d95e6e23/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c74005a7bb87752acf351c93897ec63ad77a07a0da7ecad9c050e32e7286ba34", size = 557243, upload-time = "2026-05-28T12:02:08.013Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ff/0b3d604614ffc77522c6b288fdbce68957eb583da1002aa65ba38ac0ee40/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:8213afbe8a3a906fb9acb2014423fe3359ee783d0bf90995f70623a3217bfa6c", size = 623541, upload-time = "2026-05-28T12:02:09.661Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ea/e7b0251441da9adfeaebcf29601d10f2a1455fcf0772fae9e7e19032bd96/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8c43a8a973270fd173bf48cdf80bbe66312421cba68d40845034f174f2389049", size = 586326, upload-time = "2026-05-28T12:02:11.47Z" }, +] + [[package]] name = "setuptools" version = "80.9.0" @@ -1764,15 +2469,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] -[[package]] -name = "sortedcontainers" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, -] - [[package]] name = "sqlalchemy" version = "2.0.42" @@ -1818,6 +2514,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/55/ba2546ab09a6adebc521bf3974440dc1d8c06ed342cceb30ed62a8858835/sqlalchemy-2.0.42-py3-none-any.whl", hash = "sha256:defcdff7e661f0043daa381832af65d616e060ddb54d3fe4476f51df7eaa1835", size = 1922072, upload-time = "2025-07-29T13:09:17.061Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size = 21943, upload-time = "2025-10-30T18:44:20.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" }, +] + [[package]] name = "starlette" version = "0.46.2" @@ -1848,6 +2556,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "types-protobuf" version = "6.30.2.20250703" @@ -1859,23 +2576,23 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.14.1" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "typing-inspection" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] @@ -1965,61 +2682,61 @@ wheels = [ [[package]] name = "websockets" -version = "13.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549, upload-time = "2024-09-21T17:34:21.54Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/94/d15dbfc6a5eb636dbc754303fba18208f2e88cf97e733e1d64fb9cb5c89e/websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee", size = 157815, upload-time = "2024-09-21T17:32:27.107Z" }, - { url = "https://files.pythonhosted.org/packages/30/02/c04af33f4663945a26f5e8cf561eb140c35452b50af47a83c3fbcfe62ae1/websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7", size = 155466, upload-time = "2024-09-21T17:32:28.428Z" }, - { url = "https://files.pythonhosted.org/packages/35/e8/719f08d12303ea643655e52d9e9851b2dadbb1991d4926d9ce8862efa2f5/websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6", size = 155716, upload-time = "2024-09-21T17:32:29.905Z" }, - { url = "https://files.pythonhosted.org/packages/91/e1/14963ae0252a8925f7434065d25dcd4701d5e281a0b4b460a3b5963d2594/websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b", size = 164806, upload-time = "2024-09-21T17:32:31.384Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fa/ab28441bae5e682a0f7ddf3d03440c0c352f930da419301f4a717f675ef3/websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa", size = 163810, upload-time = "2024-09-21T17:32:32.384Z" }, - { url = "https://files.pythonhosted.org/packages/44/77/dea187bd9d16d4b91566a2832be31f99a40d0f5bfa55eeb638eb2c3bc33d/websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700", size = 164125, upload-time = "2024-09-21T17:32:33.398Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d9/3af14544e83f1437eb684b399e6ba0fa769438e869bf5d83d74bc197fae8/websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c", size = 164532, upload-time = "2024-09-21T17:32:35.109Z" }, - { url = "https://files.pythonhosted.org/packages/1c/8a/6d332eabe7d59dfefe4b8ba6f46c8c5fabb15b71c8a8bc3d2b65de19a7b6/websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0", size = 163948, upload-time = "2024-09-21T17:32:36.214Z" }, - { url = "https://files.pythonhosted.org/packages/1a/91/a0aeadbaf3017467a1ee03f8fb67accdae233fe2d5ad4b038c0a84e357b0/websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f", size = 163898, upload-time = "2024-09-21T17:32:37.277Z" }, - { url = "https://files.pythonhosted.org/packages/71/31/a90fb47c63e0ae605be914b0b969d7c6e6ffe2038cd744798e4b3fbce53b/websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe", size = 158706, upload-time = "2024-09-21T17:32:38.755Z" }, - { url = "https://files.pythonhosted.org/packages/93/ca/9540a9ba80da04dc7f36d790c30cae4252589dbd52ccdc92e75b0be22437/websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a", size = 159141, upload-time = "2024-09-21T17:32:40.495Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f0/cf0b8a30d86b49e267ac84addbebbc7a48a6e7bb7c19db80f62411452311/websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19", size = 157813, upload-time = "2024-09-21T17:32:42.188Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e7/22285852502e33071a8cf0ac814f8988480ec6db4754e067b8b9d0e92498/websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5", size = 155469, upload-time = "2024-09-21T17:32:43.858Z" }, - { url = "https://files.pythonhosted.org/packages/68/d4/c8c7c1e5b40ee03c5cc235955b0fb1ec90e7e37685a5f69229ad4708dcde/websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd", size = 155717, upload-time = "2024-09-21T17:32:44.914Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e4/c50999b9b848b1332b07c7fd8886179ac395cb766fda62725d1539e7bc6c/websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02", size = 165379, upload-time = "2024-09-21T17:32:45.933Z" }, - { url = "https://files.pythonhosted.org/packages/bc/49/4a4ad8c072f18fd79ab127650e47b160571aacfc30b110ee305ba25fffc9/websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7", size = 164376, upload-time = "2024-09-21T17:32:46.987Z" }, - { url = "https://files.pythonhosted.org/packages/af/9b/8c06d425a1d5a74fd764dd793edd02be18cf6fc3b1ccd1f29244ba132dc0/websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096", size = 164753, upload-time = "2024-09-21T17:32:48.046Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5b/0acb5815095ff800b579ffc38b13ab1b915b317915023748812d24e0c1ac/websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084", size = 165051, upload-time = "2024-09-21T17:32:49.271Z" }, - { url = "https://files.pythonhosted.org/packages/30/93/c3891c20114eacb1af09dedfcc620c65c397f4fd80a7009cd12d9457f7f5/websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3", size = 164489, upload-time = "2024-09-21T17:32:50.392Z" }, - { url = "https://files.pythonhosted.org/packages/28/09/af9e19885539759efa2e2cd29b8b3f9eecef7ecefea40d46612f12138b36/websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9", size = 164438, upload-time = "2024-09-21T17:32:52.223Z" }, - { url = "https://files.pythonhosted.org/packages/b6/08/6f38b8e625b3d93de731f1d248cc1493327f16cb45b9645b3e791782cff0/websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f", size = 158710, upload-time = "2024-09-21T17:32:53.244Z" }, - { url = "https://files.pythonhosted.org/packages/fb/39/ec8832ecb9bb04a8d318149005ed8cee0ba4e0205835da99e0aa497a091f/websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557", size = 159137, upload-time = "2024-09-21T17:32:54.721Z" }, - { url = "https://files.pythonhosted.org/packages/df/46/c426282f543b3c0296cf964aa5a7bb17e984f58dde23460c3d39b3148fcf/websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", size = 157821, upload-time = "2024-09-21T17:32:56.442Z" }, - { url = "https://files.pythonhosted.org/packages/aa/85/22529867010baac258da7c45848f9415e6cf37fef00a43856627806ffd04/websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", size = 155480, upload-time = "2024-09-21T17:32:57.698Z" }, - { url = "https://files.pythonhosted.org/packages/29/2c/bdb339bfbde0119a6e84af43ebf6275278698a2241c2719afc0d8b0bdbf2/websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", size = 155715, upload-time = "2024-09-21T17:32:59.429Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d0/8612029ea04c5c22bf7af2fd3d63876c4eaeef9b97e86c11972a43aa0e6c/websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0", size = 165647, upload-time = "2024-09-21T17:33:00.495Z" }, - { url = "https://files.pythonhosted.org/packages/56/04/1681ed516fa19ca9083f26d3f3a302257e0911ba75009533ed60fbb7b8d1/websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6", size = 164592, upload-time = "2024-09-21T17:33:02.223Z" }, - { url = "https://files.pythonhosted.org/packages/38/6f/a96417a49c0ed132bb6087e8e39a37db851c70974f5c724a4b2a70066996/websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9", size = 165012, upload-time = "2024-09-21T17:33:03.288Z" }, - { url = "https://files.pythonhosted.org/packages/40/8b/fccf294919a1b37d190e86042e1a907b8f66cff2b61e9befdbce03783e25/websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68", size = 165311, upload-time = "2024-09-21T17:33:04.728Z" }, - { url = "https://files.pythonhosted.org/packages/c1/61/f8615cf7ce5fe538476ab6b4defff52beb7262ff8a73d5ef386322d9761d/websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14", size = 164692, upload-time = "2024-09-21T17:33:05.829Z" }, - { url = "https://files.pythonhosted.org/packages/5c/f1/a29dd6046d3a722d26f182b783a7997d25298873a14028c4760347974ea3/websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf", size = 164686, upload-time = "2024-09-21T17:33:06.823Z" }, - { url = "https://files.pythonhosted.org/packages/0f/99/ab1cdb282f7e595391226f03f9b498f52109d25a2ba03832e21614967dfa/websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c", size = 158712, upload-time = "2024-09-21T17:33:07.877Z" }, - { url = "https://files.pythonhosted.org/packages/46/93/e19160db48b5581feac8468330aa11b7292880a94a37d7030478596cc14e/websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3", size = 159145, upload-time = "2024-09-21T17:33:09.202Z" }, - { url = "https://files.pythonhosted.org/packages/51/20/2b99ca918e1cbd33c53db2cace5f0c0cd8296fc77558e1908799c712e1cd/websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", size = 157828, upload-time = "2024-09-21T17:33:10.987Z" }, - { url = "https://files.pythonhosted.org/packages/b8/47/0932a71d3d9c0e9483174f60713c84cee58d62839a143f21a2bcdbd2d205/websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", size = 155487, upload-time = "2024-09-21T17:33:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/a9/60/f1711eb59ac7a6c5e98e5637fef5302f45b6f76a2c9d64fd83bbb341377a/websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", size = 155721, upload-time = "2024-09-21T17:33:13.909Z" }, - { url = "https://files.pythonhosted.org/packages/6a/e6/ba9a8db7f9d9b0e5f829cf626ff32677f39824968317223605a6b419d445/websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", size = 165609, upload-time = "2024-09-21T17:33:14.967Z" }, - { url = "https://files.pythonhosted.org/packages/c1/22/4ec80f1b9c27a0aebd84ccd857252eda8418ab9681eb571b37ca4c5e1305/websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", size = 164556, upload-time = "2024-09-21T17:33:17.113Z" }, - { url = "https://files.pythonhosted.org/packages/27/ac/35f423cb6bb15600438db80755609d27eda36d4c0b3c9d745ea12766c45e/websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", size = 164993, upload-time = "2024-09-21T17:33:18.168Z" }, - { url = "https://files.pythonhosted.org/packages/31/4e/98db4fd267f8be9e52e86b6ee4e9aa7c42b83452ea0ea0672f176224b977/websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", size = 165360, upload-time = "2024-09-21T17:33:19.233Z" }, - { url = "https://files.pythonhosted.org/packages/3f/15/3f0de7cda70ffc94b7e7024544072bc5b26e2c1eb36545291abb755d8cdb/websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", size = 164745, upload-time = "2024-09-21T17:33:20.361Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732, upload-time = "2024-09-21T17:33:23.103Z" }, - { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709, upload-time = "2024-09-21T17:33:24.196Z" }, - { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144, upload-time = "2024-09-21T17:33:25.96Z" }, - { url = "https://files.pythonhosted.org/packages/2d/75/6da22cb3ad5b8c606963f9a5f9f88656256fecc29d420b4b2bf9e0c7d56f/websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238", size = 155499, upload-time = "2024-09-21T17:33:54.917Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ba/22833d58629088fcb2ccccedfae725ac0bbcd713319629e97125b52ac681/websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5", size = 155737, upload-time = "2024-09-21T17:33:56.052Z" }, - { url = "https://files.pythonhosted.org/packages/95/54/61684fe22bdb831e9e1843d972adadf359cf04ab8613285282baea6a24bb/websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9", size = 157095, upload-time = "2024-09-21T17:33:57.21Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f5/6652fb82440813822022a9301a30afde85e5ff3fb2aebb77f34aabe2b4e8/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6", size = 156701, upload-time = "2024-09-21T17:33:59.061Z" }, - { url = "https://files.pythonhosted.org/packages/67/33/ae82a7b860fa8a08aba68818bdf7ff61f04598aa5ab96df4cd5a3e418ca4/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a", size = 156654, upload-time = "2024-09-21T17:34:00.944Z" }, - { url = "https://files.pythonhosted.org/packages/63/0b/a1b528d36934f833e20f6da1032b995bf093d55cb416b9f2266f229fb237/websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23", size = 159192, upload-time = "2024-09-21T17:34:02.656Z" }, - { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134, upload-time = "2024-09-21T17:34:19.904Z" }, +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] [[package]] diff --git a/reboot/examples/kcdc-2025/web/package-lock.json b/reboot/examples/kcdc-2025/web/package-lock.json index 65c3ab11..fa1b545e 100644 --- a/reboot/examples/kcdc-2025/web/package-lock.json +++ b/reboot/examples/kcdc-2025/web/package-lock.json @@ -16,9 +16,9 @@ "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tooltip": "^1.0.7", - "@reboot-dev/reboot-react": "1.0.4", - "@reboot-dev/reboot-std-api": "1.0.4", - "@reboot-dev/reboot-std-react": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", + "@reboot-dev/reboot-std-api": "1.1.0", + "@reboot-dev/reboot-std-react": "1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "emoji-picker-react": "^4.9.2", @@ -1753,9 +1753,9 @@ "license": "MIT" }, "node_modules/@reboot-dev/reboot-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.0.4.tgz", - "integrity": "sha512-fZhhSz7Rf0wkRTJSulqdKlHorBlM6bPO2g9g/z6sL8byTqbJ+jFCaYyXupT/+s1VEC9EhdiAezossYikgBKbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0", @@ -1789,15 +1789,15 @@ } }, "node_modules/@reboot-dev/reboot-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.0.4.tgz", - "integrity": "sha512-p6cnF7B1qRu5eH3DkpneMoflXKyszTpFxpHp+CjJaZZ72EMFq2HnlLydKAIaNtJUjknCNxy4ASMgxion2KGKzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/ext-apps": "1.5.0", "@modelcontextprotocol/sdk": "1.29.0", - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0", "@types/uuid": "^9.0.4", "js-sha1": "0.7.0", @@ -1825,34 +1825,34 @@ } }, "node_modules/@reboot-dev/reboot-std-api": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.0.4.tgz", - "integrity": "sha512-Qg9GKZDGjEB/XWtqrunh7EMAmsIMpqC+nf1j96RslSp3GuWT8ZavDPoBPvq637Aw7wLCKIIMUb9KSXBVVDJkFA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-api/-/reboot-std-api-1.1.0.tgz", + "integrity": "sha512-IZioMk6LG+kJK4fpFmgAXYzF4dBeZCojL/5KpUqb7roRdQZa1gi5l/FaNnWtP6s5TNOLBKYfkN4CRkz8nfdjUA==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "1.4.0" } }, "node_modules/@reboot-dev/reboot-std-react": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-react/-/reboot-std-react-1.0.4.tgz", - "integrity": "sha512-1bnMyqqkGoz1muKkxCPqMl8w9djK2A0rjUhNx+2PytOYSmgNJvlTZWJG+xVk1F4xLT42tnuxY9VbtpvV1aGDMQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-std-react/-/reboot-std-react-1.1.0.tgz", + "integrity": "sha512-YJTIhV7wifJe/z72+dO/cPjbCxV3+uQvlMfkuqMagrTmICjDTqzoGls+gUkGTddjQRvPmb00pGqdpgL4Xd35eg==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", - "@reboot-dev/reboot-react": "1.0.4", - "@reboot-dev/reboot-std-api": "1.0.4", - "@reboot-dev/reboot-web": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", + "@reboot-dev/reboot-std-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", "@scarf/scarf": "1.4.0" } }, "node_modules/@reboot-dev/reboot-web": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.0.4.tgz", - "integrity": "sha512-l1WhRDGpRTTMcY7KuvkUlg69QcNPHqDbqiaMblUYn5UefIRuD2p6dnKbNwDnHFMPN7FQrY3sh139kPLelQxN0A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", "license": "Apache-2.0", "dependencies": { - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot-api": "1.1.0", "@scarf/scarf": "1.4.0", "js-sha1": "0.7.0", "lru-cache-idb": "^0.5.2", diff --git a/reboot/examples/kcdc-2025/web/package.json b/reboot/examples/kcdc-2025/web/package.json index 3caac843..0ab752cd 100644 --- a/reboot/examples/kcdc-2025/web/package.json +++ b/reboot/examples/kcdc-2025/web/package.json @@ -18,9 +18,9 @@ "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tooltip": "^1.0.7", - "@reboot-dev/reboot-react": "1.0.4", - "@reboot-dev/reboot-std-api": "1.0.4", - "@reboot-dev/reboot-std-react": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", + "@reboot-dev/reboot-std-api": "1.1.0", + "@reboot-dev/reboot-std-react": "1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "emoji-picker-react": "^4.9.2", diff --git a/reboot/examples/monorepo/.claude/settings.json b/reboot/examples/monorepo/.claude/settings.json new file mode 100644 index 00000000..d522b476 --- /dev/null +++ b/reboot/examples/monorepo/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "reboot-plugin": { + "source": { + "source": "github", + "repo": "reboot-dev/reboot-plugin" + } + } + }, + "enabledPlugins": { + "reboot@reboot-plugin": true + } +} diff --git a/reboot/examples/monorepo/pyproject.toml b/reboot/examples/monorepo/pyproject.toml index f43f508c..5326581e 100644 --- a/reboot/examples/monorepo/pyproject.toml +++ b/reboot/examples/monorepo/pyproject.toml @@ -1,7 +1,7 @@ [project] requires-python = ">= 3.10" dependencies = [ - "reboot==1.0.4", + "reboot==1.1.0", ] [tool.rye] @@ -9,7 +9,7 @@ dev-dependencies = [ "mypy==1.18.1", "pytest>=7.4.2", "types-protobuf>=4.24.0.20240129", - "reboot==1.0.4", + "reboot==1.1.0", ] # This project only uses `rye` to provide `python` and its dependencies, so diff --git a/reboot/examples/monorepo/requirements-dev.lock b/reboot/examples/monorepo/requirements-dev.lock index b4636b3f..3ce516d3 100644 --- a/reboot/examples/monorepo/requirements-dev.lock +++ b/reboot/examples/monorepo/requirements-dev.lock @@ -211,8 +211,9 @@ pyprctl==0.1.3 pytest==8.3.3 python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.22 # via mcp python-ulid==3.1.0 @@ -220,7 +221,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/monorepo/requirements.lock b/reboot/examples/monorepo/requirements.lock index b85b011a..635fca50 100644 --- a/reboot/examples/monorepo/requirements.lock +++ b/reboot/examples/monorepo/requirements.lock @@ -200,8 +200,9 @@ pyprctl==0.1.3 # via reboot python-dateutil==2.9.0.post0 # via kubernetes-asyncio -python-dotenv==1.2.2 +python-dotenv==1.2.1 # via pydantic-settings + # via reboot python-multipart==0.0.22 # via mcp python-ulid==3.1.0 @@ -209,7 +210,7 @@ python-ulid==3.1.0 pyyaml==6.0.2 # via kubernetes-asyncio # via reboot -reboot==1.0.4 +reboot==1.1.0 referencing==0.37.0 # via jsonschema # via jsonschema-specifications diff --git a/reboot/examples/prosemirror-zod/.claude/settings.json b/reboot/examples/prosemirror-zod/.claude/settings.json new file mode 100644 index 00000000..d522b476 --- /dev/null +++ b/reboot/examples/prosemirror-zod/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "reboot-plugin": { + "source": { + "source": "github", + "repo": "reboot-dev/reboot-plugin" + } + } + }, + "enabledPlugins": { + "reboot@reboot-plugin": true + } +} diff --git a/reboot/examples/prosemirror-zod/Dockerfile b/reboot/examples/prosemirror-zod/Dockerfile index 6cd21c0c..d456e7d3 100644 --- a/reboot/examples/prosemirror-zod/Dockerfile +++ b/reboot/examples/prosemirror-zod/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/reboot-dev/reboot-base:1.0.4 +FROM ghcr.io/reboot-dev/reboot-base:1.1.0 WORKDIR /app diff --git a/reboot/examples/prosemirror-zod/backend/package.json b/reboot/examples/prosemirror-zod/backend/package.json index 5074b65c..b6cb6ef3 100644 --- a/reboot/examples/prosemirror-zod/backend/package.json +++ b/reboot/examples/prosemirror-zod/backend/package.json @@ -10,8 +10,8 @@ "@bufbuild/protobuf": "1.10.1", "@monorepo/api": "workspace:*", "@monorepo/common": "workspace:*", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", "prosemirror-model": "^1.23.0", "prosemirror-transform": "^1.1.0" } diff --git a/reboot/examples/prosemirror-zod/web/package.json b/reboot/examples/prosemirror-zod/web/package.json index c2900511..3d18fef7 100644 --- a/reboot/examples/prosemirror-zod/web/package.json +++ b/reboot/examples/prosemirror-zod/web/package.json @@ -14,7 +14,7 @@ "@monorepo/api": "workspace:*", "@monorepo/common": "workspace:*", "@nytimes/react-prosemirror": "^0.6.2", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", "next": "15.0.5", "prosemirror-collab": "^1.3.1", "prosemirror-history": "^1.4.1", diff --git a/reboot/examples/prosemirror-zod/yarn.lock b/reboot/examples/prosemirror-zod/yarn.lock index b17d1f78..4667b14d 100644 --- a/reboot/examples/prosemirror-zod/yarn.lock +++ b/reboot/examples/prosemirror-zod/yarn.lock @@ -805,8 +805,8 @@ __metadata: "@bufbuild/protobuf": "npm:1.10.1" "@monorepo/api": "workspace:*" "@monorepo/common": "workspace:*" - "@reboot-dev/reboot": "npm:1.0.4" - "@reboot-dev/reboot-api": "npm:1.0.4" + "@reboot-dev/reboot": "npm:1.1.0" + "@reboot-dev/reboot-api": "npm:1.1.0" prosemirror-model: "npm:^1.23.0" prosemirror-transform: "npm:^1.1.0" languageName: unknown @@ -831,7 +831,7 @@ __metadata: "@monorepo/api": "workspace:*" "@monorepo/common": "workspace:*" "@nytimes/react-prosemirror": "npm:^0.6.2" - "@reboot-dev/reboot-react": "npm:1.0.4" + "@reboot-dev/reboot-react": "npm:1.1.0" "@types/node": "npm:^20" "@types/react": "npm:^18" "@types/react-dom": "npm:^18" @@ -1011,27 +1011,27 @@ __metadata: languageName: node linkType: hard -"@reboot-dev/reboot-api@npm:1.0.4": - version: 1.0.4 - resolution: "@reboot-dev/reboot-api@npm:1.0.4" +"@reboot-dev/reboot-api@npm:1.1.0": + version: 1.1.0 + resolution: "@reboot-dev/reboot-api@npm:1.1.0" dependencies: "@scarf/scarf": "npm:1.4.0" typescript: "npm:5.4.5" zod: "npm:^3.25.51" peerDependencies: "@bufbuild/protobuf": 1.10.1 - checksum: 10c0/dbc3d7fa8a449fcbe11fd6518e1652291a10bf6c56942a0321dd83492ab0d7702ef95c74bc12dfd08f8eb116500699e346055b73353c285531e3eca0b3c2738a + checksum: 10c0/895eb4d808a0932f6e379d722e51ee62c8de715c0b6470cabaed1d8469e0bebe72f737a72b6ea71b16bdce45b4d26c1970260d8aa43f88d2046e7f69db4ad754 languageName: node linkType: hard -"@reboot-dev/reboot-react@npm:1.0.4": - version: 1.0.4 - resolution: "@reboot-dev/reboot-react@npm:1.0.4" +"@reboot-dev/reboot-react@npm:1.1.0": + version: 1.1.0 + resolution: "@reboot-dev/reboot-react@npm:1.1.0" dependencies: "@modelcontextprotocol/ext-apps": "npm:1.5.0" "@modelcontextprotocol/sdk": "npm:1.29.0" - "@reboot-dev/reboot-api": "npm:1.0.4" - "@reboot-dev/reboot-web": "npm:1.0.4" + "@reboot-dev/reboot-api": "npm:1.1.0" + "@reboot-dev/reboot-web": "npm:1.1.0" "@scarf/scarf": "npm:1.4.0" "@types/uuid": "npm:^9.0.4" js-sha1: "npm:0.7.0" @@ -1042,15 +1042,15 @@ __metadata: "@bufbuild/protobuf": 1.10.1 react: ">=18.0.0" react-dom: ">=18.0.0" - checksum: 10c0/1bc7f62d46ad91a34ddfac22ac6a44ec51119803bf3d9bdd12b73b6357653cb7efbeab12081ba2a698ea5b9dfde0db47be3d96d36862587a485ac0caead3ae74 + checksum: 10c0/53b24dc49d44341494569415a7b90b1c50fa5307dcc0140236e974a9d098f1ccf0971d7d660b012bcc99bacf4a5cb8d8c6e7677637093046384475301bccac4f languageName: node linkType: hard -"@reboot-dev/reboot-web@npm:1.0.4": - version: 1.0.4 - resolution: "@reboot-dev/reboot-web@npm:1.0.4" +"@reboot-dev/reboot-web@npm:1.1.0": + version: 1.1.0 + resolution: "@reboot-dev/reboot-web@npm:1.1.0" dependencies: - "@reboot-dev/reboot-api": "npm:1.0.4" + "@reboot-dev/reboot-api": "npm:1.1.0" "@scarf/scarf": "npm:1.4.0" js-sha1: "npm:0.7.0" lru-cache-idb: "npm:^0.5.2" @@ -1059,17 +1059,17 @@ __metadata: uuid: "npm:11.1.0" peerDependencies: "@bufbuild/protobuf": 1.10.1 - checksum: 10c0/a4d9c8a506ea6011cafa2d6e08cdbffd0675c09e5f58a3f7ff2e5529e7225b00dfb52327d736b86a009aa7cd551f3c406aa2aefb7be83303c7ce6e4c3d80eef4 + checksum: 10c0/e3249165df9ba31751cdc9efe6500c5d8977301cc3a17964d0f03c333102b36fb5923e5145c432fbfe8639ea4996d03b76c5b46e13280b5a44f4f947cc6e0953 languageName: node linkType: hard -"@reboot-dev/reboot@npm:1.0.4": - version: 1.0.4 - resolution: "@reboot-dev/reboot@npm:1.0.4" +"@reboot-dev/reboot@npm:1.1.0": + version: 1.1.0 + resolution: "@reboot-dev/reboot@npm:1.1.0" dependencies: "@bufbuild/protoc-gen-es": "npm:1.10.1" "@bufbuild/protoplugin": "npm:1.10.1" - "@reboot-dev/reboot-api": "npm:1.0.4" + "@reboot-dev/reboot-api": "npm:1.1.0" "@scarf/scarf": "npm:1.4.0" "@standard-schema/spec": "npm:1.0.0" chalk: "npm:^4.1.2" @@ -1090,7 +1090,7 @@ __metadata: rbt: rbt.js rbt-esbuild: rbt-esbuild.js zod-to-proto: zod-to-proto.js - checksum: 10c0/56fd8a59ff0e451966d910cf4d1dc6d571999f478c5f259fa2446f481d710b1c5d478c149777f4fa0035aa636d374815a1cb75304df8dee5e09fce562f1b521a + checksum: 10c0/fcbe1e995fcaef42a43dd99674ac28ee98f99a24c1ff04f5c15cf93aa38432f2bb6ec944966e169e612845628d0ab05bd3684e8d82cbd02873a6fecd6ee670dd languageName: node linkType: hard diff --git a/reboot/examples/prosemirror/.claude/settings.json b/reboot/examples/prosemirror/.claude/settings.json new file mode 100644 index 00000000..d522b476 --- /dev/null +++ b/reboot/examples/prosemirror/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "reboot-plugin": { + "source": { + "source": "github", + "repo": "reboot-dev/reboot-plugin" + } + } + }, + "enabledPlugins": { + "reboot@reboot-plugin": true + } +} diff --git a/reboot/examples/prosemirror/Dockerfile b/reboot/examples/prosemirror/Dockerfile index 6cd21c0c..d456e7d3 100644 --- a/reboot/examples/prosemirror/Dockerfile +++ b/reboot/examples/prosemirror/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/reboot-dev/reboot-base:1.0.4 +FROM ghcr.io/reboot-dev/reboot-base:1.1.0 WORKDIR /app diff --git a/reboot/examples/prosemirror/backend/package.json b/reboot/examples/prosemirror/backend/package.json index 5074b65c..b6cb6ef3 100644 --- a/reboot/examples/prosemirror/backend/package.json +++ b/reboot/examples/prosemirror/backend/package.json @@ -10,8 +10,8 @@ "@bufbuild/protobuf": "1.10.1", "@monorepo/api": "workspace:*", "@monorepo/common": "workspace:*", - "@reboot-dev/reboot": "1.0.4", - "@reboot-dev/reboot-api": "1.0.4", + "@reboot-dev/reboot": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", "prosemirror-model": "^1.23.0", "prosemirror-transform": "^1.1.0" } diff --git a/reboot/examples/prosemirror/web/package.json b/reboot/examples/prosemirror/web/package.json index c2900511..3d18fef7 100644 --- a/reboot/examples/prosemirror/web/package.json +++ b/reboot/examples/prosemirror/web/package.json @@ -14,7 +14,7 @@ "@monorepo/api": "workspace:*", "@monorepo/common": "workspace:*", "@nytimes/react-prosemirror": "^0.6.2", - "@reboot-dev/reboot-react": "1.0.4", + "@reboot-dev/reboot-react": "1.1.0", "next": "15.0.5", "prosemirror-collab": "^1.3.1", "prosemirror-history": "^1.4.1", diff --git a/reboot/examples/prosemirror/yarn.lock b/reboot/examples/prosemirror/yarn.lock index 5f411ceb..c1286e18 100644 --- a/reboot/examples/prosemirror/yarn.lock +++ b/reboot/examples/prosemirror/yarn.lock @@ -776,8 +776,8 @@ __metadata: "@bufbuild/protobuf": "npm:1.10.1" "@monorepo/api": "workspace:*" "@monorepo/common": "workspace:*" - "@reboot-dev/reboot": "npm:1.0.4" - "@reboot-dev/reboot-api": "npm:1.0.4" + "@reboot-dev/reboot": "npm:1.1.0" + "@reboot-dev/reboot-api": "npm:1.1.0" prosemirror-model: "npm:^1.23.0" prosemirror-transform: "npm:^1.1.0" languageName: unknown @@ -802,7 +802,7 @@ __metadata: "@monorepo/api": "workspace:*" "@monorepo/common": "workspace:*" "@nytimes/react-prosemirror": "npm:^0.6.2" - "@reboot-dev/reboot-react": "npm:1.0.4" + "@reboot-dev/reboot-react": "npm:1.1.0" "@types/node": "npm:^20" "@types/react": "npm:^18" "@types/react-dom": "npm:^18" @@ -971,27 +971,27 @@ __metadata: languageName: node linkType: hard -"@reboot-dev/reboot-api@npm:1.0.4": - version: 1.0.4 - resolution: "@reboot-dev/reboot-api@npm:1.0.4" +"@reboot-dev/reboot-api@npm:1.1.0": + version: 1.1.0 + resolution: "@reboot-dev/reboot-api@npm:1.1.0" dependencies: "@scarf/scarf": "npm:1.4.0" typescript: "npm:5.4.5" zod: "npm:^3.25.51" peerDependencies: "@bufbuild/protobuf": 1.10.1 - checksum: 10c0/dbc3d7fa8a449fcbe11fd6518e1652291a10bf6c56942a0321dd83492ab0d7702ef95c74bc12dfd08f8eb116500699e346055b73353c285531e3eca0b3c2738a + checksum: 10c0/895eb4d808a0932f6e379d722e51ee62c8de715c0b6470cabaed1d8469e0bebe72f737a72b6ea71b16bdce45b4d26c1970260d8aa43f88d2046e7f69db4ad754 languageName: node linkType: hard -"@reboot-dev/reboot-react@npm:1.0.4": - version: 1.0.4 - resolution: "@reboot-dev/reboot-react@npm:1.0.4" +"@reboot-dev/reboot-react@npm:1.1.0": + version: 1.1.0 + resolution: "@reboot-dev/reboot-react@npm:1.1.0" dependencies: "@modelcontextprotocol/ext-apps": "npm:1.5.0" "@modelcontextprotocol/sdk": "npm:1.29.0" - "@reboot-dev/reboot-api": "npm:1.0.4" - "@reboot-dev/reboot-web": "npm:1.0.4" + "@reboot-dev/reboot-api": "npm:1.1.0" + "@reboot-dev/reboot-web": "npm:1.1.0" "@scarf/scarf": "npm:1.4.0" "@types/uuid": "npm:^9.0.4" js-sha1: "npm:0.7.0" @@ -1002,15 +1002,15 @@ __metadata: "@bufbuild/protobuf": 1.10.1 react: ">=18.0.0" react-dom: ">=18.0.0" - checksum: 10c0/1bc7f62d46ad91a34ddfac22ac6a44ec51119803bf3d9bdd12b73b6357653cb7efbeab12081ba2a698ea5b9dfde0db47be3d96d36862587a485ac0caead3ae74 + checksum: 10c0/53b24dc49d44341494569415a7b90b1c50fa5307dcc0140236e974a9d098f1ccf0971d7d660b012bcc99bacf4a5cb8d8c6e7677637093046384475301bccac4f languageName: node linkType: hard -"@reboot-dev/reboot-web@npm:1.0.4": - version: 1.0.4 - resolution: "@reboot-dev/reboot-web@npm:1.0.4" +"@reboot-dev/reboot-web@npm:1.1.0": + version: 1.1.0 + resolution: "@reboot-dev/reboot-web@npm:1.1.0" dependencies: - "@reboot-dev/reboot-api": "npm:1.0.4" + "@reboot-dev/reboot-api": "npm:1.1.0" "@scarf/scarf": "npm:1.4.0" js-sha1: "npm:0.7.0" lru-cache-idb: "npm:^0.5.2" @@ -1019,17 +1019,17 @@ __metadata: uuid: "npm:11.1.0" peerDependencies: "@bufbuild/protobuf": 1.10.1 - checksum: 10c0/a4d9c8a506ea6011cafa2d6e08cdbffd0675c09e5f58a3f7ff2e5529e7225b00dfb52327d736b86a009aa7cd551f3c406aa2aefb7be83303c7ce6e4c3d80eef4 + checksum: 10c0/e3249165df9ba31751cdc9efe6500c5d8977301cc3a17964d0f03c333102b36fb5923e5145c432fbfe8639ea4996d03b76c5b46e13280b5a44f4f947cc6e0953 languageName: node linkType: hard -"@reboot-dev/reboot@npm:1.0.4": - version: 1.0.4 - resolution: "@reboot-dev/reboot@npm:1.0.4" +"@reboot-dev/reboot@npm:1.1.0": + version: 1.1.0 + resolution: "@reboot-dev/reboot@npm:1.1.0" dependencies: "@bufbuild/protoc-gen-es": "npm:1.10.1" "@bufbuild/protoplugin": "npm:1.10.1" - "@reboot-dev/reboot-api": "npm:1.0.4" + "@reboot-dev/reboot-api": "npm:1.1.0" "@scarf/scarf": "npm:1.4.0" "@standard-schema/spec": "npm:1.0.0" chalk: "npm:^4.1.2" @@ -1050,7 +1050,7 @@ __metadata: rbt: rbt.js rbt-esbuild: rbt-esbuild.js zod-to-proto: zod-to-proto.js - checksum: 10c0/56fd8a59ff0e451966d910cf4d1dc6d571999f478c5f259fa2446f481d710b1c5d478c149777f4fa0035aa636d374815a1cb75304df8dee5e09fce562f1b521a + checksum: 10c0/fcbe1e995fcaef42a43dd99674ac28ee98f99a24c1ff04f5c15cf93aa38432f2bb6ec944966e169e612845628d0ab05bd3684e8d82cbd02873a6fecd6ee670dd languageName: node linkType: hard diff --git a/reboot/examples/reboot-swag-store/.claude/settings.json b/reboot/examples/reboot-swag-store/.claude/settings.json new file mode 100644 index 00000000..d522b476 --- /dev/null +++ b/reboot/examples/reboot-swag-store/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "reboot-plugin": { + "source": { + "source": "github", + "repo": "reboot-dev/reboot-plugin" + } + } + }, + "enabledPlugins": { + "reboot@reboot-plugin": true + } +} diff --git a/reboot/examples/reboot-swag-store/.devcontainer/devcontainer.json b/reboot/examples/reboot-swag-store/.devcontainer/devcontainer.json new file mode 100644 index 00000000..88b10cc0 --- /dev/null +++ b/reboot/examples/reboot-swag-store/.devcontainer/devcontainer.json @@ -0,0 +1,35 @@ +// For format details, see https://aka.ms/devcontainer.json. +{ + "name": "Reboot Devcontainer", + "image": "ghcr.io/reboot-dev/reboot-devcontainer", + "remoteUser": "dev", + + "customizations": { + "vscode": { + // The following VS Code extensions are nice to have when developing on + // this Reboot application. + "extensions": [ + // Python support. + "ms-python.python", + // Static type checking for Python. + "ms-python.mypy-type-checker", + // YAPF code formatter for Python. + "eeyore.yapf", + // Automatic import sorting for Python. + "ms-python.isort", + // Protocol buffer support. + "zxh404.vscode-proto3", + // Prettier code formatter. + "esbenp.prettier-vscode" + ] + } + }, + + // Ports from the devcontainer made available on your local machine. + "forwardPorts": [ + // The HTTP port exposed by default in examples. + 3000, + // The gRPC backend port exposed by default in examples. + 9991 + ] +} diff --git a/reboot/examples/reboot-swag-store/.dockerignore b/reboot/examples/reboot-swag-store/.dockerignore new file mode 100644 index 00000000..a8e77220 --- /dev/null +++ b/reboot/examples/reboot-swag-store/.dockerignore @@ -0,0 +1,35 @@ +# Keep the build context small and prevent local secrets / build +# artifacts from being uploaded to the Docker daemon. + +# Secrets. NEVER ship into a layer. +.env + +# Local Python environment (rebuilt in the image by `uv sync`). +.venv/ + +# Reboot runtime state (per-machine). +.rbt/ + +# Python / test caches. +__pycache__/ +*.py[cod] +.mypy_cache/ +.pytest_cache/ + +# Generated Reboot code (regenerated in the image by `rbt generate`). +backend/api/ +web/api/ + +# Node + Vite outputs (rebuilt in the image by the `web-build` stage). +web/node_modules/ +web/dist/ + +# Tooling noise. +.claude/ +.vscode/ +.idea/ +*.swp + +# Docker files themselves. +Dockerfile +.dockerignore diff --git a/reboot/examples/reboot-swag-store/.env.example b/reboot/examples/reboot-swag-store/.env.example new file mode 100644 index 00000000..bbcd7e7b --- /dev/null +++ b/reboot/examples/reboot-swag-store/.env.example @@ -0,0 +1,13 @@ +# The backend reads these env vars at request time. In Reboot +# Cloud, set them once with `rbt cloud secret set +# --application-name=reboot-swag-store KEY=VALUE`; Cloud injects each +# secret into the application's environment under its given +# name, so the same code works in both environments. + +# Printful API token. +# Get yours at https://www.printful.com/dashboard/developer/api-keys +PRINTFUL_API_TOKEN=your_printful_api_token_here + +# Admin key for generating coupon codes. +# Callers must pass this as a bearer token to `generate_codes`. +STORE_ADMIN_KEY=pick-any-secret-string diff --git a/reboot/examples/reboot-swag-store/.github/workflows/test.yml b/reboot/examples/reboot-swag-store/.github/workflows/test.yml new file mode 100644 index 00000000..007291c8 --- /dev/null +++ b/reboot/examples/reboot-swag-store/.github/workflows/test.yml @@ -0,0 +1,47 @@ +name: Test + +on: + push: + # Run CI (only) on our main branch to show that it is always in a green + # state. Don't run it on other branches; if a developer cares about one + # of the non-main branches they'll run tests manually or open a PR. + branches: + - "main" + pull_request: + # Run CI on pull requests. Passing checks will be required to merge. + branches: + - "**" + merge_group: + # Required if we want to use GitHub's Merge Queue feature. See: + # https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue#triggering-merge-group-checks-with-github-actions + +concurrency: + # Have at most one of these workflows running per branch, cancelling older + # runs that haven't completed yet when they become obsolete. + # + # When pushing new commits to a PR, each one of those commits triggers a new + # workflow run that would normally run to completion, even when subsequent + # pushes to the PR make their result obsolete. This consumes resources for no + # benefit. We override that default behavior to save resources, cancelling any + # older still-running workflows when a new workflow starts + # + # See documentation about the `github` context variables here: + # https://docs.github.com/en/actions/learn-github-actions/contexts#github-context + group: ${{ github.workflow }}-${{ github.ref }} + # Do not cancel runs on the main branch. On the main branch we want every + # commit to be tested. + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + backend-test: + name: Backend test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: pytest (in Dev Container) + uses: devcontainers/ci@v0.3 + with: + runCmd: | + .tests/test.sh diff --git a/reboot/examples/reboot-swag-store/.gitignore b/reboot/examples/reboot-swag-store/.gitignore new file mode 100644 index 00000000..bfb05dd6 --- /dev/null +++ b/reboot/examples/reboot-swag-store/.gitignore @@ -0,0 +1,35 @@ +# Python virtual environment +.venv/ + +# Generated API code (from rbt generate) +backend/api/ + +# Reboot runtime state +.rbt/ + +# Byte-compiled files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +dist/ +build/ +*.egg-info/ + +# mypy cache +.mypy_cache/ + +# pytest cache +.pytest_cache/ + +# IDE +.idea/ +.vscode/ +*.swp + +# Claude Code local settings (per-machine) +.claude/settings.local.json + +# Local environment variables +.env diff --git a/reboot/examples/reboot-swag-store/.mergequeue/config.yml b/reboot/examples/reboot-swag-store/.mergequeue/config.yml new file mode 100644 index 00000000..6c4cfe2e --- /dev/null +++ b/reboot/examples/reboot-swag-store/.mergequeue/config.yml @@ -0,0 +1,32 @@ +version: 1.0.0 +merge_rules: + publish_status_check: true + labels: + trigger: mergequeue-ready + skip_line: "" + merge_failed: "" + skip_delete_branch: "" + update_latest: true + delete_branch: false + use_rebase: true + enable_comments: true + ci_timeout_mins: 0 + preconditions: + number_of_approvals: 1 + use_github_mergeability: true + conversation_resolution_required: true + merge_mode: + type: default + parallel_mode: null + auto_update: + enabled: false + label: "" + max_runs_for_update: 0 + merge_commit: + use_title_and_body: false + merge_strategy: + name: squash + override_labels: + squash: "" + merge: "" + rebase: mergequeue-rebase diff --git a/reboot/examples/reboot-swag-store/.python-version b/reboot/examples/reboot-swag-store/.python-version new file mode 100644 index 00000000..c8cfe395 --- /dev/null +++ b/reboot/examples/reboot-swag-store/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/reboot/examples/reboot-swag-store/.rbtrc b/reboot/examples/reboot-swag-store/.rbtrc new file mode 100644 index 00000000..be17c2d5 --- /dev/null +++ b/reboot/examples/reboot-swag-store/.rbtrc @@ -0,0 +1,79 @@ +# Find API definitions in 'api/'. + +generate api/ + +# Tell `rbt generate` where to put generated files. + +generate --python=backend/api/ + +# Generate React bindings for web apps (into "web/api/"). + +generate --react=web/api +generate --react-extensions + +# Watch if any source files are modified. + +dev run --watch=backend/\*_/_.py + +# Tell `rbt` that this is a Python application. + +dev run --python + +# Save state between restarts. + +dev run --application-name=reboot-swag-store + +# Run the application! + +dev run --application=backend/src/main.py + +# Default to HMR mode when no --config is specified. + +dev run --default-config=hmr + +# Hot Module Replacement (HMR): Vite dev server proxied through Envoy. + +# Run Vite in a separate terminal: cd web && npm run dev + +# Envoy routes "/\_\_/web/\*\*" to Vite for HMR support. + +dev run:hmr --mcp-frontend-host=http://localhost:4444 + +# Dist mode: serve pre-built artifacts from "web/dist/" (no Vite HMR). + +# Usage: uv run rbt dev run --config=dist + +# Requires: cd web && npm run build + +dev run:dist --mcp-frontend-host="" + +# When expunging, expunge that state we've saved. + +dev expunge --application-name=reboot-swag-store + +# --- Production (`rbt serve`) and Reboot Cloud deploys. --- + +# Tell `rbt serve` that this is a Python application. + +serve run --python + +# Leave TLS termination to the external load balancer; expose a + +# non-SSL port to that load balancer. + +serve run --tls=external + +# Run the application. + +serve run --application=backend/src/main.py + +# Name the application for every Cloud/serve subcommand that + +# needs it, so `rbt cloud up` and friends operate on the same + +# deployment as `rbt dev run`. + +serve run --application-name=reboot-swag-store +cloud up --application-name=reboot-swag-store +cloud down --application-name=reboot-swag-store --expunge +cloud logs --application-name=reboot-swag-store diff --git a/reboot/examples/reboot-swag-store/.tests/test.sh b/reboot/examples/reboot-swag-store/.tests/test.sh new file mode 100755 index 00000000..e6f06401 --- /dev/null +++ b/reboot/examples/reboot-swag-store/.tests/test.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +set -e # Exit if a command exits with an error. +set -x # Echo executed commands to help debug failures. + +# MacOS tests can fail due to a race in `protoc` writing files to +# disk, so now we check only occurrences of the expected lines in +# the output. See https://github.com/reboot-dev/mono/issues/3433 +check_lines_in_file() { + local expected="$1" + local actual="$2" + + while IFS= read -r line; do + if ! grep -Fxq "$line" "$actual"; then + echo "Line $line is missing in the actual output." + exit 1 + fi + done < "$expected" +} + +# Use the published Reboot pip package by default, but allow the +# test system to override it with a different value. +if [ -n "$REBOOT_WHL_FILE" ]; then + # Install the `reboot` package from the specified path + # explicitly, overwriting the version from `pyproject.toml`. + rye remove --no-sync reboot + rye remove --no-sync --dev reboot + rye add --dev reboot --absolute --path="${SANDBOX_ROOT}$REBOOT_WHL_FILE" +fi + +# Force a fresh virtualenv. A pre-existing `.venv/` (e.g., +# carried over from a pre-baked image, or copied between +# containers/host paths during the dev-container test) will +# have absolute shebangs in its console scripts (`rbt`, +# `pytest`) pointing at the path where it was originally +# created, which produces "bad interpreter: No such file or +# directory" when those scripts are executed from a different +# location. `rye sync` only regenerates entry-point scripts +# for packages it reinstalls, so it can't repair an existing +# venv whose shebangs are stale. Nuking and re-syncing here +# guarantees the venv lives at the current path. +rm -rf .venv +rye sync --no-lock + +# Don't `source .venv/bin/activate`: that script bakes in the +# venv's original creation path on its first line, which is +# wrong when the venv has been relocated (e.g., a pre-baked +# image bind-mounted at a different path in CI). The +# console-script wrappers (`rbt`, `pytest`) also have absolute +# shebangs that point at the stale path. Set `PATH` and +# `VIRTUAL_ENV` to the real location ourselves, and invoke the +# tools via `python -m`: the `python` symlink resolves the +# venv from its own location through `pyvenv.cfg`, so it +# survives relocation, and `-m` bypasses the broken script +# shebangs. +VENV_BIN="$(pwd)/.venv/bin" +export PATH="$VENV_BIN:$PATH" +export VIRTUAL_ENV="$(pwd)/.venv" +PYTHON="$VENV_BIN/python" + +# When running in a Bazel test, our `.rbtrc` file ends up in a +# very deep directory structure, which can result in "path too +# long" errors from RocksDB. Explicitly specify a shorter path. +RBT_FLAGS="--state-directory=$(mktemp -d)" + +"$PYTHON" -m reboot.cli.rbt_main $RBT_FLAGS generate + +"$PYTHON" -m pytest backend/ + +if [ -n "$EXPECTED_RBT_DEV_OUTPUT_FILE" ]; then + actual_output_file=$(mktemp) + + # `--config=dist` overrides `.rbtrc`'s default `hmr` config, + # whose `--mcp-frontend-host=http://localhost:4444` would + # have Envoy proxy `/__/web/**` to a Vite dev server. There + # is no Vite running in CI, and on the macOS executable-Envoy + # path that proxy target makes cluster init hang + # indefinitely, so `--terminate-after-health-check` never + # fires. The `dist` config sets `--mcp-frontend-host=""`, + # which skips the proxy entirely. `web/dist/` doesn't need + # to actually exist; the health check only probes gRPC and + # Envoy listeners. + "$PYTHON" -m reboot.cli.rbt_main $RBT_FLAGS dev run \ + --config=dist \ + --terminate-after-health-check \ + > "$actual_output_file" + + check_lines_in_file \ + "${SANDBOX_ROOT}$EXPECTED_RBT_DEV_OUTPUT_FILE" \ + "$actual_output_file" + + rm "$actual_output_file" +fi diff --git a/reboot/examples/reboot-swag-store/BUILD.bazel b/reboot/examples/reboot-swag-store/BUILD.bazel new file mode 100644 index 00000000..ccc52b62 --- /dev/null +++ b/reboot/examples/reboot-swag-store/BUILD.bazel @@ -0,0 +1,26 @@ +# This `BUILD.bazel` file allows a `bazel` system (configured outside of +# this repository) to use code in this directory and its subdirectories. + +filegroup( + name = "everything", + srcs = glob( + ["**/*"], + exclude = [ + # Files that may be created by activity in this directory + # (`uv sync`, `rbt dev run`, `npm install`, `npm run build`, + # etc.) but which are not part of the "source code" of this + # example. + ".claude/**/*", + ".env", + ".mypy_cache/**/*", + ".pytest_cache/**/*", + ".rbt/**/*", + ".venv/**/*", + "backend/api/**/*", + "web/api/**/*", + "web/dist/**/*", + "web/node_modules/**/*", + ], + ), + visibility = ["//visibility:public"], +) diff --git a/reboot/examples/reboot-swag-store/Dockerfile b/reboot/examples/reboot-swag-store/Dockerfile new file mode 100644 index 00000000..92c517cf --- /dev/null +++ b/reboot/examples/reboot-swag-store/Dockerfile @@ -0,0 +1,66 @@ +# syntax=docker/dockerfile:1.6 + +# --------------------------------------------------------------------------- +# Stage 1: install Python deps and generate Reboot API bindings. +# +# Produces two useful artifacts for later stages: +# - `/app/.venv` — virtualenv with `rbt` and runtime deps. +# - `/app/backend/api/` and `/app/web/api/` — generated Python and +# TypeScript bindings from `api/reboot_swag_store/v1/store.py`. +# --------------------------------------------------------------------------- +FROM ghcr.io/reboot-dev/reboot-base:1.1.0 AS backend + +WORKDIR /app + +# `uv` is the single source of truth for Python dependencies in this +# example (see `pyproject.toml` + `uv.lock`). +RUN pip install --no-cache-dir uv + +# Install locked Python deps first, separate from the source, so code +# changes don't invalidate the dep-install layer. +COPY pyproject.toml uv.lock ./ +RUN uv sync --frozen --no-install-project --no-dev + +# Put the virtualenv's binaries (including `rbt`) first on PATH so +# subsequent RUN commands and the CMD resolve them directly. +ENV PATH="/app/.venv/bin:${PATH}" + +# Generate Reboot code. Kept in its own layer so it only re-runs when +# the API definition or `.rbtrc` changes. +COPY api/ api/ +COPY .rbtrc .rbtrc +RUN rbt generate + +# --------------------------------------------------------------------------- +# Stage 2: build the web UIs. +# +# `rbt serve` serves MCP UI resources from `web/dist/ui//index.html` +# (see `reboot.mcp.ui.ui_html`), so we need pre-built artifacts in the +# final image. The web UIs import the generated TS bindings at +# `web/api/`, which we pull from the backend stage above. +# --------------------------------------------------------------------------- +FROM node:20-alpine AS web-build + +WORKDIR /app/web + +COPY web/package.json web/package-lock.json ./ +RUN npm ci + +COPY web/ ./ +# Overlay the freshly-generated TS bindings (ignored by `.gitignore` +# and `.dockerignore`, so the `COPY web/ ./` above doesn't include +# them). +COPY --from=backend /app/web/api ./api + +RUN npm run build + +# --------------------------------------------------------------------------- +# Stage 3: runtime. Extends the backend stage so `.venv`, `backend/api/`, +# and `.rbtrc` are already in place. +# --------------------------------------------------------------------------- +FROM backend AS runtime + +COPY backend/src/ backend/src/ +COPY --from=web-build /app/web/dist/ web/dist/ + +CMD ["rbt", "serve", "run"] diff --git a/reboot/examples/reboot-swag-store/README.md b/reboot/examples/reboot-swag-store/README.md new file mode 100644 index 00000000..0532ba29 --- /dev/null +++ b/reboot/examples/reboot-swag-store/README.md @@ -0,0 +1,319 @@ +# Reboot Store + +An MCP shopping experience for Reboot-branded swag, built with +[Reboot](https://docs.reboot.dev/) and backed by +[Printful](https://www.printful.com/) for real product data and +on-demand fulfillment. + +For the impatient: + +1. [Set up your environment](#set-up-your-environment) +2. [Configure Printful and secrets](#configure-printful-and-secrets) +3. [Run the application](#run-the-application) +4. [Generate a coupon code](#generate-a-coupon-code) +5. [Connect an MCP client](#connect-an-mcp-client) +6. [Running the tests](#running-the-tests) +7. [Deploy to Reboot Cloud](#deploy-to-reboot-cloud) + +## Overview + +The Reboot Store is an AI Chat App (MCP App) that lets users browse +products, manage a shopping cart, and check out — all through an AI +chat interface. It exposes three interactive UIs: + +- **Store** — a product grid showing the products the model + picked for the current request, with color and size selection + per product. +- **Cart** — a cart view with item management, totals, a shipping + address form, and a coupon code field. +- **Order Confirmation** — a receipt view after a successful + purchase. + +The backend uses Reboot's durable state to persist carts, coupon +codes, and orders across restarts. Product data is fetched live +from Printful on each `list_products` call. Anonymous auth +generates a unique user identity per MCP session. + +Filtering is **AI-driven**, using a *data tool + widget tool* +split. `list_products` returns the full catalog with no server- +side filter; the client model reads it, picks the IDs whose +name or description match the user's intent, and then opens +`browse_store` with those `product_ids`. The widget filters +the cached catalog client-side to that subset. This pushes +natural-language understanding to the model — no regex, +stopwords, or synonym lists in the backend — and copes with any +phrasing ("show me hats", "what cold-weather gear do you have", +"the navy ones") that the model can interpret. + +> ⚠️ **Printful places real orders.** When a checkout completes, +> this app calls the Printful `POST /orders` endpoint. Use a +> Printful store in **draft mode** (or a sandbox account) while +> developing so no merch actually ships. + +### Project structure + +``` +reboot-swag-store/ +├── api/reboot_swag_store/v1/ +│ └── store.py # State models and API definition +├── backend/src/ +│ ├── main.py # Application entrypoint +│ ├── constants.py # Shared constants +│ ├── printful.py # Printful API client +│ └── servicers/ +│ └── store.py # Servicer implementations +└── web/ + ├── index.css # Shared theme + └── ui/ + ├── store/ # Store browse UI + ├── cart/ # Shopping cart UI + └── confirmation/ # Order confirmation UI +``` + +### State model + +- **User** — per-MCP-session entry point. Methods to browse the + store (`browse_store` UI, `list_products`) and create a cart + (`create_cart`). All auto-exposed as MCP tools. +- **Cart** — per-user cart. Methods to `add_item`, `remove_item`, + `get_cart`, `show_cart` (UI), and `checkout`. +- **CouponBook** — singleton (id `coupon-book`) that holds a pool + of single-use coupon codes. Exposes `generate_codes` (admin + only), `validate_code`, and `redeem_code`. On first startup, 20 + codes are generated automatically. +- **Order** — created on checkout. Stores receipt details and + runs a `fulfill` workflow that places the Printful order + at-least-once. + +## Set up your environment + +### Prerequisites + +- Python >= 3.10 +- [uv](https://docs.astral.sh/uv/) (Python package manager) +- Node.js and npm +- A [Printful](https://www.printful.com/) account with at least + one sync product + +### Install dependencies + +From the `reboot-swag-store/` directory: + +```sh +uv sync +``` + +```sh +cd web && npm install && cd .. +``` + +### Generate code + +```sh +uv run rbt generate +``` + +## Configure Printful and secrets + +1. Create a Printful store (use **draft mode** during + development). +2. Add at least one sync product so `list_products` has + something to return. +3. Create a Printful API token at + . +4. Copy `.env.example` to `.env` and fill in the values: + + ```sh + cp .env.example .env + ``` + + ```sh + # .env + PRINTFUL_API_TOKEN=your_printful_api_token_here + STORE_ADMIN_KEY=pick-any-secret-string + ``` + + The backend reads these env vars at request time. In + Reboot Cloud, the same names are injected via `rbt cloud + secret set` (see "Deploy to Reboot Cloud" below), so the + same code works in both environments. + + `STORE_ADMIN_KEY` is an app-local secret used to gate the + `generate_codes` endpoint. Pick any value; you'll pass it + as a bearer token when you generate coupon codes (see + below). + +## Run the application + +You need two terminals, both starting from the `reboot-swag-store/` +directory. + +**Terminal 1 — backend:** + +```sh +uv run rbt dev run +``` + +This starts the Reboot backend and watches for file changes. See +`.rbtrc` for the full set of flags. + +**Terminal 2 — frontend (HMR):** + +```sh +cd web && npm run dev +``` + +This starts the Vite dev server with hot module replacement. The +backend proxies UI requests to Vite via Envoy. + +## Generate a coupon code + +Checkout requires a valid coupon code, which makes the order +free. Twenty codes are generated the first time the backend +starts; you can view them (and mint more) via the Reboot inspect +dashboard and a short `curl` call. + +### View existing codes + +Open the inspect dashboard at +. In the **States** tab: + +1. Select the `reboot_swag_store.v1.CouponBook` state type. +2. Click the `coupon-book` instance. +3. The `codes` field lists every unused code. + +Pick any code from that list to use at checkout. + +### Mint new codes + +`CouponBook.generate_codes` is not exposed as an MCP tool — it's +an admin operation. Call it over HTTP with your admin key as a +bearer token: + +```sh +curl -XPOST http://localhost:9991/reboot_swag_store.v1.CouponBookMethods/GenerateCodes \ + -H "x-reboot-state-ref: reboot_swag_store.v1.CouponBook:coupon-book" \ + -H "Authorization: Bearer $STORE_ADMIN_KEY" \ + -d '{}' +``` + +The response contains 20 new six-digit codes. They're also +appended to the `CouponBook` state visible in the inspect +dashboard. + +## Connect an MCP client + +The app exposes an MCP server at `http://localhost:9991/mcp`. +You can connect any MCP-compatible client. + +### Claude Desktop / VS Code / other clients + +Add the following to your MCP client configuration (also +available in `mcp_servers.json`): + +```json +{ + "mcpServers": { + "reboot-swag-store": { + "url": "http://localhost:9991/mcp", + "useOAuth": true + } + } +} +``` + +### Try it out + +Once connected, try these prompts (substitute one of your coupon +codes for `123456`): + +- "Show me what's in the store." +- "Show me only the hats." +- "Create a cart and add the Reboot Hoodie in size L." +- "Show me my cart." +- "Check out. Ship to Jane Doe, 123 Main St, Seattle WA 98101, + US, jane@example.com. Use coupon code 123456." +- "Show me the confirmation for my order." + +## Running the tests + +```sh +cd backend && uv run pytest +``` + +The suite has two files: + +- `backend/tests/printful_helpers_test.py` — pure-function + tests for the variant-parsing helper. Fast, no Reboot + runtime. +- `backend/tests/store_servicer_test.py` — integration tests + that spin up an in-process Reboot, set the admin-key env + var to a known test value, and swap `OrderServicer.fulfill` + for a no-op so tests don't hit Printful. Covers cart + add/remove/checkout, `CartEmpty` / `InvalidCoupon` aborts, + coupon single-use, the admin-gated `generate_codes` + endpoint, and the `list_products` "full catalog, no server- + side filter" contract (using a stubbed `fetch_products`). + +## Deploy to Reboot Cloud + +The repo includes a `Dockerfile` and `rbt serve` / `rbt cloud` +configuration so the same code you ran locally can be deployed +to [Reboot Cloud](https://cloud.reboot.dev/). You'll need an +invitation + API key — sign up at . + +### 1. Upload secrets + +The backend reads `PRINTFUL_API_TOKEN` and `STORE_ADMIN_KEY` +from its environment. In the cloud, set them once with `rbt +cloud secret set`; Cloud injects each one into the application's +environment under its given name: + +```sh +uv run rbt cloud secret set \ + --api-key=YOUR_API_KEY \ + --application-name=reboot-swag-store \ + PRINTFUL_API_TOKEN=YOUR_PRINTFUL_TOKEN \ + STORE_ADMIN_KEY=YOUR_ADMIN_KEY +``` + +### 2. Bring the application up + +`rbt cloud up` builds the `Dockerfile` in this directory and +deploys the resulting image: + +```sh +uv run rbt cloud up --api-key=YOUR_API_KEY +``` + +The flags `--name=reboot-swag-store`, `--dockerfile=Dockerfile`, and +friends come from `.rbtrc`. The output prints the URL of the +deployed application; point your MCP client at `/mcp`. + +### 3. Inspect, tail logs, or tear down + +```sh +# The inspect dashboard works against Cloud too: +# /__/inspect + +uv run rbt cloud logs --api-key=YOUR_API_KEY +uv run rbt cloud down --api-key=YOUR_API_KEY # keep state +uv run rbt cloud down --api-key=YOUR_API_KEY --expunge # wipe +``` + +Fresh deploys will regenerate the initial 20 coupon codes on +first start; existing deploys persist codes (and carts, orders, +etc.) across `rbt cloud up` redeploys until you pass +`--expunge`. + +## Resetting state + +To wipe all local persisted state (carts, coupons, orders, +etc.): + +```sh +uv run rbt dev expunge --name=reboot-swag-store +``` + +The next `rbt dev run` will regenerate the initial 20 coupon +codes. diff --git a/reboot/examples/reboot-swag-store/api/reboot_swag_store/v1/store.py b/reboot/examples/reboot-swag-store/api/reboot_swag_store/v1/store.py new file mode 100644 index 00000000..1838e86e --- /dev/null +++ b/reboot/examples/reboot-swag-store/api/reboot_swag_store/v1/store.py @@ -0,0 +1,374 @@ +from reboot.api import ( + API, + UI, + Field, + Methods, + Model, + Reader, + Tool, + Transaction, + Type, + Workflow, + Writer, +) + +# -- Shared models. -- + + +class Variant(Model): + """A size/color variant of a product.""" + id: str = Field(tag=1) + size: str = Field(tag=2) + color: str = Field(tag=3) + price_cents: int = Field(tag=4) + image_url: str = Field(tag=5, default="") + + +class Product(Model): + """A product available in the store.""" + id: str = Field(tag=1) + name: str = Field(tag=2) + description: str = Field(tag=3) + price_cents: int = Field(tag=4) + # Tag 5 was previously `category`; left vacant to avoid + # rebinding the wire tag if removed-and-re-added. + image_url: str = Field(tag=6, default="") + variants: list[Variant] = Field(tag=7, default_factory=list) + + +class CartItem(Model): + """An item in a shopping cart.""" + product_id: str = Field(tag=1) + name: str = Field(tag=2) + price_cents: int = Field(tag=3) + quantity: int = Field(tag=4) + image_url: str = Field(tag=5, default="") + variant_id: str = Field(tag=6, default="") + size: str = Field(tag=7, default="") + + +class OrderItem(Model): + """An item in a completed order.""" + product_id: str = Field(tag=1) + name: str = Field(tag=2) + price_cents: int = Field(tag=3) + quantity: int = Field(tag=4) + size: str = Field(tag=5, default="") + + +class ShippingAddress(Model): + """Shipping address for order fulfillment.""" + name: str = Field(tag=1) + address1: str = Field(tag=2) + city: str = Field(tag=3) + state_code: str = Field(tag=4) + zip_code: str = Field(tag=5) + country_code: str = Field(tag=6) + address2: str = Field(tag=7) + email: str = Field(tag=8) + + +# -- User models. -- + + +class UserState(Model): + pass + + +class CreateCartResponse(Model): + cart_id: str = Field(tag=1) + + +class ListProductsResponse(Model): + products: list[Product] = Field(tag=1, default_factory=list) + + +class BrowseStoreRequest(Model): + """Subset of products to render in the storefront UI. + The model picks `product_ids` after reading the catalog + from `list_products`. Empty list shows every product.""" + product_ids: list[str] = Field(tag=1, default_factory=list) + + +# -- Cart models. -- + + +class CartState(Model): + items: list[CartItem] = Field(tag=1, default_factory=list) + # OAuth user-id of the session that created the cart; + # only that caller can read or mutate it. + owner_id: str = Field(tag=2, default="") + + +class CartCreateRequest(Model): + owner_id: str = Field(tag=1) + + +class AddItemRequest(Model): + product_id: str = Field(tag=1) + quantity: int = Field(tag=2) + variant_id: str = Field(tag=3) + name: str = Field(tag=4) + price_cents: int = Field(tag=5) + image_url: str = Field(tag=6) + size: str = Field(tag=7) + + +class RemoveItemRequest(Model): + product_id: str = Field(tag=1) + + +class GetItemsResponse(Model): + items: list[CartItem] = Field(tag=1, default_factory=list) + + +class ValidateCouponRequest(Model): + coupon_code: str = Field(tag=1) + + +class ValidateCouponResponse(Model): + valid: bool = Field(tag=1) + + +class CheckoutRequest(Model): + shipping_address: ShippingAddress = Field(tag=1) + coupon_code: str = Field(tag=2) + + +class CheckoutResponse(Model): + order_id: str = Field(tag=1) + + +class CartEmpty(Model): + """Checkout was attempted on an empty cart.""" + + +class InvalidCoupon(Model): + """Checkout was attempted with a coupon code that is + unknown or has already been redeemed.""" + coupon_code: str = Field(tag=1) + + +# -- CouponBook models. -- + + +class CouponBookState(Model): + codes: list[str] = Field(tag=1, default_factory=list) + + +class GenerateCodesResponse(Model): + codes: list[str] = Field(tag=1, default_factory=list) + + +class RedeemCodeRequest(Model): + code: str = Field(tag=1) + + +class RedeemCodeResponse(Model): + success: bool = Field(tag=1) + + +# -- Order models. -- + + +class OrderState(Model): + order_id: str = Field(tag=1, default="") + items: list[OrderItem] = Field(tag=2, default_factory=list) + subtotal_cents: int = Field(tag=3, default=0) + shipping_cents: int = Field(tag=4, default=0) + total_cents: int = Field(tag=5, default=0) + shipping_name: str = Field(tag=6, default="") + created_at: str = Field(tag=7, default="") + # OAuth user-id of the session that placed the order; + # only that caller can read it back via `get_details` or + # `show_confirmation`. + owner_id: str = Field(tag=8, default="") + + +class CreateOrderRequest(Model): + order_id: str = Field(tag=1) + items: list[OrderItem] = Field(tag=2, default_factory=list) + subtotal_cents: int = Field(tag=3) + shipping_cents: int = Field(tag=4) + total_cents: int = Field(tag=5) + shipping_name: str = Field(tag=6) + created_at: str = Field(tag=7) + owner_id: str = Field(tag=8) + + +class FulfillRequest(Model): + shipping_address: ShippingAddress = Field(tag=1) + variant_ids: list[str] = Field(tag=2, default_factory=list) + quantities: list[int] = Field(tag=3, default_factory=list) + + +class GetDetailsResponse(Model): + order_id: str = Field(tag=1) + items: list[OrderItem] = Field(tag=2, default_factory=list) + subtotal_cents: int = Field(tag=3) + shipping_cents: int = Field(tag=4) + total_cents: int = Field(tag=5) + shipping_name: str = Field(tag=6) + created_at: str = Field(tag=7) + + +api = API( + User=Type( + state=UserState, + methods=Methods( + browse_store=UI( + request=BrowseStoreRequest, + path="web/ui/store", + title="Reboot Store", + description="Open a visual storefront for " + "the Reboot swag store. To honor a user " + "filter (e.g. 'show me hats'), first call " + "`list_products` to read the catalog, pick " + "the IDs whose name or description matches " + "the user's intent, then call this with " + "those IDs in `product_ids`. Pass an empty " + "list to show every product.", + mcp=Tool(), + ), + list_products=Reader( + request=None, + response=ListProductsResponse, + description="Return the full product " + "catalog. Call this before `browse_store` " + "whenever the user wants to see, filter, " + "or shop for specific items, so you can " + "pick the IDs of products that match the " + "user's request and pass them to " + "`browse_store`.", + mcp=Tool(), + ), + create_cart=Transaction( + request=None, + response=CreateCartResponse, + description="Create a new shopping cart. " + "Returns the cart ID. Call this before " + "adding items.", + mcp=Tool(), + ), + ), + ), + Cart=Type( + state=CartState, + methods=Methods( + create=Writer( + request=CartCreateRequest, + response=None, + factory=True, + mcp=None, + ), + add_item=Writer( + request=AddItemRequest, + response=None, + description="Add a product to the cart. " + "Requires product_id, variant_id, name, " + "price_cents, image_url, and size. " + "Increments quantity if the same " + "product+variant is already in the cart.", + mcp=Tool(), + ), + remove_item=Writer( + request=RemoveItemRequest, + response=None, + description="Remove a product from the " + "cart by product ID.", + mcp=Tool(), + ), + get_cart=Reader( + request=None, + response=GetItemsResponse, + description="Get all items currently in " + "the cart.", + mcp=Tool(), + ), + show_cart=UI( + request=None, + path="web/ui/cart", + title="Shopping Cart", + description="View the shopping cart with " + "items, totals, and checkout form.", + mcp=Tool(), + ), + checkout=Transaction( + request=CheckoutRequest, + response=CheckoutResponse, + errors=[CartEmpty, InvalidCoupon], + description="Check out the cart. Provide " + "a shipping address and a valid coupon " + "code. Creates an order and empties the " + "cart. Returns the order ID.", + mcp=Tool(), + ), + ), + ), + CouponBook=Type( + state=CouponBookState, + methods=Methods( + create=Writer( + request=None, + response=None, + factory=True, + mcp=None, + ), + generate_codes=Writer( + request=None, + response=GenerateCodesResponse, + description="Generate 20 single-use " + "coupon codes. Admin only.", + mcp=None, + ), + validate_code=Reader( + request=ValidateCouponRequest, + response=ValidateCouponResponse, + description="Check whether a coupon " + "code is valid (exists and unused).", + mcp=None, + ), + redeem_code=Writer( + request=RedeemCodeRequest, + response=RedeemCodeResponse, + description="Redeem a coupon code, " + "removing it from the available pool.", + mcp=None, + ), + ), + ), + Order=Type( + state=OrderState, + methods=Methods( + create=Writer( + request=CreateOrderRequest, + response=None, + factory=True, + mcp=None, + ), + fulfill=Workflow( + request=FulfillRequest, + response=None, + description="Fulfill the order via " + "Printful. Called automatically after " + "checkout.", + mcp=None, + ), + show_confirmation=UI( + request=None, + path="web/ui/confirmation", + title="Order Confirmation", + description="View the order confirmation " + "with details and receipt.", + mcp=Tool(), + ), + get_details=Reader( + request=None, + response=GetDetailsResponse, + description="Get the details of a " + "completed order.", + mcp=Tool(), + ), + ), + ), +) diff --git a/reboot/examples/reboot-swag-store/backend/.pytest.ini b/reboot/examples/reboot-swag-store/backend/.pytest.ini new file mode 100644 index 00000000..8e918783 --- /dev/null +++ b/reboot/examples/reboot-swag-store/backend/.pytest.ini @@ -0,0 +1,5 @@ +[pytest] +pythonpath= + src/ + ../api/ + api/ diff --git a/reboot/examples/reboot-swag-store/backend/src/constants.py b/reboot/examples/reboot-swag-store/backend/src/constants.py new file mode 100644 index 00000000..ba3b63ca --- /dev/null +++ b/reboot/examples/reboot-swag-store/backend/src/constants.py @@ -0,0 +1,5 @@ +# Singleton state-id for the application's `CouponBook`. The +# app stores all coupon codes in a single instance addressed by +# this id, created once from `main.initialize` and referenced +# during checkout. +COUPON_BOOK_ID = "coupon-book" diff --git a/reboot/examples/reboot-swag-store/backend/src/main.py b/reboot/examples/reboot-swag-store/backend/src/main.py new file mode 100644 index 00000000..dad9770f --- /dev/null +++ b/reboot/examples/reboot-swag-store/backend/src/main.py @@ -0,0 +1,54 @@ +import asyncio +import logging +from constants import COUPON_BOOK_ID +from dotenv import load_dotenv +from reboot.aio.applications import Application +from reboot.aio.auth.oauth_providers import ( + Development, + OAuthProviderByEnvironment, +) +from reboot_swag_store.v1.store_rbt import CouponBook +from servicers.store import ( + CartServicer, + CouponBookServicer, + OrderServicer, + UserServicer, +) + +load_dotenv() + +logging.basicConfig( + level=logging.INFO, + format=("%(asctime)s - %(name)s - %(levelname)s" + " - %(message)s"), +) + + +async def initialize(context) -> None: + """Ensure the coupon book exists (creates initial + codes on first run).""" + await CouponBook.create(context, COUPON_BOOK_ID) + + +async def main() -> None: + application = Application( + servicers=[ + UserServicer, + CouponBookServicer, + CartServicer, + OrderServicer, + ], + oauth=OAuthProviderByEnvironment( + dev=Development(), + # TODO: set a real provider (e.g. `Google(...)`) before + # production; `prod=None` makes a production deployment fail + # to start until one is chosen. + prod=None, + ), + initialize=initialize, + ) + await application.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/reboot/examples/reboot-swag-store/backend/src/printful.py b/reboot/examples/reboot-swag-store/backend/src/printful.py new file mode 100644 index 00000000..a5f88741 --- /dev/null +++ b/reboot/examples/reboot-swag-store/backend/src/printful.py @@ -0,0 +1,330 @@ +"""Printful API client for fetching sync products and +creating orders.""" + +import asyncio +import httpx +import logging +import os +import time +from reboot_swag_store.v1.store import Product, ShippingAddress, Variant + +logger = logging.getLogger(__name__) + +PRINTFUL_API_URL = "https://api.printful.com" + +# Environment variable that holds the Printful API token. +# Locally read from `.env` (see `.env.example`). +PRINTFUL_API_TOKEN_ENV = "PRINTFUL_API_TOKEN" + +# Process-local TTL cache for `fetch_products`. Printful's +# rate limits are tight (429s land quickly) and the catalog +# changes rarely, so we serve repeated reads from memory and +# refetch at most every `_PRODUCTS_TTL_SECONDS`. The lock +# coalesces concurrent first-fetchers so we make at most one +# upstream request per cold cache. +_PRODUCTS_TTL_SECONDS = 60.0 +_products_cache: tuple[float, list[Product]] | None = None +_products_lock = asyncio.Lock() + + +def _get_token() -> str: + """Read the Printful API token from the environment.""" + value = os.environ.get(PRINTFUL_API_TOKEN_ENV) + if not value: + raise RuntimeError( + f"`{PRINTFUL_API_TOKEN_ENV}` is not set. Locally," + " copy `.env.example` to `.env` and fill it in." + ) + return value + + +def _extract_size_color( + sync_variant: dict, +) -> tuple[str, str]: + """Extract (size, color) from a Printful sync variant. + + Prefers the structured fields on `product` (the catalog + variant) when present, and falls back to splitting the + sync variant name on "/". The name fallback handles + three shapes: + - "Product / Color / Size" -> color, size + - "Product / Size" -> size if a known size + - "Product / Color" -> color otherwise + """ + catalog = sync_variant.get("product", {}) or {} + size = (catalog.get("size") or "").strip() + color = (catalog.get("color") or "").strip() + if size or color: + return size, color + + parts = [p.strip() for p in sync_variant.get("name", "").split("/")] + if len(parts) >= 3: + return parts[2], parts[1] + if len(parts) == 2: + label = parts[1] + if label.upper() in _KNOWN_SIZES: + return label, "" + return "", label + return "", "" + + +_KNOWN_SIZES = frozenset( + [ + "XS", + "S", + "M", + "L", + "XL", + "2XL", + "3XL", + "4XL", + "5XL", + "ONE SIZE", + ] +) + + +async def fetch_products() -> list[Product]: + """Fetch all sync products from Printful and convert + them to the store's `Product` model. + + Results are memoized for `_PRODUCTS_TTL_SECONDS` to + keep us under Printful's rate limit; concurrent callers + that arrive while the cache is cold share a single + upstream fetch via `_products_lock`. + + Printful's v1 API requires a separate detail call per + product to get variants and pricing, so we fan out the + detail requests concurrently with `asyncio.gather` to + keep page load snappy. + """ + global _products_cache + now = time.monotonic() + if _products_cache is not None: + cached_at, cached = _products_cache + if now - cached_at < _PRODUCTS_TTL_SECONDS: + return cached + + async with _products_lock: + # Re-check inside the lock: another coroutine may + # have populated the cache while we were waiting. + now = time.monotonic() + if _products_cache is not None: + cached_at, cached = _products_cache + if now - cached_at < _PRODUCTS_TTL_SECONDS: + return cached + products = await _fetch_products_uncached() + _products_cache = (time.monotonic(), products) + return products + + +async def _fetch_products_uncached() -> list[Product]: + token = _get_token() + headers = {"Authorization": f"Bearer {token}"} + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get( + f"{PRINTFUL_API_URL}/store/products", + headers=headers, + ) + response.raise_for_status() + data = response.json() + + summaries = [ + (item, item.get("sync_product", item)) + for item in data.get("result", []) + ] + details = await asyncio.gather( + *[ + _fetch_product_detail( + str(sync.get("id", "")), headers, client + ) for _, sync in summaries + ] + ) + + products: list[Product] = [] + for (_, sync_product), detail in zip(summaries, details): + product_id = str(sync_product.get("id", "")) + name = sync_product.get("name", "Unnamed Product") + thumbnail_url = sync_product.get("thumbnail_url", "") + products.append( + Product( + id=product_id, + name=name, + description=detail.get("description", name), + price_cents=detail.get("price_cents", 0), + image_url=thumbnail_url, + variants=detail.get("variants", []), + ) + ) + + logger.info("Fetched %d products from Printful.", len(products)) + return products + + +async def _fetch_product_detail( + product_id: str, + headers: dict[str, str], + client: httpx.AsyncClient, +) -> dict: + """Fetch detail for a single sync product, returning + a dict with `description`, `price_cents`, and + `variants`.""" + response = await client.get( + f"{PRINTFUL_API_URL}/store/products/{product_id}", + headers=headers, + ) + response.raise_for_status() + data = response.json() + + result = data.get("result", {}) + sync_product = result.get("sync_product", {}) + sync_variants = result.get("sync_variants", []) + + # Use the first variant's retail price as the + # product price. + price_cents = 0 + if sync_variants: + retail = sync_variants[0].get("retail_price") + if retail: + price_cents = int(float(retail) * 100) + + # Printful doesn't have a description field on sync + # products, so we build one from the product name. + product_name = sync_product.get("name", "") + product_type = "" + + # Get the underlying Printful catalog product info. + product_info = ( + sync_variants[0].get("product", {}) if sync_variants else {} + ) + if product_info: + product_type = product_info.get("name", "") + + description = product_name + if product_type and product_type != product_name: + description = (f"{product_name} - {product_type}") + + # Build variants list. + variants: list[Variant] = [] + for sv in sync_variants: + variant_id = str(sv.get("id", "")) + size, color = _extract_size_color(sv) + + sv_price = sv.get("retail_price") + sv_price_cents = ( + int(float(sv_price) * 100) if sv_price else price_cents + ) + + # Get the preview image for this variant. + variant_image = "" + for file_info in sv.get("files", []): + if file_info.get("type") == "preview": + variant_image = file_info.get( + "preview_url", + file_info.get("thumbnail_url", ""), + ) + break + if not variant_image: + variant_image = sv.get("product", {}).get("image", "") + + variants.append( + Variant( + id=variant_id, + size=size, + color=color, + price_cents=sv_price_cents, + image_url=variant_image, + ) + ) + + return { + "description": description, + "price_cents": price_cents, + "variants": variants, + } + + +async def create_order( + items: list[dict], + shipping: ShippingAddress, + external_id: str = "", +) -> dict: + """Create a Printful order. + + `items` should be a list of dicts with keys + `variant_id` (sync variant ID) and `quantity`. + + `external_id` is used by Printful to deduplicate + retried requests. + + Returns the Printful order response dict. + """ + token = _get_token() + headers = {"Authorization": f"Bearer {token}"} + + order_items = [] + for item in items: + order_items.append( + { + "sync_variant_id": int(item["variant_id"]), + "quantity": item["quantity"], + } + ) + + recipient = { + "name": shipping.name, + "address1": shipping.address1, + "city": shipping.city, + "state_code": shipping.state_code, + "zip": shipping.zip_code, + "country_code": shipping.country_code, + } + if shipping.address2: + recipient["address2"] = shipping.address2 + if shipping.email: + recipient["email"] = shipping.email + + payload = { + "recipient": recipient, + "items": order_items, + } + if external_id: + payload["external_id"] = (external_id.replace("-", "")) + + logger.info("Printful order payload: %s", payload) + + async with httpx.AsyncClient() as client: + response = await client.post( + f"{PRINTFUL_API_URL}/orders", + headers=headers, + json=payload, + timeout=60.0, + ) + if response.status_code >= 400: + body = response.json() + error_code = (body.get("error", {}).get("api_error_code", "")) + # OR-13: "Order with this External ID + # already exists" — treat as success + # (idempotent retry). + if error_code == "OR-13": + logger.info( + "Printful order already exists " + "(external_id=%s), treating as " + "success.", + external_id, + ) + return body.get("result", {}) + logger.error( + "Printful order failed (%d): %s", + response.status_code, + response.text, + ) + response.raise_for_status() + data = response.json() + + logger.info( + "Created Printful order: %s", + data.get("result", {}).get("id"), + ) + return data.get("result", {}) diff --git a/reboot/examples/reboot-swag-store/backend/src/servicers/store.py b/reboot/examples/reboot-swag-store/backend/src/servicers/store.py new file mode 100644 index 00000000..9ba80c7d --- /dev/null +++ b/reboot/examples/reboot-swag-store/backend/src/servicers/store.py @@ -0,0 +1,379 @@ +import os +import rbt.v1alpha1.errors_pb2 +import secrets +import uuid7 +from constants import COUPON_BOOK_ID +from datetime import datetime, timezone +from printful import create_order, fetch_products +from reboot.aio.auth.authorizers import allow_if, is_app_internal +from reboot.aio.contexts import ( + ReaderContext, + TransactionContext, + WorkflowContext, + WriterContext, +) +from reboot.aio.workflows import at_least_once +from reboot_swag_store.v1.store import ( + CartEmpty, + CartItem, + InvalidCoupon, + OrderItem, +) +from reboot_swag_store.v1.store_rbt import Cart, CouponBook, Order, User + +# Environment variable that gates `CouponBook.generate_codes`. +# Locally read from `.env` (see `.env.example`). +STORE_ADMIN_KEY_ENV = "STORE_ADMIN_KEY" + + +async def _caller_is_authenticated(*, context, **kwargs): + """Allow when the caller has any non-empty OAuth user-id. + Anonymous guest sessions still satisfy this (the framework + issues them a stable user-id); only fully unauthenticated + callers fail.""" + if context.auth is None or not context.auth.user_id: + return rbt.v1alpha1.errors_pb2.Unauthenticated() + return rbt.v1alpha1.errors_pb2.Ok() + + +async def _caller_is_owner(*, context, state, **kwargs): + """Allow when the caller's OAuth user-id matches the + `owner_id` recorded on this state. State that hasn't been + constructed yet (state is None) falls through to deny.""" + if context.auth is None or not context.auth.user_id: + return rbt.v1alpha1.errors_pb2.Unauthenticated() + if state is not None and context.auth.user_id == state.owner_id: + return rbt.v1alpha1.errors_pb2.Ok() + return rbt.v1alpha1.errors_pb2.PermissionDenied() + + +class UserServicer(User.Servicer): + # No explicit authorizer: `User` is auto-constructed + # `PER_USER_ID`, so the framework only routes calls whose + # caller user-id matches the state-id, which is exactly + # the security check we want here. + + async def list_products( + self, + context: ReaderContext, + ) -> User.ListProductsResponse: + """Return the full Printful catalog (cached). Filtering + happens on the client: the model calls this tool, picks + product IDs that match the user's intent, then passes + them to `browse_store` which renders the subset.""" + products = await fetch_products() + return User.ListProductsResponse( + products=products, + ) + + async def create_cart( + self, + context: TransactionContext, + ) -> User.CreateCartResponse: + """Create a new shopping cart owned by the calling + user. The user-id comes from the auth context (the + framework already enforced caller==state-id to reach + this method), and is recorded on the cart so only + this session can mutate it.""" + owner_id = context.auth.user_id if context.auth else "" + cart, _ = await Cart.create(context, owner_id=owner_id) + return User.CreateCartResponse( + cart_id=cart.state_id, + ) + + +async def _is_admin(*, context, state, request, **kwargs): + """Allow only requests bearing a valid admin key, read + from the `STORE_ADMIN_KEY` env var.""" + expected = os.environ.get(STORE_ADMIN_KEY_ENV) + token = context.caller_bearer_token + if not expected or token != expected: + return rbt.v1alpha1.errors_pb2.PermissionDenied() + return rbt.v1alpha1.errors_pb2.Ok() + + +class CouponBookServicer(CouponBook.Servicer): + + def authorizer(self): + return CouponBook.Authorizer( + # `create` runs once from `initialize`, and + # `redeem_code` is only ever called from + # `Cart.checkout` — both app-internal. + # `validate_code` is different: the cart UI calls + # it directly from the browser to give the buyer + # instant green/red feedback before they hit + # checkout, so it has to allow any authenticated + # session. + create=allow_if(any=[is_app_internal]), + generate_codes=allow_if(all=[_is_admin]), + validate_code=allow_if(any=[_caller_is_authenticated]), + redeem_code=allow_if(any=[is_app_internal]), + ) + + async def create(self, context: WriterContext) -> None: + """Generate initial batch of codes on creation.""" + self.state.codes = self._make_codes(20) + + async def generate_codes( + self, + context: WriterContext, + ) -> CouponBook.GenerateCodesResponse: + """Generate 20 six-digit coupon codes.""" + new_codes = self._make_codes(20) + self.state.codes.extend(new_codes) + return CouponBook.GenerateCodesResponse( + codes=new_codes, + ) + + @staticmethod + def _make_codes(count: int) -> list[str]: + return [ + "".join(str(secrets.randbelow(10)) + for _ in range(6)) + for _ in range(count) + ] + + async def validate_code( + self, + context: ReaderContext, + request: CouponBook.ValidateCodeRequest, + ) -> CouponBook.ValidateCodeResponse: + """Check if a code exists in the pool.""" + code = request.coupon_code.strip() + return CouponBook.ValidateCodeResponse( + valid=(code in self.state.codes), + ) + + async def redeem_code( + self, + context: WriterContext, + request: CouponBook.RedeemCodeRequest, + ) -> CouponBook.RedeemCodeResponse: + """Redeem a code, removing it from the pool.""" + code = request.code.strip() + if code in self.state.codes: + self.state.codes.remove(code) + return CouponBook.RedeemCodeResponse( + success=True, + ) + return CouponBook.RedeemCodeResponse( + success=False, + ) + + +class CartServicer(Cart.Servicer): + + def authorizer(self): + # `create` has no state yet, so we fall back to "any + # authenticated session" — the cart records its owner + # and every method afterwards locks to that owner. UI + # methods (e.g. `show_cart`) aren't listed: the + # framework authorizes them based on the underlying + # state, not via this map. + return Cart.Authorizer( + create=allow_if(any=[_caller_is_authenticated]), + add_item=allow_if(any=[_caller_is_owner]), + remove_item=allow_if(any=[_caller_is_owner]), + get_cart=allow_if(any=[_caller_is_owner]), + checkout=allow_if(any=[_caller_is_owner]), + ) + + async def create( + self, + context: WriterContext, + request: Cart.CreateRequest, + ) -> None: + self.state.owner_id = request.owner_id + + async def add_item( + self, + context: WriterContext, + request: Cart.AddItemRequest, + ) -> None: + """Add a product to the cart.""" + # Check if same product+variant already in cart. + for item in self.state.items: + if ( + item.product_id == request.product_id and + item.variant_id == request.variant_id + ): + item.quantity += max(request.quantity, 1) + return + + self.state.items.append( + CartItem( + product_id=request.product_id, + name=request.name, + price_cents=request.price_cents, + quantity=max(request.quantity, 1), + image_url=request.image_url, + variant_id=request.variant_id, + size=request.size, + ) + ) + + async def remove_item( + self, + context: WriterContext, + request: Cart.RemoveItemRequest, + ) -> None: + """Remove a product from the cart.""" + self.state.items = [ + item for item in self.state.items + if item.product_id != request.product_id + ] + + async def get_cart( + self, + context: ReaderContext, + ) -> Cart.GetCartResponse: + return Cart.GetCartResponse( + items=self.state.items, + ) + + async def checkout( + self, + context: TransactionContext, + request: Cart.CheckoutRequest, + ) -> Cart.CheckoutResponse: + """Process checkout: redeem coupon, create + order, schedule fulfillment, empty cart.""" + if not self.state.items: + raise Cart.CheckoutAborted(CartEmpty()) + + # Redeem the coupon code via CouponBook. + code = request.coupon_code.strip() + book = CouponBook.ref(COUPON_BOOK_ID) + result = await book.redeem_code( + context, + CouponBook.RedeemCodeRequest(code=code), + ) + if not result.success: + raise Cart.CheckoutAborted(InvalidCoupon(coupon_code=code)) + + subtotal_cents = sum( + item.price_cents * item.quantity for item in self.state.items + ) + # Coupon makes the order free. + total_cents = 0 + + order_id = str(uuid7.create()) + + order_items = [ + OrderItem( + product_id=item.product_id, + name=item.name, + price_cents=item.price_cents, + quantity=item.quantity, + size=item.size, + ) for item in self.state.items + ] + + variant_ids = [item.variant_id for item in self.state.items] + quantities = [item.quantity for item in self.state.items] + + order, _ = await Order.create( + context, + order_id, + Order.CreateRequest( + order_id=order_id, + items=order_items, + subtotal_cents=subtotal_cents, + shipping_cents=0, + total_cents=total_cents, + shipping_name=(request.shipping_address.name), + created_at=datetime.now(timezone.utc).isoformat(), + # Carry the cart's owner forward so the order + # is visible only to the buyer. + owner_id=self.state.owner_id, + ), + ) + + # Schedule fulfillment workflow (Printful order). + await order.schedule().fulfill( + context, + shipping_address=request.shipping_address, + variant_ids=variant_ids, + quantities=quantities, + ) + + # Empty the cart. + self.state.items = [] + + return Cart.CheckoutResponse(order_id=order_id) + + +class OrderServicer(Order.Servicer): + + def authorizer(self): + return Order.Authorizer( + # Orders are only created by `Cart.checkout` + # (app-internal). `fulfill` is the workflow the + # checkout schedules; it also runs internally. + # `show_confirmation` is a UI method, authorized + # by the framework via the underlying state. + create=allow_if(any=[is_app_internal]), + fulfill=allow_if(any=[is_app_internal]), + get_details=allow_if(any=[_caller_is_owner]), + ) + + @classmethod + async def fulfill( + cls, + context: WorkflowContext, + request: Order.FulfillRequest, + ) -> None: + """Fulfill the order via Printful.""" + items = [ + { + "variant_id": variant_id, + "quantity": quantity, + } for variant_id, quantity in zip( + request.variant_ids, + request.quantities, + ) + ] + + order_id = context.state_id + + async def _place_order(): + await create_order( + items, + request.shipping_address, + external_id=order_id, + ) + + await at_least_once( + order_id, + context, + _place_order, + ) + + async def create( + self, + context: WriterContext, + request: Order.CreateRequest, + ) -> None: + """Store order details in state.""" + self.state.order_id = request.order_id + self.state.items = list(request.items) + self.state.subtotal_cents = (request.subtotal_cents) + self.state.shipping_cents = (request.shipping_cents) + self.state.total_cents = request.total_cents + self.state.shipping_name = request.shipping_name + self.state.created_at = request.created_at + self.state.owner_id = request.owner_id + + async def get_details( + self, + context: ReaderContext, + ) -> Order.GetDetailsResponse: + return Order.GetDetailsResponse( + order_id=self.state.order_id, + items=self.state.items, + subtotal_cents=self.state.subtotal_cents, + shipping_cents=self.state.shipping_cents, + total_cents=self.state.total_cents, + shipping_name=self.state.shipping_name, + created_at=self.state.created_at, + ) diff --git a/reboot/examples/reboot-swag-store/backend/tests/printful_helpers_test.py b/reboot/examples/reboot-swag-store/backend/tests/printful_helpers_test.py new file mode 100644 index 00000000..10f4a1dc --- /dev/null +++ b/reboot/examples/reboot-swag-store/backend/tests/printful_helpers_test.py @@ -0,0 +1,61 @@ +"""Pure-function unit tests for the Printful client +helpers. No Reboot runtime or network required.""" + +import unittest +from printful import _extract_size_color + + +class TestExtractSizeColor(unittest.TestCase): + + def test_prefers_structured_fields(self) -> None: + sv = { + "name": "Ignore / Me / Completely", + "product": { + "size": "M", + "color": "Navy" + }, + } + self.assertEqual(_extract_size_color(sv), ("M", "Navy")) + + def test_name_three_parts_color_then_size(self) -> None: + sv = { + "name": "Unisex Staple T-Shirt / Black / L", + "product": {}, + } + self.assertEqual(_extract_size_color(sv), ("L", "Black")) + + def test_name_two_parts_size_only(self) -> None: + sv = { + "name": "Unisex Staple T-Shirt / XL", + "product": {}, + } + self.assertEqual(_extract_size_color(sv), ("XL", "")) + + def test_name_two_parts_color_only(self) -> None: + sv = { + "name": "Mug / Grey Melange", + "product": {}, + } + self.assertEqual( + _extract_size_color(sv), + ("", "Grey Melange"), + ) + + def test_name_two_parts_one_size_treated_as_size(self) -> None: + sv = { + "name": "Bucket Hat / One Size", + "product": {}, + } + self.assertEqual(_extract_size_color(sv), ("One Size", "")) + + def test_empty_when_unparseable(self) -> None: + sv = {"name": "Single Thing", "product": {}} + self.assertEqual(_extract_size_color(sv), ("", "")) + + def test_missing_product_field(self) -> None: + sv = {"name": "Tee / Red / M"} + self.assertEqual(_extract_size_color(sv), ("M", "Red")) + + +if __name__ == "__main__": + unittest.main() diff --git a/reboot/examples/reboot-swag-store/backend/tests/store_servicer_test.py b/reboot/examples/reboot-swag-store/backend/tests/store_servicer_test.py new file mode 100644 index 00000000..2937b195 --- /dev/null +++ b/reboot/examples/reboot-swag-store/backend/tests/store_servicer_test.py @@ -0,0 +1,357 @@ +"""Integration tests for the reboot-swag-store servicers. + +These spin up an in-process Reboot, set the admin-key env var to +a known test value so the admin authorizer accepts our test +bearer token, and swap the `OrderServicer.fulfill` workflow for +a no-op so tests don't hit the Printful API. +""" + +import os +import unittest +from constants import COUPON_BOOK_ID +from reboot.aio.aborted import Aborted +from reboot.aio.applications import Application +from reboot.aio.auth.oauth_providers import Anonymous +from reboot.aio.contexts import WorkflowContext +from reboot.aio.tests import OAuthProviderForTest, Reboot +from reboot_swag_store.v1.store import ( + CartEmpty, + InvalidCoupon, + Product, + ShippingAddress, +) +from reboot_swag_store.v1.store_rbt import Cart, CouponBook, Order, User +from servicers.store import ( + STORE_ADMIN_KEY_ENV, + CartServicer, + CouponBookServicer, + OrderServicer, + UserServicer, +) +from unittest.mock import AsyncMock, patch + +ADMIN_KEY = "test-admin-key" +SHIPPING = ShippingAddress( + name="Jane Doe", + email="jane@example.com", + address1="123 Main St", + address2="", + city="Seattle", + state_code="WA", + zip_code="98101", + country_code="US", +) + +HOODIE = dict( + product_id="hoodie-1", + variant_id="hoodie-1-l", + name="Reboot Hoodie", + price_cents=4000, + image_url="", + size="L", +) + + +class NoFulfillOrderServicer(OrderServicer): + """Override the `fulfill` workflow to skip the Printful + call during tests.""" + + @classmethod + async def fulfill( + cls, + context: WorkflowContext, + request: Order.FulfillRequest, + ) -> None: + return None + + +async def _initialize(context) -> None: + await CouponBook.create(context, COUPON_BOOK_ID) + + +class TestStoreServicers(unittest.IsolatedAsyncioTestCase): + + async def asyncSetUp(self) -> None: + # Provide the admin key the authorizer reads via the + # environment. The Printful API is never called in + # tests because `NoFulfillOrderServicer` short-circuits + # the only code path that would need a token. + self._prev_admin_key = os.environ.get(STORE_ADMIN_KEY_ENV) + os.environ[STORE_ADMIN_KEY_ENV] = ADMIN_KEY + self.rbt = Reboot() + await self.rbt.start() + await self.rbt.up( + Application( + servicers=[ + UserServicer, + CartServicer, + CouponBookServicer, + NoFulfillOrderServicer, + ], + initialize=_initialize, + oauth=OAuthProviderForTest(Anonymous()), + ) + ) + # Authenticated context for a "guest" user. With + # `useOAuth: true` enabled in `mcp_servers.json`, every + # session — including anonymous ones — gets a stable + # OAuth user-id, which our authorizers rely on. Tests + # bypass the MCP session hook that auto-constructs + # the matching `User` state, so we trigger it here. + self.user_id = "test-user" + self.context = self.rbt.create_external_context( + name=f"test-{self.id()}", + bearer_token=self.rbt.make_valid_oauth_access_token( + user_id=self.user_id, + ), + ) + await UserServicer._auto_construct( + self.context, + state_id=self.user_id, + ) + + async def asyncTearDown(self) -> None: + await self.rbt.stop() + if self._prev_admin_key is None: + os.environ.pop(STORE_ADMIN_KEY_ENV, None) + else: + os.environ[STORE_ADMIN_KEY_ENV] = self._prev_admin_key + + async def _admin_context(self): + return self.rbt.create_external_context( + name=f"admin-{self.id()}", + bearer_token=ADMIN_KEY, + ) + + async def _mint_coupon(self) -> str: + """Generate a fresh coupon code with the admin bearer + token and return one of the new codes.""" + admin_ctx = await self._admin_context() + response = await CouponBook.ref(COUPON_BOOK_ID + ).generate_codes(admin_ctx) + self.assertTrue(response.codes) + return response.codes[0] + + # ----- User.list_products ----------------------------------- + + async def test_list_products_returns_full_catalog(self) -> None: + """The backend never filters: filtering moved to the + client model, which calls `list_products` to read the + catalog, picks the IDs that match the user's intent, + then opens `browse_store` with those IDs. This locks + that contract — `list_products` returns whatever + `fetch_products` produced, in order, with no server- + side filtering.""" + catalog = [ + Product( + id="hat-1", + name="Bucket Hat", + description="Embroidered bucket hat.", + price_cents=2500, + ), + Product( + id="hoodie-1", + name="Reboot Hoodie", + description="Heavy-blend hoodie.", + price_cents=4000, + ), + Product( + id="tee-1", + name="Reboot Tee", + description="Classic merch tee.", + price_cents=2000, + ), + ] + with patch( + "servicers.store.fetch_products", + new=AsyncMock(return_value=catalog), + ): + response = await User.ref(self.user_id).list_products(self.context) + self.assertEqual( + [product.id for product in response.products], + ["hat-1", "hoodie-1", "tee-1"], + ) + + # ----- Authorization ---------------------------------------- + + async def test_different_user_cannot_touch_cart(self) -> None: + """A second guest session — with a valid OAuth token + but a different `user_id` — must not be able to read + someone else's cart. All `Cart` methods share the + `_caller_is_owner` rule, so checking one read is + enough to prove the authorizer wires through; we + don't probe the writers from the wrong user because + Reboot's effect-validation can't safely retry a + non-idempotent mutation that aborted.""" + cart, _ = await Cart.create( + self.context, + owner_id=self.user_id, + ) + await cart.add_item(self.context, quantity=1, **HOODIE) + + # A second authenticated guest session. A fresh + # `Cart.ref` is required because each weak reference + # binds to a single context. + other_context = self.rbt.create_external_context( + name=f"other-{self.id()}", + bearer_token=self.rbt.make_valid_oauth_access_token( + user_id="other-user", + ), + ) + other_cart = Cart.ref(cart.state_id) + + with self.assertRaises(Aborted): + await other_cart.get_cart(other_context) + + # The original owner still has access. + response = await cart.get_cart(self.context) + self.assertEqual(len(response.items), 1) + + # ----- Cart.add_item / get_cart / remove_item --------------- + + async def test_add_item_and_get_cart(self) -> None: + cart, _ = await Cart.create(self.context, owner_id=self.user_id) + await cart.add_item(self.context, quantity=2, **HOODIE) + response = await cart.get_cart(self.context) + self.assertEqual(len(response.items), 1) + item = response.items[0] + self.assertEqual(item.product_id, "hoodie-1") + self.assertEqual(item.name, "Reboot Hoodie") + self.assertEqual(item.size, "L") + self.assertEqual(item.quantity, 2) + + async def test_add_same_variant_increments_quantity( + self, + ) -> None: + cart, _ = await Cart.create(self.context, owner_id=self.user_id) + await cart.add_item(self.context, quantity=2, **HOODIE) + await cart.add_item(self.context, quantity=1, **HOODIE) + response = await cart.get_cart(self.context) + self.assertEqual(len(response.items), 1) + self.assertEqual(response.items[0].quantity, 3) + + async def test_add_different_variant_adds_a_line( + self, + ) -> None: + cart, _ = await Cart.create(self.context, owner_id=self.user_id) + small = {**HOODIE, "variant_id": "hoodie-1-s", "size": "S"} + await cart.add_item(self.context, quantity=1, **HOODIE) + await cart.add_item(self.context, quantity=1, **small) + response = await cart.get_cart(self.context) + self.assertEqual(len(response.items), 2) + sizes = sorted(item.size for item in response.items) + self.assertEqual(sizes, ["L", "S"]) + + async def test_remove_item(self) -> None: + cart, _ = await Cart.create(self.context, owner_id=self.user_id) + await cart.add_item(self.context, quantity=1, **HOODIE) + await cart.remove_item(self.context, product_id="hoodie-1") + response = await cart.get_cart(self.context) + self.assertEqual(response.items, []) + + # ----- Cart.checkout ---------------------------------------- + + async def test_checkout_on_empty_cart_raises_cart_empty( + self, + ) -> None: + cart, _ = await Cart.create(self.context, owner_id=self.user_id) + with self.assertRaises(Cart.CheckoutAborted) as cm: + await cart.checkout( + self.context, + shipping_address=SHIPPING, + coupon_code="000000", + ) + self.assertIsInstance(cm.exception.error, CartEmpty) + + async def test_checkout_with_invalid_coupon_raises( + self, + ) -> None: + cart, _ = await Cart.create(self.context, owner_id=self.user_id) + await cart.add_item(self.context, quantity=1, **HOODIE) + with self.assertRaises(Cart.CheckoutAborted) as cm: + await cart.checkout( + self.context, + shipping_address=SHIPPING, + coupon_code="definitely-not-a-real-code", + ) + self.assertIsInstance(cm.exception.error, InvalidCoupon) + # The cart is still intact after a failed checkout. + response = await cart.get_cart(self.context) + self.assertEqual(len(response.items), 1) + + async def test_checkout_happy_path(self) -> None: + cart, _ = await Cart.create(self.context, owner_id=self.user_id) + await cart.add_item(self.context, quantity=2, **HOODIE) + + code = await self._mint_coupon() + result = await cart.checkout( + self.context, + shipping_address=SHIPPING, + coupon_code=code, + ) + self.assertTrue(result.order_id) + + # Cart emptied. + response = await cart.get_cart(self.context) + self.assertEqual(response.items, []) + + # Order created with the expected line item. + order = Order.ref(result.order_id) + details = await order.get_details(self.context) + self.assertEqual(details.order_id, result.order_id) + self.assertEqual(len(details.items), 1) + self.assertEqual(details.items[0].product_id, "hoodie-1") + self.assertEqual(details.items[0].quantity, 2) + self.assertEqual(details.subtotal_cents, 8000) + # The coupon makes the order free. + self.assertEqual(details.total_cents, 0) + + async def test_checkout_redeems_coupon_so_reuse_fails( + self, + ) -> None: + code = await self._mint_coupon() + + cart, _ = await Cart.create(self.context, owner_id=self.user_id) + await cart.add_item(self.context, quantity=1, **HOODIE) + await cart.checkout( + self.context, + shipping_address=SHIPPING, + coupon_code=code, + ) + + # Same code a second time should no longer be valid. + cart2, _ = await Cart.create(self.context, owner_id=self.user_id) + await cart2.add_item(self.context, quantity=1, **HOODIE) + with self.assertRaises(Cart.CheckoutAborted) as cm: + await cart2.checkout( + self.context, + shipping_address=SHIPPING, + coupon_code=code, + ) + self.assertIsInstance(cm.exception.error, InvalidCoupon) + + # ----- CouponBook admin gating ------------------------------ + + async def test_generate_codes_requires_admin_bearer( + self, + ) -> None: + book = CouponBook.ref(COUPON_BOOK_ID) + # Anonymous caller: no bearer token. + with self.assertRaises(Aborted): + await book.generate_codes(self.context) + + async def test_generate_codes_with_admin_bearer_succeeds( + self, + ) -> None: + admin_ctx = await self._admin_context() + response = await CouponBook.ref(COUPON_BOOK_ID + ).generate_codes(admin_ctx) + self.assertEqual(len(response.codes), 20) + # Fresh codes are all six digits. + for code in response.codes: + self.assertEqual(len(code), 6) + self.assertTrue(code.isdigit()) + + +if __name__ == "__main__": + unittest.main() diff --git a/reboot/examples/reboot-swag-store/mcp_servers.json b/reboot/examples/reboot-swag-store/mcp_servers.json new file mode 100644 index 00000000..3d7b0a58 --- /dev/null +++ b/reboot/examples/reboot-swag-store/mcp_servers.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "reboot-swag-store": { + "url": "http://localhost:9991/mcp", + "useOAuth": true + } + } +} diff --git a/reboot/examples/reboot-swag-store/pyproject.toml b/reboot/examples/reboot-swag-store/pyproject.toml new file mode 100644 index 00000000..32320933 --- /dev/null +++ b/reboot/examples/reboot-swag-store/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "reboot-swag-store" +version = "0.1.0" +requires-python = ">= 3.10" +dependencies = [ + "anyio>=4.0.0", + "httpx>=0.27,<1.0", + "python-dotenv>=1.0.0", + "uuid7>=0.1.0", + "reboot==1.1.0", +] + +[tool.rye] +dev-dependencies = [ + "pytest>=7.4", + "reboot==1.1.0", +] + +# This project only uses `rye` to provide `python` and its dependencies. +virtual = true +managed = true diff --git a/reboot/examples/reboot-swag-store/requirements-dev.lock b/reboot/examples/reboot-swag-store/requirements-dev.lock new file mode 100644 index 00000000..aacc7382 --- /dev/null +++ b/reboot/examples/reboot-swag-store/requirements-dev.lock @@ -0,0 +1,288 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false + +aiofiles==23.2.1 + # via reboot +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.13.2 + # via kubernetes-asyncio + # via reboot +aiosignal==1.4.0 + # via aiohttp +annotated-types==0.7.0 + # via pydantic +anyio==4.13.0 + # via httpx + # via mcp + # via reboot + # via sse-starlette + # via starlette +async-timeout==5.0.1 + # via aiohttp +attrs==26.1.0 + # via aiohttp + # via jsonschema + # via referencing +bitarray==3.8.0 + # via reboot +certifi==2026.4.22 + # via httpcore + # via httpx + # via kubernetes-asyncio +cffi==1.17.1 + # via cryptography + # via reboot +click==8.3.3 + # via uvicorn +colorama==0.4.6 + # via reboot +cryptography==44.0.0 + # via pyjwt + # via reboot +deprecated==1.3.1 + # via opentelemetry-api + # via opentelemetry-exporter-otlp-proto-grpc + # via opentelemetry-semantic-conventions +exceptiongroup==1.3.1 + # via anyio + # via pydantic-ai-slim + # via pytest +fastapi==0.115.12 + # via reboot +frozenlist==1.8.0 + # via aiohttp + # via aiosignal +genai-prices==0.0.57 + # via pydantic-ai-slim +googleapis-common-protos==1.65.0 + # via grpcio-status + # via opentelemetry-exporter-otlp-proto-grpc + # via reboot +griffelib==2.0.2 + # via pydantic-ai-slim +grpc-interceptor==0.15.4 + # via reboot +grpcio==1.64.3 + # via grpc-interceptor + # via grpcio-health-checking + # via grpcio-reflection + # via grpcio-status + # via grpcio-tools + # via opentelemetry-exporter-otlp-proto-grpc + # via reboot +grpcio-health-checking==1.64.3 + # via reboot +grpcio-reflection==1.64.3 + # via reboot +grpcio-status==1.64.3 + # via reboot +grpcio-tools==1.64.3 + # via reboot +h11==0.16.0 + # via httpcore + # via uvicorn +h2==4.3.0 + # via reboot +hpack==4.1.0 + # via h2 +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via genai-prices + # via mcp + # via pydantic-ai-slim + # via pydantic-graph +httpx-sse==0.4.3 + # via mcp +hyperframe==6.1.0 + # via h2 +idna==3.13 + # via anyio + # via httpx + # via yarl +importlib-metadata==8.5.0 + # via opentelemetry-api +iniconfig==2.3.0 + # via pytest +jinja2==3.1.2 + # via jinja2-strcase + # via reboot +jinja2-strcase==0.0.2 + # via reboot +jsonschema==4.26.0 + # via mcp +jsonschema-specifications==2025.9.1 + # via jsonschema +kubernetes-asyncio==31.1.0 + # via reboot +logfire-api==4.32.1 + # via pydantic-graph +markupsafe==3.0.3 + # via jinja2 +mcp==1.27.0 + # via reboot +multidict==6.7.1 + # via aiohttp + # via yarl +mypy-protobuf==3.6.0 + # via reboot +opentelemetry-api==1.28.1 + # via opentelemetry-exporter-otlp-proto-grpc + # via opentelemetry-instrumentation + # via opentelemetry-instrumentation-grpc + # via opentelemetry-sdk + # via opentelemetry-semantic-conventions + # via pydantic-ai-slim + # via reboot +opentelemetry-exporter-otlp-proto-common==1.28.1 + # via opentelemetry-exporter-otlp-proto-grpc +opentelemetry-exporter-otlp-proto-grpc==1.28.1 + # via reboot +opentelemetry-instrumentation==0.49b1 + # via opentelemetry-instrumentation-grpc +opentelemetry-instrumentation-grpc==0.49b1 + # via reboot +opentelemetry-proto==1.28.1 + # via opentelemetry-exporter-otlp-proto-common + # via opentelemetry-exporter-otlp-proto-grpc +opentelemetry-sdk==1.28.1 + # via opentelemetry-exporter-otlp-proto-grpc + # via reboot +opentelemetry-semantic-conventions==0.49b1 + # via opentelemetry-instrumentation + # via opentelemetry-instrumentation-grpc + # via opentelemetry-sdk +packaging==26.2 + # via opentelemetry-instrumentation + # via pytest + # via reboot +pathspec==0.12.1 + # via reboot +pluggy==1.6.0 + # via pytest +propcache==0.4.1 + # via aiohttp + # via yarl +protobuf==5.28.3 + # via googleapis-common-protos + # via grpcio-health-checking + # via grpcio-reflection + # via grpcio-status + # via grpcio-tools + # via mypy-protobuf + # via opentelemetry-proto + # via reboot +psutil==6.0.0 + # via reboot +pycparser==3.0 + # via cffi +pydantic==2.13.3 + # via fastapi + # via genai-prices + # via mcp + # via pydantic-ai-slim + # via pydantic-graph + # via pydantic-settings + # via reboot +pydantic-ai-slim==1.87.0 + # via reboot +pydantic-core==2.46.3 + # via pydantic +pydantic-graph==1.87.0 + # via pydantic-ai-slim +pydantic-settings==2.14.0 + # via mcp +pygments==2.20.0 + # via pytest +pyjwt==2.10.1 + # via mcp + # via reboot +pyprctl==0.1.3 + # via reboot +pytest==9.0.3 +python-dateutil==2.9.0.post0 + # via kubernetes-asyncio +python-dotenv==1.2.1 + # via pydantic-settings + # via reboot +python-multipart==0.0.27 + # via mcp +python-ulid==3.1.0 + # via reboot +pyyaml==6.0.2 + # via kubernetes-asyncio + # via reboot +reboot==1.1.0 +referencing==0.37.0 + # via jsonschema + # via jsonschema-specifications +rpds-py==0.30.0 + # via jsonschema + # via referencing +setuptools==82.0.1 + # via grpcio-tools +six==1.17.0 + # via kubernetes-asyncio + # via python-dateutil +sse-starlette==3.0.3 + # via mcp +starlette==0.46.2 + # via fastapi + # via mcp + # via reboot +tabulate==0.9.0 + # via reboot +tomli==2.4.1 + # via pytest +types-protobuf==7.34.1.20260503 + # via mypy-protobuf +typing-extensions==4.15.0 + # via aiosignal + # via anyio + # via exceptiongroup + # via fastapi + # via mcp + # via multidict + # via opentelemetry-sdk + # via pydantic + # via pydantic-core + # via reboot + # via referencing + # via typing-inspection + # via uvicorn +typing-inspection==0.4.2 + # via mcp + # via pydantic + # via pydantic-ai-slim + # via pydantic-graph + # via pydantic-settings +tzlocal==5.3 + # via reboot +urllib3==1.26.15 + # via kubernetes-asyncio + # via reboot +uuid7==0.1.0 +uuid7-standard==1.1.0 + # via reboot +uvicorn==0.34.0 + # via mcp + # via reboot +watchdog==6.0.0 + # via reboot +websockets==15.0.1 + # via reboot +wrapt==1.17.3 + # via deprecated + # via opentelemetry-instrumentation + # via opentelemetry-instrumentation-grpc +yarl==1.23.0 + # via aiohttp +zipp==3.23.1 + # via importlib-metadata diff --git a/reboot/examples/reboot-swag-store/requirements.lock b/reboot/examples/reboot-swag-store/requirements.lock new file mode 100644 index 00000000..52fca223 --- /dev/null +++ b/reboot/examples/reboot-swag-store/requirements.lock @@ -0,0 +1,277 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false + +aiofiles==23.2.1 + # via reboot +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.13.2 + # via kubernetes-asyncio + # via reboot +aiosignal==1.4.0 + # via aiohttp +annotated-types==0.7.0 + # via pydantic +anyio==4.13.0 + # via httpx + # via mcp + # via reboot + # via sse-starlette + # via starlette +async-timeout==5.0.1 + # via aiohttp +attrs==26.1.0 + # via aiohttp + # via jsonschema + # via referencing +bitarray==3.8.0 + # via reboot +certifi==2026.4.22 + # via httpcore + # via httpx + # via kubernetes-asyncio +cffi==1.17.1 + # via cryptography + # via reboot +click==8.3.3 + # via uvicorn +colorama==0.4.6 + # via reboot +cryptography==44.0.0 + # via pyjwt + # via reboot +deprecated==1.3.1 + # via opentelemetry-api + # via opentelemetry-exporter-otlp-proto-grpc + # via opentelemetry-semantic-conventions +exceptiongroup==1.3.1 + # via anyio + # via pydantic-ai-slim +fastapi==0.115.12 + # via reboot +frozenlist==1.8.0 + # via aiohttp + # via aiosignal +genai-prices==0.0.57 + # via pydantic-ai-slim +googleapis-common-protos==1.65.0 + # via grpcio-status + # via opentelemetry-exporter-otlp-proto-grpc + # via reboot +griffelib==2.0.2 + # via pydantic-ai-slim +grpc-interceptor==0.15.4 + # via reboot +grpcio==1.64.3 + # via grpc-interceptor + # via grpcio-health-checking + # via grpcio-reflection + # via grpcio-status + # via grpcio-tools + # via opentelemetry-exporter-otlp-proto-grpc + # via reboot +grpcio-health-checking==1.64.3 + # via reboot +grpcio-reflection==1.64.3 + # via reboot +grpcio-status==1.64.3 + # via reboot +grpcio-tools==1.64.3 + # via reboot +h11==0.16.0 + # via httpcore + # via uvicorn +h2==4.3.0 + # via reboot +hpack==4.1.0 + # via h2 +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via genai-prices + # via mcp + # via pydantic-ai-slim + # via pydantic-graph +httpx-sse==0.4.3 + # via mcp +hyperframe==6.1.0 + # via h2 +idna==3.13 + # via anyio + # via httpx + # via yarl +importlib-metadata==8.5.0 + # via opentelemetry-api +jinja2==3.1.2 + # via jinja2-strcase + # via reboot +jinja2-strcase==0.0.2 + # via reboot +jsonschema==4.26.0 + # via mcp +jsonschema-specifications==2025.9.1 + # via jsonschema +kubernetes-asyncio==31.1.0 + # via reboot +logfire-api==4.32.1 + # via pydantic-graph +markupsafe==3.0.3 + # via jinja2 +mcp==1.27.0 + # via reboot +multidict==6.7.1 + # via aiohttp + # via yarl +mypy-protobuf==3.6.0 + # via reboot +opentelemetry-api==1.28.1 + # via opentelemetry-exporter-otlp-proto-grpc + # via opentelemetry-instrumentation + # via opentelemetry-instrumentation-grpc + # via opentelemetry-sdk + # via opentelemetry-semantic-conventions + # via pydantic-ai-slim + # via reboot +opentelemetry-exporter-otlp-proto-common==1.28.1 + # via opentelemetry-exporter-otlp-proto-grpc +opentelemetry-exporter-otlp-proto-grpc==1.28.1 + # via reboot +opentelemetry-instrumentation==0.49b1 + # via opentelemetry-instrumentation-grpc +opentelemetry-instrumentation-grpc==0.49b1 + # via reboot +opentelemetry-proto==1.28.1 + # via opentelemetry-exporter-otlp-proto-common + # via opentelemetry-exporter-otlp-proto-grpc +opentelemetry-sdk==1.28.1 + # via opentelemetry-exporter-otlp-proto-grpc + # via reboot +opentelemetry-semantic-conventions==0.49b1 + # via opentelemetry-instrumentation + # via opentelemetry-instrumentation-grpc + # via opentelemetry-sdk +packaging==26.2 + # via opentelemetry-instrumentation + # via reboot +pathspec==0.12.1 + # via reboot +propcache==0.4.1 + # via aiohttp + # via yarl +protobuf==5.28.3 + # via googleapis-common-protos + # via grpcio-health-checking + # via grpcio-reflection + # via grpcio-status + # via grpcio-tools + # via mypy-protobuf + # via opentelemetry-proto + # via reboot +psutil==6.0.0 + # via reboot +pycparser==3.0 + # via cffi +pydantic==2.13.3 + # via fastapi + # via genai-prices + # via mcp + # via pydantic-ai-slim + # via pydantic-graph + # via pydantic-settings + # via reboot +pydantic-ai-slim==1.87.0 + # via reboot +pydantic-core==2.46.3 + # via pydantic +pydantic-graph==1.87.0 + # via pydantic-ai-slim +pydantic-settings==2.14.0 + # via mcp +pyjwt==2.10.1 + # via mcp + # via reboot +pyprctl==0.1.3 + # via reboot +python-dateutil==2.9.0.post0 + # via kubernetes-asyncio +python-dotenv==1.2.1 + # via pydantic-settings + # via reboot +python-multipart==0.0.27 + # via mcp +python-ulid==3.1.0 + # via reboot +pyyaml==6.0.2 + # via kubernetes-asyncio + # via reboot +reboot==1.1.0 +referencing==0.37.0 + # via jsonschema + # via jsonschema-specifications +rpds-py==0.30.0 + # via jsonschema + # via referencing +setuptools==82.0.1 + # via grpcio-tools +six==1.17.0 + # via kubernetes-asyncio + # via python-dateutil +sse-starlette==3.0.3 + # via mcp +starlette==0.46.2 + # via fastapi + # via mcp + # via reboot +tabulate==0.9.0 + # via reboot +types-protobuf==7.34.1.20260503 + # via mypy-protobuf +typing-extensions==4.15.0 + # via aiosignal + # via anyio + # via exceptiongroup + # via fastapi + # via mcp + # via multidict + # via opentelemetry-sdk + # via pydantic + # via pydantic-core + # via reboot + # via referencing + # via typing-inspection + # via uvicorn +typing-inspection==0.4.2 + # via mcp + # via pydantic + # via pydantic-ai-slim + # via pydantic-graph + # via pydantic-settings +tzlocal==5.3 + # via reboot +urllib3==1.26.15 + # via kubernetes-asyncio + # via reboot +uuid7==0.1.0 +uuid7-standard==1.1.0 + # via reboot +uvicorn==0.34.0 + # via mcp + # via reboot +watchdog==6.0.0 + # via reboot +websockets==15.0.1 + # via reboot +wrapt==1.17.3 + # via deprecated + # via opentelemetry-instrumentation + # via opentelemetry-instrumentation-grpc +yarl==1.23.0 + # via aiohttp +zipp==3.23.1 + # via importlib-metadata diff --git a/reboot/examples/reboot-swag-store/uv.lock b/reboot/examples/reboot-swag-store/uv.lock new file mode 100644 index 00000000..966b4e8d --- /dev/null +++ b/reboot/examples/reboot-swag-store/uv.lock @@ -0,0 +1,2416 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "aiofiles" +version = "23.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/41/cfed10bc64d774f497a86e5ede9248e1d062db675504b41c320954d99641/aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a", size = 32072, upload-time = "2023-08-09T15:23:11.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/19/5af6804c4cc0fed83f47bff6e413a98a36618e7d40185cd36e69737f3b0e/aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107", size = 15727, upload-time = "2023-08-09T15:23:09.774Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/34/939730e66b716b76046dedfe0842995842fa906ccc4964bba414ff69e429/aiohttp-3.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155", size = 736471, upload-time = "2025-10-28T20:55:27.924Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/dcbdf2df7f6ca72b0bb4c0b4509701f2d8942cf54e29ca197389c214c07f/aiohttp-3.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c", size = 493985, upload-time = "2025-10-28T20:55:29.456Z" }, + { url = "https://files.pythonhosted.org/packages/9d/87/71c8867e0a1d0882dcbc94af767784c3cb381c1c4db0943ab4aae4fed65e/aiohttp-3.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636", size = 489274, upload-time = "2025-10-28T20:55:31.134Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/46c24e8dae237295eaadd113edd56dee96ef6462adf19b88592d44891dc5/aiohttp-3.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da", size = 1668171, upload-time = "2025-10-28T20:55:36.065Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c6/4cdfb4440d0e28483681a48f69841fa5e39366347d66ef808cbdadddb20e/aiohttp-3.13.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725", size = 1636036, upload-time = "2025-10-28T20:55:37.576Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/8708cf678628216fb678ab327a4e1711c576d6673998f4f43e86e9ae90dd/aiohttp-3.13.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5", size = 1727975, upload-time = "2025-10-28T20:55:39.457Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2e/3ebfe12fdcb9b5f66e8a0a42dffcd7636844c8a018f261efb2419f68220b/aiohttp-3.13.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3", size = 1815823, upload-time = "2025-10-28T20:55:40.958Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/ca2ef819488cbb41844c6cf92ca6dd15b9441e6207c58e5ae0e0fc8d70ad/aiohttp-3.13.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802", size = 1669374, upload-time = "2025-10-28T20:55:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/1fe2e1179a0d91ce09c99069684aab619bf2ccde9b20bd6ca44f8837203e/aiohttp-3.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a", size = 1555315, upload-time = "2025-10-28T20:55:44.264Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2b/f3781899b81c45d7cbc7140cddb8a3481c195e7cbff8e36374759d2ab5a5/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204", size = 1639140, upload-time = "2025-10-28T20:55:46.626Z" }, + { url = "https://files.pythonhosted.org/packages/72/27/c37e85cd3ece6f6c772e549bd5a253d0c122557b25855fb274224811e4f2/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22", size = 1645496, upload-time = "2025-10-28T20:55:48.933Z" }, + { url = "https://files.pythonhosted.org/packages/66/20/3af1ab663151bd3780b123e907761cdb86ec2c4e44b2d9b195ebc91fbe37/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d", size = 1697625, upload-time = "2025-10-28T20:55:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/95/eb/ae5cab15efa365e13d56b31b0d085a62600298bf398a7986f8388f73b598/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f", size = 1542025, upload-time = "2025-10-28T20:55:51.861Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2d/1683e8d67ec72d911397fe4e575688d2a9b8f6a6e03c8fdc9f3fd3d4c03f/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f", size = 1714918, upload-time = "2025-10-28T20:55:53.515Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ffe8e0e1c57c5e542d47ffa1fcf95ef2b3ea573bf7c4d2ee877252431efc/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6", size = 1656113, upload-time = "2025-10-28T20:55:55.438Z" }, + { url = "https://files.pythonhosted.org/packages/0d/42/d511aff5c3a2b06c09d7d214f508a4ad8ac7799817f7c3d23e7336b5e896/aiohttp-3.13.2-cp310-cp310-win32.whl", hash = "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251", size = 432290, upload-time = "2025-10-28T20:55:56.96Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ea/1c2eb7098b5bad4532994f2b7a8228d27674035c9b3234fe02c37469ef14/aiohttp-3.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514", size = 455075, upload-time = "2025-10-28T20:55:58.373Z" }, + { url = "https://files.pythonhosted.org/packages/35/74/b321e7d7ca762638cdf8cdeceb39755d9c745aff7a64c8789be96ddf6e96/aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", size = 743409, upload-time = "2025-10-28T20:56:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/99/3d/91524b905ec473beaf35158d17f82ef5a38033e5809fe8742e3657cdbb97/aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", size = 497006, upload-time = "2025-10-28T20:56:01.85Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d3/7f68bc02a67716fe80f063e19adbd80a642e30682ce74071269e17d2dba1/aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", size = 493195, upload-time = "2025-10-28T20:56:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/913f774a4708775433b7375c4f867d58ba58ead833af96c8af3621a0d243/aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", size = 1747759, upload-time = "2025-10-28T20:56:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/04efe156f4326f31c7c4a97144f82132c3bb21859b7bb84748d452ccc17c/aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", size = 1704456, upload-time = "2025-10-28T20:56:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/4e16154d8e0a9cf4ae76f692941fd52543bbb148f02f098ca73cab9b1c1b/aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", size = 1807572, upload-time = "2025-10-28T20:56:08.558Z" }, + { url = "https://files.pythonhosted.org/packages/34/58/b0583defb38689e7f06798f0285b1ffb3a6fb371f38363ce5fd772112724/aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a", size = 1895954, upload-time = "2025-10-28T20:56:10.545Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/083907ee3437425b4e376aa58b2c915eb1a33703ec0dc30040f7ae3368c6/aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", size = 1747092, upload-time = "2025-10-28T20:56:12.118Z" }, + { url = "https://files.pythonhosted.org/packages/ac/61/98a47319b4e425cc134e05e5f3fc512bf9a04bf65aafd9fdcda5d57ec693/aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", size = 1606815, upload-time = "2025-10-28T20:56:14.191Z" }, + { url = "https://files.pythonhosted.org/packages/97/4b/e78b854d82f66bb974189135d31fce265dee0f5344f64dd0d345158a5973/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", size = 1723789, upload-time = "2025-10-28T20:56:16.101Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fc/9d2ccc794fc9b9acd1379d625c3a8c64a45508b5091c546dea273a41929e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", size = 1718104, upload-time = "2025-10-28T20:56:17.655Z" }, + { url = "https://files.pythonhosted.org/packages/66/65/34564b8765ea5c7d79d23c9113135d1dd3609173da13084830f1507d56cf/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", size = 1785584, upload-time = "2025-10-28T20:56:19.238Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/f6a7a426e02fc82781afd62016417b3948e2207426d90a0e478790d1c8a4/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", size = 1595126, upload-time = "2025-10-28T20:56:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c7/8e22d5d28f94f67d2af496f14a83b3c155d915d1fe53d94b66d425ec5b42/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", size = 1800665, upload-time = "2025-10-28T20:56:22.922Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/91133c8b68b1da9fc16555706aa7276fdf781ae2bb0876c838dd86b8116e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", size = 1739532, upload-time = "2025-10-28T20:56:25.924Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/3747644d26a998774b21a616016620293ddefa4d63af6286f389aedac844/aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", size = 431876, upload-time = "2025-10-28T20:56:27.524Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/688462108c1a00eb9f05765331c107f95ae86f6b197b865d29e930b7e462/aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", size = 456205, upload-time = "2025-10-28T20:56:29.062Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623, upload-time = "2025-10-28T20:56:30.797Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664, upload-time = "2025-10-28T20:56:32.708Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808, upload-time = "2025-10-28T20:56:34.57Z" }, + { url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863, upload-time = "2025-10-28T20:56:36.377Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586, upload-time = "2025-10-28T20:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625, upload-time = "2025-10-28T20:56:39.75Z" }, + { url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281, upload-time = "2025-10-28T20:56:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431, upload-time = "2025-10-28T20:56:43.162Z" }, + { url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846, upload-time = "2025-10-28T20:56:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606, upload-time = "2025-10-28T20:56:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663, upload-time = "2025-10-28T20:56:48.528Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939, upload-time = "2025-10-28T20:56:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132, upload-time = "2025-10-28T20:56:52.568Z" }, + { url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802, upload-time = "2025-10-28T20:56:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512, upload-time = "2025-10-28T20:56:56.428Z" }, + { url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690, upload-time = "2025-10-28T20:56:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465, upload-time = "2025-10-28T20:57:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" }, + { url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" }, + { url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387, upload-time = "2025-10-28T20:57:08.685Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314, upload-time = "2025-10-28T20:57:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317, upload-time = "2025-10-28T20:57:12.563Z" }, + { url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539, upload-time = "2025-10-28T20:57:14.623Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597, upload-time = "2025-10-28T20:57:16.399Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006, upload-time = "2025-10-28T20:57:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220, upload-time = "2025-10-28T20:57:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570, upload-time = "2025-10-28T20:57:22.253Z" }, + { url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407, upload-time = "2025-10-28T20:57:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093, upload-time = "2025-10-28T20:57:26.257Z" }, + { url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084, upload-time = "2025-10-28T20:57:28.349Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987, upload-time = "2025-10-28T20:57:30.233Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859, upload-time = "2025-10-28T20:57:32.105Z" }, + { url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192, upload-time = "2025-10-28T20:57:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234, upload-time = "2025-10-28T20:57:36.415Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733, upload-time = "2025-10-28T20:57:38.205Z" }, + { url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303, upload-time = "2025-10-28T20:57:40.122Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965, upload-time = "2025-10-28T20:57:42.28Z" }, + { url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221, upload-time = "2025-10-28T20:57:44.869Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178, upload-time = "2025-10-28T20:57:47.216Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001, upload-time = "2025-10-28T20:57:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325, upload-time = "2025-10-28T20:57:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978, upload-time = "2025-10-28T20:57:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042, upload-time = "2025-10-28T20:57:55.617Z" }, + { url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085, upload-time = "2025-10-28T20:57:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238, upload-time = "2025-10-28T20:57:59.525Z" }, + { url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395, upload-time = "2025-10-28T20:58:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965, upload-time = "2025-10-28T20:58:03.972Z" }, + { url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585, upload-time = "2025-10-28T20:58:06.189Z" }, + { url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621, upload-time = "2025-10-28T20:58:08.636Z" }, + { url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627, upload-time = "2025-10-28T20:58:11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360, upload-time = "2025-10-28T20:58:13.358Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616, upload-time = "2025-10-28T20:58:15.339Z" }, + { url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131, upload-time = "2025-10-28T20:58:17.693Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168, upload-time = "2025-10-28T20:58:20.113Z" }, + { url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200, upload-time = "2025-10-28T20:58:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497, upload-time = "2025-10-28T20:58:24.672Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703, upload-time = "2025-10-28T20:58:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738, upload-time = "2025-10-28T20:58:29.787Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061, upload-time = "2025-10-28T20:58:32.529Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201, upload-time = "2025-10-28T20:58:34.618Z" }, + { url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868, upload-time = "2025-10-28T20:58:38.835Z" }, + { url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660, upload-time = "2025-10-28T20:58:41.507Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548, upload-time = "2025-10-28T20:58:43.674Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240, upload-time = "2025-10-28T20:58:45.787Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334, upload-time = "2025-10-28T20:58:47.936Z" }, + { url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685, upload-time = "2025-10-28T20:58:50.642Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093, upload-time = "2025-10-28T20:58:52.782Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "bitarray" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/06/92fdc84448d324ab8434b78e65caf4fb4c6c90b4f8ad9bdd4c8021bfaf1e/bitarray-3.8.0.tar.gz", hash = "sha256:3eae38daffd77c9621ae80c16932eea3fb3a4af141fb7cc724d4ad93eff9210d", size = 151991, upload-time = "2025-11-02T21:41:15.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/b9/8a645fd36fc4c01ee223f97eccd4699c2f2e91681ccb33c0e963881c8e58/bitarray-3.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f08342dc8d19214faa7ef99574dea6c37a2790d6d04a9793ef8fa76c188dc08d", size = 148504, upload-time = "2025-11-02T21:38:54.596Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f4/11b562e13ff732bd0674376f367f0a272034ebc28b8efbafbeb924552d21/bitarray-3.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:792462abfeeca6cc8c6c1e6d27e14319682f0182f6b0ba37befe911af794db70", size = 145481, upload-time = "2025-11-02T21:38:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7c/5a2487da579491b38abab3b437e01d3b05be6e16e69cc5eb304040dcebd5/bitarray-3.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0df69d26f21a9d2f1b20266f6737fa43f08aa5015c99900fb69f255fbe4dabb4", size = 322760, upload-time = "2025-11-02T21:38:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/8d/59/f0ef82d6a878d4af1b4961d208a716317929aa172fc0dfa5f4115319a873/bitarray-3.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b4f10d3f304be7183fac79bf2cd997f82e16aa9a9f37343d76c026c6e435a8a8", size = 350332, upload-time = "2025-11-02T21:38:58.238Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ec/d444b22fce853327d4a8adec1de9987e11b28fcc2d7204dcbc544e196ed9/bitarray-3.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fc98ff43abad61f00515ad9a06213b7716699146e46eabd256cdfe7cb522bd97", size = 360787, upload-time = "2025-11-02T21:38:59.239Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9e/60b205f52ea9ff155e9f12249090475159c909039daa29e47cd95e115dd5/bitarray-3.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81c6b4a6c1af800d52a6fa32389ef8f4281583f4f99dc1a40f2bb47667281541", size = 329050, upload-time = "2025-11-02T21:39:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/2ce373b423bc85a0eb93ee1cba3977971259a92a116932632f417b1b04d2/bitarray-3.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f3fd8df63c41ff6a676d031956aebf68ebbc687b47c507da25501eb22eec341f", size = 320507, upload-time = "2025-11-02T21:39:01.714Z" }, + { url = "https://files.pythonhosted.org/packages/2a/88/437408a2674b8bdb02063dd1535969b9c73cb8fdd197485de431e506c50e/bitarray-3.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0ce9d9e07c75da8027c62b4c9f45771d1d8aae7dc9ad7fb606c6a5aedbe9741", size = 348449, upload-time = "2025-11-02T21:39:03.124Z" }, + { url = "https://files.pythonhosted.org/packages/97/46/d799e7e731c778b6dcb4627bafd395102065e5ab15a4a31f4222a3e20706/bitarray-3.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8a9c962c64a4c08def58b9799333e33af94ec53038cf151d36edacdb41f81646", size = 344776, upload-time = "2025-11-02T21:39:04.147Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9a/129fff56d22d316b1c848c6e13e64191485756b5cd6ceb08e640edb80020/bitarray-3.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1a54d7e7999735faacdcbe8128e30207abc2caf9f9fd7102d180b32f1b78bfce", size = 325899, upload-time = "2025-11-02T21:39:05.118Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/4b01e99452ecc39f4abccf9bf83fe0f01c390e9794dad2d04b2c8b893c5f/bitarray-3.8.0-cp310-cp310-win32.whl", hash = "sha256:3ea52df96566457735314794422274bd1962066bfb609e7eea9113d70cf04ffe", size = 142756, upload-time = "2025-11-02T21:39:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/18/3f/c83635a67d90f45f88012468566c233eed1e9e9a9184fa882ba4039fadb3/bitarray-3.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:82a07de83dce09b4fa1bccbdc8bde8f188b131666af0dc9048ba0a0e448d8a3b", size = 149527, upload-time = "2025-11-02T21:39:07.377Z" }, + { url = "https://files.pythonhosted.org/packages/33/46/391b3902a523d4555313640746460b19d317c6233d9379e150af97fa1554/bitarray-3.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5ba07e58fd98c9782201e79eb8dd4225733d212a5a3700f9a84d329bd0463a6", size = 146453, upload-time = "2025-11-02T21:39:08.624Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7d/63558f1d0eb09217a3d30c1c847890879973e224a728fcff9391fab999b8/bitarray-3.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:25b9cff6c9856bc396232e2f609ea0c5ec1a8a24c500cee4cca96ba8a3cd50b6", size = 148502, upload-time = "2025-11-02T21:39:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7b/f957ad211cb0172965b5f0881b67b99e2b6d41512af0a1001f44a44ddf4a/bitarray-3.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d9984017314da772f5f7460add7a0301a4ffc06c72c2998bb16c300a6253607", size = 145484, upload-time = "2025-11-02T21:39:10.904Z" }, + { url = "https://files.pythonhosted.org/packages/9f/dc/897973734f14f91467a3a795a4624752238053ecffaec7c8bbda1e363fda/bitarray-3.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbbbfbb7d039b20d289ce56b1beb46138d65769d04af50c199c6ac4cb6054d52", size = 330909, upload-time = "2025-11-02T21:39:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/67/be/24b4b792426d92de289e73e09682915d567c2e69d47e8857586cbdc865d0/bitarray-3.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1f723e260c35e1c7c57a09d3a6ebe681bd56c83e1208ae3ce1869b7c0d10d4f", size = 358469, upload-time = "2025-11-02T21:39:13.766Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0e/2eda69a7a59a6998df8fb57cc9d1e0e62888c599fb5237b0a8b479a01afb/bitarray-3.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cbd1660fb48827381ce3a621a4fdc237959e1cd4e98b098952a8f624a0726425", size = 369131, upload-time = "2025-11-02T21:39:15.041Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7b/8a372d6635a6b2622477b2f96a569b2cd0318a62bc95a4a2144c7942c987/bitarray-3.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df6d7bf3e15b7e6e202a16ff4948a51759354016026deb04ab9b5acbbe35e096", size = 337089, upload-time = "2025-11-02T21:39:16.124Z" }, + { url = "https://files.pythonhosted.org/packages/93/f0/8eca934dbe5dee47a0e5ef44eeb72e85acacc8097c27cd164337bc4ec5d3/bitarray-3.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d5c931ec1c03111718cabf85f6012bb2815fa0ce578175567fa8d6f2cc15d3b4", size = 328504, upload-time = "2025-11-02T21:39:17.321Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/928b8e23a9950f8a8bfc42bc1e7de41f4e27f57de01a716308be5f683c2b/bitarray-3.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:41b53711f89008ba2de62e4c2d2260a8b357072fd4f18e1351b28955db2719dc", size = 356461, upload-time = "2025-11-02T21:39:18.396Z" }, + { url = "https://files.pythonhosted.org/packages/a9/93/4fb58417aff47fa2fe1874a39c9346b589a1d78c93a9cb24cccede5dc737/bitarray-3.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4f298daaaea58d45e245a132d6d2bdfb6f856da50dc03d75ebb761439fb626cf", size = 353008, upload-time = "2025-11-02T21:39:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/da/54/aa04e4a7b45aa5913f08ee377d43319b0979925e3c0407882eb29df3be66/bitarray-3.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:30989a2451b693c3f9359d91098a744992b5431a0be4858f1fdf0ec76b457125", size = 334048, upload-time = "2025-11-02T21:39:20.924Z" }, + { url = "https://files.pythonhosted.org/packages/da/52/e851f41076df014c05d6ac1ce34fbf7db5fa31241da3e2f09bb2be9e283d/bitarray-3.8.0-cp311-cp311-win32.whl", hash = "sha256:e5aed4754895942ae15ffa48c52d181e1c1463236fda68d2dba29c03aa61786b", size = 142907, upload-time = "2025-11-02T21:39:22.312Z" }, + { url = "https://files.pythonhosted.org/packages/28/01/db0006148b1dd13b4ac2686df8fa57d12f5887df313a506e939af0cb0997/bitarray-3.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:22c540ed20167d3dbb1e2d868ca935180247d620c40eace90efa774504a40e3b", size = 149670, upload-time = "2025-11-02T21:39:23.341Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ea/b7d55ee269b1426f758a535c9ec2a07c056f20f403fa981685c3c8b4798c/bitarray-3.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:84b52b2cf77bb7f703d16c4007b021078dbbe6cf8ffb57abe81a7bacfc175ef2", size = 146709, upload-time = "2025-11-02T21:39:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/0c41d893eda756315491adfdbf9bc928aee3d377a7f97a8834d453aa5de1/bitarray-3.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2fcbe9b3a5996b417e030aa33a562e7e20dfc86271e53d7e841fc5df16268b8", size = 148575, upload-time = "2025-11-02T21:39:25.718Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/12ab2f4a4429bd844b419c37877caba93d676d18be71354fbbeb21d9f4cc/bitarray-3.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd761d158f67e288fd0ebe00c3b158095ce80a4bc7c32b60c7121224003ba70d", size = 145454, upload-time = "2025-11-02T21:39:26.695Z" }, + { url = "https://files.pythonhosted.org/packages/26/58/314b3e3f219533464e120f0c51ac5123e7b1c1b91f725a4073fb70c5a858/bitarray-3.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c394a3f055b49f92626f83c1a0b6d6cd2c628f1ccd72481c3e3c6aa4695f3b20", size = 332949, upload-time = "2025-11-02T21:39:27.801Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ce/ca8c706bd8341c7a22dd92d2a528af71f7e5f4726085d93f81fd768cb03b/bitarray-3.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:969fd67de8c42affdb47b38b80f1eaa79ac0ef17d65407cdd931db1675315af1", size = 360599, upload-time = "2025-11-02T21:39:28.964Z" }, + { url = "https://files.pythonhosted.org/packages/ef/dc/aa181df85f933052d962804906b282acb433cb9318b08ec2aceb4ee34faf/bitarray-3.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:99d25aff3745c54e61ab340b98400c52ebec04290a62078155e0d7eb30380220", size = 371972, upload-time = "2025-11-02T21:39:30.228Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d9/b805bfa158c7bcf4df0ac19b1be581b47e1ddb792c11023aed80a7058e78/bitarray-3.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e645b4c365d6f1f9e0799380ad6395268f3c3b898244a650aaeb8d9d27b74c35", size = 340303, upload-time = "2025-11-02T21:39:31.342Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/5308cc97ea929e30727292617a3a88293470166851e13c9e3f16f395da55/bitarray-3.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2fa23fdb3beab313950bbb49674e8a161e61449332d3997089fe3944953f1b77", size = 330494, upload-time = "2025-11-02T21:39:32.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/89/64f1596cb80433323efdbc8dcd0d6e57c40dfbe6ea3341623f34ec397edd/bitarray-3.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:165052a0e61c880f7093808a0c524ce1b3555bfa114c0dfb5c809cd07918a60d", size = 358123, upload-time = "2025-11-02T21:39:34.331Z" }, + { url = "https://files.pythonhosted.org/packages/27/fd/f3d49c5443b57087f888b5e118c8dd78bb7c8e8cfeeed250f8e92128a05f/bitarray-3.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:337c8cd46a4c6568d367ed676cbf2d7de16f890bb31dbb54c44c1d6bb6d4a1de", size = 356046, upload-time = "2025-11-02T21:39:35.449Z" }, + { url = "https://files.pythonhosted.org/packages/aa/db/1fd0b402bd2b47142e958b6930dbb9445235d03fa703c9a24caa6e576ae2/bitarray-3.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21ca6a47bf20db9e7ad74ca04b3d479e4d76109b68333eb23535553d2705339e", size = 336872, upload-time = "2025-11-02T21:39:36.891Z" }, + { url = "https://files.pythonhosted.org/packages/58/73/680b47718f1313b4538af479c4732eaca0aeda34d93fc5b869f87932d57d/bitarray-3.8.0-cp312-cp312-win32.whl", hash = "sha256:178c5a4c7fdfb5cd79e372ae7f675390e670f3732e5bc68d327e01a5b3ff8d55", size = 143025, upload-time = "2025-11-02T21:39:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/f8/11/7792587c19c79a8283e8838f44709fa4338a8f7d2a3091dfd81c07ae89c7/bitarray-3.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:75a3b6e9c695a6570ea488db75b84bb592ff70a944957efa1c655867c575018b", size = 149969, upload-time = "2025-11-02T21:39:39.715Z" }, + { url = "https://files.pythonhosted.org/packages/9a/00/9df64b5d8a84e8e9ec392f6f9ce93f50626a5b301cb6c6b3fe3406454d66/bitarray-3.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:5591daf81313096909d973fb2612fccd87528fdfdd39f6478bdce54543178954", size = 146907, upload-time = "2025-11-02T21:39:40.815Z" }, + { url = "https://files.pythonhosted.org/packages/3e/35/480364d4baf1e34c79076750914664373f561c58abb5c31c35b3fae613ff/bitarray-3.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18214bac86341f1cc413772e66447d6cca10981e2880b70ecaf4e826c04f95e9", size = 148582, upload-time = "2025-11-02T21:39:42.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a8/718b95524c803937f4edbaaf6480f39c80f6ed189d61357b345e8361ffb6/bitarray-3.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:01c5f0dc080b0ebb432f7a68ee1e88a76bd34f6d89c9568fcec65fb16ed71f0e", size = 145433, upload-time = "2025-11-02T21:39:43.552Z" }, + { url = "https://files.pythonhosted.org/packages/03/66/4a10f30dc9e2e01e3b4ecd44a511219f98e63c86b0e0f704c90fac24059b/bitarray-3.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86685fa04067f7175f9718489ae755f6acde03593a1a9ca89305554af40e14fd", size = 332986, upload-time = "2025-11-02T21:39:44.656Z" }, + { url = "https://files.pythonhosted.org/packages/53/25/4c08774d847f80a1166e4c704b4e0f1c417c0afe6306eae0bc5e70d35faa/bitarray-3.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56896ceeffe25946c4010320629e2d858ca763cd8ded273c81672a5edbcb1e0a", size = 360634, upload-time = "2025-11-02T21:39:45.798Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/bf8ad26169ebd0b2746d5c7564db734453ca467f8aab87e9d43b0a794383/bitarray-3.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9858dcbc23ba7eaadcd319786b982278a1a2b2020720b19db43e309579ff76fb", size = 371992, upload-time = "2025-11-02T21:39:46.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/16/ce166754e7c9d10650e02914552fa637cf3b2591f7ed16632bbf6b783312/bitarray-3.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa7dec53c25f1949513457ef8b0ea1fb40e76c672cc4d2daa8ad3c8d6b73491a", size = 340315, upload-time = "2025-11-02T21:39:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/de/2a/fbba3a106ddd260e84b9a624f730257c32ba51a8a029565248dfedfdf6f2/bitarray-3.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15a2eff91f54d2b1f573cca8ca6fb58763ce8fea80e7899ab028f3987ef71cd5", size = 330473, upload-time = "2025-11-02T21:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/68/97/56cf3c70196e7307ad32318a9d6ed969dbdc6a4534bbe429112fa7dfe42e/bitarray-3.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b1572ee0eb1967e71787af636bb7d1eb9c6735d5337762c450650e7f51844594", size = 358129, upload-time = "2025-11-02T21:39:51.189Z" }, + { url = "https://files.pythonhosted.org/packages/fd/be/afd391a5c0896d3339613321b2f94af853f29afc8bd3fbc327431244c642/bitarray-3.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5bfac7f236ba1a4d402644bdce47fb9db02a7cf3214a1f637d3a88390f9e5428", size = 356005, upload-time = "2025-11-02T21:39:52.355Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a8e1a371babba29bad3378bb3a2cdca2b012170711e7fe1f22031a6b7b95/bitarray-3.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f0a55cf02d2cdd739b40ce10c09bbdd520e141217696add7a48b56e67bdfdfe6", size = 336862, upload-time = "2025-11-02T21:39:54.345Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/6dc1d0fdc06991c8dc3b1fcfe1ae49fbaced42064cd1b5f24278e73fe05f/bitarray-3.8.0-cp313-cp313-win32.whl", hash = "sha256:a2ba92f59e30ce915e9e79af37649432e3a212ddddf416d4d686b1b4825bcdb2", size = 143018, upload-time = "2025-11-02T21:39:56.361Z" }, + { url = "https://files.pythonhosted.org/packages/2e/72/76e13f5cd23b8b9071747909663ce3b02da24a5e7e22c35146338625db35/bitarray-3.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f2a5d8006db5a555e06f9437e76bf52537d3dfd130cb8ae2b30866aca32c9", size = 149977, upload-time = "2025-11-02T21:39:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/01/37/60f336c32336cc3ec03b0c61076f16ea2f05d5371c8a56e802161d218b77/bitarray-3.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:50ddbe3a7b4b6ab96812f5a4d570f401a2cdb95642fd04c062f98939610bbeee", size = 146930, upload-time = "2025-11-02T21:39:59.308Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b0/411327a6c7f6b2bead64bb06fe60b92e0344957ec1ab0645d5ccc25fdafe/bitarray-3.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8cbd4bfc933b33b85c43ef4c1f4d5e3e9d91975ea6368acf5fbac02bac06ea89", size = 148563, upload-time = "2025-11-02T21:40:01.006Z" }, + { url = "https://files.pythonhosted.org/packages/2a/bc/ff80d97c627d774f879da0ea93223adb1267feab7e07d5c17580ffe6d632/bitarray-3.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9d35d8f8a1c9ed4e2b08187b513f8a3c71958600129db3aa26d85ea3abfd1310", size = 145422, upload-time = "2025-11-02T21:40:02.535Z" }, + { url = "https://files.pythonhosted.org/packages/66/e7/b4cb6c5689aacd0a32f3aa8a507155eaa33528c63de2f182b60843fbf700/bitarray-3.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f55e14e7c56f4fafe1343480c32b110ef03836c21ff7c48bae7add6818f77c", size = 332852, upload-time = "2025-11-02T21:40:03.645Z" }, + { url = "https://files.pythonhosted.org/packages/e7/91/fbd1b047e3e2f4b65590f289c8151df1d203d75b005f5aae4e072fe77d76/bitarray-3.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dfbe2aa45b273f49e715c5345d94874cb65a28482bf231af408891c260601b8d", size = 360801, upload-time = "2025-11-02T21:40:04.827Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4a/63064c593627bac8754fdafcb5343999c93ab2aeb27bcd9d270a010abea5/bitarray-3.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64af877116edf051375b45f0bda648143176a017b13803ec7b3a3111dc05f4c5", size = 371408, upload-time = "2025-11-02T21:40:05.985Z" }, + { url = "https://files.pythonhosted.org/packages/46/97/ddc07723767bdafd170f2ff6e173c940fa874192783ee464aa3c1dedf07d/bitarray-3.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cdfbb27f2c46bb5bbdcee147530cbc5ca8ab858d7693924e88e30ada21b2c5e2", size = 340033, upload-time = "2025-11-02T21:40:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1e/e1ea9f1146fd4af032817069ff118918d73e5de519854ce3860e2ed560ff/bitarray-3.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4d73d4948dcc5591d880db8933004e01f1dd2296df9de815354d53469beb26fe", size = 330774, upload-time = "2025-11-02T21:40:08.496Z" }, + { url = "https://files.pythonhosted.org/packages/cf/9f/8242296c124a48d1eab471fd0838aeb7ea9c6fd720302d99ab7855d3e6d3/bitarray-3.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:28a85b056c0eb7f5d864c0ceef07034117e8ebfca756f50648c71950a568ba11", size = 358337, upload-time = "2025-11-02T21:40:10.035Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6b/9095d75264c67d479f298c80802422464ce18c3cdd893252eeccf4997611/bitarray-3.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:79ec4498a545733ecace48d780d22407411b07403a2e08b9a4d7596c0b97ebd7", size = 355639, upload-time = "2025-11-02T21:40:11.485Z" }, + { url = "https://files.pythonhosted.org/packages/a0/af/c93c0ae5ef824136e90ac7ddf6cceccb1232f34240b2f55a922f874da9b4/bitarray-3.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:33af25c4ff7723363cb8404dfc2eefeab4110b654f6c98d26aba8a08c745d860", size = 336999, upload-time = "2025-11-02T21:40:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/81/0f/72c951f5997b2876355d5e671f78dd2362493254876675cf22dbd24389ae/bitarray-3.8.0-cp314-cp314-win32.whl", hash = "sha256:2c3bb96b6026643ce24677650889b09073f60b9860a71765f843c99f9ab38b25", size = 142169, upload-time = "2025-11-02T21:40:14.031Z" }, + { url = "https://files.pythonhosted.org/packages/8a/55/ef1b4de8107bf13823da8756c20e1fbc9452228b4e837f46f6d9ddba3eb3/bitarray-3.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:847c7f61964225fc489fe1d49eda7e0e0d253e98862c012cecf845f9ad45cdf4", size = 148737, upload-time = "2025-11-02T21:40:15.436Z" }, + { url = "https://files.pythonhosted.org/packages/5f/26/bc0784136775024ac56cc67c0d6f9aa77a7770de7f82c3a7c9be11c217cd/bitarray-3.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:a2cb35a6efaa0e3623d8272471371a12c7e07b51a33e5efce9b58f655d864b4e", size = 146083, upload-time = "2025-11-02T21:40:17.135Z" }, + { url = "https://files.pythonhosted.org/packages/6e/64/57984e64264bf43d93a1809e645972771566a2d0345f4896b041ce20b000/bitarray-3.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:15e8d0597cc6e8496de6f4dea2a6880c57e1251502a7072f5631108a1aa28521", size = 149455, upload-time = "2025-11-02T21:40:18.558Z" }, + { url = "https://files.pythonhosted.org/packages/81/c0/0d5f2eaef1867f462f764bdb07d1e116c33a1bf052ea21889aefe4282f5b/bitarray-3.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8ffe660e963ae711cb9e2b8d8461c9b1ad6167823837fc17d59d5e539fb898fa", size = 146491, upload-time = "2025-11-02T21:40:19.665Z" }, + { url = "https://files.pythonhosted.org/packages/65/c6/bc1261f7a8862c0c59220a484464739e52235fd1e2afcb24d7f7d3fb5702/bitarray-3.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4779f356083c62e29b4198d290b7b17a39a69702d150678b7efff0fdddf494a8", size = 339721, upload-time = "2025-11-02T21:40:21.277Z" }, + { url = "https://files.pythonhosted.org/packages/81/d8/289ca55dd2939ea17b1108dc53bffc0fdc5160ba44f77502dfaae35d08c6/bitarray-3.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:025d133bf4ca8cf75f904eeb8ea946228d7c043231866143f31946a6f4dd0bf3", size = 367823, upload-time = "2025-11-02T21:40:22.463Z" }, + { url = "https://files.pythonhosted.org/packages/91/a2/61e7461ca9ac0fcb70f327a2e84b006996d2a840898e69037a39c87c6d06/bitarray-3.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:451f9958850ea98440d542278368c8d1e1ea821e2494b204570ba34a340759df", size = 377341, upload-time = "2025-11-02T21:40:23.789Z" }, + { url = "https://files.pythonhosted.org/packages/6c/87/4a0c9c8bdb13916d443e04d8f8542eef9190f31425da3c17c3478c40173f/bitarray-3.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d79f659965290af60d6acc8e2716341865fe74609a7ede2a33c2f86ad893b8f", size = 344985, upload-time = "2025-11-02T21:40:25.261Z" }, + { url = "https://files.pythonhosted.org/packages/17/4c/ff9259b916efe53695b631772e5213699c738efc2471b5ffe273f4000994/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fbf05678c2ae0064fb1b8de7e9e8f0fc30621b73c8477786dd0fb3868044a8c8", size = 336796, upload-time = "2025-11-02T21:40:26.942Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4b/51b2468bbddbade5e2f3b8d5db08282c5b309e8687b0f02f75a8b5ff559c/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:c396358023b876cff547ce87f4e8ff8a2280598873a137e8cc69e115262260b8", size = 365085, upload-time = "2025-11-02T21:40:28.224Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/53473bfc2e052c6dbb628cdc1b156be621c77aaeb715918358b01574be55/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed3493a369fe849cce98542d7405c88030b355e4d2e113887cb7ecc86c205773", size = 361012, upload-time = "2025-11-02T21:40:29.635Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/242bf2e44bfc69e73fa2b954b425d761a8e632f78ea31008f1c3cfad0854/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c764fb167411d5afaef88138542a4bfa28bd5e5ded5e8e42df87cef965efd6e9", size = 340644, upload-time = "2025-11-02T21:40:31.089Z" }, + { url = "https://files.pythonhosted.org/packages/cf/01/12e5ecf30a5de28a32485f226cad4b8a546845f65f755ce0365057ab1e92/bitarray-3.8.0-cp314-cp314t-win32.whl", hash = "sha256:e12769d3adcc419e65860de946df8d2ed274932177ac1cdb05186e498aaa9149", size = 143630, upload-time = "2025-11-02T21:40:32.351Z" }, + { url = "https://files.pythonhosted.org/packages/b6/92/6b6ade587b08024a8a890b07724775d29da9cf7497be5c3cbe226185e463/bitarray-3.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0ca70ccf789446a6dfde40b482ec21d28067172cd1f8efd50d5548159fccad9e", size = 150250, upload-time = "2025-11-02T21:40:33.596Z" }, + { url = "https://files.pythonhosted.org/packages/ed/40/be3858ffed004e47e48a2cefecdbf9b950d41098b780f9dc3aa609a88351/bitarray-3.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2a3d1b05ffdd3e95687942ae7b13c63689f85d3f15c39b33329e3cb9ce6c015f", size = 147015, upload-time = "2025-11-02T21:40:35.064Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "44.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/4c/45dfa6829acffa344e3967d6006ee4ae8be57af746ae2eba1c431949b32c/cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", size = 710657, upload-time = "2024-11-27T18:07:10.168Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/09/8cc67f9b84730ad330b3b72cf867150744bf07ff113cda21a15a1c6d2c7c/cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", size = 6541833, upload-time = "2024-11-27T18:05:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5b/3759e30a103144e29632e7cb72aec28cedc79e514b2ea8896bb17163c19b/cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", size = 3922710, upload-time = "2024-11-27T18:05:58.621Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/3b14bf39f1a0cfd679e753e8647ada56cddbf5acebffe7db90e184c76168/cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", size = 4137546, upload-time = "2024-11-27T18:06:01.062Z" }, + { url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420, upload-time = "2024-11-27T18:06:03.487Z" }, + { url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498, upload-time = "2024-11-27T18:06:05.763Z" }, + { url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569, upload-time = "2024-11-27T18:06:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721, upload-time = "2024-11-27T18:06:11.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915, upload-time = "2024-11-27T18:06:13.515Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925, upload-time = "2024-11-27T18:06:16.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/b1/50d7739254d2002acae64eed4fc43b24ac0cc44bf0a0d388d1ca06ec5bb1/cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", size = 3202055, upload-time = "2024-11-27T18:06:19.113Z" }, + { url = "https://files.pythonhosted.org/packages/11/18/61e52a3d28fc1514a43b0ac291177acd1b4de00e9301aaf7ef867076ff8a/cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", size = 6542801, upload-time = "2024-11-27T18:06:21.431Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/5f165b6c65696ef75601b781a280fc3b33f1e0cd6aa5a92d9fb96c410e97/cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", size = 3922613, upload-time = "2024-11-27T18:06:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/6b3ac1d80fc174812486561cf25194338151780f27e438526f9c64e16869/cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", size = 4137925, upload-time = "2024-11-27T18:06:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417, upload-time = "2024-11-27T18:06:28.959Z" }, + { url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160, upload-time = "2024-11-27T18:06:30.866Z" }, + { url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331, upload-time = "2024-11-27T18:06:33.432Z" }, + { url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372, upload-time = "2024-11-27T18:06:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657, upload-time = "2024-11-27T18:06:41.045Z" }, + { url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672, upload-time = "2024-11-27T18:06:43.566Z" }, + { url = "https://files.pythonhosted.org/packages/97/9b/443270b9210f13f6ef240eff73fd32e02d381e7103969dc66ce8e89ee901/cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", size = 3202071, upload-time = "2024-11-27T18:06:45.586Z" }, + { url = "https://files.pythonhosted.org/packages/77/d4/fea74422326388bbac0c37b7489a0fcb1681a698c3b875959430ba550daa/cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", size = 3338857, upload-time = "2024-11-27T18:06:48.88Z" }, + { url = "https://files.pythonhosted.org/packages/1a/aa/ba8a7467c206cb7b62f09b4168da541b5109838627f582843bbbe0235e8e/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4", size = 3850615, upload-time = "2024-11-27T18:06:50.774Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/b160e10a64cc395d090105be14f399b94e617c879efd401188ce0fea39ee/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", size = 4081622, upload-time = "2024-11-27T18:06:55.126Z" }, + { url = "https://files.pythonhosted.org/packages/47/8f/20ff0656bb0cf7af26ec1d01f780c5cfbaa7666736063378c5f48558b515/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", size = 3867546, upload-time = "2024-11-27T18:06:57.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/d9/28edf32ee2fcdca587146bcde90102a7319b2f2c690edfa627e46d586050/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", size = 4090937, upload-time = "2024-11-27T18:07:00.338Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9d/37e5da7519de7b0b070a3fedd4230fe76d50d2a21403e0f2153d70ac4163/cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", size = 3128774, upload-time = "2024-11-27T18:07:02.157Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +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 = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastapi" +version = "0.115.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "genai-prices" +version = "0.0.57" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/30/11f3d683cf3b1d9612475ad8bfffe3423ce9f50fc617733109033e73a038/genai_prices-0.0.57.tar.gz", hash = "sha256:6e101e9c53975557ceffa237b0995787d81fe75aac12410f2898504188bcad89", size = 66555, upload-time = "2026-04-21T13:42:52.554Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/fe/d0095040c120d97cb63d055224ecd4e913dc5655315c203c8e83bf13aa86/genai_prices-0.0.57-py3-none-any.whl", hash = "sha256:14e50fb69cdc5a06ddb2a6df5a7fe06741b9e44304ce3f1728f56abdf1856cca", size = 69654, upload-time = "2026-04-21T13:42:51.236Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.65.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/3b/1599ceafa875ffb951480c8c74f4b77646a6b80e80970698f2aa93c216ce/googleapis_common_protos-1.65.0.tar.gz", hash = "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0", size = 113657, upload-time = "2024-08-27T16:16:54.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/08/49bfe7cf737952cc1a9c43e80cc258ed45dad7f183c5b8276fc94cb3862d/googleapis_common_protos-1.65.0-py2.py3-none-any.whl", hash = "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63", size = 220890, upload-time = "2024-08-27T16:16:52.675Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, +] + +[[package]] +name = "grpc-interceptor" +version = "0.15.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/28/57449d5567adf4c1d3e216aaca545913fbc21a915f2da6790d6734aac76e/grpc-interceptor-0.15.4.tar.gz", hash = "sha256:1f45c0bcb58b6f332f37c637632247c9b02bc6af0fdceb7ba7ce8d2ebbfb0926", size = 19322, upload-time = "2023-11-16T02:05:42.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/ac/8d53f230a7443401ce81791ec50a3b0e54924bf615ad287654fa4a2f5cdc/grpc_interceptor-0.15.4-py3-none-any.whl", hash = "sha256:0035f33228693ed3767ee49d937bac424318db173fef4d2d0170b3215f254d9d", size = 20848, upload-time = "2023-11-16T02:05:40.913Z" }, +] + +[[package]] +name = "grpcio" +version = "1.64.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/0a/4cf11fe9861ff7a5a43fb558e3df9455a652a2b669287cc47bb11cab8e9a/grpcio-1.64.3.tar.gz", hash = "sha256:f37a0297293918c695e625d7148f99f4e401298d1b6e2bea7a8e9130aa940419", size = 12211207, upload-time = "2024-08-06T00:09:00.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/2a/e880b7098e6837e512bd7791c0a6cbc29f70f382a8b98300d5d8e6b493c7/grpcio-1.64.3-cp310-cp310-linux_armv7l.whl", hash = "sha256:32b6d78f378df38914cbb6340cec5e02ed78cb3c9cc9f7db3bb8c8132ccd1a9a", size = 4825375, upload-time = "2024-08-06T00:03:08.171Z" }, + { url = "https://files.pythonhosted.org/packages/52/13/6e66362966a1002de588ec00ff27d1851e5fe0f72efd27a863f4e4b4f013/grpcio-1.64.3-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:5900314a91ac4b4bad70b64c7ccd013605dcbad92a1e28f73b54dd7d1d32f09e", size = 10319599, upload-time = "2024-08-06T00:03:11.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/fb/3752df95412751936debb5f72c6d4dcba25f1165748bc01fdee588a75e5e/grpcio-1.64.3-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:b6bb9d6180fc71a32a0608724f80f40d3c7e26910b65e9dd88e7c38d8400214f", size = 5345521, upload-time = "2024-08-06T00:03:15.26Z" }, + { url = "https://files.pythonhosted.org/packages/3f/32/0a15eeff14cea9b46de9d94a019648fb2d1282f0a1b8f2d2bb314918f752/grpcio-1.64.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efaadc7b76d8475aec84cbf79169457960274f018fb53e7da19eb752d0d6e924", size = 5904542, upload-time = "2024-08-06T00:03:18.777Z" }, + { url = "https://files.pythonhosted.org/packages/c7/89/c400134893472afba31b8ca1208dd46f86411f63cf4933bd437d3068b1d8/grpcio-1.64.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f6c9b6b91dcfe68fe50b15ea89bc602feb597b5191631ac3b3353d5dddc5a0d", size = 5605544, upload-time = "2024-08-06T00:03:21.385Z" }, + { url = "https://files.pythonhosted.org/packages/d6/09/e28b1038f29bc247a1d4ee229e9853cc1ef79fbb50bc80839d550a47f8ca/grpcio-1.64.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0d25eee06cfdfe2387d3394b313acee5a22148613a7d8dd3f994b369c019fc92", size = 6216355, upload-time = "2024-08-06T00:03:24.292Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/f38fc947b8256c2a9f03864e16f174bb755577714675aa6efd3b820b010b/grpcio-1.64.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7d613f6f4bfb4c475201c35bcdfd596ddfa0ed41906441dd514d5a972c7f364a", size = 5850415, upload-time = "2024-08-06T00:03:27.34Z" }, + { url = "https://files.pythonhosted.org/packages/63/53/09d8b8d5112e8c9a5132b5e1f94c3763164d3ebabbc36f9ba9662cdca38b/grpcio-1.64.3-cp310-cp310-win32.whl", hash = "sha256:577249998c8f6db7275413431f05717be0fdc258a1f427827967a9fe21f83ad4", size = 3355768, upload-time = "2024-08-06T00:03:30.437Z" }, + { url = "https://files.pythonhosted.org/packages/8b/1f/1b9af38c2a65d0e381014c0d96fd6398fe3bdeb9ece4353a57762c8875d9/grpcio-1.64.3-cp310-cp310-win_amd64.whl", hash = "sha256:9b11173fae31abd5ce81315696bad87daed5bdb74160e3cacd4bec9c352870d7", size = 4031116, upload-time = "2024-08-06T00:03:34.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/88/7bf48942bf07cc59d708e4f1471a59baa2b8ffdb11b0c8839f49bd5fbd89/grpcio-1.64.3-cp311-cp311-linux_armv7l.whl", hash = "sha256:56d21c7392aaf7c193a4ba1341974400cf268941007203b05e9bee707d0f2d83", size = 4834245, upload-time = "2024-08-06T00:03:36.901Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/bf6969ef0b441210be6a9bf51b13e7c278019f9b71eb23ad2fa35089b76f/grpcio-1.64.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:145069b1ee5ee8bb1060f32b00f0f462838064879788cbcbc43d599cdbf5ab9e", size = 10347783, upload-time = "2024-08-06T00:03:39.699Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6d/0e7cc8e50bf74abdcb03f7f00c9fc99a18cbd6b6259d88efd4a4da9fdf9f/grpcio-1.64.3-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:a9f8a8c4cfbd43a44e6415e42995d7dbb8b98cb3a9d88eff34291ef670e69121", size = 5348808, upload-time = "2024-08-06T00:03:42.894Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a2/0542a72149087aafc572545efbacc305e2b0608c4be0ff5f0b6164654351/grpcio-1.64.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6902b9ebfc833a927aa0f41fe1ffa986a2666ae96c909c7d0cf265cabc78ce93", size = 5907194, upload-time = "2024-08-06T00:03:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/a2/97/027ea004e499ec5de032a0f910b2d8dbc1952c64308cc72389aeaed24925/grpcio-1.64.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee2987148d689f14f53b8cdb3c2545d42826343e38d0d31c00ab9249ecbe579d", size = 5604252, upload-time = "2024-08-06T00:03:48.362Z" }, + { url = "https://files.pythonhosted.org/packages/9e/08/4b3e58d262527049d59a5ad379b3c4f103b744726d73dcaf4324c2de53b0/grpcio-1.64.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a87076b01979d7e0297f6cd79d6ad90f305bd0168a2d217c6ae9870023f76776", size = 6221507, upload-time = "2024-08-06T00:03:51.725Z" }, + { url = "https://files.pythonhosted.org/packages/64/8c/959cbed18b21bc428f44a6a002d31c2ff7ebd5487c30377a53252673131c/grpcio-1.64.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fe357d7b2114568b55dde795c324c272e0029a48fdcb7c2eb5ee06a311c19b91", size = 5849986, upload-time = "2024-08-06T00:03:54.354Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/f06034343ba9e560f7a13b63dd921190b8307a17185126589b0dd0340b01/grpcio-1.64.3-cp311-cp311-win32.whl", hash = "sha256:48933a53b57941ed02a2c97a5a821872fec80d4240c936df7800a5af0c89263f", size = 3355438, upload-time = "2024-08-06T00:03:56.925Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2b/d66092eefaa933ac2f5be31ed1e84e22fd6f0f227fc5b2c28781cc548c51/grpcio-1.64.3-cp311-cp311-win_amd64.whl", hash = "sha256:5bbeea3aac7dc25fdbf39a42cd99e7b8cce9ad248ed99747c403de540fc1157d", size = 4032873, upload-time = "2024-08-06T00:04:02.903Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/90ab76e7bd7f3cb69b71fef5e5c86876f687227ab263ef5e7455845e45c5/grpcio-1.64.3-cp312-cp312-linux_armv7l.whl", hash = "sha256:90dc5acc2059737b98b849b910fde8ff83467fe5d791042333d007136085c7e0", size = 4779889, upload-time = "2024-08-06T00:04:06.787Z" }, + { url = "https://files.pythonhosted.org/packages/13/44/c0a88ccc456697946e0a6419d092b27ad617aec69f31f5fd883be5962674/grpcio-1.64.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f5674f0c9ff5af675a85e1ff03546f938b1e9f28022535e18482835165016cc7", size = 10306622, upload-time = "2024-08-06T00:04:10.463Z" }, + { url = "https://files.pythonhosted.org/packages/10/2f/2fa0c92a381fbf83b9182eba1bb7f1649337e1c2d92475bda13fec9ff95e/grpcio-1.64.3-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:9a6a1c0b5133fdcde4473112000eae10d04cafd8bcd6d0a1fe01b04535e24f49", size = 5286631, upload-time = "2024-08-06T00:04:13.561Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/0e6e269b78a92bfe1a3bea8c3c6731442fb5673ab85ac67fee3f8f8dc307/grpcio-1.64.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb19ec6c0b3064a8f94e0842804be185ce2c7a872ea45327f4c7b626b67b663b", size = 5843819, upload-time = "2024-08-06T00:04:16.481Z" }, + { url = "https://files.pythonhosted.org/packages/d4/b9/d05c50a427fe6d018a0f09e32cd49dfe026b0f62dee63641cd25abe489a9/grpcio-1.64.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54fe53b503746f76981b96e7c6c8fde7d3cc1fdfd804e7aa399e7eb0d24d5b65", size = 5549312, upload-time = "2024-08-06T00:04:20.428Z" }, + { url = "https://files.pythonhosted.org/packages/af/a1/1b1150b5320c3a5324d7e74e56935977e8c8d76f0a70fe4e66f782433af4/grpcio-1.64.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:448cb624544caa17f90acce094b6cfab8f7f788c616be591114d679c580e8485", size = 6162839, upload-time = "2024-08-06T00:04:24.32Z" }, + { url = "https://files.pythonhosted.org/packages/22/f9/7a71b0e44065e8016e4b64c312332e6d22e182ac6fb25cb2acf2cb993f0c/grpcio-1.64.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:786f6c15d648b29ef25278de9026c9f2042903b9e875830b64f27816e3055b93", size = 5795676, upload-time = "2024-08-06T00:04:27.374Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b3/95680d5a20c1442a60f6f5c78dcff77665a12aa49e068c7c3f47bc00632f/grpcio-1.64.3-cp312-cp312-win32.whl", hash = "sha256:7599dc7d4ff6079612386fe93f45a98c3f2bea66e59bcf1c5de811d2c4da8084", size = 3341433, upload-time = "2024-08-06T00:04:30.458Z" }, + { url = "https://files.pythonhosted.org/packages/23/95/04f134dc25ba9be6c51c63da8bc08a8b3a3ffbcb52d73ffe39eb4187112d/grpcio-1.64.3-cp312-cp312-win_amd64.whl", hash = "sha256:2b3154eb0cb1db36934c7fbb7686698650a607b9581bad103101ed86462e369e", size = 4022214, upload-time = "2024-08-06T00:04:33.984Z" }, +] + +[[package]] +name = "grpcio-health-checking" +version = "1.64.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/bb/e15b4fbfb947d290bb4b658c2999141fc56224e75a03fdfda18fb7a64cd0/grpcio_health_checking-1.64.3.tar.gz", hash = "sha256:72054ca6ddf31e256a3a4380c375fcee004e8c059331e8368f22b44a3e7886e6", size = 16627, upload-time = "2024-08-06T00:09:07.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/76/47881ef4c8149da5669f0f31f0b84b3ce067421ffbfa87702d51575b0195/grpcio_health_checking-1.64.3-py3-none-any.whl", hash = "sha256:a538815c07ec964d6bb708d0fb3527812ac39d3bc5c85ffa88a30f42997a5432", size = 18948, upload-time = "2024-08-06T00:05:45.825Z" }, +] + +[[package]] +name = "grpcio-reflection" +version = "1.64.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/fd/9f8733fcffe41ece93d7d678cbf942ecc6372ea6b8c7aca8da8230cda571/grpcio_reflection-1.64.3.tar.gz", hash = "sha256:194c4f4701fb0df1a2ed716c44f40d187e71c60ff3fb508bc95d376d3081b9e4", size = 18665, upload-time = "2024-08-06T00:09:10.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/78/8ebca5a6d6c596e0719686f629c2843be55e9daf2ca320402008332f9216/grpcio_reflection-1.64.3-py3-none-any.whl", hash = "sha256:ffcf43c859e48a09dc5c1a14a6d780890bfe840648175b693e5372b6d3029ed6", size = 22635, upload-time = "2024-08-06T00:06:34.248Z" }, +] + +[[package]] +name = "grpcio-status" +version = "1.64.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f4/829dda7759e796381b4afc8d8dfffa2904cabd13137a0debf9c371b3e80d/grpcio_status-1.64.3.tar.gz", hash = "sha256:3827094d31161d8905c16625d7443d6e7d9a1cc4e0e0e6b5cc8b707402832d54", size = 13522, upload-time = "2024-08-06T00:09:12.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/6d/5ad42b26ed315de8b76f4220ec3d9f7c6dd5b89cbe138c2333a8f7261b4c/grpcio_status-1.64.3-py3-none-any.whl", hash = "sha256:317624a36b559eb887592617e094746abbbd4e18c2fb96366b46f879d00ef5bc", size = 14445, upload-time = "2024-08-06T00:06:35.909Z" }, +] + +[[package]] +name = "grpcio-tools" +version = "1.64.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/c5/a37dc0ae8571d0c2efdf10fbcf4e3a2dce1feade58c405ad184be023c017/grpcio_tools-1.64.3.tar.gz", hash = "sha256:b25bcef86da6c5f98aed4e5269df072be142ee6ed13588bb868441aa39005543", size = 5027614, upload-time = "2024-08-06T00:09:15.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/fd/ba085353d810fe73834d6b066fa3c45ac13c6d9548dfffb19749a7a8e952/grpcio_tools-1.64.3-cp310-cp310-linux_armv7l.whl", hash = "sha256:b465bd64800e127068498e8f2df24b1fc6a5918c31c6819a509411d09fb2e963", size = 2238326, upload-time = "2024-08-06T00:06:39.644Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/addd107a32b5d99231d3a0213c3a41898f88801d31a88c622f5ed40ec190/grpcio_tools-1.64.3-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:725bdaf814c4a910e6e077ad72c08754d99d7fb554b225bdb7bafa8b7639edaf", size = 5322796, upload-time = "2024-08-06T00:06:42.727Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8d/6cd9d6c9b83b247fdbfc53aa3e9444c0410abcc057038e5a9d3217cdd972/grpcio_tools-1.64.3-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:f49f6f29163b20c149f2392e218562cf374573285f3e06590e59bb81b166bb41", size = 2212473, upload-time = "2024-08-06T00:06:46.212Z" }, + { url = "https://files.pythonhosted.org/packages/a9/fa/fa8119d42824924f1b628e374ffba4d6352a1d9af3818b30aede9bd19424/grpcio_tools-1.64.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:551154647d2cc754e3be53ec08c18e47061eeec137ac8a29255b36bdf1709c80", size = 2546874, upload-time = "2024-08-06T00:06:48.327Z" }, + { url = "https://files.pythonhosted.org/packages/45/71/986e0ad582c1dcf765ea527ea5232213726cea62a2e62a2833b41be5fd14/grpcio_tools-1.64.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6537aef96077767b93c6367e2a3ec3f899a505bebf19e71cb76294752b881f2", size = 2348796, upload-time = "2024-08-06T00:06:50.556Z" }, + { url = "https://files.pythonhosted.org/packages/07/80/1aefa7da520b5221644badf8417432f296da7902cd9decaa24c40a8967b6/grpcio_tools-1.64.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bac2ffae8c0479ab071057c2d8587b22fcc41d916124d98d419a99f58ca7c6e5", size = 3142491, upload-time = "2024-08-06T00:06:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/3d/07/e672ac73b87f33cff389078081b46f21a2d65d191d5079e987ad1526c5ca/grpcio_tools-1.64.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:852a0eaa25933f2e8c241a053b69d323c9284d0bd63228fb2efb37c37553cc14", size = 2795835, upload-time = "2024-08-06T00:06:55.607Z" }, + { url = "https://files.pythonhosted.org/packages/02/06/50bd6c15ab5d0f0c3270e981399f5474d72d39acf2b95ae9b0115c35c834/grpcio_tools-1.64.3-cp310-cp310-win32.whl", hash = "sha256:a2d6074fceb34625a6ab6b514ad7931609844de95f69b12064c79a70975d9445", size = 923569, upload-time = "2024-08-06T00:06:58.16Z" }, + { url = "https://files.pythonhosted.org/packages/89/f1/15ef33bfd1a949c5d414921ba1ddb702fcd22ef2ab65cfbd495d4f7a2a52/grpcio_tools-1.64.3-cp310-cp310-win_amd64.whl", hash = "sha256:7efa62e0ed4471d98be810567afad8b44bcf0584f99c1ecaa68b75696460b54d", size = 1074721, upload-time = "2024-08-06T00:07:00.298Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/c351716492019b2fd61c998e56b35153dafb110e62749d5e699f1bb5c2f1/grpcio_tools-1.64.3-cp311-cp311-linux_armv7l.whl", hash = "sha256:ab2eb0acd08540b86fb9d1603f27e111a3fab6e92368ac3df5c4242b867420e7", size = 2238348, upload-time = "2024-08-06T00:07:03.19Z" }, + { url = "https://files.pythonhosted.org/packages/05/c7/71f44bd23dd1a2abe8136d6d2e27e1cbb1fc7dec0b658225bd87dc531f46/grpcio_tools-1.64.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2462e091a9734b35a445bd9b98f3213a5779ab10652e42a4ddff1082ced0e54e", size = 5347814, upload-time = "2024-08-06T00:07:05.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/23/77a5798ae500c69aaea115a3c541346fc09aa9c52fdd414c541aca52793e/grpcio_tools-1.64.3-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:5afd3598fc74c220c5e611e227470c661e9f8fe7953240765b35f9fe1a21d997", size = 2212504, upload-time = "2024-08-06T00:07:08.17Z" }, + { url = "https://files.pythonhosted.org/packages/17/ef/fd383e3ab913f57ea4747c301f787c7c487e2eae4617994515e2baa7c712/grpcio_tools-1.64.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e2d104b39d866b60de6266f253b2c56fa54f3c90da367ecfdd340edcda226d1", size = 2547096, upload-time = "2024-08-06T00:07:10.462Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3b/d862dea98963122325783a9579cb87ee812e3e006d1414924b8e50dac4ab/grpcio_tools-1.64.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a559215ac287dbdd6b2598ea0948a072684e916ea91e25dde8318c4ca8a9a13", size = 2348685, upload-time = "2024-08-06T00:07:13.696Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/16590993e0036f9480daa755687b2f864b4d9d84702975c7ea23eefe7e02/grpcio_tools-1.64.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:597a76ea1265538cc70d992aa6afe0c9fb8860689d2ef8832002cff4f852d4ef", size = 3143066, upload-time = "2024-08-06T00:07:16.034Z" }, + { url = "https://files.pythonhosted.org/packages/ca/72/68feb0ca8c049f032b1fc1421402f42d90d5a0836019b4df97b82c35af50/grpcio_tools-1.64.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f914308c89bed10337dd1ec800a7f3d870003e352fd059ec07851d46bb1562af", size = 2795983, upload-time = "2024-08-06T00:07:18.828Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d6/2c688c55835770de2cef1a1628b7d36cb46d8c441cdf5ba8130734c2b762/grpcio_tools-1.64.3-cp311-cp311-win32.whl", hash = "sha256:e44f1a4bc1eeb9d0736fbe9f6b66ec41a32301cbe7d8d98b64f3e939c8010aaa", size = 923361, upload-time = "2024-08-06T00:07:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/4a/17/73bbb6b30ddb6da4fdb28a09954510ae23553a0d9f4c8cf931b82018b4c1/grpcio_tools-1.64.3-cp311-cp311-win_amd64.whl", hash = "sha256:cbd3f1f17b2a472c5356cbadce437293408f8244777e6ac916c4f4c5f5576df1", size = 1074918, upload-time = "2024-08-06T00:07:23.786Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d5/f7d01ccc0a258abfce34a16a1a60ce154dbb81f65079d421f20cd80fe88b/grpcio_tools-1.64.3-cp312-cp312-linux_armv7l.whl", hash = "sha256:21dde212bdcffa7884269db13cbd7a7bb548560c2705a5f8054e3fbbb6c64dcd", size = 2239195, upload-time = "2024-08-06T00:07:26.326Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/43618c5db6f794586af753b538f281fdec23bdfea801f3fdb8b80ab66a45/grpcio_tools-1.64.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a1e9d06804f6d510f51b14aadf7ee91cc1e5fa6d5aa2d09978c82084a1993ed9", size = 5347284, upload-time = "2024-08-06T00:07:29.264Z" }, + { url = "https://files.pythonhosted.org/packages/01/41/8a8cb5dedfb3060a08af682fb3a98f248d081ae7d839e6a1d37e44f13542/grpcio_tools-1.64.3-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:4c5ac82400f74fe8c2eece026be67d4372a6e51753f927e3be03dd36356f825e", size = 2214115, upload-time = "2024-08-06T00:07:32.594Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/a8237cbd883267d843a15d95da0f91f163554bb02d2db5b471656e69a017/grpcio_tools-1.64.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7153369c25dfb12966cf2ddaf343d4894849dad1c076edfcf3c0321ce5221e8", size = 2548255, upload-time = "2024-08-06T00:07:35.703Z" }, + { url = "https://files.pythonhosted.org/packages/61/95/2976acde8bd30d01bf4d199d4824ffe5396f298d87eb1c2c68605ab59022/grpcio_tools-1.64.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b45238cb3b8cccd0017d88d68c4a93849e5f29f475c9221eb53a1c09ecda582c", size = 2349244, upload-time = "2024-08-06T00:07:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/12/53/eda0a6210abcd76c14d219315d9cb5f07e8b3389fdda40326cc631592dc3/grpcio_tools-1.64.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8d2bfad41d0bfc9e0f5d782e7802bbec1adb00ea6b2a3ee76f4a39e319c1d8db", size = 3143766, upload-time = "2024-08-06T00:07:41.061Z" }, + { url = "https://files.pythonhosted.org/packages/3f/de/99c3e56a5192c269e7512499b669fc09a20db5576455afeaa51d733dc79d/grpcio_tools-1.64.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b8cb0d4746523166243b5e2c9406d3185760d0a68f0d9131bc9b9e14420cf3c9", size = 2796701, upload-time = "2024-08-06T00:07:43.43Z" }, + { url = "https://files.pythonhosted.org/packages/bc/14/e5ac91108600dc023698fc62e450f3f271e760264f8888616be5b9b80e2e/grpcio_tools-1.64.3-cp312-cp312-win32.whl", hash = "sha256:4ae1f2e4e059941ca19e98bf5af9d47f1f54bd52af730313ad7eda1001678cd3", size = 923060, upload-time = "2024-08-06T00:07:46.491Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e6/8edf7adf6bada51d79f6356d1843496d23f7e934c472533ef6ec2955df10/grpcio_tools-1.64.3-cp312-cp312-win_amd64.whl", hash = "sha256:9bb5f15ca02a27de26eddc08ae308376bc6f9ffe887e1a35d11b263b6342566d", size = 1074133, upload-time = "2024-08-06T00:07:48.969Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "idna" +version = "3.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/ff/75c28576a1d900e87eb6335b063fab47a8ef3c8b4d88524c4bf78f670cce/Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", size = 268239, upload-time = "2022-04-28T17:21:27.579Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", size = 133101, upload-time = "2022-04-28T17:21:25.336Z" }, +] + +[[package]] +name = "jinja2-strcase" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/87/723a3da34957c135f4274570bb769a47cd4b5e0fc008fc31ea7cb8aefd87/jinja2-strcase-0.0.2.tar.gz", hash = "sha256:d90c37f7bd40d345aacc8f78b087f66e6c5aa4c968ab23a791573fcf756c3379", size = 2857, upload-time = "2020-06-21T16:52:37.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/3f/821f2adeb240ffca16a3f89f56aa3733a99574a023e02ed31446bbdf4f61/jinja2_strcase-0.0.2-py2.py3-none-any.whl", hash = "sha256:e3067062f6158cd836ab495f805fc0d1d83cf92049e180509b004db86f6b745c", size = 3933, upload-time = "2020-06-21T16:52:35.979Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "kubernetes-asyncio" +version = "31.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "certifi" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "six" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/dc/4ca52809f3b662df8c5e6058b0beb489d8d6426d5c189c28ec5a3168fdd4/kubernetes_asyncio-31.1.0.tar.gz", hash = "sha256:00128a96eb0284de0cbee53bd2fe044593f2e1547c48d09901cddf9258adfd88", size = 962239, upload-time = "2024-09-14T22:22:18.717Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/2a/93b7a49598d660e259d0db8dd92b97e99910d5e7a349a8995797dd49be9d/kubernetes_asyncio-31.1.0-py3-none-any.whl", hash = "sha256:76898fea5dee601b209fefeae4ecee2fb20bfe3ebf872b5ff37c96230fbda6cc", size = 1902366, upload-time = "2024-09-14T22:22:16.811Z" }, +] + +[[package]] +name = "logfire-api" +version = "4.32.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/1b/0c74ad85f977743ba4c589e46e0cb138d6a6e69487830f4e86ebbdb145a3/logfire_api-4.32.1.tar.gz", hash = "sha256:5e8714b2bb5fb5d1f4a4a833941e4ca711b75d2c1f98e76c5ad680fe6991af6a", size = 78788, upload-time = "2026-04-15T14:11:58.788Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ab/d5adeab6253c7ecd5904fc5ef3265859f218610caf4e1e55efe9aff6ac49/logfire_api-4.32.1-py3-none-any.whl", hash = "sha256:4b4c27cf6e27e8e26ef4b22a77f2a2988dd1d07e2d24ee70673ef34b234fb8a5", size = 124394, upload-time = "2026-04-15T14:11:56.157Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mcp" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "mypy-protobuf" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, + { name = "types-protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/282d64d66bf48ce60e38a6560753f784e0f88ab245ac2fb5e93f701a36cd/mypy-protobuf-3.6.0.tar.gz", hash = "sha256:02f242eb3409f66889f2b1a3aa58356ec4d909cdd0f93115622e9e70366eca3c", size = 24445, upload-time = "2024-04-01T20:24:42.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/73/d6b999782ae22f16971cc05378b3b33f6a89ede3b9619e8366aa23484bca/mypy_protobuf-3.6.0-py3-none-any.whl", hash = "sha256:56176e4d569070e7350ea620262478b49b7efceba4103d468448f1d21492fd6c", size = 16434, upload-time = "2024-04-01T20:24:40.583Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "importlib-metadata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/f7/5f8771e591f7641ba019904e2a6be151998a6c8f3e1137654773ca060b04/opentelemetry_api-1.28.1.tar.gz", hash = "sha256:6fa7295a12c707f5aebef82da3d9ec5afe6992f3e42bfe7bec0339a44b3518e7", size = 62804, upload-time = "2024-11-08T19:25:21.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/39/7a9c2fde8e0309e9fd339aa953110a49ebbdf8797eb497d8357f1933ec5d/opentelemetry_api-1.28.1-py3-none-any.whl", hash = "sha256:bfe86c95576cf19a914497f439fd79c9553a38de0adbdc26f7cfc46b0c00b16c", size = 64316, upload-time = "2024-11-08T19:24:53.081Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/ff/99803ddffb90bc895b2f665fa9d79efee8fa9a0fe3cc6d318c19ce18b4d9/opentelemetry_exporter_otlp_proto_common-1.28.1.tar.gz", hash = "sha256:6e55e7f5d59296cc87a74c08b8e0ddf87403f73a62302ec7ee042c1a1f4a8f70", size = 19040, upload-time = "2024-11-08T19:25:24.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/b1/33d69035e87fbd7c962be00315c3ea2567a6a45be71946d2b3bf008719b3/opentelemetry_exporter_otlp_proto_common-1.28.1-py3-none-any.whl", hash = "sha256:56ea6cf28c90f767733f046a54525dc7271a25faff86b1955e5252b55f4e007f", size = 18452, upload-time = "2024-11-08T19:24:57.795Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/22/0b350c488c1a20c840b674f4256d8248efad3105211c0ef824c94b89e54f/opentelemetry_exporter_otlp_proto_grpc-1.28.1.tar.gz", hash = "sha256:9c84a103734d0c9cf9a4ba973d9c15c21996a554ab2bbd6208b3925873912642", size = 26234, upload-time = "2024-11-08T19:25:25.53Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/a2/80d2afb742dbbb61d21e3617aa4a45dde955f46fb5881614f300baccf8f1/opentelemetry_exporter_otlp_proto_grpc-1.28.1-py3-none-any.whl", hash = "sha256:fd494b9dd7869975138cef68d52ed45b9ca584c1fa31bef2d01ecfd537445dfa", size = 18532, upload-time = "2024-11-08T19:24:59.536Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.49b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/2c/ce74e9f484a07d13cc91c36dd75d76aee2e651ad95beb967e208f5c15988/opentelemetry_instrumentation-0.49b1.tar.gz", hash = "sha256:2d0e41181b7957ba061bb436b969ad90545ac3eba65f290830009b4264d2824e", size = 26465, upload-time = "2024-11-08T19:32:35.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/98/9c40915677f24b6bd0bd4ec6e84f929815a581d78cd67eab5213c630c6b6/opentelemetry_instrumentation-0.49b1-py3-none-any.whl", hash = "sha256:0a9d3821736104013693ef3b8a9d29b41f2f3a81ee2d8c9288b52d62bae5747c", size = 30688, upload-time = "2024-11-08T19:31:28.674Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-grpc" +version = "0.49b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/92/6950579438a2473b66571ce661ce85b5a3452aa4876b9d4ce946894987eb/opentelemetry_instrumentation_grpc-0.49b1.tar.gz", hash = "sha256:df7ee2f079d280f7d0ff78d43ff23593594165a93e368c75374c914af02a82ae", size = 30758, upload-time = "2024-11-08T19:32:52.905Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/bf/03b359eeec2325e9727742485359c94bf8604c6ba513c9e4ddff4726057f/opentelemetry_instrumentation_grpc-0.49b1-py3-none-any.whl", hash = "sha256:81a54369e807ac699b60be6c7ccb41b70262f84b0d74a5fce3e0b76df601e83c", size = 27088, upload-time = "2024-11-08T19:31:56.714Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/5d/da18070fbd436baa49bad9f1393b2346f650800aa5b3a7b2d3640510eb0e/opentelemetry_proto-1.28.1.tar.gz", hash = "sha256:6f9e9d9958822ab3e3cdcd2a24806d62aa10282349fd4338aafe32c69c87fc15", size = 34333, upload-time = "2024-11-08T19:25:36.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/cb/272d2ef811dba0b98d7dcd23687900d8ba6855fd289119c4cf44c1dc77c7/opentelemetry_proto-1.28.1-py3-none-any.whl", hash = "sha256:cb406ec69f1d11439e60fb43c6b744783fc8ee4deecdab61b3e29f112b0602f9", size = 55831, upload-time = "2024-11-08T19:25:13.991Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/c8/83996963ca80c149583260c22492022c9b48c854d4ca877aa3b6be8fbd3d/opentelemetry_sdk-1.28.1.tar.gz", hash = "sha256:100fa371b2046ffba6a340c18f0b2a0463acad7461e5177e126693b613a6ca57", size = 157162, upload-time = "2024-11-08T19:25:37.637Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/f3/09e86288ee3aace7306b2778127565f64c53d6ec1634dd67d128848d5a4f/opentelemetry_sdk-1.28.1-py3-none-any.whl", hash = "sha256:72aad7f5fcbe37113c4ab4899f6cdeb6ac77ed3e62f25a85e3627b12583dad0f", size = 118732, upload-time = "2024-11-08T19:25:16.015Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.49b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "opentelemetry-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/61/2715d9d24842ef2250cbd6a44198b6d134b6238d515c6b2f9042ea5aee63/opentelemetry_semantic_conventions-0.49b1.tar.gz", hash = "sha256:91817883b159ffb94c2ca9548509c4fe0aafce7c24f437aa6ac3fc613aa9a758", size = 95221, upload-time = "2024-11-08T19:25:39.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/1d/01ad9c2a8f8346258bf87c20fc024c8baa410492e2c6b397140383381a28/opentelemetry_semantic_conventions-0.49b1-py3-none-any.whl", hash = "sha256:dd6f3ac8169d2198c752e1a63f827e5f5e110ae9b0ce33f2aad9a3baf0739743", size = 159213, upload-time = "2024-11-08T19:25:18.022Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "protobuf" +version = "5.28.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/6e/e69eb906fddcb38f8530a12f4b410699972ab7ced4e21524ece9d546ac27/protobuf-5.28.3.tar.gz", hash = "sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b", size = 422479, upload-time = "2024-10-23T01:07:26.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c5/05163fad52d7c43e124a545f1372d18266db36036377ad29de4271134a6a/protobuf-5.28.3-cp310-abi3-win32.whl", hash = "sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24", size = 419624, upload-time = "2024-10-23T01:07:08.068Z" }, + { url = "https://files.pythonhosted.org/packages/9c/4c/4563ebe001ff30dca9d7ed12e471fa098d9759712980cde1fd03a3a44fb7/protobuf-5.28.3-cp310-abi3-win_amd64.whl", hash = "sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868", size = 431464, upload-time = "2024-10-23T01:07:11.819Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f2/baf397f3dd1d3e4af7e3f5a0382b868d25ac068eefe1ebde05132333436c/protobuf-5.28.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a3f6857551e53ce35e60b403b8a27b0295f7d6eb63d10484f12bc6879c715687", size = 414743, upload-time = "2024-10-23T01:07:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/85/50/cd61a358ba1601f40e7d38bcfba22e053f40ef2c50d55b55926aecc8fec7/protobuf-5.28.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:3fa2de6b8b29d12c61911505d893afe7320ce7ccba4df913e2971461fa36d584", size = 316511, upload-time = "2024-10-23T01:07:14.51Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ae/3257b09328c0b4e59535e497b0c7537d4954038bdd53a2f0d2f49d15a7c4/protobuf-5.28.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135", size = 316624, upload-time = "2024-10-23T01:07:16.192Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/2377c159e28ea89a91cf1ca223f827ae8deccb2c9c401e5ca233cd73002f/protobuf-5.28.3-py3-none-any.whl", hash = "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed", size = 169511, upload-time = "2024-10-23T01:07:24.738Z" }, +] + +[[package]] +name = "psutil" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/8c6872f7372eb6a6b2e4708b88419fb46b857f7a2e1892966b851cc79fc9/psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2", size = 508067, upload-time = "2024-06-18T21:40:10.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/37/f8da2fbd29690b3557cca414c1949f92162981920699cd62095a984983bf/psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0", size = 250961, upload-time = "2024-06-18T21:41:11.662Z" }, + { url = "https://files.pythonhosted.org/packages/35/56/72f86175e81c656a01c4401cd3b1c923f891b31fbcebe98985894176d7c9/psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0", size = 287478, upload-time = "2024-06-18T21:41:16.18Z" }, + { url = "https://files.pythonhosted.org/packages/19/74/f59e7e0d392bc1070e9a70e2f9190d652487ac115bb16e2eff6b22ad1d24/psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd", size = 290455, upload-time = "2024-06-18T21:41:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5f/60038e277ff0a9cc8f0c9ea3d0c5eb6ee1d2470ea3f9389d776432888e47/psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132", size = 292046, upload-time = "2024-06-18T21:41:33.53Z" }, + { url = "https://files.pythonhosted.org/packages/8b/20/2ff69ad9c35c3df1858ac4e094f20bd2374d33c8643cf41da8fd7cdcb78b/psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d", size = 253560, upload-time = "2024-06-18T21:41:46.067Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/561092313ae925f3acfaace6f9ddc4f6a9c748704317bad9c8c8f8a36a79/psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3", size = 257399, upload-time = "2024-06-18T21:41:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/7c/06/63872a64c312a24fb9b4af123ee7007a306617da63ff13bcc1432386ead7/psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0", size = 251988, upload-time = "2024-06-18T21:41:57.337Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "1.87.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "genai-prices" }, + { name = "griffelib" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/f9/76c7943f208b09b320dc65a000689929df6a5d3b143d56b48deade6db486/pydantic_ai_slim-1.87.0.tar.gz", hash = "sha256:25822985ca21d6f2995310da915080fc3f75763aec82e815a3388257b06d6b84", size = 573802, upload-time = "2026-04-25T01:09:21.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/90/810dbc478bf063677cf56babc4404f2f0c59794017a5022ac93345a26d75/pydantic_ai_slim-1.87.0-py3-none-any.whl", hash = "sha256:6a9b4f9bcac3709ef47f3b3cda70446c002eb55901038a50d6224ee6743fe31a", size = 732159, upload-time = "2026-04-25T01:09:13.025Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/98/b50eb9a411e87483b5c65dba4fa430a06bac4234d3403a40e5a9905ebcd0/pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1", size = 2108971, upload-time = "2026-04-20T14:43:51.945Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/f364b9d161718ff2217160a4b5d41ce38de60aed91c3689ebffa1c939d23/pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f", size = 1949588, upload-time = "2026-04-20T14:44:10.386Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8b/30bd03ee83b2f5e29f5ba8e647ab3c456bf56f2ec72fdbcc0215484a0854/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3", size = 1975986, upload-time = "2026-04-20T14:43:57.106Z" }, + { url = "https://files.pythonhosted.org/packages/3c/54/13ccf954d84ec275d5d023d5786e4aa48840bc9f161f2838dc98e1153518/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a", size = 2055830, upload-time = "2026-04-20T14:44:15.499Z" }, + { url = "https://files.pythonhosted.org/packages/be/0e/65f38125e660fdbd72aa858e7dfae893645cfa0e7b13d333e174a367cd23/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807", size = 2222340, upload-time = "2026-04-20T14:41:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/d1/88/f3ab7739efe0e7e80777dbb84c59eb98518e3f57ea433206194c2e425272/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda", size = 2280727, upload-time = "2026-04-20T14:41:30.461Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6d/c228219080817bec4982f9531cadb18da6aaa770fdeb114f49c237ac2c9f/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57", size = 2092158, upload-time = "2026-04-20T14:44:07.305Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b1/525a16711e7c6d61635fac3b0bd54600b5c5d9f60c6fc5aaab26b64a2297/pydantic_core-2.46.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045", size = 2116626, upload-time = "2026-04-20T14:42:34.118Z" }, + { url = "https://files.pythonhosted.org/packages/ef/7c/17d30673351439a6951bf54f564cf2443ab00ae264ec9df00e2efd710eb5/pydantic_core-2.46.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943", size = 2160691, upload-time = "2026-04-20T14:41:14.023Z" }, + { url = "https://files.pythonhosted.org/packages/86/66/af8adbcbc0886ead7f1a116606a534d75a307e71e6e08226000d51b880d2/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f", size = 2182543, upload-time = "2026-04-20T14:40:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/b0/37/6de71e0f54c54a4190010f57deb749e1ddf75c568ada3b1320b70067f121/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4", size = 2324513, upload-time = "2026-04-20T14:42:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/51/b1/9fc74ce94f603d5ef59ff258ca9c2c8fb902fb548d340a96f77f4d1c3b7f/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a", size = 2361853, upload-time = "2026-04-20T14:43:24.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/d0/4c652fc592db35f100279ee751d5a145aca1b9a7984b9684ba7c1b5b0535/pydantic_core-2.46.3-cp310-cp310-win32.whl", hash = "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7", size = 1980465, upload-time = "2026-04-20T14:44:46.239Z" }, + { url = "https://files.pythonhosted.org/packages/27/b8/a920453c38afbe1f355e1ea0b0d94a0a3e0b0879d32d793108755fa171d5/pydantic_core-2.46.3-cp310-cp310-win_amd64.whl", hash = "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6", size = 2073884, upload-time = "2026-04-20T14:43:01.201Z" }, + { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" }, + { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" }, + { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, + { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" }, + { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "1.87.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/fa/b2306c6dbb06e4dfe6ce6b7c5a28b82bee536d965e1dd1800b49c386b389/pydantic_graph-1.87.0.tar.gz", hash = "sha256:0f44848f8e83908ce372491c32ef349dfaf05e29f39fade0bae9309ab4f015cd", size = 59251, upload-time = "2026-04-25T01:09:23.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/9a/e99edfa37527ee04c14db7fc4b8da3f8e9c913f91c541a4b2d08438b461e/pydantic_graph-1.87.0-py3-none-any.whl", hash = "sha256:fd39e4e852808e36163474fe2af48e88a046b5e5e00596730f33c17d2429b7d2", size = 73063, upload-time = "2026-04-25T01:09:16.493Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyprctl" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/16/6ed71ebcad76c1cd5f22185bcc6b31c0ee62fc5e693b626febea8fedeba3/pyprctl-0.1.3.tar.gz", hash = "sha256:1fb54d3ab030ec02e4afc38fb9662d6634c12834e91ae7959de56a9c09f69c26", size = 18739, upload-time = "2021-10-26T23:52:03.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/5e/62765de39bbce8111fb1f4453a4a804913bf49179fa265fb713ed66c9d15/pyprctl-0.1.3-py3-none-any.whl", hash = "sha256:6302e5114f078fb33e5799835d0a69e2fc180bb6b28ad073515fa40c5272f1dd", size = 20016, upload-time = "2021-10-26T23:52:02.986Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.27" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/9b/f23807317a113dc36e74e75eb265a02dd1a4d9082abc3c1064acd22997c4/python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602", size = 44043, upload-time = "2026-04-27T10:51:26.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" }, +] + +[[package]] +name = "python-ulid" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/7e/0d6c82b5ccc71e7c833aed43d9e8468e1f2ff0be1b3f657a6fcafbb8433d/python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636", size = 93175, upload-time = "2025-08-18T16:09:26.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/a0/4ed6632b70a52de845df056654162acdebaf97c20e3212c559ac43e7216e/python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619", size = 11577, upload-time = "2025-08-18T16:09:25.047Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "reboot" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "aiohttp" }, + { name = "anyio" }, + { name = "bitarray" }, + { name = "cffi" }, + { name = "colorama" }, + { name = "cryptography" }, + { name = "fastapi" }, + { name = "googleapis-common-protos" }, + { name = "grpc-interceptor" }, + { name = "grpcio" }, + { name = "grpcio-health-checking" }, + { name = "grpcio-reflection" }, + { name = "grpcio-status" }, + { name = "grpcio-tools" }, + { name = "h2" }, + { name = "jinja2" }, + { name = "jinja2-strcase" }, + { name = "kubernetes-asyncio" }, + { name = "mcp" }, + { name = "mypy-protobuf" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-instrumentation-grpc" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "protobuf" }, + { name = "psutil" }, + { name = "pydantic" }, + { name = "pydantic-ai-slim" }, + { name = "pyjwt" }, + { name = "pyprctl" }, + { name = "python-dotenv" }, + { name = "python-ulid" }, + { name = "pyyaml" }, + { name = "starlette" }, + { name = "tabulate" }, + { name = "typing-extensions" }, + { name = "tzlocal" }, + { name = "urllib3" }, + { name = "uuid7-standard" }, + { name = "uvicorn" }, + { name = "watchdog" }, + { name = "websockets" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/3a/3656c155ba7daf8a5b4b8de71345c82b11af5875ef8c727d804e6188b48e/reboot-1.1.0-py3-none-macosx_13_0_arm64.whl", hash = "sha256:458c18a0fd897870d37cec3a396d13cc97e1d0ce7c604de5c94ff2fe363c0b05", size = 20695822, upload-time = "2026-06-04T19:03:56.637Z" }, + { url = "https://files.pythonhosted.org/packages/66/a5/2784d4b02a8b9b5a27d710ac1de9e89b84e17c7954fc354b2735ee46ac02/reboot-1.1.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:f3a55ab0b70703b0c07a128cc8c0b35a619627ec28dda427904cf4104a624fe9", size = 24093172, upload-time = "2026-06-04T18:03:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/0a/50/47016a92fbd9078b86e2e08f2e669f57fc51059989e477266915c9bb0043/reboot-1.1.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:ef64a530c25e84be8f8992706050086dfaf9ff213eed958925e2f6ad9209307e", size = 24221768, upload-time = "2026-06-04T18:02:07.117Z" }, +] + +[[package]] +name = "reboot-swag-store" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "python-dotenv" }, + { name = "reboot" }, + { name = "uuid7" }, +] + +[package.metadata] +requires-dist = [ + { name = "anyio", specifier = ">=4.0.0" }, + { name = "httpx", specifier = ">=0.27,<1.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "reboot", specifier = "==1.1.0" }, + { name = "uuid7", specifier = ">=0.1.0" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size = 21943, upload-time = "2025-10-30T18:44:20.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "types-protobuf" +version = "7.34.1.20260503" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/31/87969cb3e62287bde7598b78b3c098d2873d54f5fb5a7cfbcaa73b8c965e/types_protobuf-7.34.1.20260503.tar.gz", hash = "sha256:effbc819aa17e02448dde99f089c6794662d66f4b2797e922f185ffe0b24e766", size = 68830, upload-time = "2026-05-03T05:19:50.739Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/67/a33fb18090a927794a5ee4b1a30730b528ace0dad6b18932540d21258184/types_protobuf-7.34.1.20260503-py3-none-any.whl", hash = "sha256:75fd66121d56785c91828b8bf7b511f39ba847f11e682573e41847f01e9cd1de", size = 86019, upload-time = "2026-05-03T05:19:49.486Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/cc/11360404b20a6340b9b4ed39a3338c4af47bc63f87f6cea94dbcbde07029/tzlocal-5.3.tar.gz", hash = "sha256:2fafbfc07e9d8b49ade18f898d6bcd37ae88ce3ad6486842a2e4f03af68323d2", size = 30480, upload-time = "2025-02-13T13:37:20.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/9f/1c0b69d3abf4c65acac051ad696b8aea55afbb746dea8017baab53febb5e/tzlocal-5.3-py3-none-any.whl", hash = "sha256:3814135a1bb29763c6e4f08fd6e41dbb435c7a60bfbb03270211bcc537187d8c", size = 17920, upload-time = "2025-02-13T13:37:18.462Z" }, +] + +[[package]] +name = "urllib3" +version = "1.26.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/79/6372d8c0d0641b4072889f3ff84f279b738cd8595b64c8e0496d4e848122/urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", size = 301444, upload-time = "2023-03-11T00:01:41.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/f5/890a0baca17a61c1f92f72b81d3c31523c99bec609e60c292ea55b387ae8/urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42", size = 140881, upload-time = "2023-03-11T00:01:39.031Z" }, +] + +[[package]] +name = "uuid7" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/19/7472bd526591e2192926247109dbf78692e709d3e56775792fec877a7720/uuid7-0.1.0.tar.gz", hash = "sha256:8c57aa32ee7456d3cc68c95c4530bc571646defac01895cfc73545449894a63c", size = 14052, upload-time = "2021-12-29T01:38:21.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/77/8852f89a91453956582a85024d80ad96f30a41fed4c2b3dce0c9f12ecc7e/uuid7-0.1.0-py2.py3-none-any.whl", hash = "sha256:5e259bb63c8cb4aded5927ff41b444a80d0c7124e8a0ced7cf44efa1f5cccf61", size = 7477, upload-time = "2021-12-29T01:38:20.418Z" }, +] + +[[package]] +name = "uuid7-standard" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/0d/bbb440a1fad38c1be8cdc4c379e061ec10661061e43b68cf7e9ae718455b/uuid7_standard-1.1.0.tar.gz", hash = "sha256:c7efdef8309265444fd11439bda8df10bd6469b56be62c1d3d78e014d16b4b9a", size = 2181, upload-time = "2025-07-04T21:00:12.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/3e/568b8382627e0fec58ef3c83c3cafa241b86ff41ba37874df057693c12d1/uuid7_standard-1.1.0-py3-none-any.whl", hash = "sha256:725f8f9202662dd6212df6b8caa1e101c2a61935261f43438bca842e058c5726", size = 2561, upload-time = "2025-07-04T21:00:11.939Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568, upload-time = "2024-12-15T13:33:30.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315, upload-time = "2024-12-15T13:33:27.467Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, + { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, + { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, + { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, + { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, + { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, + { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +] diff --git a/reboot/examples/reboot-swag-store/web/.gitignore b/reboot/examples/reboot-swag-store/web/.gitignore new file mode 100644 index 00000000..92dde881 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Reboot generated code +api/* diff --git a/reboot/examples/reboot-swag-store/web/index.css b/reboot/examples/reboot-swag-store/web/index.css new file mode 100644 index 00000000..10e93823 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/index.css @@ -0,0 +1,26 @@ +:root { + --color-bg: #f7f5ed; + --color-bg-card: #ffffff; + --color-border: #e2dfd6; + --color-text: #0d0d0d; + --color-text-secondary: #103761; + --color-text-muted: #6b7280; + --color-accent: #aedc91; + --color-accent-dark: #8bc46d; + --color-navy: #103761; + --color-danger: #dc2626; + --font-sans: "DM Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", + Roboto, sans-serif; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-sans); + background: var(--color-bg); + color: var(--color-text); +} diff --git a/reboot/examples/reboot-swag-store/web/package-lock.json b/reboot/examples/reboot-swag-store/web/package-lock.json new file mode 100644 index 00000000..1eee81a6 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/package-lock.json @@ -0,0 +1,3517 @@ +{ + "name": "reboot-swag-store-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "reboot-swag-store-web", + "version": "0.1.0", + "dependencies": { + "@modelcontextprotocol/ext-apps": "1.5.0", + "@modelcontextprotocol/sdk": "1.29.0", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-react": "1.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "zod": "^3.25.0" + }, + "devDependencies": { + "@types/react": "^18.2.67", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.7.0", + "concurrently": "^9.1.2", + "typescript": "^5.9.2", + "vite": "^6.3.5", + "vite-plugin-singlefile": "^2.0.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz", + "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)", + "peer": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@modelcontextprotocol/ext-apps": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.5.0.tgz", + "integrity": "sha512-q4fut89TOoP2LEPHSGfZErIf1K1xOTTzV+41h/bB2BqKw2gKb0uLKbHusOy1UtbY0puS16zBho/vFp3f5XMVbQ==", + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@reboot-dev/reboot-api": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-api/-/reboot-api-1.1.0.tgz", + "integrity": "sha512-jN/yF1EINALMaEqQkeR9pQVCN0w3/2UoGm0EWTSOrVo1H6BzUb6E/KQHH1hFxWqmL33WwU/ZFJkrYLVA7hrKTw==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "1.4.0", + "typescript": "5.4.5", + "zod": "^3.25.51" + }, + "peerDependencies": { + "@bufbuild/protobuf": "1.10.1" + } + }, + "node_modules/@reboot-dev/reboot-api/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@reboot-dev/reboot-react": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-react/-/reboot-react-1.1.0.tgz", + "integrity": "sha512-AbD+eZrGvlhpMrR2G79z7q0qnry2GGcyQbahS+BtN9qBpVjQ4ZsKE8qiuewPUzHIKripyODOCijc7S1Z+I5oFQ==", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/ext-apps": "1.5.0", + "@modelcontextprotocol/sdk": "1.29.0", + "@reboot-dev/reboot-api": "1.1.0", + "@reboot-dev/reboot-web": "1.1.0", + "@scarf/scarf": "1.4.0", + "@types/uuid": "^9.0.4", + "js-sha1": "0.7.0", + "tslib": "^2.6.2", + "typescript": "4.8.4", + "uuid": "11.1.0" + }, + "peerDependencies": { + "@bufbuild/protobuf": "1.10.1", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@reboot-dev/reboot-react/node_modules/typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@reboot-dev/reboot-web": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reboot-dev/reboot-web/-/reboot-web-1.1.0.tgz", + "integrity": "sha512-EximPrCQlETHhqOc0mSbs1sTT4Zvd7GROqNDTu8aBRxq/u07mnWYvII4JziiybUWP7X892z80+eGtDozlSEg7A==", + "license": "Apache-2.0", + "dependencies": { + "@reboot-dev/reboot-api": "1.1.0", + "@scarf/scarf": "1.4.0", + "js-sha1": "0.7.0", + "lru-cache-idb": "^0.5.2", + "tslib": "^2.6.2", + "typescript": "4.8.4", + "uuid": "11.1.0" + }, + "peerDependencies": { + "@bufbuild/protobuf": "1.10.1" + } + }, + "node_modules/@reboot-dev/reboot-web/node_modules/typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz", + "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.349", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz", + "integrity": "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.0.tgz", + "integrity": "sha512-XKhFohWaSBdVJNTi5TaHziqnPkv04I9UQV6q1Wy7Ui6GGQZVW12ojDFwqer14EvCXxjvPG0CyWXx7cAXpALB4Q==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.17", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.17.tgz", + "integrity": "sha512-FbJJNb/XgX7YW0hX/V8w5oYLztKEsRLykCMZWt1WdLtsfjzMvmoqWBA4H4t5norinq8/rh20oiZYr+WSl4UzAQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-sha1": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/js-sha1/-/js-sha1-0.7.0.tgz", + "integrity": "sha512-oQZ1Mo7440BfLSv9TX87VNEyU52pXPVG19F9PL3gTgNt0tVxlZ8F4O6yze3CLuLx28TxotxvlyepCNaaV0ZjMw==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-cache-idb": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/lru-cache-idb/-/lru-cache-idb-0.5.2.tgz", + "integrity": "sha512-POlgTgPauh/q/mdh51G828HXqSI+/7KU6tKf59lppkTeTUznztGcqjeUxwjJPVOpKTgWH28DOFtbp9DpXq0zLA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-singlefile": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.3.tgz", + "integrity": "sha512-XVnGH0QzbOa8fxRSsHdCarVN1BSBXNi7uLMQYlrGRN5apdHkk62XQWRJhVever0lnfuyBkwn+kvVChdm/OoOUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">18.0.0" + }, + "peerDependencies": { + "rollup": "^4.59.0", + "vite": "^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/reboot/examples/reboot-swag-store/web/package.json b/reboot/examples/reboot-swag-store/web/package.json new file mode 100644 index 00000000..4cc08ee9 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/package.json @@ -0,0 +1,35 @@ +{ + "name": "reboot-swag-store-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build:store": "vite build --mode store", + "build:cart": "vite build --mode cart", + "build:confirmation": "vite build --mode confirmation", + "build:watch:store": "vite build --mode store --watch", + "build:watch:cart": "vite build --mode cart --watch", + "build:watch:confirmation": "vite build --mode confirmation --watch", + "build": "tsc --noEmit && npm run build:store && npm run build:cart && npm run build:confirmation", + "build:watch": "concurrently \"npm:build:watch:*\"" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "1.5.0", + "@modelcontextprotocol/sdk": "1.29.0", + "@reboot-dev/reboot-react": "1.1.0", + "@reboot-dev/reboot-api": "1.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "zod": "^3.25.0" + }, + "devDependencies": { + "@types/react": "^18.2.67", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.7.0", + "concurrently": "^9.1.2", + "typescript": "^5.9.2", + "vite": "^6.3.5", + "vite-plugin-singlefile": "^2.0.3" + } +} diff --git a/reboot/examples/reboot-swag-store/web/tsconfig.app.json b/reboot/examples/reboot-swag-store/web/tsconfig.app.json new file mode 100644 index 00000000..910e9002 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@api/*": ["./api/*"] + } + }, + "include": ["ui"] +} diff --git a/reboot/examples/reboot-swag-store/web/tsconfig.json b/reboot/examples/reboot-swag-store/web/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/reboot/examples/reboot-swag-store/web/tsconfig.node.json b/reboot/examples/reboot-swag-store/web/tsconfig.node.json new file mode 100644 index 00000000..4d19ae8f --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/reboot/examples/reboot-swag-store/web/ui/cart/App.module.css b/reboot/examples/reboot-swag-store/web/ui/cart/App.module.css new file mode 100644 index 00000000..e944c9d5 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/ui/cart/App.module.css @@ -0,0 +1,269 @@ +.container { + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-sans); + padding: 24px 20px; + max-width: 500px; + margin: 0 auto; +} + +.title { + font-size: 20px; + font-weight: 700; + margin-bottom: 16px; + color: var(--color-navy); +} + +.empty { + color: var(--color-text-muted); + font-size: 13px; + text-align: center; + padding: 40px 0; +} + +.items { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 16px; +} + +.item { + display: flex; + align-items: center; + gap: 10px; + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: 10px; + padding: 10px 12px; +} + +.itemImage { + width: 40px; + height: 40px; + object-fit: cover; + border-radius: 6px; + background: var(--color-bg); +} + +.itemEmoji { + font-size: 24px; +} + +.itemInfo { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; +} + +.itemName { + font-size: 12px; + font-weight: 700; + color: var(--color-navy); +} + +.itemQty { + font-size: 11px; + color: var(--color-text-muted); +} + +.itemPrice { + font-size: 13px; + font-weight: 700; + color: var(--color-text); +} + +.removeButton { + font-size: 12px; + font-family: var(--font-sans); + width: 24px; + height: 24px; + border: 1px solid var(--color-border); + border-radius: 6px; + background: var(--color-bg); + color: var(--color-text-muted); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + transition: all 0.15s ease; +} + +.removeButton:hover { + border-color: var(--color-danger); + color: var(--color-danger); +} + +.totals { + border-top: 1px solid var(--color-border); + padding-top: 12px; + margin-bottom: 20px; +} + +.totalRow { + display: flex; + justify-content: space-between; + font-size: 13px; + padding: 4px 0; + color: var(--color-text-muted); +} + +.totalFinal { + font-size: 15px; + font-weight: 700; + color: var(--color-navy); + border-top: 1px solid var(--color-border); + padding-top: 8px; + margin-top: 4px; +} + +.payment { + margin-bottom: 16px; +} + +.sectionTitle { + font-size: 15px; + font-weight: 700; + margin-bottom: 10px; + color: var(--color-navy); +} + +.form { + display: flex; + flex-direction: column; + gap: 8px; +} + +.row { + display: flex; + gap: 8px; + align-items: flex-start; +} + +.row .input { + flex: 1; +} + +.fieldCol { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; +} + +.input[aria-invalid="true"] { + border-color: var(--color-danger, #dc2626); +} + +.fieldError { + font-size: 11px; + color: var(--color-danger, #dc2626); + padding-left: 2px; +} + +.input { + font-size: 13px; + font-family: var(--font-sans); + padding: 10px 12px; + border: 1px solid var(--color-border); + border-radius: 10px; + background: var(--color-bg-card); + color: var(--color-text); + outline: none; + transition: border-color 0.15s ease; +} + +.input:focus { + border-color: var(--color-navy); +} + +.input::placeholder { + color: var(--color-text-muted); +} + +.checkoutButton { + width: 100%; + font-size: 14px; + font-family: var(--font-sans); + font-weight: 700; + padding: 12px; + border: 2px solid var(--color-navy); + border-radius: 14px; + background: var(--color-navy); + color: var(--color-bg); + cursor: pointer; + transition: opacity 0.15s ease; +} + +.checkoutButton:hover:not(:disabled) { + opacity: 0.9; +} + +.checkoutButton:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.discount { + color: #16a34a; + font-weight: 700; +} + +.couponSection { + margin-bottom: 16px; +} + +.couponRow { + display: flex; + gap: 8px; + align-items: stretch; +} + +.couponRow .input { + flex: 1; +} + +.applyButton { + font-size: 12px; + font-family: var(--font-sans); + font-weight: 700; + padding: 10px 16px; + border: 2px solid var(--color-navy); + border-radius: 10px; + background: var(--color-bg); + color: var(--color-navy); + cursor: pointer; + transition: opacity 0.15s ease; + white-space: nowrap; +} + +.applyButton:hover:not(:disabled) { + opacity: 0.8; +} + +.applyButton:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.couponSuccess { + font-size: 12px; + color: #16a34a; + margin-top: 6px; + font-weight: 600; +} + +.couponErrorMsg { + font-size: 12px; + color: var(--color-danger, #dc2626); + margin-top: 6px; +} + +.loading { + color: var(--color-text-muted); + font-size: 13px; + text-align: center; + padding: 40px 0; +} diff --git a/reboot/examples/reboot-swag-store/web/ui/cart/App.tsx b/reboot/examples/reboot-swag-store/web/ui/cart/App.tsx new file mode 100644 index 00000000..589213ce --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/ui/cart/App.tsx @@ -0,0 +1,506 @@ +import { useMemo, useState, type FC } from "react"; +import { + useCart, + useCouponBook, +} from "@api/reboot_swag_store/v1/store_rbt_react"; +import { CartEmpty, InvalidCoupon } from "@api/reboot_swag_store/v1/store_pb"; +import { useMcpApp } from "@reboot-dev/reboot-react"; +import css from "./App.module.css"; + +const COUPON_BOOK_ID = "coupon-book"; + +function formatPrice(cents: number): string { + return `$${(cents / 100).toFixed(2)}`; +} + +// Lightweight shipping-address validation. We only ship to +// the 50 US states + DC by default, which matches what +// Printful accepts for US orders; non-US countries get a +// looser check so this example still works internationally. +const US_STATES = new Set([ + "AL", + "AK", + "AZ", + "AR", + "CA", + "CO", + "CT", + "DE", + "FL", + "GA", + "HI", + "ID", + "IL", + "IN", + "IA", + "KS", + "KY", + "LA", + "ME", + "MD", + "MA", + "MI", + "MN", + "MS", + "MO", + "MT", + "NE", + "NV", + "NH", + "NJ", + "NM", + "NY", + "NC", + "ND", + "OH", + "OK", + "OR", + "PA", + "RI", + "SC", + "SD", + "TN", + "TX", + "UT", + "VT", + "VA", + "WA", + "WV", + "WI", + "WY", + "DC", +]); +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const US_ZIP_RE = /^\d{5}(-\d{4})?$/; +const COUNTRY_RE = /^[A-Z]{2}$/; + +type AddressField = + | "name" + | "email" + | "address1" + | "city" + | "stateCode" + | "zipCode" + | "countryCode"; + +type Address = Record; + +function validateAddress(a: Address): Partial> { + const errors: Partial> = {}; + if (!a.name.trim()) errors.name = "Required"; + if (!a.email.trim()) { + errors.email = "Required"; + } else if (!EMAIL_RE.test(a.email.trim())) { + errors.email = "Enter a valid email address"; + } + if (!a.address1.trim()) errors.address1 = "Required"; + if (!a.city.trim()) errors.city = "Required"; + + const country = a.countryCode.trim().toUpperCase(); + if (!COUNTRY_RE.test(country)) { + errors.countryCode = "Two-letter code (e.g. US)"; + } + + const state = a.stateCode.trim().toUpperCase(); + if (!state) { + errors.stateCode = "Required"; + } else if (country === "US" && !US_STATES.has(state)) { + errors.stateCode = "Unknown US state"; + } + + const zip = a.zipCode.trim(); + if (!zip) { + errors.zipCode = "Required"; + } else if (country === "US" && !US_ZIP_RE.test(zip)) { + errors.zipCode = "Use 5 or 9-digit ZIP"; + } + + return errors; +} + +export const CartApp: FC = () => { + const app = useMcpApp(); + const cart = useCart(); + const couponBook = useCouponBook({ + id: COUPON_BOOK_ID, + }); + const { response, isLoading } = cart.useGetCart(); + + // Shipping address fields. + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [address1, setAddress1] = useState(""); + const [address2, setAddress2] = useState(""); + const [city, setCity] = useState(""); + const [stateCode, setStateCode] = useState(""); + const [zipCode, setZipCode] = useState(""); + const [countryCode, setCountryCode] = useState("US"); + + // Coupon state. + const [couponCode, setCouponCode] = useState(""); + const [couponValid, setCouponValid] = useState(false); + const [couponError, setCouponError] = useState(""); + const [applyingCoupon, setApplyingCoupon] = useState(false); + + const [isPending, setIsPending] = useState(false); + const [checkoutError, setCheckoutError] = useState(""); + + // Which address fields the user has blurred. We only + // surface per-field errors once the field is touched + // (or the user clicks checkout), so the form doesn't + // yell "Required" before they've typed anything. + const [touched, setTouched] = useState>>( + {} + ); + const markTouched = (field: AddressField) => + setTouched((t) => ({ ...t, [field]: true })); + + const addressErrors = useMemo( + () => + validateAddress({ + name, + email, + address1, + city, + stateCode, + zipCode, + countryCode, + }), + [name, email, address1, city, stateCode, zipCode, countryCode] + ); + const addressValid = Object.keys(addressErrors).length === 0; + const errorFor = (field: AddressField) => + touched[field] ? addressErrors[field] : undefined; + + const items = response?.items ?? []; + const subtotalCents = items.reduce( + (sum, item) => sum + item.priceCents * item.quantity, + 0 + ); + const totalCents = couponValid ? 0 : subtotalCents; + + const handleRemove = async (productId: string) => { + await cart.removeItem({ productId }); + }; + + const handleApplyCoupon = async () => { + if (!couponCode.trim()) return; + setApplyingCoupon(true); + setCouponError(""); + try { + const result = await couponBook.validateCode({ + couponCode: couponCode.trim(), + }); + if ("response" in result && result.response?.valid) { + setCouponValid(true); + setCouponError(""); + } else { + setCouponValid(false); + setCouponError("Invalid coupon code."); + } + } catch { + setCouponValid(false); + setCouponError("Could not validate coupon."); + } finally { + setApplyingCoupon(false); + } + }; + + const handleCheckout = async () => { + if (items.length === 0 || !couponValid) return; + if (!addressValid) { + setTouched({ + name: true, + email: true, + address1: true, + city: true, + stateCode: true, + zipCode: true, + countryCode: true, + }); + return; + } + setIsPending(true); + setCheckoutError(""); + try { + const { response, aborted } = await cart.checkout({ + shippingAddress: { + name, + email, + address1, + address2, + city, + stateCode, + zipCode, + countryCode, + }, + couponCode: couponCode.trim(), + }); + if (aborted !== undefined) { + if (aborted.error instanceof CartEmpty) { + setCheckoutError("Your cart is empty."); + } else if (aborted.error instanceof InvalidCoupon) { + setCheckoutError("That coupon code is not valid."); + setCouponValid(false); + } else { + setCheckoutError("Checkout failed. Please try again."); + } + return; + } + if (response) { + app?.sendMessage({ + role: "user", + content: [ + { + type: "text", + text: + "Order placed! Show my order " + + `confirmation for order ` + + `${response.orderId}`, + }, + ], + }); + } + } finally { + setIsPending(false); + } + }; + + const canCheckout = + items.length > 0 && addressValid && couponValid && !isPending; + + if (isLoading && items.length === 0) { + return ( +
+
Loading cart...
+
+ ); + } + + if (items.length === 0) { + return ( +
+

Shopping Cart

+
+ Your cart is empty. Browse the store to add items! +
+
+ ); + } + + return ( +
+

Shopping Cart

+ +
+ {items.map((item) => ( +
+ {item.imageUrl && ( + {item.name} + )} +
+ + {item.name} + {item.size && ` (${item.size})`} + + Qty: {item.quantity} +
+ + {formatPrice(item.priceCents * item.quantity)} + + +
+ ))} +
+ +
+
+ Subtotal + {formatPrice(subtotalCents)} +
+ {couponValid && ( +
+ Coupon discount + -{formatPrice(subtotalCents)} +
+ )} +
+ Total + {formatPrice(totalCents)} +
+
+ +
+

Shipping Address

+
+
+ setName(e.target.value)} + onBlur={() => markTouched("name")} + aria-invalid={!!errorFor("name")} + /> + {errorFor("name") && ( +
{errorFor("name")}
+ )} +
+
+ setEmail(e.target.value)} + onBlur={() => markTouched("email")} + aria-invalid={!!errorFor("email")} + /> + {errorFor("email") && ( +
{errorFor("email")}
+ )} +
+
+ setAddress1(e.target.value)} + onBlur={() => markTouched("address1")} + aria-invalid={!!errorFor("address1")} + /> + {errorFor("address1") && ( +
{errorFor("address1")}
+ )} +
+ setAddress2(e.target.value)} + /> +
+
+ setCity(e.target.value)} + onBlur={() => markTouched("city")} + aria-invalid={!!errorFor("city")} + /> + {errorFor("city") && ( +
{errorFor("city")}
+ )} +
+
+ setStateCode(e.target.value.toUpperCase())} + onBlur={() => markTouched("stateCode")} + aria-invalid={!!errorFor("stateCode")} + maxLength={2} + /> + {errorFor("stateCode") && ( +
{errorFor("stateCode")}
+ )} +
+
+
+
+ setZipCode(e.target.value)} + onBlur={() => markTouched("zipCode")} + aria-invalid={!!errorFor("zipCode")} + maxLength={10} + /> + {errorFor("zipCode") && ( +
{errorFor("zipCode")}
+ )} +
+
+ setCountryCode(e.target.value.toUpperCase())} + onBlur={() => markTouched("countryCode")} + aria-invalid={!!errorFor("countryCode")} + maxLength={2} + /> + {errorFor("countryCode") && ( +
{errorFor("countryCode")}
+ )} +
+
+
+
+ +
+

Coupon Code

+
+ { + setCouponCode(e.target.value); + setCouponValid(false); + setCouponError(""); + }} + /> + +
+ {couponValid && ( +
+ Coupon applied! Your order is free. +
+ )} + {couponError &&
{couponError}
} +
+ + + {checkoutError && ( +
{checkoutError}
+ )} +
+ ); +}; diff --git a/reboot/examples/reboot-swag-store/web/ui/cart/index.html b/reboot/examples/reboot-swag-store/web/ui/cart/index.html new file mode 100644 index 00000000..fb98aa91 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/ui/cart/index.html @@ -0,0 +1,12 @@ + + + + + + Shopping Cart + + +
+ + + diff --git a/reboot/examples/reboot-swag-store/web/ui/cart/main.tsx b/reboot/examples/reboot-swag-store/web/ui/cart/main.tsx new file mode 100644 index 00000000..cbddb77e --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/ui/cart/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { RebootClientProvider } from "@reboot-dev/reboot-react"; +import { CartApp } from "./App"; +import "../../index.css"; + +createRoot(document.getElementById("root")!).render( + + + + + +); diff --git a/reboot/examples/reboot-swag-store/web/ui/confirmation/App.module.css b/reboot/examples/reboot-swag-store/web/ui/confirmation/App.module.css new file mode 100644 index 00000000..a6fff815 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/ui/confirmation/App.module.css @@ -0,0 +1,118 @@ +.container { + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-sans); + padding: 24px 20px; + max-width: 500px; + margin: 0 auto; + text-align: center; +} + +.checkmark { + width: 48px; + height: 48px; + background: var(--color-accent); + color: var(--color-navy); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + font-weight: bold; + margin: 0 auto 12px; +} + +.title { + font-size: 20px; + font-weight: 700; + color: var(--color-navy); + margin-bottom: 4px; +} + +.orderId { + font-size: 12px; + color: var(--color-text-muted); + margin-bottom: 2px; +} + +.date { + font-size: 11px; + color: var(--color-text-muted); + margin-bottom: 20px; +} + +.items { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 16px; + text-align: left; +} + +.item { + display: flex; + align-items: center; + justify-content: space-between; + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: 10px; + padding: 10px 12px; +} + +.itemInfo { + display: flex; + align-items: center; + gap: 8px; +} + +.itemName { + font-size: 12px; + font-weight: 700; + color: var(--color-navy); +} + +.itemQty { + font-size: 11px; + color: var(--color-text-muted); +} + +.itemPrice { + font-size: 12px; + font-weight: 700; + color: var(--color-text); +} + +.totals { + border-top: 1px solid var(--color-border); + padding-top: 12px; + margin-bottom: 16px; + text-align: left; +} + +.totalRow { + display: flex; + justify-content: space-between; + font-size: 13px; + padding: 4px 0; + color: var(--color-text-muted); +} + +.totalFinal { + font-size: 15px; + font-weight: 700; + color: var(--color-navy); + border-top: 1px solid var(--color-border); + padding-top: 8px; + margin-top: 4px; +} + +.cardInfo { + font-size: 12px; + color: var(--color-text-muted); +} + +.loading { + color: var(--color-text-muted); + font-size: 13px; + padding: 40px 0; +} diff --git a/reboot/examples/reboot-swag-store/web/ui/confirmation/App.tsx b/reboot/examples/reboot-swag-store/web/ui/confirmation/App.tsx new file mode 100644 index 00000000..cb0c1093 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/ui/confirmation/App.tsx @@ -0,0 +1,85 @@ +import { type FC } from "react"; +import { useOrder } from "@api/reboot_swag_store/v1/store_rbt_react"; +import css from "./App.module.css"; + +function formatPrice(cents: number): string { + return `$${(cents / 100).toFixed(2)}`; +} + +function formatDate(iso: string): string { + if (!iso) return ""; + const date = new Date(iso); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +export const ConfirmationApp: FC = () => { + const order = useOrder(); + const { response, isLoading } = order.useGetDetails(); + + if (isLoading && !response) { + return ( +
+
Loading order...
+
+ ); + } + + if (!response || !response.orderId) { + return ( +
+
Order not found.
+
+ ); + } + + return ( +
+
+

Order Confirmed!

+

Order #{response.orderId.slice(0, 8)}

+

{formatDate(response.createdAt)}

+ +
+ {response.items.map((item) => ( +
+
+ + {item.name} + {item.size && ` (${item.size})`} + + x{item.quantity} +
+ + {formatPrice(item.priceCents * item.quantity)} + +
+ ))} +
+ +
+
+ Subtotal + {formatPrice(response.subtotalCents)} +
+
+ Shipping + {formatPrice(response.shippingCents)} +
+
+ Total + {formatPrice(response.totalCents)} +
+
+ + {response.shippingName && ( +

Shipping to {response.shippingName}

+ )} +
+ ); +}; diff --git a/reboot/examples/reboot-swag-store/web/ui/confirmation/index.html b/reboot/examples/reboot-swag-store/web/ui/confirmation/index.html new file mode 100644 index 00000000..b3dc71df --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/ui/confirmation/index.html @@ -0,0 +1,12 @@ + + + + + + Order Confirmation + + +
+ + + diff --git a/reboot/examples/reboot-swag-store/web/ui/confirmation/main.tsx b/reboot/examples/reboot-swag-store/web/ui/confirmation/main.tsx new file mode 100644 index 00000000..d3756070 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/ui/confirmation/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { RebootClientProvider } from "@reboot-dev/reboot-react"; +import { ConfirmationApp } from "./App"; +import "../../index.css"; + +createRoot(document.getElementById("root")!).render( + + + + + +); diff --git a/reboot/examples/reboot-swag-store/web/ui/store/App.module.css b/reboot/examples/reboot-swag-store/web/ui/store/App.module.css new file mode 100644 index 00000000..8a236645 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/ui/store/App.module.css @@ -0,0 +1,201 @@ +.container { + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-sans); + padding: 24px 20px; + max-width: 600px; + margin: 0 auto; +} + +.title { + font-size: 22px; + font-weight: 700; + margin-bottom: 4px; + color: var(--color-navy); +} + +.subtitle { + font-size: 13px; + color: var(--color-text-muted); + margin-bottom: 20px; +} + +.grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.card { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: 12px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.productImage { + width: 100%; + aspect-ratio: 1; + object-fit: cover; + border-radius: 8px; + background: var(--color-bg); +} + +.emoji { + font-size: 36px; + text-align: center; + padding: 8px 0; +} + +.info { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; +} + +.name { + font-size: 13px; + font-weight: 700; + color: var(--color-navy); +} + +.description { + font-size: 11px; + color: var(--color-text-muted); + line-height: 1.4; + flex: 1; +} + +.colorRow { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 6px; +} + +.colorButton { + font-size: 10px; + font-family: var(--font-sans); + padding: 3px 8px; + border: 1px solid var(--color-border); + border-radius: 6px; + background: var(--color-bg); + color: var(--color-text-muted); + cursor: pointer; + font-weight: 600; + transition: all 0.15s ease; +} + +.colorButton:hover { + border-color: var(--color-navy); + color: var(--color-navy); +} + +.colorSelected { + border-color: var(--color-navy); + background: var(--color-navy); + color: var(--color-bg); +} + +.sizeSelect { + font-size: 11px; + font-family: var(--font-sans); + padding: 5px 8px; + border: 1px solid var(--color-border); + border-radius: 8px; + background: var(--color-bg); + color: var(--color-text); + cursor: pointer; + margin-top: 6px; + outline: none; + width: fit-content; +} + +.sizeSelect:focus { + border-color: var(--color-navy); +} + +.cardFooter { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 8px; +} + +.price { + font-size: 14px; + font-weight: 700; + color: var(--color-text); +} + +.addButton { + font-size: 11px; + font-family: var(--font-sans); + padding: 6px 12px; + border: 2px solid var(--color-navy); + border-radius: 14px; + background: var(--color-navy); + color: var(--color-bg); + cursor: pointer; + font-weight: 700; + transition: opacity 0.15s ease, background 0.2s ease, border-color 0.2s ease; + min-width: 80px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; +} + +.addButton:hover { + opacity: 0.85; +} + +.addButtonBusy { + pointer-events: none; + opacity: 0.9; +} + +.spinner { + width: 12px; + height: 12px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: #fff; + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.check { + font-size: 14px; + font-weight: 700; + line-height: 1; + animation: pop 0.25s ease; +} + +@keyframes pop { + 0% { + transform: scale(0); + } + 60% { + transform: scale(1.3); + } + 100% { + transform: scale(1); + } +} + +.loading { + color: var(--color-text-muted); + font-size: 13px; + text-align: center; + padding: 40px 0; +} diff --git a/reboot/examples/reboot-swag-store/web/ui/store/App.tsx b/reboot/examples/reboot-swag-store/web/ui/store/App.tsx new file mode 100644 index 00000000..0fd4a045 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/ui/store/App.tsx @@ -0,0 +1,245 @@ +import { useState, useMemo, useCallback, type FC } from "react"; +import { useUser } from "@api/reboot_swag_store/v1/store_rbt_react"; +import { useMcpApp, useMcpToolData } from "@reboot-dev/reboot-react"; +import css from "./App.module.css"; + +function formatPrice(cents: number): string { + return `$${(cents / 100).toFixed(2)}`; +} + +type ButtonState = "idle" | "loading" | "done"; + +const AddToCartButton: FC<{ + onClick: () => Promise; + disabled?: boolean; +}> = ({ onClick, disabled }) => { + const [state, setState] = useState("idle"); + + const handleClick = useCallback(async () => { + if (state !== "idle") return; + setState("loading"); + try { + await onClick(); + setState("done"); + setTimeout(() => setState("idle"), 1200); + } catch { + setState("idle"); + } + }, [onClick, state]); + + return ( + + ); +}; + +type VariantType = { + id: string; + size: string; + color: string; + priceCents: number; + imageUrl: string; +}; + +type ProductCardProps = { + product: { + id: string; + name: string; + description: string; + priceCents: number; + imageUrl: string; + variants: VariantType[]; + }; + onAdd: (variantId: string) => Promise; +}; + +const ProductCard: FC = ({ product, onAdd }) => { + const variants = product.variants; + + // Get unique colors and sizes. + const colors = useMemo( + () => [...new Set(variants.map((v) => v.color).filter(Boolean))], + [variants] + ); + const sizes = useMemo( + () => [...new Set(variants.map((v) => v.size).filter(Boolean))], + [variants] + ); + + const hasColors = colors.length > 1; + const hasSizes = sizes.length > 1; + + // Pre-select the first color and first size. + const [selectedColor, setSelectedColor] = useState(colors[0] ?? ""); + const [selectedSize, setSelectedSize] = useState(sizes[0] ?? ""); + + // Find the matching variant. + const selectedVariant = useMemo(() => { + if (variants.length === 0) return null; + if (variants.length === 1) return variants[0]; + return ( + variants.find( + (v) => + (!hasColors || v.color === selectedColor) && + (!hasSizes || v.size === selectedSize) + ) ?? variants[0] + ); + }, [variants, selectedColor, selectedSize, hasColors, hasSizes]); + + // Find the image for the selected color (first + // variant with that color that has an image). + const displayImage = useMemo(() => { + if (hasColors) { + const colorVariant = variants.find( + (v) => v.color === selectedColor && v.imageUrl + ); + if (colorVariant) return colorVariant.imageUrl; + } + return selectedVariant?.imageUrl || product.imageUrl; + }, [variants, selectedColor, selectedVariant, hasColors, product.imageUrl]); + + return ( +
+ {displayImage && ( + {product.name} + )} +
+

{product.name}

+

{product.description}

+ {hasColors && ( +
+ {colors.map((color) => ( + + ))} +
+ )} + {hasSizes && ( + + )} +
+ + {formatPrice(selectedVariant?.priceCents ?? product.priceCents)} + + onAdd(selectedVariant!.id)} + /> +
+
+
+ ); +}; + +export const StoreApp: FC = () => { + const app = useMcpApp(); + const user = useUser(); + // The MCP host merges the tool's input arguments into + // `toolData`, so a call of `browse_store({"product_ids": + // ["abc", "def"]})` lands at `toolData.product_ids`. The + // model picks these IDs after reading the catalog from + // `list_products`; an empty (or missing) list means show + // everything. + const toolData = useMcpToolData(); + const productIds = useMemo( + () => + Array.isArray(toolData?.product_ids) + ? toolData.product_ids.filter( + (id): id is string => typeof id === "string" + ) + : [], + [toolData] + ); + const { response, isLoading } = user.useListProducts(); + + const allProducts = response?.products ?? []; + // Filter client-side to the IDs the model selected. Drop + // unknown IDs silently — models will occasionally + // hallucinate one, and a missing match is better UX than + // an error. + const products = useMemo(() => { + if (productIds.length === 0) return allProducts; + const wanted = new Set(productIds); + return allProducts.filter((product) => wanted.has(product.id)); + }, [allProducts, productIds]); + + if (isLoading && allProducts.length === 0) { + return ( +
+
Loading store...
+
+ ); + } + + const isFiltered = productIds.length > 0; + + return ( +
+

Reboot Swag Store

+

+ {isFiltered + ? `Showing ${products.length} of ${allProducts.length} products` + : "Official Reboot merch"} +

+
+ {products.map((product) => ( + { + const variant = product.variants.find((v) => v.id === variantId); + await app?.sendMessage({ + role: "user", + content: [ + { + type: "text", + text: + `Add to my cart: ` + + `product_id="${product.id}", ` + + `variant_id="${variantId}", ` + + `name="${product.name}", ` + + `price_cents=${ + variant?.priceCents ?? product.priceCents + }, ` + + `image_url="${variant?.imageUrl || product.imageUrl}", ` + + `size="${variant?.size ?? ""}"`, + }, + ], + }); + }} + /> + ))} +
+
+ ); +}; diff --git a/reboot/examples/reboot-swag-store/web/ui/store/index.html b/reboot/examples/reboot-swag-store/web/ui/store/index.html new file mode 100644 index 00000000..f546ba19 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/ui/store/index.html @@ -0,0 +1,12 @@ + + + + + + Reboot Store + + +
+ + + diff --git a/reboot/examples/reboot-swag-store/web/ui/store/main.tsx b/reboot/examples/reboot-swag-store/web/ui/store/main.tsx new file mode 100644 index 00000000..3d882a69 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/ui/store/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { RebootClientProvider } from "@reboot-dev/reboot-react"; +import { StoreApp } from "./App"; +import "../../index.css"; + +createRoot(document.getElementById("root")!).render( + + + + + +); diff --git a/reboot/examples/reboot-swag-store/web/vite.config.ts b/reboot/examples/reboot-swag-store/web/vite.config.ts new file mode 100644 index 00000000..b300a4a2 --- /dev/null +++ b/reboot/examples/reboot-swag-store/web/vite.config.ts @@ -0,0 +1,88 @@ +// Vite configuration for Reboot UIs. +import fs from "fs"; +import path from "path"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; +import { viteSingleFile } from "vite-plugin-singlefile"; + +// Auto-discover UIs from ui/ directory. +const uiDir = path.resolve(__dirname, "ui"); +const uis: Record = + Object.fromEntries( + fs + .readdirSync(uiDir) + .filter((d) => fs.existsSync(path.join(uiDir, d, "index.html"))) + .map((name) => [ + name, + { input: `ui/${name}/index.html`, output: `${name}.html` }, + ]) + ); + +export default defineConfig(({ command, mode }) => { + // Path alias for API imports (@api/... -> ./api/...). + const resolve = { + alias: { + "@api": path.resolve(__dirname, "./api"), + }, + dedupe: ["react", "react-dom", "zod"], + }; + + // Dev server configuration. + // + // UIs use a double iframe architecture: + // MCP Host -> srcdoc (origin=null) -> iframe (origin=localhost:9991) + // + // The inner iframe loads from Envoy ("/__/web/**"), which proxies + // to Vite. Because the inner iframe has a real origin, Vite's URLs + // work normally. `base: "/__/web/"` ensures all paths route through + // Envoy. + // + // Hot Module Replacement works automatically: Vite's client connects + // to the page's origin, and Envoy proxies WebSocket upgrades to + // Vite. This also works with tunnels (ngrok) since the tunnel + // points to Envoy. + if (command === "serve") { + const port = parseInt(process.env.RBT_VITE_PORT || "4444", 10); + + return { + plugins: [react()], + root: ".", + resolve, + base: "/__/web/", + server: { + port, + strictPort: true, + // Listen on all interfaces since requests come through + // Envoy (and tunnels). + host: true, + allowedHosts: true, + }, + }; + } + + // Build mode: `vite build --mode ` + const ui = uis[mode]; + if (!ui) { + const valid = Object.keys(uis).join(", "); + throw new Error(`Unknown UI: ${mode}. Use --mode with: ${valid}`); + } + + return { + plugins: [react(), viteSingleFile()], + build: { + outDir: "dist", + emptyOutDir: false, + assetsInlineLimit: 100000000, + cssCodeSplit: false, + rollupOptions: { + input: ui.input, + output: { + inlineDynamicImports: true, + entryFileNames: ui.output.replace(".html", ".js"), + assetFileNames: ui.output.replace(".html", ".[ext]"), + }, + }, + }, + resolve, + }; +}); diff --git a/reboot/helpers.py b/reboot/helpers.py index 87aeb703..5e0bbdc7 100644 --- a/reboot/helpers.py +++ b/reboot/helpers.py @@ -1,4 +1,3 @@ -import asyncio import base64 import re from google.api import annotations_pb2, http_pb2 @@ -306,12 +305,3 @@ def base64_serialize_proto_descriptor_set( file_descriptor_set: FileDescriptorSet ) -> bytes: return base64.b64encode(file_descriptor_set.SerializeToString()) - - -async def maybe_cancel_task(task: Optional[asyncio.Task]): - if task is not None and not task.done(): - task.cancel() - try: - await task - except asyncio.CancelledError: - pass diff --git a/reboot/inspect/BUILD.bazel b/reboot/inspect/BUILD.bazel index da08937b..3668fae2 100644 --- a/reboot/inspect/BUILD.bazel +++ b/reboot/inspect/BUILD.bazel @@ -61,11 +61,12 @@ py_library( deps = [ "//rbt/v1alpha1/inspect:inspect_py_grpc", "//rbt/v1alpha1/inspect:inspect_py_proto", + "//reboot:wait_for_tasks_py", + "//reboot/aio:aborted_py", + "//reboot/aio:state_managers_py", + "//reboot/aio:stubs_py", "//reboot/aio/auth:admin_auth_py", + "//reboot/aio/internals:middleware_py", "//reboot/templates:tools_py", - "@com_github_reboot_dev_reboot//reboot/aio:aborted_py", - "@com_github_reboot_dev_reboot//reboot/aio:state_managers_py", - "@com_github_reboot_dev_reboot//reboot/aio:stubs_py", - "@com_github_reboot_dev_reboot//reboot/aio/internals:middleware_py", ], ) diff --git a/reboot/inspect/servicer.py b/reboot/inspect/servicer.py index 3fa8b8c9..eba83f0c 100644 --- a/reboot/inspect/servicer.py +++ b/reboot/inspect/servicer.py @@ -31,6 +31,7 @@ from reboot.aio.state_managers import StateManager from reboot.aio.types import ApplicationId, ServerId, StateTypeName from reboot.controller.settings import ENVVAR_REBOOT_REPLICA_INDEX +from reboot.wait_for_tasks import wait_for_tasks from typing import AsyncIterator, Optional logger = get_logger(__name__) @@ -43,7 +44,7 @@ async def chunks_get_state( ) -> AsyncIterator[GetStateResponse]: """ Helper to chunk up `struct` into `chunk_size_bytes` chunks. - + This lets `GetState` responses avoid any max message size issues along the way, i.e., 4MB by default for gRPC, but also Envoy might not send more than 1MB. @@ -197,21 +198,11 @@ async def call_other_server(server_id: ServerId): yield ListStatesResponse(state_infos=state_infos) finally: - for task in all_tasks: - if not task.done(): - task.cancel() - # We need to explicitly `await` or call `.result()` on - # each task otherwise we'll get 'Task exception was never - # retrieved'. - for task in all_tasks: - try: - await task - except: - # We'll let what ever other exception has been - # raised propagate instead of this exception which - # might just be `CancelledError` from us - # cancelling the task. - pass + # Cancel any still running tasks and wait for all of them + # to finish, also making sure that we consume each task + # result avoiding 'Task exception was never retrieved' + # warnings. + await wait_for_tasks(all_tasks, cancel=True) async def ListStates( self, diff --git a/reboot/mcp/BUILD.bazel b/reboot/mcp/BUILD.bazel index 5abf70a1..49e40afc 100644 --- a/reboot/mcp/BUILD.bazel +++ b/reboot/mcp/BUILD.bazel @@ -16,19 +16,32 @@ py_library( ) py_library( - name = "proxy_py", - srcs = ["proxy.py"], + name = "request_state_py", + srcs = ["request_state.py"], srcs_version = "PY3", visibility = ["//visibility:public"], ) +py_library( + name = "iframe_py", + srcs = ["iframe.py"], + srcs_version = "PY3", + visibility = ["//visibility:public"], + deps = [ + ":request_state_py", + ], +) + py_library( name = "ui_py", srcs = ["ui.py"], + # Served verbatim by `dev_inline_html` when a Claude client hits a + # UI over a free-tier ngrok tunnel; read from runfiles at runtime. + data = ["claude_ngrok_error.html"], srcs_version = "PY3", visibility = ["//visibility:public"], deps = [ - ":proxy_py", + ":iframe_py", "//reboot:settings_py", ], ) @@ -40,6 +53,7 @@ py_library( visibility = ["//visibility:public"], deps = [ ":context_py", + ":request_state_py", "@com_github_reboot_dev_reboot//log:log_py", requirement("mcp"), requirement("anyio"), @@ -63,7 +77,8 @@ py_library( ":context_py", ":factories_py", ":helpers_py", - ":proxy_py", + ":iframe_py", + ":request_state_py", ":ui_py", ], ) diff --git a/reboot/mcp/claude_ngrok_error.html b/reboot/mcp/claude_ngrok_error.html new file mode 100644 index 00000000..e944c42c --- /dev/null +++ b/reboot/mcp/claude_ngrok_error.html @@ -0,0 +1,226 @@ + + + + + + + UI unavailable — Reboot + + + +
+ +

UI unavailable

+

Claude can’t view this UI over a free ngrok tunnel

+

+ ngrok’s free tier puts a browser interstitial in front of some + requests, which prevents this UI from loading. +

+

To view this UI, do any one of:

+
    +
  • + Use a paid ngrok plan, or a different tunnel, that doesn’t + inject an interstitial. +
  • +
  • + Open the app in a different MCP client — for example ChatGPT, or + MCPJam. These do work over free ngrok. +
  • +
+
+ + + diff --git a/reboot/mcp/factories.py b/reboot/mcp/factories.py index 0ff1810c..caadc3af 100644 --- a/reboot/mcp/factories.py +++ b/reboot/mcp/factories.py @@ -19,9 +19,8 @@ _init_session_state, _set_user_id, ) -from reboot.mcp.proxy import _UI_ASSETS_PREFIX +from reboot.mcp.request_state import _UI_ASSETS_PREFIX, _request_user_agent from reboot.mcp.ui import ( - _request_user_agent, _resolve_dist_path, _resource_meta, _ui_tool_cache_bust_info, @@ -238,8 +237,14 @@ async def reject(reason: str) -> None: def create_mcp_factory( *, server: FastMCP, - new_session_hooks: Sequence[Callable[[ExternalContext, Optional[str]], - Awaitable[None]]], + new_session_hooks: Sequence[Callable[ + [ExternalContext, Optional[str]], + Awaitable[None], + ]], + per_request_hooks: Sequence[Callable[ + [ExternalContext, Optional[str], Request], + Awaitable[None], + ]] = (), token_verifier: Optional[Any], ) -> Callable[ [Callable[[Request], Any]], @@ -253,6 +258,11 @@ def create_mcp_factory( new_session_hooks: Async callables to invoke when a new MCP session starts (e.g. to auto-construct state). Arguments passed: (external_context, user_id_or_none). + per_request_hooks: Async callables to invoke on *every* + inbound MCP request, regardless of session boundary. + Arguments passed: (external_context, user_id_or_none, + request) — the Starlette `Request` lets hooks inspect + headers (e.g., `x-forwarded-host`). token_verifier: Optional MCP SDK `TokenVerifier` (from `mcp.server.auth.provider`). When set, it can reject requests with missing or expired bearer tokens with an HTTP @@ -286,7 +296,7 @@ async def mcp_asgi_app( ) -> None: # Static-asset branch: serve the UI's dist files # directly from disk for production-mode MCP Apps. - # The path shape, set by `prod_loader_html`, is + # The path shape, set by `cache_busting_iframe_html`, is # `/mcp<_UI_ASSETS_PREFIX>//`. # Handled before any MCP-protocol logic (including # bearer-token auth) so the iframe, which has no @@ -303,11 +313,9 @@ async def mcp_asgi_app( request = Request(scope, receive, send) - # Publish the incoming User-Agent so `ui_html()` can - # branch on host identity (ChatGPT gets the relay - # iframe; others get the inlined bundle). Set here, - # before handing off to the MCP transport, so the - # contextvar is captured into downstream tasks. + # Publish the incoming User-Agent. Set here, before handing + # off to the MCP transport, so the contextvar is captured + # into downstream tasks. _request_user_agent.set(request.headers.get("user-agent")) # If no tools are registered, there is nothing useful for an @@ -408,6 +416,25 @@ async def mcp_asgi_app( for hook in new_session_hooks: await hook(external_context, user_id) + # Per-request hooks fire on every inbound MCP + # call. Failures are logged and swallowed — these hooks + # are meant to be observational, so a backend hiccup in a + # hook must never break the user-visible MCP call. + if per_request_hooks: + user_id_for_per_request = _get_user_id(request) + for per_request_hook in per_request_hooks: + try: + await per_request_hook( + external_context, + user_id_for_per_request, + request, + ) + except Exception: + logger.exception( + f"[{ui_id}] per-request hook " + f"{per_request_hook.__name__!r} raised; continuing" + ) + logger.debug(f"[{ui_id}] {request.method} {request.url.path}") # Per-request task group — clean lifecycle, no diff --git a/reboot/mcp/proxy.py b/reboot/mcp/iframe.py similarity index 55% rename from reboot/mcp/proxy.py rename to reboot/mcp/iframe.py index 3cbdc354..1c6fc6fc 100644 --- a/reboot/mcp/proxy.py +++ b/reboot/mcp/iframe.py @@ -1,145 +1,112 @@ -"""HMR loader for UIs (nested iframe with buffered relay). - -Why nested iframes: - -ext-apps renders UI HTML inside a sandbox-proxy iframe. The -sandbox-proxy has origin "null" (srcdoc), so relative URLs -don't resolve. We nest a real iframe that loads from Envoy -with a proper origin, so Vite HMR and relative imports work. - -Traffic flow:: - - Host -> sandbox-proxy -> dev_loader (this HTML) -> inner iframe - | - Envoy `/__/web/**` - | - Vite dev server - -Buffered relay (added with ext-apps v1.0.1 uplift): - -With ext-apps >=1.0.1, the sandbox-proxy delivers -`ui/notifications/tool-input` and `ui/notifications/tool-result` -immediately after its own initialization — before the inner -iframe's `PostMessageTransport` listener is ready. Without -buffering these messages are lost. - -The relay queues messages from the host until the inner -iframe sends its first message (the `ui/initialize` request -from `PostMessageTransport`), then flushes the queue. After -that, messages flow directly. - -On reconnect (backend restart causes a new `ui/initialize`), -saved `tool-input`/`tool-result` notifications are replayed so -the user doesn't need to manually refresh. - -Size handling (added with ext-apps v1.0.1 uplift): - -The inner iframe's `ui/notifications/size-changed` messages -are NOT forwarded to the host. Instead, the relay reflects -them into `document.body.style.height`, which the -sandbox-proxy's own `ResizeObserver` picks up. Forwarding -directly causes oscillation: the sandbox-proxy resizes, -triggering a new `size-changed` from the inner iframe, in a -loop. The CSS uses `overflow: hidden` and `display: block` on -the iframe to prevent scrollbar-induced layout thrash and the -inline baseline gap. - -Prior to ext-apps v1.0.1 the size-changed notification was -broken in the sandbox-proxy; the uplift to v1.0.1 fixed it -but exposed the oscillation issue in our nested-iframe -pattern, hence the interception here. -""" - import logging +from reboot.mcp.request_state import _UI_ASSETS_PREFIX, _request_user_agent from urllib.parse import quote logger = logging.getLogger(__name__) -# URL prefix for the production UI-assets endpoint. Used by -# `prod_loader_html` to build the inner-iframe URL and by the -# MCP ASGI dispatcher in `factories.py` to recognize incoming -# requests. Concatenated against the MCP mount path (`/mcp`) -# and matched verbatim against `scope["path"]`. -_UI_ASSETS_PREFIX = "/ui-assets/" - -def dev_loader_html( - ui_path: str, - reboot_url: str, - ui_name: str, -) -> str: - """Generate wrapper HTML with buffered relay for dev server. - - The sandbox-proxy (ext-apps host code) loads this HTML via - srcdoc. We create an inner iframe that loads from Envoy so - that the React app has a real origin for Vite HMR and - relative imports. - - Messages from the host (via sandbox-proxy) are buffered - until the inner iframe's `PostMessageTransport` sends its - first message (`ui/initialize`), then replayed in order. On - reconnect (new `ui/initialize`), saved `tool-input`/ - `tool-result` notifications are replayed. +def host_needs_cache_busting_iframe() -> bool: + """ + Return True if the incoming request looks like it's from ChatGPT. - Args: - ui_path: Path to the UI relative to project root - (e.g., "web/ui/clicker"). - reboot_url: Reboot server URL (extracted from request - headers). - ui_name: Display name from the API definition - (e.g., "show_clicker"). + See `cache_busting_iframe_html` below for why ChatGPT needs this. - Returns: - HTML wrapper with embedded iframe and buffered relay. + Matches `openai-mcp/*` and User-Agents containing `ChatGPT` + so the detection works across minor client-version bumps. """ + user_agent = _request_user_agent.get() + if not user_agent: + return False + return (user_agent.startswith("openai-mcp") or "ChatGPT" in user_agent) - # Inner iframe loads from Envoy which routes - # "/__/web/**" to dev server. - ui_url = ( - f"{reboot_url}/__/{ui_path}/index.html" - f"?mcpUiTitle={ui_name}" - ) - logger.debug(f"[mcp] Generating iframe wrapper for: {ui_url}") - - return _iframe_relay(ui_url=ui_url, ui_name=ui_name, variant="Dev") +def host_supports_iframe() -> bool: + """ + Return True if the client can render iframes. + + Most MCP hosts can, so this defaults to True. + + The known exception is the **claude.ai website**, which ignores the + `frameDomains` CSP and so can't load an inner iframe at all + (https://github.com/anthropics/claude-ai-mcp/issues/54). We detect + it by `Claude` (case-insensitive) in the User-Agent. + + NOTE: we could not find a User-Agent that reliably separates the + claude.ai website from **Claude Desktop** — which *does* support the + iframe and would benefit from it through tunnels. Remote MCP + connectors for every Claude surface (web, Desktop, mobile) connect + from Anthropic's shared cloud infrastructure rather than the local + client, so they appear to present the same User-Agent. We therefore + treat all `Claude` clients conservatively as iframe-incapable: + inline renders correctly everywhere, it's just less robust through + tunnels (e.g. the free ngrok tier's interstitial). TODO: If a + distinguishing signal for Claude Desktop turns up, narrow this match + so Desktop gets the iframe. + """ + user_agent = _request_user_agent.get() + if not user_agent: + # No User-Agent — assume a capable host (the default path). + return True + return "claude" not in user_agent.lower() -def prod_loader_html( +def cache_busting_iframe_html( ui_name: str, cache_bust: str, reboot_url: str, ) -> str: - """Generate wrapper HTML for a production (built) UI. - - Same shape as `dev_loader_html`: the outer sandbox-proxy - iframe (srcdoc, null origin) nests a real iframe that loads - from a Reboot backend URL with a proper origin, so relative - imports and CSP `connect-src` work. The buffered relay - forwards postMessage traffic between the sandbox-proxy and - the inner app. - - Why this exists for production: - - ChatGPT runs `tools/list` only once per connector lifetime; - once it has cached a UI tool's `_meta.ui.resourceUri`, it - keeps using that URI forever (uninstall + reinstall is the - only refresh path). UI resource URIs are discovered at - list-tools time, so the cache-bust token embedded in the - URI was computed at first-connection time and never - rotates from ChatGPT's perspective. Hosts that re-list per - session (Claude, MCPJam) get a fresh URI each session and - don't have this problem. - - Returning the bundle inline therefore strands ChatGPT on - whatever bytes were on disk when it first connected. By - returning a thin relay whose nested iframe loads from - `/mcp{_UI_ASSETS_PREFIX}//index.html`, - freshness no longer depends on the URI changing. Every - iframe mount triggers a new GET against the server, which - serves the current dist file from disk (the `` - segment is ignored server-side), and the user sees the - latest bundle. + """ + Generate wrapper HTML for MCP clients that need a cache-busting iframe. + + Most notably, that's ChatGPT. + + ## Why is this here? + + ChatGPT needs a cache-busting (nested) iframe. ChatGPT only runs tool + discovery at app-install time, and discovers UI resource URIs (only) + at that point. If the application changes its UIs (and thereby, + because of cache busting, their URIs) ChatGPT will not notice and + keep serving the old UIs - until the customer uninstalls and + reinstalls the app. + + There's no way for us to make things better in terms of tool + discovery, but we can at least give customers fresh UIs - by + bringing in a nested iframe. The static content that ChatGPT caches + will be only an iframe, which will point at a URL that serves the + latest UI. + + ## Buffered message relay + + With ext-apps >=1.0.1, the sandbox-proxy delivers + `ui/notifications/tool-input` and `ui/notifications/tool-result` + immediately after its own initialization — before the inner iframe's + `PostMessageTransport` listener is ready. Without buffering these + messages are lost. + + The cache-busting iframe queues messages from the host until the inner + iframe sends its first message (the `ui/initialize` request from + `PostMessageTransport`), then flushes the queue. After that, + messages flow directly. + + On reconnect (backend restart causes a new `ui/initialize`), saved + `tool-input`/`tool-result` notifications are replayed so the user + doesn't need to manually refresh. + + ## Size handling + + The inner iframe's `ui/notifications/size-changed` messages are NOT + forwarded to the host. Instead, the cache-busting iframe reflects them + into `document.body.style.height`, which the sandbox-proxy's own + `ResizeObserver` picks up. Forwarding directly causes oscillation: + the sandbox-proxy resizes, triggering a new `size-changed` from the + inner iframe, in a loop. The CSS uses `overflow: hidden` and + `display: block` on the iframe to prevent scrollbar-induced layout + thrash and the inline baseline gap. + + Prior to ext-apps v1.0.1 the size-changed notification was broken in + the sandbox-proxy; the uplift to v1.0.1 fixed it but exposed the + oscillation issue in our nested-iframe pattern, hence the + interception here. Args: ui_name: Display name from the API definition @@ -169,27 +136,60 @@ def prod_loader_html( logger.debug(f"[mcp] Generating prod iframe wrapper for: {ui_url}") - return _iframe_relay(ui_url=ui_url, ui_name=ui_name, variant="") + return _iframe_html(ui_url=ui_url, ui_name=ui_name) -def _iframe_relay(ui_url: str, ui_name: str, variant: str) -> str: - """Shared HTML: an outer page with a nested iframe and - postMessage forwarding. Used by both `dev_loader_html` and - `prod_loader_html`. +def dev_iframe_html( + ui_url_path: str, + reboot_url: str, + ui_name: str, +) -> str: + """ + Nested-iframe wrapper for dev/HMR. + + The inner iframe loads the page the Vite dev server is serving + (through Envoy, which proxies `/__/web/**`) rather than a built + artifact. Because the inner iframe has a real origin (the Reboot + server), Vite HMR and the page's own relative/absolute asset URLs + resolve on the same origin — making this path "just work", including + through tunnels. + + Args: + ui_url_path: The dev server URL path (rooted at the Reboot + origin) at which Vite serves this UI's `index.html` + directory, e.g. "/__/web/ui/clicker". + reboot_url: Reboot server URL (extracted from request + headers). The inner iframe is loaded from this origin. + ui_name: Display name from the API definition (e.g., + "show_clicker"). + + Returns: HTML. + """ + ui_url = ( + f"{reboot_url}{ui_url_path}/index.html" + f"?mcpUiTitle={quote(ui_name, safe='')}" + ) + + logger.debug(f"[mcp] Generating dev iframe wrapper for: {ui_url}") + + return _iframe_html(ui_url=ui_url, ui_name=ui_name) + + +def _iframe_html(ui_url: str, ui_name: str) -> str: + """ + HTML for an outer page with an iframe and `postMessage` forwarding. - The relay queues messages from the host until the inner - iframe's `PostMessageTransport` sends its first message - (`ui/initialize`), then replays them in order. On reconnect - (new `ui/initialize`), saved `tool-input`/`tool-result` - notifications are replayed. + Queues messages from the host until the iframe's + `PostMessageTransport` sends its first message (`ui/initialize`), + then replays them in order. On reconnect (new `ui/initialize`), + saved `tool-input`/`tool-result` notifications are replayed. """ - title_suffix = f" ({variant})" if variant else "" return f''' - {ui_name.title()}{title_suffix} + {ui_name.title()} + + + + + + + + + + + + + + + diff --git a/reboot/rootpage/hero.tsx b/reboot/rootpage/hero.tsx new file mode 100644 index 00000000..71f7ff41 --- /dev/null +++ b/reboot/rootpage/hero.tsx @@ -0,0 +1,103 @@ +import type { ReactNode } from "react"; +import { ConnectionState } from "./wizard"; + +interface HeroProps { + title: string; + // Absent when the developer didn't pass `description=`; the hero + // then shows a placeholder nudge instead of the description. + description?: string; + // `false` until the `Get` reader has produced at least one + // response. While this is `false` we don't know the app's real + // title or description yet, so the hero falls back to generic + // copy + an animated ellipsis in place of the unknown fields. + titleReady: boolean; + // Whether the app exposes MCP tools/resources. Switches the + // headline between the chat-client wizard framing and the + // generic "API is ready" framing. + hasMcpTools: boolean; + connectionState: ConnectionState; +} + +// Map the Reboot subscription's state to the badge text + CSS +// modifier on the hero pill. "Live" while we're receiving updates; +// "Disconnected" if the subscription aborted (e.g. the app was +// stopped); "Connecting…" before the first response arrives. +const CONNECTION_LABEL: Record = { + connected: "✓ Connected", + connecting: ( + <> + {"✗ Connecting"} +