diff --git a/.changeset/lazy-terminal-run-data.md b/.changeset/lazy-terminal-run-data.md new file mode 100644 index 0000000000..65b7cded83 --- /dev/null +++ b/.changeset/lazy-terminal-run-data.md @@ -0,0 +1,5 @@ +--- +"@workflow/world": patch +--- + +Accept lazy completed and failed runs whose payload is represented by a remote reference. diff --git a/.changeset/python-cancellable-steps.md b/.changeset/python-cancellable-steps.md new file mode 100644 index 0000000000..f7eaece157 --- /dev/null +++ b/.changeset/python-cancellable-steps.md @@ -0,0 +1,4 @@ +--- +--- + +Expand Python conformance coverage for cancellable steps. diff --git a/.changeset/stream-read-key-prefetch.md b/.changeset/stream-read-key-prefetch.md new file mode 100644 index 0000000000..66b20fa841 --- /dev/null +++ b/.changeset/stream-read-key-prefetch.md @@ -0,0 +1,5 @@ +--- +'@workflow/core': patch +--- + +Prefetch run encryption keys when reading workflow streams. diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index 782186335e..ab2abe2b10 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 () => { @@ -3822,7 +3834,7 @@ describe.concurrent('e2e', () => { // AbortController / AbortSignal // ========================================================================== - describeJsOnly('AbortController', () => { + describe('AbortController', () => { test( 'abortTimeoutWorkflow: timeout cancels long-running step', { timeout: 60_000 }, @@ -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/packages/core/src/reconnecting-framed-stream.test.ts b/packages/core/src/reconnecting-framed-stream.test.ts index ce22485f7b..6b2805a77e 100644 --- a/packages/core/src/reconnecting-framed-stream.test.ts +++ b/packages/core/src/reconnecting-framed-stream.test.ts @@ -461,6 +461,164 @@ describe('createReconnectingFramedStream', () => { expect(chunks).toEqual([payloadFrame(1), payloadFrame(2), payloadFrame(3)]); }); + it('starts key resolution concurrently with the first stream GET', async () => { + let resolveStream: (stream: ReadableStream) => void; + const streamPromise = new Promise>((resolve) => { + resolveStream = resolve; + }); + let resolveKey: () => void; + const keyPromise = new Promise((resolve) => { + resolveKey = resolve; + }); + const get = vi.fn().mockReturnValue(streamPromise); + const prefetchKey = vi.fn().mockReturnValue(keyPromise); + setWorld({ + specVersion: SPEC_VERSION_CURRENT, + streams: { get }, + } as unknown as World); + + const read = readAll( + createReconnectingFramedStream(RUN_ID, 's', 0, prefetchKey) + ); + await vi.waitFor(() => { + expect(get).toHaveBeenCalledOnce(); + expect(prefetchKey).toHaveBeenCalledOnce(); + }); + + resolveKey?.(); + resolveStream?.( + scriptedStream([ + { kind: 'value', value: payloadFrame(7) }, + { kind: 'close' }, + ]) + ); + await expect(read).resolves.toEqual([payloadFrame(7)]); + }); + + it('finishes key resolution before the first raw frame', async () => { + let releaseFrame: () => void; + const frameReady = new Promise((resolve) => { + releaseFrame = resolve; + }); + const prefetchKey = vi.fn().mockResolvedValue(undefined); + const { world } = makeWorldWithScriptedStreams({ + 0: () => + new ReadableStream({ + async pull(controller) { + await frameReady; + controller.enqueue(payloadFrame(7)); + controller.close(); + }, + }), + }); + setWorld(world); + + const read = readAll( + createReconnectingFramedStream(RUN_ID, 's', 0, prefetchKey) + ); + await vi.waitFor(() => expect(prefetchKey).toHaveBeenCalledOnce()); + // The resolver has already settled by the time the raw frame is released. + await Promise.resolve(); + releaseFrame?.(); + await expect(read).resolves.toEqual([payloadFrame(7)]); + expect(prefetchKey).toHaveBeenCalledOnce(); + }); + + it('prefetches one key promise across reconnects', async () => { + const prefetchKey = vi.fn().mockResolvedValue(undefined); + const { world, calls } = makeWorldWithScriptedStreams({ + 0: () => + scriptedStream([ + { kind: 'value', value: payloadFrame(1) }, + { kind: 'error', err: new Error('connection reset') }, + ]), + 1: () => + scriptedStream([ + { kind: 'value', value: payloadFrame(2) }, + { kind: 'close' }, + ]), + }); + setWorld(world); + + await expect( + readAll(createReconnectingFramedStream(RUN_ID, 's', 0, prefetchKey)) + ).resolves.toEqual([payloadFrame(1), payloadFrame(2)]); + expect(calls).toEqual([0, 1]); + expect(prefetchKey).toHaveBeenCalledOnce(); + }); + + it('keeps a stream GET failure primary when its speculative key lookup also fails', async () => { + const streamError = new Error('stream connection failed'); + const keyError = new Error('key lookup failed'); + const unhandled = vi.fn(); + process.once('unhandledRejection', unhandled); + setWorld({ + specVersion: SPEC_VERSION_CURRENT, + streams: { get: vi.fn().mockRejectedValue(streamError) }, + } as unknown as World); + + await expect( + readAll( + createReconnectingFramedStream(RUN_ID, 's', -1, () => + Promise.reject(keyError) + ) + ) + ).rejects.toThrow('stream connection failed'); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(unhandled).not.toHaveBeenCalled(); + }); + + it('observes a rejected speculative key lookup after cancellation', async () => { + const keyError = new Error('key lookup failed'); + const unhandled = vi.fn(); + process.once('unhandledRejection', unhandled); + const stream = createReconnectingFramedStream(RUN_ID, 's', 0, () => + Promise.reject(keyError) + ); + const reader = stream.getReader(); + const pending = reader.read(); + await reader.cancel(); + await expect(pending).resolves.toMatchObject({ done: true }); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(unhandled).not.toHaveBeenCalled(); + }); + + it('cancels an acquired underlying reader while a prefetched key is pending', async () => { + let cancelCount = 0; + let resolveKey: () => void; + const prefetchKey = vi.fn().mockReturnValue( + new Promise((resolve) => { + resolveKey = resolve; + }) + ); + const source = new ReadableStream({ + pull() { + // Keep the first raw read pending until the consumer cancels. + }, + cancel() { + cancelCount++; + }, + }); + const get = vi.fn().mockResolvedValue(source); + setWorld({ + specVersion: SPEC_VERSION_CURRENT, + streams: { get }, + } as unknown as World); + + const reader = createReconnectingFramedStream( + RUN_ID, + 's', + 0, + prefetchKey + ).getReader(); + const pendingRead = reader.read(); + await vi.waitFor(() => expect(get).toHaveBeenCalledOnce()); + await reader.cancel(); + await expect(pendingRead).resolves.toMatchObject({ done: true }); + expect(cancelCount).toBe(1); + resolveKey?.(); + }); + it('threads runId through to streams.get', async () => { const getSpy = vi.fn( async (_runId: string, _name: string, _startIndex?: number) => diff --git a/packages/core/src/runtime/run.ts b/packages/core/src/runtime/run.ts index 08afacf859..2f116612fe 100644 --- a/packages/core/src/runtime/run.ts +++ b/packages/core/src/runtime/run.ts @@ -13,7 +13,7 @@ import { type PayloadKey, } from '../serialization/encryption.js'; import { - getExternalRevivers, + getRunReadableStream, hydrateRunError, hydrateWorkflowReturnValue, } from '../serialization.js'; @@ -224,9 +224,9 @@ export class Run { } /** - * Defer fetching the run and its encryption key until serialized stream data - * is actually read. An empty or metadata-only stream must not start an - * unobserved run lookup. + * Defers fetching the run and its encryption key until a readable is first + * consumed. The first pull prefetches it so an encrypted first frame can join + * the lookup; this also applies to an empty consumed stream. * @internal */ #getEncryptionKeyLazily(): () => Promise { @@ -370,15 +370,18 @@ export class Run { 'use step'; const { ops = [], global = globalThis, startIndex, namespace } = options; const name = getWorkflowRunStreamId(this.runId, namespace); - // The resolver starts only when the deserialize stream sees its first - // chunk, so creating or probing an empty stream cannot reject in the - // background. + // The resolver starts only on the readable's first pull, so construction + // is inert. A consumed empty stream still performs the speculative lookup + // to keep the first encrypted-frame path concurrent. const encryptionKey = this.#getEncryptionKeyLazily(); - const stream = getExternalRevivers(global, ops, this.runId, encryptionKey) - .ReadableStream!({ + const stream = getRunReadableStream( + global, + ops, + this.runId, name, startIndex, - }) as ReadableStream; + encryptionKey + ); const worldPromise = this.#lazyWorldPromise; const runId = this.runId; diff --git a/packages/core/src/runtime/runs.test.ts b/packages/core/src/runtime/runs.test.ts index 10b540f100..b57d13464f 100644 --- a/packages/core/src/runtime/runs.test.ts +++ b/packages/core/src/runtime/runs.test.ts @@ -25,10 +25,12 @@ vi.mock('../serialization.js', async (importActual) => { }); import { registerSerializationClass } from '../class-serialization.js'; +import { deriveRunPayloadKeys } from '../serialization/encryption.js'; import { dehydrateRunError, dehydrateStepReturnValue, dehydrateWorkflowReturnValue, + getSerializeStream, hydrateStepReturnValue, } from '../serialization.js'; import { getReturnValuePollIntervalMs, Run } from './run.js'; @@ -267,6 +269,21 @@ describe('Run.getReadable', () => { setWorld(undefined as unknown as World); }); + async function encryptedFrames(value: unknown, material: Uint8Array) { + const serialize = getSerializeStream( + {}, + await deriveRunPayloadKeys(material) + ); + const reader = serialize.readable.getReader(); + const read = reader.read(); + const writer = serialize.writable.getWriter(); + await writer.write(value); + await writer.close(); + const first = await read; + if (!first.value) throw new Error('Expected serialized frame'); + return first.value; + } + it('does not fetch the run encryption key for an empty stream', async () => { const world = createMockWorld(); world.getEncryptionKeyForRun = vi.fn().mockResolvedValue(undefined); @@ -284,9 +301,148 @@ describe('Run.getReadable', () => { new Run('wrun_123').getReadable(); await new Promise((resolve) => setTimeout(resolve, 0)); + expect(world.streams.get).not.toHaveBeenCalled(); expect(world.runs.get).not.toHaveBeenCalled(); expect(world.getEncryptionKeyForRun).not.toHaveBeenCalled(); }); + + it('prefetches the run key when a consumed stream is empty', async () => { + const world = createMockWorld(); + world.getEncryptionKeyForRun = vi.fn().mockResolvedValue(undefined); + world.streams = { + get: vi.fn().mockResolvedValue( + new ReadableStream({ + start(controller) { + controller.close(); + }, + }) + ), + } as unknown as World['streams']; + setWorld(world); + + await expect( + new Run('wrun_123').getReadable().getReader().read() + ).resolves.toMatchObject({ done: true }); + expect(world.streams.get).toHaveBeenCalledOnce(); + expect(world.runs.get).toHaveBeenCalledOnce(); + expect(world.getEncryptionKeyForRun).toHaveBeenCalledOnce(); + }); + + it('resolves supplied ops when the caller releases an open readable lock', async () => { + const ops: Promise[] = []; + const material = new Uint8Array(32).fill(6); + const frame = await encryptedFrames({ open: true }, material); + const world = createMockWorld(); + world.getEncryptionKeyForRun = vi.fn().mockResolvedValue(material); + world.streams = { + get: vi.fn().mockResolvedValue( + new ReadableStream({ + start(controller) { + controller.enqueue(frame); + // Deliberately remain open: releaseLock(), not EOF, is the signal. + }, + }) + ), + } as unknown as World['streams']; + setWorld(world); + + const reader = new Run('wrun_123').getReadable({ ops }).getReader(); + await reader.read(); + reader.releaseLock(); + await expect(Promise.all(ops)).resolves.toEqual([undefined]); + }); + + it('starts stream GET and the cached run-key lookup on first read', async () => { + const material = new Uint8Array(32).fill(7); + const frame = await encryptedFrames({ first: true }, material); + let resolveRun: (run: any) => void; + const runPromise = new Promise((resolve) => { + resolveRun = resolve; + }); + const world = createMockWorld(); + world.runs.get = vi.fn().mockReturnValue(runPromise); + world.getEncryptionKeyForRun = vi.fn().mockResolvedValue(material); + world.streams = { + get: vi.fn().mockResolvedValue( + new ReadableStream({ + start(controller) { + controller.enqueue(frame); + controller.close(); + }, + }) + ), + } as unknown as World['streams']; + setWorld(world); + + const read = new Run('wrun_123').getReadable().getReader().read(); + await vi.waitFor(() => { + expect(world.streams.get).toHaveBeenCalledOnce(); + expect(world.runs.get).toHaveBeenCalledOnce(); + }); + resolveRun?.({ + runId: 'wrun_123', + deploymentId: 'test-deployment', + }); + + await expect(read).resolves.toMatchObject({ value: { first: true } }); + expect(world.getEncryptionKeyForRun).toHaveBeenCalledOnce(); + }); + + it('reuses one run-key promise across readable sessions', async () => { + const material = new Uint8Array(32).fill(8); + const frame = await encryptedFrames({ reusable: true }, material); + const world = createMockWorld(); + world.getEncryptionKeyForRun = vi.fn().mockResolvedValue(material); + world.streams = { + get: vi.fn().mockImplementation( + async () => + new ReadableStream({ + start(controller) { + controller.enqueue(frame); + controller.close(); + }, + }) + ), + } as unknown as World['streams']; + setWorld(world); + + const run = new Run('wrun_123'); + await expect(run.getReadable().getReader().read()).resolves.toMatchObject({ + value: { reusable: true }, + }); + await expect(run.getReadable().getReader().read()).resolves.toMatchObject({ + value: { reusable: true }, + }); + + expect(world.runs.get).toHaveBeenCalledOnce(); + expect(world.getEncryptionKeyForRun).toHaveBeenCalledOnce(); + expect(world.streams.get).toHaveBeenCalledTimes(2); + }); + + it('surfaces a prefetched key failure when an encrypted frame is consumed', async () => { + const material = new Uint8Array(32).fill(9); + const frame = await encryptedFrames({ secret: true }, material); + const keyError = new Error('key lookup failed'); + const world = createMockWorld(); + world.getEncryptionKeyForRun = vi.fn().mockRejectedValue(keyError); + world.streams = { + get: vi.fn().mockResolvedValue( + new ReadableStream({ + start(controller) { + controller.enqueue(frame); + controller.close(); + }, + }) + ), + } as unknown as World['streams']; + setWorld(world); + + await expect( + new Run('wrun_123').getReadable().getReader().read() + ).rejects.toThrow('key lookup failed'); + expect(world.runs.get).toHaveBeenCalledOnce(); + expect(world.getEncryptionKeyForRun).toHaveBeenCalledOnce(); + }); }); describe('Run.wakeUp', () => { diff --git a/packages/core/src/serialization.ts b/packages/core/src/serialization.ts index 51a8e667c6..be043f918c 100644 --- a/packages/core/src/serialization.ts +++ b/packages/core/src/serialization.ts @@ -698,6 +698,29 @@ function recordReadTimeToFirstChunk( })(); } +/** + * Record speculative run-key resolution independently from raw stream TTFC. + * This phase never carries key material and is intentionally separate from + * `workflow.stream.read`, whose endpoint remains the first raw frame. + */ +function recordStreamReadKeyResolution( + startEpochMs: number, + runId: string, + name: string, + succeeded: boolean +): void { + void (async () => { + await recordElapsedSpan('workflow.stream.read.resolve_key', startEpochMs, { + kind: await getSpanKind('CLIENT'), + attributes: { + 'workflow.run.id': runId, + 'workflow.stream.name': name, + 'workflow.stream.read.key_succeeded': succeeded, + }, + }); + })(); +} + /** * Emit the client-observed read-completion span when a stream read drains: * back-dated to the read dispatch, so its duration is the total read, with @@ -891,7 +914,9 @@ const getFramedStreamMaxTotalReconnects = (): number => export function createReconnectingFramedStream( runId: string, name: string, - startIndex?: number + startIndex?: number, + prefetchEncryptionKey: () => Promise = async () => + undefined ): ReadableStream { const reconnectSupported = startIndex === undefined || startIndex >= 0; let currentStartIndex = startIndex ?? 0; @@ -909,6 +934,26 @@ export function createReconnectingFramedStream( let firstChunkReported = false; let chunksDelivered = 0; let bytesDelivered = 0; + let keyPrefetched = false; + + function prefetchKey(): void { + if (keyPrefetched) return; + keyPrefetched = true; + const keyStart = Date.now(); + // The raw stream and key lookup deliberately race. Observe a speculative + // failure here: if the stream is cancelled or fails before an encrypted + // frame reaches the deserialize transform, this promise otherwise has no + // consumer and would become an unhandled rejection. The transform still + // awaits the same promise and surfaces the original error on consumption. + void prefetchEncryptionKey().then( + (key) => { + // Unencrypted runs do not need a key-resolution span. Their resolver + // still runs speculatively so an encrypted frame can join it. + if (key) recordStreamReadKeyResolution(keyStart, runId, name, true); + }, + () => recordStreamReadKeyResolution(keyStart, runId, name, false) + ); + } async function connect(): Promise { if (canceled) return false; @@ -1000,6 +1045,11 @@ export function createReconnectingFramedStream( pull: async (controller) => { if (canceled) return; if (readStart === undefined) readStart = Date.now(); + // Begin resolving the key before awaiting streams.get()/the first raw + // frame. This is intentionally not awaited: buffered raw frames remain + // bounded by the Web Streams backpressure chain while the deserialize + // transform joins this promise only if an encrypted frame needs it. + prefetchKey(); // Loop until we emit something, hit EOF, or fatally error. Reads that // only extend the in-flight-frame buffer don't enqueue anything; we // keep reading rather than returning empty-handed. @@ -2736,6 +2786,69 @@ async function getForwardedWritableEncryptionKey( return rawKey ? await importKey(rawKey, ['encrypt']) : undefined; } +/** + * Create a run's object readable without dispatching its stream GET or + * encryption-key lookup until the caller reads it. The external reviver starts + * its background pipe immediately, so this boundary belongs here—next to the + * source/transform assembly—rather than in `Run`. + * + * @internal + */ +export function getRunReadableStream( + global: Record, + ops: Promise[], + runId: string, + name: string, + startIndex: number | undefined, + cryptoKey: EncryptionKeyParam +): ReadableStream { + let reader: ReadableStreamDefaultReader | undefined; + let lockState: ReturnType | undefined; + let lockPollingStarted = false; + let userReadable: ReadableStream; + + userReadable = new ReadableStream( + { + async pull(controller) { + try { + if (!reader) { + const stream = getExternalRevivers(global, ops, runId, cryptoKey, { + onReadableState: (state) => { + lockState = state; + }, + }).ReadableStream!({ name, startIndex }) as ReadableStream; + reader = stream.getReader(); + if (lockState && !lockPollingStarted) { + lockPollingStarted = true; + // The caller owns this wrapper's reader, so polling it preserves + // the documented releaseLock() completion signal. + pollReadableLock(userReadable, lockState); + } + } + const result = await reader.read(); + if (result.done) controller.close(); + else controller.enqueue(result.value); + } catch (error) { + controller.error(error); + } + }, + async cancel(reason) { + await reader?.cancel(reason).catch(() => {}); + }, + }, + // A positive default high-water mark would run pull at construction to + // fill the queue, turning an unread Run.getReadable() into I/O. + { highWaterMark: 0 } + ); + return userReadable; +} + +/** Options for externally revived object streams. @internal */ +type ExternalReviverOptions = { + /** Receives completion state when a wrapper owns the public readable. */ + onReadableState?: (state: ReturnType) => void; +}; + /** * Revivers for deserialization boundary from the client side, * receiving the return value from the workflow handler. @@ -2748,7 +2861,8 @@ export function getExternalRevivers( global: Record = globalThis, ops: Promise[], runId: string, - cryptoKey: EncryptionKeyParam + cryptoKey: EncryptionKeyParam, + options?: ExternalReviverOptions ): Partial { return { ...getCommonRevivers(global), @@ -2840,33 +2954,47 @@ export function getExternalRevivers( // Errors are handled via state.reject }); - // Start polling to detect when user releases lock - pollReadableLock(userReadable, state); + // Direct reviver callers hold this readable. A future public wrapper + // can provide the state and poll the readable it hands to the caller. + if (options?.onReadableState) options.onReadableState(state); + else pollReadableLock(userReadable, state); return userReadable; } else { // Non-byte streams carry length-prefixed frames, so we can count // completed frames and transparently reconnect when the server // stream connection times out mid-run. + // Memoize this resolver per readable session. The first raw pull + // starts it concurrently with the stream GET; getDeserializeStream + // joins the same promise when an encrypted frame arrives. This also + // avoids invoking arbitrary EncryptionKeyParam callbacks twice. + let keyPromise: Promise | undefined; + const resolveKey = (): Promise => { + keyPromise ??= resolveEncryptionKey(cryptoKey); + return keyPromise; + }; const readable = createReconnectingFramedStream( runId, value.name, - value.startIndex + value.startIndex, + resolveKey ); const transform = getDeserializeStream( - getExternalRevivers(global, ops, runId, cryptoKey), - cryptoKey + getExternalRevivers(global, ops, runId, resolveKey), + resolveKey ); const state = createFlushableState(); ops.push(state.promise); - // Start the flushable pipe in the background + // Start the flushable pipe in the background. flushablePipe(readable, transform.writable, state).catch(() => { // Errors are handled via state.reject }); - // Start polling to detect when user releases lock - pollReadableLock(transform.readable, state); + // Direct reviver callers hold this readable. The public Run factory + // wraps it for first-pull laziness and polls that wrapper instead. + if (options?.onReadableState) options.onReadableState(state); + else pollReadableLock(transform.readable, state); return transform.readable; } diff --git a/packages/world/src/runs.test.ts b/packages/world/src/runs.test.ts index 7f4e0520e0..a6a5286463 100644 --- a/packages/world/src/runs.test.ts +++ b/packages/world/src/runs.test.ts @@ -1,11 +1,37 @@ import { describe, expect, it } from 'vitest'; +import { z } from 'zod'; import { BULK_CANCEL_MAX_RUN_IDS, BulkCancelWorkflowRunResultSchema, BulkCancelWorkflowRunsRequestSchema, BulkCancelWorkflowRunsResultSchema, + WorkflowRunSchema, } from './runs.js'; +describe('WorkflowRunSchema', () => { + it('accepts terminal runs without materialized payloads', () => { + const completedSchema = WorkflowRunSchema.options[2]; + const failedSchema = WorkflowRunSchema.options[3]; + const run = { + runId: 'wrun_1', + deploymentId: 'dpl_1', + workflowName: 'workflow_1', + createdAt: new Date(), + updatedAt: new Date(), + completedAt: new Date(), + }; + + expect(completedSchema.shape.output).toBeInstanceOf(z.ZodOptional); + expect(failedSchema.shape.error).toBeInstanceOf(z.ZodOptional); + expect( + WorkflowRunSchema.safeParse({ ...run, status: 'completed' }).success + ).toBe(true); + expect( + WorkflowRunSchema.safeParse({ ...run, status: 'failed' }).success + ).toBe(true); + }); +}); + describe('BulkCancelWorkflowRunsRequestSchema', () => { it('accepts 1 unique ID with an optional cancelReason', () => { const parsed = BulkCancelWorkflowRunsRequestSchema.parse({ diff --git a/packages/world/src/runs.ts b/packages/world/src/runs.ts index 3412e1939e..73e79fd83c 100644 --- a/packages/world/src/runs.ts +++ b/packages/world/src/runs.ts @@ -146,7 +146,7 @@ export const WorkflowRunSchema = z.discriminatedUnion('status', [ // Completed state - output can be v1 or v2 format WorkflowRunBaseSchema.extend({ status: z.literal('completed'), - output: SerializedDataSchema, + output: SerializedDataSchema.optional(), error: z.undefined().optional(), completedAt: z.coerce.date(), }), @@ -154,7 +154,7 @@ export const WorkflowRunSchema = z.discriminatedUnion('status', [ WorkflowRunBaseSchema.extend({ status: z.literal('failed'), output: z.undefined().optional(), - error: SerializedDataSchema, + error: SerializedDataSchema.optional(), completedAt: z.coerce.date(), }), ]); diff --git a/workbench/python/README.md b/workbench/python/README.md index e41ad5447b..5d9cb6c795 100644 --- a/workbench/python/README.md +++ b/workbench/python/README.md @@ -1,314 +1,75 @@ -# 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 -``` - -## Running it against the Vercel world - -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: - -- `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 +cd workbench/python +node ./node_modules/workflow/bin/run.js inspect --json runs ``` -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. - -Production runs are unaffected on both counts: the secret is `''` on `main`, so -each side talks to `vercel-workflow.com`, which is not protected. +## Update the Python SDK -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. +Regenerate the lockfile to update the pinned `vercel-py/main` revision, then +rerun the suite: -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 +```bash +cd workbench/python +rm uv.lock +uv lock +uv sync --locked ``` -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 +Remove `uv.lock` instead of using `--upgrade-package` due to a uv bug that can +switch transitive `vercel-*` dependencies from the `vercel-py` Git repository +to their PyPI releases during an incremental lock update. -This app is honest about being early. In rough order of how much it costs: +## Vercel deployment -- **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. +The `workbench-python-workflow` Vercel project is rooted at this directory and +is included in the `e2e-vercel-prod` matrix. -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 c5bc6a6aec..7211f6a8af 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. - -What it does *not* do is translate any protocol. `LocalWorld.create_queue_handler` +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. + +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 @@ 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 @@ FIXTURES_MODULE = "workflows.99_e2e" fixtures = importlib.import_module(FIXTURES_MODULE) registry = fixtures.app +flow_handler = registry.http_handler +manifest_handler = registry.manifest_handler -# 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" - - -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 @@ async def aiter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[byte 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 55f8e276dc..0fa74a0394 100644 --- a/workbench/python/e2e-conformance.json +++ b/workbench/python/e2e-conformance.json @@ -9,11 +9,61 @@ ], "language": "python", "fixtures": [ + "abortParallelWorkflow", + "abortReasonWorkflow", + "abortTimeoutWorkflow", + "abortViaHookWorkflow", "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 f7151b5005..16af3068f8 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 681bf5e382..c46ed90d86 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 8e7c91090b..4d926d0456 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" } -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" } +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/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" } -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" }, +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/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]] @@ -383,7 +371,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.4" +version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -391,134 +379,125 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.46.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, - { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, - { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, - { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, -] - -[[package]] -name = "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" }, +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/6b/8f79692844269427abb3e4dd9e68edfcbe65ae25527d99183214de716c59/pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6", size = 2076533, upload-time = "2026-08-28T09:57:35.421Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d0/c787604c71c2bdcda1a5656942fc822cd0f9cd879b9484bb84fc42172703/pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615", size = 1924650, upload-time = "2026-08-28T09:57:37.944Z" }, + { url = "https://files.pythonhosted.org/packages/4a/77/ca2f8e997d9bfdb32205297aff38f210f398822d895b1af1b59fd9df9c13/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb", size = 1951261, upload-time = "2026-08-28T09:57:39.339Z" }, + { url = "https://files.pythonhosted.org/packages/a0/53/bd12e1a9255df4edee00353778e2614b5346265d51e1567ab72153e803a2/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b", size = 2021808, upload-time = "2026-08-28T09:57:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/d7/41/f7f312751ebc6d6767da91964a9c7954c18e226a1720ab234e3dfb9d6c17/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6", size = 2196184, upload-time = "2026-08-28T09:57:42.275Z" }, + { url = "https://files.pythonhosted.org/packages/3d/93/ce93aa030ab6bac4683ba8861e7baad89dd24b02e66b8801a0e4f6a00311/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793", size = 2238212, upload-time = "2026-08-28T09:57:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/34/a1/c8e6b66f499f510752c07a092dfe27621f9c255635e59d38704b5681c35a/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b", size = 2064073, upload-time = "2026-08-28T09:57:45.613Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/605e2b127ee30dbf4b1da9da4843587cf2b2d16486c241cc7a5be2d2c1bd/pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461", size = 2093102, upload-time = "2026-08-28T09:57:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f7/1ab28093c09032ddce7c92c7a55d503b6ecd70f42c32492946c1cb5477b1/pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736", size = 2133452, upload-time = "2026-08-28T09:57:48.362Z" }, + { url = "https://files.pythonhosted.org/packages/30/c8/47c79b756f12f85e8b0fbdb2b495f6b6eb32e6c98a4beae7a570a0b7c63c/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3", size = 2146477, upload-time = "2026-08-28T09:57:49.74Z" }, + { url = "https://files.pythonhosted.org/packages/13/5c/79fc00cb8f651d6061991de8d7cedf1c78c73cbd4862c42ef418f03b8bfa/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f", size = 2300832, upload-time = "2026-08-28T09:57:51.639Z" }, + { url = "https://files.pythonhosted.org/packages/b4/72/dd1a29853cf6d22a1ebd9e3baf0239cbc57d2d16caff36a89e38eb9b1db3/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1", size = 2320505, upload-time = "2026-08-28T09:57:53.236Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/ba4a8e06a9ddad0b4caf69cfaeecc0fbfcec20473bd808f5127fd16491c4/pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069", size = 1956853, upload-time = "2026-08-28T09:57:54.592Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/205ed9d7ddaf44acd489889708ea124a3f41bdb42c141c8684d528ad0e7a/pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d", size = 2042551, upload-time = "2026-08-28T09:57:56.017Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, + { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, + { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, + { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, + { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, + { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, ] [[package]] @@ -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" }, -] +version = "0.7.2" +source = { git = "https://github.com/vercel/vercel-py?subdirectory=src%2Fvercel-headers&branch=main#cc65153ae8417f27ca9569470ef1cb0904ad0735" } [[package]] name = "vercel-internal-core" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } +version = "0.1.3" +source = { git = "https://github.com/vercel/vercel-py?subdirectory=src%2Fvercel-internal-core&branch=main#cc65153ae8417f27ca9569470ef1cb0904ad0735" } 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" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] [[package]] name = "vercel-oidc" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } +version = "0.8.1" +source = { git = "https://github.com/vercel/vercel-py?subdirectory=src%2Fvercel-oidc&branch=main#cc65153ae8417f27ca9569470ef1cb0904ad0735" } 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" } +version = "0.8.1" +source = { git = "https://github.com/vercel/vercel-py?subdirectory=src%2Fvercel-queue&branch=main#cc65153ae8417f27ca9569470ef1cb0904ad0735" } 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" } +version = "0.10.0" +source = { git = "https://github.com/vercel/vercel-py?subdirectory=src%2Fvercel-workflow&branch=main#cc65153ae8417f27ca9569470ef1cb0904ad0735" } 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 86d4782e13..c1574b4a5c 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 3cd1e0d51c..01b00ed06c 100644 --- a/workbench/python/workflows/99_e2e.py +++ b/workbench/python/workflows/99_e2e.py @@ -23,20 +23,36 @@ """ 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,1264 @@ 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} + + +########################################################## +# Cancellable steps +# +# These fixtures cover cancellation behavior that is shared across runtimes: +# timeout, parallel cancellation, reason propagation, and hook-triggered +# cancellation. JavaScript implements them with AbortSignal; Python opts a step +# in with `cancellable=True` and cancels the asyncio task awaiting it. The APIs +# differ, but the driver asserts only the shared behavior. + + +@app.step(cancellable=True) +async def cancellableLongStep() -> dict: + try: + await asyncio.sleep(30) + except asyncio.CancelledError as error: + return { + "result": "aborted", + "reason": str(error.args[0]) if error.args else None, + } + return {"result": "completed", "reason": None} + + +async def _cancelAndWait(task: asyncio.Task, reason: str | None = None) -> dict: + task.cancel(reason) + return await task + + +@app.workflow +async def abortTimeoutWorkflow() -> dict: + longStep = asyncio.ensure_future(cancellableLongStep()) + winner = await _race(longStep, sleep("3s")) + if winner is None: + state = await _cancelAndWait(longStep) + return {"status": "timed out", "aborted": state["result"] == "aborted"} + return {"status": "completed", "result": winner["result"]} + + +@app.workflow +async def abortParallelWorkflow() -> dict: + steps = [asyncio.ensure_future(cancellableLongStep()) for _ in range(3)] + timeout = asyncio.ensure_future(sleep("3s")) + done, _pending = await asyncio.wait( + [*steps, timeout], return_when=asyncio.FIRST_COMPLETED + ) + if timeout in done: + results = await asyncio.gather(*(_cancelAndWait(step) for step in steps)) + return { + "status": "timed out", + "results": [result["result"] for result in results], + } + results = await asyncio.gather(*steps) + return { + "status": "completed", + "results": [result["result"] for result in results], + } + + +@app.workflow +async def abortReasonWorkflow() -> dict: + longStep = asyncio.ensure_future(cancellableLongStep()) + winner = await _race(longStep, sleep("2s")) + if winner is None: + state = await _cancelAndWait(longStep, "custom timeout reason") + else: + state = winner + return { + "aborted": state["result"] == "aborted", + "reason": state["reason"], + } + + +@dataclasses.dataclass +class CancellationHookPayload(BaseHook): + reason: str + + +@app.workflow +async def abortViaHookWorkflow(hookToken: str) -> dict: + hook = CancellationHookPayload.wait(token=hookToken) + longStep = asyncio.ensure_future(cancellableLongStep()) + winner = await _race(longStep, hook) + + if isinstance(winner, CancellationHookPayload): + state = await _cancelAndWait(longStep, winner.reason) + hook.dispose() + if state["result"] == "aborted": + return {"status": "cancelled", "reason": state["reason"]} + return {"status": "completed", "result": state["result"]} + + hook.dispose() + return {"status": "completed", "result": winner["result"]} + + +########################################################## +# 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