diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index 782186335..320f19be1 100644 --- a/packages/core/e2e/e2e.test.ts +++ b/packages/core/e2e/e2e.test.ts @@ -149,10 +149,10 @@ const e2e = (fn: string) => { * mean testing something else, so a non-JS app skips them instead of carrying * them as gaps. * - * A handful of markers are weaker than that: health check, the webhook route, and - * app-provided API routes are protocol-level and *ought* to travel, but no other - * SDK serves them yet, so there is nothing to conform to. Those sites say so, and - * should move back to plain `test` as soon as a second implementation lands. + * A handful of markers are weaker than that: the webhook route and app-provided + * API routes are protocol-level and *ought* to travel, but no other SDK serves + * them yet, so there is nothing to conform to. Those sites say so, and should + * move back to plain `test` as soon as a second implementation lands. * * Every test not marked here is in scope for cross-language conformance, and is * gated only by `e2e-conformance.json`. No-op for the JS workbench apps. @@ -2929,12 +2929,7 @@ describe.concurrent('e2e', () => { // For production use on Vercel with Deployment Protection enabled, use the // queue-based `healthCheck(world, options)` function instead, which // bypasses protection by sending messages through the Queue infrastructure. - // JS-only for now, though no longer for want of a second implementation: - // vercel-py answers both probes as of vercel-py#292. What it omits is - // `workflowCoreVersion`, asserted below, on the grounds that it names a - // JavaScript package's version. Moving all three health-check tests out of - // js-only together means settling what a non-JS SDK reports there. - testJsOnly.skipIf(!isLocalDeployment())( + test.skipIf(!isLocalDeployment())( 'health check endpoint (HTTP) - workflow endpoint responds to __health query parameter', { timeout: 30_000 }, async () => { @@ -2955,21 +2950,32 @@ describe.concurrent('e2e', () => { ); expect(flowRes.status).toBe(200); expect(flowRes.headers.get('Content-Type')).toBe('application/json'); - const flowBody = await flowRes.json(); + const { workflowCoreVersion, ...flowBody } = await flowRes.json(); expect(flowBody).toEqual({ healthy: true, endpoint: '/.well-known/workflow/v1/flow', // specVersion comes from the World's declared specVersion (e.g. 3 // for world-vercel) or falls back to SPEC_VERSION_CURRENT (2). specVersion: expect.any(Number), - workflowCoreVersion: expect.any(String), }); - expect(flowBody.specVersion).toBeGreaterThanOrEqual(SPEC_VERSION_CURRENT); + // A JavaScript app is built from the same `@workflow/core` as this driver, + // so advertising an older spec version than the library it ships with is a + // regression. A second implementation's spec version is its own: it reports + // what it *writes*, so the only portable claim is that it is a real version. + if (isJsApp()) { + expect(flowBody.specVersion).toBeGreaterThanOrEqual( + SPEC_VERSION_CURRENT + ); + // See comments in the next test about workflowCoreVersion + expect(typeof workflowCoreVersion).toBe('string'); + } else { + expect(flowBody.specVersion).toBeGreaterThanOrEqual(1); + } // V2: no separate step endpoint — combined into the flow handler. } ); - testJsOnly( + test( 'health check (queue-based) - workflow endpoint responds to health check messages', { timeout: 60_000 }, async () => { @@ -2983,14 +2989,20 @@ describe.concurrent('e2e', () => { timeout: 30000, }); expect(workflowResult.healthy).toBe(true); - // The deployed app advertises its `@workflow/core` version so + // A JavaScript app advertises its `@workflow/core` version so // callers can derive capability metadata (see `getRunCapabilities` // in `capabilities.ts`). - expect(typeof workflowResult.workflowCoreVersion).toBe('string'); + // An SDK in another language has no such package, and the field is + // not advertised; cross-language capability detection should not + // be built on top of emulated `@workflow/core` version, thus needs + // further design and evolution. + if (isJsApp()) { + expect(typeof workflowResult.workflowCoreVersion).toBe('string'); + } } ); - testJsOnly( + test( 'health check (CLI) - workflow health command reports healthy endpoints', { timeout: 60_000 }, async () => { @@ -4734,7 +4746,13 @@ describe.concurrent('e2e', () => { // catch, with a message naming the violated rule and its limit. expect(outcomes.reserved).toMatch(/^FatalError: /); expect(outcomes.reserved).toContain('reserved prefix'); - expect(outcomes.reserved).toContain('allowReservedAttributes'); + // The message names the opt-out parameter, and each SDK names it in its + // own casing — `allowReservedAttributes` here, `allow_reserved_attributes` + // in Python. Assert that it points at the escape hatch, not how one + // language spells it. + expect(outcomes.reserved).toMatch( + /allow[_]?[rR]eserved[_]?[aA]ttributes/ + ); expect(outcomes.emptyKey).toContain('must not be empty'); expect(outcomes.keyTooLong).toContain( 'key length 257 exceeds limit 256' @@ -4747,7 +4765,10 @@ describe.concurrent('e2e', () => { 'byte length 400 exceeds limit 256' ); expect(outcomes.overCap).toContain('exceed limit 64'); - expect(outcomes.nonObject).toContain('requires a plain object'); + // Same idea: "plain object" in JS is "mapping" in Python. What the test + // is for is that a non-object argument is rejected by name at the call + // site, which either wording satisfies. + expect(outcomes.nonObject).toMatch(/requires a (plain object|mapping)/); // No invalid write reached the run, and the run stayed healthy // enough to complete a valid write afterwards. diff --git a/workbench/python/README.md b/workbench/python/README.md index e41ad5447..695776424 100644 --- a/workbench/python/README.md +++ b/workbench/python/README.md @@ -1,314 +1,69 @@ -# Python workbench app +# Python conformance workbench -A Python implementation of the workflow app side, so `packages/core/e2e/e2e.test.ts` -has something other than JavaScript to run against. The test driver stays in -TypeScript as the source of truth for the protocol, and this app is the -second implementation it drives. +This app runs the shared Workflow SDK e2e suite against the Python runtime. The +test driver and assertions remain in `packages/core/e2e/e2e.test.ts`; this +workbench supplies equivalent Python fixtures and exposes them through the same +manifest and workflow protocols as the JavaScript workbenches. -Built on [vercel-py](https://github.com/vercel/vercel-py), pinned by commit in -`pyproject.toml`: one `[tool.uv.sources]` entry for the umbrella `vercel` -package. The only code this app imports is `vercel.workflow`, which since -vercel-py#299 is its own `vercel-workflow` distribution. However, depending on that -name directly puts `@vercel/python` in workers mode and silently unsubscribes -the deployment from the queue, so the umbrella stays until the builder learns -the split. The note above the dependency has the detail. Everything but -`vercel` itself therefore resolves from PyPI, the runtime -included; the note above the source entry says how to check that what you got -matches the rev you asked for. Bump the rev deliberately, and rerun the suite -when you do. +`vercel-workflow` is sourced from the `main` branch of +[vercel-py](https://github.com/vercel/vercel-py). `uv.lock` records the exact +commit used by local development, CI, and Vercel builds, so updating upstream +does not change this workbench until the lockfile is refreshed. -## Running it +## Layout + +- `workflows/99_e2e.py` contains the Python fixtures. +- `e2e-conformance.json` declares which fixtures and tests Python supports. It + is a ratchet: a declared capability that disappears fails the suite instead + of silently becoming skipped. +- The `99_e2e` module and its camelCase fixture functions intentionally match + the TypeScript fixture path and exported names. +- `app.py` adapts the SDK's public HTTP handlers to bare ASGI. +- `pyproject.toml` selects the SDK source and the Vercel web/workflow + entrypoints. +- `vercel.json` selects the platform Python builder and exposes the manifest. + +## Run locally + +Install the locked Python environment and start the app: ```bash cd workbench/python uv sync --locked -WORKFLOW_PUBLIC_MANIFEST=1 pnpm dev # uvicorn on :3000 +WORKFLOW_PUBLIC_MANIFEST=1 pnpm dev ``` -Both of those guard the lock against your personal `~/.config/uv/uv.toml` -(`--locked` refuses to rewrite it, and `pnpm dev` passes `--no-config` to -`uv run`, which re-locks on startup otherwise). If you find an `[options]` block -at the top of `uv.lock`, something ran uv without one of them and CI will reject -the result. The note above `[tool.uv.sources]` in `pyproject.toml` has the rest, -including how to bump the pin. - -Then, from the repo root: +Then run the shared suite from the repository root: ```bash DEPLOYMENT_URL="http://localhost:3000" APP_NAME="python" \ pnpm vitest run packages/core/e2e/e2e.test.ts ``` -Both sides talk to the same `world-local` data directory -(`WORKFLOW_LOCAL_DATA_DIR`, defaulting to `.workflow-data` here) with -byte-compatible file formats, so the TypeScript CLI can inspect runs the Python -app produced: +The app and TypeScript driver share the local World data directory, so the +TypeScript CLI can inspect runs created by Python: ```bash -cd workbench/python && node ./node_modules/workflow/bin/run.js inspect --json runs +cd workbench/python +node ./node_modules/workflow/bin/run.js inspect --json runs ``` -## Running it against the Vercel world +## Update the Python SDK -Both sides are wired: the `workbench-python-workflow` project is rooted at -`workbench/python` and Git-connected, and the `e2e-vercel-prod` matrix carries a -`python` row (excluded from the `quickjs` VM axis, which is a JS-engine dimension -with no Python meaning). What makes the build work: +Update the locked `vercel-py/main` revision, then rerun the suite: -- `vercel.json` declares `pyproject.toml` as the build src. That is what puts - `@vercel/python` in "declared-only" mode. Without it, the `[tool.vercel]` - keys are ignored because the builder only attaches workflows to recognized - Python frameworks or to declared builds, and a bare ASGI app is neither. -- That `use` carries an **explicit builder version**, which the other workbench - projects do not need. An unpinned `use` resolves to whatever `@vercel/python` - ships inside the Vercel CLI the build platform happens to run, and that lags - npm. Builds were picking up 6.53.0 (CLI 58.1.0) well after 6.55.x was out. - 6.53.0 predates queue mode, so the builder silently fell back to "workers" - mode and pointed the workflow function straight at `app:registry`. That is a - `Workflows` object, not an ASGI callable, so every queue delivery 500'd with - `Could not determine the application interface for 'app:registry'`. The run - was created, the message was delivered, and nothing ever executed. Everything - below about consumer groups depends on queue mode, so the pin is what makes it - reachable at all. Bump it deliberately. -- `[tool.vercel] entrypoint = "app:app"` builds the web function. On Vercel it - serves exactly one useful route, `manifest.json`. Runs arrive over the queue, - so the hand-written `POST /flow` adapter is dead code there. It is how the - *local* world delivers, and only that. -- `[[tool.vercel.workflows]] entrypoint = "app:registry"` builds the workflow - function. At build time the builder imports `app`, reads - `vercel.queue.get_subscriptions()`, and writes one `queue/v2beta` trigger per - subscription. You can run that introspection yourself, exactly as the builder - does: - - ```bash - VERCEL=1 VERCEL_REGION=iad1 VERCEL_DEPLOYMENT_ID=dpl_introspection \ - uv run python -c 'import importlib; importlib.import_module("app") - from vercel.queue import get_subscriptions - print([(s.topic, s.consumer_group) for s in get_subscriptions()])' - # [('__wkf_workflow_*', 'default')] - ``` - - One topic, not two: since vercel-py #251 a step invocation rides the workflow - topic as a `stepId` on the invoke payload, the way the TypeScript SDK does it. - - `default` is the point. It is the consumer group `createWorkflowQueueTrigger` - writes for the TypeScript SDK on the same topics, which is what makes the - platform deliver to a Python consumer at all. Getting there needed - vercel/vercel#17236 (`@vercel/python` 6.54.0), which replaced a consumer name - derived from the output path with the introspected one. - - Reaching queue mode at all has a second, blunter condition: the builder looks - for the literal name `vercel` in `[project].dependencies` and then reads - `importlib.metadata.version("vercel")`, requiring >= 0.8.0. Both halves name - the umbrella distribution, so declaring only `vercel-workflow` (the package - that actually holds the runtime since vercel-py#299) drops the build back to - workers mode. That failure is quieter than the 6.53.0 one above: the trigger - is written for consumer `app_registry` on `__wkf_*` rather than `default` on - `__wkf_workflow_*`, so deliveries do not 500, they never arrive. Every run - times out with zero requests logged against it. No released builder knows the - new name (checked through 6.56.2). - -`.python-version` pins 3.14 so the deployed interpreter matches the local venv; -the builder would otherwise default to 3.12. - -The project also needs a `VERCEL_WORKFLOW_SERVER_URL` env var scoped to -**Preview**, with the same value every other workbench project has. On a PR the -`e2e-vercel-prod` job points the *driver* at a branch workflow-server -(`tests.yml:484`); without the matching variable on the project the deployed app -keeps writing to production `vercel-workflow.com`, and the two sides end up on -different primary stores. vercel-py reads it in -`vercel/workflow/_internal/worlds/vercel.py`. Production runs need nothing: the secret -resolves to `''` on `main`, so both sides use `vercel-workflow.com`. - -That variable is about the *app* reaching the right store, and pointing it at the -branch workflow-server (`e2e.vercel-workflow.com`) means clearing that server's -deployment protection. Two callers need to, and they need it differently. - -The *driver* clears it with the workbench project's own identity, so -`workbench-python-workflow` had to be listed in **that** project's Trusted -Sources alongside the JS workbench projects. It now is: driver writes land, and -runs are created. Before that, every write failed with `v4 createEvent: response -missing required x-wf-* headers` and a `SyntaxError: Unexpected token '<'`. -The response was an HTML SSO page, not a workflow-server response. - -The *app* clears it with a header, and until `vercel-py#278` vercel-py did not -send that header. `getHttpConfig` (`packages/world-vercel/src/utils.ts:366`) sets -both `Authorization: Bearer ` **and** -`x-vercel-trusted-oidc-idp-token: `; vercel-py's `_cbor_request` set only -the first. Trusted Sources reads the second, so the first call the workflow -handler made (`runs_get`, before any replay) came back `302` to the SSO page, -and every delivery 500'd: - -```text -HTTP Request: GET https://e2e.vercel-workflow.com/api/v2/runs/wrun_... "HTTP/1.1 302 Found" - File ".../worlds/vercel.py", line 219, in _cbor_request - result = resp.json() -json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) -cbor2.CBORDecodeEOF: premature end of stream +```bash +cd workbench/python +uv lock --upgrade-package vercel-workflow +uv sync --locked ``` -That the redirect surfaced as a CBOR decode error rather than as "you were -redirected to a login page" was a second bug in the same function: it parsed the -body before checking the status. Both are fixed in the pinned rev. The auth -headers now split proxy from direct the way `getHttpConfig` does, and a non-2xx -derives its error from the status before the body is touched. +## Vercel deployment -Production runs are unaffected on both counts: the secret is `''` on `main`, so -each side talks to `vercel-workflow.com`, which is not protected. +The `workbench-python-workflow` Vercel project is rooted at this directory and +is included in the `e2e-vercel-prod` matrix. -CI reaches *this* deployment past deployment protection through the project's -`trustedSources.oidcProviders` entry for `token.actions.githubusercontent.com`. -A `trustedSources.projects` entry would additionally let a locally pulled -`VERCEL_OIDC_TOKEN` in; the other workbench projects have one, this project does -not need it for CI. - -The divergences that cause failures are cataloged in vercel-py's queue notes: -region routing, no delivery cap, and a one-second floor on immediate re-enqueues. - -The one that used to block this lane outright was the queue transport. Once -queue mode was reached, every delivery 500'd on `UnicodeDecodeError: 'utf-8' -codec can't decode byte 0xb9` → `MessageCorruptedError`. `0xb9` is CBOR: -`@workflow/world-vercel` publishes the invoke payload with `CborTransport` -(`packages/world-vercel/src/queue.ts:34`) whenever the run's `specVersion >= 3`, -and the run's spec version comes from whoever *created* it. Here that is the -TypeScript driver, stamping 5 from `world-vercel`'s own `specVersion`, so -Python's declared 2 never enters the decision. vercel-py had only a JSON -consumer path. Nothing in this repo could override either side without pinning -the lane to a legacy spec version, which would have made the one lane testing -the current protocol the one lane not testing it. - -vercel-py fixed it in two parts, spanning two packages: `vercel-py#265` attaches -a CBOR-with-JSON-fallback transport to the workflow topic, and `vercel-py#266` -taught `Topic` / `TopicPattern` to carry a codec in the first place. That is why -the pin briefly needed a second `[tool.uv.sources]` entry. `vercel-queue` 0.8.0 -carries #266, so PyPI is enough again. Attaching it to the *subscription* rather than to a -client is what makes it work when deployed. The function that receives a push -delivery is the builder-generated `_vc_queue_handlers/.py`, whose -`vercel.queue.asgi_app()` constructs its own `QueueClient` with no arguments, so -a transport set on the world's client would never have been consulted. - -Confirmed fixed against a real deployment: on `36d5763a` the invoke payload -decodes and the workflow handler runs, which is how the protection-header gap -above became visible at all. It is the next call the handler makes. - -`world-local` was never affected. Python is producer and consumer there, so -JSON on both ends is self-consistent. - -Behind that sat one more, and it is why `cryptography` has to be installed. On -Vercel the run input is -written by the *driver*, and `@workflow/world-vercel` encrypts every payload it -can resolve a per-run key for, which on a deployment is all of them, with no -opt-out env var or config flag. Each run therefore arrived as an `encr` -envelope and died at the first hydration of its input, before any fixture code -ran: - -```text -SerializationError: the input of run wrun_... uses the 'encr' format, - which this SDK cannot read -``` - -vercel-py added read support in `vercel-py#279`: HKDF-SHA256 over the -deployment secret (32 zero salt bytes, `info = "|"`) to -derive the per-run key, then AES-256-GCM over `[nonce 12][ciphertext+tag]`. -The plaintext carries its own format prefix, so it re-enters `hydrate` as the -`devl` payload it would have been. AES-GCM comes from `cryptography`, which -`vercel-workflow` now requires outright; while the runtime lived in `vercel` it -sat behind an `encryption` extra this app had to remember to ask for, and -forgetting it produced the same failure with a message naming what to install. - -Reading is all this app needs. Python still writes plain `devl`, and the -TypeScript reader passes non-`encr` payloads through untouched -(`maybeDecrypt`, `packages/core/src/serialization/encryption.ts:284`), so the -two sides interoperate without Python ever encrypting anything. `encp`, the -X25519 sealed-box format one run uses to write to another, became readable in -`vercel-py#297`; nothing in the suite produces one either way. - -Again, `world-local` is exempt. It has no deployment key to derive from, so -the local lane could never have caught this. It is the clearest case so far of -why the Vercel lane earns its keep. - -The last one arrived from this repo rather than being found here. #3389 gave -`world-vercel` `specVersion: 6` (slot-numbered event IDs), so every run the -driver creates is stamped 6, while vercel-py typed the field as -`Literal[1, 2, 3, 4, 5]` on the shared event base. The write itself succeeded. -Parsing the `200` response raised. vercel-py now splits the two meanings the -way TypeScript does: `SPEC_VERSION_CURRENT` stays 2 (what Python stamps) and -`SPEC_VERSION_MAX_SUPPORTED` is 6 (what Python will read), which is what keeps -version 7 from being the same outage. `world-local` is on 5, so once more only -the deployed lane saw it. - -Slot IDs themselves need nothing from Python: a slot body is 26 zero-padded -decimal digits, which every ULID validator already accepts and which sorts the -same way. The one thing that breaks is decoding a timestamp out of an event id, -and vercel-py never does. It mints IDs only for `world-local`. - -The last one only ever showed up here, and it read as flakiness for two rounds -before it read as a bug: one arbitrary run per suite died with `WorkflowWorldError: -workflow run wrun_… not found` about 400ms after `start()`, the queue did not -redeliver, and the driver sat out its 60-second timeout. A different fixture each -time, which is what made it look random. It is not: `start()` -(`packages/core/src/runtime/start.ts:577`) issues `run_created` and the queue push -**in parallel**, deliberately. "If events.create fails with 429/5xx, the run was -still accepted via the queue," so a consumer that finds no run row has found a -normal state, not an error one. The TypeScript consumer never reads the row at -all: it writes `run_started`, which is idempotent and creates the run when -`run_created` was never seen, and takes the run entity out of that response. -vercel-py had it the other way round (`runs_get`, then `run_started` only if -`status == "pending"`) and 500'd on the 404, and it dropped the `runInput` the -message carries for exactly this purpose: input, deployment ID, workflow name, -spec version, attribute seed. `vercel-py#282` carries `runInput` through, -`#284` bootstraps from `run_started`, and `#283` gives `world-local` the same -resilient start. This also retired the app's last `unsupported` entry, -the deterministic version of the same defect: *resilient start: addTenWorkflow -completes when run_created returns 500* deletes the row on purpose. - -`world-local` did have the race, and could never lose it: both writes are local -files in one process while the queue delivery takes an HTTP round trip, so -`run_created` always won. - -## Conformance baseline - -What the suite runs here is declared in `e2e-conformance.json`: the ported -fixtures and, when there is one, an `unsupported` map naming individual tests -whose failure is a runtime gap rather than a missing fixture. There is none right -now. Both axes are ratchets: a claim that stops being true fails the run instead -of quietly skipping, so growing the file is the only way to move. -`ConformanceConfig` in `packages/core/e2e/utils.ts` spells out each direction. - -Current baseline: **9 passing, 128 skipped, of 137** on `world-local`, and -**8 of 156** on Vercel. It is one baseline, not two. The extra 19 collected on -Vercel are `e2e-agent.test.ts`, which that lane also picks up and skips whole, -and the ninth pass is `deploymentId: 'latest' is a no-op in non-Vercel worlds`, -which is local by definition. - -## What is missing - -This app is honest about being early. In rough order of how much it costs: - -- **Most fixtures are not ported yet.** This includes 66 tests across 52 fixtures. - They are not blocked on one thing anymore: the largest blocks are hooks (19 - tests, where vercel-py's `BaseHook.wait()` has a different shape than the - async-iterable hook the fixtures use), streams (11, where vercel-py now has - `read_stream` / `get_writable` and nothing here uses them yet), - `setAttributes` (9, no Python equivalent), and `FatalError` / - `RetryableError` (7; `FatalError` is exported now, and `RetryableError` has no - Python counterpart at all). -- **The `.well-known/workflow/v1` surface lives in `app.py`, not the SDK**, and - reaching it needs three `vercel.workflow._internal` imports - (`workflow_entrypoint`, `FLOW_ROUTE`, and the `HTTPRequest` base), none of - which has a public equivalent. The module docstring explains why that surface - belongs in the app. -- **The health-check tests stay JS-only, but no longer for want of an - implementation.** vercel-py answers both probes as of `vercel-py#292`: the - `?__health` one inside `workflow_entrypoint` (so `app.py` only routes to it) - and the queue-based one in `workflow_handler`. What the driver additionally - asserts is a `workflowCoreVersion` string, which Python deliberately omits - because it names a JavaScript package's version and the reader feeds it to - that package's capability tables. Moving the three tests across means deciding - what a non-JS SDK should report there. -- **No webhook route and no app-specific API routes**, so the webhook and - direct-step-call tests are JS-only too. - -Two things in `workflows/99_e2e.py` look like mistakes but are intentional: a module -name starting with a digit, and Python functions in camelCase. Its docstring -says why both are load-bearing; don't "fix" either without reading it. +`vercel.json` selects the Python builder, while `pyproject.toml` declares the +ASGI web entrypoint and workflow registry. `.python-version` selects the +deployed Python version. Preview deployments use `VERCEL_WORKFLOW_SERVER_URL` +to target the same workflow server as the test driver. diff --git a/workbench/python/app.py b/workbench/python/app.py index c5bc6a6ae..7211f6a8a 100644 --- a/workbench/python/app.py +++ b/workbench/python/app.py @@ -1,18 +1,15 @@ """ASGI adapter that puts a Python workflow app behind the routes the TypeScript e2e suite drives. -The Python SDK has no `.well-known/workflow/v1` HTTP surface: deployed Python is -invoked by platform queue triggers on `/_py_workflows/`, so it ships no -manifest generator, no route table, and no concrete `HTTPRequest`. This file -supplies all three. They live here rather than in the SDK because they implement -a contract the SDK does not claim to serve. What the SDK does own is the flow -route's *behaviour*: `workflow_entrypoint` knows the path it belongs on -(`runtime.FLOW_ROUTE`) and answers the `?__health` probe itself. +The Python SDK owns the workflow and manifest handlers, their public route +constants, and the abstract HTTP request/response types. This file only adapts +those handlers to a bare ASGI app; a framework integration would do the same +routing itself. -What it does *not* do is translate any protocol. `LocalWorld.create_queue_handler` +It does not translate any protocol. `LocalWorld.create_queue_handler` already reads exactly the headers `@workflow/world-local` sends and answers `{"ok": true}`, and both sides read `WORKFLOW_LOCAL_DATA_DIR` with byte-compatible -file formats. So the whole adapter is routing plus a manifest. +file formats. Only the *first* workflow trigger of a run arrives over HTTP. `LocalWorld.queue` publishes to an embedded in-process queue service, so every step invocation and @@ -22,12 +19,11 @@ workflow continuation after that stays inside this process. from __future__ import annotations import importlib -import json from typing import Any, AsyncIterator, Callable import httpx +from vercel.workflow import ENDPOINT_PATH, MANIFEST_PATH, HTTPRequest, HTTPResponse from vercel.workflow._internal import world as w -from vercel.workflow._internal.runtime import FLOW_ROUTE, workflow_entrypoint # The fixture module is named to match `workbench/example/workflows/99_e2e.ts`, # which a plain `import` statement cannot express — a module name may not start @@ -35,57 +31,11 @@ from vercel.workflow._internal.runtime import FLOW_ROUTE, workflow_entrypoint FIXTURES_MODULE = "workflows.99_e2e" fixtures = importlib.import_module(FIXTURES_MODULE) registry = fixtures.app - -# One entrypoint serves both replays and step invocations: since vercel-py #251 -# a step rides the `__wkf_workflow_*` topic as a `stepId` on the invoke payload, -# matching the TypeScript SDK. Calling this is also what subscribes the handler -# to that topic, which is how in-process dispatch gets delivered. -flow_handler = workflow_entrypoint(registry) - -# The SDK's own constant, so the two agree by construction. Everything else this -# app serves is a sibling of it. -ROUTE_BASE = FLOW_ROUTE.rsplit("/", 1)[0] -MANIFEST_VERSION = "1.0.0" +flow_handler = registry.http_handler +manifest_handler = registry.manifest_handler -def _module_to_file(module: str) -> str: - """`workflows.99_e2e` -> `workflows/99_e2e.py`. - - The suite looks fixtures up by their TypeScript path (`workflows/99_e2e.ts`) - and matches manifest keys by suffix after stripping the extension, so the - stem has to agree even though the extension does not. - """ - return f"{module.replace('.', '/')}.py" - - -def build_manifest() -> dict[str, Any]: - """Emit the manifest from the registry. - - `Workflows` exposes no enumeration API, so this reads the private dicts. - Publishing `qualname -> workflow_id` straight from them is the reason the - fixtures use the TS functions' camelCase names: there is no name mapping to - maintain, and a fixture that gets renamed or unregistered disappears from the - manifest instead of silently pointing at the wrong ID. - """ - workflows: dict[str, dict[str, Any]] = {} - steps: dict[str, dict[str, Any]] = {} - - for workflow in registry._workflows.values(): - file = _module_to_file(workflow.module) - workflows.setdefault(file, {})[workflow.qualname] = { - "workflowId": workflow.workflow_id - } - - for step in registry._steps.values(): - # Step names are `step//{module}//{qualname}`. - _, module, qualname = step.name.split("//", 2) - file = _module_to_file(module) - steps.setdefault(file, {})[qualname] = {"stepId": step.name} - - return {"version": MANIFEST_VERSION, "workflows": workflows, "steps": steps} - - -class AsgiRequest(w.HTTPRequest): +class AsgiRequest(HTTPRequest): """The `HTTPRequest` the SDK's flow handler expects, over an ASGI scope.""" def __init__(self, scope: dict[str, Any], receive: Callable) -> None: @@ -123,19 +73,18 @@ class AsgiRequest(w.HTTPRequest): break -async def _send_json(send: Callable, status: int, payload: Any) -> None: - body = json.dumps(payload).encode() +async def _send_response(send: Callable, response: HTTPResponse) -> None: await send( { "type": "http.response.start", - "status": status, + "status": response.status, "headers": [ - (b"content-type", b"application/json"), - (b"content-length", str(len(body)).encode()), - ], + (key.encode(), value.encode()) for key, value in response.headers.items() + ] + + [(b"content-length", str(len(response.body)).encode())], } ) - await send({"type": "http.response.body", "body": body}) + await send({"type": "http.response.body", "body": response.body}) async def app(scope: dict[str, Any], receive: Callable, send: Callable) -> None: @@ -156,8 +105,9 @@ async def app(scope: dict[str, Any], receive: Callable, send: Callable) -> None: method = scope["method"] query = scope.get("query_string", b"").decode() - if path == f"{ROUTE_BASE}/manifest.json" and method in ("GET", "HEAD"): - await _send_json(send, 200, build_manifest()) + if path == MANIFEST_PATH and method in ("GET", "HEAD"): + response = await manifest_handler(AsgiRequest(scope, receive)) + await _send_response(send, response) return # Two things share this route, and the handler tells them apart itself: @@ -168,19 +118,12 @@ async def app(scope: dict[str, Any], receive: Callable, send: Callable) -> None: # headers, so this app only routes. The other, stream-based `healthCheck()` # protocol needs nothing here at all: it arrives as a queue delivery, and # `workflow_handler` answers it before it looks for a run. - if path == FLOW_ROUTE and (method == "POST" or "__health" in query): + if path == ENDPOINT_PATH and (method == "POST" or "__health" in query): response = await flow_handler(AsgiRequest(scope, receive)) - await send( - { - "type": "http.response.start", - "status": response.status, - "headers": [ - (k.encode(), v.encode()) for k, v in response.headers.items() - ] - + [(b"content-length", str(len(response.body)).encode())], - } - ) - await send({"type": "http.response.body", "body": response.body}) + await _send_response(send, response) return - await _send_json(send, 404, {"error": f"No route for {method} {path}"}) + await _send_response( + send, + HTTPResponse.json({"error": f"No route for {method} {path}"}, status=404), + ) diff --git a/workbench/python/e2e-conformance.json b/workbench/python/e2e-conformance.json index 55f8e276d..fbf2c2cb6 100644 --- a/workbench/python/e2e-conformance.json +++ b/workbench/python/e2e-conformance.json @@ -10,10 +10,56 @@ "language": "python", "fixtures": [ "addTenWorkflow", + "errorFatalCatchable", + "errorRetryCustomDelay", + "errorRetryDisabled", + "errorRetryFatal", + "errorRetrySuccess", + "errorStepThrowFatalRoundTrip", + "errorWorkflowThrowFatalRoundTrip", + "hookAdoptOwnerResultWorkflow", + "hookClaimOnlyMutexWorkflow", + "hookCleanupTestWorkflow", + "hookDisposeTestWorkflow", + "hookGetConflictThenStepParallelWorkflow", + "hookGetConflictWithParallelStepWorkflow", + "hookGetConflictWithPriorStepWorkflow", + "hookGetConflictWorkflow", + "hookSignalOwnerWorkflow", + "hookTokenReuseLoopWorkflow", + "hookWithSleepFinalStepWorkflow", + "hookWithSleepWorkflow", + "hookWorkflow", + "metadataFromHelperWorkflow", "nullByteWorkflow", + "outputStreamInsideStepWorkflow", + "outputStreamWorkflow", "parallelSleepWorkflow", "promiseAllWorkflow", + "promiseAnyWorkflow", + "promiseRaceStressTestWorkflow", + "promiseRaceWorkflow", + "retainedInterleavingWorkflow", + "setAttributesFireAndForgetWorkflow", + "setAttributesInsideStepWorkflow", + "setAttributesParallelWorkflow", + "setAttributesThrowsAfterWorkflow", + "setAttributesValidationWorkflow", + "setAttributesWorkflow", "sleepInLoopWorkflow", - "sleepingWorkflow" - ] + "sleepWinsRaceWorkflow", + "sleepWithSequentialStepsWorkflow", + "sleepingWorkflow", + "spawnWorkflowFromStepWorkflow", + "stepNotRegisteredCatchable", + "stepNotRegisteredUncaught", + "stepWinsRaceWorkflow", + "utf8StreamWorkflow", + "writableForwardedFromStepWorkflow", + "writableForwardedFromWorkflowWorkflow" + ], + "unsupported": { + "fire-and-forget: void setAttributes lands without awaiting": "`set_attributes()` is now an `async def`: scheduling it can reproduce the unawaited writes before a later suspension, but the final scheduled coroutine does not start before the workflow body returns, so there is no final `attr_set` to drain. The run completes with `phase: mid` instead of `phase: done`.", + "validation DX: invalid writes throw catchable FatalErrors naming rule and limit": "The new `set_attributes()` validator still raises a catchable `FatalError` for a reserved key, but its message ends at `starts with reserved prefix '$'` and no longer names the `allow_reserved_attributes=True` escape hatch that this test requires. The other six validation cases and the valid write afterwards pass." + } } diff --git a/workbench/python/package.json b/workbench/python/package.json index f7151b500..16af3068f 100644 --- a/workbench/python/package.json +++ b/workbench/python/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "license": "Apache-2.0", "scripts": { - "dev": "uv run --no-config uvicorn app:app --port 3000" + "dev": "uv run uvicorn app:app --port 3000" }, "devDependencies": { "workflow": "workspace:*" diff --git a/workbench/python/pyproject.toml b/workbench/python/pyproject.toml index 681bf5e38..c46ed90d8 100644 --- a/workbench/python/pyproject.toml +++ b/workbench/python/pyproject.toml @@ -4,88 +4,15 @@ version = "0.0.0" description = "Python conformance target for the @workflow/core e2e suite" requires-python = ">=3.10" dependencies = [ - # The only thing imported here is `vercel.workflow`, which as of vercel-py#299 - # is its own `vercel-workflow` distribution — but depending on *that* directly - # is what takes the Vercel lane down. `@vercel/python` picks queue mode over - # workers mode by looking for the literal name `vercel` in this list - # (`hasExplicitVercelSdkDependency`) and then probing - # `importlib.metadata.version("vercel")`; neither has heard of the split, up - # to and including 6.56.2. With no match it silently serves workflows through - # vercel-workers: the trigger becomes `__wkf_*` / consumer `app_registry` - # instead of `__wkf_workflow_*` / consumer `default`, so nothing the - # TypeScript driver enqueues is ever delivered and every run times out having - # logged zero requests. Depend on the umbrella package and let it pull the - # runtime (it requires `vercel-workflow>=0.9.0,<1.1.0`) until the builder - # learns the new name. - # - # `cryptography` now comes along outright rather than behind the `encryption` - # extra this app used to have to ask for. It is not optional either way: on - # Vercel the run input is written by the TypeScript driver and - # `@workflow/world-vercel` encrypts it whenever it can resolve a per-run key, - # which on a deployment it always can, with no opt-out. - "vercel", - # vercel-py carries uvicorn as a dev-only dependency, so declare it here. + "vercel-workflow", "uvicorn>=0.30", ] -# Pinned to a vercel-py commit rather than a release: this app tracks the -# Python SDK's `main` so the conformance suite reports on what is actually -# being built. Bump `rev` deliberately, and re-run the suite when you do. -# -# `subdirectory` is required — the vercel-py repo root is a uv workspace whose -# build backend raises, so only the leaf packages are installable on their own. -# -# One entry, and note what it does *not* cover. A `[tool.uv.sources]` entry -# applies only to a package this project requires directly, so every sibling — -# `vercel-workflow` included, which is where all the code this app exercises -# now lives — resolves from PyPI at whatever the last release was, regardless -# of the rev above. At this rev that is harmless: PyPI's `vercel-workflow` -# 0.9.0 is byte-identical to `src/vercel-workflow` at `4814d61f`, because #299 -# created the package and the release was cut from it. It will stop being -# harmless the moment main moves ahead of the release. -# -# So when bumping the rev, diff the runtime you actually got against the rev -# you asked for, and git-pin `vercel-workflow` here *and* in `dependencies` -# above if they differ — a source alone does nothing: -# -# rm uv.lock && uv lock --no-config -# grep -c 'source = { git' uv.lock # 1 per git-pinned package -# uv sync --locked --no-config -# git -C archive :src/vercel-workflow/vercel | tar x -C /tmp/wf -# diff -rq /tmp/wf .venv/lib/python*/site-packages/vercel | grep -v '^Only in .venv' -# -# Two traps, each of which fails quietly: -# -# - Relocking *in place* keeps the old lock as preferences and can hold a -# package at its old version. Hence the `rm`. `uv lock` on a -# self-consistent lock is also a no-op ("Resolved 34 packages in 5ms"), so -# without the `rm` a bad lock survives untouched. -# - `--no-config` keeps a personal `~/.config/uv/uv.toml` (an `exclude-newer`, -# say) from being recorded as an `[options]` block that CI and the Vercel -# builder then reject under `--locked`. `uv run` re-locks too, which is why -# the `dev` script passes it as well. [tool.uv.sources] -vercel = { git = "https://github.com/vercel/vercel-py", rev = "4814d61fa2ac1074ea5d67aede8f159ba1daae9b", subdirectory = "src/vercel" } +vercel-workflow = { git = "https://github.com/vercel/vercel-py", branch = "main", subdirectory = "src/vercel-workflow" } -# How `@vercel/python` builds this app. Reached only because `vercel.json` -# declares `pyproject.toml` as the build src, which puts the builder in -# "declared-only" mode: filename-based entrypoint detection never runs, and -# `[[tool.vercel.workflows]]` is honoured. Without that src the two keys below -# are silently ignored — a bare ASGI app is not a recognised Python framework, -# and the builder only attaches workflows to frameworks or declared builds. [tool.vercel] -# The web function. It serves `manifest.json`, which is the only route the e2e -# driver calls over HTTP on Vercel — runs are delivered through the queue, not -# through `/flow`. entrypoint = "app:app" -# The workflow function. The builder imports this module at build time, reads -# `vercel.queue.get_subscriptions()`, and emits one `queue/v2beta` trigger per -# subscription onto a generated `vercel.queue.asgi_app()` handler. Importing -# `app` is what registers them, because `app.py` calls `workflow_entrypoint()` -# at import. -# -# Only one entry is allowed: every workflow consumer receives every `__wkf_*` -# message, so a second one would fail on runs registered only in the first. [[tool.vercel.workflows]] entrypoint = "app:registry" diff --git a/workbench/python/uv.lock b/workbench/python/uv.lock index 8e7c91090..97bdb566c 100644 --- a/workbench/python/uv.lock +++ b/workbench/python/uv.lock @@ -202,80 +202,68 @@ wheels = [ [[package]] name = "click" -version = "8.4.2" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, -] - -[[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" }, + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, ] [[package]] name = "cryptography" -version = "50.0.0" +version = "50.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, - { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, - { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, - { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, - { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, - { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, - { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, - { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, - { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, - { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, - { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, - { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, - { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, - { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, - { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, - { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, - { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, - { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, - { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, - { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, - { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, - { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, - { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, - { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, - { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, - { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, - { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, - { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, - { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, - { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, - { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, - { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, - { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, - { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, - { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, - { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350, upload-time = "2026-08-25T19:45:26.551Z" }, + { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675, upload-time = "2026-08-25T19:45:28.658Z" }, + { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410, upload-time = "2026-08-25T19:45:30.816Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378, upload-time = "2026-08-25T19:45:33.043Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889, upload-time = "2026-08-25T19:45:35.086Z" }, + { url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" }, ] [[package]] @@ -365,11 +353,11 @@ wheels = [ [[package]] name = "idna" -version = "3.18" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -512,15 +500,6 @@ wheels = [ { 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 = "python-dotenv" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, -] - [[package]] name = "python-multipart" version = "0.0.32" @@ -530,6 +509,60 @@ 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 = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0" @@ -553,107 +586,47 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.52.3" +version = "0.52.4" 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/2e/28/64ca011edf31c715b4fad359c587ea52391aaffa125065695590241ff617/uvicorn-0.52.3.tar.gz", hash = "sha256:18857b9e6579300be55c91c0a1cfd37d9a2cf0cabea33b88275f199eb73b8b58", size = 100621, upload-time = "2026-08-13T16:50:02.899Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/2b/ebd108734a8204c6b4b93c681c9a38c5273b3ccd5d129fee4ffc1d97772c/uvicorn-0.52.3-py3-none-any.whl", hash = "sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c", size = 79859, upload-time = "2026-08-13T16:50:01.323Z" }, -] - -[[package]] -name = "vercel" -version = "0.10.0" -source = { git = "https://github.com/vercel/vercel-py?subdirectory=src%2Fvercel&rev=4814d61fa2ac1074ea5d67aede8f159ba1daae9b#4814d61fa2ac1074ea5d67aede8f159ba1daae9b" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, - { name = "vercel-cache" }, - { name = "vercel-headers" }, - { name = "vercel-internal-core" }, - { name = "vercel-internal-telemetry" }, - { name = "vercel-oidc" }, - { name = "vercel-queue" }, - { name = "vercel-sandbox" }, - { name = "vercel-workflow" }, - { name = "websockets" }, -] - -[[package]] -name = "vercel-cache" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "vercel-headers" }, - { name = "vercel-internal-telemetry" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f2/c7/f264c2e7a5ec54851257a504e746cbd20d37c85a39a285a7c4e89c06c1e3/vercel_cache-0.7.2.tar.gz", hash = "sha256:d0ef5499b412adbe813a6cc77fad32f908b7453c1e3c8a9471fd2ad0718397cb", size = 9919, upload-time = "2026-08-08T00:33:48.089Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/d4/62c87adef7e7802854eb019c22eef7115ff9282c15f16cde34471bf913c9/vercel_cache-0.7.2-py3-none-any.whl", hash = "sha256:f744d6b3916aace4ca943239336362363994c30583654599fd8d415ae6915948", size = 10346, upload-time = "2026-08-08T00:33:47.208Z" }, + { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" }, ] [[package]] name = "vercel-headers" version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/50/2fbe5563b1f710c5cec3076efe94df887dd89c7ce0f4238b81f169f5d733/vercel_headers-0.7.1.tar.gz", hash = "sha256:46e63a99a3095f3b4fa324c80e5aeee5886ca474ef5ecbfe6eabb5f2bd64928a", size = 5410, upload-time = "2026-07-16T14:23:35.768Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/b8/5484e7ecf876c031831623efc547edcf94e2e33db4251d17c2d117c18cb0/vercel_headers-0.7.1-py3-none-any.whl", hash = "sha256:6ef65f0baffdf19775b693265d6118e125251456fb192324f4006dd341c77abc", size = 3907, upload-time = "2026-07-16T14:23:34.942Z" }, -] +source = { git = "https://github.com/vercel/vercel-py?subdirectory=src%2Fvercel-headers&branch=main#ac7f7587db7b96191a24dd9e35f2f5eb6fd0198f" } [[package]] name = "vercel-internal-core" version = "0.1.2" -source = { registry = "https://pypi.org/simple" } +source = { git = "https://github.com/vercel/vercel-py?subdirectory=src%2Fvercel-internal-core&branch=main#ac7f7587db7b96191a24dd9e35f2f5eb6fd0198f" } dependencies = [ { name = "anyio" }, { name = "httpx" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/3b/187c5e5974e5bd9a9bcfb6b0a54aaa032a784d5eb0bfbfe8e8de7fd786fa/vercel_internal_core-0.1.2.tar.gz", hash = "sha256:044ac7da4e16d31fa3ef95726194ec35bdf1821a5226e4d7109be1eed71c828d", size = 18663, upload-time = "2026-08-07T23:45:59.862Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/b4/853d703b06a12f5ff1a661e569efa9b45674fa1dbd8fa27a3129dd618240/vercel_internal_core-0.1.2-py3-none-any.whl", hash = "sha256:a70f900723a3e8a08d7746489ae8307d68e4dc385fc35d9fd4245b4ea1fd4f2c", size = 22507, upload-time = "2026-08-07T23:45:58.909Z" }, -] - -[[package]] -name = "vercel-internal-telemetry" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "vercel-oidc" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/d8/fdbe1e76dca9520e628715dd232f8567d045cd0b3cc2c559182caf2c74c5/vercel_internal_telemetry-0.7.2.tar.gz", hash = "sha256:bbe3d943252911853615722767a54be8e0873c00d1b079d25121f1ea88ed70f8", size = 9176, upload-time = "2026-08-08T00:32:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/db/fa4879dc9b1914424496e2e12966aec0f3a5849556f4586592235c8fa312/vercel_internal_telemetry-0.7.2-py3-none-any.whl", hash = "sha256:6c6dd20e0803ab070b71e887c9b56a1e0d7e1ccdabcaf45aa0b12b6be54ba7a3", size = 8253, upload-time = "2026-08-08T00:32:51.095Z" }, -] [[package]] name = "vercel-oidc" version = "0.8.0" -source = { registry = "https://pypi.org/simple" } +source = { git = "https://github.com/vercel/vercel-py?subdirectory=src%2Fvercel-oidc&branch=main#ac7f7587db7b96191a24dd9e35f2f5eb6fd0198f" } dependencies = [ { name = "anyio" }, { name = "httpx" }, { name = "vercel-headers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/c5/fa8c0c298f86e9e144fb012b3f6d45490e9af240dd6d3319cd2e2441002b/vercel_oidc-0.8.0.tar.gz", hash = "sha256:dbcb1e19697c87f89ae25c5bfeb8018fdb3904d9d92290796cc290ae7eeef61e", size = 16117, upload-time = "2026-08-08T00:16:40.203Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/d6/c380d2d171840a67883a30f453b2edb3adabab3b5519941b445c312135c6/vercel_oidc-0.8.0-py3-none-any.whl", hash = "sha256:fb8bb715f20edf5539554951d0374b4956550688c8a8b7ccb69f8275ff464e22", size = 15996, upload-time = "2026-08-08T00:16:39.349Z" }, -] [[package]] name = "vercel-queue" version = "0.8.0" -source = { registry = "https://pypi.org/simple" } +source = { git = "https://github.com/vercel/vercel-py?subdirectory=src%2Fvercel-queue&branch=main#ac7f7587db7b96191a24dd9e35f2f5eb6fd0198f" } dependencies = [ { name = "anyio" }, { name = "httpx", extra = ["http2"] }, @@ -663,162 +636,23 @@ dependencies = [ { name = "vercel-internal-core" }, { name = "vercel-oidc" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/c9/805f5d3ee0c5c2685732293a5d6294da746cc48004432f1461d2f8417ada/vercel_queue-0.8.0.tar.gz", hash = "sha256:f8758804f1efeba14d1d0e4ca55a0e9d63d9917a8c37645073a6688d44129967", size = 81809, upload-time = "2026-08-12T23:40:56.177Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/a9/981ac481e938807103ba0253831dde93ed7c7a6aa6e00d9b5c6ea21082d8/vercel_queue-0.8.0-py3-none-any.whl", hash = "sha256:077dc24cc2cf4557193df03b59b61acdde41d29bab12d994c18f4b133a79437e", size = 97105, upload-time = "2026-08-12T23:40:55.172Z" }, -] - -[[package]] -name = "vercel-sandbox" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "pydantic" }, - { name = "vercel-internal-core" }, - { name = "vercel-oidc" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a0/96/190a7c397a3f05ae4186c3b7d2c84afde9257e5da554df5654d14e870b95/vercel_sandbox-0.4.0.tar.gz", hash = "sha256:e0a58b727cce3712fca8ad90132554b50361e6f68b5d4328a247e42ff78bb481", size = 66629, upload-time = "2026-08-08T00:33:33.553Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/f5/f2c73825ebc24f614f28ce0940b8372ea609a51bc88331f0a880ac414f6a/vercel_sandbox-0.4.0-py3-none-any.whl", hash = "sha256:b516e087e93805a75cc02d5f6379c942ebbe939f8adc680ae69d9cc8f5cb564b", size = 78054, upload-time = "2026-08-08T00:33:32.631Z" }, -] [[package]] name = "vercel-workflow" version = "0.9.0" -source = { registry = "https://pypi.org/simple" } +source = { git = "https://github.com/vercel/vercel-py?subdirectory=src%2Fvercel-workflow&branch=main#ac7f7587db7b96191a24dd9e35f2f5eb6fd0198f" } dependencies = [ { name = "anyio" }, { name = "cbor2" }, { name = "cryptography" }, { name = "httpx" }, { name = "pydantic" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "vercel-internal-core" }, { name = "vercel-oidc" }, { name = "vercel-queue" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/91/e852e0803d45c06c0f493d1b96fe8b1e6a7246f971889d47182e7722b547/vercel_workflow-0.9.0.tar.gz", hash = "sha256:88f3483e2ea3595e02db48741af6de8e545dd574146453295d66a5a6a06d44da", size = 103391, upload-time = "2026-08-14T17:03:16.59Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/9f/84892f5e2bc4d4d44a2823d1fd5dc1f72fe91e9921c419631d4fd56df71c/vercel_workflow-0.9.0-py3-none-any.whl", hash = "sha256:9da5ee97f0460e87ec185b107166c66e57d93e11d82e6953a22ed994888e93af", size = 112718, upload-time = "2026-08-14T17:03:15.258Z" }, -] - -[[package]] -name = "websockets" -version = "16.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/e7/d1671fb984f9dd844e1da5288070c7c23c9eaba3082d3871aae19c3ab8b9/websockets-16.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d", size = 179570, upload-time = "2026-07-17T22:48:24.032Z" }, - { url = "https://files.pythonhosted.org/packages/99/f5/70df723bf571f5e0b1b845e0a4ff1c966eeb84f667599fc251caa37d15a3/websockets-16.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731", size = 177252, upload-time = "2026-07-17T22:48:25.775Z" }, - { url = "https://files.pythonhosted.org/packages/90/72/2f14b2e167170b8bf1c8bb7f9b0d78000f470d41a2085a91f33e3917b6c9/websockets-16.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4", size = 177530, upload-time = "2026-07-17T22:48:27.337Z" }, - { url = "https://files.pythonhosted.org/packages/f3/18/a17e2f0cde02dc10154c808deed7e1d8528afff93612f70d3f0a5b19b011/websockets-16.1.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb", size = 186038, upload-time = "2026-07-17T22:48:28.756Z" }, - { url = "https://files.pythonhosted.org/packages/d5/b0/41de283899cf5929d637b72a508cdbc9aa40dc0f317c6b77613fd1000488/websockets-16.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838", size = 187278, upload-time = "2026-07-17T22:48:30.328Z" }, - { url = "https://files.pythonhosted.org/packages/50/61/874aab5257e027f9f61b5004cec65e592babca7942b1bc09f38e72b7f1fd/websockets-16.1.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87", size = 189936, upload-time = "2026-07-17T22:48:31.896Z" }, - { url = "https://files.pythonhosted.org/packages/a6/1a/42173913ac5519607220849ed417c864d77384e4119f06dbba964a50f096/websockets-16.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3", size = 187796, upload-time = "2026-07-17T22:48:33.344Z" }, - { url = "https://files.pythonhosted.org/packages/1b/f4/37c1840bd89b529479aec41470b97b7c683b107ca90b6399ac5afb99dedf/websockets-16.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4", size = 186481, upload-time = "2026-07-17T22:48:34.843Z" }, - { url = "https://files.pythonhosted.org/packages/9e/70/652d9b964adcfbeb056f42e0ca6bece34d108fe75534e74df20643cae199/websockets-16.1.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3", size = 184351, upload-time = "2026-07-17T22:48:36.307Z" }, - { url = "https://files.pythonhosted.org/packages/13/f1/af3850e5d48d482921985be72ebcb169c6180b3a77b57bd612deebcee23b/websockets-16.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b", size = 186791, upload-time = "2026-07-17T22:48:37.762Z" }, - { url = "https://files.pythonhosted.org/packages/1d/40/1a4e3ed4969ec378dcad337e5f1472c5e292cb3e733bc392f0dc2e230abd/websockets-16.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d", size = 185413, upload-time = "2026-07-17T22:48:39.127Z" }, - { url = "https://files.pythonhosted.org/packages/aa/3e/4e3fa1afe8f1a6a780434cd9ba8eb422632b044eff3dd73f6af67523c147/websockets-16.1.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d", size = 187178, upload-time = "2026-07-17T22:48:40.676Z" }, - { url = "https://files.pythonhosted.org/packages/71/ab/dd742766aa5dda7f349be0de49e4d565b84cf6f7f7fa02e07692f0f2bdd9/websockets-16.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165", size = 185051, upload-time = "2026-07-17T22:48:42.098Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f5/76438c6560f416f1c0a7f587679fb97cc6e99ed336011d43ce2002dd27c1/websockets-16.1.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc", size = 185846, upload-time = "2026-07-17T22:48:43.472Z" }, - { url = "https://files.pythonhosted.org/packages/62/12/5c0320f2127823d27b2d56d611d31b0b284ad4edcb41364d66bf4c92b537/websockets-16.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a", size = 186066, upload-time = "2026-07-17T22:48:44.884Z" }, - { url = "https://files.pythonhosted.org/packages/a2/97/875986b857b955c3f9dd192cb8a1af81254dfb2ea22cc9590f0a1e020b8b/websockets-16.1.1-cp310-cp310-win32.whl", hash = "sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9", size = 179940, upload-time = "2026-07-17T22:48:46.481Z" }, - { url = "https://files.pythonhosted.org/packages/54/82/1013a5fe7ddae8e102bc3b4b39db81d8d28fd02100a324ce6ede8cd832b1/websockets-16.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f", size = 180239, upload-time = "2026-07-17T22:48:48.043Z" }, - { url = "https://files.pythonhosted.org/packages/2b/03/47debfe28e9d6d354be5d777b67fd44c359b9eb299a5d103500bd7cc3e37/websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c", size = 179566, upload-time = "2026-07-17T22:48:49.596Z" }, - { url = "https://files.pythonhosted.org/packages/72/93/31efa1ed78c17e5cfc229fd449e3966e1b9cc15753204cd585cc8dd01f4a/websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a", size = 177250, upload-time = "2026-07-17T22:48:50.942Z" }, - { url = "https://files.pythonhosted.org/packages/01/4a/542378ab3972b0c1cf1df3df3eff9591cea0d30c58c3aa3c4ddbc244e787/websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22", size = 177528, upload-time = "2026-07-17T22:48:52.59Z" }, - { url = "https://files.pythonhosted.org/packages/33/d9/162321f63c7eed558e9e1798ed7a1e34a4f6dab51f35419e4ed7a4907979/websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2", size = 186859, upload-time = "2026-07-17T22:48:53.915Z" }, - { url = "https://files.pythonhosted.org/packages/de/09/87df740f7430ce564bd52402e9c9458d4d0459cc7d2ee29e530c8204851b/websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01", size = 188095, upload-time = "2026-07-17T22:48:55.384Z" }, - { url = "https://files.pythonhosted.org/packages/d2/12/3d2703af7cc095f3c81904c92208cc1ae79affbc67376944b50ee9301f73/websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0", size = 191385, upload-time = "2026-07-17T22:48:56.742Z" }, - { url = "https://files.pythonhosted.org/packages/1d/69/986aa0234a964a00f5149cfc46e136e96c8faad1c783474550f40d31aef4/websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29", size = 188653, upload-time = "2026-07-17T22:48:58.134Z" }, - { url = "https://files.pythonhosted.org/packages/35/6b/10f9d03e3970a69ba67bd3b46b87a929b586d0300fadbfe14f57c1f85490/websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512", size = 187426, upload-time = "2026-07-17T22:48:59.515Z" }, - { url = "https://files.pythonhosted.org/packages/56/db/bb3aad62bf63d8bb3f0634b2eabffcfb3677a34bd19492110ff6869cf703/websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3", size = 184882, upload-time = "2026-07-17T22:49:00.916Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4c/c09a2ea9bfbeccce52fdc383e5f28af4bc8843338aabac28c81489af6120/websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57", size = 187584, upload-time = "2026-07-17T22:49:02.283Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8b/31bb4eb4d9eaacf1fdd39d115772a8aeaedfc19b5dc262e57ffbc8a9d42c/websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3", size = 186174, upload-time = "2026-07-17T22:49:03.973Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e4/dc02d725610a1ad49e193ef91a548194d71bdc6cdf27da83067dd1f73995/websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648", size = 187986, upload-time = "2026-07-17T22:49:05.553Z" }, - { url = "https://files.pythonhosted.org/packages/e0/73/30ed84c8bfd14c73d4af29d5ed9323c3073b48e0b7b23b67070f4e7fd59b/websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d", size = 185565, upload-time = "2026-07-17T22:49:06.959Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d3/4be8d4959f51e31b4f8fc0ece12b45bd3b6c0d15ea23b9990d9c11fc805f/websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be", size = 186598, upload-time = "2026-07-17T22:49:08.293Z" }, - { url = "https://files.pythonhosted.org/packages/26/fa/abb38597a52d84ed9cfacadc7a0c6f2db282c0ab23cdf72b58a666a21227/websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81", size = 186834, upload-time = "2026-07-17T22:49:09.766Z" }, - { url = "https://files.pythonhosted.org/packages/59/80/1119ad08a228b90c4eb77fbe48df7836731a605f5f881ba701ca826a4a65/websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57", size = 179940, upload-time = "2026-07-17T22:49:11.196Z" }, - { url = "https://files.pythonhosted.org/packages/71/b2/e511c1c6f64a95c2f3fc54bffda0e14eaa7e9442be605c29270f7589b918/websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a", size = 180239, upload-time = "2026-07-17T22:49:12.519Z" }, - { url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" }, - { url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" }, - { url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" }, - { url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" }, - { url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" }, - { url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" }, - { url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" }, - { url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" }, - { url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" }, - { url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" }, - { url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" }, - { url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" }, - { url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" }, - { url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" }, - { url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" }, - { url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" }, - { url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" }, - { url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" }, - { url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" }, - { url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" }, - { url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" }, - { url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" }, - { url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" }, - { url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" }, - { url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" }, - { url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" }, - { url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" }, - { url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" }, - { url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" }, - { url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" }, - { url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" }, - { url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" }, - { url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" }, - { url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" }, - { url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" }, - { url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" }, - { url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" }, - { url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" }, - { url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" }, - { url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" }, - { url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" }, - { url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" }, - { url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" }, - { url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" }, - { url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" }, - { url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" }, - { url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" }, - { url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" }, - { url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" }, - { url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" }, - { url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" }, - { url = "https://files.pythonhosted.org/packages/e1/ed/71fea6e141590cafc40b14dc5943b0845606bee87bdb52a21b6a73eb4311/websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869", size = 177185, upload-time = "2026-07-17T22:50:56.665Z" }, - { url = "https://files.pythonhosted.org/packages/01/ec/00e7eeca200facf9266a83e4cbbf1bed0e67fba1d4d45031d3e5b3d81b5c/websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9", size = 177459, upload-time = "2026-07-17T22:50:58.197Z" }, - { url = "https://files.pythonhosted.org/packages/75/fd/5774c4b33f7c0d8f0c51809c8b3a93456c48e3543579262cfa64eb5f522e/websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e", size = 178294, upload-time = "2026-07-17T22:50:59.641Z" }, - { url = "https://files.pythonhosted.org/packages/37/c3/48e2c03d2bd79bb45948841c592d24156312dd5f58cdf8f549febe652fb6/websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d", size = 179190, upload-time = "2026-07-17T22:51:01.129Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3f/73e511ecf2496ceac57dd4ed8388efe2bcf0769338a2dbf242c8366ae87e/websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5", size = 180330, upload-time = "2026-07-17T22:51:02.603Z" }, - { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" }, -] [[package]] name = "workflow-example-python" @@ -826,11 +660,11 @@ version = "0.0.0" source = { virtual = "." } dependencies = [ { name = "uvicorn" }, - { name = "vercel" }, + { name = "vercel-workflow" }, ] [package.metadata] requires-dist = [ { name = "uvicorn", specifier = ">=0.30" }, - { name = "vercel", git = "https://github.com/vercel/vercel-py?subdirectory=src%2Fvercel&rev=4814d61fa2ac1074ea5d67aede8f159ba1daae9b" }, + { name = "vercel-workflow", git = "https://github.com/vercel/vercel-py?subdirectory=src%2Fvercel-workflow&branch=main" }, ] diff --git a/workbench/python/vercel.json b/workbench/python/vercel.json index 86d4782e1..c1574b4a5 100644 --- a/workbench/python/vercel.json +++ b/workbench/python/vercel.json @@ -7,10 +7,5 @@ "env": { "WORKFLOW_PUBLIC_MANIFEST": "1" }, - "builds": [ - { - "src": "pyproject.toml", - "use": "@vercel/python@6.55.1" - } - ] + "framework": "python" } diff --git a/workbench/python/workflows/99_e2e.py b/workbench/python/workflows/99_e2e.py index 3cd1e0d51..745a9fde1 100644 --- a/workbench/python/workflows/99_e2e.py +++ b/workbench/python/workflows/99_e2e.py @@ -23,20 +23,36 @@ that list is skipped by the suite. Only add a name there once its test passes. """ import asyncio +import dataclasses +import json import random +import re import time +from typing import Any, Awaitable, TypeVar -from vercel.workflow import Workflows, sleep, time_ns +import pydantic -# `as_vercel_job=False` because `app.py` wires the queue entrypoints itself: the -# default constructor creates them and discards the HTTP handlers, and calling -# the entrypoints again to recover a reference would register a second -# subscriber on the same topic and consumer group. -app = Workflows(as_vercel_job=False) +from vercel.workflow import ( + BaseHook, + FatalError, + RetryableError, + Run, + WorkflowWritable, + Workflows, + get_step_metadata, + get_writable, + set_attributes, + sleep, + start, + time_ns, +) +from vercel.workflow._internal.core import Step + +app = Workflows() ########################################################## -# nullByteWorkflow — 99_e2e.ts:291 +# nullByteWorkflow # # A NUL byte surviving the step return is a real cross-language signal: it has # to round-trip the devalue codec and whatever the world writes to disk without @@ -54,7 +70,7 @@ async def nullByteWorkflow() -> str: ########################################################## -# addTenWorkflow — 99_e2e.ts:28 +# addTenWorkflow # # The suite starts this one with a positional input array (`[123]`), and the # step takes two positional parameters. Both directions of the argument @@ -75,7 +91,7 @@ async def addTenWorkflow(input: int) -> int: ########################################################## -# promiseAllWorkflow — 99_e2e.ts:44 +# promiseAllWorkflow # # `asyncio.gather` is Python's `Promise.all`, and the interesting part is the # same in both: three steps suspend in a single turn, so the orchestrator has to @@ -99,14 +115,15 @@ async def promiseAllWorkflow() -> str: ########################################################## -# sleepingWorkflow — 99_e2e.ts:201 -# parallelSleepWorkflow — 99_e2e.ts:209 -# sleepInLoopWorkflow — 99_e2e.ts:3045 +# sleepingWorkflow +# parallelSleepWorkflow +# sleepInLoopWorkflow # # `sleep` is the one workflow primitive with no step behind it: the orchestrator # suspends on a `wait_created` event and the world redelivers the run when the # timer fires. Porting it checks that Python emits a wait the TypeScript driver # recognises — `cancelRun` waits for exactly that event type before cancelling. +# TypeScript fixture inputs are milliseconds; numeric Python durations are seconds. # # The clock needs care. The tests do arithmetic on the returned timestamps and # compare against millisecond thresholds, so these return `time_ns() // 1e6` @@ -117,7 +134,7 @@ async def promiseAllWorkflow() -> str: @app.workflow async def sleepingWorkflow(durationMs: int = 10_000) -> dict: startTime = time_ns() // 1_000_000 - await sleep(durationMs) + await sleep(durationMs / 1_000) endTime = time_ns() // 1_000_000 return {"startTime": startTime, "endTime": endTime} @@ -148,6 +165,1171 @@ async def sleepInLoopWorkflow() -> dict: result = await noopStep(i) timestamps.append(result["ts"]) if i < iterations - 1: - await sleep(sleepMs) + await sleep(sleepMs / 1_000) return {"timestamps": timestamps, "totalElapsed": timestamps[-1] - timestamps[0]} + + +########################################################## +# Racing suspensions +# +# `Promise.race` and `Promise.any` have no asyncio spelling that takes bare +# awaitables, so the five race fixtures below share these two helpers. Both +# resolve ties by the order the awaitables were passed rather than by set +# iteration order — `asyncio.wait` returns a `set`, and a workflow body has to +# be deterministic across replays, so picking `next(iter(done))` would be a +# latent replay divergence the moment two steps land in the same turn. +# +# Nothing cancels the losers, matching JS: a race that resolves leaves the +# other steps running, the body returns, and `_run_in_loop` cancels the +# orphaned tasks on its way out. Their step invocations are already in flight +# and complete against a run that has finished, exactly as they do on the +# TypeScript side. + +_T = TypeVar("_T") + + +async def _race(*awaitables: Awaitable[_T]) -> _T: + """`Promise.race`: settle with the first to settle, error included.""" + tasks = [asyncio.ensure_future(a) for a in awaitables] + done, _pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) + for task in tasks: + if task in done: + return task.result() + raise AssertionError("asyncio.wait returned no completed task") + + +async def _any(*awaitables: Awaitable[_T]) -> _T: + """`Promise.any`: the first to *succeed*; failures are skipped.""" + tasks = [asyncio.ensure_future(a) for a in awaitables] + pending = set(tasks) + while pending: + done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED) + for task in tasks: + if task in done and task.exception() is None: + return task.result() + raise RuntimeError("all awaitables rejected") + + +########################################################## +# promiseRaceWorkflow +# promiseAnyWorkflow +# +# The counterpart to `promiseAllWorkflow`: three steps suspend in one turn, but +# the body resumes on the first one instead of all three. That is the case +# where replay and completion overlap — the run finishes while two step +# invocations are still outstanding. +# +# `promiseAnyWorkflow` additionally needs a step failure to be *skippable* +# rather than fatal. Python surfaces a failed step to the body as a +# `RuntimeError` carrying the step's error text (see the note on +# `errorRetryDisabled` below), which is all `_any` needs — it only asks whether +# the task raised. + + +@app.step +async def specificDelay(delay: int, v: str) -> str: + await asyncio.sleep(delay / 1000) + return v.upper() + + +@app.workflow +async def promiseRaceWorkflow() -> str: + return await _race( + specificDelay(10_000, "a"), + specificDelay(100, "b"), # "b" should always win + specificDelay(20_000, "c"), + ) + + +@app.step +async def stepThatFails() -> str: + raise FatalError("step failed") + + +@app.workflow +async def promiseAnyWorkflow() -> str: + return await _any( + stepThatFails(), + specificDelay(100, "b"), # "b" should always win + specificDelay(6_000, "c"), + ) + + +########################################################## +# sleepWinsRaceWorkflow +# stepWinsRaceWorkflow +# +# A race between the two suspension kinds. `sleep` resumes from a timer the +# world owns and a step resumes from an event the step handler writes, so these +# assert that the orchestrator resolves whichever lands first without waiting +# for the other — the driver bounds `durationMs` at 5s against a 10s loser. + + +@app.step +async def delayMsStep(ms: int, label: str) -> str: + await asyncio.sleep(ms / 1000) + return label + + +async def _sleepThen(duration: str, label: str) -> str: + """`sleep(...).then(() => label)` — a plain coroutine, not a step.""" + await sleep(duration) + return label + + +@app.workflow +async def sleepWinsRaceWorkflow() -> dict: + startTime = time_ns() // 1_000_000 + winner = await _race(delayMsStep(10_000, "step"), _sleepThen("1s", "sleep")) + endTime = time_ns() // 1_000_000 + return {"winner": winner, "durationMs": endTime - startTime} + + +@app.workflow +async def stepWinsRaceWorkflow() -> dict: + startTime = time_ns() // 1_000_000 + winner = await _race(delayMsStep(1_000, "step"), _sleepThen("10s", "sleep")) + endTime = time_ns() // 1_000_000 + return {"winner": winner, "durationMs": endTime - startTime} + + +########################################################## +# promiseRaceStressTestWorkflow +# +# Five steps 5s apart, raced and retired one at a time, so the body re-enters +# `_race` over a shrinking set across five separate replays. Each replay has to +# rebuild the same suspensions in the same order and match them to the events +# already in the log; a single misaligned slot shows up as a missing or +# duplicated entry in the returned list. + + +@app.step +async def promiseRaceStressTestDelayStep(dur: int, resp: int) -> int: + await asyncio.sleep(dur / 1000) + return resp + + +@app.workflow +async def promiseRaceStressTestWorkflow() -> list: + # Tasks rather than coroutines: unlike a JS promise, a coroutine can only + # be awaited once, and every loop iteration races the survivors again. + promises = { + i: asyncio.ensure_future(promiseRaceStressTestDelayStep(1000 * 5 * i, i)) + for i in range(5) + } + done = [] + + while promises: + res = await _race(*promises.values()) + done.append(res) + del promises[res] + + return done + + +########################################################## +# errorRetrySuccess +# errorRetryDisabled +# +# The two halves of the step retry policy: the default (`DEFAULT_MAX_RETRIES`, +# 3 in both SDKs) and `max_retries=0`. `get_step_metadata().attempt` is what +# makes them observable from inside the step, and it is the same 1-based +# counter the driver reads back off the step entity through the CLI. +# +# These two work because neither inspects the exception. `errorRetryDisabled` +# reads its attempt number back out of the message *text*, which is the one part +# of a thrown error that survives the event log — see the note on +# `errorFatalCatchable` below for what does not. + + +@app.step +async def retryUntilAttempt3() -> int: + attempt = get_step_metadata().attempt + if attempt < 3: + raise RuntimeError(f"Failed on attempt {attempt}") + return attempt + + +@app.workflow +async def errorRetrySuccess() -> dict: + return {"finalAttempt": await retryUntilAttempt3()} + + +@app.step(max_retries=0) +async def throwWithNoRetries() -> None: + raise RuntimeError(f"Failed on attempt {get_step_metadata().attempt}") + + +@app.workflow +async def errorRetryDisabled() -> dict: + try: + await throwWithNoRetries() + return {"failed": False, "attempt": None} + except Exception as e: + match = re.search(r"attempt (\d+)", str(e)) + return {"failed": True, "attempt": int(match.group(1)) if match else None} + + +########################################################## +# outputStreamWorkflow +# outputStreamInsideStepWorkflow +# utf8StreamWorkflow +# +# Run-scoped streams, which are the one part of the protocol that does not +# travel through the event log: chunks are appended to a separate per-run log +# that a reader tails live. The driver reads them with `run.getReadable()`, so +# these fixtures assert that Python's framing and payload encoding are the ones +# `@workflow/core`'s `getDeserializeStream` expects. +# +# Both spellings of the same API are covered, because they take different paths +# through the SDK. `get_writable()` in the *workflow body* returns a +# `WorkflowStreamHandle` — the body replays and has no network, so it cannot +# write — and passing that handle into a step's arguments is what turns it into +# a writer, via the serialization layer. `get_writable()` in a *step* returns +# the writer directly. The two have to name the same stream for the workflow to +# be able to hand one to a step at all. +# +# A `bytes` chunk arrives on the TypeScript side as a `Uint8Array` and anything +# else as its devalue value, which is why the driver reads chunk 0 as binary +# and chunk 1 as an object. +# +# Nothing closes a stream implicitly in either SDK, and the driver asserts the +# reader sees `done` — hence the explicit `stepCloseOutputStream` at the end of +# each fixture. + + +@app.step +async def stepWithOutputStreamBinary(writable: WorkflowWritable, text: str) -> None: + await writable.write(text.encode()) + + +@app.step +async def stepWithOutputStreamObject(writable: WorkflowWritable, obj: Any) -> None: + await writable.write(obj) + + +@app.step +async def stepCloseOutputStream(writable: WorkflowWritable) -> None: + await writable.close() + + +@app.workflow +async def outputStreamWorkflow() -> str: + writable = get_writable() + namedWritable = get_writable(namespace="test") + await sleep("1s") + await stepWithOutputStreamBinary(writable, "Hello, world!") + await sleep("1s") + await stepWithOutputStreamBinary(namedWritable, "Hello, named stream!") + await sleep("1s") + await stepWithOutputStreamObject(writable, {"foo": "test"}) + await sleep("1s") + await stepWithOutputStreamObject(namedWritable, {"foo": "bar"}) + await sleep("1s") + await stepCloseOutputStream(writable) + await stepCloseOutputStream(namedWritable) + return "done" + + +@app.step +async def stepWithOutputStreamInsideStep(text: str) -> None: + await get_writable().write(text.encode()) + + +@app.step +async def stepWithNamedOutputStreamInsideStep(namespace: str, obj: Any) -> None: + await get_writable(namespace=namespace).write(obj) + + +@app.step +async def stepCloseOutputStreamInsideStep(namespace: str | None = None) -> None: + await get_writable(namespace=namespace).close() + + +@app.workflow +async def outputStreamInsideStepWorkflow() -> str: + await sleep("1s") + await stepWithOutputStreamInsideStep("Hello from step!") + await sleep("1s") + await stepWithNamedOutputStreamInsideStep( + "step-ns", {"message": "Hello from named stream in step!"} + ) + await sleep("1s") + await stepWithOutputStreamInsideStep("Second message") + await sleep("1s") + await stepWithNamedOutputStreamInsideStep("step-ns", {"counter": 42}) + await sleep("1s") + await stepCloseOutputStreamInsideStep() + await stepCloseOutputStreamInsideStep("step-ns") + return "done" + + +@app.step +async def stepWriteUtf8Text(writable: WorkflowWritable, text: str) -> None: + await writable.write(text.encode()) + + +@app.step +async def stepWriteUtf8Json(writable: WorkflowWritable, value: Any) -> None: + await writable.write(json.dumps(value, ensure_ascii=False).encode()) + + +@app.workflow +async def utf8StreamWorkflow() -> str: + writable = get_writable() + await sleep("1s") + await stepWriteUtf8Text(writable, "Hello, world!") + await stepWriteUtf8Text(writable, "Café — naïve résumé") + await stepWriteUtf8Text(writable, "你好,世界!🌍✨") + await stepWriteUtf8Text(writable, "مرحبا بالعالم") + await stepWriteUtf8Json(writable, {"greeting": "안녕하세요", "emoji": "🎉"}) + await stepCloseOutputStream(writable) + return "done" + + +########################################################## +# errorRetryFatal +# errorFatalCatchable +# errorStepThrowFatalRoundTrip +# errorWorkflowThrowFatalRoundTrip +# +# `FatalError` is a retry-control error the step handler honours: `fatal or +# attempt >= max_retries + 1` is what decides whether to write `step_failed` +# instead of `step_retrying`, so a step that raises it burns exactly one attempt. +# +# All four turn on the error surviving the event log, which vercel-py now does +# the way upstream does — the thrown value goes through the serialization +# pipeline onto `step_failed` / `run_failed`, on the same devalue tags +# `@workflow/core` uses. So the `except` below catches the `FatalError` the step +# raised, `__cause__` is the `TypeError` it was raised from, and the run carries +# `errorCode: USER_ERROR` rather than the name of whichever class reached the +# handler. +# +# Two notes on the Python spelling of the round-trip fixture. `isFatal` and +# `isInstanceOf` are one check here: TypeScript distinguishes `FatalError.is()` +# (a name check, for errors from another realm) from `instanceof`, and Python's +# sandbox shares the host's class object so there is nothing to distinguish. And +# the cause is attached with `raise ... from ...`, which is `__cause__` — the +# explicit attribution JavaScript's `cause` also means. + + +@app.step +async def throwFatalError() -> None: + raise FatalError("Fatal step error") + + +@app.workflow +async def errorRetryFatal() -> str: + await throwFatalError() + return "never reached" + + +@app.workflow +async def errorFatalCatchable() -> dict: + try: + await throwFatalError() + return {"caught": False, "isFatal": False} + except Exception as e: + return {"caught": True, "isFatal": isinstance(e, FatalError)} + + +@app.step +async def throwFatalErrorWithCause() -> None: + raise FatalError("fatal with cause") from TypeError("underlying type error") + + +@app.workflow +async def errorStepThrowFatalRoundTrip() -> dict: + try: + await throwFatalErrorWithCause() + return {"caught": False} + except Exception as e: + cause = e.__cause__ + return { + "caught": True, + "isFatal": isinstance(e, FatalError), + "isInstanceOf": isinstance(e, FatalError), + "message": str(e), + "name": type(e).__name__, + "hasFatalProp": getattr(e, "fatal", None) is True, + "causeIsTypeError": isinstance(cause, TypeError), + "causeName": type(cause).__name__ if cause is not None else None, + "causeMessage": str(cause) if cause is not None else None, + } + + +@app.workflow +async def errorWorkflowThrowFatalRoundTrip() -> str: + # `ValueError` where the TypeScript fixture throws a `RangeError`: they are + # the pair on the wire, so a JavaScript reader of this run's `run_failed` + # gets a real `RangeError` back out of the cause. + raise FatalError("workflow exploded") from ValueError("out of bounds") + + +########################################################## +# metadataFromHelperWorkflow +# +# Upstream's #1577 regression test: the metadata accessors have to work from a +# helper defined at module level rather than inline in the step body. The +# mechanism differs — `AsyncLocalStorage` there, a `contextvars.ContextVar` +# here — but the failure mode it guards against is the same one, a context +# that only propagates as far as the decorated function. +# +# Only the step half is checked. `getStepMetadata()`'s counterpart +# `getWorkflowMetadata()` has no Python equivalent, so `workflowRunId` comes +# off `StepInfo.run_id`, which is the same run id the TypeScript fixture reads +# out of the workflow metadata. That also makes `workflowAndStepMetadataWorkflow` +# — which asserts the two metadata objects against each other — unportable for +# now, so it is not in this file. + + +async def _withStrictMetadataCheck(fn): + stepMetadata = get_step_metadata() + return await fn(), stepMetadata + + +@app.step +async def metadataHelperStep(label: str) -> dict: + async def _produce() -> str: + return label + + _result, stepMetadata = await _withStrictMetadataCheck(_produce) + + return { + "label": label, + "workflowRunId": stepMetadata.run_id, + "stepId": stepMetadata.step_id, + "attempt": stepMetadata.attempt, + } + + +@app.workflow +async def metadataFromHelperWorkflow(label: str) -> dict: + return await metadataHelperStep(label) + + +########################################################## +# spawnWorkflowFromStepWorkflow +# +# A run that starts another run. `start()` is a world write, so it can only +# happen in a step — the workflow body replays and its sandbox has no network, +# which is the same restriction the TypeScript fixture states in a comment. +# Waiting for the child is a step for the same reason. +# +# `Run(run_id).return_value()` polls the child's status; the TypeScript +# `getRun(runId).returnValue` is the same shape. Both hold the parent's step +# open for as long as the child takes, which is the caveat the TS fixture's +# `fibonacciWorkflow` neighbour documents at length — worth remembering before +# porting that one, since its recursion needs the worker pool to be deep enough +# for every waiting parent. + + +@app.step +async def doubleValue(value: int) -> int: + return value * 2 + + +@app.workflow +async def childWorkflow(value: int) -> dict: + return {"childResult": await doubleValue(value), "originalValue": value} + + +@app.step +async def spawnChildWorkflow(value: int) -> str: + childRun = await start(childWorkflow, value) + return childRun.run_id + + +@app.step +async def awaitWorkflowResult(runId: str) -> Any: + return await Run(runId).return_value() + + +@app.workflow +async def spawnWorkflowFromStepWorkflow(inputValue: int) -> dict: + childRunId = await spawnChildWorkflow(inputValue) + childResult = await awaitWorkflowResult(childRunId) + return { + "parentInput": inputValue, + "childRunId": childRunId, + "childResult": childResult, + } + + +########################################################## +# stepNotRegisteredCatchable +# stepNotRegisteredUncaught +# +# These fixtures deliberately construct Step wrappers without registering them +# with `app`. TypeScript reaches the same otherwise-impossible deployment shape +# through its internal WORKFLOW_USE_STEP symbol. The private constructor is +# confined to this conformance fixture; application code should use `@app.step`. + + +async def nonExistentStep() -> None: + raise AssertionError("an unregistered step body must never execute") + + +async def anotherNonExistentStep() -> None: + raise AssertionError("an unregistered step body must never execute") + + +_nonExistentStep = Step(nonExistentStep) +_anotherNonExistentStep = Step(anotherNonExistentStep) + + +@app.workflow +async def stepNotRegisteredCatchable() -> dict: + try: + await _nonExistentStep() + return {"caught": False, "error": None} + except Exception as error: + return {"caught": True, "error": str(error)} + + +@app.workflow +async def stepNotRegisteredUncaught() -> None: + await _anotherNonExistentStep() + + +########################################################## +# hookWithSleepWorkflow +# hookWithSleepFinalStepWorkflow +# hookTokenReuseLoopWorkflow +# sleepWithSequentialStepsWorkflow +# +# `HookEvent` implements both `__await__` (one payload) and `__aiter__` / +# `__anext__` (a stream of them), so `for await (const p of hook)` ports to +# `async for payload in hook` directly. `Hook.set_result` requires the +# class handed to `wait()` to be a dataclass or a pydantic model, and calls +# `hook_cls(**raw)` on the plain JSON the resumer sent — so the port is a +# dataclass with a default per optional field, and the fixture's structural +# type becomes a declared one. The declaration has to stay loose in the same +# places the TypeScript type is optional: the driver resumes with `{type, id}` +# on one payload and `{type, done}` on another, and a required field would +# raise on whichever call omitted it. +# +# Three translation details matter here: +# +# - **`using hook` is not `try/finally`.** A Python workflow body unwinds +# through a `_SuspendException` on *every* suspension, so a `finally` around +# an `await` runs once per turn rather than once at scope exit. Disposing a +# hook there deletes the suspension before the orchestrator can flush its +# `hook_created`, and the run stalls with no hook for the driver to resume. +# Dispose on the normal path only. +# - **`void sleep('1d')`** is `asyncio.ensure_future(sleep("1d"))`. The wait is +# created and never completes; the body returns first and the orphaned task +# is cancelled with the loop. +# - **A step takes the payload as a dict**, not as the dataclass: keeping the +# step signature `dict` avoids registering a serializer for a type that only +# exists to satisfy `set_result`. +# +# `sleepWithSequentialStepsWorkflow` is the cluster's control and has no hook in +# it at all — a fire-and-forget sleep plus three sequential steps. + + +@dataclasses.dataclass +class SleepHookPayload(BaseHook): + type: str + id: int | None = None + done: bool | None = None + + +@app.step +async def processPayload(payload: dict) -> dict: + return {"processed": True, "type": payload["type"], "id": payload.get("id")} + + +@app.workflow +async def hookWithSleepWorkflow(token: str) -> list: + hook = SleepHookPayload.wait(token=token) + + # Concurrent sleep that won't complete during the test + asyncio.ensure_future(sleep("1d")) + + results = [] + async for payload in hook: + results.append(await processPayload(dataclasses.asdict(payload))) + if payload.done: + break + + hook.dispose() + return results + + +@app.workflow +async def hookWithSleepFinalStepWorkflow(token: str) -> dict: + hook = SleepHookPayload.wait(token=token) + asyncio.ensure_future(sleep("1d")) + + seen = [] + finalResult = None + async for payload in hook: + if payload.id is not None: + seen.append(payload.id) + if payload.done: + finalResult = await processPayload(dataclasses.asdict(payload)) + break + + hook.dispose() + return {"seen": seen, "finalResult": finalResult} + + +@dataclasses.dataclass +class ReuseHookPayload(BaseHook): + message: str + + +@app.workflow +async def hookTokenReuseLoopWorkflow(token: str, rounds: int) -> dict: + received = [] + for round_index in range(rounds): + hook = ReuseHookPayload.wait(token=token) + + conflict = await hook.get_conflict() + if conflict is not None: + return {"received": received, "conflictRound": round_index} + + payload = await hook + received.append(payload.message) + hook.dispose() + + return {"received": received, "conflictRound": None} + + +@app.step +async def addNumbers(a: int, b: int) -> int: + return a + b + + +@app.workflow +async def sleepWithSequentialStepsWorkflow() -> dict: + shouldCancel = False + + async def _cancelAfterSleep() -> None: + nonlocal shouldCancel + await sleep("1d") + shouldCancel = True + + asyncio.ensure_future(_cancelAfterSleep()) + + a = await addNumbers(1, 2) + b = await addNumbers(a, 3) + c = await addNumbers(b, 4) + return {"a": a, "b": b, "c": c, "shouldCancel": shouldCancel} + + +########################################################## +# writableForwardedFromWorkflowWorkflow +# writableForwardedFromStepWorkflow +# +# A stream reference crossing a *run* boundary: the parent hands its writable to +# a child run as part of that run's input, and the driver then reads the bytes +# off the **parent's** stream. So the handle has to survive `start()`'s input +# serialization, arrive in another run's body, and still name the stream it came +# from rather than the child's own — which is why `WorkflowStreamHandle` carries +# a run id instead of deriving one from the ambient run. +# +# The two variants differ in where the parent's `get_writable()` is called, and +# they are not the same path through the SDK. Variant 1 calls it in the workflow +# body, so a *handle* is serialized into a step's arguments, revived there as a +# writer, and serialized again into the child's input. Variant 2 calls it inside +# the step that also calls `start()`, so what gets forwarded is the step-context +# writer directly. Both have to land on the same stream. +# +# `start()` lives in a step for the usual reason: it is a world write, and the +# workflow body replays with no network. The TypeScript fixture says the same +# thing in a comment, which is a good sign the restriction is protocol-shaped +# rather than Python-shaped. + + +@app.step +async def writeBytesToWritable(writable: WorkflowWritable, payload: str) -> None: + await writable.write(payload.encode()) + + +@app.workflow +async def writableForwardedChildWorkflow( + parentWritable: WorkflowWritable, payload: str +) -> str: + await writeBytesToWritable(parentWritable, payload) + return "child-done" + + +@app.step +async def startChildWithWorkflowWritable( + parentWritable: WorkflowWritable, payload: str +) -> str: + childRun = await start(writableForwardedChildWorkflow, parentWritable, payload) + # Let the child finish writing before the parent is allowed to close. + await childRun.return_value() + return childRun.run_id + + +@app.workflow +async def writableForwardedFromWorkflowWorkflow(payload: str) -> dict: + writable = get_writable() + childRunId = await startChildWithWorkflowWritable(writable, payload) + await stepCloseOutputStream(writable) + return {"childRunId": childRunId} + + +@app.step +async def startChildWithStepWritable(payload: str) -> str: + writable = get_writable() + childRun = await start(writableForwardedChildWorkflow, writable, payload) + await childRun.return_value() + await writable.close() + return childRun.run_id + + +@app.workflow +async def writableForwardedFromStepWorkflow(payload: str) -> dict: + return {"childRunId": await startChildWithStepWritable(payload)} + + +########################################################## +# retainedInterleavingWorkflow +# +# Every suspension kind this app can produce, in one body, with the exact +# composite result asserted — so a dropped, duplicated or misordered boundary +# fails loudly rather than shifting a number nobody checks. +# +# Upstream wrote it for VM retention (`WORKFLOW_RETAINED_VM`): primitive step +# arguments keep the retained VM, a non-primitive argument demotes the boundary +# to a cold replay. Python has no such VM, so `unwrapValue`'s object argument is +# just an object argument here. That does not make the fixture pointless on this +# side — what the test actually asserts is that nine values come back right +# across a step / gather / race / sleep / hook interleaving, and that is the +# same claim in any language. +# +# The hook is created with a token and no metadata, and it is awaited +# *concurrently with a step* — +# `asyncio.gather(hook, add(...))`, since `HookEvent` is awaitable. Dispose on +# the normal path only; see the hook cluster above for why `finally` would break +# it. + + +@app.step +async def unwrapValue(box: dict) -> int: + return box["value"] + + +@dataclasses.dataclass +class DeltaPayload(BaseHook): + delta: int + + +@app.workflow +async def retainedInterleavingWorkflow(token: str) -> dict: + a = await add(1, 2) + b = await unwrapValue({"value": a}) + c, d = await asyncio.gather(add(b, 10), add(b, 20)) + e, f = await asyncio.gather(unwrapValue({"value": c}), add(d, 1)) + winner = await _race(delayMsStep(100, "step"), _sleepThen("30s", "sleep")) + await sleep("1s") + + hook = DeltaPayload.wait(token=token) + payload, g = await asyncio.gather(hook, add(e + f, 100)) + h = await add(g, payload.delta) + hook.dispose() + + return { + "a": a, + "b": b, + "c": c, + "d": d, + "e": e, + "f": f, + "winner": winner, + "g": g, + "h": h, + } + + +########################################################## +# hookWorkflow +# hookCleanupTestWorkflow +# hookDisposeTestWorkflow +# +# The three fixtures that needed hook *metadata* and nothing else. Metadata is +# how a run tells its resumer what it is waiting for: attached once when the hook +# is registered, read back off the hook entity rather than out of a payload. The +# suite leans on it hard — `hookWorkflow`'s driver resumes with +# `customData: hook.metadata?.customData` and then asserts the workflow saw that +# exact value, so a missing metadata field does not weaken the test, it fails it. +# +# `hookWorkflow`'s payload is a **pydantic model** rather than a dataclass, and +# that is load-bearing rather than a style choice. The driver sends `done` only +# on the last payload, and the test asserts the first two come back with `done` +# *absent* — `undefined`, not `false` and not `null`. A dataclass materializes +# every optional field, so `dataclasses.asdict` would report `done: None` and the +# assertion would fail on the difference between "not sent" and "sent as null". +# `model_dump(exclude_unset=True)` reproduces what the resumer actually sent, +# which is the property the test is really about. The other two fixtures have no +# optional fields and stay dataclasses. +# +# `using hook` becomes an explicit `dispose()` on the normal path — never a +# `finally`; see the hook cluster above for why. Where that dispose lands matters +# only in `hookDisposeTestWorkflow`, and there it is the whole point: it releases +# the token *before* the 5s sleep, so another run can claim it while this one is +# still going. In the other two the run completes right after, which frees the +# token anyway, so the call is a formality kept for symmetry with the fixture. + + +class HookPayload(BaseHook, pydantic.BaseModel): + message: str + customData: str + done: bool | None = None + + +@app.workflow +async def hookWorkflow(token: str, customData: str) -> list: + hook = HookPayload.wait(token=token, metadata={"customData": customData}) + + payloads = [] + async for payload in hook: + payloads.append(payload.model_dump(exclude_unset=True)) + if payload.done: + break + + hook.dispose() + return payloads + + +@dataclasses.dataclass +class MessagePayload(BaseHook): + message: str + customData: str + + +@app.workflow +async def hookCleanupTestWorkflow(token: str, customData: str) -> dict: + hook = MessagePayload.wait(token=token, metadata={"customData": customData}) + payload = await hook + hook.dispose() + return { + "message": payload.message, + "customData": payload.customData, + "hookCleanupTestData": "workflow_completed", + } + + +########################################################## +# hookGetConflict* and run-idempotency fixtures +# +# `get_conflict()` is a distinct suspension from awaiting a hook payload: it +# commits the hook registration, then resumes with either `None` or a `Run` for +# the current owner. Python's Run methods perform world I/O directly rather than +# becoming durable step proxies, so owner status/result lookups live in steps — +# the same split `spawnWorkflowFromStepWorkflow` uses above. + + +@dataclasses.dataclass +class ConflictPayload(BaseHook): + pass + + +@dataclasses.dataclass +class AdoptPayload(BaseHook): + value: str + + +@dataclasses.dataclass +class SignalPayload(BaseHook): + message: str + + +@app.step +async def hookGetConflictStep(customData: str) -> dict: + return { + "customData": customData, + "hookGetConflictStepData": "step_completed", + } + + +@app.step +async def hookGetConflictTimedStep(label: str, delayMs: int) -> dict: + startedAt = int(get_step_metadata().step_started_at.timestamp() * 1000) + await asyncio.sleep(delayMs / 1000) + return { + "label": label, + "startedAt": startedAt, + "endedAt": time.time_ns() // 1_000_000, + } + + +@app.step +async def getRunStatus(runId: str) -> str: + return await Run(runId).status() + + +@app.workflow +async def hookGetConflictWorkflow(token: str, customData: str) -> dict: + hook = ConflictPayload.wait(token=token, metadata={"customData": customData}) + conflict = await hook.get_conflict() + + if conflict is not None: + return { + "token": token, + "customData": customData, + "conflictRunId": conflict.run_id, + "conflictStatus": await getRunStatus(conflict.run_id), + "hookGetConflictTestData": "hook_token_conflict_detected", + } + + return { + "token": token, + "customData": customData, + "conflictRunId": None, + "hookGetConflictTestData": "hook_registered_without_payload", + } + + +@app.workflow +async def hookGetConflictWithPriorStepWorkflow(token: str, customData: str) -> dict: + hook = ConflictPayload.wait(token=token, metadata={"customData": customData}) + stepTask = asyncio.ensure_future(hookGetConflictStep(customData)) + conflict = await hook.get_conflict() + return { + "token": token, + "customData": customData, + "conflictRunId": None if conflict is None else conflict.run_id, + "stepResult": await stepTask, + "hookGetConflictTestData": "prior_step_completed_after_registration", + } + + +@app.workflow +async def hookGetConflictWithParallelStepWorkflow(token: str, customData: str) -> dict: + hook = ConflictPayload.wait(token=token, metadata={"customData": customData}) + stepResult, conflict = await asyncio.gather( + hookGetConflictStep(customData), hook.get_conflict() + ) + return { + "token": token, + "customData": customData, + "conflictRunId": None if conflict is None else conflict.run_id, + "stepResult": stepResult, + "hookGetConflictTestData": "parallel_step_completed_with_registration", + } + + +@app.workflow +async def hookGetConflictThenStepParallelWorkflow(token: str, customData: str) -> dict: + hook = ConflictPayload.wait(token=token, metadata={"customData": customData}) + + async def runStepB() -> dict: + await hook.get_conflict() + return await hookGetConflictTimedStep("B", 100) + + stepBTask = asyncio.ensure_future(runStepB()) + stepAResult = await hookGetConflictTimedStep("A", 10_000) + stepBResult = await stepBTask + return { + "token": token, + "customData": customData, + "stepAResult": stepAResult, + "stepBResult": stepBResult, + "hookGetConflictTestData": "registration_then_step_runs_in_parallel", + } + + +@app.workflow +async def hookClaimOnlyMutexWorkflow(token: str, holdMs: int) -> dict: + hook = ConflictPayload.wait(token=token) + conflict = await hook.get_conflict() + if conflict is not None: + return {"role": "duplicate", "conflictRunId": conflict.run_id} + + work = await hookGetConflictTimedStep("A", holdMs) + return {"role": "owner", "workEndedAt": work["endedAt"]} + + +@app.workflow +async def hookAdoptOwnerResultWorkflow(token: str, marker: str) -> dict: + hook = AdoptPayload.wait(token=token) + conflict = await hook.get_conflict() + if conflict is not None: + adopted = await awaitWorkflowResult(conflict.run_id) + return { + "role": "duplicate", + "conflictRunId": conflict.run_id, + "adopted": adopted, + } + + payload = await hook + return {"role": "owner", "marker": marker, "value": payload.value} + + +@app.step +async def forwardPayloadToOwner(token: str, message: str) -> None: + await SignalPayload(message=message).resume(token) + + +@app.workflow +async def hookSignalOwnerWorkflow(token: str, message: str) -> dict: + hook = SignalPayload.wait(token=token) + conflict = await hook.get_conflict() + if conflict is not None: + await forwardPayloadToOwner(token, message) + return {"role": "duplicate", "forwardedTo": conflict.run_id} + + payload = await hook + return {"role": "owner", "received": payload.message} + + +@app.workflow +async def hookDisposeTestWorkflow(token: str, customData: str) -> dict: + hook = MessagePayload.wait(token=token, metadata={"customData": customData}) + payload = await hook + message, customDataResult = payload.message, payload.customData + + # Releases the token here rather than at run completion, which is what lets + # the test's second run claim it while this one is still sleeping. + hook.dispose() + await sleep("5s") + + return { + "message": message, + "customData": customDataResult, + "disposed": True, + "hookDisposeTestData": "workflow_completed", + } + + +########################################################## +# errorRetryCustomDelay +# +# The third of the TypeScript retry block's fixtures, and the one that needed +# two things at once: `RetryableError(retry_after=…)` to steer the wait, and +# `StepInfo.step_started_at` to measure it. `step_started_at` is when the +# *first* attempt began, so `now - step_started_at` on attempt 2 is how long the +# step has been going across the retry — which is what the test bounds at 10s. +# +# Wall clock rather than the workflow clock, because this runs in a step: the +# deterministic clock is a replay construct and would report the same instant on +# both attempts. + + +@app.step +async def throwRetryableError() -> dict: + metadata = get_step_metadata() + if metadata.attempt == 1: + raise RetryableError("Retryable error", retry_after="10s") + startedAt = int(metadata.step_started_at.timestamp() * 1000) + return { + "attempt": metadata.attempt, + "duration": time.time_ns() // 1_000_000 - startedAt, + } + + +@app.workflow +async def errorRetryCustomDelay() -> dict: + return await throwRetryableError() + + +########################################################## +# setAttributesWorkflow +# setAttributesInsideStepWorkflow +# setAttributesFireAndForgetWorkflow +# setAttributesParallelWorkflow +# setAttributesThrowsAfterWorkflow +# setAttributesValidationWorkflow +# +# Plaintext key/value metadata on the run, and the whole `setAttributes` block in +# one go now that `set_attributes()` exists. Six fixtures covering the axes the +# implementation can get wrong independently: from the workflow body, from a step +# body, unawaited, concurrently over disjoint keys, on a run that then fails, and +# with every input the validator is supposed to reject. +# +# Two Python-shaped details, neither of them a workaround: +# +# - **`undefined` is `None`.** `setAttributes({ source: undefined })` removes the +# key; the Python spelling is `{"source": None}`, which is what the SDK's +# `Mapping[str, str | None]` signature already says. +# - **Fire-and-forget is a scheduled task.** `set_attributes()` is an async +# function, so Python has to schedule the coroutine to start it without +# awaiting its completion. The next workflow suspension gives those tasks a +# turn to register their writes, which is the `void setAttributes(...)` shape +# this fixture exercises. + + +@app.workflow +async def setAttributesWorkflow(input: int) -> int: + await set_attributes({"phase": "init", "source": "workflow-body"}) + tripled = input * 3 + await set_attributes({"phase": "done"}) + # `None` removes the key, the way `undefined` does on the TypeScript side. + await set_attributes({"source": None}) + return tripled + + +@app.step +async def setAttributesFromStep(input: int) -> int: + await set_attributes( + {"phase": "step-started", "source": "step-body", "input": str(input)} + ) + await set_attributes({"phase": "step-done"}) + return input * 4 + + +@app.workflow +async def setAttributesInsideStepWorkflow(input: int) -> int: + return await setAttributesFromStep(input) + + +@app.workflow +async def setAttributesFireAndForgetWorkflow() -> str: + # Deliberately scheduled and not awaited: the write starts now and lands at + # the next suspension, matching `void setAttributes(...)` in TypeScript. + asyncio.create_task(set_attributes({"phase": "init", "mode": "fire-and-forget"})) + await sleep("100ms") + asyncio.create_task(set_attributes({"phase": "mid"})) + await sleep("100ms") + # This final scheduled write is the unsupported edge: the workflow body + # returns before the coroutine starts, so the runtime has nothing to drain. + asyncio.create_task(set_attributes({"phase": "done"})) + return "completed" + + +@app.workflow +async def setAttributesParallelWorkflow() -> str: + await asyncio.gather( + set_attributes({"a": "1"}), + set_attributes({"b": "2"}), + set_attributes({"c": "3"}), + ) + return "done" + + +@app.workflow +async def setAttributesThrowsAfterWorkflow() -> None: + await set_attributes({"phase": "about-to-fail", "reason": "intentional"}) + raise FatalError("intentional failure to test attribute persistence") + + +@app.workflow +async def setAttributesValidationWorkflow() -> dict: + outcomes = {} + + async def attempt(label: str, attrs) -> None: + try: + await set_attributes(attrs) + outcomes[label] = "no-error" + except Exception as e: + outcomes[label] = f"{type(e).__name__}: {e}" + + await attempt("reserved", {"$system": "nope"}) + await attempt("emptyKey", {"": "v"}) + await attempt("keyTooLong", {"k" * 257: "v"}) + await attempt("valueTooLong", {"note": "v" * 257}) + # The cap is bytes, not characters: 200 two-byte characters is 400 bytes. + await attempt("valueTooManyBytes", {"note": "é" * 200}) + await attempt("overCap", {f"k{i}": "v" for i in range(65)}) + await attempt("nonObject", "phase=init") + + # The run must remain healthy after every rejected call. + await set_attributes({"phase": "validated"}) + return outcomes