diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 00000000..0f4298b7
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @temporalio/sdk
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fae3c740..4443cbdd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,4 +1,7 @@
name: Continuous Integration
+permissions:
+ contents: read
+ actions: write
on: # rebuild any PRs and main branch changes
pull_request:
push:
@@ -12,23 +15,50 @@ jobs:
strategy:
fail-fast: true
matrix:
- python: ["3.7", "3.10"]
- os: [ubuntu-latest, macos-latest, windows-latest]
- runs-on: ${{ matrix.os }}
+ python: ["3.10", "3.14"]
+ os: [ubuntu-latest, macos-intel, macos-arm, windows-latest]
+ include:
+ - os: macos-intel
+ runsOn: macos-15-intel
+ - os: macos-arm
+ runsOn: macos-latest
+ runs-on: ${{ matrix.runsOn || matrix.os }}
steps:
+ - uses: astral-sh/setup-uv@v5
- name: Print build information
run: "echo head_ref: ${{ github.head_ref }}, ref: ${{ github.ref }}, os: ${{ matrix.os }}, python: ${{ matrix.python }}"
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
with:
submodules: recursive
- - uses: actions/setup-python@v1
+ - uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- # Using fixed Poetry version until
- # https://github.com/python-poetry/poetry/pull/7694 is fixed
- - run: python -m pip install --upgrade wheel "poetry==1.4.0" poethepoet
- - run: poetry install --with pydantic
+ - run: uv tool install poethepoet
+ - run: uv sync --group=dsl --group=encryption --group=trio-async
- run: poe lint
- - run: poe test -s -o log_cli_level=DEBUG
- - run: poe test -s -o log_cli_level=DEBUG --workflow-environment time-skipping
+ - run: mkdir junit-xml
+ - run: poe test -s --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}.xml
+ - run: poe test -s --workflow-environment time-skipping --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}--time-skipping.xml
+ # This must remain the last step since it downgrades pydantic
+ - name: Uninstall pydantic
+ shell: bash
+ run: |
+ echo y | uv run pip uninstall pydantic
+ echo y | uv run pip uninstall pydantic-core
+ uv run pip install pydantic==1.10
+ poe test -s --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}--pydantic-v1.xml tests/pydantic_converter_v1/workflow_test.py
+ # On latest, run gevent test
+ - name: Gevent test
+ if: ${{ matrix.python == '3.12' }}
+ run: |
+ uv sync --group gevent
+ uv run gevent_async/test/run_combined.py
+
+ - name: Upload junit-xml artifacts
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: junit-xml--${{github.run_id}}--${{github.run_attempt}}--${{ matrix.python }}--${{ matrix.os }}
+ path: junit-xml
+ retention-days: 14
diff --git a/.gitignore b/.gitignore
index 033df5fb..157a7418 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,6 @@
.venv
+.idea
__pycache__
+.vscode
+.DS_Store
+.claude
diff --git a/README.md b/README.md
index 5c9b8cbd..d4d6a61b 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,26 @@
# Temporal Python SDK Samples
-This is the set of Python samples for the [Python SDK](https://github.com/temporalio/sdk-python).
+This is a collection of samples showing how to use the [Python SDK](https://github.com/temporalio/sdk-python).
## Usage
Prerequisites:
-* Python >= 3.7
-* [Poetry](https://python-poetry.org)
-* [Local Temporal server running](https://docs.temporal.io/application-development/foundations#run-a-development-cluster)
+* [uv](https://docs.astral.sh/uv/)
+* [Temporal CLI installed](https://docs.temporal.io/cli#install)
+* [Local Temporal server running](https://docs.temporal.io/cli/server#start-dev)
+
+The SDK requires Python >= 3.10. You can install Python using uv. For example,
+
+ uv python install 3.13
With this repository cloned, run the following at the root of the directory:
- poetry install
+ uv sync
-That loads all required dependencies. Then to run a sample, usually you just run it in Python. For example:
+That loads all required dependencies. Then to run a sample, usually you just run it under uv. For example:
- poetry run python hello/hello_activity.py
+ uv run hello/hello_activity.py
Some examples require extra dependencies. See each sample's directory for specific instructions.
@@ -37,6 +41,7 @@ Some examples require extra dependencies. See each sample's directory for specif
* [hello_async_activity_completion](hello/hello_async_activity_completion.py) - Complete an activity outside of the
function that was called.
* [hello_cancellation](hello/hello_cancellation.py) - Manually react to cancellation inside workflows and activities.
+ * [hello_change_log_level](hello/hello_change_log_level.py) - Change the level of workflow task failure from WARN to ERROR.
* [hello_child_workflow](hello/hello_child_workflow.py) - Execute a child workflow from a workflow.
* [hello_continue_as_new](hello/hello_continue_as_new.py) - Use continue as new to restart a workflow.
* [hello_cron](hello/hello_cron.py) - Execute a workflow once a minute.
@@ -50,12 +55,26 @@ Some examples require extra dependencies. See each sample's directory for specif
* [hello_search_attributes](hello/hello_search_attributes.py) - Start workflow with search attributes then change
while running.
* [hello_signal](hello/hello_signal.py) - Send signals to a workflow.
+ * [hello update](hello/hello_update.py) - Send a request to and a response from a client to a workflow execution.
-* [activity_sticky_queue](activity_sticky_queue) - Uses unique task queues to ensure activities run on specific workers.
* [activity_worker](activity_worker) - Use Python activities from a workflow in another language.
+* [batch_sliding_window](batch_sliding_window) - Batch processing with a sliding window of child workflows.
+* [bedrock](bedrock) - Orchestrate a chatbot with Amazon Bedrock.
+* [cloud_export_to_parquet](cloud_export_to_parquet) - Set up schedule workflow to process exported files on an hourly basis
+* [context_propagation](context_propagation) - Context propagation through workflows/activities via interceptor.
* [custom_converter](custom_converter) - Use a custom payload converter to handle custom types.
* [custom_decorator](custom_decorator) - Custom decorator to auto-heartbeat a long-running activity.
+* [custom_metric](custom_metric) - Custom metric to record the workflow type in the activity schedule to start latency.
+* [dsl](dsl) - DSL workflow that executes steps defined in a YAML file.
+* [eager_wf_start](eager_wf_start) - Run a workflow using Eager Workflow Start
* [encryption](encryption) - Apply end-to-end encryption for all input/output.
+* [env_config](env_config) - Load client configuration from TOML files with programmatic overrides.
+* [gevent_async](gevent_async) - Combine gevent and Temporal.
+* [hello_standalone_activity](hello_standalone_activity) - Use activities without using a workflow.
+* [langchain](langchain) - Orchestrate workflows for LangChain.
+* [message_passing/introduction](message_passing/introduction/) - Introduction to queries, signals, and updates.
+* [message_passing/safe_message_handlers](message_passing/safe_message_handlers/) - Safely handling updates and signals.
+* [message_passing/update_with_start/lazy_initialization](message_passing/update_with_start/lazy_initialization/) - Use update-with-start to update a Shopping Cart, starting it if it does not exist.
* [open_telemetry](open_telemetry) - Trace workflows with OpenTelemetry.
* [patching](patching) - Alter workflows safely with `patch` and `deprecate_patch`.
* [polling](polling) - Recommended implementation of an activity that needs to periodically poll an external resource waiting its successful completion.
@@ -63,13 +82,14 @@ Some examples require extra dependencies. See each sample's directory for specif
* [pydantic_converter](pydantic_converter) - Data converter for using Pydantic models.
* [schedules](schedules) - Demonstrates a Workflow Execution that occurs according to a schedule.
* [sentry](sentry) - Report errors to Sentry.
+* [trio_async](trio_async) - Use asyncio Temporal in Trio-based environments.
+* [updatable_timer](updatable_timer) - A timer that can be updated while sleeping.
+* [worker_specific_task_queues](worker_specific_task_queues) - Use unique task queues to ensure activities run on specific workers.
+* [worker_versioning](worker_versioning) - Use the Worker Versioning feature to more easily version your workflows & other code.
+* [worker_multiprocessing](worker_multiprocessing) - Leverage Python multiprocessing to parallelize workflow tasks and other CPU bound operations by running multiple workers.
## Test
-Running the tests requires `poe` to be installed.
-
- python -m pip install poethepoet
-
-Once you have `poe` installed you can run:
+To run the tests:
- poe test
+ uv run poe test
diff --git a/activity_sticky_queues/README.md b/activity_sticky_queues/README.md
deleted file mode 100644
index 52c09a8c..00000000
--- a/activity_sticky_queues/README.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# Sticky Activity Queues
-
-This sample is a Python implementation of the [TypeScript "Sticky Workers" example](https://github.com/temporalio/samples-typescript/tree/main/activities-sticky-queues), full credit for the design to the authors of that sample. A [sticky execution](https://docs.temporal.io/tasks#sticky-execution) is a job distribution design pattern where all workflow computational tasks are executed on a single worker. In the Go and Java SDKs this is explicitly supported via the Session option, but in other SDKs a different approach is required.
-
-Typical use cases for sticky executions include tasks where interaction with a filesystem is required, such as data processing or interacting with legacy access structures. This example will write text files to folders corresponding to each worker, located in the `demo_fs` folder. In production, these folders would typically be independent machines in a worker cluster.
-
-This strategy is:
-- Create a `get_available_task_queue` activity that generates a unique task queue name, `unique_worker_task_queue`.
-- For activities intended to be "sticky", only register them in one Worker, and have that be the only Worker listening on that `unique_worker_task_queue`. This will be run on a series of `FileProcessing` workflows.
-- Execute workflows from the Client like normal. Check the Temporal Web UI to confirm tasks were staying with their respective worker.
-
-It doesn't matter where the `get_available_task_queue` activity is run, so it can be "non sticky" as per Temporal default behavior. In this demo, `unique_worker_task_queue` is simply a `uuid` initialized in the Worker, but you can inject smart logic here to uniquely identify the Worker, [as Netflix did](https://community.temporal.io/t/using-dynamic-task-queues-for-traffic-routing/3045). Our example differs from the Node sample by running across 5 unique task queues.
-
-Activities have been artificially slowed with `time.sleep(3)` to simulate slow activities.
-
-### Running This Sample
-
-To run, first see [README.md](../README.md) for prerequisites. Then, run the following from this directory to start the
-worker:
-
- poetry run python worker.py
-
-This will start the worker. Then, in another terminal, run the following to execute the workflow:
-
- poetry run python starter.py
-
-#### Example output:
-
-```bash
-(temporalio-samples-py3.10) user@machine:~/samples-python/activities_sticky_queues$ poetry run python starter.py
-Output checksums:
-49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
-49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
-49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
-49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
-49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
-49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
-49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
-49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
-49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
-49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
-```
-
-
-Checking the history to see where activities are run
-All activities for the one workflow are running against the same task queue, which corresponds to unique workers:
-
-
-
-
-
-
diff --git a/activity_worker/README.md b/activity_worker/README.md
index 8b821281..068d7e99 100644
--- a/activity_worker/README.md
+++ b/activity_worker/README.md
@@ -6,8 +6,8 @@ First run the Go workflow worker by running this in the `go_workflow` directory
go run .
-Then in another terminal, run the sample from this directory:
+Then in another terminal, run the sample from the root directory:
- poetry run python activity_worker.py
+ uv run activity_worker/activity_worker.py
The Python code will invoke the Go workflow which will execute the Python activity and return.
\ No newline at end of file
diff --git a/activity_worker/activity_worker.py b/activity_worker/activity_worker.py
index 3e613169..986b0ae9 100644
--- a/activity_worker/activity_worker.py
+++ b/activity_worker/activity_worker.py
@@ -4,6 +4,7 @@
from temporalio import activity
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
task_queue = "say-hello-task-queue"
@@ -18,7 +19,9 @@ async def say_hello_activity(name: str) -> str:
async def main():
# Create client to localhost on default namespace
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run activity worker
async with Worker(client, task_queue=task_queue, activities=[say_hello_activity]):
diff --git a/activity_worker/go_workflow/go.mod b/activity_worker/go_workflow/go.mod
index b2ef422d..b7e39fcb 100644
--- a/activity_worker/go_workflow/go.mod
+++ b/activity_worker/go_workflow/go.mod
@@ -21,9 +21,9 @@ require (
github.com/stretchr/testify v1.7.0 // indirect
go.temporal.io/api v1.7.1-0.20220223032354-6e6fe738916a // indirect
go.uber.org/atomic v1.9.0 // indirect
- golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
- golang.org/x/sys v0.0.0-20220222200937-f2425489ef4c // indirect
- golang.org/x/text v0.3.7 // indirect
+ golang.org/x/net v0.7.0 // indirect
+ golang.org/x/sys v0.5.0 // indirect
+ golang.org/x/text v0.7.0 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect
google.golang.org/grpc v1.44.0 // indirect
diff --git a/activity_worker/go_workflow/go.sum b/activity_worker/go_workflow/go.sum
index e2c6ea53..c3a593cd 100644
--- a/activity_worker/go_workflow/go.sum
+++ b/activity_worker/go_workflow/go.sum
@@ -102,6 +102,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.temporal.io/api v1.7.1-0.20220223032354-6e6fe738916a h1:SgkeoCikBXMd/3fNNtymIfhpxk8o/E3zIZFBFkHzTtU=
go.temporal.io/api v1.7.1-0.20220223032354-6e6fe738916a/go.mod h1:OnUq5eS+Nyx+irKb3Ws5YB7yjGFf5XmI3WcVRU9COEo=
@@ -115,6 +116,7 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -122,6 +124,7 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -132,9 +135,12 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -145,6 +151,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -156,15 +163,20 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220222200937-f2425489ef4c h1:sSIdNI2Dd6vGv47bKc/xArpfxVmEz2+3j0E6I484xC4=
golang.org/x/sys v0.0.0-20220222200937-f2425489ef4c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -176,6 +188,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/batch_sliding_window/README.md b/batch_sliding_window/README.md
new file mode 100644
index 00000000..8d573ca3
--- /dev/null
+++ b/batch_sliding_window/README.md
@@ -0,0 +1,21 @@
+# Batch Sliding Window
+
+This sample demonstrates a batch processing workflow that maintains a sliding window of record processing workflows.
+
+A `SlidingWindowWorkflow` starts a configured number (sliding window size) of `RecordProcessorWorkflow` children in parallel. Each child processes a single record. When a child completes, a new child is started.
+
+The `SlidingWindowWorkflow` calls continue-as-new after starting a preconfigured number of children to keep its history size bounded. A `RecordProcessorWorkflow` reports its completion through a signal to its parent, which allows notification of a parent that called continue-as-new.
+
+A single instance of `SlidingWindowWorkflow` has limited window size and throughput. To support larger window size and overall throughput, multiple instances of `SlidingWindowWorkflow` run in parallel.
+
+### Running This Sample
+
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from root directory to start the worker:
+
+ uv run batch_sliding_window/worker.py
+
+This will start the worker. Then, in another terminal, run the following to execute the workflow:
+
+ uv run batch_sliding_window/starter.py
+
+The workflow will process 90 records using a sliding window of 10 parallel workers across 3 partitions, with a page size of 5 records per continue-as-new iteration.
diff --git a/batch_sliding_window/__init__.py b/batch_sliding_window/__init__.py
new file mode 100644
index 00000000..959ab031
--- /dev/null
+++ b/batch_sliding_window/__init__.py
@@ -0,0 +1,40 @@
+"""Sliding Window Batch Processing Sample.
+
+This sample demonstrates a batch processing workflow that maintains a sliding window
+of record processing workflows. It includes:
+
+- ProcessBatchWorkflow: Main workflow that partitions work across multiple sliding windows
+- SlidingWindowWorkflow: Implements the sliding window pattern with continue-as-new
+- RecordProcessorWorkflow: Processes individual records
+- RecordLoader: Activity for loading records from external sources
+"""
+
+from batch_sliding_window.batch_workflow import (
+ ProcessBatchWorkflow,
+ ProcessBatchWorkflowInput,
+)
+from batch_sliding_window.record_loader_activity import (
+ GetRecordsInput,
+ GetRecordsOutput,
+ RecordLoader,
+ SingleRecord,
+)
+from batch_sliding_window.record_processor_workflow import RecordProcessorWorkflow
+from batch_sliding_window.sliding_window_workflow import (
+ SlidingWindowState,
+ SlidingWindowWorkflow,
+ SlidingWindowWorkflowInput,
+)
+
+__all__ = [
+ "ProcessBatchWorkflow",
+ "ProcessBatchWorkflowInput",
+ "SlidingWindowWorkflow",
+ "SlidingWindowWorkflowInput",
+ "SlidingWindowState",
+ "RecordProcessorWorkflow",
+ "RecordLoader",
+ "GetRecordsInput",
+ "GetRecordsOutput",
+ "SingleRecord",
+]
diff --git a/batch_sliding_window/batch_workflow.py b/batch_sliding_window/batch_workflow.py
new file mode 100644
index 00000000..a8d1d488
--- /dev/null
+++ b/batch_sliding_window/batch_workflow.py
@@ -0,0 +1,110 @@
+import asyncio
+from dataclasses import dataclass
+from datetime import timedelta
+from typing import List
+
+from temporalio import workflow
+from temporalio.common import WorkflowIDReusePolicy
+from temporalio.exceptions import ApplicationError
+
+from batch_sliding_window.record_loader_activity import RecordLoader
+from batch_sliding_window.sliding_window_workflow import (
+ SlidingWindowWorkflow,
+ SlidingWindowWorkflowInput,
+)
+
+
+@dataclass
+class ProcessBatchWorkflowInput:
+ """Input for the ProcessBatchWorkflow.
+
+ A single input structure is preferred to multiple workflow arguments
+ to simplify backward compatible API changes.
+ """
+
+ page_size: int # Number of children started by a single sliding window workflow run
+ sliding_window_size: int # Maximum number of children to run in parallel
+ partitions: int # How many sliding windows to run in parallel
+
+
+@workflow.defn
+class ProcessBatchWorkflow:
+ """Sample workflow that partitions the data set into continuous ranges.
+
+ A real application can choose any other way to divide the records
+ into multiple collections.
+ """
+
+ @workflow.run
+ async def run(self, input: ProcessBatchWorkflowInput) -> int:
+ # Get total record count
+ record_count: int = await workflow.execute_activity_method(
+ RecordLoader.get_record_count,
+ start_to_close_timeout=timedelta(seconds=5),
+ )
+
+ if input.sliding_window_size < input.partitions:
+ raise ApplicationError(
+ "SlidingWindowSize cannot be less than number of partitions"
+ )
+
+ partitions = self._divide_into_partitions(record_count, input.partitions)
+ window_sizes = self._divide_into_partitions(
+ input.sliding_window_size, input.partitions
+ )
+
+ workflow.logger.info(
+ f"ProcessBatchWorkflow started",
+ extra={
+ "input": input,
+ "record_count": record_count,
+ "partitions": partitions,
+ "window_sizes": window_sizes,
+ },
+ )
+
+ # Start child workflows for each partition
+ tasks = []
+ offset = 0
+
+ for i in range(input.partitions):
+ # Make child id more user-friendly
+ child_id = f"{workflow.info().workflow_id}/{i}"
+
+ # Define partition boundaries
+ maximum_partition_offset = offset + partitions[i]
+ if maximum_partition_offset > record_count:
+ maximum_partition_offset = record_count
+
+ child_input = SlidingWindowWorkflowInput(
+ page_size=input.page_size,
+ sliding_window_size=window_sizes[i],
+ offset=offset, # inclusive
+ maximum_offset=maximum_partition_offset, # exclusive
+ progress=0,
+ current_records=None,
+ )
+
+ task = workflow.execute_child_workflow(
+ SlidingWindowWorkflow.run,
+ child_input,
+ id=child_id,
+ id_reuse_policy=WorkflowIDReusePolicy.ALLOW_DUPLICATE,
+ )
+ tasks.append(task)
+ offset += partitions[i]
+
+ # Wait for all child workflows to complete
+ results = await asyncio.gather(*tasks)
+ return sum(results)
+
+ def _divide_into_partitions(self, number: int, n: int) -> List[int]:
+ """Divide a number into n partitions as evenly as possible."""
+ base = number // n
+ remainder = number % n
+ partitions = [base] * n
+
+ for i in range(remainder):
+ partitions[i] += 1
+
+ return partitions
diff --git a/batch_sliding_window/record_loader_activity.py b/batch_sliding_window/record_loader_activity.py
new file mode 100644
index 00000000..26ae14b1
--- /dev/null
+++ b/batch_sliding_window/record_loader_activity.py
@@ -0,0 +1,59 @@
+from dataclasses import dataclass
+from typing import List
+
+from temporalio import activity
+
+
+@dataclass
+class GetRecordsInput:
+ """Input for the GetRecords activity."""
+
+ page_size: int
+ offset: int
+ max_offset: int
+
+
+@dataclass
+class SingleRecord:
+ """Represents a single record to be processed."""
+
+ id: int
+
+
+@dataclass
+class GetRecordsOutput:
+ """Output from the GetRecords activity."""
+
+ records: List[SingleRecord]
+
+
+class RecordLoader:
+ """Activities for loading records from an external data source."""
+
+ def __init__(self, record_count: int):
+ self.record_count = record_count
+
+ @activity.defn
+ async def get_record_count(self) -> int:
+ """Get the total record count.
+
+ Used to partition processing across parallel sliding windows.
+ The sample implementation just returns a fake value passed during worker initialization.
+ """
+ return self.record_count
+
+ @activity.defn
+ async def get_records(self, input: GetRecordsInput) -> GetRecordsOutput:
+ """Get records loaded from an external data source.
+
+ The sample returns fake records.
+ """
+ if input.max_offset > self.record_count:
+ raise ValueError(
+ f"max_offset({input.max_offset}) > record_count({self.record_count})"
+ )
+
+ limit = min(input.offset + input.page_size, input.max_offset)
+ records = [SingleRecord(id=i) for i in range(input.offset, limit)]
+
+ return GetRecordsOutput(records=records)
diff --git a/batch_sliding_window/record_processor_workflow.py b/batch_sliding_window/record_processor_workflow.py
new file mode 100644
index 00000000..8921a808
--- /dev/null
+++ b/batch_sliding_window/record_processor_workflow.py
@@ -0,0 +1,33 @@
+import asyncio
+import random
+
+from temporalio import workflow
+
+from batch_sliding_window.record_loader_activity import SingleRecord
+
+
+@workflow.defn
+class RecordProcessorWorkflow:
+ """Workflow that implements processing of a single record."""
+
+ @workflow.run
+ async def run(self, record: SingleRecord) -> None:
+ await self._process_record(record)
+
+ # Notify parent about completion via signal
+ parent = workflow.info().parent
+
+ # This workflow is always expected to have a parent.
+ # But for unit testing it might be useful to skip the notification if there is none.
+ if parent:
+ # Don't specify run_id as parent calls continue-as-new
+ handle = workflow.get_external_workflow_handle(parent.workflow_id)
+ await handle.signal("report_completion", record.id)
+
+ async def _process_record(self, record: SingleRecord) -> None:
+ """Simulate application specific record processing."""
+ # Use workflow.random() to get a random number to ensure workflow determinism
+ sleep_duration = workflow.random().randint(1, 10)
+ await workflow.sleep(sleep_duration)
+
+ workflow.logger.info(f"Processed record {record}")
diff --git a/batch_sliding_window/sliding_window_workflow.py b/batch_sliding_window/sliding_window_workflow.py
new file mode 100644
index 00000000..a416fb99
--- /dev/null
+++ b/batch_sliding_window/sliding_window_workflow.py
@@ -0,0 +1,167 @@
+import asyncio
+from dataclasses import dataclass
+from datetime import timedelta
+from typing import Dict, List, Optional, Set
+
+from temporalio import workflow
+from temporalio.common import WorkflowIDReusePolicy
+
+from batch_sliding_window.record_loader_activity import (
+ GetRecordsInput,
+ GetRecordsOutput,
+ RecordLoader,
+ SingleRecord,
+)
+from batch_sliding_window.record_processor_workflow import RecordProcessorWorkflow
+
+
+@dataclass
+class SlidingWindowWorkflowInput:
+ """Contains SlidingWindowWorkflow arguments."""
+
+ page_size: int
+ sliding_window_size: int
+ offset: int # inclusive
+ maximum_offset: int # exclusive
+ progress: int = 0
+ # The set of record ids currently being processed
+ current_records: Optional[Set[int]] = None
+
+
+@dataclass
+class SlidingWindowState:
+ """Used as a 'state' query result."""
+
+ current_records: List[int] # record ids currently being processed
+ children_started_by_this_run: int
+ offset: int
+ progress: int
+
+
+@workflow.defn
+class SlidingWindowWorkflow:
+ """Workflow processes a range of records using a requested number of child workflows.
+
+ As soon as a child workflow completes a new one is started.
+ """
+
+ def __init__(self):
+ self.current_records: Set[int] = set()
+ self.children_started_by_this_run = []
+ self.offset = 0
+ self.progress = 0
+ self._completion_signals_received = 0
+
+ @workflow.run
+ async def run(self, input: SlidingWindowWorkflowInput) -> int:
+ workflow.logger.info(
+ f"SlidingWindowWorkflow started",
+ extra={
+ "sliding_window_size": input.sliding_window_size,
+ "page_size": input.page_size,
+ "offset": input.offset,
+ "maximum_offset": input.maximum_offset,
+ "progress": input.progress,
+ },
+ )
+
+ # Initialize state from input
+ self.current_records = input.current_records or set()
+ self.offset = input.offset
+ self.progress = input.progress
+
+ # Set up query handler
+ workflow.set_query_handler("state", self._handle_state_query)
+
+ # Set up signal handler for completion notifications
+ workflow.set_signal_handler("report_completion", self._handle_completion_signal)
+
+ return await self._execute(input)
+
+ async def _execute(self, input: SlidingWindowWorkflowInput) -> int:
+ """Main execution logic."""
+ # Get records for this page if we haven't reached the end
+ records = []
+ if self.offset < input.maximum_offset:
+ get_records_input = GetRecordsInput(
+ page_size=input.page_size,
+ offset=self.offset,
+ max_offset=input.maximum_offset,
+ )
+ get_records_output: GetRecordsOutput = (
+ await workflow.execute_activity_method(
+ RecordLoader.get_records,
+ get_records_input,
+ start_to_close_timeout=timedelta(seconds=5),
+ )
+ )
+ records = get_records_output.records
+
+ workflow_id = workflow.info().workflow_id
+
+ # Process records
+ for record in records:
+ # Wait until we have capacity in the sliding window
+ await workflow.wait_condition(
+ lambda: len(self.current_records) < input.sliding_window_size
+ )
+
+ # Start child workflow for this record
+ child_id = f"{workflow_id}/{record.id}"
+ self.current_records.add(record.id)
+ child_handle = await workflow.start_child_workflow(
+ RecordProcessorWorkflow.run,
+ record,
+ id=child_id,
+ id_reuse_policy=WorkflowIDReusePolicy.ALLOW_DUPLICATE,
+ parent_close_policy=workflow.ParentClosePolicy.ABANDON,
+ )
+
+ self.children_started_by_this_run.append(child_handle)
+
+ return await self._continue_as_new_or_complete(input)
+
+ async def _continue_as_new_or_complete(
+ self, input: SlidingWindowWorkflowInput
+ ) -> int:
+ """Continue-as-new after starting page_size children or complete if done."""
+ # Update offset based on children started in this run
+ new_offset = input.offset + len(self.children_started_by_this_run)
+
+ if new_offset < input.maximum_offset:
+ # In Python, await start_child_workflow() already waits until
+ # the start has been accepted by the server, so no additional wait needed
+
+ # Continue-as-new with updated state
+ new_input = SlidingWindowWorkflowInput(
+ page_size=input.page_size,
+ sliding_window_size=input.sliding_window_size,
+ offset=new_offset,
+ maximum_offset=input.maximum_offset,
+ progress=self.progress,
+ current_records=self.current_records,
+ )
+
+ workflow.continue_as_new(new_input)
+
+ # Last run in the continue-as-new chain
+ # Wait for all children to complete
+ await workflow.wait_condition(lambda: len(self.current_records) == 0)
+ return self.progress
+
+ def _handle_completion_signal(self, record_id: int) -> None:
+ """Handle completion signal from child workflow."""
+ # Check for duplicate signals
+ if record_id in self.current_records:
+ self.current_records.remove(record_id)
+ self.progress += 1
+
+ def _handle_state_query(self) -> SlidingWindowState:
+ """Handle state query for monitoring."""
+ current_record_ids = sorted(list(self.current_records))
+ return SlidingWindowState(
+ current_records=current_record_ids,
+ children_started_by_this_run=len(self.children_started_by_this_run),
+ offset=self.offset,
+ progress=self.progress,
+ )
diff --git a/batch_sliding_window/starter.py b/batch_sliding_window/starter.py
new file mode 100644
index 00000000..7e3b1fb3
--- /dev/null
+++ b/batch_sliding_window/starter.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+"""Starter for the batch sliding window sample."""
+
+import asyncio
+import datetime
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from batch_sliding_window.batch_workflow import (
+ ProcessBatchWorkflow,
+ ProcessBatchWorkflowInput,
+)
+
+
+async def main():
+ """Start the ProcessBatchWorkflow."""
+ # Set up logging
+ logging.basicConfig(level=logging.INFO)
+
+ # Create client
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Create unique workflow ID with timestamp
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
+ workflow_id = f"batch_sliding_window_example_{timestamp}"
+
+ # Define workflow input
+ workflow_input = ProcessBatchWorkflowInput(
+ page_size=5,
+ sliding_window_size=10,
+ partitions=3,
+ )
+
+ print(f"Starting workflow with ID: {workflow_id}")
+ print(f"Input: {workflow_input}")
+
+ # Start workflow
+ handle = await client.start_workflow(
+ ProcessBatchWorkflow.run,
+ workflow_input,
+ id=workflow_id,
+ task_queue="batch_sliding_window_task_queue",
+ )
+
+ print(f"Workflow started with ID: {handle.id}")
+ print(f"Waiting for workflow to complete...")
+
+ # Wait for result
+ try:
+ result = await handle.result()
+ print(f"Workflow completed successfully!")
+ print(f"Total records processed: {result}")
+ except Exception as e:
+ print(f"Workflow failed with error: {e}")
+ raise
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/batch_sliding_window/worker.py b/batch_sliding_window/worker.py
new file mode 100644
index 00000000..a72ed96c
--- /dev/null
+++ b/batch_sliding_window/worker.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+"""Worker for the batch sliding window sample."""
+
+import asyncio
+import logging
+
+from temporalio import worker
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from batch_sliding_window.batch_workflow import ProcessBatchWorkflow
+from batch_sliding_window.record_loader_activity import RecordLoader
+from batch_sliding_window.record_processor_workflow import RecordProcessorWorkflow
+from batch_sliding_window.sliding_window_workflow import SlidingWindowWorkflow
+
+
+async def main():
+ """Run the worker that registers all workflows and activities."""
+ # Set up logging
+ logging.basicConfig(level=logging.INFO)
+
+ # Create client
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Create RecordLoader activity with sample data
+ record_loader = RecordLoader(record_count=90)
+
+ # Create worker
+ temporal_worker = worker.Worker(
+ client,
+ task_queue="batch_sliding_window_task_queue",
+ workflows=[
+ ProcessBatchWorkflow,
+ SlidingWindowWorkflow,
+ RecordProcessorWorkflow,
+ ],
+ activities=[
+ record_loader.get_record_count,
+ record_loader.get_records,
+ ],
+ )
+
+ print("Starting worker...")
+ # Run the worker
+ await temporal_worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/bedrock/README.md b/bedrock/README.md
new file mode 100644
index 00000000..42a1f4d5
--- /dev/null
+++ b/bedrock/README.md
@@ -0,0 +1,25 @@
+# AI Chatbot example using Amazon Bedrock
+
+Demonstrates how Temporal and Amazon Bedrock can be used to quickly build bulletproof AI applications.
+
+## Samples
+
+* [basic](basic) - A basic Bedrock workflow to process a single prompt.
+* [signals_and_queries](signals_and_queries) - Extension to the basic workflow to allow multiple prompts through signals & queries.
+* [entity](entity) - Full multi-Turn chat using an entity workflow..
+
+## Pre-requisites
+
+1. An AWS account with Bedrock enabled.
+2. A machine that has access to Bedrock.
+3. A local Temporal server running on the same machine. See [Temporal's dev server docs](https://docs.temporal.io/cli#start-dev-server) for more information.
+
+These examples use Amazon's Python SDK (Boto3). To configure Boto3 to use your AWS credentials, follow the instructions in [the Boto3 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html).
+
+## Running the samples
+
+For these sample, the optional `bedrock` dependency group must be included. To include, run:
+
+ uv sync --group bedrock
+
+There are 3 Bedrock samples, see the README.md in each sub-directory for instructions on running each.
\ No newline at end of file
diff --git a/activity_sticky_queues/__init__.py b/bedrock/__init__.py
similarity index 100%
rename from activity_sticky_queues/__init__.py
rename to bedrock/__init__.py
diff --git a/bedrock/basic/README.md b/bedrock/basic/README.md
new file mode 100644
index 00000000..4f3f7430
--- /dev/null
+++ b/bedrock/basic/README.md
@@ -0,0 +1,10 @@
+# Basic Amazon Bedrock workflow
+
+A basic Bedrock workflow. Starts a workflow with a prompt, generates a response and ends the workflow.
+
+To run, first see `samples-python` [README.md](../../README.md), and `bedrock` [README.md](../README.md) for prerequisites specific to this sample. Once set up, run the following from the root directory:
+
+1. Run the worker: `uv run bedrock/basic/run_worker.py`
+2. In another terminal run the client with a prompt:
+
+ e.g. `uv run bedrock/basic/send_message.py 'What animals are marsupials?'`
\ No newline at end of file
diff --git a/bedrock/basic/__init__.py b/bedrock/basic/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/bedrock/basic/run_worker.py b/bedrock/basic/run_worker.py
new file mode 100644
index 00000000..085b695c
--- /dev/null
+++ b/bedrock/basic/run_worker.py
@@ -0,0 +1,39 @@
+import asyncio
+import concurrent.futures
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+from workflows import BasicBedrockWorkflow
+
+from bedrock.shared.activities import BedrockActivities
+
+
+async def main():
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ activities = BedrockActivities()
+
+ # Run the worker
+ with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor:
+ worker = Worker(
+ client,
+ task_queue="bedrock-task-queue",
+ workflows=[BasicBedrockWorkflow],
+ activities=[activities.prompt_bedrock],
+ activity_executor=activity_executor,
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ print("Starting worker")
+ print("Then run 'python send_message.py \"\"'")
+
+ logging.basicConfig(level=logging.INFO)
+
+ asyncio.run(main())
diff --git a/bedrock/basic/send_message.py b/bedrock/basic/send_message.py
new file mode 100644
index 00000000..692a4927
--- /dev/null
+++ b/bedrock/basic/send_message.py
@@ -0,0 +1,32 @@
+import asyncio
+import sys
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from workflows import BasicBedrockWorkflow
+
+
+async def main(prompt: str) -> str:
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Start the workflow
+ workflow_id = "basic-bedrock-workflow"
+ handle = await client.start_workflow(
+ BasicBedrockWorkflow.run,
+ prompt, # Initial prompt
+ id=workflow_id,
+ task_queue="bedrock-task-queue",
+ )
+ return await handle.result()
+
+
+if __name__ == "__main__":
+ if len(sys.argv) != 2:
+ print("Usage: python send_message.py ''")
+ print("Example: python send_message.py 'What animals are marsupials?'")
+ else:
+ result = asyncio.run(main(sys.argv[1]))
+ print(result)
diff --git a/bedrock/basic/workflows.py b/bedrock/basic/workflows.py
new file mode 100644
index 00000000..0ebf0216
--- /dev/null
+++ b/bedrock/basic/workflows.py
@@ -0,0 +1,23 @@
+from datetime import timedelta
+
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from bedrock.shared.activities import BedrockActivities
+
+
+@workflow.defn
+class BasicBedrockWorkflow:
+ @workflow.run
+ async def run(self, prompt: str) -> str:
+ workflow.logger.info("Prompt: %s" % prompt)
+
+ response = await workflow.execute_activity_method(
+ BedrockActivities.prompt_bedrock,
+ prompt,
+ schedule_to_close_timeout=timedelta(seconds=20),
+ )
+
+ workflow.logger.info("Response: %s" % response)
+
+ return response
diff --git a/bedrock/entity/README.md b/bedrock/entity/README.md
new file mode 100644
index 00000000..3f8e90da
--- /dev/null
+++ b/bedrock/entity/README.md
@@ -0,0 +1,19 @@
+# Multi-turn chat with Amazon Bedrock Entity Workflow
+
+Multi-Turn Chat using an Entity Workflow. The workflow runs forever unless explicitly ended. The workflow continues as new after a configurable number of chat turns to keep the prompt size small and the Temporal event history small. Each continued-as-new workflow receives a summary of the conversation history so far for context.
+
+To run, first see `samples-python` [README.md](../../README.md), and `bedrock` [README.md](../README.md) for prerequisites specific to this sample. Once set up, run the following from the root directory:
+
+1. Run the worker: `uv run bedrock/entity/run_worker.py`
+2. In another terminal run the client with a prompt.
+
+ Example: `uv run bedrock/entity/send_message.py 'What animals are marsupials?'`
+
+3. View the worker's output for the response.
+4. Give followup prompts by signaling the workflow.
+
+ Example: `uv run bedrock/entity/send_message.py 'Do they lay eggs?'`
+5. Get the conversation history summary by querying the workflow.
+
+ Example: `uv run bedrock/entity/get_history.py`
+6. To end the chat session, run `uv run bedrock/entity/end_chat.py`
diff --git a/bedrock/entity/__init__.py b/bedrock/entity/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/bedrock/entity/end_chat.py b/bedrock/entity/end_chat.py
new file mode 100644
index 00000000..19984202
--- /dev/null
+++ b/bedrock/entity/end_chat.py
@@ -0,0 +1,25 @@
+import asyncio
+import sys
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from workflows import EntityBedrockWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ workflow_id = "entity-bedrock-workflow"
+
+ handle = client.get_workflow_handle_for(EntityBedrockWorkflow.run, workflow_id)
+
+ # Sends a signal to the workflow
+ await handle.signal(EntityBedrockWorkflow.end_chat)
+
+
+if __name__ == "__main__":
+ print("Sending signal to end chat.")
+ asyncio.run(main())
diff --git a/bedrock/entity/get_history.py b/bedrock/entity/get_history.py
new file mode 100644
index 00000000..ffcfa31e
--- /dev/null
+++ b/bedrock/entity/get_history.py
@@ -0,0 +1,35 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from workflows import EntityBedrockWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ workflow_id = "entity-bedrock-workflow"
+
+ handle = client.get_workflow_handle(workflow_id)
+
+ # Queries the workflow for the conversation history
+ history = await handle.query(EntityBedrockWorkflow.get_conversation_history)
+
+ print("Conversation History")
+ print(
+ *(f"{speaker.title()}: {message}\n" for speaker, message in history), sep="\n"
+ )
+
+ # Queries the workflow for the conversation summary
+ summary = await handle.query(EntityBedrockWorkflow.get_summary_from_history)
+
+ if summary is not None:
+ print("Conversation Summary:")
+ print(summary)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/bedrock/entity/run_worker.py b/bedrock/entity/run_worker.py
new file mode 100644
index 00000000..ecc76c52
--- /dev/null
+++ b/bedrock/entity/run_worker.py
@@ -0,0 +1,39 @@
+import asyncio
+import concurrent.futures
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+from workflows import EntityBedrockWorkflow
+
+from bedrock.shared.activities import BedrockActivities
+
+
+async def main():
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ activities = BedrockActivities()
+
+ # Run the worker
+ with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor:
+ worker = Worker(
+ client,
+ task_queue="bedrock-task-queue",
+ workflows=[EntityBedrockWorkflow],
+ activities=[activities.prompt_bedrock],
+ activity_executor=activity_executor,
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ print("Starting worker")
+ print("Then run 'python send_message.py \"\"'")
+
+ logging.basicConfig(level=logging.INFO)
+
+ asyncio.run(main())
diff --git a/bedrock/entity/send_message.py b/bedrock/entity/send_message.py
new file mode 100644
index 00000000..be7897f0
--- /dev/null
+++ b/bedrock/entity/send_message.py
@@ -0,0 +1,33 @@
+import asyncio
+import sys
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from workflows import BedrockParams, EntityBedrockWorkflow
+
+
+async def main(prompt):
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ workflow_id = "entity-bedrock-workflow"
+
+ # Sends a signal to the workflow (and starts it if needed)
+ await client.start_workflow(
+ EntityBedrockWorkflow.run,
+ BedrockParams(None, None),
+ id=workflow_id,
+ task_queue="bedrock-task-queue",
+ start_signal="user_prompt",
+ start_signal_args=[prompt],
+ )
+
+
+if __name__ == "__main__":
+ if len(sys.argv) != 2:
+ print("Usage: python send_message.py ''")
+ print("Example: python send_message.py 'What animals are marsupials?'")
+ else:
+ asyncio.run(main(sys.argv[1]))
diff --git a/bedrock/entity/workflows.py b/bedrock/entity/workflows.py
new file mode 100644
index 00000000..710f0916
--- /dev/null
+++ b/bedrock/entity/workflows.py
@@ -0,0 +1,164 @@
+import asyncio
+from collections import deque
+from dataclasses import dataclass
+from datetime import timedelta
+from typing import Deque, List, Optional, Tuple
+
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from bedrock.shared.activities import BedrockActivities
+
+
+@dataclass
+class BedrockParams:
+ conversation_summary: Optional[str] = None
+ prompt_queue: Optional[Deque[str]] = None
+
+
+@workflow.defn
+class EntityBedrockWorkflow:
+ def __init__(self) -> None:
+ # List to store prompt history
+ self.conversation_history: List[Tuple[str, str]] = []
+ self.prompt_queue: Deque[str] = deque()
+ self.conversation_summary: Optional[str] = None
+ self.continue_as_new_per_turns: int = 6
+ self.chat_ended: bool = False
+
+ @workflow.run
+ async def run(
+ self,
+ params: BedrockParams,
+ ) -> str:
+ if params and params.conversation_summary:
+ self.conversation_history.append(
+ ("conversation_summary", params.conversation_summary)
+ )
+
+ self.conversation_summary = params.conversation_summary
+
+ if params and params.prompt_queue:
+ self.prompt_queue.extend(params.prompt_queue)
+
+ while True:
+ workflow.logger.info("Waiting for prompts...")
+
+ # Wait for a chat message (signal) or timeout
+ await workflow.wait_condition(
+ lambda: bool(self.prompt_queue) or self.chat_ended
+ )
+
+ if self.prompt_queue:
+ # Fetch next user prompt and add to conversation history
+ prompt = self.prompt_queue.popleft()
+ self.conversation_history.append(("user", prompt))
+
+ workflow.logger.info("Prompt: " + prompt)
+
+ # Send prompt to Amazon Bedrock
+ response = await workflow.execute_activity_method(
+ BedrockActivities.prompt_bedrock,
+ self.prompt_with_history(prompt),
+ schedule_to_close_timeout=timedelta(seconds=20),
+ )
+
+ workflow.logger.info(f"{response}")
+
+ # Append the response to the conversation history
+ self.conversation_history.append(("response", response))
+
+ # Continue as new every x conversational turns to avoid event
+ # history size getting too large. This is also to avoid the
+ # prompt (with conversational history) getting too large for
+ # AWS Bedrock.
+
+ # We summarize the chat to date and use that as input to the
+ # new workflow
+ if len(self.conversation_history) >= self.continue_as_new_per_turns:
+ # Summarize the conversation to date using Amazon Bedrock
+ self.conversation_summary = await workflow.start_activity_method(
+ BedrockActivities.prompt_bedrock,
+ self.prompt_summary_from_history(),
+ schedule_to_close_timeout=timedelta(seconds=20),
+ )
+
+ workflow.logger.info(
+ "Continuing as new due to %i conversational turns."
+ % self.continue_as_new_per_turns,
+ )
+
+ workflow.continue_as_new(
+ args=[
+ BedrockParams(
+ self.conversation_summary,
+ self.prompt_queue,
+ )
+ ]
+ )
+
+ continue
+
+ # If end chat signal was sent
+ if self.chat_ended:
+ # The workflow might be continued as new without any
+ # chat to summarize, so only call Bedrock if there
+ # is more than the previous summary in the history.
+ if len(self.conversation_history) > 1:
+ # Summarize the conversation to date using Amazon Bedrock
+ self.conversation_summary = await workflow.start_activity_method(
+ BedrockActivities.prompt_bedrock,
+ self.prompt_summary_from_history(),
+ schedule_to_close_timeout=timedelta(seconds=20),
+ )
+
+ workflow.logger.info(
+ "Chat ended. Conversation summary:\n"
+ + f"{self.conversation_summary}"
+ )
+
+ return f"{self.conversation_history}"
+
+ @workflow.signal
+ async def user_prompt(self, prompt: str) -> None:
+ # Chat ended but the workflow is waiting for a chat summary to be generated
+ if self.chat_ended:
+ workflow.logger.warn(f"Message dropped due to chat closed: {prompt}")
+ return
+
+ self.prompt_queue.append(prompt)
+
+ @workflow.signal
+ async def end_chat(self) -> None:
+ self.chat_ended = True
+
+ @workflow.query
+ def get_conversation_history(self) -> List[Tuple[str, str]]:
+ return self.conversation_history
+
+ @workflow.query
+ def get_summary_from_history(self) -> Optional[str]:
+ return self.conversation_summary
+
+ # Helper method used in prompts to Amazon Bedrock
+ def format_history(self) -> str:
+ return " ".join(f"{text}" for _, text in self.conversation_history)
+
+ # Create the prompt given to Amazon Bedrock for each conversational turn
+ def prompt_with_history(self, prompt: str) -> str:
+ history_string = self.format_history()
+ return (
+ f"Here is the conversation history: {history_string} Please add "
+ + "a few sentence response to the prompt in plain text sentences. "
+ + "Don't editorialize or add metadata like response. Keep the "
+ + f"text a plain explanation based on the history. Prompt: {prompt}"
+ )
+
+ # Create the prompt to Amazon Bedrock to summarize the conversation history
+ def prompt_summary_from_history(self) -> str:
+ history_string = self.format_history()
+ return (
+ "Here is the conversation history between a user and a chatbot: "
+ + f"{history_string} -- Please produce a two sentence summary of "
+ + "this conversation."
+ )
diff --git a/bedrock/shared/__init__.py b/bedrock/shared/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/bedrock/shared/activities.py b/bedrock/shared/activities.py
new file mode 100644
index 00000000..98c8d055
--- /dev/null
+++ b/bedrock/shared/activities.py
@@ -0,0 +1,39 @@
+import json
+
+import boto3
+from botocore.config import Config
+from temporalio import activity
+
+config = Config(region_name="us-west-2")
+
+
+class BedrockActivities:
+ def __init__(self) -> None:
+ self.bedrock = boto3.client(service_name="bedrock-runtime", config=config)
+
+ @activity.defn
+ def prompt_bedrock(self, prompt: str) -> str:
+ # Model params
+ modelId = "meta.llama2-70b-chat-v1"
+ accept = "application/json"
+ contentType = "application/json"
+ max_gen_len = 512
+ temperature = 0.1
+ top_p = 0.2
+
+ body = json.dumps(
+ {
+ "prompt": prompt,
+ "max_gen_len": max_gen_len,
+ "temperature": temperature,
+ "top_p": top_p,
+ }
+ )
+
+ response = self.bedrock.invoke_model(
+ body=body, modelId=modelId, accept=accept, contentType=contentType
+ )
+
+ response_body = json.loads(response.get("body").read())
+
+ return response_body.get("generation")
diff --git a/bedrock/signals_and_queries/README.md b/bedrock/signals_and_queries/README.md
new file mode 100644
index 00000000..9eb5df8d
--- /dev/null
+++ b/bedrock/signals_and_queries/README.md
@@ -0,0 +1,19 @@
+# Amazon Bedrock workflow using Signals and Queries
+
+Adding signals & queries to the [basic Bedrock sample](../1_basic). Starts a workflow with a prompt, allows follow-up prompts to be given using Temporal signals, and allows the conversation history to be queried using Temporal queries.
+
+To run, first see `samples-python` [README.md](../../README.md), and `bedrock` [README.md](../README.md) for prerequisites specific to this sample. Once set up, run the following from the root directory:
+
+1. Run the worker: `uv run bedrock/signals_and_queries/run_worker.py`
+2. In another terminal run the client with a prompt.
+
+ Example: `uv run bedrock/signals_and_queries/send_message.py 'What animals are marsupials?'`
+
+3. View the worker's output for the response.
+4. Give followup prompts by signaling the workflow.
+
+ Example: `uv run bedrock/signals_and_queries/send_message.py 'Do they lay eggs?'`
+5. Get the conversation history by querying the workflow.
+
+ Example: `uv run bedrock/signals_and_queries/get_history.py`
+6. The workflow will timeout after inactivity.
diff --git a/bedrock/signals_and_queries/__init__.py b/bedrock/signals_and_queries/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/bedrock/signals_and_queries/get_history.py b/bedrock/signals_and_queries/get_history.py
new file mode 100644
index 00000000..2bd6049f
--- /dev/null
+++ b/bedrock/signals_and_queries/get_history.py
@@ -0,0 +1,35 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from workflows import SignalQueryBedrockWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ workflow_id = "bedrock-workflow-with-signals"
+
+ handle = client.get_workflow_handle(workflow_id)
+
+ # Queries the workflow for the conversation history
+ history = await handle.query(SignalQueryBedrockWorkflow.get_conversation_history)
+
+ print("Conversation History")
+ print(
+ *(f"{speaker.title()}: {message}\n" for speaker, message in history), sep="\n"
+ )
+
+ # Queries the workflow for the conversation summary
+ summary = await handle.query(SignalQueryBedrockWorkflow.get_summary_from_history)
+
+ if summary is not None:
+ print("Conversation Summary:")
+ print(summary)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/bedrock/signals_and_queries/run_worker.py b/bedrock/signals_and_queries/run_worker.py
new file mode 100644
index 00000000..9d611588
--- /dev/null
+++ b/bedrock/signals_and_queries/run_worker.py
@@ -0,0 +1,39 @@
+import asyncio
+import concurrent.futures
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+from workflows import SignalQueryBedrockWorkflow
+
+from bedrock.shared.activities import BedrockActivities
+
+
+async def main():
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ activities = BedrockActivities()
+
+ # Run the worker
+ with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor:
+ worker = Worker(
+ client,
+ task_queue="bedrock-task-queue",
+ workflows=[SignalQueryBedrockWorkflow],
+ activities=[activities.prompt_bedrock],
+ activity_executor=activity_executor,
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ print("Starting worker")
+ print("Then run 'python send_message.py \"\"'")
+
+ logging.basicConfig(level=logging.INFO)
+
+ asyncio.run(main())
diff --git a/bedrock/signals_and_queries/send_message.py b/bedrock/signals_and_queries/send_message.py
new file mode 100644
index 00000000..35a9df1c
--- /dev/null
+++ b/bedrock/signals_and_queries/send_message.py
@@ -0,0 +1,34 @@
+import asyncio
+import sys
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from workflows import SignalQueryBedrockWorkflow
+
+
+async def main(prompt):
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ workflow_id = "bedrock-workflow-with-signals"
+ inactivity_timeout_minutes = 1
+
+ # Sends a signal to the workflow (and starts it if needed)
+ await client.start_workflow(
+ SignalQueryBedrockWorkflow.run,
+ inactivity_timeout_minutes,
+ id=workflow_id,
+ task_queue="bedrock-task-queue",
+ start_signal="user_prompt",
+ start_signal_args=[prompt],
+ )
+
+
+if __name__ == "__main__":
+ if len(sys.argv) != 2:
+ print("Usage: python send_message.py ''")
+ print("Example: python send_message.py 'What animals are marsupials?'")
+ else:
+ asyncio.run(main(sys.argv[1]))
diff --git a/bedrock/signals_and_queries/workflows.py b/bedrock/signals_and_queries/workflows.py
new file mode 100644
index 00000000..0c9147d7
--- /dev/null
+++ b/bedrock/signals_and_queries/workflows.py
@@ -0,0 +1,110 @@
+import asyncio
+from collections import deque
+from datetime import timedelta
+from typing import Deque, List, Optional, Tuple
+
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from bedrock.shared.activities import BedrockActivities
+
+
+@workflow.defn
+class SignalQueryBedrockWorkflow:
+ def __init__(self) -> None:
+ # List to store prompt history
+ self.conversation_history: List[Tuple[str, str]] = []
+ self.prompt_queue: Deque[str] = deque()
+ self.conversation_summary = ""
+ self.chat_timeout: bool = False
+
+ @workflow.run
+ async def run(self, inactivity_timeout_minutes: int) -> str:
+ while True:
+ workflow.logger.info(
+ "Waiting for prompts... or closing chat after "
+ + f"{inactivity_timeout_minutes} minute(s)"
+ )
+
+ # Wait for a chat message (signal) or timeout
+ try:
+ await workflow.wait_condition(
+ lambda: bool(self.prompt_queue),
+ timeout=timedelta(minutes=inactivity_timeout_minutes),
+ )
+ # If timeout was reached
+ except asyncio.TimeoutError:
+ self.chat_timeout = True
+ workflow.logger.info("Chat closed due to inactivity")
+ # End the workflow
+ break
+
+ while self.prompt_queue:
+ # Fetch next user prompt and add to conversation history
+ prompt = self.prompt_queue.popleft()
+ self.conversation_history.append(("user", prompt))
+
+ workflow.logger.info(f"Prompt: {prompt}")
+
+ # Send the prompt to Amazon Bedrock
+ response = await workflow.execute_activity_method(
+ BedrockActivities.prompt_bedrock,
+ self.prompt_with_history(prompt),
+ schedule_to_close_timeout=timedelta(seconds=20),
+ )
+
+ workflow.logger.info(f"{response}")
+
+ # Append the response to the conversation history
+ self.conversation_history.append(("response", response))
+
+ # Generate a summary before ending the workflow
+ self.conversation_summary = await workflow.start_activity_method(
+ BedrockActivities.prompt_bedrock,
+ self.prompt_summary_from_history(),
+ schedule_to_close_timeout=timedelta(seconds=20),
+ )
+
+ workflow.logger.info(f"Conversation summary:\n{self.conversation_summary}")
+
+ return f"{self.conversation_history}"
+
+ @workflow.signal
+ async def user_prompt(self, prompt: str) -> None:
+ # Chat timed out but the workflow is waiting for a chat summary to be generated
+ if self.chat_timeout:
+ workflow.logger.warn(f"Message dropped due to chat closed: {prompt}")
+ return
+
+ self.prompt_queue.append(prompt)
+
+ @workflow.query
+ def get_conversation_history(self) -> List[Tuple[str, str]]:
+ return self.conversation_history
+
+ @workflow.query
+ def get_summary_from_history(self) -> str:
+ return self.conversation_summary
+
+ # Helper method used in prompts to Amazon Bedrock
+ def format_history(self) -> str:
+ return " ".join(f"{text}" for _, text in self.conversation_history)
+
+ # Create the prompt given to Amazon Bedrock for each conversational turn
+ def prompt_with_history(self, prompt: str) -> str:
+ history_string = self.format_history()
+ return (
+ f"Here is the conversation history: {history_string} Please add "
+ + "a few sentence response to the prompt in plain text sentences. "
+ + "Don't editorialize or add metadata like response. Keep the "
+ + f"text a plain explanation based on the history. Prompt: {prompt}"
+ )
+
+ # Create the prompt to Amazon Bedrock to summarize the conversation history
+ def prompt_summary_from_history(self) -> str:
+ history_string = self.format_history()
+ return (
+ "Here is the conversation history between a user and a chatbot: "
+ + f"{history_string} -- Please produce a two sentence summary of "
+ + "this conversation."
+ )
diff --git a/cloud_export_to_parquet/README.md b/cloud_export_to_parquet/README.md
new file mode 100644
index 00000000..3079c9f1
--- /dev/null
+++ b/cloud_export_to_parquet/README.md
@@ -0,0 +1,23 @@
+# Cloud Export to parquet sample
+
+This is an example workflow to convert exported file from proto to parquet file. The workflow is an hourly schedule.
+
+Please make sure your python version is 3.10 or above. For this sample, run:
+
+ uv sync --group=cloud-export-to-parquet
+
+Before you start, please modify workflow input in `create_schedule.py` with your s3 bucket and namespace. Also make sure you've the right AWS permission set up in your environment to allow this workflow read and write to your s3 bucket.
+
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from the root directory to start the worker:
+
+```bash
+uv run cloud_export_to_parquet/run_worker.py
+```
+
+This will start the worker. Then, in another terminal, run the following to execute the schedule:
+
+```bash
+uv run cloud_export_to_parquet/create_schedule.py
+```
+
+The workflow should convert exported file in your input s3 bucket to parquet in your specified location.
diff --git a/cloud_export_to_parquet/__init__.py b/cloud_export_to_parquet/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/cloud_export_to_parquet/create_schedule.py b/cloud_export_to_parquet/create_schedule.py
new file mode 100644
index 00000000..1f40a3ed
--- /dev/null
+++ b/cloud_export_to_parquet/create_schedule.py
@@ -0,0 +1,68 @@
+import asyncio
+import traceback
+from datetime import datetime, timedelta
+
+from temporalio.client import (
+ Client,
+ Schedule,
+ ScheduleActionStartWorkflow,
+ ScheduleIntervalSpec,
+ ScheduleSpec,
+ WorkflowFailureError,
+)
+from temporalio.envconfig import ClientConfig
+
+from cloud_export_to_parquet.workflows import (
+ ProtoToParquet,
+ ProtoToParquetWorkflowInput,
+)
+
+
+async def main() -> None:
+ """Main function to run temporal workflow."""
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # TODO: update s3_bucket and namespace to the actual usecase
+ wf_input = ProtoToParquetWorkflowInput(
+ num_delay_hour=2,
+ export_s3_bucket="test-input-bucket",
+ namespace="test.namespace",
+ output_s3_bucket="test-output-bucket",
+ )
+
+ # Run the workflow
+ # try:
+ # await client.start_workflow(
+ # ProtoToParquet.run,
+ # wf_input,
+ # id = f"proto-to-parquet-{datetime.now()}",
+ # task_queue="DATA_TRANSFORMATION_TASK_QUEUE",
+ # )
+ # except WorkflowFailureError:
+ # print("Got exception: ", traceback.format_exc())
+
+ # Create the schedule
+ try:
+ await client.create_schedule(
+ "hourly-proto-to-parquet-wf-schedule",
+ Schedule(
+ action=ScheduleActionStartWorkflow(
+ ProtoToParquet.run,
+ wf_input,
+ id=f"proto-to-parquet-{datetime.now()}",
+ task_queue="DATA_TRANSFORMATION_TASK_QUEUE",
+ ),
+ spec=ScheduleSpec(
+ intervals=[ScheduleIntervalSpec(every=timedelta(hours=1))]
+ ),
+ ),
+ )
+ except WorkflowFailureError:
+ print("Got exception: ", traceback.format_exc())
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/cloud_export_to_parquet/data_trans_activities.py b/cloud_export_to_parquet/data_trans_activities.py
new file mode 100644
index 00000000..b91f6b1a
--- /dev/null
+++ b/cloud_export_to_parquet/data_trans_activities.py
@@ -0,0 +1,123 @@
+import json
+import uuid
+from dataclasses import dataclass
+from typing import List
+
+import boto3
+import pandas as pd
+import temporalio.api.export.v1 as export
+from google.protobuf.json_format import MessageToJson
+from temporalio import activity
+
+
+@dataclass
+class GetObjectKeysActivityInput:
+ bucket: str
+ path: str
+
+
+@dataclass
+class DataTransAndLandActivityInput:
+ export_s3_bucket: str
+ object_key: str
+ output_s3_bucket: str
+ write_path: str
+
+
+@activity.defn
+def get_object_keys(activity_input: GetObjectKeysActivityInput) -> List[str]:
+ """Function that list objects by key."""
+ object_keys = []
+ s3 = boto3.client("s3")
+ response = s3.list_objects_v2(
+ Bucket=activity_input.bucket, Prefix=activity_input.path
+ )
+ for obj in response.get("Contents", []):
+ object_keys.append(obj["Key"])
+ if len(object_keys) == 0:
+ raise FileNotFoundError(
+ f"No files found in {activity_input.bucket}/{activity_input.path}"
+ )
+
+ return object_keys
+
+
+@activity.defn
+def data_trans_and_land(activity_input: DataTransAndLandActivityInput) -> str:
+ """Function that convert proto to parquet and save to S3."""
+ key = activity_input.object_key
+ data = get_data_from_object_key(activity_input.export_s3_bucket, key)
+ activity.logger.info("Convert proto to parquet for file: %s", key)
+ parquet_data = convert_proto_to_parquet_flatten(data)
+ activity.logger.info("Finish transformation for file: %s", key)
+ return save_to_sink(
+ parquet_data, activity_input.output_s3_bucket, activity_input.write_path
+ )
+
+
+def get_data_from_object_key(
+ bucket_name: str, object_key: str
+) -> export.WorkflowExecutions:
+ """Function that get object by key."""
+ v = export.WorkflowExecutions()
+
+ s3 = boto3.client("s3")
+ try:
+ data = s3.get_object(Bucket=bucket_name, Key=object_key)["Body"].read()
+ except Exception as e:
+ activity.logger.error(f"Error reading object: {e}")
+ raise e
+ v.ParseFromString(data)
+ return v
+
+
+def convert_proto_to_parquet_flatten(wfs: export.WorkflowExecutions) -> pd.DataFrame:
+ """Function that convert flatten proto data to parquet."""
+ dfs = []
+ for wf in wfs.items:
+ start_attributes = wf.history.events[
+ 0
+ ].workflow_execution_started_event_attributes
+ histories = wf.history
+ json_str = MessageToJson(histories)
+ row = {
+ "WorkflowID": start_attributes.workflow_id,
+ "RunID": start_attributes.original_execution_run_id,
+ "Histories": json.loads(json_str),
+ }
+ dfs.append(pd.DataFrame([row]))
+ df = pd.concat(dfs, ignore_index=True)
+ rows_flatten = []
+ for _, row in df.iterrows():
+ wf_histories_raw = row["Histories"]["events"]
+ worfkow_id = row["WorkflowID"]
+ run_id = row["RunID"]
+ for history_event in wf_histories_raw:
+ row_flatten = pd.json_normalize(history_event, sep="_")
+ skip_name = ["payloads", "."]
+ columns_to_drop = [
+ col for col in row_flatten.columns for skip in skip_name if skip in col
+ ]
+ row_flatten.drop(columns_to_drop, axis=1, inplace=True)
+ row_flatten.insert(0, "WorkflowId", worfkow_id)
+ row_flatten.insert(1, "RunId", run_id)
+ rows_flatten.append(row_flatten)
+ df_flatten = pd.concat(rows_flatten, ignore_index=True)
+ return df_flatten
+
+
+def save_to_sink(data: pd.DataFrame, s3_bucket: str, write_path: str) -> str:
+ """Function that save object to s3 bucket."""
+ write_bytes = data.to_parquet(None, compression="snappy", index=False)
+ uuid_name = uuid.uuid1()
+ file_name = f"{uuid_name}.parquet"
+ activity.logger.info("Writing to S3 bucket: %s", file_name)
+
+ s3 = boto3.client("s3")
+ try:
+ key = f"{write_path}/{file_name}"
+ s3.put_object(Bucket=s3_bucket, Key=key, Body=write_bytes)
+ return key
+ except Exception as e:
+ activity.logger.error(f"Error saving to sink: {e}")
+ raise e
diff --git a/cloud_export_to_parquet/run_worker.py b/cloud_export_to_parquet/run_worker.py
new file mode 100644
index 00000000..6062abcd
--- /dev/null
+++ b/cloud_export_to_parquet/run_worker.py
@@ -0,0 +1,41 @@
+import asyncio
+from concurrent.futures import ThreadPoolExecutor
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+from temporalio.worker.workflow_sandbox import (
+ SandboxedWorkflowRunner,
+ SandboxRestrictions,
+)
+
+from cloud_export_to_parquet.data_trans_activities import (
+ data_trans_and_land,
+ get_object_keys,
+)
+from cloud_export_to_parquet.workflows import ProtoToParquet
+
+
+async def main() -> None:
+ """Main worker function."""
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Run the worker
+ worker: Worker = Worker(
+ client,
+ task_queue="DATA_TRANSFORMATION_TASK_QUEUE",
+ workflows=[ProtoToParquet],
+ activities=[get_object_keys, data_trans_and_land],
+ workflow_runner=SandboxedWorkflowRunner(
+ restrictions=SandboxRestrictions.default.with_passthrough_modules("boto3")
+ ),
+ activity_executor=ThreadPoolExecutor(100),
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/cloud_export_to_parquet/workflows.py b/cloud_export_to_parquet/workflows.py
new file mode 100644
index 00000000..3c9fe270
--- /dev/null
+++ b/cloud_export_to_parquet/workflows.py
@@ -0,0 +1,74 @@
+from datetime import timedelta
+
+from temporalio import workflow
+from temporalio.common import RetryPolicy
+from temporalio.exceptions import ActivityError
+
+with workflow.unsafe.imports_passed_through():
+ from cloud_export_to_parquet.data_trans_activities import (
+ DataTransAndLandActivityInput,
+ GetObjectKeysActivityInput,
+ data_trans_and_land,
+ get_object_keys,
+ )
+from dataclasses import dataclass
+
+
+@dataclass
+class ProtoToParquetWorkflowInput:
+ num_delay_hour: int
+ export_s3_bucket: str
+ namespace: str
+ output_s3_bucket: str
+
+
+@workflow.defn
+class ProtoToParquet:
+ """Proto to parquet workflow."""
+
+ @workflow.run
+ async def run(self, workflow_input: ProtoToParquetWorkflowInput) -> str:
+ """Run proto to parquet workflow."""
+ retry_policy = RetryPolicy(
+ maximum_attempts=10, maximum_interval=timedelta(seconds=5)
+ )
+
+ # Read from export S3 bucket and given at least 2 hour delay to ensure the file has been uploaded
+ read_time = workflow.now() - timedelta(hours=workflow_input.num_delay_hour)
+ common_path = f"{workflow_input.namespace}/{read_time.year}/{read_time.month:02}/{read_time.day:02}/{read_time.hour:02}/00"
+ path = f"temporal-workflow-history/export/{common_path}"
+ get_object_keys_input = GetObjectKeysActivityInput(
+ workflow_input.export_s3_bucket, path
+ )
+
+ # Read Input File
+ object_keys_output = await workflow.execute_activity(
+ get_object_keys,
+ get_object_keys_input,
+ start_to_close_timeout=timedelta(minutes=5),
+ retry_policy=retry_policy,
+ )
+
+ write_path = f"temporal-workflow-history/parquet/{common_path}"
+
+ try:
+ # Could create a list of corountine objects to process files in parallel
+ for key in object_keys_output:
+ data_trans_and_land_input = DataTransAndLandActivityInput(
+ workflow_input.export_s3_bucket,
+ key,
+ workflow_input.output_s3_bucket,
+ write_path,
+ )
+ # Convert proto to parquet and save to S3
+ await workflow.execute_activity(
+ data_trans_and_land,
+ data_trans_and_land_input,
+ start_to_close_timeout=timedelta(minutes=15),
+ retry_policy=retry_policy,
+ )
+ except ActivityError as output_err:
+ workflow.logger.error(f"Data transformation failed: {output_err}")
+ raise output_err
+
+ return write_path
diff --git a/context_propagation/README.md b/context_propagation/README.md
new file mode 100644
index 00000000..fc79f80b
--- /dev/null
+++ b/context_propagation/README.md
@@ -0,0 +1,16 @@
+# Context Propagation Interceptor Sample
+
+This sample shows how to use an interceptor to propagate contextual information through workflows and activities. For
+this example, [contextvars](https://docs.python.org/3/library/contextvars.html) holds the contextual information.
+
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from the root directory to start the
+worker:
+
+ uv run context_propagation/worker.py
+
+This will start the worker. Then, in another terminal, run the following to execute the workflow:
+
+ uv run context_propagation/starter.py
+
+The starter terminal should complete with the hello result and the worker terminal should show the logs with the
+propagated user ID contextual information flowing through the workflows/activities.
\ No newline at end of file
diff --git a/context_propagation/__init__.py b/context_propagation/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/context_propagation/activities.py b/context_propagation/activities.py
new file mode 100644
index 00000000..77cde015
--- /dev/null
+++ b/context_propagation/activities.py
@@ -0,0 +1,9 @@
+from temporalio import activity
+
+from context_propagation import shared
+
+
+@activity.defn
+async def say_hello_activity(name: str) -> str:
+ activity.logger.info(f"Activity called by user {shared.user_id.get()}")
+ return f"Hello, {name}"
diff --git a/context_propagation/interceptor.py b/context_propagation/interceptor.py
new file mode 100644
index 00000000..b9058548
--- /dev/null
+++ b/context_propagation/interceptor.py
@@ -0,0 +1,193 @@
+from __future__ import annotations
+
+from contextlib import contextmanager
+from typing import Any, Mapping, Protocol, Type
+
+import temporalio.activity
+import temporalio.api.common.v1
+import temporalio.client
+import temporalio.converter
+import temporalio.worker
+import temporalio.workflow
+
+with temporalio.workflow.unsafe.imports_passed_through():
+ from context_propagation.shared import HEADER_KEY, user_id
+
+
+class _InputWithHeaders(Protocol):
+ headers: Mapping[str, temporalio.api.common.v1.Payload]
+
+
+def set_header_from_context(
+ input: _InputWithHeaders, payload_converter: temporalio.converter.PayloadConverter
+) -> None:
+ user_id_val = user_id.get()
+ if user_id_val:
+ input.headers = {
+ **input.headers,
+ HEADER_KEY: payload_converter.to_payload(user_id_val),
+ }
+
+
+@contextmanager
+def context_from_header(
+ input: _InputWithHeaders, payload_converter: temporalio.converter.PayloadConverter
+):
+ payload = input.headers.get(HEADER_KEY)
+ token = (
+ user_id.set(payload_converter.from_payload(payload, str)) if payload else None
+ )
+ try:
+ yield
+ finally:
+ if token:
+ user_id.reset(token)
+
+
+class ContextPropagationInterceptor(
+ temporalio.client.Interceptor, temporalio.worker.Interceptor
+):
+ """Interceptor that propagates a value through client, workflow and activity calls.
+
+ This interceptor implements methods `temporalio.client.Interceptor` and `temporalio.worker.Interceptor` so that
+
+ (1) a user ID key is taken from context by the client code and sent in a header field with outbound requests
+ (2) workflows take this value from their task input, set it in context, and propagate it into the header field of
+ their outbound calls
+ (3) activities similarly take the value from their task input and set it in context so that it's available for their
+ outbound calls
+ """
+
+ def __init__(
+ self,
+ payload_converter: temporalio.converter.PayloadConverter = temporalio.converter.default().payload_converter,
+ ) -> None:
+ self._payload_converter = payload_converter
+
+ def intercept_client(
+ self, next: temporalio.client.OutboundInterceptor
+ ) -> temporalio.client.OutboundInterceptor:
+ return _ContextPropagationClientOutboundInterceptor(
+ next, self._payload_converter
+ )
+
+ def intercept_activity(
+ self, next: temporalio.worker.ActivityInboundInterceptor
+ ) -> temporalio.worker.ActivityInboundInterceptor:
+ return _ContextPropagationActivityInboundInterceptor(next)
+
+ def workflow_interceptor_class(
+ self, input: temporalio.worker.WorkflowInterceptorClassInput
+ ) -> Type[_ContextPropagationWorkflowInboundInterceptor]:
+ return _ContextPropagationWorkflowInboundInterceptor
+
+
+class _ContextPropagationClientOutboundInterceptor(
+ temporalio.client.OutboundInterceptor
+):
+ def __init__(
+ self,
+ next: temporalio.client.OutboundInterceptor,
+ payload_converter: temporalio.converter.PayloadConverter,
+ ) -> None:
+ super().__init__(next)
+ self._payload_converter = payload_converter
+
+ async def start_workflow(
+ self, input: temporalio.client.StartWorkflowInput
+ ) -> temporalio.client.WorkflowHandle[Any, Any]:
+ set_header_from_context(input, self._payload_converter)
+ return await super().start_workflow(input)
+
+ async def query_workflow(self, input: temporalio.client.QueryWorkflowInput) -> Any:
+ set_header_from_context(input, self._payload_converter)
+ return await super().query_workflow(input)
+
+ async def signal_workflow(
+ self, input: temporalio.client.SignalWorkflowInput
+ ) -> None:
+ set_header_from_context(input, self._payload_converter)
+ await super().signal_workflow(input)
+
+ async def start_workflow_update(
+ self, input: temporalio.client.StartWorkflowUpdateInput
+ ) -> temporalio.client.WorkflowUpdateHandle[Any]:
+ set_header_from_context(input, self._payload_converter)
+ return await self.next.start_workflow_update(input)
+
+
+class _ContextPropagationActivityInboundInterceptor(
+ temporalio.worker.ActivityInboundInterceptor
+):
+ async def execute_activity(
+ self, input: temporalio.worker.ExecuteActivityInput
+ ) -> Any:
+ with context_from_header(input, temporalio.activity.payload_converter()):
+ return await self.next.execute_activity(input)
+
+
+class _ContextPropagationWorkflowInboundInterceptor(
+ temporalio.worker.WorkflowInboundInterceptor
+):
+ def init(self, outbound: temporalio.worker.WorkflowOutboundInterceptor) -> None:
+ self.next.init(_ContextPropagationWorkflowOutboundInterceptor(outbound))
+
+ async def execute_workflow(
+ self, input: temporalio.worker.ExecuteWorkflowInput
+ ) -> Any:
+ with context_from_header(input, temporalio.workflow.payload_converter()):
+ return await self.next.execute_workflow(input)
+
+ async def handle_signal(self, input: temporalio.worker.HandleSignalInput) -> None:
+ with context_from_header(input, temporalio.workflow.payload_converter()):
+ return await self.next.handle_signal(input)
+
+ async def handle_query(self, input: temporalio.worker.HandleQueryInput) -> Any:
+ with context_from_header(input, temporalio.workflow.payload_converter()):
+ return await self.next.handle_query(input)
+
+ def handle_update_validator(
+ self, input: temporalio.worker.HandleUpdateInput
+ ) -> None:
+ with context_from_header(input, temporalio.workflow.payload_converter()):
+ self.next.handle_update_validator(input)
+
+ async def handle_update_handler(
+ self, input: temporalio.worker.HandleUpdateInput
+ ) -> Any:
+ with context_from_header(input, temporalio.workflow.payload_converter()):
+ return await self.next.handle_update_handler(input)
+
+
+class _ContextPropagationWorkflowOutboundInterceptor(
+ temporalio.worker.WorkflowOutboundInterceptor
+):
+ async def signal_child_workflow(
+ self, input: temporalio.worker.SignalChildWorkflowInput
+ ) -> None:
+ set_header_from_context(input, temporalio.workflow.payload_converter())
+ return await self.next.signal_child_workflow(input)
+
+ async def signal_external_workflow(
+ self, input: temporalio.worker.SignalExternalWorkflowInput
+ ) -> None:
+ set_header_from_context(input, temporalio.workflow.payload_converter())
+ return await self.next.signal_external_workflow(input)
+
+ def start_activity(
+ self, input: temporalio.worker.StartActivityInput
+ ) -> temporalio.workflow.ActivityHandle:
+ set_header_from_context(input, temporalio.workflow.payload_converter())
+ return self.next.start_activity(input)
+
+ async def start_child_workflow(
+ self, input: temporalio.worker.StartChildWorkflowInput
+ ) -> temporalio.workflow.ChildWorkflowHandle:
+ set_header_from_context(input, temporalio.workflow.payload_converter())
+ return await self.next.start_child_workflow(input)
+
+ def start_local_activity(
+ self, input: temporalio.worker.StartLocalActivityInput
+ ) -> temporalio.workflow.ActivityHandle:
+ set_header_from_context(input, temporalio.workflow.payload_converter())
+ return self.next.start_local_activity(input)
diff --git a/context_propagation/shared.py b/context_propagation/shared.py
new file mode 100644
index 00000000..faae59d8
--- /dev/null
+++ b/context_propagation/shared.py
@@ -0,0 +1,6 @@
+from contextvars import ContextVar
+from typing import Optional
+
+HEADER_KEY = "__my_user_id"
+
+user_id: ContextVar[Optional[str]] = ContextVar("user_id", default=None)
diff --git a/context_propagation/starter.py b/context_propagation/starter.py
new file mode 100644
index 00000000..4d141dc0
--- /dev/null
+++ b/context_propagation/starter.py
@@ -0,0 +1,39 @@
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from context_propagation import interceptor, shared, workflows
+
+
+async def main():
+ logging.basicConfig(level=logging.INFO)
+
+ # Set the user ID
+ shared.user_id.set("some-user")
+
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
+ # Connect client
+ client = await Client.connect(
+ **config,
+ # Use our interceptor
+ interceptors=[interceptor.ContextPropagationInterceptor()],
+ )
+
+ # Start workflow, send signal, wait for completion, issue query
+ handle = await client.start_workflow(
+ workflows.SayHelloWorkflow.run,
+ "Temporal",
+ id=f"context-propagation-workflow-id",
+ task_queue="context-propagation-task-queue",
+ )
+ await handle.signal(workflows.SayHelloWorkflow.signal_complete)
+ result = await handle.result()
+ logging.info(f"Workflow result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/context_propagation/worker.py b/context_propagation/worker.py
new file mode 100644
index 00000000..70ffa368
--- /dev/null
+++ b/context_propagation/worker.py
@@ -0,0 +1,45 @@
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from context_propagation import activities, interceptor, workflows
+
+interrupt_event = asyncio.Event()
+
+
+async def main():
+ logging.basicConfig(level=logging.INFO)
+
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
+ # Connect client
+ client = await Client.connect(
+ **config,
+ # Use our interceptor
+ interceptors=[interceptor.ContextPropagationInterceptor()],
+ )
+
+ # Run a worker for the workflow
+ async with Worker(
+ client,
+ task_queue="context-propagation-task-queue",
+ activities=[activities.say_hello_activity],
+ workflows=[workflows.SayHelloWorkflow],
+ ):
+ # Wait until interrupted
+ logging.info("Worker started, ctrl+c to exit")
+ await interrupt_event.wait()
+ logging.info("Shutting down")
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/context_propagation/workflows.py b/context_propagation/workflows.py
new file mode 100644
index 00000000..e9c120b3
--- /dev/null
+++ b/context_propagation/workflows.py
@@ -0,0 +1,28 @@
+from datetime import timedelta
+
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from context_propagation.activities import say_hello_activity
+ from context_propagation.shared import user_id
+
+
+@workflow.defn
+class SayHelloWorkflow:
+ def __init__(self) -> None:
+ self._complete = False
+
+ @workflow.run
+ async def run(self, name: str) -> str:
+ workflow.logger.info(f"Workflow called by user {user_id.get()}")
+
+ # Wait for signal then run activity
+ await workflow.wait_condition(lambda: self._complete)
+ return await workflow.execute_activity(
+ say_hello_activity, name, start_to_close_timeout=timedelta(minutes=5)
+ )
+
+ @workflow.signal
+ async def signal_complete(self) -> None:
+ workflow.logger.info(f"Signal called by user {user_id.get()}")
+ self._complete = True
diff --git a/custom_converter/README.md b/custom_converter/README.md
index d4c9f588..4ded4198 100644
--- a/custom_converter/README.md
+++ b/custom_converter/README.md
@@ -2,14 +2,14 @@
This sample shows how to make a custom payload converter for a type not natively supported by Temporal.
-To run, first see [README.md](../README.md) for prerequisites. Then, run the following from this directory to start the
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from the root directory to start the
worker:
- poetry run python worker.py
+ uv run custom_converter/worker.py
This will start the worker. Then, in another terminal, run the following to execute the workflow:
- poetry run python starter.py
+ uv run custom_converter/starter.py
The workflow should complete with the hello result. If the custom converter was not set for the custom input and output
classes, we would get an error on the client side and on the worker side.
\ No newline at end of file
diff --git a/custom_converter/shared.py b/custom_converter/shared.py
new file mode 100644
index 00000000..2aa71f16
--- /dev/null
+++ b/custom_converter/shared.py
@@ -0,0 +1,65 @@
+import dataclasses
+from typing import Any, Optional, Type
+
+import temporalio.converter
+from temporalio.api.common.v1 import Payload
+from temporalio.converter import (
+ CompositePayloadConverter,
+ DefaultPayloadConverter,
+ EncodingPayloadConverter,
+)
+
+
+class GreetingInput:
+ def __init__(self, name: str) -> None:
+ self.name = name
+
+
+class GreetingOutput:
+ def __init__(self, result: str) -> None:
+ self.result = result
+
+
+class GreetingEncodingPayloadConverter(EncodingPayloadConverter):
+ @property
+ def encoding(self) -> str:
+ return "text/my-greeting-encoding"
+
+ def to_payload(self, value: Any) -> Optional[Payload]:
+ if isinstance(value, GreetingInput):
+ return Payload(
+ metadata={"encoding": self.encoding.encode(), "is_input": b"true"},
+ data=value.name.encode(),
+ )
+ elif isinstance(value, GreetingOutput):
+ return Payload(
+ metadata={"encoding": self.encoding.encode()},
+ data=value.result.encode(),
+ )
+ else:
+ return None
+
+ def from_payload(self, payload: Payload, type_hint: Optional[Type] = None) -> Any:
+ if payload.metadata.get("is_input") == b"true":
+ # Confirm proper type hint if present
+ assert not type_hint or type_hint is GreetingInput
+ return GreetingInput(payload.data.decode())
+ else:
+ assert not type_hint or type_hint is GreetingOutput
+ return GreetingOutput(payload.data.decode())
+
+
+class GreetingPayloadConverter(CompositePayloadConverter):
+ def __init__(self) -> None:
+ # Just add ours as first before the defaults
+ super().__init__(
+ GreetingEncodingPayloadConverter(),
+ *DefaultPayloadConverter.default_encoding_payload_converters,
+ )
+
+
+# Use the default data converter, but change the payload converter.
+greeting_data_converter = dataclasses.replace(
+ temporalio.converter.default(),
+ payload_converter_class=GreetingPayloadConverter,
+)
diff --git a/custom_converter/starter.py b/custom_converter/starter.py
index ff2d94b1..cc500ae4 100644
--- a/custom_converter/starter.py
+++ b/custom_converter/starter.py
@@ -1,28 +1,26 @@
import asyncio
-import dataclasses
-import temporalio.converter
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
-from custom_converter.worker import (
+from custom_converter.shared import (
GreetingInput,
GreetingOutput,
- GreetingPayloadConverter,
- GreetingWorkflow,
+ greeting_data_converter,
)
+from custom_converter.workflow import GreetingWorkflow
async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
# Connect client
client = await Client.connect(
- "localhost:7233",
- # Use the default data converter, but change the payload converter.
+ **config,
# Without this we get:
# TypeError: Object of type GreetingInput is not JSON serializable
- data_converter=dataclasses.replace(
- temporalio.converter.default(),
- payload_converter_class=GreetingPayloadConverter,
- ),
+ data_converter=greeting_data_converter,
)
# Run workflow
diff --git a/custom_converter/worker.py b/custom_converter/worker.py
index f7a4724a..17186aee 100644
--- a/custom_converter/worker.py
+++ b/custom_converter/worker.py
@@ -1,89 +1,25 @@
import asyncio
-import dataclasses
-from typing import Any, Optional, Type
-import temporalio.converter
-from temporalio import workflow
-from temporalio.api.common.v1 import Payload
from temporalio.client import Client
-from temporalio.converter import (
- CompositePayloadConverter,
- DefaultPayloadConverter,
- EncodingPayloadConverter,
-)
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
-
-class GreetingInput:
- def __init__(self, name: str) -> None:
- self.name = name
-
-
-class GreetingOutput:
- def __init__(self, result: str) -> None:
- self.result = result
-
-
-@workflow.defn
-class GreetingWorkflow:
- @workflow.run
- async def run(self, input: GreetingInput) -> GreetingOutput:
- return GreetingOutput(f"Hello, {input.name}")
-
-
-class GreetingEncodingPayloadConverter(EncodingPayloadConverter):
- @property
- def encoding(self) -> str:
- return "text/my-greeting-encoding"
-
- def to_payload(self, value: Any) -> Optional[Payload]:
- if isinstance(value, GreetingInput):
- return Payload(
- metadata={"encoding": self.encoding.encode(), "is_input": b"true"},
- data=value.name.encode(),
- )
- elif isinstance(value, GreetingOutput):
- return Payload(
- metadata={"encoding": self.encoding.encode()},
- data=value.result.encode(),
- )
- else:
- return None
-
- def from_payload(self, payload: Payload, type_hint: Optional[Type] = None) -> Any:
- if payload.metadata.get("is_input") == b"true":
- # Confirm proper type hint if present
- assert not type_hint or type_hint is GreetingInput
- return GreetingInput(payload.data.decode())
- else:
- assert not type_hint or type_hint is GreetingOutput
- return GreetingOutput(payload.data.decode())
-
-
-class GreetingPayloadConverter(CompositePayloadConverter):
- def __init__(self) -> None:
- # Just add ours as first before the defaults
- super().__init__(
- GreetingEncodingPayloadConverter(),
- # TODO(cretz): Make this list available without instantiation - https://github.com/temporalio/sdk-python/issues/139
- *DefaultPayloadConverter().converters.values(),
- )
-
+from custom_converter.shared import greeting_data_converter
+from custom_converter.workflow import GreetingWorkflow
interrupt_event = asyncio.Event()
async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
# Connect client
client = await Client.connect(
- "localhost:7233",
- # Use the default data converter, but change the payload converter.
+ **config,
# Without this, when trying to run a workflow, we get:
# KeyError: 'Unknown payload encoding my-greeting-encoding
- data_converter=dataclasses.replace(
- temporalio.converter.default(),
- payload_converter_class=GreetingPayloadConverter,
- ),
+ data_converter=greeting_data_converter,
)
# Run a worker for the workflow
diff --git a/custom_converter/workflow.py b/custom_converter/workflow.py
new file mode 100644
index 00000000..0071d5b3
--- /dev/null
+++ b/custom_converter/workflow.py
@@ -0,0 +1,11 @@
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from custom_converter.shared import GreetingInput, GreetingOutput
+
+
+@workflow.defn
+class GreetingWorkflow:
+ @workflow.run
+ async def run(self, input: GreetingInput) -> GreetingOutput:
+ return GreetingOutput(f"Hello, {input.name}")
diff --git a/custom_decorator/README.md b/custom_decorator/README.md
index 65f59482..24cd516d 100644
--- a/custom_decorator/README.md
+++ b/custom_decorator/README.md
@@ -3,14 +3,14 @@
This sample shows a custom decorator can help with Temporal code reuse. Specifically, this makes a `@auto_heartbeater`
decorator that automatically configures an activity to heartbeat twice as frequently as the heartbeat timeout is set to.
-To run, first see [README.md](../README.md) for prerequisites. Then, run the following from this directory to start the
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from the root directory to start the
worker:
- poetry run python worker.py
+ uv run custom_decorator/worker.py
This will start the worker. Then, in another terminal, run the following to execute the workflow:
- poetry run python starter.py
+ uv run custom_decorator/starter.py
The workflow will be started, and then after 5 seconds will be sent a signal to cancel its forever-running activity.
The activity has a heartbeat timeout set to 2s, so since it has the `@auto_heartbeater` decorator set, it will heartbeat
diff --git a/custom_decorator/starter.py b/custom_decorator/starter.py
index 98bf542f..aff675da 100644
--- a/custom_decorator/starter.py
+++ b/custom_decorator/starter.py
@@ -1,13 +1,16 @@
import asyncio
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from custom_decorator.worker import WaitForCancelWorkflow
async def main():
# Connect client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Start the workflow
handle = await client.start_workflow(
diff --git a/custom_decorator/worker.py b/custom_decorator/worker.py
index 7d0d25ca..0d25145b 100644
--- a/custom_decorator/worker.py
+++ b/custom_decorator/worker.py
@@ -4,6 +4,7 @@
from temporalio import activity, workflow
from temporalio.client import Client
from temporalio.common import RetryPolicy
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
from custom_decorator.activity_utils import auto_heartbeater
@@ -51,7 +52,9 @@ def cancel_activity(self) -> None:
async def main():
# Connect client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
diff --git a/custom_metric/README.md b/custom_metric/README.md
new file mode 100644
index 00000000..de1d51d5
--- /dev/null
+++ b/custom_metric/README.md
@@ -0,0 +1,36 @@
+# Custom Metric
+
+This sample deminstrates two things: (1) how to make a custom metric, and (2) how to use an interceptor.
+The custom metric in this sample is an activity schedule-to-start-latency with a workflow type tag.
+
+Please see the top-level [README](../README.md) for prerequisites such as Python, uv, starting the local temporal development server, etc.
+
+1. Run the worker with `uv run custom_metric/worker.py`
+2. Request execution of the workflow with `uv run custom_metric/starter.py`
+3. Go to `http://127.0.0.1:9090/metrics` in your browser
+
+You'll get something like the following:
+
+```txt
+custom_activity_schedule_to_start_latency_bucket{activity_type="print_and_sleep",namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",workflow_type="StartTwoActivitiesWorkflow",le="100"} 1
+custom_activity_schedule_to_start_latency_bucket{activity_type="print_and_sleep",namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",workflow_type="StartTwoActivitiesWorkflow",le="500"} 1
+custom_activity_schedule_to_start_latency_bucket{activity_type="print_and_sleep",namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",workflow_type="StartTwoActivitiesWorkflow",le="1000"} 1
+custom_activity_schedule_to_start_latency_bucket{activity_type="print_and_sleep",namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",workflow_type="StartTwoActivitiesWorkflow",le="5000"} 2
+custom_activity_schedule_to_start_latency_bucket{activity_type="print_and_sleep",namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",workflow_type="StartTwoActivitiesWorkflow",le="10000"} 2
+custom_activity_schedule_to_start_latency_bucket{activity_type="print_and_sleep",namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",workflow_type="StartTwoActivitiesWorkflow",le="100000"} 2
+custom_activity_schedule_to_start_latency_bucket{activity_type="print_and_sleep",namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",workflow_type="StartTwoActivitiesWorkflow",le="1000000"} 2
+custom_activity_schedule_to_start_latency_bucket{activity_type="print_and_sleep",namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",workflow_type="StartTwoActivitiesWorkflow",le="+Inf"} 2
+custom_activity_schedule_to_start_latency_sum{activity_type="print_and_sleep",namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",workflow_type="StartTwoActivitiesWorkflow"} 1010
+custom_activity_schedule_to_start_latency_count{activity_type="print_and_sleep",namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",workflow_type="StartTwoActivitiesWorkflow"} 2
+...
+temporal_activity_schedule_to_start_latency_bucket{namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",le="100"} 1
+temporal_activity_schedule_to_start_latency_bucket{namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",le="500"} 1
+temporal_activity_schedule_to_start_latency_bucket{namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",le="1000"} 1
+temporal_activity_schedule_to_start_latency_bucket{namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",le="5000"} 2
+temporal_activity_schedule_to_start_latency_bucket{namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",le="10000"} 2
+temporal_activity_schedule_to_start_latency_bucket{namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",le="100000"} 2
+temporal_activity_schedule_to_start_latency_bucket{namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",le="1000000"} 2
+temporal_activity_schedule_to_start_latency_bucket{namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue",le="+Inf"} 2
+temporal_activity_schedule_to_start_latency_sum{namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue"} 1010
+temporal_activity_schedule_to_start_latency_count{namespace="default",service_name="temporal-core-sdk",task_queue="custom-metric-task-queue"} 2
+```
diff --git a/custom_metric/__init__.py b/custom_metric/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/custom_metric/activity.py b/custom_metric/activity.py
new file mode 100644
index 00000000..7f2ee116
--- /dev/null
+++ b/custom_metric/activity.py
@@ -0,0 +1,9 @@
+import time
+
+from temporalio import activity
+
+
+@activity.defn
+def print_and_sleep():
+ print("In the activity.")
+ time.sleep(1)
diff --git a/custom_metric/starter.py b/custom_metric/starter.py
new file mode 100644
index 00000000..aeb6d2b7
--- /dev/null
+++ b/custom_metric/starter.py
@@ -0,0 +1,23 @@
+import asyncio
+import uuid
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from custom_metric.workflow import StartTwoActivitiesWorkflow
+
+
+async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ await client.start_workflow(
+ StartTwoActivitiesWorkflow.run,
+ id="execute-activity-workflow-" + str(uuid.uuid4()),
+ task_queue="custom-metric-task-queue",
+ )
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/custom_metric/worker.py b/custom_metric/worker.py
new file mode 100644
index 00000000..21dd9c93
--- /dev/null
+++ b/custom_metric/worker.py
@@ -0,0 +1,73 @@
+import asyncio
+from concurrent.futures import ThreadPoolExecutor
+
+from temporalio import activity
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.runtime import PrometheusConfig, Runtime, TelemetryConfig
+from temporalio.worker import (
+ ActivityInboundInterceptor,
+ ExecuteActivityInput,
+ Interceptor,
+ Worker,
+)
+
+from custom_metric.activity import print_and_sleep
+from custom_metric.workflow import StartTwoActivitiesWorkflow
+
+
+class SimpleWorkerInterceptor(Interceptor):
+ def intercept_activity(
+ self, next: ActivityInboundInterceptor
+ ) -> ActivityInboundInterceptor:
+ return CustomScheduleToStartInterceptor(next)
+
+
+class CustomScheduleToStartInterceptor(ActivityInboundInterceptor):
+ async def execute_activity(self, input: ExecuteActivityInput):
+ schedule_to_start = (
+ activity.info().started_time
+ - activity.info().current_attempt_scheduled_time
+ )
+ # Could do the original schedule time instead of current attempt
+ # schedule_to_start_second_option = activity.info().started_time - activity.info().scheduled_time
+
+ meter = activity.metric_meter()
+ histogram = meter.create_histogram_timedelta(
+ "custom_activity_schedule_to_start_latency",
+ description="Time between activity scheduling and start",
+ unit="duration",
+ )
+ histogram.record(
+ schedule_to_start, {"workflow_type": activity.info().workflow_type or ""}
+ )
+ return await self.next.execute_activity(input)
+
+
+async def main():
+ runtime = Runtime(
+ telemetry=TelemetryConfig(metrics=PrometheusConfig(bind_address="0.0.0.0:9090"))
+ )
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(
+ **config,
+ runtime=runtime,
+ )
+ worker = Worker(
+ client,
+ task_queue="custom-metric-task-queue",
+ interceptors=[SimpleWorkerInterceptor()],
+ workflows=[StartTwoActivitiesWorkflow],
+ activities=[print_and_sleep],
+ # only one activity executor with two concurrently scheduled activities
+ # to force a nontrivial schedule to start times
+ activity_executor=ThreadPoolExecutor(1),
+ max_concurrent_activities=1,
+ )
+
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/custom_metric/workflow.py b/custom_metric/workflow.py
new file mode 100644
index 00000000..cf37823b
--- /dev/null
+++ b/custom_metric/workflow.py
@@ -0,0 +1,25 @@
+import asyncio
+from datetime import timedelta
+
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from custom_metric.activity import print_and_sleep
+
+
+@workflow.defn
+class StartTwoActivitiesWorkflow:
+ @workflow.run
+ async def run(self):
+ # Request two concurrent activities with only one task slot so
+ # we can see nontrivial schedule to start times.
+ activity1 = workflow.execute_activity(
+ print_and_sleep,
+ start_to_close_timeout=timedelta(seconds=5),
+ )
+ activity2 = workflow.execute_activity(
+ print_and_sleep,
+ start_to_close_timeout=timedelta(seconds=5),
+ )
+ await asyncio.gather(activity1, activity2)
+ return None
diff --git a/dsl/README.md b/dsl/README.md
new file mode 100644
index 00000000..5eca4fa4
--- /dev/null
+++ b/dsl/README.md
@@ -0,0 +1,29 @@
+# DSL Sample
+
+This sample shows how to have a workflow interpret/invoke arbitrary steps defined in a DSL. It is similar to the DSL
+samples [in TypeScript](https://github.com/temporalio/samples-typescript/tree/main/dsl-interpreter) and
+[in Go](https://github.com/temporalio/samples-go/tree/main/dsl).
+
+For this sample, the optional `dsl` dependency group must be included. To include, run:
+
+ uv sync --group dsl
+
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from the root directory to start the
+worker:
+
+ uv run dsl/worker.py
+
+This will start the worker. Then, in another terminal, run the following to execute a workflow of steps defined in
+[workflow1.yaml](dsl/workflow1.yaml):
+
+ uv run dsl/starter.py dsl/workflow1.yaml
+
+This will run the workflow and show the final variables that the workflow returns. Looking in the worker terminal, each
+step executed will be visible.
+
+Similarly we can do the same for the more advanced [workflow2.yaml](dsl/workflow2.yaml) file:
+
+ uv run dsl/starter.py dsl/workflow2.yaml
+
+This sample gives a guide of how one can write a workflow to interpret arbitrary steps from a user-provided DSL. Many
+DSL models are more advanced and are more specific to conform to business logic needs.
\ No newline at end of file
diff --git a/dsl/__init__.py b/dsl/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/dsl/activities.py b/dsl/activities.py
new file mode 100644
index 00000000..cba79253
--- /dev/null
+++ b/dsl/activities.py
@@ -0,0 +1,28 @@
+from temporalio import activity
+
+
+class DSLActivities:
+ @activity.defn
+ async def activity1(self, arg: str) -> str:
+ activity.logger.info(f"Executing activity1 with arg: {arg}")
+ return f"[result from activity1: {arg}]"
+
+ @activity.defn
+ async def activity2(self, arg: str) -> str:
+ activity.logger.info(f"Executing activity2 with arg: {arg}")
+ return f"[result from activity2: {arg}]"
+
+ @activity.defn
+ async def activity3(self, arg1: str, arg2: str) -> str:
+ activity.logger.info(f"Executing activity3 with args: {arg1} and {arg2}")
+ return f"[result from activity3: {arg1} {arg2}]"
+
+ @activity.defn
+ async def activity4(self, arg: str) -> str:
+ activity.logger.info(f"Executing activity4 with arg: {arg}")
+ return f"[result from activity4: {arg}]"
+
+ @activity.defn
+ async def activity5(self, arg1: str, arg2: str) -> str:
+ activity.logger.info(f"Executing activity5 with args: {arg1} and {arg2}")
+ return f"[result from activity5: {arg1} {arg2}]"
diff --git a/dsl/starter.py b/dsl/starter.py
new file mode 100644
index 00000000..eb0f328d
--- /dev/null
+++ b/dsl/starter.py
@@ -0,0 +1,49 @@
+import asyncio
+import logging
+import sys
+import uuid
+
+import dacite
+import yaml
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from dsl.workflow import DSLInput, DSLWorkflow
+
+
+async def main(dsl_yaml: str) -> None:
+ # Convert the YAML to our dataclass structure. We use PyYAML + dacite to do
+ # this but it can be done any number of ways.
+ dsl_input = dacite.from_dict(DSLInput, yaml.safe_load(dsl_yaml))
+
+ # Connect client
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Run workflow
+ result = await client.execute_workflow(
+ DSLWorkflow.run,
+ dsl_input,
+ id=f"dsl-workflow-id-{uuid.uuid4()}",
+ task_queue="dsl-task-queue",
+ )
+ logging.info(
+ f"Final variables:\n "
+ + "\n ".join((f"{k}: {v}" for k, v in result.items()))
+ )
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
+
+ # Require the YAML file as an argument. We read this _outside_ of the async
+ # def function because thread-blocking IO should never happen in async def
+ # functions.
+ if len(sys.argv) != 2:
+ raise RuntimeError("Expected single argument for YAML file")
+ with open(sys.argv[1], "r") as yaml_file:
+ dsl_yaml = yaml_file.read()
+
+ # Run
+ asyncio.run(main(dsl_yaml))
diff --git a/dsl/worker.py b/dsl/worker.py
new file mode 100644
index 00000000..e52ec872
--- /dev/null
+++ b/dsl/worker.py
@@ -0,0 +1,47 @@
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from dsl.activities import DSLActivities
+from dsl.workflow import DSLWorkflow
+
+interrupt_event = asyncio.Event()
+
+
+async def main():
+ # Connect client
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Run a worker for the activities and workflow
+ activities = DSLActivities()
+ async with Worker(
+ client,
+ task_queue="dsl-task-queue",
+ activities=[
+ activities.activity1,
+ activities.activity2,
+ activities.activity3,
+ activities.activity4,
+ activities.activity5,
+ ],
+ workflows=[DSLWorkflow],
+ ):
+ # Wait until interrupted
+ logging.info("Worker started, ctrl+c to exit")
+ await interrupt_event.wait()
+ logging.info("Shutting down")
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
+ loop = asyncio.new_event_loop()
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/dsl/workflow.py b/dsl/workflow.py
new file mode 100644
index 00000000..53cf3ec2
--- /dev/null
+++ b/dsl/workflow.py
@@ -0,0 +1,85 @@
+from __future__ import annotations
+
+import asyncio
+import dataclasses
+from dataclasses import dataclass
+from datetime import timedelta
+from typing import Any, Dict, List, Optional, Union
+
+from temporalio import workflow
+
+
+@dataclass
+class DSLInput:
+ root: Statement
+ variables: Dict[str, Any] = dataclasses.field(default_factory=dict)
+
+
+@dataclass
+class ActivityStatement:
+ activity: ActivityInvocation
+
+
+@dataclass
+class ActivityInvocation:
+ name: str
+ arguments: List[str] = dataclasses.field(default_factory=list)
+ result: Optional[str] = None
+
+
+@dataclass
+class SequenceStatement:
+ sequence: Sequence
+
+
+@dataclass
+class Sequence:
+ elements: List[Statement]
+
+
+@dataclass
+class ParallelStatement:
+ parallel: Parallel
+
+
+@dataclass
+class Parallel:
+ branches: List[Statement]
+
+
+Statement = Union[ActivityStatement, SequenceStatement, ParallelStatement]
+
+
+@workflow.defn
+class DSLWorkflow:
+ @workflow.run
+ async def run(self, input: DSLInput) -> Dict[str, Any]:
+ self.variables = dict(input.variables)
+ workflow.logger.info("Running DSL workflow")
+ await self.execute_statement(input.root)
+ workflow.logger.info("DSL workflow completed")
+ return self.variables
+
+ async def execute_statement(self, stmt: Statement) -> None:
+ if isinstance(stmt, ActivityStatement):
+ # Invoke activity loading arguments from variables and optionally
+ # storing result as a variable
+ result = await workflow.execute_activity(
+ stmt.activity.name,
+ args=[self.variables.get(arg, "") for arg in stmt.activity.arguments],
+ start_to_close_timeout=timedelta(minutes=1),
+ )
+ if stmt.activity.result:
+ self.variables[stmt.activity.result] = result
+ elif isinstance(stmt, SequenceStatement):
+ # Execute each statement in order
+ for elem in stmt.sequence.elements:
+ await self.execute_statement(elem)
+ elif isinstance(stmt, ParallelStatement):
+ # Execute all in parallel. Note, this will raise an exception when
+ # the first activity fails and will not cancel the others. We could
+ # store tasks and cancel if we wanted. In newer Python versions this
+ # would use a TaskGroup instead.
+ await asyncio.gather(
+ *[self.execute_statement(branch) for branch in stmt.parallel.branches]
+ )
diff --git a/dsl/workflow1.yaml b/dsl/workflow1.yaml
new file mode 100644
index 00000000..85da5236
--- /dev/null
+++ b/dsl/workflow1.yaml
@@ -0,0 +1,28 @@
+# This sample workflows execute 3 steps in sequence.
+# 1) Activity1, takes arg1 as input, and put result as result1.
+# 2) Activity2, takes result1 as input, and put result as result2.
+# 3) Activity3, takes args2 and result2 as input, and put result as result3.
+
+variables:
+ arg1: value1
+ arg2: value2
+
+root:
+ sequence:
+ elements:
+ - activity:
+ name: activity1
+ arguments:
+ - arg1
+ result: result1
+ - activity:
+ name: activity2
+ arguments:
+ - result1
+ result: result2
+ - activity:
+ name: activity3
+ arguments:
+ - arg2
+ - result2
+ result: result3
\ No newline at end of file
diff --git a/dsl/workflow2.yaml b/dsl/workflow2.yaml
new file mode 100644
index 00000000..cf19fdd6
--- /dev/null
+++ b/dsl/workflow2.yaml
@@ -0,0 +1,58 @@
+# This sample workflow executes 3 steps in sequence.
+# 1) activity1, takes arg1 as input, and put result as result1.
+# 2) it runs a parallel block which runs below sequence branches in parallel
+# 2.1) sequence 1
+# 2.1.1) activity2, takes result1 as input, and put result as result2
+# 2.1.2) activity3, takes arg2 and result2 as input, and put result as result3
+# 2.2) sequence 2
+# 2.2.1) activity4, takes result1 as input, and put result as result4
+# 2.2.2) activity5, takes arg3 and result4 as input, and put result as result5
+# 3) activity3, takes result3 and result5 as input, and put result as result6.
+
+variables:
+ arg1: value1
+ arg2: value2
+ arg3: value3
+
+root:
+ sequence:
+ elements:
+ - activity:
+ name: activity1
+ arguments:
+ - arg1
+ result: result1
+ - parallel:
+ branches:
+ - sequence:
+ elements:
+ - activity:
+ name: activity2
+ arguments:
+ - result1
+ result: result2
+ - activity:
+ name: activity3
+ arguments:
+ - arg2
+ - result2
+ result: result3
+ - sequence:
+ elements:
+ - activity:
+ name: activity4
+ arguments:
+ - result1
+ result: result4
+ - activity:
+ name: activity5
+ arguments:
+ - arg3
+ - result4
+ result: result5
+ - activity:
+ name: activity3
+ arguments:
+ - result3
+ - result5
+ result: result6
\ No newline at end of file
diff --git a/eager_wf_start/README.md b/eager_wf_start/README.md
new file mode 100644
index 00000000..834c6a55
--- /dev/null
+++ b/eager_wf_start/README.md
@@ -0,0 +1,16 @@
+# Eager Workflow Start
+
+This sample shows how to create a workflow that uses Eager Workflow Start.
+
+The target use case is workflows whose first task needs to execute quickly (ex: payment verification in an online checkout workflow). That work typically can't be done directly in the workflow (ex: using web APIs, databases, etc.), and also needs to avoid the overhead of dispatching another task. Using a Local Activity suffices both needs, which this sample demonstrates.
+
+You can read more about Eager Workflow Start in our:
+
+- [Eager Workflow Start blog](https://temporal.io/blog/improving-latency-with-eager-workflow-start)
+- [Worker Performance Docs](https://docs.temporal.io/develop/worker-performance#eager-workflow-start)
+
+To run, first see the main [README.md](../README.md) for prerequisites.
+
+Then run the sample via:
+
+ uv run eager_wf_start/run.py
\ No newline at end of file
diff --git a/eager_wf_start/__init__.py b/eager_wf_start/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/eager_wf_start/activities.py b/eager_wf_start/activities.py
new file mode 100644
index 00000000..6533140f
--- /dev/null
+++ b/eager_wf_start/activities.py
@@ -0,0 +1,6 @@
+from temporalio import activity
+
+
+@activity.defn()
+async def greeting(name: str) -> str:
+ return f"Hello {name}!"
diff --git a/eager_wf_start/run.py b/eager_wf_start/run.py
new file mode 100644
index 00000000..34d807e0
--- /dev/null
+++ b/eager_wf_start/run.py
@@ -0,0 +1,45 @@
+import asyncio
+import uuid
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from eager_wf_start.activities import greeting
+from eager_wf_start.workflows import EagerWorkflow
+
+TASK_QUEUE = "eager-wf-start-task-queue"
+
+
+async def main():
+ # Note that the worker and client run in the same process and share the same client connection.
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ worker = Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[EagerWorkflow],
+ activities=[greeting],
+ )
+
+ # Run worker in the background
+ async with worker:
+ # Start workflow(s) while worker is running
+ wf_handle = await client.start_workflow(
+ EagerWorkflow.run,
+ "Temporal",
+ id=f"eager-workflow-id-{uuid.uuid4()}",
+ task_queue=TASK_QUEUE,
+ request_eager_start=True,
+ )
+
+ # This is an internal flag not intended to be used publicly.
+ # It is used here purely to display that the workflow was eagerly started.
+ print(f"Workflow eagerly started: {wf_handle.__temporal_eagerly_started}")
+ print(await wf_handle.result())
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/eager_wf_start/workflows.py b/eager_wf_start/workflows.py
new file mode 100644
index 00000000..9107d402
--- /dev/null
+++ b/eager_wf_start/workflows.py
@@ -0,0 +1,15 @@
+from datetime import timedelta
+
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from eager_wf_start.activities import greeting
+
+
+@workflow.defn
+class EagerWorkflow:
+ @workflow.run
+ async def run(self, name: str) -> str:
+ return await workflow.execute_local_activity(
+ greeting, name, schedule_to_close_timeout=timedelta(seconds=5)
+ )
diff --git a/encryption/README.md b/encryption/README.md
index b3e1de43..c323e38b 100644
--- a/encryption/README.md
+++ b/encryption/README.md
@@ -4,51 +4,48 @@ This sample shows how to make an encryption codec for end-to-end encryption. It
samples [in TypeScript](https://github.com/temporalio/samples-typescript/tree/main/encryption) and
[in Go](https://github.com/temporalio/samples-go/tree/main/encryption).
+
For this sample, the optional `encryption` dependency group must be included. To include, run:
- poetry install --with encryption
+ uv sync --group encryption
-To run, first see [README.md](../README.md) for prerequisites. Then, run the following from this directory to start the
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from the root directory to start the
worker:
- poetry run python worker.py
+ uv run encryption/worker.py
This will start the worker. Then, in another terminal, run the following to execute the workflow:
- poetry run python starter.py
+ uv run encryption/starter.py
-The workflow should complete with the hello result. To view the workflow, use [tctl](https://docs.temporal.io/tctl-v1/):
+The workflow should complete with the hello result. To view the workflow, use [temporal](https://docs.temporal.io/cli):
- tctl workflow show --workflow_id encryption-workflow-id
+ temporal workflow show --workflow-id encryption-workflow-id
-Note how the input/result look like (with wrapping removed):
+Note how the result looks like (with wrapping removed):
```
- Input:[encoding binary/encrypted: payload encoding is not supported]
- ...
- Result:[encoding binary/encrypted: payload encoding is not supported]
+ Output:[encoding binary/encrypted: payload encoding is not supported]
```
-This is because the data is encrypted and not visible. To make data visible to external Temporal tools like `tctl` and
+This is because the data is encrypted and not visible. To make data visible to external Temporal tools like `temporal` and
the UI, start a codec server in another terminal:
- poetry run python codec_server.py
+ uv run encryption/codec_server.py
-Now with that running, run `tctl` again with the codec endpoint:
+Now with that running, run `temporal` again with the codec endpoint:
- tctl --codec_endpoint http://localhost:8081 workflow show --workflow_id encryption-workflow-id
+ temporal workflow show --workflow-id encryption-workflow-id --codec-endpoint http://localhost:8081
Notice now the output has the unencrypted values:
```
- Input:["Temporal"]
- ...
Result:["Hello, Temporal"]
```
This decryption did not leave the local machine here.
Same case with the web UI. If you go to the web UI, you'll only see encrypted input/results. But, assuming your web UI
-is at `http://localhost:8080`, if you set the "Remote Codec Endpoint" in the web UI to `http://localhost:8081` you can
+is at `http://localhost:8233` (this is the default for the local dev server), if you set the "Remote Codec Endpoint" in the web UI to `http://localhost:8081` you can
then see the unencrypted results. This is possible because CORS settings in the codec server allow the browser to access
the codec server directly over localhost. They can be changed to suit Temporal cloud web UI instead if necessary.
\ No newline at end of file
diff --git a/encryption/codec.py b/encryption/codec.py
index 6aaa58be..9f79526f 100644
--- a/encryption/codec.py
+++ b/encryption/codec.py
@@ -1,4 +1,3 @@
-import base64
import os
from typing import Iterable, List
diff --git a/encryption/codec_server.py b/encryption/codec_server.py
index 4bd04214..3e3029f2 100644
--- a/encryption/codec_server.py
+++ b/encryption/codec_server.py
@@ -12,8 +12,8 @@ def build_codec_server() -> web.Application:
# Cors handler
async def cors_options(req: web.Request) -> web.Response:
resp = web.Response()
- if req.headers.get(hdrs.ORIGIN) == "http://localhost:8080":
- resp.headers[hdrs.ACCESS_CONTROL_ALLOW_ORIGIN] = "http://localhost:8080"
+ if req.headers.get(hdrs.ORIGIN) == "http://localhost:8233":
+ resp.headers[hdrs.ACCESS_CONTROL_ALLOW_ORIGIN] = "http://localhost:8233"
resp.headers[hdrs.ACCESS_CONTROL_ALLOW_METHODS] = "POST"
resp.headers[hdrs.ACCESS_CONTROL_ALLOW_HEADERS] = "content-type,x-namespace"
return resp
diff --git a/encryption/starter.py b/encryption/starter.py
index 4c39f553..f2570936 100644
--- a/encryption/starter.py
+++ b/encryption/starter.py
@@ -3,15 +3,19 @@
import temporalio.converter
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from encryption.codec import EncryptionCodec
from encryption.worker import GreetingWorkflow
async def main():
+ # Load configuration
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
# Connect client
client = await Client.connect(
- "localhost:7233",
+ **config,
# Use the default converter, but change the codec
data_converter=dataclasses.replace(
temporalio.converter.default(), payload_codec=EncryptionCodec()
diff --git a/encryption/worker.py b/encryption/worker.py
index b99a2eab..d3387c70 100644
--- a/encryption/worker.py
+++ b/encryption/worker.py
@@ -4,6 +4,7 @@
import temporalio.converter
from temporalio import workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
from encryption.codec import EncryptionCodec
@@ -20,9 +21,11 @@ async def run(self, name: str) -> str:
async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
# Connect client
client = await Client.connect(
- "localhost:7233",
+ **config,
# Use the default converter, but change the codec
data_converter=dataclasses.replace(
temporalio.converter.default(), payload_codec=EncryptionCodec()
diff --git a/env_config/README.md b/env_config/README.md
new file mode 100644
index 00000000..6496900a
--- /dev/null
+++ b/env_config/README.md
@@ -0,0 +1,43 @@
+# Temporal External Client Configuration Samples
+
+This directory contains Python samples that demonstrate how to use the Temporal SDK's external client configuration feature. This feature allows you to configure a `temporalio.client.Client` using a TOML file and/or programmatic overrides, decoupling connection settings from your application code.
+
+## Prerequisites
+
+To run, first see [README.md](../README.md) for prerequisites.
+
+## Configuration File
+
+The `config.toml` file defines three profiles for different environments:
+
+- `[profile.default]`: A working configuration for local development.
+- `[profile.staging]`: A configuration with an intentionally **incorrect** address (`localhost:9999`) to demonstrate how it can be corrected by an override.
+- `[profile.prod]`: A non-runnable, illustrative-only configuration showing a realistic setup for Temporal Cloud with placeholder credentials. This profile is not used by the samples but serves as a reference.
+
+## Samples
+
+The following Python scripts demonstrate different ways to load and use these configuration profiles. Each runnable sample highlights a unique feature.
+
+### `load_from_file.py`
+
+This sample shows the most common use case: loading the `default` profile from the `config.toml` file.
+
+**To run this sample:**
+
+```bash
+uv run env_config/load_from_file.py
+```
+
+### `load_profile.py`
+
+This sample demonstrates loading the `staging` profile by name (which has an incorrect address) and then correcting the address programmatically. This highlights the recommended approach for overriding configuration values at runtime.
+
+**To run this sample:**
+
+```bash
+uv run env_config/load_profile.py
+```
+
+## Running the Samples
+
+You can run each sample script directly from the root of the `samples-python` repository. Ensure you have the necessary dependencies installed by running `pip install -e .` (or the equivalent for your environment).
\ No newline at end of file
diff --git a/env_config/__init__.py b/env_config/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/env_config/config.toml b/env_config/config.toml
new file mode 100644
index 00000000..81f07f78
--- /dev/null
+++ b/env_config/config.toml
@@ -0,0 +1,40 @@
+# This is a sample configuration file for demonstrating Temporal's environment
+# configuration feature. It defines multiple profiles for different environments,
+# such as local development, production, and staging.
+
+# Default profile for local development
+[profile.default]
+address = "localhost:7233"
+namespace = "default"
+
+# Optional: Add custom gRPC headers
+[profile.default.grpc_meta]
+my-custom-header = "development-value"
+trace-id = "dev-trace-123"
+
+# Staging profile with inline certificate data
+[profile.staging]
+address = "localhost:9999"
+namespace = "staging"
+
+# An example production profile for Temporal Cloud
+[profile.prod]
+address = "your-namespace.a1b2c.tmprl.cloud:7233"
+namespace = "your-namespace"
+# Replace with your actual Temporal Cloud API key
+api_key = "your-api-key-here"
+
+# TLS configuration for production
+[profile.prod.tls]
+# TLS is auto-enabled when an API key is present, but you can configure it
+# explicitly.
+# disabled = false
+
+# Use certificate files for mTLS. Replace with actual paths.
+client_cert_path = "/etc/temporal/certs/client.pem"
+client_key_path = "/etc/temporal/certs/client.key"
+
+# Custom headers for production
+[profile.prod.grpc_meta]
+environment = "production"
+service-version = "v1.2.3"
\ No newline at end of file
diff --git a/env_config/load_from_file.py b/env_config/load_from_file.py
new file mode 100644
index 00000000..ab3bad14
--- /dev/null
+++ b/env_config/load_from_file.py
@@ -0,0 +1,46 @@
+"""
+This sample demonstrates loading the default environment configuration profile
+from a TOML file.
+"""
+
+import asyncio
+from pathlib import Path
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+
+async def main():
+ """
+ Loads the default profile from the config.toml file in this directory.
+ """
+ print("--- Loading default profile from config.toml ---")
+
+ # For this sample to be self-contained, we explicitly provide the path to
+ # the config.toml file included in this directory.
+ # By default though, the config.toml file will be loaded from
+ # ~/.config/temporalio/temporal.toml (or the equivalent standard config directory on your OS).
+ config_file = Path(__file__).parent / "config.toml"
+
+ # load_client_connect_config is a helper that loads a profile and prepares
+ # the config dictionary for Client.connect. By default, it loads the
+ # "default" profile.
+ connect_config = ClientConfig.load_client_connect_config(
+ config_file=str(config_file)
+ )
+
+ print(f"Loaded 'default' profile from {config_file}.")
+ print(f" Address: {connect_config.get('target_host')}")
+ print(f" Namespace: {connect_config.get('namespace')}")
+ print(f" gRPC Metadata: {connect_config.get('rpc_metadata')}")
+
+ print("\nAttempting to connect to client...")
+ try:
+ await Client.connect(**connect_config) # type: ignore
+ print("✅ Client connected successfully!")
+ except Exception as e:
+ print(f"❌ Failed to connect: {e}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/env_config/load_profile.py b/env_config/load_profile.py
new file mode 100644
index 00000000..fe4f51cf
--- /dev/null
+++ b/env_config/load_profile.py
@@ -0,0 +1,52 @@
+"""
+This sample demonstrates loading a named environment configuration profile and
+programmatically overriding its values.
+"""
+
+import asyncio
+from pathlib import Path
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+
+async def main():
+ """
+ Demonstrates loading a named profile and overriding values programmatically.
+ """
+ print("--- Loading 'staging' profile with programmatic overrides ---")
+
+ config_file = Path(__file__).parent / "config.toml"
+ profile_name = "staging"
+
+ print(
+ "The 'staging' profile in config.toml has an incorrect address (localhost:9999)."
+ )
+ print("We'll programmatically override it to the correct address.")
+
+ # Load the 'staging' profile.
+ connect_config = ClientConfig.load_client_connect_config(
+ profile=profile_name,
+ config_file=str(config_file),
+ )
+
+ # Override the target host to the correct address.
+ # This is the recommended way to override configuration values.
+ connect_config["target_host"] = "localhost:7233"
+
+ print(f"\nLoaded '{profile_name}' profile from {config_file} with overrides.")
+ print(
+ f" Address: {connect_config.get('target_host')} (overridden from localhost:9999)"
+ )
+ print(f" Namespace: {connect_config.get('namespace')}")
+
+ print("\nAttempting to connect to client...")
+ try:
+ await Client.connect(**connect_config) # type: ignore
+ print("✅ Client connected successfully!")
+ except Exception as e:
+ print(f"❌ Failed to connect: {e}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/gevent_async/README.md b/gevent_async/README.md
new file mode 100644
index 00000000..0c66a54b
--- /dev/null
+++ b/gevent_async/README.md
@@ -0,0 +1,28 @@
+# Gevent Sample
+
+This sample shows how to run Temporal in an environment that gevent has patched.
+
+Gevent is built to patch Python libraries to attempt to seamlessly convert threaded code into coroutine-based code.
+However, it is well known within the gevent community that it does not work well with `asyncio`, which is the modern
+Python approach to coroutines. Temporal leverages `asyncio` which means by default it is incompatible with gevent. Users
+are encouraged to abandon gevent in favor of more modern approaches where they can but it is not always possible.
+
+This sample shows how to use a customized gevent executor to run `asyncio` Temporal clients, workers, activities, and
+workflows.
+
+For this sample, the optional `gevent` dependency group must be included. To include, run:
+
+ uv sync --group gevent
+
+To run the sample, first see [README.md](../README.md) for prerequisites such as having a localhost Temporal server
+running. Then, run the following from the root directory to start the worker:
+
+ uv run gevent_async/worker.py
+
+This will start the worker. The worker has a workflow and two activities, one `asyncio` based and one gevent based. Now
+in another terminal, run the following to execute the workflow:
+
+ uv run gevent_async/starter.py
+
+The workflow should run and complete with the hello result. Note on the worker terminal there will be logs of the
+workflow and activity executions.
\ No newline at end of file
diff --git a/gevent_async/__init__.py b/gevent_async/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gevent_async/activity.py b/gevent_async/activity.py
new file mode 100644
index 00000000..2fbabe4b
--- /dev/null
+++ b/gevent_async/activity.py
@@ -0,0 +1,25 @@
+from dataclasses import dataclass
+
+import gevent
+from temporalio import activity
+
+
+@dataclass
+class ComposeGreetingInput:
+ greeting: str
+ name: str
+
+
+@activity.defn
+async def compose_greeting_async(input: ComposeGreetingInput) -> str:
+ activity.logger.info(f"Running async activity with parameter {input}")
+ return f"{input.greeting}, {input.name}!"
+
+
+@activity.defn
+def compose_greeting_sync(input: ComposeGreetingInput) -> str:
+ activity.logger.info(
+ f"Running sync activity with parameter {input}, "
+ f"in greenlet: {gevent.getcurrent()}"
+ )
+ return f"{input.greeting}, {input.name}!"
diff --git a/gevent_async/executor.py b/gevent_async/executor.py
new file mode 100644
index 00000000..16c8896e
--- /dev/null
+++ b/gevent_async/executor.py
@@ -0,0 +1,41 @@
+import functools
+from concurrent.futures import Future
+from typing import Callable, TypeVar
+
+from gevent import threadpool
+from typing_extensions import ParamSpec
+
+T = TypeVar("T")
+P = ParamSpec("P")
+
+
+class GeventExecutor(threadpool.ThreadPoolExecutor):
+ def submit(
+ self, fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs
+ ) -> Future[T]:
+ # Gevent's returned futures do not map well to Python futures, so we
+ # must translate. We can't just use set_result/set_exception because
+ # done callbacks are not always called in gevent's case and it doesn't
+ # seem to support cancel, so we instead wrap the caller function.
+ python_fut: Future[T] = Future()
+
+ @functools.wraps(fn)
+ def wrapper(*w_args: P.args, **w_kwargs: P.kwargs) -> None:
+ try:
+ result = fn(*w_args, **w_kwargs)
+ # Swallow InvalidStateError in case Python future was cancelled
+ try:
+ python_fut.set_result(result)
+ except:
+ pass
+ except Exception as exc:
+ # Swallow InvalidStateError in case Python future was cancelled
+ try:
+ python_fut.set_exception(exc)
+ except:
+ pass
+
+ # Submit our wrapper to gevent
+ super().submit(wrapper, *args, **kwargs)
+ # Return Python future to user
+ return python_fut
diff --git a/gevent_async/starter.py b/gevent_async/starter.py
new file mode 100644
index 00000000..010803e5
--- /dev/null
+++ b/gevent_async/starter.py
@@ -0,0 +1,43 @@
+# Init gevent
+from gevent import monkey
+
+monkey.patch_all()
+
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from gevent_async import workflow
+from gevent_async.executor import GeventExecutor
+
+
+def main():
+ logging.basicConfig(level=logging.INFO)
+
+ # Create single-worker gevent executor and run asyncio.run(async_main()) in
+ # it, waiting for result. This executor cannot be used for anything else in
+ # Temporal, it is just a single thread for running asyncio.
+ with GeventExecutor(max_workers=1) as executor:
+ executor.submit(asyncio.run, async_main()).result()
+
+
+async def async_main():
+ # Connect client
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Run workflow
+ result = await client.execute_workflow(
+ workflow.GreetingWorkflow.run,
+ "Temporal",
+ id="gevent_async-workflow-id",
+ task_queue="gevent_async-task-queue",
+ )
+ logging.info(f"Workflow result: {result}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/gevent_async/test/__init__.py b/gevent_async/test/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gevent_async/test/run_combined.py b/gevent_async/test/run_combined.py
new file mode 100644
index 00000000..4946cb16
--- /dev/null
+++ b/gevent_async/test/run_combined.py
@@ -0,0 +1,56 @@
+# Init gevent
+from gevent import monkey
+
+monkey.patch_all()
+
+import asyncio
+import logging
+
+from temporalio.testing import WorkflowEnvironment
+from temporalio.worker import Worker
+
+from gevent_async import activity, workflow
+from gevent_async.executor import GeventExecutor
+
+# This basically combines ../worker.py and ../starter.py for use by CI to
+# confirm this works in all environments
+
+
+def main():
+ logging.basicConfig(level=logging.INFO)
+ with GeventExecutor(max_workers=1) as executor:
+ executor.submit(asyncio.run, async_main()).result()
+
+
+async def async_main():
+ logging.info("Starting local server")
+ async with await WorkflowEnvironment.start_local() as env:
+ logging.info("Starting worker")
+ with GeventExecutor(max_workers=200) as executor:
+ async with Worker(
+ env.client,
+ task_queue="gevent_async-task-queue",
+ workflows=[workflow.GreetingWorkflow],
+ activities=[
+ activity.compose_greeting_async,
+ activity.compose_greeting_sync,
+ ],
+ activity_executor=executor,
+ workflow_task_executor=executor,
+ max_concurrent_activities=100,
+ max_concurrent_workflow_tasks=100,
+ ):
+ logging.info("Running workflow")
+ result = await env.client.execute_workflow(
+ workflow.GreetingWorkflow.run,
+ "Temporal",
+ id="gevent_async-workflow-id",
+ task_queue="gevent_async-task-queue",
+ )
+ if result != "Hello, Temporal!":
+ raise RuntimeError(f"Unexpected result: {result}")
+ logging.info(f"Workflow complete, result: {result}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/gevent_async/worker.py b/gevent_async/worker.py
new file mode 100644
index 00000000..db419399
--- /dev/null
+++ b/gevent_async/worker.py
@@ -0,0 +1,77 @@
+# Init gevent
+from gevent import monkey
+
+monkey.patch_all()
+
+import asyncio
+import logging
+import signal
+
+import gevent
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from gevent_async import activity, workflow
+from gevent_async.executor import GeventExecutor
+
+
+def main():
+ logging.basicConfig(level=logging.INFO)
+
+ # Create single-worker gevent executor and run asyncio.run(async_main()) in
+ # it, waiting for result. This executor cannot be used for anything else in
+ # Temporal, it is just a single thread for running asyncio. This means that
+ # inside of async_main we must create another executor specifically for
+ # executing activity and workflow tasks.
+ with GeventExecutor(max_workers=1) as executor:
+ executor.submit(asyncio.run, async_main()).result()
+
+
+async def async_main():
+ # Create ctrl+c handler. We do this by telling gevent on SIGINT to set the
+ # asyncio event. But asyncio calls are not thread safe, so we have to invoke
+ # it via call_soon_threadsafe.
+ interrupt_event = asyncio.Event()
+ gevent.signal_handler(
+ signal.SIGINT,
+ asyncio.get_running_loop().call_soon_threadsafe,
+ interrupt_event.set,
+ )
+
+ # Connect client
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Create an executor for use by Temporal. This cannot be the outer one
+ # running this async main. The max_workers here needs to have enough room to
+ # support the max concurrent activities/workflows settings.
+ with GeventExecutor(max_workers=200) as executor:
+ # Run a worker for the workflow and activities
+ async with Worker(
+ client,
+ task_queue="gevent_async-task-queue",
+ workflows=[workflow.GreetingWorkflow],
+ activities=[
+ activity.compose_greeting_async,
+ activity.compose_greeting_sync,
+ ],
+ # Set the executor for activities (only used for non-async
+ # activities) and workflow tasks
+ activity_executor=executor,
+ workflow_task_executor=executor,
+ # Set the max concurrent activities/workflows. These are the same as
+ # the defaults, but this makes it clear that the 100 + 100 = 200 for
+ # max_workers settings.
+ max_concurrent_activities=100,
+ max_concurrent_workflow_tasks=100,
+ ):
+ # Wait until interrupted
+ logging.info("Worker started, ctrl+c to exit")
+ await interrupt_event.wait()
+ logging.info("Shutting down")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/gevent_async/workflow.py b/gevent_async/workflow.py
new file mode 100644
index 00000000..bc192639
--- /dev/null
+++ b/gevent_async/workflow.py
@@ -0,0 +1,34 @@
+from datetime import timedelta
+
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from gevent_async.activity import (
+ ComposeGreetingInput,
+ compose_greeting_async,
+ compose_greeting_sync,
+ )
+
+
+@workflow.defn
+class GreetingWorkflow:
+ @workflow.run
+ async def run(self, name: str) -> str:
+ workflow.logger.info("Running workflow with parameter %s" % name)
+
+ # Run an async and a sync activity
+ async_res = await workflow.execute_activity(
+ compose_greeting_async,
+ ComposeGreetingInput("Hello", name),
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+ sync_res = await workflow.execute_activity(
+ compose_greeting_sync,
+ ComposeGreetingInput("Hello", name),
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+
+ # Confirm the same, return one
+ if async_res != sync_res:
+ raise ValueError("Results are not the same")
+ return sync_res
diff --git a/hello/README.md b/hello/README.md
index 191b4621..7b5ca6eb 100644
--- a/hello/README.md
+++ b/hello/README.md
@@ -2,32 +2,34 @@
These samples show basic workflow and activity features.
-To run, first see [README.md](../README.md) for prerequisites. Then, run the following from this directory to run the
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from the root directory to run the
`hello_activity.py` sample:
- poetry run python hello_activity.py
+ uv run hello/hello_activity.py
The result will be:
Result: Hello, World!
-Replace `hello_activity.py` in the command with any other example filename to run it instead.
+Replace `hello/hello_activity.py` in the command with any other example filename (with the `hello/` prefix) to run it instead.
## Samples
* [hello_activity](hello_activity.py) - Execute an activity from a workflow.
+* [hello_activity_async](hello_activity_async.py) - Execute an async activity from a workflow.
* [hello_activity_choice](hello_activity_choice.py) - Execute certain activities inside a workflow based on dynamic
input.
* [hello_activity_method](hello_activity_method.py) - Demonstrate an activity that is an instance method on a
class and can access class state.
+* [hello_activity_heartbeat](hello_activity_heartbeat.py) - Demonstrate usage of heartbeat timeouts.
* [hello_activity_multiprocess](hello_activity_multiprocess.py) - Execute a synchronous activity on a process pool.
* [hello_activity_retry](hello_activity_retry.py) - Demonstrate activity retry by failing until a certain number of
attempts.
-* [hello_activity_threaded](hello_activity_threaded.py) - Execute a synchronous activity on a thread pool.
* [hello_async_activity_completion](hello_async_activity_completion.py) - Complete an activity outside of the function
that was called.
* [hello_cancellation](hello_cancellation.py) - Manually react to cancellation inside workflows and activities.
+* [hello_change_log_level](hello_change_log_level.py) - Change the level of workflow task failure from WARN to ERROR.
* [hello_child_workflow](hello_child_workflow.py) - Execute a child workflow from a workflow.
* [hello_continue_as_new](hello_continue_as_new.py) - Use continue as new to restart a workflow.
* [hello_cron](hello_cron.py) - Execute a workflow once a minute.
@@ -37,7 +39,13 @@ Replace `hello_activity.py` in the command with any other example filename to ru
* [hello_mtls](hello_mtls.py) - Accept URL, namespace, and certificate info as CLI args and use mTLS for connecting to
server.
* [hello_parallel_activity](hello_parallel_activity.py) - Execute multiple activities at once.
+* [hello_patch](hello_patch.py) - Demonstrates how to patch executions.
* [hello_query](hello_query.py) - Invoke queries on a workflow.
* [hello_search_attributes](hello_search_attributes.py) - Start workflow with search attributes then change while
running.
* [hello_signal](hello_signal.py) - Send signals to a workflow.
+* [hello_update](hello_update.py) - **Send a request to and a response from a client to a workflow execution.**
+
+Note: To enable the workflow update, set the `frontend.enableUpdateWorkflowExecution` dynamic config value to true.
+
+ temporal server start-dev --dynamic-config-value frontend.enableUpdateWorkflowExecution=true
diff --git a/hello/hello_activity.py b/hello/hello_activity.py
index 6615b55d..c7972c2e 100644
--- a/hello/hello_activity.py
+++ b/hello/hello_activity.py
@@ -1,10 +1,11 @@
import asyncio
-import logging
+from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import timedelta
from temporalio import activity, workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@@ -19,7 +20,7 @@ class ComposeGreetingInput:
# Basic activity that logs and does string concatenation
@activity.defn
-async def compose_greeting(input: ComposeGreetingInput) -> str:
+def compose_greeting(input: ComposeGreetingInput) -> str:
activity.logger.info("Running activity with parameter %s" % input)
return f"{input.greeting}, {input.name}!"
@@ -38,11 +39,16 @@ async def run(self, name: str) -> str:
async def main():
- # Uncomment the line below to see logging
+ # Uncomment the lines below to see logging output
+ # import logging
# logging.basicConfig(level=logging.INFO)
+ # Load configuration
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
# Start client
- client = await Client.connect("localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
@@ -50,8 +56,11 @@ async def main():
task_queue="hello-activity-task-queue",
workflows=[GreetingWorkflow],
activities=[compose_greeting],
+ # Non-async activities require an executor;
+ # a thread pool executor is recommended.
+ # This same thread pool could be passed to multiple workers if desired.
+ activity_executor=ThreadPoolExecutor(5),
):
-
# While the worker is running, use the client to run the workflow and
# print out its result. Note, in many production setups, the client
# would be in a completely separate process from the worker.
diff --git a/hello/hello_activity_async.py b/hello/hello_activity_async.py
new file mode 100644
index 00000000..47b5bd79
--- /dev/null
+++ b/hello/hello_activity_async.py
@@ -0,0 +1,73 @@
+import asyncio
+from dataclasses import dataclass
+from datetime import timedelta
+
+from temporalio import activity, workflow
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+
+# While we could use multiple parameters in the activity, Temporal strongly
+# encourages using a single dataclass instead which can have fields added to it
+# in a backwards-compatible way.
+@dataclass
+class ComposeGreetingInput:
+ greeting: str
+ name: str
+
+
+# Basic activity that logs and does string concatenation
+@activity.defn
+async def compose_greeting(input: ComposeGreetingInput) -> str:
+ activity.logger.info("Running activity with parameter %s" % input)
+ return f"{input.greeting}, {input.name}!"
+
+
+# Basic workflow that logs and invokes an activity
+@workflow.defn
+class GreetingWorkflow:
+ @workflow.run
+ async def run(self, name: str) -> str:
+ workflow.logger.info("Running workflow with parameter %s" % name)
+ return await workflow.execute_activity(
+ compose_greeting,
+ ComposeGreetingInput("Hello", name),
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+
+
+async def main():
+ # Uncomment the lines below to see logging output
+ # import logging
+ # logging.basicConfig(level=logging.INFO)
+
+ # Start client
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Run a worker for the workflow
+ async with Worker(
+ client,
+ task_queue="hello-activity-task-queue",
+ workflows=[GreetingWorkflow],
+ activities=[compose_greeting],
+ # If the worker is only running async activities, you don't need
+ # to supply an activity executor because they run in
+ # the worker's event loop.
+ ):
+ # While the worker is running, use the client to run the workflow and
+ # print out its result. Note, in many production setups, the client
+ # would be in a completely separate process from the worker.
+ result = await client.execute_workflow(
+ GreetingWorkflow.run,
+ "World",
+ id="hello-activity-workflow-id",
+ task_queue="hello-activity-task-queue",
+ )
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/hello/hello_activity_choice.py b/hello/hello_activity_choice.py
index 56b7b7e6..6da9fe69 100644
--- a/hello/hello_activity_choice.py
+++ b/hello/hello_activity_choice.py
@@ -1,4 +1,5 @@
import asyncio
+from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import timedelta
from enum import IntEnum
@@ -6,28 +7,29 @@
from temporalio import activity, workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
# Activities that will be called by the workflow
@activity.defn
-async def order_apples(amount: int) -> str:
+def order_apples(amount: int) -> str:
return f"Ordered {amount} Apples..."
@activity.defn
-async def order_bananas(amount: int) -> str:
+def order_bananas(amount: int) -> str:
return f"Ordered {amount} Bananas..."
@activity.defn
-async def order_cherries(amount: int) -> str:
+def order_cherries(amount: int) -> str:
return f"Ordered {amount} Cherries..."
@activity.defn
-async def order_oranges(amount: int) -> str:
+def order_oranges(amount: int) -> str:
return f"Ordered {amount} Oranges..."
@@ -54,50 +56,36 @@ class ShoppingList:
@workflow.defn
class PurchaseFruitsWorkflow:
@workflow.run
- async def run(self, list: ShoppingList) -> str:
+ async def run(self, shopping_list: ShoppingList) -> str:
# Order each thing on the list
ordered: List[str] = []
- for item in list.items:
+ for item in shopping_list.items:
if item.fruit is Fruit.APPLE:
- ordered.append(
- await workflow.execute_activity(
- order_apples,
- item.amount,
- start_to_close_timeout=timedelta(seconds=5),
- )
- )
+ order_function = order_apples
elif item.fruit is Fruit.BANANA:
- ordered.append(
- await workflow.execute_activity(
- order_bananas,
- item.amount,
- start_to_close_timeout=timedelta(seconds=5),
- )
- )
+ order_function = order_bananas
elif item.fruit is Fruit.CHERRY:
- ordered.append(
- await workflow.execute_activity(
- order_cherries,
- item.amount,
- start_to_close_timeout=timedelta(seconds=5),
- )
- )
+ order_function = order_cherries
elif item.fruit is Fruit.ORANGE:
- ordered.append(
- await workflow.execute_activity(
- order_oranges,
- item.amount,
- start_to_close_timeout=timedelta(seconds=5),
- )
- )
+ order_function = order_oranges
else:
raise ValueError(f"Unrecognized fruit: {item.fruit}")
+ ordered.append(
+ await workflow.execute_activity(
+ order_function,
+ item.amount,
+ start_to_close_timeout=timedelta(seconds=5),
+ )
+ )
return "".join(ordered)
async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
# Start client
- client = await Client.connect("localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
@@ -105,8 +93,8 @@ async def main():
task_queue="hello-activity-choice-task-queue",
workflows=[PurchaseFruitsWorkflow],
activities=[order_apples, order_bananas, order_cherries, order_oranges],
+ activity_executor=ThreadPoolExecutor(5),
):
-
# While the worker is running, use the client to run the workflow and
# print out its result. Note, in many production setups, the client
# would be in a completely separate process from the worker.
diff --git a/hello/hello_activity_threaded.py b/hello/hello_activity_heartbeat.py
similarity index 75%
rename from hello/hello_activity_threaded.py
rename to hello/hello_activity_heartbeat.py
index e7bf8344..2b80f9a6 100644
--- a/hello/hello_activity_threaded.py
+++ b/hello/hello_activity_heartbeat.py
@@ -1,5 +1,4 @@
import asyncio
-import threading
import time
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
@@ -7,6 +6,7 @@
from temporalio import activity, workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@@ -21,7 +21,7 @@ def compose_greeting(input: ComposeGreetingInput) -> str:
# We'll wait for 3 seconds, heartbeating in between (like all long-running
# activities should do), then return the greeting
for _ in range(0, 3):
- print(f"Heartbeating activity on thread {threading.get_ident()}")
+ print(f"Heartbeating activity")
activity.heartbeat()
time.sleep(1)
return f"{input.greeting}, {input.name}!"
@@ -42,30 +42,28 @@ async def run(self, name: str) -> str:
async def main():
# Start client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
client,
- task_queue="hello-activity-threaded-task-queue",
+ task_queue="hello-activity-heartbeating-task-queue",
workflows=[GreetingWorkflow],
activities=[compose_greeting],
- # Synchronous activities are not allowed unless we provide some kind of
- # executor. This same thread pool could be passed to multiple workers if
- # desired.
activity_executor=ThreadPoolExecutor(5),
):
-
# While the worker is running, use the client to run the workflow and
# print out its result. Note, in many production setups, the client
# would be in a completely separate process from the worker.
result = await client.execute_workflow(
GreetingWorkflow.run,
"World",
- id="hello-activity-threaded-workflow-id",
- task_queue="hello-activity-threaded-task-queue",
+ id="hello-activity-heartbeating-workflow-id",
+ task_queue="hello-activity-heartbeating-task-queue",
)
- print(f"Result on thread {threading.get_ident()}: {result}")
+ print(f"Result: {result}")
if __name__ == "__main__":
diff --git a/hello/hello_activity_method.py b/hello/hello_activity_method.py
index db527263..d87bc8e7 100644
--- a/hello/hello_activity_method.py
+++ b/hello/hello_activity_method.py
@@ -3,6 +3,7 @@
from temporalio import activity, workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@@ -32,7 +33,9 @@ async def run(self) -> None:
async def main():
# Start client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Create our database client that can then be used in the activity
db_client = MyDatabaseClient()
@@ -47,7 +50,6 @@ async def main():
workflows=[MyWorkflow],
activities=[my_activities.do_database_thing],
):
-
# While the worker is running, use the client to run the workflow and
# print out its result. Note, in many production setups, the client
# would be in a completely separate process from the worker.
diff --git a/hello/hello_activity_multiprocess.py b/hello/hello_activity_multiprocess.py
index 6630234d..c7793c2c 100644
--- a/hello/hello_activity_multiprocess.py
+++ b/hello/hello_activity_multiprocess.py
@@ -8,6 +8,7 @@
from temporalio import activity, workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import SharedStateManager, Worker
@@ -43,7 +44,9 @@ async def run(self, name: str) -> str:
async def main():
# Start client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
@@ -65,7 +68,6 @@ async def main():
multiprocessing.Manager()
),
):
-
# While the worker is running, use the client to run the workflow and
# print out its result. Note, in many production setups, the client
# would be in a completely separate process from the worker.
diff --git a/hello/hello_activity_retry.py b/hello/hello_activity_retry.py
index 233f9613..105aa847 100644
--- a/hello/hello_activity_retry.py
+++ b/hello/hello_activity_retry.py
@@ -1,10 +1,12 @@
import asyncio
+from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import timedelta
from temporalio import activity, workflow
from temporalio.client import Client
from temporalio.common import RetryPolicy
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@@ -15,7 +17,7 @@ class ComposeGreetingInput:
@activity.defn
-async def compose_greeting(input: ComposeGreetingInput) -> str:
+def compose_greeting(input: ComposeGreetingInput) -> str:
print(f"Invoking activity, attempt number {activity.info().attempt}")
# Fail the first 3 attempts, succeed the 4th
if activity.info().attempt < 4:
@@ -44,7 +46,9 @@ async def run(self, name: str) -> str:
async def main():
# Start client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
@@ -52,8 +56,8 @@ async def main():
task_queue="hello-activity-retry-task-queue",
workflows=[GreetingWorkflow],
activities=[compose_greeting],
+ activity_executor=ThreadPoolExecutor(5),
):
-
# While the worker is running, use the client to run the workflow and
# print out its result. Note, in many production setups, the client
# would be in a completely separate process from the worker.
diff --git a/hello/hello_async_activity_completion.py b/hello/hello_async_activity_completion.py
index 10aa89df..e777c49f 100644
--- a/hello/hello_async_activity_completion.py
+++ b/hello/hello_async_activity_completion.py
@@ -4,6 +4,7 @@
from temporalio import activity, workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@@ -68,7 +69,9 @@ async def run(self, name: str) -> str:
async def main():
# Start client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
composer = GreetingComposer(client)
@@ -78,7 +81,6 @@ async def main():
workflows=[GreetingWorkflow],
activities=[composer.compose_greeting],
):
-
# While the worker is running, use the client to run the workflow and
# print out its result. Note, in many production setups, the client
# would be in a completely separate process from the worker.
diff --git a/hello/hello_cancellation.py b/hello/hello_cancellation.py
index 3467893c..a74bc2b4 100644
--- a/hello/hello_cancellation.py
+++ b/hello/hello_cancellation.py
@@ -1,29 +1,33 @@
import asyncio
+import time
import traceback
+from concurrent.futures import ThreadPoolExecutor
from datetime import timedelta
from typing import NoReturn
from temporalio import activity, workflow
from temporalio.client import Client, WorkflowFailureError
+from temporalio.envconfig import ClientConfig
+from temporalio.exceptions import CancelledError
from temporalio.worker import Worker
@activity.defn
-async def never_complete_activity() -> NoReturn:
+def never_complete_activity() -> NoReturn:
# All long-running activities should heartbeat. Heartbeat is how
# cancellation is delivered from the server.
try:
while True:
print("Heartbeating activity")
activity.heartbeat()
- await asyncio.sleep(1)
- except asyncio.CancelledError:
+ time.sleep(1)
+ except CancelledError:
print("Activity cancelled")
raise
@activity.defn
-async def cleanup_activity() -> None:
+def cleanup_activity() -> None:
print("Executing cleanup activity")
@@ -47,8 +51,11 @@ async def run(self) -> None:
async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
# Start client
- client = await Client.connect("localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
@@ -56,8 +63,8 @@ async def main():
task_queue="hello-cancellation-task-queue",
workflows=[CancellationWorkflow],
activities=[never_complete_activity, cleanup_activity],
+ activity_executor=ThreadPoolExecutor(5),
):
-
# While the worker is running, use the client to start the workflow.
# Note, in many production setups, the client would be in a completely
# separate process from the worker.
diff --git a/hello/hello_change_log_level.py b/hello/hello_change_log_level.py
new file mode 100644
index 00000000..4b7697f4
--- /dev/null
+++ b/hello/hello_change_log_level.py
@@ -0,0 +1,71 @@
+"""
+Changes the log level of workflow task failures from WARN to ERROR.
+
+Note that the __temporal_error_identifier attribute was added in
+version 1.13.0 of the Python SDK.
+"""
+
+import asyncio
+import logging
+import sys
+
+from temporalio import workflow
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+# --- Begin logging set‑up ----------------------------------------------------------
+logging.basicConfig(
+ stream=sys.stdout,
+ level=logging.INFO,
+ format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
+)
+
+
+class CustomLogFilter(logging.Filter):
+ def filter(self, record: logging.LogRecord) -> bool:
+ # Note that the __temporal_error_identifier attribute was added in
+ # version 1.13.0 of the Python SDK.
+ if (
+ hasattr(record, "__temporal_error_identifier")
+ and getattr(record, "__temporal_error_identifier") == "WorkflowTaskFailure"
+ ):
+ record.levelno = logging.ERROR
+ record.levelname = logging.getLevelName(logging.ERROR)
+ return True
+
+
+for h in logging.getLogger().handlers:
+ h.addFilter(CustomLogFilter())
+# --- End logging set‑up ----------------------------------------------------------
+
+
+LOG_MESSAGE = "This error is an experiment to check the log level"
+
+
+@workflow.defn
+class GreetingWorkflow:
+ @workflow.run
+ async def run(self):
+ raise RuntimeError(LOG_MESSAGE)
+
+
+async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ async with Worker(
+ client,
+ task_queue="hello-change-log-level-task-queue",
+ workflows=[GreetingWorkflow],
+ ):
+ await client.execute_workflow(
+ GreetingWorkflow.run,
+ id="hello-change-log-level-workflow-id",
+ task_queue="hello-change-log-level-task-queue",
+ )
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/hello/hello_child_workflow.py b/hello/hello_child_workflow.py
index fad9af81..136b788f 100644
--- a/hello/hello_child_workflow.py
+++ b/hello/hello_child_workflow.py
@@ -3,6 +3,7 @@
from temporalio import workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@@ -13,7 +14,7 @@ class ComposeGreetingInput:
@workflow.defn
-class ComposeGreeting:
+class ComposeGreetingWorkflow:
@workflow.run
async def run(self, input: ComposeGreetingInput) -> str:
return f"{input.greeting}, {input.name}!"
@@ -24,23 +25,25 @@ class GreetingWorkflow:
@workflow.run
async def run(self, name: str) -> str:
return await workflow.execute_child_workflow(
- ComposeGreeting.run,
+ ComposeGreetingWorkflow.run,
ComposeGreetingInput("Hello", name),
id="hello-child-workflow-workflow-child-id",
)
async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
# Start client
- client = await Client.connect("localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
client,
task_queue="hello-child-workflow-task-queue",
- workflows=[GreetingWorkflow, ComposeGreeting],
+ workflows=[GreetingWorkflow, ComposeGreetingWorkflow],
):
-
# While the worker is running, use the client to run the workflow and
# print out its result. Note, in many production setups, the client
# would be in a completely separate process from the worker.
diff --git a/hello/hello_continue_as_new.py b/hello/hello_continue_as_new.py
index 586aac1d..a899c7fc 100644
--- a/hello/hello_continue_as_new.py
+++ b/hello/hello_continue_as_new.py
@@ -3,6 +3,7 @@
from temporalio import workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@@ -22,7 +23,9 @@ async def main():
logging.basicConfig(level=logging.INFO)
# Start client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
@@ -30,7 +33,6 @@ async def main():
task_queue="hello-continue-as-new-task-queue",
workflows=[LoopingWorkflow],
):
-
# While the worker is running, use the client to run the workflow. Note,
# in many production setups, the client would be in a completely
# separate process from the worker.
diff --git a/hello/hello_cron.py b/hello/hello_cron.py
index 68b26099..1ca29ea1 100644
--- a/hello/hello_cron.py
+++ b/hello/hello_cron.py
@@ -1,9 +1,11 @@
import asyncio
+from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import timedelta
from temporalio import activity, workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@@ -14,7 +16,7 @@ class ComposeGreetingInput:
@activity.defn
-async def compose_greeting(input: ComposeGreetingInput) -> str:
+def compose_greeting(input: ComposeGreetingInput) -> str:
return f"{input.greeting}, {input.name}!"
@@ -32,7 +34,9 @@ async def run(self, name: str) -> None:
async def main():
# Start client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
@@ -40,8 +44,8 @@ async def main():
task_queue="hello-cron-task-queue",
workflows=[GreetingWorkflow],
activities=[compose_greeting],
+ activity_executor=ThreadPoolExecutor(5),
):
-
print("Running workflow once a minute")
# While the worker is running, use the client to start the workflow.
diff --git a/hello/hello_exception.py b/hello/hello_exception.py
index bfb198d5..6e94bf7c 100644
--- a/hello/hello_exception.py
+++ b/hello/hello_exception.py
@@ -1,5 +1,6 @@
import asyncio
import logging
+from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import timedelta
from typing import NoReturn, Optional
@@ -7,6 +8,7 @@
from temporalio import activity, workflow
from temporalio.client import Client, WorkflowFailureError
from temporalio.common import RetryPolicy
+from temporalio.envconfig import ClientConfig
from temporalio.exceptions import FailureError
from temporalio.worker import Worker
@@ -18,7 +20,7 @@ class ComposeGreetingInput:
@activity.defn
-async def compose_greeting(input: ComposeGreetingInput) -> NoReturn:
+def compose_greeting(input: ComposeGreetingInput) -> NoReturn:
# Always raise exception
raise RuntimeError(f"Greeting exception: {input.greeting}, {input.name}!")
@@ -38,7 +40,9 @@ async def run(self, name: str) -> str:
async def main():
# Start client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
@@ -46,8 +50,8 @@ async def main():
task_queue="hello-exception-task-queue",
workflows=[GreetingWorkflow],
activities=[compose_greeting],
+ activity_executor=ThreadPoolExecutor(5),
):
-
# While the worker is running, use the client to run the workflow and
# print out its result. Note, in many production setups, the client
# would be in a completely separate process from the worker.
diff --git a/hello/hello_local_activity.py b/hello/hello_local_activity.py
index 08c1d9f2..8954ea42 100644
--- a/hello/hello_local_activity.py
+++ b/hello/hello_local_activity.py
@@ -1,9 +1,11 @@
import asyncio
+from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import timedelta
from temporalio import activity, workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@@ -14,7 +16,7 @@ class ComposeGreetingInput:
@activity.defn
-async def compose_greeting(input: ComposeGreetingInput) -> str:
+def compose_greeting(input: ComposeGreetingInput) -> str:
return f"{input.greeting}, {input.name}!"
@@ -31,7 +33,9 @@ async def run(self, name: str) -> str:
async def main():
# Start client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
@@ -39,8 +43,8 @@ async def main():
task_queue="hello-local-activity-task-queue",
workflows=[GreetingWorkflow],
activities=[compose_greeting],
+ activity_executor=ThreadPoolExecutor(5),
):
-
# While the worker is running, use the client to run the workflow and
# print out its result. Note, in many production setups, the client
# would be in a completely separate process from the worker.
diff --git a/hello/hello_mtls.py b/hello/hello_mtls.py
index f613fba4..b88c5f85 100644
--- a/hello/hello_mtls.py
+++ b/hello/hello_mtls.py
@@ -1,11 +1,13 @@
import argparse
import asyncio
+from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import timedelta
from typing import Optional
from temporalio import activity, workflow
-from temporalio.client import Client, TLSConfig
+from temporalio.client import Client
+from temporalio.service import TLSConfig
from temporalio.worker import Worker
@@ -17,7 +19,7 @@ class ComposeGreetingInput:
# Basic activity that logs and does string concatenation
@activity.defn
-async def compose_greeting(input: ComposeGreetingInput) -> str:
+def compose_greeting(input: ComposeGreetingInput) -> str:
return f"{input.greeting}, {input.name}!"
@@ -78,8 +80,8 @@ async def main():
task_queue="hello-mtls-task-queue",
workflows=[GreetingWorkflow],
activities=[compose_greeting],
+ activity_executor=ThreadPoolExecutor(5),
):
-
# While the worker is running, use the client to run the workflow and
# print out its result. Note, in many production setups, the client
# would be in a completely separate process from the worker.
diff --git a/hello/hello_parallel_activity.py b/hello/hello_parallel_activity.py
index 9de09846..42931a70 100644
--- a/hello/hello_parallel_activity.py
+++ b/hello/hello_parallel_activity.py
@@ -1,14 +1,16 @@
import asyncio
+from concurrent.futures import ThreadPoolExecutor
from datetime import timedelta
from typing import List
from temporalio import activity, workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@activity.defn
-async def say_hello_activity(name: str) -> str:
+def say_hello_activity(name: str) -> str:
return f"Hello, {name}!"
@@ -40,7 +42,9 @@ async def run(self) -> List[str]:
async def main():
# Start client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
@@ -48,8 +52,8 @@ async def main():
task_queue="hello-parallel-activity-task-queue",
workflows=[SayHelloWorkflow],
activities=[say_hello_activity],
+ activity_executor=ThreadPoolExecutor(10),
):
-
# While the worker is running, use the client to run the workflow and
# print out its result. Note, in many production setups, the client
# would be in a completely separate process from the worker.
diff --git a/hello/hello_patch.py b/hello/hello_patch.py
index 43371d4f..a26e5474 100644
--- a/hello/hello_patch.py
+++ b/hello/hello_patch.py
@@ -1,11 +1,12 @@
import asyncio
-import logging
import sys
+from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import timedelta
from temporalio import activity, exceptions, workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@@ -20,7 +21,7 @@ class ComposeGreetingInput:
# Basic activity that logs and does string concatenation
@activity.defn
-async def compose_greeting(input: ComposeGreetingInput) -> str:
+def compose_greeting(input: ComposeGreetingInput) -> str:
activity.logger.info("Running activity with parameter %s" % input)
return f"{input.greeting}, {input.name}!"
@@ -96,11 +97,14 @@ async def main():
version = sys.argv[1]
- # Uncomment the line below to see logging
+ # Uncomment the lines below to see logging output
+ # import logging
# logging.basicConfig(level=logging.INFO)
# Start client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Set workflow_class to the proper class based on version
workflow_class = ""
@@ -123,6 +127,7 @@ async def main():
task_queue="hello-patch-task-queue",
workflows=[workflow_class], # type: ignore
activities=[compose_greeting],
+ activity_executor=ThreadPoolExecutor(5),
):
try:
result = await client.execute_workflow(
diff --git a/hello/hello_query.py b/hello/hello_query.py
index 8deb30ba..d1bc54aa 100644
--- a/hello/hello_query.py
+++ b/hello/hello_query.py
@@ -2,6 +2,7 @@
from temporalio import workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@@ -26,8 +27,11 @@ def greeting(self) -> str:
async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
# Start client
- client = await Client.connect("localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
@@ -35,7 +39,6 @@ async def main():
task_queue="hello-query-task-queue",
workflows=[GreetingWorkflow],
):
-
# While the worker is running, use the client to start the workflow.
# Note, in many production setups, the client would be in a completely
# separate process from the worker.
diff --git a/hello/hello_search_attributes.py b/hello/hello_search_attributes.py
index 6892fb71..d6d1a205 100644
--- a/hello/hello_search_attributes.py
+++ b/hello/hello_search_attributes.py
@@ -1,10 +1,8 @@
import asyncio
-from typing import List
from temporalio import workflow
-from temporalio.client import Client, WorkflowExecutionDescription
-from temporalio.common import SearchAttributeValues
-from temporalio.converter import default as default_converter
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@@ -19,7 +17,9 @@ async def run(self) -> None:
async def main():
# Start client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
@@ -27,7 +27,6 @@ async def main():
task_queue="hello-search-attributes-task-queue",
workflows=[GreetingWorkflow],
):
-
# While the worker is running, use the client to start the workflow.
# Note, in many production setups, the client would be in a completely
# separate process from the worker.
diff --git a/hello/hello_signal.py b/hello/hello_signal.py
index a4f9b554..02b9d3ce 100644
--- a/hello/hello_signal.py
+++ b/hello/hello_signal.py
@@ -3,6 +3,7 @@
from temporalio import workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@@ -40,8 +41,11 @@ def exit(self) -> None:
async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
# Start client
- client = await Client.connect("localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
@@ -49,7 +53,6 @@ async def main():
task_queue="hello-signal-task-queue",
workflows=[GreetingWorkflow],
):
-
# While the worker is running, use the client to start the workflow.
# Note, in many production setups, the client would be in a completely
# separate process from the worker.
diff --git a/hello/hello_update.py b/hello/hello_update.py
new file mode 100644
index 00000000..4daa250a
--- /dev/null
+++ b/hello/hello_update.py
@@ -0,0 +1,57 @@
+import asyncio
+
+from temporalio import workflow
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+
+@workflow.defn
+class GreetingWorkflow:
+ def __init__(self):
+ self.is_complete = False
+
+ @workflow.run
+ async def run(self) -> str:
+ await workflow.wait_condition(lambda: self.is_complete)
+ return "Hello, World!"
+
+ @workflow.update
+ async def update_workflow_status(self) -> str:
+ self.is_complete = True
+ return "Workflow status updated"
+
+
+async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Run a worker for the workflow
+ async with Worker(
+ client,
+ task_queue="update-workflow-task-queue",
+ workflows=[GreetingWorkflow],
+ ):
+ # While the worker is running, use the client to start the workflow.
+ # Note, in many production setups, the client would be in a completely
+ # separate process from the worker.
+ handle = await client.start_workflow(
+ GreetingWorkflow.run,
+ id="hello-update-workflow-id",
+ task_queue="update-workflow-task-queue",
+ )
+
+ # Perform the update for GreetingWorkflow
+ update_result = await handle.execute_update(
+ GreetingWorkflow.update_workflow_status
+ )
+ print(f"Update Result: {update_result}")
+
+ # Get the result for GreetingWorkflow
+ result = await handle.result()
+ print(f"Workflow Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/hello_nexus/README.md b/hello_nexus/README.md
new file mode 100644
index 00000000..e8067ec3
--- /dev/null
+++ b/hello_nexus/README.md
@@ -0,0 +1,37 @@
+This sample shows how to define a Nexus service, implement the operation handlers, and
+call the operations from a workflow.
+
+### Sample directory structure
+
+- [service.py](./service.py) - shared Nexus service definition
+- [caller](./caller) - a caller workflow that executes Nexus operations, together with a worker and starter code
+- [handler](./handler) - Nexus operation handlers, together with a workflow used by one of the Nexus operations, and a worker that polls for both workflow and Nexus tasks.
+
+
+### Instructions
+
+Start a Temporal server. (See the main samples repo [README](../README.md)).
+
+Run the following to create the caller and handler namespaces, and the Nexus endpoint:
+
+```
+temporal operator namespace create --namespace hello-nexus-basic-handler-namespace
+temporal operator namespace create --namespace hello-nexus-basic-caller-namespace
+
+temporal operator nexus endpoint create \
+ --name hello-nexus-basic-nexus-endpoint \
+ --target-namespace hello-nexus-basic-handler-namespace \
+ --target-task-queue my-handler-task-queue \
+ --description-file hello_nexus/endpoint_description.md
+```
+
+In one terminal, run the Temporal worker in the handler namespace:
+```
+uv run hello_nexus/handler/worker.py
+```
+
+In another terminal, run the Temporal worker in the caller namespace and start the caller
+workflow:
+```
+uv run hello_nexus/caller/app.py
+```
diff --git a/hello_nexus/__init__.py b/hello_nexus/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/hello_nexus/caller/__init__.py b/hello_nexus/caller/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/hello_nexus/caller/app.py b/hello_nexus/caller/app.py
new file mode 100644
index 00000000..639456fa
--- /dev/null
+++ b/hello_nexus/caller/app.py
@@ -0,0 +1,46 @@
+import asyncio
+import uuid
+from typing import Optional
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from hello_nexus.caller.workflows import CallerWorkflow
+from hello_nexus.service import MyOutput
+
+NAMESPACE = "hello-nexus-basic-caller-namespace"
+TASK_QUEUE = "hello-nexus-basic-caller-task-queue"
+
+
+async def execute_caller_workflow(
+ client: Optional[Client] = None,
+) -> tuple[MyOutput, MyOutput]:
+ if not client:
+ config = ClientConfig.load_client_connect_config()
+ # Override the namespace from config file.
+ config.setdefault("target_host", "localhost:7233")
+ config.setdefault("namespace", NAMESPACE)
+ client = await Client.connect(**config)
+
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[CallerWorkflow],
+ ):
+ return await client.execute_workflow(
+ CallerWorkflow.run,
+ arg="world",
+ id=str(uuid.uuid4()),
+ task_queue=TASK_QUEUE,
+ )
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ try:
+ results = loop.run_until_complete(execute_caller_workflow())
+ for output in results:
+ print(output.message)
+ except KeyboardInterrupt:
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/hello_nexus/caller/workflows.py b/hello_nexus/caller/workflows.py
new file mode 100644
index 00000000..2016f99f
--- /dev/null
+++ b/hello_nexus/caller/workflows.py
@@ -0,0 +1,35 @@
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from hello_nexus.service import MyInput, MyNexusService, MyOutput
+
+NEXUS_ENDPOINT = "hello-nexus-basic-nexus-endpoint"
+
+
+# This is a workflow that calls two nexus operations.
+@workflow.defn
+class CallerWorkflow:
+ # An __init__ method is always optional on a workflow class. Here we use it to set the
+ # nexus client, but that could alternatively be done in the run method.
+ def __init__(self):
+ self.nexus_client = workflow.create_nexus_client(
+ service=MyNexusService,
+ endpoint=NEXUS_ENDPOINT,
+ )
+
+ # The workflow run method invokes two nexus operations.
+ @workflow.run
+ async def run(self, name: str) -> tuple[MyOutput, MyOutput]:
+ # Start the nexus operation and wait for the result in one go, using execute_operation.
+ op_1_result = await self.nexus_client.execute_operation(
+ MyNexusService.my_sync_operation,
+ MyInput(name),
+ )
+ # Alternatively, you can use start_operation to obtain the operation handle and
+ # then `await` the handle to obtain the result.
+ op_2_handle = await self.nexus_client.start_operation(
+ MyNexusService.my_workflow_run_operation,
+ MyInput(name),
+ )
+ op_2_result = await op_2_handle
+ return op_1_result, op_2_result
diff --git a/hello_nexus/endpoint_description.md b/hello_nexus/endpoint_description.md
new file mode 100644
index 00000000..9a381cd0
--- /dev/null
+++ b/hello_nexus/endpoint_description.md
@@ -0,0 +1,3 @@
+## Service: [MyNexusService](https://github.com/temporalio/samples-python/blob/main/hello_nexus/basic/service.py)
+ - operation: `my_sync_operation`
+ - operation: `my_workflow_run_operation`
diff --git a/hello_nexus/handler/__init__.py b/hello_nexus/handler/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/hello_nexus/handler/service_handler.py b/hello_nexus/handler/service_handler.py
new file mode 100644
index 00000000..1295abd1
--- /dev/null
+++ b/hello_nexus/handler/service_handler.py
@@ -0,0 +1,49 @@
+"""
+This file demonstrates how to implement a Nexus service.
+"""
+
+from __future__ import annotations
+
+import uuid
+
+import nexusrpc
+from temporalio import nexus
+
+from hello_nexus.handler.workflows import WorkflowStartedByNexusOperation
+from hello_nexus.service import MyInput, MyNexusService, MyOutput
+
+
+@nexusrpc.handler.service_handler(service=MyNexusService)
+class MyNexusServiceHandler:
+ # You can create an __init__ method accepting what is needed by your operation
+ # handlers to handle requests. You typically instantiate your service handler class
+ # when starting your worker. See hello_nexus/basic/handler/worker.py.
+
+ # This is a nexus operation that is backed by a Temporal workflow. The start method
+ # starts a workflow, and returns a nexus operation token. Meanwhile, the workflow
+ # executes in the background; Temporal server takes care of delivering the eventual
+ # workflow result (success or failure) to the calling workflow.
+ #
+ # The token will be used by the caller if it subsequently wants to cancel the Nexus
+ # operation.
+ @nexus.workflow_run_operation
+ async def my_workflow_run_operation(
+ self, ctx: nexus.WorkflowRunOperationContext, input: MyInput
+ ) -> nexus.WorkflowHandle[MyOutput]:
+ return await ctx.start_workflow(
+ WorkflowStartedByNexusOperation.run,
+ input,
+ id=str(uuid.uuid4()),
+ )
+
+ # This is a Nexus operation that responds synchronously to all requests. That means
+ # that unlike the workflow run operation above, in this case the `start` method
+ # returns the final operation result.
+ #
+ # Sync operations are free to make arbitrary network calls, or perform CPU-bound
+ # computations. Total execution duration must not exceed 10s.
+ @nexusrpc.handler.sync_operation
+ async def my_sync_operation(
+ self, ctx: nexusrpc.handler.StartOperationContext, input: MyInput
+ ) -> MyOutput:
+ return MyOutput(message=f"Hello {input.name} from sync operation!")
diff --git a/hello_nexus/handler/worker.py b/hello_nexus/handler/worker.py
new file mode 100644
index 00000000..ded9c5ab
--- /dev/null
+++ b/hello_nexus/handler/worker.py
@@ -0,0 +1,49 @@
+import asyncio
+import logging
+from typing import Optional
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from hello_nexus.handler.service_handler import MyNexusServiceHandler
+from hello_nexus.handler.workflows import WorkflowStartedByNexusOperation
+
+interrupt_event = asyncio.Event()
+
+NAMESPACE = "hello-nexus-basic-handler-namespace"
+TASK_QUEUE = "my-handler-task-queue"
+
+
+async def main(client: Optional[Client] = None):
+ logging.basicConfig(level=logging.INFO)
+
+ if not client:
+ config = ClientConfig.load_client_connect_config()
+ # Override the address and namespace from the config file.
+ config.setdefault("target_host", "localhost:7233")
+ config.setdefault("namespace", NAMESPACE)
+ client = await Client.connect(**config)
+
+ # Start the worker, passing the Nexus service handler instance, in addition to the
+ # workflow classes that are started by your nexus operations, and any activities
+ # needed. This Worker will poll for both workflow tasks and Nexus tasks (this example
+ # doesn't use any activities).
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[WorkflowStartedByNexusOperation],
+ nexus_service_handlers=[MyNexusServiceHandler()],
+ ):
+ logging.info("Worker started, ctrl+c to exit")
+ await interrupt_event.wait()
+ logging.info("Shutting down")
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/hello_nexus/handler/workflows.py b/hello_nexus/handler/workflows.py
new file mode 100644
index 00000000..a41b29ef
--- /dev/null
+++ b/hello_nexus/handler/workflows.py
@@ -0,0 +1,12 @@
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from hello_nexus.service import MyInput, MyOutput
+
+
+# This is the workflow that is started by the `my_workflow_run_operation` nexus operation.
+@workflow.defn
+class WorkflowStartedByNexusOperation:
+ @workflow.run
+ async def run(self, input: MyInput) -> MyOutput:
+ return MyOutput(message=f"Hello {input.name} from workflow run operation!")
diff --git a/hello_nexus/service.py b/hello_nexus/service.py
new file mode 100644
index 00000000..352375ca
--- /dev/null
+++ b/hello_nexus/service.py
@@ -0,0 +1,34 @@
+"""
+This is a Nexus service definition.
+
+A service definition defines a Nexus service as a named collection of operations, each
+with input and output types. It does not implement operation handling: see the service
+handler and operation handlers in hello_nexus.handler.nexus_service for that.
+
+A Nexus service definition is used by Nexus callers (e.g. a Temporal workflow) to create
+type-safe clients, and it is used by Nexus handlers to validate that they implement
+correctly-named operation handlers with the correct input and output types.
+
+The service defined in this file exposes two operations: my_sync_operation and
+my_workflow_run_operation.
+"""
+
+from dataclasses import dataclass
+
+import nexusrpc
+
+
+@dataclass
+class MyInput:
+ name: str
+
+
+@dataclass
+class MyOutput:
+ message: str
+
+
+@nexusrpc.service
+class MyNexusService:
+ my_sync_operation: nexusrpc.Operation[MyInput, MyOutput]
+ my_workflow_run_operation: nexusrpc.Operation[MyInput, MyOutput]
diff --git a/hello_standalone_activity/README.md b/hello_standalone_activity/README.md
new file mode 100644
index 00000000..ebd63931
--- /dev/null
+++ b/hello_standalone_activity/README.md
@@ -0,0 +1,115 @@
+# Standalone Activity
+
+This sample shows how to execute Activities directly from a Temporal Client, without a Workflow.
+
+For full documentation, see [Standalone Activities - Python SDK](https://docs.temporal.io/develop/python/standalone-activities).
+
+### Sample directory structure
+
+- [my_activity.py](./my_activity.py) - Activity definition with `@activity.defn`
+- [worker.py](./worker.py) - Worker that registers and runs the Activity
+- [execute_activity.py](./execute_activity.py) - Execute a Standalone Activity and wait for the result
+- [start_activity.py](./start_activity.py) - Start a Standalone Activity, get a handle, then wait for the result
+- [list_activities.py](./list_activities.py) - List Standalone Activity Executions
+- [count_activities.py](./count_activities.py) - Count Standalone Activity Executions
+
+### Quickstart
+
+**1. Start the Temporal dev server**
+
+```bash
+temporal server start-dev
+```
+
+**2. Run the Worker** (in a separate terminal)
+
+```bash
+uv run hello_standalone_activity/worker.py
+```
+
+**3. Execute a Standalone Activity** (in a separate terminal)
+
+Execute and wait for the result:
+
+```bash
+uv run hello_standalone_activity/execute_activity.py
+```
+
+Or use the Temporal CLI:
+
+```bash
+temporal activity execute \
+ --type compose_greeting \
+ --activity-id my-standalone-activity-id \
+ --task-queue my-standalone-activity-task-queue \
+ --start-to-close-timeout 10s \
+ --input '{"greeting": "Hello", "name": "World"}'
+```
+
+**4. Start a Standalone Activity (without waiting)**
+
+Start, get a handle, then wait for the result:
+
+```bash
+uv run hello_standalone_activity/start_activity.py
+```
+
+Or use the Temporal CLI:
+
+```bash
+temporal activity start \
+ --type compose_greeting \
+ --activity-id my-standalone-activity-id \
+ --task-queue my-standalone-activity-task-queue \
+ --start-to-close-timeout 10s \
+ --input '{"greeting": "Hello", "name": "World"}'
+```
+
+**5. List Standalone Activities**
+
+```bash
+uv run hello_standalone_activity/list_activities.py
+```
+
+Or use the Temporal CLI:
+
+```bash
+temporal activity list --query "TaskQueue = 'my-standalone-activity-task-queue'"
+```
+
+Note: `list` and `count` are only available in the [Standalone Activity prerelease CLI](https://github.com/temporalio/cli/releases/tag/v1.6.2-standalone-activity).
+
+**6. Count Standalone Activities**
+
+```bash
+uv run hello_standalone_activity/count_activities.py
+```
+
+Or use the Temporal CLI:
+
+```bash
+temporal activity count --query "TaskQueue = 'my-standalone-activity-task-queue'"
+```
+
+### Temporal Cloud
+
+The same code works against Temporal Cloud - just set environment variables. No code changes needed.
+
+**Connect with mTLS:**
+
+```bash
+export TEMPORAL_ADDRESS=..tmprl.cloud:7233
+export TEMPORAL_NAMESPACE=.
+export TEMPORAL_TLS_CLIENT_CERT_PATH='path/to/your/client.pem'
+export TEMPORAL_TLS_CLIENT_KEY_PATH='path/to/your/client.key'
+```
+
+**Connect with an API key:**
+
+```bash
+export TEMPORAL_ADDRESS=..api.temporal.io:7233
+export TEMPORAL_NAMESPACE=.
+export TEMPORAL_API_KEY=
+```
+
+Then run the worker and starter as shown above.
diff --git a/hello_standalone_activity/__init__.py b/hello_standalone_activity/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/hello_standalone_activity/count_activities.py b/hello_standalone_activity/count_activities.py
new file mode 100644
index 00000000..14a89e7e
--- /dev/null
+++ b/hello_standalone_activity/count_activities.py
@@ -0,0 +1,23 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+
+async def my_application():
+ connect_config = ClientConfig.load_client_connect_config()
+ connect_config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**connect_config)
+
+ resp = await client.count_activities(
+ query="TaskQueue = 'my-standalone-activity-task-queue'",
+ )
+
+ print("Total activities:", resp.count)
+
+ for group in resp.groups:
+ print(f"Group {group.group_values}: {group.count}")
+
+
+if __name__ == "__main__":
+ asyncio.run(my_application())
diff --git a/hello_standalone_activity/execute_activity.py b/hello_standalone_activity/execute_activity.py
new file mode 100644
index 00000000..529a1feb
--- /dev/null
+++ b/hello_standalone_activity/execute_activity.py
@@ -0,0 +1,26 @@
+import asyncio
+from datetime import timedelta
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from hello_standalone_activity.my_activity import ComposeGreetingInput, compose_greeting
+
+
+async def my_application():
+ connect_config = ClientConfig.load_client_connect_config()
+ connect_config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**connect_config)
+
+ activity_result = await client.execute_activity(
+ compose_greeting,
+ args=[ComposeGreetingInput("Hello", "World")],
+ id="my-standalone-activity-id",
+ task_queue="my-standalone-activity-task-queue",
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+ print(f"Activity result: {activity_result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(my_application())
diff --git a/hello_standalone_activity/list_activities.py b/hello_standalone_activity/list_activities.py
new file mode 100644
index 00000000..ef7af969
--- /dev/null
+++ b/hello_standalone_activity/list_activities.py
@@ -0,0 +1,23 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+
+async def my_application():
+ connect_config = ClientConfig.load_client_connect_config()
+ connect_config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**connect_config)
+
+ activities = client.list_activities(
+ query="TaskQueue = 'my-standalone-activity-task-queue'",
+ )
+
+ async for info in activities:
+ print(
+ f"ActivityID: {info.activity_id}, Type: {info.activity_type}, Status: {info.status}"
+ )
+
+
+if __name__ == "__main__":
+ asyncio.run(my_application())
diff --git a/hello_standalone_activity/my_activity.py b/hello_standalone_activity/my_activity.py
new file mode 100644
index 00000000..086b08a0
--- /dev/null
+++ b/hello_standalone_activity/my_activity.py
@@ -0,0 +1,15 @@
+from dataclasses import dataclass
+
+from temporalio import activity
+
+
+@dataclass
+class ComposeGreetingInput:
+ greeting: str
+ name: str
+
+
+@activity.defn
+def compose_greeting(input: ComposeGreetingInput) -> str:
+ activity.logger.info("Running activity with parameter %s" % input)
+ return f"{input.greeting}, {input.name}!"
diff --git a/hello_standalone_activity/start_activity.py b/hello_standalone_activity/start_activity.py
new file mode 100644
index 00000000..8aa9fa54
--- /dev/null
+++ b/hello_standalone_activity/start_activity.py
@@ -0,0 +1,31 @@
+import asyncio
+from datetime import timedelta
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from hello_standalone_activity.my_activity import ComposeGreetingInput, compose_greeting
+
+
+async def my_application():
+ connect_config = ClientConfig.load_client_connect_config()
+ connect_config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**connect_config)
+
+ # Start the activity without waiting for the result
+ activity_handle = await client.start_activity(
+ compose_greeting,
+ args=[ComposeGreetingInput("Hello", "World")],
+ id="my-standalone-activity-id",
+ task_queue="my-standalone-activity-task-queue",
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+ print(f"Started activity: {activity_handle.id}")
+
+ # Wait for the result
+ activity_result = await activity_handle.result()
+ print(f"Activity result: {activity_result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(my_application())
diff --git a/hello_standalone_activity/worker.py b/hello_standalone_activity/worker.py
new file mode 100644
index 00000000..d093cc03
--- /dev/null
+++ b/hello_standalone_activity/worker.py
@@ -0,0 +1,26 @@
+import asyncio
+from concurrent.futures import ThreadPoolExecutor
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from hello_standalone_activity.my_activity import compose_greeting
+
+
+async def main():
+ connect_config = ClientConfig.load_client_connect_config()
+ connect_config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**connect_config)
+ worker = Worker(
+ client,
+ task_queue="my-standalone-activity-task-queue",
+ activities=[compose_greeting],
+ activity_executor=ThreadPoolExecutor(5),
+ )
+ print("worker running...", end="", flush=True)
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/langchain/README.md b/langchain/README.md
new file mode 100644
index 00000000..506a379d
--- /dev/null
+++ b/langchain/README.md
@@ -0,0 +1,30 @@
+# LangChain Sample
+
+This sample shows you how you can use Temporal to orchestrate workflows for [LangChain](https://www.langchain.com). It includes an interceptor that makes LangSmith traces work seamlessly across Temporal clients, workflows and activities.
+
+For this sample, the optional `langchain` dependency group must be included. To include, run:
+
+ uv sync --group langchain
+
+Export your [OpenAI API key](https://platform.openai.com/api-keys) as an environment variable. Replace `YOUR_API_KEY` with your actual OpenAI API key.
+
+ export OPENAI_API_KEY='...'
+
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from the root directory to start the
+worker:
+
+ uv run langchain/worker.py
+
+This will start the worker. Then, in another terminal, run the following to execute a workflow:
+
+ uv run langchain/starter.py
+
+Then, in another terminal, run the following command to translate a phrase:
+
+ curl -X POST "http://localhost:8000/translate?phrase=hello%20world&language1=Spanish&language2=French&language3=Russian"
+
+Which should produce some output like:
+
+ {"translations":{"French":"Bonjour tout le monde","Russian":"Привет, мир","Spanish":"Hola mundo"}}
+
+Check [LangSmith](https://smith.langchain.com/) for the corresponding trace.
\ No newline at end of file
diff --git a/langchain/activities.py b/langchain/activities.py
new file mode 100644
index 00000000..a9dec70c
--- /dev/null
+++ b/langchain/activities.py
@@ -0,0 +1,34 @@
+from dataclasses import dataclass
+
+from langchain_openai import ChatOpenAI
+from temporalio import activity
+
+from langchain.prompts import ChatPromptTemplate
+
+
+@dataclass
+class TranslateParams:
+ phrase: str
+ language: str
+
+
+@activity.defn
+async def translate_phrase(params: TranslateParams) -> str:
+ # LangChain setup
+ template = """You are a helpful assistant who translates between languages.
+ Translate the following phrase into the specified language: {phrase}
+ Language: {language}"""
+ chat_prompt = ChatPromptTemplate.from_messages(
+ [
+ ("system", template),
+ ("human", "Translate"),
+ ]
+ )
+ chain = chat_prompt | ChatOpenAI()
+ # Use the asynchronous invoke method
+ return (
+ dict(
+ await chain.ainvoke({"phrase": params.phrase, "language": params.language})
+ ).get("content")
+ or ""
+ )
diff --git a/langchain/langchain_interceptor.py b/langchain/langchain_interceptor.py
new file mode 100644
index 00000000..bb230b6b
--- /dev/null
+++ b/langchain/langchain_interceptor.py
@@ -0,0 +1,181 @@
+from __future__ import annotations
+
+from typing import Any, Mapping, Protocol, Type
+
+from temporalio import activity, api, client, converter, worker, workflow
+
+with workflow.unsafe.imports_passed_through():
+ from contextlib import contextmanager
+
+ from langsmith import trace, tracing_context
+ from langsmith.run_helpers import get_current_run_tree
+
+# Header key for LangChain context
+LANGCHAIN_CONTEXT_KEY = "langchain-context"
+
+
+class _InputWithHeaders(Protocol):
+ headers: Mapping[str, api.common.v1.Payload]
+
+
+def set_header_from_context(
+ input: _InputWithHeaders, payload_converter: converter.PayloadConverter
+) -> None:
+ # Get current LangChain run tree
+ run_tree = get_current_run_tree()
+ if run_tree:
+ headers = run_tree.to_headers()
+ input.headers = {
+ **input.headers,
+ LANGCHAIN_CONTEXT_KEY: payload_converter.to_payload(headers),
+ }
+
+
+@contextmanager
+def context_from_header(
+ input: _InputWithHeaders, payload_converter: converter.PayloadConverter
+):
+ payload = input.headers.get(LANGCHAIN_CONTEXT_KEY)
+ if payload:
+ run_tree = payload_converter.from_payload(payload, dict)
+ # Set the run tree in the current context
+ with tracing_context(parent=run_tree):
+ yield
+ else:
+ yield
+
+
+class LangChainContextPropagationInterceptor(client.Interceptor, worker.Interceptor):
+ """Interceptor that propagates LangChain context through Temporal."""
+
+ def __init__(
+ self,
+ payload_converter: converter.PayloadConverter = converter.default().payload_converter,
+ ) -> None:
+ self._payload_converter = payload_converter
+
+ def intercept_client(
+ self, next: client.OutboundInterceptor
+ ) -> client.OutboundInterceptor:
+ return _LangChainContextPropagationClientOutboundInterceptor(
+ next, self._payload_converter
+ )
+
+ def intercept_activity(
+ self, next: worker.ActivityInboundInterceptor
+ ) -> worker.ActivityInboundInterceptor:
+ return _LangChainContextPropagationActivityInboundInterceptor(next)
+
+ def workflow_interceptor_class(
+ self, input: worker.WorkflowInterceptorClassInput
+ ) -> Type[_LangChainContextPropagationWorkflowInboundInterceptor]:
+ return _LangChainContextPropagationWorkflowInboundInterceptor
+
+
+class _LangChainContextPropagationClientOutboundInterceptor(client.OutboundInterceptor):
+ def __init__(
+ self,
+ next: client.OutboundInterceptor,
+ payload_converter: converter.PayloadConverter,
+ ) -> None:
+ super().__init__(next)
+ self._payload_converter = payload_converter
+
+ async def start_workflow(
+ self, input: client.StartWorkflowInput
+ ) -> client.WorkflowHandle[Any, Any]:
+ with trace(name=f"start_workflow:{input.workflow}"):
+ set_header_from_context(input, self._payload_converter)
+ return await super().start_workflow(input)
+
+
+class _LangChainContextPropagationActivityInboundInterceptor(
+ worker.ActivityInboundInterceptor
+):
+ async def execute_activity(self, input: worker.ExecuteActivityInput) -> Any:
+ if isinstance(input.fn, str):
+ name = input.fn
+ elif callable(input.fn):
+ defn = activity._Definition.from_callable(input.fn)
+ name = (
+ defn.name if defn is not None and defn.name is not None else "unknown"
+ )
+ else:
+ name = "unknown"
+
+ with context_from_header(input, activity.payload_converter()):
+ with trace(name=f"execute_activity:{name}"):
+ return await self.next.execute_activity(input)
+
+
+class _LangChainContextPropagationWorkflowInboundInterceptor(
+ worker.WorkflowInboundInterceptor
+):
+ def init(self, outbound: worker.WorkflowOutboundInterceptor) -> None:
+ self.next.init(
+ _LangChainContextPropagationWorkflowOutboundInterceptor(outbound)
+ )
+
+ async def execute_workflow(self, input: worker.ExecuteWorkflowInput) -> Any:
+ if isinstance(input.run_fn, str):
+ name = input.run_fn
+ elif callable(input.run_fn):
+ defn = workflow._Definition.from_run_fn(input.run_fn)
+ name = (
+ defn.name if defn is not None and defn.name is not None else "unknown"
+ )
+ else:
+ name = "unknown"
+
+ with context_from_header(input, workflow.payload_converter()):
+ # This is a sandbox friendly way to write
+ # with trace(...):
+ # return await self.next.execute_workflow(input)
+ with workflow.unsafe.sandbox_unrestricted():
+ t = trace(
+ name=f"execute_workflow:{name}", run_id=workflow.info().run_id
+ )
+ with workflow.unsafe.imports_passed_through():
+ t.__enter__()
+ try:
+ return await self.next.execute_workflow(input)
+ finally:
+ with workflow.unsafe.sandbox_unrestricted():
+ # Cannot use __aexit__ because it's internally uses
+ # loop.run_in_executor which is not available in the sandbox
+ t.__exit__()
+
+
+class _LangChainContextPropagationWorkflowOutboundInterceptor(
+ worker.WorkflowOutboundInterceptor
+):
+ def start_activity(
+ self, input: worker.StartActivityInput
+ ) -> workflow.ActivityHandle:
+ with workflow.unsafe.sandbox_unrestricted():
+ t = trace(name=f"start_activity:{input.activity}", run_id=workflow.uuid4())
+ with workflow.unsafe.imports_passed_through():
+ t.__enter__()
+ try:
+ set_header_from_context(input, workflow.payload_converter())
+ return self.next.start_activity(input)
+ finally:
+ with workflow.unsafe.sandbox_unrestricted():
+ t.__exit__()
+
+ async def start_child_workflow(
+ self, input: worker.StartChildWorkflowInput
+ ) -> workflow.ChildWorkflowHandle:
+ with workflow.unsafe.sandbox_unrestricted():
+ t = trace(
+ name=f"start_child_workflow:{input.workflow}", run_id=workflow.uuid4()
+ )
+ with workflow.unsafe.imports_passed_through():
+ t.__enter__()
+
+ try:
+ set_header_from_context(input, workflow.payload_converter())
+ return await self.next.start_child_workflow(input)
+ finally:
+ with workflow.unsafe.sandbox_unrestricted():
+ t.__exit__()
diff --git a/langchain/starter.py b/langchain/starter.py
new file mode 100644
index 00000000..6d9e00c2
--- /dev/null
+++ b/langchain/starter.py
@@ -0,0 +1,47 @@
+from contextlib import asynccontextmanager
+from typing import List
+from uuid import uuid4
+
+import uvicorn
+from activities import TranslateParams
+from fastapi import FastAPI, HTTPException
+from langchain_interceptor import LangChainContextPropagationInterceptor
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from workflow import LangChainWorkflow, TranslateWorkflowParams
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
+ client = await Client.connect(
+ **config,
+ interceptors=[LangChainContextPropagationInterceptor()],
+ )
+ yield
+
+
+app = FastAPI(lifespan=lifespan)
+
+
+@app.post("/translate")
+async def translate(phrase: str, language1: str, language2: str, language3: str):
+ languages = [language1, language2, language3]
+ client = app.state.temporal_client
+ try:
+ result = await client.execute_workflow(
+ LangChainWorkflow.run,
+ TranslateWorkflowParams(phrase, languages),
+ id=f"langchain-translation-{uuid4()}",
+ task_queue="langchain-task-queue",
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+ return {"translations": result}
+
+
+if __name__ == "__main__":
+ uvicorn.run(app, host="localhost", port=8000)
diff --git a/langchain/worker.py b/langchain/worker.py
new file mode 100644
index 00000000..b7fb7741
--- /dev/null
+++ b/langchain/worker.py
@@ -0,0 +1,44 @@
+import asyncio
+
+from activities import translate_phrase
+from langchain_interceptor import LangChainContextPropagationInterceptor
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+from workflow import LangChainChildWorkflow, LangChainWorkflow
+
+interrupt_event = asyncio.Event()
+
+
+async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ worker = Worker(
+ client,
+ task_queue="langchain-task-queue",
+ workflows=[LangChainWorkflow, LangChainChildWorkflow],
+ activities=[translate_phrase],
+ interceptors=[LangChainContextPropagationInterceptor()],
+ )
+
+ print("\nWorker started, ctrl+c to exit\n")
+ await worker.run()
+ try:
+ # Wait indefinitely until the interrupt event is set
+ await interrupt_event.wait()
+ finally:
+ # The worker will be shutdown gracefully due to the async context manager
+ print("\nShutting down the worker\n")
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ print("\nInterrupt received, shutting down...\n")
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/langchain/workflow.py b/langchain/workflow.py
new file mode 100644
index 00000000..31861c07
--- /dev/null
+++ b/langchain/workflow.py
@@ -0,0 +1,53 @@
+import asyncio
+from dataclasses import dataclass
+from datetime import timedelta
+from typing import List
+
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from activities import TranslateParams, translate_phrase
+
+
+@workflow.defn
+class LangChainChildWorkflow:
+ @workflow.run
+ async def run(self, params: TranslateParams) -> str:
+ return await workflow.execute_activity(
+ translate_phrase,
+ params,
+ schedule_to_close_timeout=timedelta(seconds=30),
+ )
+
+
+@dataclass
+class TranslateWorkflowParams:
+ phrase: str
+ languages: List[str]
+
+
+@workflow.defn
+class LangChainWorkflow:
+ @workflow.run
+ async def run(self, params: TranslateWorkflowParams) -> dict:
+ result1, result2, result3 = await asyncio.gather(
+ workflow.execute_activity(
+ translate_phrase,
+ TranslateParams(params.phrase, params.languages[0]),
+ schedule_to_close_timeout=timedelta(seconds=30),
+ ),
+ workflow.execute_activity(
+ translate_phrase,
+ TranslateParams(params.phrase, params.languages[1]),
+ schedule_to_close_timeout=timedelta(seconds=30),
+ ),
+ workflow.execute_child_workflow(
+ LangChainChildWorkflow.run,
+ TranslateParams(params.phrase, params.languages[2]),
+ ),
+ )
+ return {
+ params.languages[0]: result1,
+ params.languages[1]: result2,
+ params.languages[2]: result3,
+ }
diff --git a/message_passing/__init__.py b/message_passing/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/message_passing/introduction/README.md b/message_passing/introduction/README.md
new file mode 100644
index 00000000..e4e15b1c
--- /dev/null
+++ b/message_passing/introduction/README.md
@@ -0,0 +1,18 @@
+# Introduction to message-passing
+
+This sample provides an introduction to using Query, Signal, and Update.
+
+See https://docs.temporal.io/develop/python/message-passing.
+
+To run, first see the main [README.md](../../README.md) for prerequisites.
+
+Then create two terminals.
+
+Run the worker in one terminal:
+
+ uv run message_passing/introduction/worker.py
+
+And execute the workflow in the other terminal:
+
+ uv run message_passing/introduction/starter.py
+
diff --git a/message_passing/introduction/__init__.py b/message_passing/introduction/__init__.py
new file mode 100644
index 00000000..79d4a7b1
--- /dev/null
+++ b/message_passing/introduction/__init__.py
@@ -0,0 +1,13 @@
+from enum import IntEnum
+
+TASK_QUEUE = "message-passing-introduction-task-queue"
+
+
+class Language(IntEnum):
+ ARABIC = 1
+ CHINESE = 2
+ ENGLISH = 3
+ FRENCH = 4
+ HINDI = 5
+ PORTUGUESE = 6
+ SPANISH = 7
diff --git a/message_passing/introduction/activities.py b/message_passing/introduction/activities.py
new file mode 100644
index 00000000..7f2c9c56
--- /dev/null
+++ b/message_passing/introduction/activities.py
@@ -0,0 +1,25 @@
+import asyncio
+from typing import Optional
+
+from temporalio import activity
+
+from message_passing.introduction import Language
+
+
+@activity.defn
+async def call_greeting_service(to_language: Language) -> Optional[str]:
+ """
+ An Activity that simulates a call to a remote greeting service.
+ The remote greeting service supports the full range of languages.
+ """
+ greetings = {
+ Language.ARABIC: "مرحبا بالعالم",
+ Language.CHINESE: "你好,世界",
+ Language.ENGLISH: "Hello, world",
+ Language.FRENCH: "Bonjour, monde",
+ Language.HINDI: "नमस्ते दुनिया",
+ Language.PORTUGUESE: "Olá mundo",
+ Language.SPANISH: "Hola mundo",
+ }
+ await asyncio.sleep(0.2) # Simulate a network call
+ return greetings.get(to_language)
diff --git a/message_passing/introduction/starter.py b/message_passing/introduction/starter.py
new file mode 100644
index 00000000..aa5b8967
--- /dev/null
+++ b/message_passing/introduction/starter.py
@@ -0,0 +1,58 @@
+import asyncio
+from typing import Optional
+
+from temporalio.client import Client, WorkflowUpdateStage
+from temporalio.envconfig import ClientConfig
+
+from message_passing.introduction import TASK_QUEUE
+from message_passing.introduction.workflows import (
+ ApproveInput,
+ GetLanguagesInput,
+ GreetingWorkflow,
+ Language,
+ SetLanguageInput,
+)
+
+
+async def main(client: Optional[Client] = None):
+ if not client:
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ wf_handle = await client.start_workflow(
+ GreetingWorkflow.run,
+ id="greeting-workflow-1234",
+ task_queue=TASK_QUEUE,
+ )
+
+ # 👉 Send a Query
+ supported_languages = await wf_handle.query(
+ GreetingWorkflow.get_languages, GetLanguagesInput(include_unsupported=False)
+ )
+ print(f"supported languages: {supported_languages}")
+
+ # 👉 Execute an Update
+ previous_language = await wf_handle.execute_update(
+ GreetingWorkflow.set_language, SetLanguageInput(language=Language.CHINESE)
+ )
+ assert await wf_handle.query(GreetingWorkflow.get_language) == Language.CHINESE
+ print(f"language changed: {previous_language.name} -> {Language.CHINESE.name}")
+
+ # 👉 Start an Update and then wait for it to complete
+ update_handle = await wf_handle.start_update(
+ GreetingWorkflow.set_language_using_activity,
+ SetLanguageInput(language=Language.ARABIC),
+ wait_for_stage=WorkflowUpdateStage.ACCEPTED,
+ )
+ previous_language = await update_handle.result()
+ assert await wf_handle.query(GreetingWorkflow.get_language) == Language.ARABIC
+ print(f"language changed: {previous_language.name} -> {Language.ARABIC.name}")
+
+ # 👉 Send a Signal
+ await wf_handle.signal(GreetingWorkflow.approve, ApproveInput(name=""))
+ print(await wf_handle.result())
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/message_passing/introduction/worker.py b/message_passing/introduction/worker.py
new file mode 100644
index 00000000..34974801
--- /dev/null
+++ b/message_passing/introduction/worker.py
@@ -0,0 +1,39 @@
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from message_passing.introduction import TASK_QUEUE
+from message_passing.introduction.activities import call_greeting_service
+from message_passing.introduction.workflows import GreetingWorkflow
+
+interrupt_event = asyncio.Event()
+
+
+async def main():
+ logging.basicConfig(level=logging.INFO)
+
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[GreetingWorkflow],
+ activities=[call_greeting_service],
+ ):
+ logging.info("Worker started, ctrl+c to exit")
+ await interrupt_event.wait()
+ logging.info("Shutting down")
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/message_passing/introduction/workflows.py b/message_passing/introduction/workflows.py
new file mode 100644
index 00000000..8988d581
--- /dev/null
+++ b/message_passing/introduction/workflows.py
@@ -0,0 +1,124 @@
+import asyncio
+from dataclasses import dataclass
+from datetime import timedelta
+from typing import List, Optional
+
+from temporalio import workflow
+from temporalio.exceptions import ApplicationError
+
+with workflow.unsafe.imports_passed_through():
+ from message_passing.introduction import Language
+ from message_passing.introduction.activities import call_greeting_service
+
+
+@dataclass
+class GetLanguagesInput:
+ include_unsupported: bool
+
+
+@dataclass
+class SetLanguageInput:
+ language: Language
+
+
+@dataclass
+class ApproveInput:
+ name: str
+
+
+@workflow.defn
+class GreetingWorkflow:
+ """
+ A workflow that that returns a greeting in one of multiple supported
+ languages.
+
+ It exposes a query to obtain the current language, a signal to approve the
+ workflow so that it is allowed to return its result, and two updates for
+ changing the current language and receiving the previous language in
+ response.
+
+ One of the update handlers is not an `async def`, so it can only mutate and
+ return local workflow state; the other update handler is `async def` and
+ executes an activity which calls a remote service, giving access to language
+ translations which are not available in local workflow state.
+ """
+
+ def __init__(self) -> None:
+ self.approved_for_release = False
+ self.approver_name: Optional[str] = None
+ self.greetings = {
+ Language.CHINESE: "你好,世界",
+ Language.ENGLISH: "Hello, world",
+ }
+ self.language = Language.ENGLISH
+ self.lock = asyncio.Lock() # used by the async handler below
+
+ @workflow.run
+ async def run(self) -> str:
+ # 👉 In addition to waiting for the `approve` Signal, we also wait for
+ # all handlers to finish. Otherwise, the Workflow might return its
+ # result while an async set_language_using_activity Update is in
+ # progress.
+ await workflow.wait_condition(
+ lambda: self.approved_for_release and workflow.all_handlers_finished()
+ )
+ return self.greetings[self.language]
+
+ @workflow.query
+ def get_languages(self, input: GetLanguagesInput) -> List[Language]:
+ # 👉 A Query handler returns a value: it can inspect but must not mutate the Workflow state.
+ if input.include_unsupported:
+ return sorted(Language)
+ else:
+ return sorted(self.greetings)
+
+ @workflow.signal
+ def approve(self, input: ApproveInput) -> None:
+ # 👉 A Signal handler mutates the Workflow state but cannot return a value.
+ self.approved_for_release = True
+ self.approver_name = input.name
+
+ @workflow.update
+ def set_language(self, input: SetLanguageInput) -> Language:
+ # 👉 An Update handler can mutate the Workflow state and return a value.
+ previous_language, self.language = self.language, input.language
+ return previous_language
+
+ @set_language.validator
+ def validate_language(self, input: SetLanguageInput) -> None:
+ if input.language not in self.greetings:
+ # 👉 In an Update validator you raise any exception to reject the Update.
+ raise ValueError(f"{input.language.name} is not supported")
+
+ @workflow.update
+ async def set_language_using_activity(self, input: SetLanguageInput) -> Language:
+ # 👉 This update handler is async, so it can execute an activity.
+ if input.language not in self.greetings:
+ # 👉 We use a lock so that, if this handler is executed multiple
+ # times, each execution can schedule the activity only when the
+ # previously scheduled activity has completed. This ensures that
+ # multiple calls to set_language are processed in order.
+ async with self.lock:
+ greeting = await workflow.execute_activity(
+ call_greeting_service,
+ input.language,
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+ # 👉 The requested language might not be supported by the remote
+ # service. If so, we raise ApplicationError, which will fail the
+ # Update. The WorkflowExecutionUpdateAccepted event will still
+ # be added to history. (Update validators can be used to reject
+ # updates before any event is written to history, but they
+ # cannot be async, and so we cannot use an update validator for
+ # this purpose.)
+ if greeting is None:
+ raise ApplicationError(
+ f"Greeting service does not support {input.language.name}"
+ )
+ self.greetings[input.language] = greeting
+ previous_language, self.language = self.language, input.language
+ return previous_language
+
+ @workflow.query
+ def get_language(self) -> Language:
+ return self.language
diff --git a/message_passing/safe_message_handlers/README.md b/message_passing/safe_message_handlers/README.md
new file mode 100644
index 00000000..f8e4db8a
--- /dev/null
+++ b/message_passing/safe_message_handlers/README.md
@@ -0,0 +1,21 @@
+# Atomic message handlers
+
+This sample shows off important techniques for handling signals and updates, aka messages. In particular, it illustrates how message handlers can interleave or not be completed before the workflow completes, and how you can manage that.
+
+* Here, using workflow.wait_condition, signal and update handlers will only operate when the workflow is within a certain state--between cluster_started and cluster_shutdown.
+* Message handlers can block and their actions can be interleaved with one another and with the main workflow. This can easily cause bugs, so you can use a lock to protect shared state from interleaved access.
+* An "Entity" workflow, i.e. a long-lived workflow, periodically "continues as new". It must do this to prevent its history from growing too large, and it passes its state to the next workflow. You can check `workflow.info().is_continue_as_new_suggested()` to see when it's time.
+* Most people want their message handlers to finish before the workflow run completes or continues as new. Use `await workflow.wait_condition(lambda: workflow.all_handlers_finished())` to achieve this.
+* Message handlers can be made idempotent. See update `ClusterManager.assign_nodes_to_job`.
+
+To run, first see [README.md](../../README.md) for prerequisites.
+
+Then, run the following from the root directory to run the worker:
+
+ uv run message_passing/safe_message_handlers/worker.py
+
+Then, in another terminal, run the following to execute the workflow:
+
+ uv run message_passing/safe_message_handlers/starter.py
+
+This will start a worker to run your workflow and activities, then start a ClusterManagerWorkflow and put it through its paces.
diff --git a/message_passing/safe_message_handlers/__init__.py b/message_passing/safe_message_handlers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/message_passing/safe_message_handlers/activities.py b/message_passing/safe_message_handlers/activities.py
new file mode 100644
index 00000000..da8a8be0
--- /dev/null
+++ b/message_passing/safe_message_handlers/activities.py
@@ -0,0 +1,55 @@
+import asyncio
+from dataclasses import dataclass
+from typing import List, Set
+
+from temporalio import activity
+
+
+@dataclass
+class AssignNodesToJobInput:
+ nodes: List[str]
+ job_name: str
+
+
+@dataclass
+class ClusterState:
+ node_ids: List[str]
+
+
+@activity.defn
+async def start_cluster() -> ClusterState:
+ return ClusterState(node_ids=[f"node-{i}" for i in range(25)])
+
+
+@activity.defn
+async def assign_nodes_to_job(input: AssignNodesToJobInput) -> None:
+ print(f"Assigning nodes {input.nodes} to job {input.job_name}")
+ await asyncio.sleep(0.1)
+
+
+@dataclass
+class UnassignNodesForJobInput:
+ nodes: List[str]
+ job_name: str
+
+
+@activity.defn
+async def unassign_nodes_for_job(input: UnassignNodesForJobInput) -> None:
+ print(f"Deallocating nodes {input.nodes} from job {input.job_name}")
+ await asyncio.sleep(0.1)
+
+
+@dataclass
+class FindBadNodesInput:
+ nodes_to_check: Set[str]
+
+
+@activity.defn
+async def find_bad_nodes(input: FindBadNodesInput) -> Set[str]:
+ await asyncio.sleep(0.1)
+ bad_nodes = set([id for id in input.nodes_to_check if hash(id) % 5 == 0])
+ if bad_nodes:
+ print(f"Found bad nodes: {bad_nodes}")
+ else:
+ print("No new bad nodes found.")
+ return bad_nodes
diff --git a/message_passing/safe_message_handlers/starter.py b/message_passing/safe_message_handlers/starter.py
new file mode 100644
index 00000000..c8f1b5d3
--- /dev/null
+++ b/message_passing/safe_message_handlers/starter.py
@@ -0,0 +1,89 @@
+import argparse
+import asyncio
+import logging
+import uuid
+from typing import Optional
+
+from temporalio import common
+from temporalio.client import Client, WorkflowHandle
+from temporalio.envconfig import ClientConfig
+
+from message_passing.safe_message_handlers.workflow import (
+ ClusterManagerAssignNodesToJobInput,
+ ClusterManagerDeleteJobInput,
+ ClusterManagerInput,
+ ClusterManagerWorkflow,
+)
+
+
+async def do_cluster_lifecycle(wf: WorkflowHandle, delay_seconds: Optional[int] = None):
+ cluster_status = await wf.execute_update(
+ ClusterManagerWorkflow.wait_until_cluster_started
+ )
+ print(f"Cluster started with {len(cluster_status.nodes)} nodes")
+
+ print("Assigning jobs to nodes...")
+ allocation_updates = []
+ for i in range(6):
+ allocation_updates.append(
+ wf.execute_update(
+ ClusterManagerWorkflow.assign_nodes_to_job,
+ ClusterManagerAssignNodesToJobInput(
+ total_num_nodes=2, job_name=f"task-{i}"
+ ),
+ )
+ )
+ await asyncio.gather(*allocation_updates)
+
+ print(f"Sleeping for {delay_seconds} second(s)")
+ if delay_seconds:
+ await asyncio.sleep(delay_seconds)
+
+ print("Deleting jobs...")
+ deletion_updates = []
+ for i in range(6):
+ deletion_updates.append(
+ wf.execute_update(
+ ClusterManagerWorkflow.delete_job,
+ ClusterManagerDeleteJobInput(job_name=f"task-{i}"),
+ )
+ )
+ await asyncio.gather(*deletion_updates)
+
+ await wf.signal(ClusterManagerWorkflow.shutdown_cluster)
+
+
+async def main(should_test_continue_as_new: bool):
+ # Connect to Temporal
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ print("Starting cluster")
+ cluster_manager_handle = await client.start_workflow(
+ ClusterManagerWorkflow.run,
+ ClusterManagerInput(test_continue_as_new=should_test_continue_as_new),
+ id=f"ClusterManagerWorkflow-{uuid.uuid4()}",
+ task_queue="safe-message-handlers-task-queue",
+ id_conflict_policy=common.WorkflowIDConflictPolicy.TERMINATE_EXISTING,
+ )
+ delay_seconds = 10 if should_test_continue_as_new else 1
+ await do_cluster_lifecycle(cluster_manager_handle, delay_seconds=delay_seconds)
+ result = await cluster_manager_handle.result()
+ print(
+ f"Cluster shut down successfully."
+ f" It had {result.num_currently_assigned_nodes} nodes assigned at the end."
+ )
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
+ parser = argparse.ArgumentParser(description="Atomic message handlers")
+ parser.add_argument(
+ "--test-continue-as-new",
+ help="Make the ClusterManagerWorkflow continue as new before shutting down",
+ action="store_true",
+ default=False,
+ )
+ args = parser.parse_args()
+ asyncio.run(main(args.test_continue_as_new))
diff --git a/message_passing/safe_message_handlers/worker.py b/message_passing/safe_message_handlers/worker.py
new file mode 100644
index 00000000..31e538d4
--- /dev/null
+++ b/message_passing/safe_message_handlers/worker.py
@@ -0,0 +1,47 @@
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from message_passing.safe_message_handlers.workflow import (
+ ClusterManagerWorkflow,
+ assign_nodes_to_job,
+ find_bad_nodes,
+ start_cluster,
+ unassign_nodes_for_job,
+)
+
+interrupt_event = asyncio.Event()
+
+
+async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ async with Worker(
+ client,
+ task_queue="safe-message-handlers-task-queue",
+ workflows=[ClusterManagerWorkflow],
+ activities=[
+ assign_nodes_to_job,
+ unassign_nodes_for_job,
+ find_bad_nodes,
+ start_cluster,
+ ],
+ ):
+ logging.info("ClusterManagerWorkflow worker started, ctrl+c to exit")
+ await interrupt_event.wait()
+ logging.info("Shutting down")
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
+ loop = asyncio.new_event_loop()
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/message_passing/safe_message_handlers/workflow.py b/message_passing/safe_message_handlers/workflow.py
new file mode 100644
index 00000000..ca549a61
--- /dev/null
+++ b/message_passing/safe_message_handlers/workflow.py
@@ -0,0 +1,265 @@
+import asyncio
+import dataclasses
+from dataclasses import dataclass
+from datetime import timedelta
+from typing import Dict, List, Optional, Set
+
+from temporalio import workflow
+from temporalio.common import RetryPolicy
+from temporalio.exceptions import ApplicationError
+
+from message_passing.safe_message_handlers.activities import (
+ AssignNodesToJobInput,
+ FindBadNodesInput,
+ UnassignNodesForJobInput,
+ assign_nodes_to_job,
+ find_bad_nodes,
+ start_cluster,
+ unassign_nodes_for_job,
+)
+
+
+# In workflows that continue-as-new, it's convenient to store all your state in one serializable structure
+# to make it easier to pass between runs
+@dataclass
+class ClusterManagerState:
+ cluster_started: bool = False
+ cluster_shutdown: bool = False
+ nodes: Dict[str, Optional[str]] = dataclasses.field(default_factory=dict)
+ jobs_assigned: Set[str] = dataclasses.field(default_factory=set)
+
+
+@dataclass
+class ClusterManagerInput:
+ state: Optional[ClusterManagerState] = None
+ test_continue_as_new: bool = False
+
+
+@dataclass
+class ClusterManagerResult:
+ num_currently_assigned_nodes: int
+ num_bad_nodes: int
+
+
+# Be in the habit of storing message inputs and outputs in serializable structures.
+# This makes it easier to add more over time in a backward-compatible way.
+@dataclass
+class ClusterManagerAssignNodesToJobInput:
+ # If larger or smaller than previous amounts, will resize the job.
+ total_num_nodes: int
+ job_name: str
+
+
+@dataclass
+class ClusterManagerDeleteJobInput:
+ job_name: str
+
+
+@dataclass
+class ClusterManagerAssignNodesToJobResult:
+ nodes_assigned: Set[str]
+
+
+# ClusterManagerWorkflow keeps track of the assignments of a cluster of nodes.
+# Via signals, the cluster can be started and shutdown.
+# Via updates, clients can also assign jobs to nodes and delete jobs.
+# These updates must run atomically.
+@workflow.defn
+class ClusterManagerWorkflow:
+ @workflow.init
+ def __init__(self, input: ClusterManagerInput) -> None:
+ if input.state:
+ self.state = input.state
+ else:
+ self.state = ClusterManagerState()
+
+ if input.test_continue_as_new:
+ self.max_history_length: Optional[int] = 120
+ self.sleep_interval_seconds = 1
+ else:
+ self.max_history_length = None
+ self.sleep_interval_seconds = 600
+
+ # Protects workflow state from interleaved access
+ self.nodes_lock = asyncio.Lock()
+
+ @workflow.update
+ async def wait_until_cluster_started(self) -> ClusterManagerState:
+ await workflow.wait_condition(lambda: self.state.cluster_started)
+ return self.state
+
+ @workflow.signal
+ async def shutdown_cluster(self) -> None:
+ await workflow.wait_condition(lambda: self.state.cluster_started)
+ self.state.cluster_shutdown = True
+ workflow.logger.info("Cluster shut down")
+
+ # This is an update as opposed to a signal because the client may want to wait for nodes to be allocated
+ # before sending work to those nodes.
+ # Returns the list of node names that were allocated to the job.
+ @workflow.update
+ async def assign_nodes_to_job(
+ self, input: ClusterManagerAssignNodesToJobInput
+ ) -> ClusterManagerAssignNodesToJobResult:
+ await workflow.wait_condition(lambda: self.state.cluster_started)
+ if self.state.cluster_shutdown:
+ # If you want the client to receive a failure, either add an update validator and throw the
+ # exception from there, or raise an ApplicationError. Other exceptions in the main handler
+ # will cause the workflow to keep retrying and get it stuck.
+ raise ApplicationError(
+ "Cannot assign nodes to a job: Cluster is already shut down"
+ )
+
+ async with self.nodes_lock:
+ # Idempotency guard.
+ if input.job_name in self.state.jobs_assigned:
+ return ClusterManagerAssignNodesToJobResult(
+ self.get_assigned_nodes(job_name=input.job_name)
+ )
+ unassigned_nodes = self.get_unassigned_nodes()
+ if len(unassigned_nodes) < input.total_num_nodes:
+ # If you want the client to receive a failure, either add an update validator and throw the
+ # exception from there, or raise an ApplicationError. Other exceptions in the main handler
+ # will cause the workflow to keep retrying and get it stuck.
+ raise ApplicationError(
+ f"Cannot assign {input.total_num_nodes} nodes; have only {len(unassigned_nodes)} available"
+ )
+ nodes_to_assign = unassigned_nodes[: input.total_num_nodes]
+ # This await would be dangerous without nodes_lock because it yields control and allows interleaving
+ # with delete_job and perform_health_checks, which both touch self.state.nodes.
+ await self._assign_nodes_to_job(nodes_to_assign, input.job_name)
+ return ClusterManagerAssignNodesToJobResult(
+ nodes_assigned=self.get_assigned_nodes(job_name=input.job_name)
+ )
+
+ async def _assign_nodes_to_job(
+ self, assigned_nodes: List[str], job_name: str
+ ) -> None:
+ await workflow.execute_activity(
+ assign_nodes_to_job,
+ AssignNodesToJobInput(nodes=assigned_nodes, job_name=job_name),
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+ for node in assigned_nodes:
+ self.state.nodes[node] = job_name
+ self.state.jobs_assigned.add(job_name)
+
+ # Even though it returns nothing, this is an update because the client may want to track it, for example
+ # to wait for nodes to be unassigned before reassigning them.
+ @workflow.update
+ async def delete_job(self, input: ClusterManagerDeleteJobInput) -> None:
+ await workflow.wait_condition(lambda: self.state.cluster_started)
+ if self.state.cluster_shutdown:
+ # If you want the client to receive a failure, either add an update validator and throw the
+ # exception from there, or raise an ApplicationError. Other exceptions in the main handler
+ # will cause the workflow to keep retrying and get it stuck.
+ raise ApplicationError("Cannot delete a job: Cluster is already shut down")
+
+ async with self.nodes_lock:
+ nodes_to_unassign = [
+ k for k, v in self.state.nodes.items() if v == input.job_name
+ ]
+ # This await would be dangerous without nodes_lock because it yields control and allows interleaving
+ # with assign_nodes_to_job and perform_health_checks, which all touch self.state.nodes.
+ await self._unassign_nodes_for_job(nodes_to_unassign, input.job_name)
+
+ async def _unassign_nodes_for_job(
+ self, nodes_to_unassign: List[str], job_name: str
+ ):
+ await workflow.execute_activity(
+ unassign_nodes_for_job,
+ UnassignNodesForJobInput(nodes=nodes_to_unassign, job_name=job_name),
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+ for node in nodes_to_unassign:
+ self.state.nodes[node] = None
+
+ def get_unassigned_nodes(self) -> List[str]:
+ return [k for k, v in self.state.nodes.items() if v is None]
+
+ def get_bad_nodes(self) -> Set[str]:
+ return set([k for k, v in self.state.nodes.items() if v == "BAD!"])
+
+ def get_assigned_nodes(self, *, job_name: Optional[str] = None) -> Set[str]:
+ if job_name:
+ return set([k for k, v in self.state.nodes.items() if v == job_name])
+ else:
+ return set(
+ [
+ k
+ for k, v in self.state.nodes.items()
+ if v is not None and v != "BAD!"
+ ]
+ )
+
+ async def perform_health_checks(self) -> None:
+ async with self.nodes_lock:
+ assigned_nodes = self.get_assigned_nodes()
+ try:
+ # This await would be dangerous without nodes_lock because it yields control and allows interleaving
+ # with assign_nodes_to_job and delete_job, which both touch self.state.nodes.
+ bad_nodes = await workflow.execute_activity(
+ find_bad_nodes,
+ FindBadNodesInput(nodes_to_check=assigned_nodes),
+ start_to_close_timeout=timedelta(seconds=10),
+ # This health check is optional, and our lock would block the whole workflow if we let it retry forever.
+ retry_policy=RetryPolicy(maximum_attempts=1),
+ )
+ for node in bad_nodes:
+ self.state.nodes[node] = "BAD!"
+ except Exception as e:
+ workflow.logger.warn(
+ f"Health check failed with error {type(e).__name__}:{e}"
+ )
+
+ @workflow.run
+ async def run(self, input: ClusterManagerInput) -> ClusterManagerResult:
+ cluster_state = await workflow.execute_activity(
+ start_cluster, schedule_to_close_timeout=timedelta(seconds=10)
+ )
+ self.state.nodes = {k: None for k in cluster_state.node_ids}
+ self.state.cluster_started = True
+ workflow.logger.info("Cluster started")
+
+ # Perform health checks at intervals.
+ while True:
+ await self.perform_health_checks()
+ try:
+ await workflow.wait_condition(
+ lambda: self.state.cluster_shutdown
+ or self.should_continue_as_new(),
+ timeout=timedelta(seconds=self.sleep_interval_seconds),
+ )
+ except asyncio.TimeoutError:
+ pass
+ if self.state.cluster_shutdown:
+ break
+ # The cluster manager is a long-running "entity" workflow so we need to periodically checkpoint its state and
+ # continue-as-new.
+ if self.should_continue_as_new():
+ # We don't want to leave any job assignment or deletion handlers half-finished when we continue as new.
+ await workflow.wait_condition(lambda: workflow.all_handlers_finished())
+ workflow.logger.info("Continuing as new")
+ workflow.continue_as_new(
+ ClusterManagerInput(
+ state=self.state,
+ test_continue_as_new=input.test_continue_as_new,
+ )
+ )
+ # Make sure we finish off handlers such as deleting jobs before we complete the workflow.
+ await workflow.wait_condition(lambda: workflow.all_handlers_finished())
+ return ClusterManagerResult(
+ len(self.get_assigned_nodes()),
+ len(self.get_bad_nodes()),
+ )
+
+ def should_continue_as_new(self) -> bool:
+ if workflow.info().is_continue_as_new_suggested():
+ return True
+ # This is just for ease-of-testing. In production, we trust temporal to tell us when to continue as new.
+ if (
+ self.max_history_length
+ and workflow.info().get_current_history_length() > self.max_history_length
+ ):
+ return True
+ return False
diff --git a/message_passing/update_with_start/lazy_initialization/README.md b/message_passing/update_with_start/lazy_initialization/README.md
new file mode 100644
index 00000000..46f4e813
--- /dev/null
+++ b/message_passing/update_with_start/lazy_initialization/README.md
@@ -0,0 +1,18 @@
+# Update With Start: Lazy init
+
+This sample illustrates the use of update-with-start to send Updates to a Workflow, starting the Workflow if
+it is not running yet ("lazy init"). The Workflow represents a Shopping Cart in an e-commerce application, and
+update-with-start is used to add items to the cart, receiving back the updated cart subtotal.
+
+To run, first see the main [README.md](../../../README.md) for prerequisites.
+
+Then run the following from the root directory:
+
+ uv run message_passing/update_with_start/lazy_initialization/worker.py
+
+Then, in another terminal:
+
+ uv run message_passing/update_with_start/lazy_initialization/starter.py
+
+This will start a worker to run your workflow and activities, then simulate a backend application receiving
+requests to add items to a shopping cart, before finalizing the order.
diff --git a/message_passing/update_with_start/lazy_initialization/__init__.py b/message_passing/update_with_start/lazy_initialization/__init__.py
new file mode 100644
index 00000000..ac3632ee
--- /dev/null
+++ b/message_passing/update_with_start/lazy_initialization/__init__.py
@@ -0,0 +1 @@
+TASK_QUEUE = "update-with-start-lazy-initialization"
diff --git a/message_passing/update_with_start/lazy_initialization/activities.py b/message_passing/update_with_start/lazy_initialization/activities.py
new file mode 100644
index 00000000..fa730e47
--- /dev/null
+++ b/message_passing/update_with_start/lazy_initialization/activities.py
@@ -0,0 +1,20 @@
+import asyncio
+from dataclasses import dataclass
+from typing import Optional
+
+from temporalio import activity
+
+
+@dataclass
+class ShoppingCartItem:
+ sku: str
+ quantity: int
+
+
+@activity.defn
+async def get_price(item: ShoppingCartItem) -> Optional[int]:
+ await asyncio.sleep(0.1)
+ price = None if item.sku == "sku-456" else 599
+ if price is None:
+ return None
+ return price * item.quantity
diff --git a/message_passing/update_with_start/lazy_initialization/starter.py b/message_passing/update_with_start/lazy_initialization/starter.py
new file mode 100644
index 00000000..ec874939
--- /dev/null
+++ b/message_passing/update_with_start/lazy_initialization/starter.py
@@ -0,0 +1,81 @@
+import asyncio
+import uuid
+from typing import Optional, Tuple
+
+from temporalio import common
+from temporalio.client import (
+ Client,
+ WithStartWorkflowOperation,
+ WorkflowHandle,
+ WorkflowUpdateFailedError,
+)
+from temporalio.envconfig import ClientConfig
+from temporalio.exceptions import ApplicationError
+
+from message_passing.update_with_start.lazy_initialization import TASK_QUEUE
+from message_passing.update_with_start.lazy_initialization.workflows import (
+ ShoppingCartItem,
+ ShoppingCartWorkflow,
+)
+
+
+async def handle_add_item_request(
+ session_id: str, item_id: str, quantity: int, temporal_client: Client
+) -> Tuple[Optional[int], WorkflowHandle]:
+ """
+ Handle a client request to add an item to the shopping cart. The user is not logged in, but a session ID is
+ available from a cookie, and we use this as the cart ID. The Temporal client was created at service-start
+ time and is shared by all request handlers.
+
+ A Workflow Type exists that can be used to represent a shopping cart. The method uses update-with-start to
+ add an item to the shopping cart, creating the cart if it doesn't already exist.
+
+ Note that the workflow handle is available, even if the Update fails.
+ """
+ cart_id = f"cart-{session_id}"
+ start_op = WithStartWorkflowOperation(
+ ShoppingCartWorkflow.run,
+ id=cart_id,
+ id_conflict_policy=common.WorkflowIDConflictPolicy.USE_EXISTING,
+ task_queue=TASK_QUEUE,
+ )
+ try:
+ price = await temporal_client.execute_update_with_start_workflow(
+ ShoppingCartWorkflow.add_item,
+ ShoppingCartItem(sku=item_id, quantity=quantity),
+ start_workflow_operation=start_op,
+ )
+ except WorkflowUpdateFailedError as err:
+ if (
+ isinstance(err.cause, ApplicationError)
+ and err.cause.type == "ItemUnavailableError"
+ ):
+ price = None
+ else:
+ raise err
+
+ workflow_handle = await start_op.workflow_handle()
+
+ return price, workflow_handle
+
+
+async def main():
+ print("🛒")
+ session_id = f"session-{uuid.uuid4()}"
+
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ subtotal_1, _ = await handle_add_item_request(session_id, "sku-123", 1, client)
+ subtotal_2, wf_handle = await handle_add_item_request(
+ session_id, "sku-456", 1, client
+ )
+ print(f"subtotals were, {[subtotal_1, subtotal_2]}")
+ await wf_handle.signal(ShoppingCartWorkflow.checkout)
+ final_order = await wf_handle.result()
+ print(f"final order: {final_order}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/message_passing/update_with_start/lazy_initialization/worker.py b/message_passing/update_with_start/lazy_initialization/worker.py
new file mode 100644
index 00000000..1cc4f6ff
--- /dev/null
+++ b/message_passing/update_with_start/lazy_initialization/worker.py
@@ -0,0 +1,38 @@
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from message_passing.update_with_start.lazy_initialization import TASK_QUEUE, workflows
+from message_passing.update_with_start.lazy_initialization.activities import get_price
+
+interrupt_event = asyncio.Event()
+
+
+async def main():
+ logging.basicConfig(level=logging.INFO)
+
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[workflows.ShoppingCartWorkflow],
+ activities=[get_price],
+ ):
+ logging.info("Worker started, ctrl+c to exit")
+ await interrupt_event.wait()
+ logging.info("Shutting down")
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/message_passing/update_with_start/lazy_initialization/workflows.py b/message_passing/update_with_start/lazy_initialization/workflows.py
new file mode 100644
index 00000000..49964ff2
--- /dev/null
+++ b/message_passing/update_with_start/lazy_initialization/workflows.py
@@ -0,0 +1,60 @@
+from dataclasses import dataclass
+from datetime import timedelta
+from typing import List, Tuple
+
+from temporalio import workflow
+from temporalio.exceptions import ApplicationError
+
+with workflow.unsafe.imports_passed_through():
+ from message_passing.update_with_start.lazy_initialization.activities import (
+ ShoppingCartItem,
+ get_price,
+ )
+
+
+@dataclass
+class FinalizedOrder:
+ id: str
+ items: List[Tuple[ShoppingCartItem, int]]
+ total: int
+
+
+@workflow.defn
+class ShoppingCartWorkflow:
+ def __init__(self):
+ self.items: List[Tuple[ShoppingCartItem, int]] = []
+ self.order_submitted = False
+
+ @workflow.run
+ async def run(self) -> FinalizedOrder:
+ await workflow.wait_condition(
+ lambda: workflow.all_handlers_finished() and self.order_submitted
+ )
+ return FinalizedOrder(
+ id=workflow.info().workflow_id,
+ items=self.items,
+ total=sum(price for _, price in self.items),
+ )
+
+ @workflow.update
+ async def add_item(self, item: ShoppingCartItem) -> int:
+ price = await workflow.execute_activity(
+ get_price, item, start_to_close_timeout=timedelta(seconds=10)
+ )
+ if price is None:
+ raise ApplicationError(
+ f"Item unavailable: {item}",
+ type="ItemUnavailableError",
+ )
+ self.items.append((item, price))
+
+ return sum(price for _, price in self.items)
+
+ @add_item.validator
+ def validate_add_item(self, item: ShoppingCartItem) -> None:
+ if self.order_submitted:
+ raise ApplicationError("Order already submitted")
+
+ @workflow.signal
+ def checkout(self):
+ self.order_submitted = True
diff --git a/message_passing/waiting_for_handlers/README.md b/message_passing/waiting_for_handlers/README.md
new file mode 100644
index 00000000..9c9e3f7c
--- /dev/null
+++ b/message_passing/waiting_for_handlers/README.md
@@ -0,0 +1,41 @@
+# Waiting for message handlers
+
+This workflow demonstrates how to wait for signal and update handlers to
+finish in the following circumstances:
+
+- Before a successful return
+- On failure
+- On cancellation
+
+Your workflow can also exit via Continue-As-New. In that case you would
+usually wait for the handlers to finish immediately before the call to
+continue_as_new(); that's not illustrated in this sample.
+
+
+To run, open two terminals.
+
+Run the worker in one terminal:
+
+ uv run message_passing/waiting_for_handlers/worker.py
+
+And run the workflow-starter code in the other terminal:
+
+ uv run message_passing/waiting_for_handlers/starter.py
+
+
+Here's the output you'll see:
+
+```
+workflow exit type: SUCCESS
+ 🟢 caller received update result
+ 🟢 caller received workflow result
+
+
+workflow exit type: FAILURE
+ 🟢 caller received update result
+ 🔴 caught exception while waiting for workflow result: Workflow execution failed: deliberately failing workflow
+
+
+workflow exit type: CANCELLATION
+ 🟢 caller received update result
+```
\ No newline at end of file
diff --git a/message_passing/waiting_for_handlers/__init__.py b/message_passing/waiting_for_handlers/__init__.py
new file mode 100644
index 00000000..1274f9bf
--- /dev/null
+++ b/message_passing/waiting_for_handlers/__init__.py
@@ -0,0 +1,21 @@
+from dataclasses import dataclass
+from enum import IntEnum
+
+TASK_QUEUE = "my-task-queue"
+WORKFLOW_ID = "my-workflow-id"
+
+
+class WorkflowExitType(IntEnum):
+ SUCCESS = 0
+ FAILURE = 1
+ CANCELLATION = 2
+
+
+@dataclass
+class WorkflowInput:
+ exit_type: WorkflowExitType
+
+
+@dataclass
+class WorkflowResult:
+ data: str
diff --git a/message_passing/waiting_for_handlers/activities.py b/message_passing/waiting_for_handlers/activities.py
new file mode 100644
index 00000000..610db849
--- /dev/null
+++ b/message_passing/waiting_for_handlers/activities.py
@@ -0,0 +1,8 @@
+import asyncio
+
+from temporalio import activity
+
+
+@activity.defn
+async def activity_executed_by_update_handler():
+ await asyncio.sleep(1)
diff --git a/message_passing/waiting_for_handlers/starter.py b/message_passing/waiting_for_handlers/starter.py
new file mode 100644
index 00000000..9d3fbc0c
--- /dev/null
+++ b/message_passing/waiting_for_handlers/starter.py
@@ -0,0 +1,75 @@
+import asyncio
+
+from temporalio import client, common
+from temporalio.envconfig import ClientConfig
+
+from message_passing.waiting_for_handlers import (
+ TASK_QUEUE,
+ WORKFLOW_ID,
+ WorkflowExitType,
+ WorkflowInput,
+)
+from message_passing.waiting_for_handlers.workflows import WaitingForHandlersWorkflow
+
+
+async def starter(exit_type: WorkflowExitType):
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ cl = await client.Client.connect(**config)
+
+ wf_handle = await cl.start_workflow(
+ WaitingForHandlersWorkflow.run,
+ WorkflowInput(exit_type=exit_type),
+ id=WORKFLOW_ID,
+ task_queue=TASK_QUEUE,
+ id_conflict_policy=common.WorkflowIDConflictPolicy.TERMINATE_EXISTING,
+ )
+ await _check_run(wf_handle, exit_type)
+
+
+async def _check_run(
+ wf_handle: client.WorkflowHandle,
+ exit_type: WorkflowExitType,
+):
+ try:
+ up_handle = await wf_handle.start_update(
+ WaitingForHandlersWorkflow.my_update,
+ wait_for_stage=client.WorkflowUpdateStage.ACCEPTED,
+ )
+ except Exception as e:
+ print(
+ f" 🔴 caught exception while starting update: {e}: {e.__cause__ or ''}"
+ )
+
+ if exit_type == WorkflowExitType.CANCELLATION:
+ await wf_handle.cancel()
+
+ try:
+ await up_handle.result()
+ print(" 🟢 caller received update result")
+ except Exception as e:
+ print(
+ f" 🔴 caught exception while waiting for update result: {e}: {e.__cause__ or ''}"
+ )
+
+ try:
+ await wf_handle.result()
+ print(" 🟢 caller received workflow result")
+ except BaseException as e:
+ print(
+ f" 🔴 caught exception while waiting for workflow result: {e}: {e.__cause__ or ''}"
+ )
+
+
+async def main():
+ for exit_type in [
+ WorkflowExitType.SUCCESS,
+ WorkflowExitType.FAILURE,
+ WorkflowExitType.CANCELLATION,
+ ]:
+ print(f"\n\nworkflow exit type: {exit_type.name}")
+ await starter(exit_type)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/message_passing/waiting_for_handlers/worker.py b/message_passing/waiting_for_handlers/worker.py
new file mode 100644
index 00000000..e32a2dcb
--- /dev/null
+++ b/message_passing/waiting_for_handlers/worker.py
@@ -0,0 +1,43 @@
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from message_passing.waiting_for_handlers import TASK_QUEUE
+from message_passing.waiting_for_handlers.activities import (
+ activity_executed_by_update_handler,
+)
+from message_passing.waiting_for_handlers.workflows import WaitingForHandlersWorkflow
+
+interrupt_event = asyncio.Event()
+
+
+async def main():
+ logging.basicConfig(level=logging.INFO)
+
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[WaitingForHandlersWorkflow],
+ activities=[
+ activity_executed_by_update_handler,
+ ],
+ ):
+ logging.info("Worker started, ctrl+c to exit")
+ await interrupt_event.wait()
+ logging.info("Shutting down")
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/message_passing/waiting_for_handlers/workflows.py b/message_passing/waiting_for_handlers/workflows.py
new file mode 100644
index 00000000..b4ed2991
--- /dev/null
+++ b/message_passing/waiting_for_handlers/workflows.py
@@ -0,0 +1,95 @@
+import asyncio
+from datetime import timedelta
+
+from temporalio import exceptions, workflow
+
+from message_passing.waiting_for_handlers import (
+ WorkflowExitType,
+ WorkflowInput,
+ WorkflowResult,
+)
+from message_passing.waiting_for_handlers.activities import (
+ activity_executed_by_update_handler,
+)
+
+
+def is_workflow_exit_exception(e: BaseException) -> bool:
+ """
+ True if the exception is of a type that will cause the workflow to exit.
+
+ This is as opposed to exceptions that cause a workflow task failure, which
+ are retried automatically by Temporal.
+ """
+ # 👉 If you have set additional failure_exception_types you should also
+ # check for these here.
+ return isinstance(e, (asyncio.CancelledError, exceptions.FailureError))
+
+
+@workflow.defn
+class WaitingForHandlersWorkflow:
+ @workflow.run
+ async def run(self, input: WorkflowInput) -> WorkflowResult:
+ """
+ This workflow.run method demonstrates a pattern that can be used to wait for signal and
+ update handlers to finish in the following circumstances:
+
+ - On successful workflow return
+ - On workflow cancellation
+ - On workflow failure
+
+ Your workflow can also exit via Continue-As-New. In that case you would usually wait for
+ the handlers to finish immediately before the call to continue_as_new(); that's not
+ illustrated in this sample.
+
+ If you additionally need to perform cleanup or compensation on workflow failure or
+ cancellation, see the message_passing/waiting_for_handlers_and_compensation sample.
+ """
+ try:
+ # 👉 Use this `try...except` style, instead of waiting for message
+ # handlers to finish in a `finally` block. The reason is that some
+ # exception types cause a workflow task failure as opposed to
+ # workflow exit, in which case we do *not* want to wait for message
+ # handlers to finish.
+ result = await self._my_workflow_application_logic(input)
+ await workflow.wait_condition(workflow.all_handlers_finished)
+ return result
+ # 👉 Catch BaseException since asyncio.CancelledError does not inherit
+ # from Exception.
+ except BaseException as e:
+ if is_workflow_exit_exception(e):
+ await workflow.wait_condition(workflow.all_handlers_finished)
+ raise
+
+ # Methods below this point can be ignored unless you are interested in
+ # the implementation details of this sample.
+
+ def __init__(self) -> None:
+ self._update_started = False
+
+ @workflow.update
+ async def my_update(self) -> str:
+ self._update_started = True
+ await workflow.execute_activity(
+ activity_executed_by_update_handler,
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+ return "update-result"
+
+ async def _my_workflow_application_logic(
+ self, input: WorkflowInput
+ ) -> WorkflowResult:
+ # The main workflow logic is implemented in a separate method in order
+ # to separate "platform-level" concerns (waiting for handlers to finish
+ # and error handling) from application logic.
+
+ # Wait until handlers have started, so that we are demonstrating that we
+ # wait for them to finish.
+ await workflow.wait_condition(lambda: self._update_started)
+ if input.exit_type == WorkflowExitType.SUCCESS:
+ return WorkflowResult(data="workflow-result")
+ elif input.exit_type == WorkflowExitType.FAILURE:
+ raise exceptions.ApplicationError("deliberately failing workflow")
+ elif input.exit_type == WorkflowExitType.CANCELLATION:
+ # Block forever; the starter will send a workflow cancellation request.
+ await asyncio.Future()
+ raise AssertionError("unreachable")
diff --git a/message_passing/waiting_for_handlers_and_compensation/README.md b/message_passing/waiting_for_handlers_and_compensation/README.md
new file mode 100644
index 00000000..d23d484e
--- /dev/null
+++ b/message_passing/waiting_for_handlers_and_compensation/README.md
@@ -0,0 +1,39 @@
+# Waiting for message handlers, and performing compensation and cleanup in message handlers
+
+This sample demonstrates how to do the following:
+
+1. Ensure that all update/signal handlers are finished before a successful
+ workflow return, and on workflow cancellation and failure.
+2. Perform compensation/cleanup in an update handler when the workflow is
+ cancelled or fails.
+
+For a simpler sample showing how to do (1) without (2), see [safe_message_handlers](../safe_message_handlers/README.md).
+
+To run, open two terminals.
+
+Run the worker in one terminal:
+
+ uv run message_passing/waiting_for_handlers_and_compensation/worker.py
+
+And run the workflow-starter code in the other terminal:
+
+ uv run message_passing/waiting_for_handlers_and_compensation/starter.py
+
+
+Here's the output you'll see:
+
+```
+workflow exit type: SUCCESS
+ 🟢 caller received update result
+ 🟢 caller received workflow result
+
+
+workflow exit type: FAILURE
+ 🔴 caught exception while waiting for update result: Workflow update failed: The update failed because the workflow run exited
+ 🔴 caught exception while waiting for workflow result: Workflow execution failed: deliberately failing workflow
+
+
+workflow exit type: CANCELLATION
+ 🔴 caught exception while waiting for update result: Workflow update failed: The update failed because the workflow run exited
+ 🔴 caught exception while waiting for workflow result: Workflow execution failed: Workflow cancelled
+```
\ No newline at end of file
diff --git a/message_passing/waiting_for_handlers_and_compensation/__init__.py b/message_passing/waiting_for_handlers_and_compensation/__init__.py
new file mode 100644
index 00000000..1274f9bf
--- /dev/null
+++ b/message_passing/waiting_for_handlers_and_compensation/__init__.py
@@ -0,0 +1,21 @@
+from dataclasses import dataclass
+from enum import IntEnum
+
+TASK_QUEUE = "my-task-queue"
+WORKFLOW_ID = "my-workflow-id"
+
+
+class WorkflowExitType(IntEnum):
+ SUCCESS = 0
+ FAILURE = 1
+ CANCELLATION = 2
+
+
+@dataclass
+class WorkflowInput:
+ exit_type: WorkflowExitType
+
+
+@dataclass
+class WorkflowResult:
+ data: str
diff --git a/message_passing/waiting_for_handlers_and_compensation/activities.py b/message_passing/waiting_for_handlers_and_compensation/activities.py
new file mode 100644
index 00000000..36c5a5cb
--- /dev/null
+++ b/message_passing/waiting_for_handlers_and_compensation/activities.py
@@ -0,0 +1,18 @@
+import asyncio
+
+from temporalio import activity
+
+
+@activity.defn
+async def activity_executed_to_perform_workflow_compensation():
+ await asyncio.sleep(1)
+
+
+@activity.defn
+async def activity_executed_by_update_handler():
+ await asyncio.sleep(1)
+
+
+@activity.defn
+async def activity_executed_by_update_handler_to_perform_compensation():
+ await asyncio.sleep(1)
diff --git a/message_passing/waiting_for_handlers_and_compensation/starter.py b/message_passing/waiting_for_handlers_and_compensation/starter.py
new file mode 100644
index 00000000..6f25c9a5
--- /dev/null
+++ b/message_passing/waiting_for_handlers_and_compensation/starter.py
@@ -0,0 +1,77 @@
+import asyncio
+
+from temporalio import client, common
+from temporalio.envconfig import ClientConfig
+
+from message_passing.waiting_for_handlers_and_compensation import (
+ TASK_QUEUE,
+ WORKFLOW_ID,
+ WorkflowExitType,
+ WorkflowInput,
+)
+from message_passing.waiting_for_handlers_and_compensation.workflows import (
+ WaitingForHandlersAndCompensationWorkflow,
+)
+
+
+async def starter(exit_type: WorkflowExitType):
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ cl = await client.Client.connect(**config)
+
+ wf_handle = await cl.start_workflow(
+ WaitingForHandlersAndCompensationWorkflow.run,
+ WorkflowInput(exit_type=exit_type),
+ id=WORKFLOW_ID,
+ task_queue=TASK_QUEUE,
+ id_conflict_policy=common.WorkflowIDConflictPolicy.TERMINATE_EXISTING,
+ )
+ await _check_run(wf_handle, exit_type)
+
+
+async def _check_run(
+ wf_handle: client.WorkflowHandle,
+ exit_type: WorkflowExitType,
+):
+ try:
+ up_handle = await wf_handle.start_update(
+ WaitingForHandlersAndCompensationWorkflow.my_update,
+ wait_for_stage=client.WorkflowUpdateStage.ACCEPTED,
+ )
+ except Exception as e:
+ print(
+ f" 🔴 caught exception while starting update: {e}: {e.__cause__ or ''}"
+ )
+
+ if exit_type == WorkflowExitType.CANCELLATION:
+ await wf_handle.cancel()
+
+ try:
+ await up_handle.result()
+ print(" 🟢 caller received update result")
+ except Exception as e:
+ print(
+ f" 🔴 caught exception while waiting for update result: {e}: {e.__cause__ or ''}"
+ )
+
+ try:
+ await wf_handle.result()
+ print(" 🟢 caller received workflow result")
+ except Exception as e:
+ print(
+ f" 🔴 caught exception while waiting for workflow result: {e}: {e.__cause__ or ''}"
+ )
+
+
+async def main():
+ for exit_type in [
+ WorkflowExitType.SUCCESS,
+ WorkflowExitType.FAILURE,
+ WorkflowExitType.CANCELLATION,
+ ]:
+ print(f"\n\nworkflow exit type: {exit_type.name}")
+ await starter(exit_type)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/message_passing/waiting_for_handlers_and_compensation/worker.py b/message_passing/waiting_for_handlers_and_compensation/worker.py
new file mode 100644
index 00000000..2a27769d
--- /dev/null
+++ b/message_passing/waiting_for_handlers_and_compensation/worker.py
@@ -0,0 +1,48 @@
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from message_passing.waiting_for_handlers_and_compensation import TASK_QUEUE
+from message_passing.waiting_for_handlers_and_compensation.activities import (
+ activity_executed_by_update_handler,
+ activity_executed_by_update_handler_to_perform_compensation,
+ activity_executed_to_perform_workflow_compensation,
+)
+from message_passing.waiting_for_handlers_and_compensation.workflows import (
+ WaitingForHandlersAndCompensationWorkflow,
+)
+
+interrupt_event = asyncio.Event()
+
+
+async def main():
+ logging.basicConfig(level=logging.INFO)
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[WaitingForHandlersAndCompensationWorkflow],
+ activities=[
+ activity_executed_by_update_handler,
+ activity_executed_by_update_handler_to_perform_compensation,
+ activity_executed_to_perform_workflow_compensation,
+ ],
+ ):
+ logging.info("Worker started, ctrl+c to exit")
+ await interrupt_event.wait()
+ logging.info("Shutting down")
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/message_passing/waiting_for_handlers_and_compensation/workflows.py b/message_passing/waiting_for_handlers_and_compensation/workflows.py
new file mode 100644
index 00000000..450dff2c
--- /dev/null
+++ b/message_passing/waiting_for_handlers_and_compensation/workflows.py
@@ -0,0 +1,176 @@
+import asyncio
+from datetime import timedelta
+from typing import cast
+
+from temporalio import exceptions, workflow
+
+from message_passing.waiting_for_handlers_and_compensation import (
+ WorkflowExitType,
+ WorkflowInput,
+ WorkflowResult,
+)
+from message_passing.waiting_for_handlers_and_compensation.activities import (
+ activity_executed_by_update_handler,
+ activity_executed_by_update_handler_to_perform_compensation,
+ activity_executed_to_perform_workflow_compensation,
+)
+
+
+@workflow.defn
+class WaitingForHandlersAndCompensationWorkflow:
+ """
+ This Workflow demonstrates how to wait for message handlers to finish and
+ perform compensation/cleanup:
+
+ 1. It ensures that all signal and update handlers have finished before a
+ successful return, and on failure and cancellation.
+ 2. The update handler performs any necessary compensation/cleanup when the
+ workflow is cancelled or fails.
+
+ If all you need to do is wait for handlers, without performing cleanup or compensation,
+ then see the simpler sample message_passing/waiting_for_handlers.
+ """
+
+ def __init__(self) -> None:
+ # 👉 If the workflow exits prematurely, this future will be completed
+ # with the associated exception as its value. Message handlers can then
+ # "race" this future against a task performing the message handler's own
+ # application logic; if this future completes before the message handler
+ # task then the handler should abort and perform compensation.
+ self.workflow_exit: asyncio.Future[None] = asyncio.Future()
+
+ # The following attributes are implementation detail of this sample and can be
+ # ignored
+ self._update_started = False
+ self._update_compensation_done = False
+ self._workflow_compensation_done = False
+
+ @workflow.run
+ async def run(self, input: WorkflowInput) -> WorkflowResult:
+ try:
+ # 👉 Use this `try...except` style, instead of waiting for message
+ # handlers to finish in a `finally` block. The reason is that some
+ # exception types cause a workflow task failure as opposed to
+ # workflow exit, in which case we do *not* want to wait for message
+ # handlers to finish.
+
+ # 👉 The actual workflow application logic is implemented in a
+ # separate method in order to separate "platform-level" concerns
+ # (waiting for handlers to finish and ensuring that compensation is
+ # performed when appropriate) from application logic.
+ result = await self._my_workflow_application_logic(input)
+ self.workflow_exit.set_result(None)
+ await workflow.wait_condition(workflow.all_handlers_finished)
+ return result
+ # 👉 Catch BaseException since asyncio.CancelledError does not inherit
+ # from Exception.
+ except BaseException as e:
+ if is_workflow_exit_exception(e):
+ self.workflow_exit.set_exception(e)
+ await workflow.wait_condition(workflow.all_handlers_finished)
+ await self.workflow_compensation()
+ self._workflow_compensation_done = True
+ raise
+
+ async def workflow_compensation(self):
+ await workflow.execute_activity(
+ activity_executed_to_perform_workflow_compensation,
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+ self._update_compensation_done = True
+
+ @workflow.update
+ async def my_update(self) -> str:
+ """
+ An update handler that handles exceptions raised in its own execution
+ and in that of the main workflow method.
+
+ It ensures that:
+ - Compensation/cleanup is always performed when appropriate
+ - The update caller gets the update result, or WorkflowUpdateFailedError
+ """
+ # 👉 "Race" the workflow_exit future against the handler's own
+ # application logic. Always use `workflow.wait` instead of
+ # `asyncio.wait` in Workflow code: asyncio's version is
+ # non-deterministic. (Note that coroutines must be wrapped in tasks in
+ # order to use workflow.wait.)
+ update_task = asyncio.create_task(self._my_update_application_logic())
+ await workflow.wait( # type: ignore
+ [update_task, self.workflow_exit], return_when=asyncio.FIRST_EXCEPTION
+ )
+ try:
+ if update_task.done():
+ # 👉 The update has finished (whether successfully or not).
+ # Regardless of whether the main workflow method is about to
+ # exit or not, the update caller should receive a response
+ # informing them of the outcome of the update. So return the
+ # result, or raise the exception that caused the update handler
+ # to exit.
+ return await update_task
+ else:
+ # 👉 The main workflow method exited prematurely due to an
+ # error, and this happened before the update finished. Fail the
+ # update with the workflow exception as cause.
+ raise exceptions.ApplicationError(
+ "The update failed because the workflow run exited"
+ ) from cast(BaseException, self.workflow_exit.exception())
+ # 👉 Catch BaseException since asyncio.CancelledError does not inherit
+ # from Exception.
+ except BaseException as e:
+ if is_workflow_exit_exception(e):
+ try:
+ await self.my_update_compensation()
+ except BaseException as e:
+ raise exceptions.ApplicationError(
+ "Update compensation failed"
+ ) from e
+ raise
+
+ async def my_update_compensation(self):
+ await workflow.execute_activity(
+ activity_executed_by_update_handler_to_perform_compensation,
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+ self._update_compensation_done = True
+
+ @workflow.query
+ def workflow_compensation_done(self) -> bool:
+ return self._workflow_compensation_done
+
+ @workflow.query
+ def update_compensation_done(self) -> bool:
+ return self._update_compensation_done
+
+ # Methods below this point are placeholders for the actual application logic
+ # that you would perform in your main workflow method or update handler.
+ # They can be ignored unless you are interested in the implementation
+ # details of this sample.
+
+ async def _my_update_application_logic(self) -> str:
+ self._update_started = True
+ await workflow.execute_activity(
+ activity_executed_by_update_handler,
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+ return "update-result"
+
+ async def _my_workflow_application_logic(
+ self, input: WorkflowInput
+ ) -> WorkflowResult:
+ # Wait until handlers have started, so that we are demonstrating that we
+ # wait for them to finish.
+ await workflow.wait_condition(lambda: self._update_started)
+ if input.exit_type == WorkflowExitType.SUCCESS:
+ return WorkflowResult(data="workflow-result")
+ elif input.exit_type == WorkflowExitType.FAILURE:
+ raise exceptions.ApplicationError("deliberately failing workflow")
+ elif input.exit_type == WorkflowExitType.CANCELLATION:
+ # Block forever; the starter will send a workflow cancellation request.
+ await asyncio.Future()
+ raise AssertionError("unreachable")
+
+
+def is_workflow_exit_exception(e: BaseException) -> bool:
+ # 👉 If you have set additional failure_exception_types you should also
+ # check for these here.
+ return isinstance(e, (asyncio.CancelledError, exceptions.FailureError))
diff --git a/nexus_cancel/README.md b/nexus_cancel/README.md
new file mode 100644
index 00000000..2f7f5703
--- /dev/null
+++ b/nexus_cancel/README.md
@@ -0,0 +1,50 @@
+# Nexus Cancellation
+
+This sample shows how a caller workflow can fan out multiple Nexus operations concurrently, take the first result, and cancel the rest using `WAIT_REQUESTED` cancellation semantics.
+
+With `WAIT_REQUESTED`, the caller proceeds once the handler has received the cancel request — it does not wait for the handler to finish processing the cancellation.
+
+Start a Temporal server. (See the main samples repo [README](../README.md)).
+
+Run the following:
+
+```
+temporal operator namespace create --namespace nexus-cancel-handler-namespace
+temporal operator namespace create --namespace nexus-cancel-caller-namespace
+
+temporal operator nexus endpoint create \
+ --name nexus-cancel-endpoint \
+ --target-namespace nexus-cancel-handler-namespace \
+ --target-task-queue nexus-cancel-handler-task-queue
+```
+
+Next, in separate terminal windows:
+
+## Nexus Handler Worker
+
+```bash
+uv run nexus_cancel/handler/worker.py
+```
+
+## Nexus Caller App
+
+```bash
+uv run nexus_cancel/caller/app.py
+```
+
+## Expected Output
+
+On the caller side, you should see a greeting in whichever language completed first:
+```
+Hello Nexus 👋
+```
+
+On the handler side, you should see cancellation log messages for the remaining operations:
+```
+HelloHandlerWorkflow was cancelled successfully.
+HelloHandlerWorkflow was cancelled successfully.
+HelloHandlerWorkflow was cancelled successfully.
+HelloHandlerWorkflow was cancelled successfully.
+```
+
+The caller workflow returns before all handler workflows have completed their cancellation cleanup. This demonstrates `WAIT_REQUESTED` semantics: the caller didn't wait for the handler workflows to finish, but still guaranteed that all handlers received the cancellation request.
diff --git a/nexus_cancel/__init__.py b/nexus_cancel/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexus_cancel/caller/__init__.py b/nexus_cancel/caller/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexus_cancel/caller/app.py b/nexus_cancel/caller/app.py
new file mode 100644
index 00000000..bb74e9e0
--- /dev/null
+++ b/nexus_cancel/caller/app.py
@@ -0,0 +1,43 @@
+import asyncio
+import uuid
+from typing import Optional
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from nexus_cancel.caller.workflows import HelloCallerWorkflow
+
+NAMESPACE = "nexus-cancel-caller-namespace"
+TASK_QUEUE = "nexus-cancel-caller-task-queue"
+
+
+async def execute_caller_workflow(
+ client: Optional[Client] = None,
+) -> str:
+ if client is None:
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ config.setdefault("namespace", NAMESPACE)
+ client = await Client.connect(**config)
+
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[HelloCallerWorkflow],
+ ):
+ return await client.execute_workflow(
+ HelloCallerWorkflow.run,
+ "Nexus",
+ id=f"hello-caller-{uuid.uuid4()}",
+ task_queue=TASK_QUEUE,
+ )
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ try:
+ result = loop.run_until_complete(execute_caller_workflow())
+ print(result)
+ except KeyboardInterrupt:
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/nexus_cancel/caller/workflows.py b/nexus_cancel/caller/workflows.py
new file mode 100644
index 00000000..f8a2e1ff
--- /dev/null
+++ b/nexus_cancel/caller/workflows.py
@@ -0,0 +1,69 @@
+"""
+Caller workflow that demonstrates Nexus operation cancellation.
+
+Fans out 5 concurrent Nexus hello operations (one per language), takes the first
+result, and cancels the rest using WAIT_REQUESTED cancellation semantics.
+"""
+
+import asyncio
+from datetime import timedelta
+
+from temporalio import workflow
+from temporalio.exceptions import CancelledError, NexusOperationError
+
+with workflow.unsafe.imports_passed_through():
+ from nexus_cancel.service import HelloInput, Language, NexusService
+
+NEXUS_ENDPOINT = "nexus-cancel-endpoint"
+
+
+@workflow.defn
+class HelloCallerWorkflow:
+ def __init__(self) -> None:
+ self.nexus_client = workflow.create_nexus_client(
+ service=NexusService,
+ endpoint=NEXUS_ENDPOINT,
+ )
+
+ @workflow.run
+ async def run(self, message: str) -> str:
+ # Fan out 5 concurrent Nexus calls, one per language.
+ # Each task starts and awaits its own operation so all race concurrently.
+ async def run_operation(language: Language):
+ handle = await self.nexus_client.start_operation(
+ NexusService.hello,
+ HelloInput(name=message, language=language),
+ schedule_to_close_timeout=timedelta(seconds=10),
+ cancellation_type=workflow.NexusOperationCancellationType.WAIT_REQUESTED,
+ )
+ return await handle
+
+ tasks = [asyncio.create_task(run_operation(lang)) for lang in Language]
+
+ # Wait for the first operation to complete
+ workflow.logger.info(
+ f"Started {len(tasks)} operations, waiting for first to complete..."
+ )
+ done, pending = await workflow.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
+
+ # Get the result from the first completed operation
+ result = await done.pop()
+ workflow.logger.info(f"First operation completed with: {result.message}")
+
+ # Cancel all remaining operations
+ workflow.logger.info(f"Cancelling {len(pending)} remaining operations...")
+ for task in pending:
+ task.cancel()
+
+ # Wait for all cancellations to be acknowledged.
+ # If the workflow completes before cancellation requests are delivered,
+ # the server drops them. Waiting ensures all handlers receive the
+ # cancellation.
+ for task in pending:
+ try:
+ await task
+ except (NexusOperationError, CancelledError):
+ # Expected: the operation was cancelled
+ workflow.logger.info("Operation was cancelled")
+
+ return result.message
diff --git a/nexus_cancel/handler/__init__.py b/nexus_cancel/handler/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexus_cancel/handler/service_handler.py b/nexus_cancel/handler/service_handler.py
new file mode 100644
index 00000000..92868510
--- /dev/null
+++ b/nexus_cancel/handler/service_handler.py
@@ -0,0 +1,27 @@
+"""
+Nexus service handler for the cancellation sample.
+
+The hello operation is backed by a workflow, using the Nexus request ID as the
+workflow ID for idempotency across retries.
+"""
+
+from __future__ import annotations
+
+import nexusrpc
+from temporalio import nexus
+
+from nexus_cancel.handler.workflows import HelloHandlerWorkflow
+from nexus_cancel.service import HelloInput, HelloOutput, NexusService
+
+
+@nexusrpc.handler.service_handler(service=NexusService)
+class NexusServiceHandler:
+ @nexus.workflow_run_operation
+ async def hello(
+ self, ctx: nexus.WorkflowRunOperationContext, input: HelloInput
+ ) -> nexus.WorkflowHandle[HelloOutput]:
+ return await ctx.start_workflow(
+ HelloHandlerWorkflow.run,
+ input,
+ id=ctx.request_id,
+ )
diff --git a/nexus_cancel/handler/worker.py b/nexus_cancel/handler/worker.py
new file mode 100644
index 00000000..e29df355
--- /dev/null
+++ b/nexus_cancel/handler/worker.py
@@ -0,0 +1,48 @@
+"""
+Worker for the handler namespace that processes Nexus operations and workflows.
+"""
+
+import asyncio
+import logging
+from typing import Optional
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from nexus_cancel.handler.service_handler import NexusServiceHandler
+from nexus_cancel.handler.workflows import HelloHandlerWorkflow
+
+interrupt_event = asyncio.Event()
+
+NAMESPACE = "nexus-cancel-handler-namespace"
+TASK_QUEUE = "nexus-cancel-handler-task-queue"
+
+
+async def main(client: Optional[Client] = None):
+ logging.basicConfig(level=logging.INFO)
+
+ if not client:
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ config.setdefault("namespace", NAMESPACE)
+ client = await Client.connect(**config)
+
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[HelloHandlerWorkflow],
+ nexus_service_handlers=[NexusServiceHandler()],
+ ):
+ logging.info("Worker started, ctrl+c to exit")
+ await interrupt_event.wait()
+ logging.info("Shutting down")
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/nexus_cancel/handler/workflows.py b/nexus_cancel/handler/workflows.py
new file mode 100644
index 00000000..d799c62b
--- /dev/null
+++ b/nexus_cancel/handler/workflows.py
@@ -0,0 +1,49 @@
+"""
+Handler workflow started by the hello Nexus operation.
+
+Demonstrates how to handle cancellation from the caller workflow using a
+detached cancellation scope (asyncio.shield) for cleanup work.
+"""
+
+import asyncio
+
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from nexus_cancel.service import HelloInput, HelloOutput, Language
+
+GREETINGS = {
+ Language.EN: "Hello {name} 👋",
+ Language.FR: "Bonjour {name} 👋",
+ Language.DE: "Hallo {name} 👋",
+ Language.ES: "¡Hola! {name} 👋",
+ Language.TR: "Merhaba {name} 👋",
+}
+
+
+@workflow.defn
+class HelloHandlerWorkflow:
+ @workflow.run
+ async def run(self, input: HelloInput) -> HelloOutput:
+ try:
+ # Sleep for a random duration to simulate work (0-5 seconds)
+ random_seconds = workflow.random().randint(0, 5)
+ workflow.logger.info(f"Working for {random_seconds} seconds...")
+ await asyncio.sleep(random_seconds)
+
+ # Return a greeting based on the language
+ greeting = GREETINGS[input.language].format(name=input.name)
+ return HelloOutput(message=greeting)
+
+ except asyncio.CancelledError:
+ # Perform cleanup in a detached cancellation scope.
+ # asyncio.shield prevents the cleanup work from being cancelled.
+ workflow.logger.info("Received cancellation request, performing cleanup...")
+ try:
+ cleanup_seconds = workflow.random().randint(0, 5)
+ await asyncio.shield(asyncio.sleep(cleanup_seconds))
+ except asyncio.CancelledError:
+ pass
+ workflow.logger.info("HelloHandlerWorkflow was cancelled successfully.")
+ # Re-raise the cancellation error
+ raise
diff --git a/nexus_cancel/service.py b/nexus_cancel/service.py
new file mode 100644
index 00000000..454a32f7
--- /dev/null
+++ b/nexus_cancel/service.py
@@ -0,0 +1,35 @@
+"""
+Nexus service definition for the cancellation sample.
+
+Defines a NexusService with a single `hello` operation that takes a name and
+language, and returns a greeting message.
+"""
+
+from dataclasses import dataclass
+from enum import IntEnum
+
+import nexusrpc
+
+
+class Language(IntEnum):
+ EN = 0
+ FR = 1
+ DE = 2
+ ES = 3
+ TR = 4
+
+
+@dataclass
+class HelloInput:
+ name: str
+ language: Language
+
+
+@dataclass
+class HelloOutput:
+ message: str
+
+
+@nexusrpc.service
+class NexusService:
+ hello: nexusrpc.Operation[HelloInput, HelloOutput]
diff --git a/nexus_multiple_args/README.md b/nexus_multiple_args/README.md
new file mode 100644
index 00000000..8da0f146
--- /dev/null
+++ b/nexus_multiple_args/README.md
@@ -0,0 +1,33 @@
+This sample shows how to map a Nexus operation to a handler workflow that takes multiple input arguments. The Nexus operation receives a single input object but unpacks it into multiple arguments when starting the workflow.
+
+### Sample directory structure
+
+- [service.py](./service.py) - shared Nexus service definition
+- [caller](./caller) - a caller workflow that executes Nexus operations, together with a worker and starter code
+- [handler](./handler) - Nexus operation handlers, together with a workflow used by the Nexus operation, and a worker that polls for both workflow and Nexus tasks.
+
+### Instructions
+
+Start a Temporal server. (See the main samples repo [README](../README.md)).
+
+Run the following:
+
+```
+temporal operator namespace create --namespace nexus-multiple-args-handler-namespace
+temporal operator namespace create --namespace nexus-multiple-args-caller-namespace
+
+temporal operator nexus endpoint create \
+ --name nexus-multiple-args-nexus-endpoint \
+ --target-namespace nexus-multiple-args-handler-namespace \
+ --target-task-queue nexus-multiple-args-handler-task-queue
+```
+
+In one terminal, run the Temporal worker in the handler namespace:
+```
+uv run nexus_multiple_args/handler/worker.py
+```
+
+In another terminal, run the Temporal worker in the caller namespace and start the caller workflow:
+```
+uv run nexus_multiple_args/caller/app.py
+```
\ No newline at end of file
diff --git a/nexus_multiple_args/__init__.py b/nexus_multiple_args/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexus_multiple_args/caller/__init__.py b/nexus_multiple_args/caller/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexus_multiple_args/caller/app.py b/nexus_multiple_args/caller/app.py
new file mode 100644
index 00000000..b4d7ebc3
--- /dev/null
+++ b/nexus_multiple_args/caller/app.py
@@ -0,0 +1,55 @@
+import asyncio
+import uuid
+from typing import Optional
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from nexus_multiple_args.caller.workflows import CallerWorkflow
+
+NAMESPACE = "nexus-multiple-args-caller-namespace"
+TASK_QUEUE = "nexus-multiple-args-caller-task-queue"
+
+
+async def execute_caller_workflow(
+ client: Optional[Client] = None,
+) -> tuple[str, str]:
+ if client is None:
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ config.setdefault("namespace", NAMESPACE)
+ client = await Client.connect(**config)
+
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[CallerWorkflow],
+ ):
+ # Execute workflow with English language
+ result1 = await client.execute_workflow(
+ CallerWorkflow.run,
+ args=["Nexus", "en"],
+ id=str(uuid.uuid4()),
+ task_queue=TASK_QUEUE,
+ )
+
+ # Execute workflow with Spanish language
+ result2 = await client.execute_workflow(
+ CallerWorkflow.run,
+ args=["Nexus", "es"],
+ id=str(uuid.uuid4()),
+ task_queue=TASK_QUEUE,
+ )
+
+ return result1, result2
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ try:
+ results = loop.run_until_complete(execute_caller_workflow())
+ for result in results:
+ print(result)
+ except KeyboardInterrupt:
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/nexus_multiple_args/caller/workflows.py b/nexus_multiple_args/caller/workflows.py
new file mode 100644
index 00000000..940a032f
--- /dev/null
+++ b/nexus_multiple_args/caller/workflows.py
@@ -0,0 +1,30 @@
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from nexus_multiple_args.service import HelloInput, MyNexusService
+
+NEXUS_ENDPOINT = "nexus-multiple-args-nexus-endpoint"
+
+
+# This is a workflow that calls a nexus operation with multiple arguments.
+@workflow.defn
+class CallerWorkflow:
+ # An __init__ method is always optional on a workflow class. Here we use it to set the
+ # nexus client, but that could alternatively be done in the run method.
+ def __init__(self):
+ self.nexus_client = workflow.create_nexus_client(
+ service=MyNexusService,
+ endpoint=NEXUS_ENDPOINT,
+ )
+
+ # The workflow run method demonstrates calling a nexus operation with multiple arguments
+ # packed into an input object.
+ @workflow.run
+ async def run(self, name: str, language: str) -> str:
+ # Start the nexus operation and wait for the result in one go, using execute_operation.
+ # The multiple arguments (name and language) are packed into a HelloInput object.
+ result = await self.nexus_client.execute_operation(
+ MyNexusService.hello,
+ HelloInput(name=name, language=language),
+ )
+ return result.message
diff --git a/nexus_multiple_args/handler/__init__.py b/nexus_multiple_args/handler/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexus_multiple_args/handler/service_handler.py b/nexus_multiple_args/handler/service_handler.py
new file mode 100644
index 00000000..c2ddfb92
--- /dev/null
+++ b/nexus_multiple_args/handler/service_handler.py
@@ -0,0 +1,39 @@
+from __future__ import annotations
+
+import uuid
+
+import nexusrpc
+from temporalio import nexus
+
+from nexus_multiple_args.handler.workflows import HelloHandlerWorkflow
+from nexus_multiple_args.service import HelloInput, HelloOutput, MyNexusService
+
+
+# @@@SNIPSTART samples-python-nexus-handler-multiargs
+@nexusrpc.handler.service_handler(service=MyNexusService)
+class MyNexusServiceHandler:
+ """
+ Service handler that demonstrates multiple argument handling in Nexus operations.
+ """
+
+ # This is a nexus operation that is backed by a Temporal workflow.
+ # The key feature here is that it demonstrates how to map a single input object
+ # (HelloInput) to a workflow that takes multiple individual arguments.
+ @nexus.workflow_run_operation
+ async def hello(
+ self, ctx: nexus.WorkflowRunOperationContext, input: HelloInput
+ ) -> nexus.WorkflowHandle[HelloOutput]:
+ """
+ Start a workflow with multiple arguments unpacked from the input object.
+ """
+ return await ctx.start_workflow(
+ HelloHandlerWorkflow.run,
+ args=[
+ input.name, # First argument: name
+ input.language, # Second argument: language
+ ],
+ id=str(uuid.uuid4()),
+ )
+
+
+# @@@SNIPEND
diff --git a/nexus_multiple_args/handler/worker.py b/nexus_multiple_args/handler/worker.py
new file mode 100644
index 00000000..079d08ae
--- /dev/null
+++ b/nexus_multiple_args/handler/worker.py
@@ -0,0 +1,47 @@
+import asyncio
+import logging
+from typing import Optional
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from nexus_multiple_args.handler.service_handler import MyNexusServiceHandler
+from nexus_multiple_args.handler.workflows import HelloHandlerWorkflow
+
+interrupt_event = asyncio.Event()
+
+NAMESPACE = "nexus-multiple-args-handler-namespace"
+TASK_QUEUE = "nexus-multiple-args-handler-task-queue"
+
+
+async def main(client: Optional[Client] = None):
+ logging.basicConfig(level=logging.INFO)
+
+ if client is None:
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ config.setdefault("namespace", NAMESPACE)
+ client = await Client.connect(**config)
+
+ # Start the worker, passing the Nexus service handler instance, in addition to the
+ # workflow classes that are started by your nexus operations, and any activities
+ # needed. This Worker will poll for both workflow tasks and Nexus tasks.
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[HelloHandlerWorkflow],
+ nexus_service_handlers=[MyNexusServiceHandler()],
+ ):
+ logging.info("Worker started, ctrl+c to exit")
+ await interrupt_event.wait()
+ logging.info("Shutting down")
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/nexus_multiple_args/handler/workflows.py b/nexus_multiple_args/handler/workflows.py
new file mode 100644
index 00000000..15bd0824
--- /dev/null
+++ b/nexus_multiple_args/handler/workflows.py
@@ -0,0 +1,32 @@
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from nexus_multiple_args.service import HelloOutput
+
+
+# This is the workflow that is started by the `hello` nexus operation.
+# It demonstrates handling multiple arguments passed from the Nexus service.
+@workflow.defn
+class HelloHandlerWorkflow:
+ @workflow.run
+ async def run(self, name: str, language: str) -> HelloOutput:
+ """
+ Handle the hello workflow with multiple arguments.
+
+ This method receives the individual arguments (name and language)
+ that were unpacked from the HelloInput in the service handler.
+ """
+ if language == "en":
+ message = f"Hello {name} 👋"
+ elif language == "fr":
+ message = f"Bonjour {name} 👋"
+ elif language == "de":
+ message = f"Hallo {name} 👋"
+ elif language == "es":
+ message = f"¡Hola! {name} 👋"
+ elif language == "tr":
+ message = f"Merhaba {name} 👋"
+ else:
+ raise ValueError(f"Unsupported language: {language}")
+
+ return HelloOutput(message=message)
diff --git a/nexus_multiple_args/service.py b/nexus_multiple_args/service.py
new file mode 100644
index 00000000..ccae11fd
--- /dev/null
+++ b/nexus_multiple_args/service.py
@@ -0,0 +1,34 @@
+"""
+This is a Nexus service definition that demonstrates multiple argument handling.
+
+A service definition defines a Nexus service as a named collection of operations, each
+with input and output types. It does not implement operation handling: see the service
+handler and operation handlers in nexus_multiple_args.handler.service_handler for that.
+
+A Nexus service definition is used by Nexus callers (e.g. a Temporal workflow) to create
+type-safe clients, and it is used by Nexus handlers to validate that they implement
+correctly-named operation handlers with the correct input and output types.
+
+The service defined in this file features one operation: hello, where hello
+demonstrates handling multiple arguments through a single input object.
+"""
+
+from dataclasses import dataclass
+
+import nexusrpc
+
+
+@dataclass
+class HelloInput:
+ name: str
+ language: str
+
+
+@dataclass
+class HelloOutput:
+ message: str
+
+
+@nexusrpc.service
+class MyNexusService:
+ hello: nexusrpc.Operation[HelloInput, HelloOutput]
diff --git a/nexus_sync_operations/README.md b/nexus_sync_operations/README.md
new file mode 100644
index 00000000..10e266ec
--- /dev/null
+++ b/nexus_sync_operations/README.md
@@ -0,0 +1,39 @@
+This sample shows how to create a Nexus service that is backed by a long-running workflow and
+exposes operations that execute updates and queries against that workflow. The long-running
+workflow, and the updates/queries are private implementation detail of the nexus service: the caller
+does not know how the operations are implemented.
+
+### Sample directory structure
+
+- [service.py](./service.py) - shared Nexus service definition
+- [caller](./caller) - a caller workflow that executes Nexus operations, together with a worker and starter code
+- [handler](./handler) - Nexus operation handlers, together with a workflow used by one of the Nexus operations, and a worker that polls for both workflow, activity, and Nexus tasks.
+
+
+### Instructions
+
+Start a Temporal server. (See the main samples repo [README](../README.md)).
+
+Run the following to create the caller and handler namespaces, and the Nexus endpoint:
+
+```
+temporal operator namespace create --namespace nexus-sync-operations-handler-namespace
+temporal operator namespace create --namespace nexus-sync-operations-caller-namespace
+
+temporal operator nexus endpoint create \
+ --name nexus-sync-operations-nexus-endpoint \
+ --target-namespace nexus-sync-operations-handler-namespace \
+ --target-task-queue nexus-sync-operations-handler-task-queue \
+ --description-file nexus_sync_operations/endpoint_description.md
+```
+
+In one terminal, run the Temporal worker in the handler namespace:
+```
+uv run nexus_sync_operations/handler/worker.py
+```
+
+In another terminal, run the Temporal worker in the caller namespace and start the caller
+workflow:
+```
+uv run nexus_sync_operations/caller/app.py
+```
diff --git a/nexus_sync_operations/__init__.py b/nexus_sync_operations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexus_sync_operations/caller/__init__.py b/nexus_sync_operations/caller/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexus_sync_operations/caller/app.py b/nexus_sync_operations/caller/app.py
new file mode 100644
index 00000000..375628d2
--- /dev/null
+++ b/nexus_sync_operations/caller/app.py
@@ -0,0 +1,43 @@
+import asyncio
+import uuid
+from typing import Optional
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from nexus_sync_operations.caller.workflows import CallerWorkflow
+
+NAMESPACE = "nexus-sync-operations-caller-namespace"
+TASK_QUEUE = "nexus-sync-operations-caller-task-queue"
+
+
+async def execute_caller_workflow(
+ client: Optional[Client] = None,
+) -> None:
+ if client is None:
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ config.setdefault("namespace", NAMESPACE)
+ client = await Client.connect(**config)
+
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[CallerWorkflow],
+ ):
+ log = await client.execute_workflow(
+ CallerWorkflow.run,
+ id=str(uuid.uuid4()),
+ task_queue=TASK_QUEUE,
+ )
+ for line in log:
+ print(line)
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ try:
+ loop.run_until_complete(execute_caller_workflow())
+ except KeyboardInterrupt:
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/nexus_sync_operations/caller/workflows.py b/nexus_sync_operations/caller/workflows.py
new file mode 100644
index 00000000..a358d764
--- /dev/null
+++ b/nexus_sync_operations/caller/workflows.py
@@ -0,0 +1,46 @@
+"""
+This is a workflow that calls nexus operations. The caller does not have information about how these
+operations are implemented by the nexus service.
+"""
+
+from temporalio import workflow
+
+from message_passing.introduction import Language
+from message_passing.introduction.workflows import GetLanguagesInput, SetLanguageInput
+
+with workflow.unsafe.imports_passed_through():
+ from nexus_sync_operations.service import GreetingService
+
+NEXUS_ENDPOINT = "nexus-sync-operations-nexus-endpoint"
+
+
+@workflow.defn
+class CallerWorkflow:
+ @workflow.run
+ async def run(self) -> list[str]:
+ log = []
+ nexus_client = workflow.create_nexus_client(
+ service=GreetingService,
+ endpoint=NEXUS_ENDPOINT,
+ )
+
+ # Get supported languages
+ supported_languages = await nexus_client.execute_operation(
+ GreetingService.get_languages, GetLanguagesInput(include_unsupported=False)
+ )
+ log.append(f"supported languages: {supported_languages}")
+
+ # Set language
+ previous_language = await nexus_client.execute_operation(
+ GreetingService.set_language,
+ SetLanguageInput(language=Language.ARABIC),
+ )
+ assert (
+ await nexus_client.execute_operation(GreetingService.get_language, None)
+ == Language.ARABIC
+ )
+ log.append(
+ f"language changed: {previous_language.name} -> {Language.ARABIC.name}"
+ )
+
+ return log
diff --git a/nexus_sync_operations/endpoint_description.md b/nexus_sync_operations/endpoint_description.md
new file mode 100644
index 00000000..a33b60cf
--- /dev/null
+++ b/nexus_sync_operations/endpoint_description.md
@@ -0,0 +1,4 @@
+## Service: [GreetingService](https://github.com/temporalio/samples-python/blob/main/nexus_sync_operations/service.py)
+- operation: `get_languages`
+- operation: `get_language`
+- operation: `set_language`
diff --git a/nexus_sync_operations/handler/__init__.py b/nexus_sync_operations/handler/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexus_sync_operations/handler/service_handler.py b/nexus_sync_operations/handler/service_handler.py
new file mode 100644
index 00000000..626948f0
--- /dev/null
+++ b/nexus_sync_operations/handler/service_handler.py
@@ -0,0 +1,83 @@
+"""
+This file demonstrates how to implement a Nexus service that is backed by a long-running workflow
+and exposes operations that perform updates and queries against that workflow.
+"""
+
+from __future__ import annotations
+
+import nexusrpc
+from temporalio import nexus
+from temporalio.client import Client, WorkflowHandle
+from temporalio.common import WorkflowIDConflictPolicy
+
+from message_passing.introduction import Language
+from message_passing.introduction.workflows import (
+ GetLanguagesInput,
+ GreetingWorkflow,
+ SetLanguageInput,
+)
+from nexus_sync_operations.service import GreetingService
+
+
+@nexusrpc.handler.service_handler(service=GreetingService)
+class GreetingServiceHandler:
+ def __init__(self, workflow_id: str):
+ self.workflow_id = workflow_id
+
+ @classmethod
+ async def create(
+ cls, workflow_id: str, client: Client, task_queue: str
+ ) -> GreetingServiceHandler:
+ # Start the long-running "entity" workflow, if it is not already running.
+ await client.start_workflow(
+ GreetingWorkflow.run,
+ id=workflow_id,
+ task_queue=task_queue,
+ id_conflict_policy=WorkflowIDConflictPolicy.USE_EXISTING,
+ )
+ return cls(workflow_id)
+
+ @property
+ def greeting_workflow_handle(self) -> WorkflowHandle[GreetingWorkflow, str]:
+ # In nexus operation handler code, nexus.client() is always available, returning a client
+ # connected to the handler namespace (it's the same client instance that your nexus worker
+ # is using to poll the server for nexus tasks). This client can be used to interact with the
+ # handler namespace, for example to send signals, queries, or updates. Remember however,
+ # that a sync_operation handler must return quickly (no more than a few seconds). To do
+ # long-running work in a nexus operation handler, use
+ # temporalio.nexus.workflow_run_operation (see the hello_nexus sample).
+ return nexus.client().get_workflow_handle_for(
+ GreetingWorkflow.run, self.workflow_id
+ )
+
+ # 👉 This is a handler for a nexus operation whose internal implementation involves executing a
+ # query against a long-running workflow that is private to the nexus service.
+ @nexusrpc.handler.sync_operation
+ async def get_languages(
+ self, ctx: nexusrpc.handler.StartOperationContext, input: GetLanguagesInput
+ ) -> list[Language]:
+ return await self.greeting_workflow_handle.query(
+ GreetingWorkflow.get_languages, input
+ )
+
+ # 👉 This is a handler for a nexus operation whose internal implementation involves executing a
+ # query against a long-running workflow that is private to the nexus service.
+ @nexusrpc.handler.sync_operation
+ async def get_language(
+ self, ctx: nexusrpc.handler.StartOperationContext, input: None
+ ) -> Language:
+ return await self.greeting_workflow_handle.query(GreetingWorkflow.get_language)
+
+ # 👉 This is a handler for a nexus operation whose internal implementation involves executing an
+ # update against a long-running workflow that is private to the nexus service. Although updates
+ # can run for an arbitrarily long time, when exposing an update via a nexus sync operation the
+ # update should execute quickly (sync operations must complete in under 10s).
+ @nexusrpc.handler.sync_operation
+ async def set_language(
+ self,
+ ctx: nexusrpc.handler.StartOperationContext,
+ input: SetLanguageInput,
+ ) -> Language:
+ return await self.greeting_workflow_handle.execute_update(
+ GreetingWorkflow.set_language_using_activity, input
+ )
diff --git a/nexus_sync_operations/handler/worker.py b/nexus_sync_operations/handler/worker.py
new file mode 100644
index 00000000..97c8eb04
--- /dev/null
+++ b/nexus_sync_operations/handler/worker.py
@@ -0,0 +1,52 @@
+import asyncio
+import logging
+from typing import Optional
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from message_passing.introduction.activities import call_greeting_service
+from message_passing.introduction.workflows import GreetingWorkflow
+from nexus_sync_operations.handler.service_handler import GreetingServiceHandler
+
+interrupt_event = asyncio.Event()
+
+NAMESPACE = "nexus-sync-operations-handler-namespace"
+TASK_QUEUE = "nexus-sync-operations-handler-task-queue"
+
+
+async def main(client: Optional[Client] = None):
+ logging.basicConfig(level=logging.INFO)
+
+ if client is None:
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ config.setdefault("namespace", NAMESPACE)
+ client = await Client.connect(**config)
+
+ # Create the nexus service handler instance, starting the long-running entity workflow that
+ # backs the Nexus service
+ greeting_service_handler = await GreetingServiceHandler.create(
+ "nexus-sync-operations-greeting-workflow", client, TASK_QUEUE
+ )
+
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[GreetingWorkflow],
+ activities=[call_greeting_service],
+ nexus_service_handlers=[greeting_service_handler],
+ ):
+ logging.info("Worker started, ctrl+c to exit")
+ await interrupt_event.wait()
+ logging.info("Shutting down")
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/nexus_sync_operations/service.py b/nexus_sync_operations/service.py
new file mode 100644
index 00000000..3436d5f3
--- /dev/null
+++ b/nexus_sync_operations/service.py
@@ -0,0 +1,20 @@
+"""
+This module defines a Nexus service that exposes three operations.
+
+It is used by the nexus service handler to validate that the operation handlers implement the
+correct input and output types, and by the caller workflow to create a type-safe client. It does not
+contain the implementation of the operations; see nexus_sync_operations.handler.service_handler for
+that.
+"""
+
+import nexusrpc
+
+from message_passing.introduction import Language
+from message_passing.introduction.workflows import GetLanguagesInput, SetLanguageInput
+
+
+@nexusrpc.service
+class GreetingService:
+ get_languages: nexusrpc.Operation[GetLanguagesInput, list[Language]]
+ get_language: nexusrpc.Operation[None, Language]
+ set_language: nexusrpc.Operation[SetLanguageInput, Language]
diff --git a/open_telemetry/README.md b/open_telemetry/README.md
index 3f71d8c3..e95deaca 100644
--- a/open_telemetry/README.md
+++ b/open_telemetry/README.md
@@ -4,57 +4,34 @@ This sample shows how to configure OpenTelemetry to capture workflow traces and
For this sample, the optional `open_telemetry` dependency group must be included. To include, run:
- poetry install --with open_telemetry
+ uv sync --group open-telemetry
-To run, first see [README.md](../README.md) for prerequisites. Then run the following to start a Jaeger container to
-view the trace results:
+To run, first see [README.md](../README.md) for prerequisites. Then run the following to start an [Aspire](https://hub.docker.com/r/microsoft/dotnet-aspire-dashboard/) OTEL collector
- docker run -d --name jaeger \
- -p 16686:16686 \
- -p 6831:6831/udp \
- jaegertracing/all-in-one:latest
+ docker compose up
-Since that is running in the background (`-d`), you can also run the metrics collector in the foreground:
+Now, start the worker in its own terminal:
- docker run -p 4317:4317 \
- -v /path/to/samples-python/open_telemetry/otel-metrics-collector-config.yaml:/etc/otel-collector-config.yaml \
- otel/opentelemetry-collector:latest \
- --config=/etc/otel-collector-config.yaml
+ uv run open_telemetry/worker.py
-Replace `/path/to/samples-python` with the absolute path to the cloned samples repo.
+Then, in another terminal, run the following to execute the workflow:
-Now, from this directory, start the worker in its own terminal:
+ uv run open_telemetry/starter.py
- poetry run python worker.py
+The workflow should complete with the hello result.
-This will start the worker. Then, in another terminal, run the following to execute the workflow:
+Now view the Aspire UI at http://localhost:18888/.
- poetry run python starter.py
+To view metrics sent describing the worker and the workflow that was executed, select `Metrics` on the left and under "Select a resource" select "temporal-core-sdk". It may look like this:
-The workflow should complete with the hello result. The workflow trace can now be viewed in Jaeger at
-http://localhost:16686/. Under service, select `my-service` and "Find Traces". The workflow should appear and when
-clicked, may look something like:
+
-
+
+To view workflow spans, select `Traces` on the left and under "Select a resource" select "temporal-core-sdk". It may look like this:
+
+
Note, in-workflow spans do not have a time associated with them. This is by intention since due to replay. In
OpenTelemetry, only the process that started the span may end it. But in Temporal a span may cross workers/processes.
Therefore we intentionally start-then-end in-workflow spans immediately. So while the start time and hierarchy is
-accurate, the duration is not.
-
-The metrics should have been dumped out in the terminal where the OpenTelemetry collector container is running.
-
-## OTLP gRPC
-
-Currently for tracing this example uses the `opentelemetry-exporter-jaeger-thrift` exporter because the common OTLP gRPC
-exporter `opentelemetry-exporter-otlp-proto-grpc` uses an older, incompatible `protobuf` library. See
-[this issue](https://github.com/open-telemetry/opentelemetry-python/issues/2880) for more information.
-
-Once OTel supports latest protobuf, the exporter can be changed and Jaeger could be run with:
-
- docker run -d --name jaeger \
- -e COLLECTOR_OTLP_ENABLED=true \
- -p 16686:16686 \
- -p 4317:4317 \
- -p 4318:4318 \
- jaegertracing/all-in-one:latest
\ No newline at end of file
+accurate, the duration is not.
\ No newline at end of file
diff --git a/open_telemetry/aspire-metrics-screenshot.png b/open_telemetry/aspire-metrics-screenshot.png
new file mode 100644
index 00000000..ed4ab8e9
Binary files /dev/null and b/open_telemetry/aspire-metrics-screenshot.png differ
diff --git a/open_telemetry/aspire-traces-screenshot.png b/open_telemetry/aspire-traces-screenshot.png
new file mode 100644
index 00000000..298c07eb
Binary files /dev/null and b/open_telemetry/aspire-traces-screenshot.png differ
diff --git a/open_telemetry/docker-compose.yaml b/open_telemetry/docker-compose.yaml
new file mode 100644
index 00000000..da3c02ae
--- /dev/null
+++ b/open_telemetry/docker-compose.yaml
@@ -0,0 +1,8 @@
+services:
+ aspire-dashboard:
+ environment:
+ Dashboard__Frontend__AuthMode: Unsecured
+ image: mcr.microsoft.com/dotnet/aspire-dashboard:8.0
+ ports:
+ - 4317:18889
+ - 18888:18888
\ No newline at end of file
diff --git a/open_telemetry/jaeger-screenshot.png b/open_telemetry/jaeger-screenshot.png
deleted file mode 100644
index 8376d49c..00000000
Binary files a/open_telemetry/jaeger-screenshot.png and /dev/null differ
diff --git a/open_telemetry/otel-metrics-collector-config.yaml b/open_telemetry/otel-metrics-collector-config.yaml
deleted file mode 100644
index 45265f88..00000000
--- a/open_telemetry/otel-metrics-collector-config.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-receivers:
- otlp:
- protocols:
- grpc:
-exporters:
- logging:
- loglevel: debug
-processors:
- batch:
-service:
- pipelines:
- metrics:
- receivers: [otlp]
- exporters: [logging]
- processors: [batch]
\ No newline at end of file
diff --git a/open_telemetry/starter.py b/open_telemetry/starter.py
index 86360368..9e8650b0 100644
--- a/open_telemetry/starter.py
+++ b/open_telemetry/starter.py
@@ -2,6 +2,7 @@
from temporalio.client import Client
from temporalio.contrib.opentelemetry import TracingInterceptor
+from temporalio.envconfig import ClientConfig
from open_telemetry.worker import GreetingWorkflow, init_runtime_with_telemetry
@@ -9,9 +10,11 @@
async def main():
runtime = init_runtime_with_telemetry()
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
# Connect client
client = await Client.connect(
- "localhost:7233",
+ **config,
# Use OpenTelemetry interceptor
interceptors=[TracingInterceptor()],
runtime=runtime,
diff --git a/open_telemetry/worker.py b/open_telemetry/worker.py
index 3002213f..631bd008 100644
--- a/open_telemetry/worker.py
+++ b/open_telemetry/worker.py
@@ -1,17 +1,15 @@
import asyncio
from datetime import timedelta
-import opentelemetry.context
from opentelemetry import trace
-
-# See note in README about why Thrift
-from opentelemetry.exporter.jaeger.thrift import JaegerExporter
-from opentelemetry.sdk.resources import SERVICE_NAME, Resource
+from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
+from opentelemetry.sdk.resources import SERVICE_NAME, Resource # type: ignore
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from temporalio import activity, workflow
from temporalio.client import Client
from temporalio.contrib.opentelemetry import TracingInterceptor
+from temporalio.envconfig import ClientConfig
from temporalio.runtime import OpenTelemetryConfig, Runtime, TelemetryConfig
from temporalio.worker import Worker
@@ -38,7 +36,8 @@ async def compose_greeting(name: str) -> str:
def init_runtime_with_telemetry() -> Runtime:
# Setup global tracer for workflow traces
provider = TracerProvider(resource=Resource.create({SERVICE_NAME: "my-service"}))
- provider.add_span_processor(BatchSpanProcessor(JaegerExporter()))
+ exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True)
+ provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
# Setup SDK metrics to OTel endpoint
@@ -52,12 +51,12 @@ def init_runtime_with_telemetry() -> Runtime:
async def main():
runtime = init_runtime_with_telemetry()
- # See https://github.com/temporalio/sdk-python/issues/199
- opentelemetry.context.get_current()
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
# Connect client
client = await Client.connect(
- "localhost:7233",
+ **config,
# Use OpenTelemetry interceptor
interceptors=[TracingInterceptor()],
runtime=runtime,
diff --git a/openai_agents/README.md b/openai_agents/README.md
new file mode 100644
index 00000000..9404278f
--- /dev/null
+++ b/openai_agents/README.md
@@ -0,0 +1,38 @@
+# Temporal OpenAI Agents SDK Integration
+
+⚠️ **Public Preview** - This integration is experimental and its interfaces may change prior to General Availability.
+
+This directory contains samples demonstrating how to use the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) with Temporal's durable execution engine.
+These samples are adapted from the [OpenAI Agents SDK examples](https://github.com/openai/openai-agents-python/tree/main/examples) and extended with Temporal's durability and orchestration capabilities.
+
+See the [module documentation](https://github.com/temporalio/sdk-python/blob/main/temporalio/contrib/openai_agents/README.md) for more information.
+
+## Overview
+
+The integration combines:
+- **Temporal workflows** for orchestrating agent control flow and state management
+- **OpenAI Agents SDK** for AI agent creation and tool interactions
+
+This approach ensures that AI agent workflows are durable, observable, and can handle failures gracefully.
+
+## Prerequisites
+
+- Temporal server [running locally](https://docs.temporal.io/cli/server#start-dev)
+- Required dependencies installed via `uv sync --group openai-agents`
+- OpenAI API key set as environment variable: `export OPENAI_API_KEY=your_key_here`
+
+## Examples
+
+Each directory contains a complete example with its own README for detailed instructions:
+
+- **[Basic Examples](./basic/README.md)** - Simple agent examples including a hello world agent and a tools-enabled agent that can access external APIs like weather services.
+- **[Agent Patterns](./agent_patterns/README.md)** - Advanced patterns for agent composition, including using agents as tools within other agents.
+- **[Tools](./tools/README.md)** - Demonstrates available tools such as file search, image generation, and others.
+- **[Handoffs](./handoffs/README.md)** - Agents collaborating via handoffs.
+- **[Hosted MCP](./hosted_mcp/README.md)** - Using the MCP client functionality of the OpenAI Responses API.
+- **[MCP](./mcp/README.md)** - Local MCP servers (filesystem/stdio, streamable HTTP, SSE, prompt server) integrated with Temporal workflows.
+- **[Model Providers](./model_providers/README.md)** - Using custom LLM providers (e.g., Anthropic via LiteLLM).
+- **[Research Bot](./research_bot/README.md)** - Multi-agent research system with specialized roles: a planner agent, search agent, and writer agent working together to conduct comprehensive research.
+- **[Customer Service](./customer_service/README.md)** - Interactive customer service agent with escalation capabilities, demonstrating conversational workflows.
+- **[Reasoning Content](./reasoning_content/README.md)** - Example of how to retrieve the thought process of reasoning models.
+- **[Financial Research Agent](./financial_research_agent/README.md)** - Multi-agent financial research system with planner, search, analyst, writer, and verifier agents collaborating.
diff --git a/openai_agents/__init__.py b/openai_agents/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/openai_agents/agent_patterns/README.md b/openai_agents/agent_patterns/README.md
new file mode 100644
index 00000000..33784747
--- /dev/null
+++ b/openai_agents/agent_patterns/README.md
@@ -0,0 +1,97 @@
+# Agent Patterns
+
+Common agentic patterns extended with Temporal's durable execution capabilities.
+
+*Adapted from [OpenAI Agents SDK agent patterns](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns)*
+
+Before running these examples, be sure to review the [prerequisites and background on the integration](../README.md).
+
+## Running the Examples
+
+First, start the worker (supports all patterns):
+```bash
+uv run openai_agents/agent_patterns/run_worker.py
+```
+
+Then run individual examples in separate terminals:
+
+### Deterministic Flows
+Sequential agent execution with validation gates - demonstrates breaking complex tasks into smaller steps:
+```bash
+uv run openai_agents/agent_patterns/run_deterministic_workflow.py
+```
+
+### Parallelization
+Run multiple agents in parallel and select the best result - useful for improving quality or reducing latency:
+```bash
+uv run openai_agents/agent_patterns/run_parallelization_workflow.py
+```
+
+### LLM-as-a-Judge
+Iterative improvement using feedback loops - generate content, evaluate it, and improve until satisfied:
+```bash
+uv run openai_agents/agent_patterns/run_llm_as_a_judge_workflow.py
+```
+
+### Agents as Tools
+Use agents as callable tools within other agents - enables composition and specialized task delegation:
+```bash
+uv run openai_agents/agent_patterns/run_agents_as_tools_workflow.py
+```
+
+### Agent Routing and Handoffs
+Route requests to specialized agents based on content analysis (adapted for non-streaming):
+```bash
+uv run openai_agents/agent_patterns/run_routing_workflow.py
+```
+
+### Input Guardrails
+Pre-execution validation to prevent unwanted requests - demonstrates safety mechanisms:
+```bash
+uv run openai_agents/agent_patterns/run_input_guardrails_workflow.py
+```
+
+### Output Guardrails
+Post-execution validation to detect sensitive content - ensures safe responses:
+```bash
+uv run openai_agents/agent_patterns/run_output_guardrails_workflow.py
+```
+
+### Forcing Tool Use
+Control tool execution strategies - choose between different approaches to tool usage:
+```bash
+uv run openai_agents/agent_patterns/run_forcing_tool_use_workflow.py
+```
+
+## Pattern Details
+
+### Deterministic Flows
+A common tactic is to break down a task into a series of smaller steps. Each task can be performed by an agent, and the output of one agent is used as input to the next. For example, if your task was to generate a story, you could break it down into the following steps:
+
+1. Generate an outline
+2. Check outline quality and genre
+3. Write the story (only if outline passes validation)
+
+Each of these steps can be performed by an agent. The output of one agent is used as input to the next.
+
+### Parallelization
+Running multiple agents in parallel is a common pattern. This can be useful for both latency (e.g. if you have multiple steps that don't depend on each other) and also for other reasons e.g. generating multiple responses and picking the best one.
+
+### LLM-as-a-Judge
+LLMs can often improve the quality of their output if given feedback. A common pattern is to generate a response using a model, and then use a second model to provide feedback. You can even use a small model for the initial generation and a larger model for the feedback, to optimize cost.
+
+### Agents as Tools
+The mental model for handoffs is that the new agent "takes over". It sees the previous conversation history, and owns the conversation from that point onwards. However, this is not the only way to use agents. You can also use agents as a tool - the tool agent goes off and runs on its own, and then returns the result to the original agent.
+
+### Guardrails
+Related to parallelization, you often want to run input guardrails to make sure the inputs to your agents are valid. For example, if you have a customer support agent, you might want to make sure that the user isn't trying to ask for help with a math problem.
+
+You can definitely do this without any special Agents SDK features by using parallelization, but we support a special guardrail primitive. Guardrails can have a "tripwire" - if the tripwire is triggered, the agent execution will immediately stop and a `GuardrailTripwireTriggered` exception will be raised.
+
+This is really useful for latency: for example, you might have a very fast model that runs the guardrail and a slow model that runs the actual agent. You wouldn't want to wait for the slow model to finish, so guardrails let you quickly reject invalid inputs.
+
+## Omitted Examples
+
+The following patterns from the [reference repository](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns) are not included in this Temporal adaptation:
+
+- **Streaming Guardrails**: Requires streaming capabilities which are not yet available in the Temporal integration
\ No newline at end of file
diff --git a/openai_agents/agent_patterns/run_agents_as_tools_workflow.py b/openai_agents/agent_patterns/run_agents_as_tools_workflow.py
new file mode 100644
index 00000000..8b0c1345
--- /dev/null
+++ b/openai_agents/agent_patterns/run_agents_as_tools_workflow.py
@@ -0,0 +1,32 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.agent_patterns.workflows.agents_as_tools_workflow import (
+ AgentsAsToolsWorkflow,
+)
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ AgentsAsToolsWorkflow.run,
+ "Please translate 'Good morning, how are you?' to Spanish and French",
+ id="agents-as-tools-workflow-example",
+ task_queue="openai-agents-patterns-task-queue",
+ )
+
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/agent_patterns/run_deterministic_workflow.py b/openai_agents/agent_patterns/run_deterministic_workflow.py
new file mode 100644
index 00000000..abb2e4de
--- /dev/null
+++ b/openai_agents/agent_patterns/run_deterministic_workflow.py
@@ -0,0 +1,31 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.agent_patterns.workflows.deterministic_workflow import (
+ DeterministicWorkflow,
+)
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ DeterministicWorkflow.run,
+ "Write a science fiction story about time travel",
+ id="deterministic-workflow-example",
+ task_queue="openai-agents-patterns-task-queue",
+ )
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/agent_patterns/run_forcing_tool_use_workflow.py b/openai_agents/agent_patterns/run_forcing_tool_use_workflow.py
new file mode 100644
index 00000000..5dfa42c5
--- /dev/null
+++ b/openai_agents/agent_patterns/run_forcing_tool_use_workflow.py
@@ -0,0 +1,50 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.agent_patterns.workflows.forcing_tool_use_workflow import (
+ ForcingToolUseWorkflow,
+)
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute workflows with different tool use behaviors
+ print("Testing default behavior:")
+ result1 = await client.execute_workflow(
+ ForcingToolUseWorkflow.run,
+ "default",
+ id="forcing-tool-use-workflow-default",
+ task_queue="openai-agents-patterns-task-queue",
+ )
+ print(f"Default result: {result1}")
+
+ print("\nTesting first_tool behavior:")
+ result2 = await client.execute_workflow(
+ ForcingToolUseWorkflow.run,
+ "first_tool",
+ id="forcing-tool-use-workflow-first-tool",
+ task_queue="openai-agents-patterns-task-queue",
+ )
+ print(f"First tool result: {result2}")
+
+ print("\nTesting custom behavior:")
+ result3 = await client.execute_workflow(
+ ForcingToolUseWorkflow.run,
+ "custom",
+ id="forcing-tool-use-workflow-custom",
+ task_queue="openai-agents-patterns-task-queue",
+ )
+ print(f"Custom result: {result3}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/agent_patterns/run_input_guardrails_workflow.py b/openai_agents/agent_patterns/run_input_guardrails_workflow.py
new file mode 100644
index 00000000..82536e26
--- /dev/null
+++ b/openai_agents/agent_patterns/run_input_guardrails_workflow.py
@@ -0,0 +1,40 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.agent_patterns.workflows.input_guardrails_workflow import (
+ InputGuardrailsWorkflow,
+)
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow with a normal question (should pass)
+ result1 = await client.execute_workflow(
+ InputGuardrailsWorkflow.run,
+ "What's the capital of California?",
+ id="input-guardrails-workflow-normal",
+ task_queue="openai-agents-patterns-task-queue",
+ )
+ print(f"Normal question result: {result1}")
+
+ # Execute a workflow with a math homework question (should be blocked)
+ result2 = await client.execute_workflow(
+ InputGuardrailsWorkflow.run,
+ "Can you help me solve for x: 2x + 5 = 11?",
+ id="input-guardrails-workflow-blocked",
+ task_queue="openai-agents-patterns-task-queue",
+ )
+ print(f"Math homework result: {result2}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/agent_patterns/run_llm_as_a_judge_workflow.py b/openai_agents/agent_patterns/run_llm_as_a_judge_workflow.py
new file mode 100644
index 00000000..aa6d97a6
--- /dev/null
+++ b/openai_agents/agent_patterns/run_llm_as_a_judge_workflow.py
@@ -0,0 +1,31 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.agent_patterns.workflows.llm_as_a_judge_workflow import (
+ LLMAsAJudgeWorkflow,
+)
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ LLMAsAJudgeWorkflow.run,
+ "A thrilling adventure story about pirates searching for treasure",
+ id="llm-as-a-judge-workflow-example",
+ task_queue="openai-agents-patterns-task-queue",
+ )
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/agent_patterns/run_output_guardrails_workflow.py b/openai_agents/agent_patterns/run_output_guardrails_workflow.py
new file mode 100644
index 00000000..16d64764
--- /dev/null
+++ b/openai_agents/agent_patterns/run_output_guardrails_workflow.py
@@ -0,0 +1,40 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.agent_patterns.workflows.output_guardrails_workflow import (
+ OutputGuardrailsWorkflow,
+)
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow with a normal question (should pass)
+ result1 = await client.execute_workflow(
+ OutputGuardrailsWorkflow.run,
+ "What's the capital of California?",
+ id="output-guardrails-workflow-normal",
+ task_queue="openai-agents-patterns-task-queue",
+ )
+ print(f"Normal question result: {result1}")
+
+ # Execute a workflow with input that might trigger sensitive data output
+ result2 = await client.execute_workflow(
+ OutputGuardrailsWorkflow.run,
+ "My phone number is 650-123-4567. Where do you think I live?",
+ id="output-guardrails-workflow-sensitive",
+ task_queue="openai-agents-patterns-task-queue",
+ )
+ print(f"Sensitive data result: {result2}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/agent_patterns/run_parallelization_workflow.py b/openai_agents/agent_patterns/run_parallelization_workflow.py
new file mode 100644
index 00000000..5b8d9f5d
--- /dev/null
+++ b/openai_agents/agent_patterns/run_parallelization_workflow.py
@@ -0,0 +1,31 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.agent_patterns.workflows.parallelization_workflow import (
+ ParallelizationWorkflow,
+)
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ ParallelizationWorkflow.run,
+ "Hello, world! How are you today?",
+ id="parallelization-workflow-example",
+ task_queue="openai-agents-patterns-task-queue",
+ )
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/agent_patterns/run_routing_workflow.py b/openai_agents/agent_patterns/run_routing_workflow.py
new file mode 100644
index 00000000..51c28233
--- /dev/null
+++ b/openai_agents/agent_patterns/run_routing_workflow.py
@@ -0,0 +1,29 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.agent_patterns.workflows.routing_workflow import RoutingWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ RoutingWorkflow.run,
+ "Bonjour! Comment allez-vous aujourd'hui?",
+ id="routing-workflow-example",
+ task_queue="openai-agents-patterns-task-queue",
+ )
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/agent_patterns/run_worker.py b/openai_agents/agent_patterns/run_worker.py
new file mode 100644
index 00000000..4edb8ae4
--- /dev/null
+++ b/openai_agents/agent_patterns/run_worker.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import asyncio
+from datetime import timedelta
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import ModelActivityParameters, OpenAIAgentsPlugin
+from temporalio.worker import Worker
+
+from openai_agents.agent_patterns.workflows.agents_as_tools_workflow import (
+ AgentsAsToolsWorkflow,
+)
+from openai_agents.agent_patterns.workflows.deterministic_workflow import (
+ DeterministicWorkflow,
+)
+from openai_agents.agent_patterns.workflows.forcing_tool_use_workflow import (
+ ForcingToolUseWorkflow,
+)
+from openai_agents.agent_patterns.workflows.input_guardrails_workflow import (
+ InputGuardrailsWorkflow,
+)
+from openai_agents.agent_patterns.workflows.llm_as_a_judge_workflow import (
+ LLMAsAJudgeWorkflow,
+)
+from openai_agents.agent_patterns.workflows.output_guardrails_workflow import (
+ OutputGuardrailsWorkflow,
+)
+from openai_agents.agent_patterns.workflows.parallelization_workflow import (
+ ParallelizationWorkflow,
+)
+from openai_agents.agent_patterns.workflows.routing_workflow import RoutingWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(
+ model_params=ModelActivityParameters(
+ start_to_close_timeout=timedelta(seconds=30)
+ )
+ ),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="openai-agents-patterns-task-queue",
+ workflows=[
+ AgentsAsToolsWorkflow,
+ DeterministicWorkflow,
+ ParallelizationWorkflow,
+ LLMAsAJudgeWorkflow,
+ ForcingToolUseWorkflow,
+ InputGuardrailsWorkflow,
+ OutputGuardrailsWorkflow,
+ RoutingWorkflow,
+ ],
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/agent_patterns/workflows/agents_as_tools_workflow.py b/openai_agents/agent_patterns/workflows/agents_as_tools_workflow.py
new file mode 100644
index 00000000..db849c1c
--- /dev/null
+++ b/openai_agents/agent_patterns/workflows/agents_as_tools_workflow.py
@@ -0,0 +1,85 @@
+from agents import Agent, ItemHelpers, MessageOutputItem, RunConfig, Runner, trace
+from temporalio import workflow
+
+"""
+This example shows the agents-as-tools pattern. The frontline agent receives a user message and
+then picks which agents to call, as tools. In this case, it picks from a set of translation
+agents.
+"""
+
+
+def orchestrator_agent() -> Agent:
+ spanish_agent = Agent(
+ name="spanish_agent",
+ instructions="You translate the user's message to Spanish",
+ handoff_description="An english to spanish translator",
+ )
+
+ french_agent = Agent(
+ name="french_agent",
+ instructions="You translate the user's message to French",
+ handoff_description="An english to french translator",
+ )
+
+ italian_agent = Agent(
+ name="italian_agent",
+ instructions="You translate the user's message to Italian",
+ handoff_description="An english to italian translator",
+ )
+
+ orchestrator_agent = Agent(
+ name="orchestrator_agent",
+ instructions=(
+ "You are a translation agent. You use the tools given to you to translate."
+ "If asked for multiple translations, you call the relevant tools in order."
+ "You never translate on your own, you always use the provided tools."
+ ),
+ tools=[
+ spanish_agent.as_tool(
+ tool_name="translate_to_spanish",
+ tool_description="Translate the user's message to Spanish",
+ ),
+ french_agent.as_tool(
+ tool_name="translate_to_french",
+ tool_description="Translate the user's message to French",
+ ),
+ italian_agent.as_tool(
+ tool_name="translate_to_italian",
+ tool_description="Translate the user's message to Italian",
+ ),
+ ],
+ )
+ return orchestrator_agent
+
+
+def synthesizer_agent() -> Agent:
+ return Agent(
+ name="synthesizer_agent",
+ instructions="You inspect translations, correct them if needed, and produce a final concatenated response.",
+ )
+
+
+@workflow.defn
+class AgentsAsToolsWorkflow:
+ @workflow.run
+ async def run(self, msg: str) -> str:
+ config = RunConfig()
+
+ # Run the entire orchestration in a single trace
+ with trace("Orchestrator evaluator"):
+ orchestrator = orchestrator_agent()
+ synthesizer = synthesizer_agent()
+
+ orchestrator_result = await Runner.run(orchestrator, msg, run_config=config)
+
+ for item in orchestrator_result.new_items:
+ if isinstance(item, MessageOutputItem):
+ text = ItemHelpers.text_message_output(item)
+ if text:
+ workflow.logger.info(f" - Translation step: {text}")
+
+ synthesizer_result = await Runner.run(
+ synthesizer, orchestrator_result.to_input_list(), run_config=config
+ )
+
+ return synthesizer_result.final_output
diff --git a/openai_agents/agent_patterns/workflows/deterministic_workflow.py b/openai_agents/agent_patterns/workflows/deterministic_workflow.py
new file mode 100644
index 00000000..339e7a0f
--- /dev/null
+++ b/openai_agents/agent_patterns/workflows/deterministic_workflow.py
@@ -0,0 +1,90 @@
+from agents import Agent, RunConfig, Runner, trace
+from pydantic import BaseModel
+from temporalio import workflow
+
+"""
+This example demonstrates a deterministic flow, where each step is performed by an agent.
+1. The first agent generates a story outline
+2. We feed the outline into the second agent
+3. The second agent checks if the outline is good quality and if it is a scifi story
+4. If the outline is not good quality or not a scifi story, we stop here
+5. If the outline is good quality and a scifi story, we feed the outline into the third agent
+6. The third agent writes the story
+
+*Adapted from the OpenAI Agents SDK deterministic pattern example*
+"""
+
+
+class OutlineCheckerOutput(BaseModel):
+ good_quality: bool
+ is_scifi: bool
+
+
+def story_outline_agent() -> Agent:
+ return Agent(
+ name="story_outline_agent",
+ instructions="Generate a very short story outline based on the user's input.",
+ )
+
+
+def outline_checker_agent() -> Agent:
+ return Agent(
+ name="outline_checker_agent",
+ instructions="Read the given story outline, and judge the quality. Also, determine if it is a scifi story.",
+ output_type=OutlineCheckerOutput,
+ )
+
+
+def story_agent() -> Agent:
+ return Agent(
+ name="story_agent",
+ instructions="Write a short story based on the given outline.",
+ output_type=str,
+ )
+
+
+@workflow.defn
+class DeterministicWorkflow:
+ @workflow.run
+ async def run(self, input_prompt: str) -> str:
+ config = RunConfig()
+
+ # Ensure the entire workflow is a single trace
+ with trace("Deterministic story flow"):
+ # 1. Generate an outline
+ outline_result = await Runner.run(
+ story_outline_agent(),
+ input_prompt,
+ run_config=config,
+ )
+ workflow.logger.info("Outline generated")
+
+ # 2. Check the outline
+ outline_checker_result = await Runner.run(
+ outline_checker_agent(),
+ outline_result.final_output,
+ run_config=config,
+ )
+
+ # 3. Add a gate to stop if the outline is not good quality or not a scifi story
+ assert isinstance(outline_checker_result.final_output, OutlineCheckerOutput)
+ if not outline_checker_result.final_output.good_quality:
+ workflow.logger.info("Outline is not good quality, so we stop here.")
+ return "Story generation stopped: Outline quality insufficient."
+
+ if not outline_checker_result.final_output.is_scifi:
+ workflow.logger.info("Outline is not a scifi story, so we stop here.")
+ return "Story generation stopped: Outline is not science fiction."
+
+ workflow.logger.info(
+ "Outline is good quality and a scifi story, so we continue to write the story."
+ )
+
+ # 4. Write the story
+ story_result = await Runner.run(
+ story_agent(),
+ outline_result.final_output,
+ run_config=config,
+ )
+
+ return f"Final story: {story_result.final_output}"
diff --git a/openai_agents/agent_patterns/workflows/forcing_tool_use_workflow.py b/openai_agents/agent_patterns/workflows/forcing_tool_use_workflow.py
new file mode 100644
index 00000000..9d3aee3b
--- /dev/null
+++ b/openai_agents/agent_patterns/workflows/forcing_tool_use_workflow.py
@@ -0,0 +1,84 @@
+from typing import Any, Literal
+
+from agents import (
+ Agent,
+ FunctionToolResult,
+ ModelSettings,
+ RunConfig,
+ RunContextWrapper,
+ Runner,
+ ToolsToFinalOutputFunction,
+ ToolsToFinalOutputResult,
+ function_tool,
+)
+from pydantic import BaseModel
+from temporalio import workflow
+
+"""
+This example shows how to force the agent to use a tool. It uses `ModelSettings(tool_choice="required")`
+to force the agent to use any tool.
+
+You can run it with 3 options:
+1. `default`: The default behavior, which is to send the tool output to the LLM. In this case,
+ `tool_choice` is not set, because otherwise it would result in an infinite loop - the LLM would
+ call the tool, the tool would run and send the results to the LLM, and that would repeat
+ (because the model is forced to use a tool every time.)
+2. `first_tool_result`: The first tool result is used as the final output.
+3. `custom`: A custom tool use behavior function is used. The custom function receives all the tool
+ results, and chooses to use the first tool result to generate the final output.
+
+*Adapted from the OpenAI Agents SDK forcing_tool_use pattern example*
+"""
+
+
+class Weather(BaseModel):
+ city: str
+ temperature_range: str
+ conditions: str
+
+
+@function_tool
+def get_weather(city: str) -> Weather:
+ workflow.logger.info("[debug] get_weather called")
+ return Weather(city=city, temperature_range="14-20C", conditions="Sunny with wind")
+
+
+async def custom_tool_use_behavior(
+ context: RunContextWrapper[Any], results: list[FunctionToolResult]
+) -> ToolsToFinalOutputResult:
+ weather: Weather = results[0].output
+ return ToolsToFinalOutputResult(
+ is_final_output=True, final_output=f"{weather.city} is {weather.conditions}."
+ )
+
+
+@workflow.defn
+class ForcingToolUseWorkflow:
+ @workflow.run
+ async def run(self, tool_use_behavior: str = "default") -> str:
+ config = RunConfig()
+
+ if tool_use_behavior == "default":
+ behavior: (
+ Literal["run_llm_again", "stop_on_first_tool"]
+ | ToolsToFinalOutputFunction
+ ) = "run_llm_again"
+ elif tool_use_behavior == "first_tool":
+ behavior = "stop_on_first_tool"
+ elif tool_use_behavior == "custom":
+ behavior = custom_tool_use_behavior
+
+ agent = Agent(
+ name="Weather agent",
+ instructions="You are a helpful agent.",
+ tools=[get_weather],
+ tool_use_behavior=behavior,
+ model_settings=ModelSettings(
+ tool_choice="required" if tool_use_behavior != "default" else None
+ ),
+ )
+
+ result = await Runner.run(
+ agent, input="What's the weather in Tokyo?", run_config=config
+ )
+ return str(result.final_output)
diff --git a/openai_agents/agent_patterns/workflows/input_guardrails_workflow.py b/openai_agents/agent_patterns/workflows/input_guardrails_workflow.py
new file mode 100644
index 00000000..83677840
--- /dev/null
+++ b/openai_agents/agent_patterns/workflows/input_guardrails_workflow.py
@@ -0,0 +1,87 @@
+from agents import (
+ Agent,
+ GuardrailFunctionOutput,
+ InputGuardrailTripwireTriggered,
+ RunConfig,
+ RunContextWrapper,
+ Runner,
+ TResponseInputItem,
+ input_guardrail,
+)
+from pydantic import BaseModel
+from temporalio import workflow
+
+"""
+This example shows how to use input guardrails.
+
+Guardrails are checks that run in parallel to the agent's execution.
+They can be used to do things like:
+- Check if input messages are off-topic
+- Check that input messages don't violate any policies
+- Take over control of the agent's execution if an unexpected input is detected
+
+In this example, we'll setup an input guardrail that trips if the user is asking to do math homework.
+If the guardrail trips, we'll respond with a refusal message.
+
+*Adapted from the OpenAI Agents SDK input_guardrails pattern example*
+"""
+
+
+class MathHomeworkOutput(BaseModel):
+ reasoning: str
+ is_math_homework: bool
+
+
+guardrail_agent = Agent(
+ name="Guardrail check",
+ instructions="Check if the user is asking you to do their math homework.",
+ output_type=MathHomeworkOutput,
+)
+
+
+@input_guardrail
+async def math_guardrail(
+ context: RunContextWrapper[None],
+ agent: Agent,
+ input: str | list[TResponseInputItem],
+) -> GuardrailFunctionOutput:
+ """This is an input guardrail function, which happens to call an agent to check if the input
+ is a math homework question.
+ """
+ result = await Runner.run(guardrail_agent, input, context=context.context)
+ final_output = result.final_output_as(MathHomeworkOutput)
+
+ return GuardrailFunctionOutput(
+ output_info=final_output,
+ tripwire_triggered=final_output.is_math_homework,
+ )
+
+
+@workflow.defn
+class InputGuardrailsWorkflow:
+ @workflow.run
+ async def run(self, user_input: str) -> str:
+ config = RunConfig()
+ agent = Agent(
+ name="Customer support agent",
+ instructions="You are a customer support agent. You help customers with their questions.",
+ input_guardrails=[math_guardrail],
+ )
+
+ input_data: list[TResponseInputItem] = [
+ {
+ "role": "user",
+ "content": user_input,
+ }
+ ]
+
+ try:
+ result = await Runner.run(agent, input_data, run_config=config)
+ return str(result.final_output)
+ except InputGuardrailTripwireTriggered:
+ # If the guardrail triggered, we instead return a refusal message
+ message = "Sorry, I can't help you with your math homework."
+ workflow.logger.info(
+ "Input guardrail triggered - refusing to help with math homework"
+ )
+ return message
diff --git a/openai_agents/agent_patterns/workflows/llm_as_a_judge_workflow.py b/openai_agents/agent_patterns/workflows/llm_as_a_judge_workflow.py
new file mode 100644
index 00000000..7ad536f2
--- /dev/null
+++ b/openai_agents/agent_patterns/workflows/llm_as_a_judge_workflow.py
@@ -0,0 +1,86 @@
+from dataclasses import dataclass
+from typing import Literal
+
+from agents import Agent, ItemHelpers, RunConfig, Runner, TResponseInputItem, trace
+from temporalio import workflow
+
+"""
+This example shows the LLM as a judge pattern. The first agent generates an outline for a story.
+The second agent judges the outline and provides feedback. We loop until the judge is satisfied
+with the outline.
+
+*Adapted from the OpenAI Agents SDK llm_as_a_judge pattern example*
+"""
+
+
+@dataclass
+class EvaluationFeedback:
+ feedback: str
+ score: Literal["pass", "needs_improvement", "fail"]
+
+
+def story_outline_generator() -> Agent:
+ return Agent[None](
+ name="story_outline_generator",
+ instructions=(
+ "You generate a very short story outline based on the user's input."
+ "If there is any feedback provided, use it to improve the outline."
+ ),
+ )
+
+
+def evaluator() -> Agent:
+ return Agent[None](
+ name="evaluator",
+ instructions=(
+ "You evaluate a story outline and decide if it's good enough."
+ "If it's not good enough, you provide feedback on what needs to be improved."
+ "Never give it a pass on the first try. After 5 attempts, you can give it a pass if story outline is good enough - do not go for perfection"
+ ),
+ output_type=EvaluationFeedback,
+ )
+
+
+@workflow.defn
+class LLMAsAJudgeWorkflow:
+ @workflow.run
+ async def run(self, msg: str) -> str:
+ config = RunConfig()
+ input_items: list[TResponseInputItem] = [{"content": msg, "role": "user"}]
+ latest_outline: str | None = None
+
+ # We'll run the entire workflow in a single trace
+ with trace("LLM as a judge"):
+ while True:
+ story_outline_result = await Runner.run(
+ story_outline_generator(),
+ input_items,
+ run_config=config,
+ )
+
+ input_items = story_outline_result.to_input_list()
+ latest_outline = ItemHelpers.text_message_outputs(
+ story_outline_result.new_items
+ )
+ workflow.logger.info("Story outline generated")
+
+ evaluator_result = await Runner.run(
+ evaluator(),
+ input_items,
+ run_config=config,
+ )
+ result: EvaluationFeedback = evaluator_result.final_output
+
+ workflow.logger.info(f"Evaluator score: {result.score}")
+
+ if result.score == "pass":
+ workflow.logger.info("Story outline is good enough, exiting.")
+ break
+
+ workflow.logger.info("Re-running with feedback")
+
+ input_items.append(
+ {"content": f"Feedback: {result.feedback}", "role": "user"}
+ )
+
+ return f"Final story outline: {latest_outline}"
diff --git a/openai_agents/agent_patterns/workflows/output_guardrails_workflow.py b/openai_agents/agent_patterns/workflows/output_guardrails_workflow.py
new file mode 100644
index 00000000..58a306c9
--- /dev/null
+++ b/openai_agents/agent_patterns/workflows/output_guardrails_workflow.py
@@ -0,0 +1,78 @@
+from agents import (
+ Agent,
+ GuardrailFunctionOutput,
+ OutputGuardrailTripwireTriggered,
+ RunConfig,
+ RunContextWrapper,
+ Runner,
+ output_guardrail,
+)
+from pydantic import BaseModel, Field
+from temporalio import workflow
+
+"""
+This example shows how to use output guardrails.
+
+Output guardrails are checks that run on the final output of an agent.
+They can be used to do things like:
+- Check if the output contains sensitive data
+- Check if the output is a valid response to the user's message
+
+In this example, we'll use a (contrived) example where we check if the agent's response contains
+a phone number.
+
+*Adapted from the OpenAI Agents SDK output_guardrails pattern example*
+"""
+
+
+class MessageOutput(BaseModel):
+ reasoning: str = Field(
+ description="Thoughts on how to respond to the user's message"
+ )
+ response: str = Field(description="The response to the user's message")
+ user_name: str | None = Field(
+ description="The name of the user who sent the message, if known"
+ )
+
+
+@output_guardrail
+async def sensitive_data_check(
+ context: RunContextWrapper, agent: Agent, output: MessageOutput
+) -> GuardrailFunctionOutput:
+ phone_number_in_response = "650" in output.response
+ phone_number_in_reasoning = "650" in output.reasoning
+
+ return GuardrailFunctionOutput(
+ output_info={
+ "phone_number_in_response": phone_number_in_response,
+ "phone_number_in_reasoning": phone_number_in_reasoning,
+ },
+ tripwire_triggered=phone_number_in_response or phone_number_in_reasoning,
+ )
+
+
+def assistant_agent() -> Agent:
+ return Agent(
+ name="Assistant",
+ instructions="You are a helpful assistant.",
+ output_type=MessageOutput,
+ output_guardrails=[sensitive_data_check],
+ )
+
+
+@workflow.defn
+class OutputGuardrailsWorkflow:
+ @workflow.run
+ async def run(self, user_input: str) -> str:
+ config = RunConfig()
+ agent = assistant_agent()
+
+ try:
+ result = await Runner.run(agent, user_input, run_config=config)
+ output = result.final_output_as(MessageOutput)
+ return f"Response: {output.response}"
+ except OutputGuardrailTripwireTriggered as e:
+ workflow.logger.info(
+ f"Output guardrail triggered. Info: {e.guardrail_result.output.output_info}"
+ )
+ return f"Output guardrail triggered due to sensitive data detection. Info: {e.guardrail_result.output.output_info}"
diff --git a/openai_agents/agent_patterns/workflows/parallelization_workflow.py b/openai_agents/agent_patterns/workflows/parallelization_workflow.py
new file mode 100644
index 00000000..5a07a030
--- /dev/null
+++ b/openai_agents/agent_patterns/workflows/parallelization_workflow.py
@@ -0,0 +1,70 @@
+import asyncio
+
+from agents import Agent, ItemHelpers, RunConfig, Runner, trace
+from temporalio import workflow
+
+"""
+This example shows the parallelization pattern. We run the agent three times in parallel, and pick
+the best result.
+
+*Adapted from the OpenAI Agents SDK parallelization pattern example*
+"""
+
+
+def spanish_agent() -> Agent:
+ return Agent(
+ name="spanish_agent",
+ instructions="You translate the user's message to Spanish",
+ )
+
+
+def translation_picker() -> Agent:
+ return Agent(
+ name="translation_picker",
+ instructions="You pick the best Spanish translation from the given options.",
+ )
+
+
+@workflow.defn
+class ParallelizationWorkflow:
+ @workflow.run
+ async def run(self, msg: str) -> str:
+ config = RunConfig()
+
+ # Ensure the entire workflow is a single trace
+ with trace("Parallel translation"):
+ # Run three translation agents in parallel
+ res_1, res_2, res_3 = await asyncio.gather(
+ Runner.run(
+ spanish_agent(),
+ msg,
+ run_config=config,
+ ),
+ Runner.run(
+ spanish_agent(),
+ msg,
+ run_config=config,
+ ),
+ Runner.run(
+ spanish_agent(),
+ msg,
+ run_config=config,
+ ),
+ )
+
+ outputs = [
+ ItemHelpers.text_message_outputs(res_1.new_items),
+ ItemHelpers.text_message_outputs(res_2.new_items),
+ ItemHelpers.text_message_outputs(res_3.new_items),
+ ]
+
+ translations = "\n\n".join(outputs)
+ workflow.logger.info(f"Generated translations:\n{translations}")
+
+ best_translation = await Runner.run(
+ translation_picker(),
+ f"Input: {msg}\n\nTranslations:\n{translations}",
+ run_config=config,
+ )
+
+ return f"Best translation: {best_translation.final_output}"
diff --git a/openai_agents/agent_patterns/workflows/routing_workflow.py b/openai_agents/agent_patterns/workflows/routing_workflow.py
new file mode 100644
index 00000000..4d821349
--- /dev/null
+++ b/openai_agents/agent_patterns/workflows/routing_workflow.py
@@ -0,0 +1,67 @@
+from agents import Agent, RunConfig, Runner, TResponseInputItem, trace
+from temporalio import workflow
+
+"""
+This example shows the handoffs/routing pattern. The triage agent receives the first message, and
+then hands off to the appropriate agent based on the language of the request.
+
+Note: This is adapted from the original streaming version to work with Temporal's non-streaming approach.
+
+*Adapted from the OpenAI Agents SDK routing pattern example*
+"""
+
+
+def french_agent() -> Agent:
+ return Agent(
+ name="french_agent",
+ instructions="You only speak French",
+ )
+
+
+def spanish_agent() -> Agent:
+ return Agent(
+ name="spanish_agent",
+ instructions="You only speak Spanish",
+ )
+
+
+def english_agent() -> Agent:
+ return Agent(
+ name="english_agent",
+ instructions="You only speak English",
+ )
+
+
+def triage_agent() -> Agent:
+ return Agent(
+ name="triage_agent",
+ instructions="Handoff to the appropriate agent based on the language of the request.",
+ handoffs=[french_agent(), spanish_agent(), english_agent()],
+ )
+
+
+@workflow.defn
+class RoutingWorkflow:
+ @workflow.run
+ async def run(self, msg: str) -> str:
+ config = RunConfig()
+
+ with trace("Routing example"):
+ inputs: list[TResponseInputItem] = [{"content": msg, "role": "user"}]
+
+ # Run the triage agent to determine which language agent to handoff to
+ result = await Runner.run(
+ triage_agent(),
+ input=inputs,
+ run_config=config,
+ )
+
+ # Get the final response after handoff
+ # Note: current_agent attribute may not be available in all SDK versions
+ workflow.logger.info("Handoff completed")
+
+ # Convert result to proper input format for next agent
+ inputs = result.to_input_list()
+
+ # Return the result from the handoff (either the handoff agent's response or triage response)
+ return f"Response: {result.final_output}"
diff --git a/openai_agents/basic/README.md b/openai_agents/basic/README.md
new file mode 100644
index 00000000..e593ee48
--- /dev/null
+++ b/openai_agents/basic/README.md
@@ -0,0 +1,79 @@
+# Basic Agent Examples
+
+Simple examples to get started with OpenAI Agents SDK integrated with Temporal workflows.
+
+*Adapted from [OpenAI Agents SDK basic examples](https://github.com/openai/openai-agents-python/tree/main/examples/basic)*
+
+Before running these examples, be sure to review the [prerequisites and background on the integration](../README.md).
+
+## Running the Examples
+
+First, start the worker (supports all basic examples):
+```bash
+uv run openai_agents/basic/run_worker.py
+```
+
+Then run individual examples in separate terminals:
+
+### Hello World Agent
+Basic agent that only responds in haikus:
+```bash
+uv run openai_agents/basic/run_hello_world_workflow.py
+```
+
+### Tools Agent
+Agent with access to external tools (simulated weather API):
+```bash
+uv run openai_agents/basic/run_tools_workflow.py
+```
+
+### Agent Lifecycle with Hooks
+Demonstrates agent lifecycle events and handoffs between agents:
+```bash
+uv run openai_agents/basic/run_agent_lifecycle_workflow.py
+```
+
+### Lifecycle with Usage Tracking
+Shows detailed usage tracking with RunHooks (requests, tokens, etc.):
+```bash
+uv run openai_agents/basic/run_lifecycle_workflow.py
+```
+
+### Dynamic System Prompts
+Agent with dynamic instruction generation based on context (haiku/pirate/robot):
+```bash
+uv run openai_agents/basic/run_dynamic_system_prompt_workflow.py
+```
+
+### Non-Strict Output Types
+Demonstrates different JSON schema validation approaches:
+```bash
+uv run openai_agents/basic/run_non_strict_output_workflow.py
+```
+
+Note: `CustomOutputSchema` is not supported by the Temporal OpenAI Agents SDK integration and is omitted in this example.
+
+### Image Processing - Local
+Process local image files with AI vision:
+```bash
+uv run openai_agents/basic/run_local_image_workflow.py
+```
+
+### Image Processing - Remote
+Process remote image URLs with AI vision:
+```bash
+uv run openai_agents/basic/run_remote_image_workflow.py
+```
+
+### Previous Response ID
+Demonstrates conversation continuity using response IDs:
+```bash
+uv run openai_agents/basic/run_previous_response_id_workflow.py
+```
+
+## Omitted Examples
+
+The following examples from the [reference repository](https://github.com/openai/openai-agents-python/tree/main/examples/basic) are not included in this Temporal adaptation:
+
+- **Session** - Stores state in local SQLite database, not appropriate for distributed workflows
+- **Stream Items/Stream Text** - Streaming is not supported in Temporal OpenAI Agents SDK integration
\ No newline at end of file
diff --git a/openai_agents/basic/activities/get_weather_activity.py b/openai_agents/basic/activities/get_weather_activity.py
new file mode 100644
index 00000000..c8be473c
--- /dev/null
+++ b/openai_agents/basic/activities/get_weather_activity.py
@@ -0,0 +1,18 @@
+from dataclasses import dataclass
+
+from temporalio import activity
+
+
+@dataclass
+class Weather:
+ city: str
+ temperature_range: str
+ conditions: str
+
+
+@activity.defn
+async def get_weather(city: str) -> Weather:
+ """
+ Get the weather for a given city.
+ """
+ return Weather(city=city, temperature_range="14-20C", conditions="Sunny with wind.")
diff --git a/openai_agents/basic/activities/image_activities.py b/openai_agents/basic/activities/image_activities.py
new file mode 100644
index 00000000..23c770b1
--- /dev/null
+++ b/openai_agents/basic/activities/image_activities.py
@@ -0,0 +1,19 @@
+import base64
+
+from temporalio import activity
+
+
+@activity.defn
+async def read_image_as_base64(image_path: str) -> str:
+ """
+ Read an image file and convert it to base64 string.
+
+ Args:
+ image_path: Path to the image file
+
+ Returns:
+ Base64 encoded string of the image
+ """
+ with open(image_path, "rb") as image_file:
+ encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
+ return encoded_string
diff --git a/openai_agents/basic/activities/math_activities.py b/openai_agents/basic/activities/math_activities.py
new file mode 100644
index 00000000..1e875b23
--- /dev/null
+++ b/openai_agents/basic/activities/math_activities.py
@@ -0,0 +1,15 @@
+import random
+
+from temporalio import activity
+
+
+@activity.defn
+async def random_number(max_value: int) -> int:
+ """Generate a random number up to the provided maximum."""
+ return random.randint(0, max_value)
+
+
+@activity.defn
+async def multiply_by_two(x: int) -> int:
+ """Simple multiplication by two."""
+ return x * 2
diff --git a/openai_agents/basic/media/image_bison.jpg b/openai_agents/basic/media/image_bison.jpg
new file mode 100644
index 00000000..b113c91f
Binary files /dev/null and b/openai_agents/basic/media/image_bison.jpg differ
diff --git a/openai_agents/basic/run_agent_lifecycle_workflow.py b/openai_agents/basic/run_agent_lifecycle_workflow.py
new file mode 100644
index 00000000..2cd16c60
--- /dev/null
+++ b/openai_agents/basic/run_agent_lifecycle_workflow.py
@@ -0,0 +1,31 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from openai_agents.basic.workflows.agent_lifecycle_workflow import (
+ AgentLifecycleWorkflow,
+)
+
+
+async def main() -> None:
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ user_input = input("Enter a max number: ")
+ max_number = int(user_input)
+
+ result = await client.execute_workflow(
+ AgentLifecycleWorkflow.run,
+ max_number,
+ id="agent-lifecycle-workflow",
+ task_queue="openai-agents-basic-task-queue",
+ )
+
+ print(f"Final result: {result}")
+ print("Done!")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/basic/run_dynamic_system_prompt_workflow.py b/openai_agents/basic/run_dynamic_system_prompt_workflow.py
new file mode 100644
index 00000000..46c53c6d
--- /dev/null
+++ b/openai_agents/basic/run_dynamic_system_prompt_workflow.py
@@ -0,0 +1,41 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.basic.workflows.dynamic_system_prompt_workflow import (
+ DynamicSystemPromptWorkflow,
+)
+
+
+async def main():
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ user_message = "Tell me a joke."
+
+ result = await client.execute_workflow(
+ DynamicSystemPromptWorkflow.run,
+ user_message,
+ id="dynamic-prompt-workflow",
+ task_queue="openai-agents-basic-task-queue",
+ )
+ print(result)
+ print()
+
+ # Run with specific style
+ result = await client.execute_workflow(
+ DynamicSystemPromptWorkflow.run,
+ args=[user_message, "pirate"],
+ id="dynamic-prompt-pirate-workflow",
+ task_queue="openai-agents-basic-task-queue",
+ )
+ print(result)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/basic/run_hello_world_workflow.py b/openai_agents/basic/run_hello_world_workflow.py
new file mode 100644
index 00000000..0662a4fa
--- /dev/null
+++ b/openai_agents/basic/run_hello_world_workflow.py
@@ -0,0 +1,29 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.basic.workflows.hello_world_workflow import HelloWorldAgent
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ HelloWorldAgent.run,
+ "Tell me about recursion in programming.",
+ id="my-workflow-id",
+ task_queue="openai-agents-basic-task-queue",
+ )
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/basic/run_lifecycle_workflow.py b/openai_agents/basic/run_lifecycle_workflow.py
new file mode 100644
index 00000000..00286ece
--- /dev/null
+++ b/openai_agents/basic/run_lifecycle_workflow.py
@@ -0,0 +1,31 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.basic.workflows.lifecycle_workflow import LifecycleWorkflow
+
+
+async def main():
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ user_input = input("Enter a max number: ")
+ max_number = int(user_input)
+
+ result = await client.execute_workflow(
+ LifecycleWorkflow.run,
+ max_number,
+ id="lifecycle-workflow",
+ task_queue="openai-agents-basic-task-queue",
+ )
+
+ print(f"Final result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/basic/run_local_image_workflow.py b/openai_agents/basic/run_local_image_workflow.py
new file mode 100644
index 00000000..6dfec809
--- /dev/null
+++ b/openai_agents/basic/run_local_image_workflow.py
@@ -0,0 +1,32 @@
+import asyncio
+import os
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.basic.workflows.local_image_workflow import LocalImageWorkflow
+
+
+async def main():
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Use the media file from the original example
+ image_path = os.path.join(os.path.dirname(__file__), "media/image_bison.jpg")
+
+ result = await client.execute_workflow(
+ LocalImageWorkflow.run,
+ args=[image_path, "What do you see in this image?"],
+ id="local-image-workflow",
+ task_queue="openai-agents-basic-task-queue",
+ )
+
+ print(f"Agent response: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/basic/run_non_strict_output_workflow.py b/openai_agents/basic/run_non_strict_output_workflow.py
new file mode 100644
index 00000000..192e1206
--- /dev/null
+++ b/openai_agents/basic/run_non_strict_output_workflow.py
@@ -0,0 +1,35 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.basic.workflows.non_strict_output_workflow import (
+ NonStrictOutputWorkflow,
+)
+
+
+async def main():
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ input_message = "Tell me 3 short jokes."
+
+ result = await client.execute_workflow(
+ NonStrictOutputWorkflow.run,
+ input_message,
+ id="non-strict-output-workflow",
+ task_queue="openai-agents-basic-task-queue",
+ )
+
+ print("=== Non-Strict Output Type Results ===")
+ for key, value in result.items():
+ print(f"{key}: {value}")
+ print()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/basic/run_previous_response_id_workflow.py b/openai_agents/basic/run_previous_response_id_workflow.py
new file mode 100644
index 00000000..ddae5107
--- /dev/null
+++ b/openai_agents/basic/run_previous_response_id_workflow.py
@@ -0,0 +1,35 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.basic.workflows.previous_response_id_workflow import (
+ PreviousResponseIdWorkflow,
+)
+
+
+async def main():
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ first_question = "What is the largest country in South America?"
+ follow_up_question = "What is the capital of that country?"
+
+ result = await client.execute_workflow(
+ PreviousResponseIdWorkflow.run,
+ args=[first_question, follow_up_question],
+ id="previous-response-id-workflow",
+ task_queue="openai-agents-basic-task-queue",
+ )
+
+ print("\nFinal results:")
+ print(f"1. {result[0]}")
+ print(f"2. {result[1]}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/basic/run_remote_image_workflow.py b/openai_agents/basic/run_remote_image_workflow.py
new file mode 100644
index 00000000..f2175f7f
--- /dev/null
+++ b/openai_agents/basic/run_remote_image_workflow.py
@@ -0,0 +1,35 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+from temporalio.envconfig import ClientConfig
+
+from openai_agents.basic.workflows.remote_image_workflow import RemoteImageWorkflow
+
+
+async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
+ client = await Client.connect(
+ **config,
+ plugins=[OpenAIAgentsPlugin()],
+ )
+
+ # Use the URL from the original example
+ image_url = (
+ "https://upload.wikimedia.org/wikipedia/commons/0/0c/GoldenGateBridge-001.jpg"
+ )
+
+ result = await client.execute_workflow(
+ RemoteImageWorkflow.run,
+ args=[image_url, "What do you see in this image?"],
+ id="remote-image-workflow",
+ task_queue="openai-agents-basic-task-queue",
+ )
+
+ print(f"Agent response: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/basic/run_tools_workflow.py b/openai_agents/basic/run_tools_workflow.py
new file mode 100644
index 00000000..9aed2dbf
--- /dev/null
+++ b/openai_agents/basic/run_tools_workflow.py
@@ -0,0 +1,30 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.basic.workflows.tools_workflow import ToolsWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ ToolsWorkflow.run,
+ "What is the weather in Tokio?",
+ id="tools-workflow",
+ task_queue="openai-agents-basic-task-queue",
+ )
+
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/basic/run_worker.py b/openai_agents/basic/run_worker.py
new file mode 100644
index 00000000..94d6a882
--- /dev/null
+++ b/openai_agents/basic/run_worker.py
@@ -0,0 +1,73 @@
+from __future__ import annotations
+
+import asyncio
+from datetime import timedelta
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import ModelActivityParameters, OpenAIAgentsPlugin
+from temporalio.worker import Worker
+
+from openai_agents.basic.activities.get_weather_activity import get_weather
+from openai_agents.basic.activities.image_activities import read_image_as_base64
+from openai_agents.basic.activities.math_activities import (
+ multiply_by_two,
+ random_number,
+)
+from openai_agents.basic.workflows.agent_lifecycle_workflow import (
+ AgentLifecycleWorkflow,
+)
+from openai_agents.basic.workflows.dynamic_system_prompt_workflow import (
+ DynamicSystemPromptWorkflow,
+)
+from openai_agents.basic.workflows.hello_world_workflow import HelloWorldAgent
+from openai_agents.basic.workflows.lifecycle_workflow import LifecycleWorkflow
+from openai_agents.basic.workflows.local_image_workflow import LocalImageWorkflow
+from openai_agents.basic.workflows.non_strict_output_workflow import (
+ NonStrictOutputWorkflow,
+)
+from openai_agents.basic.workflows.previous_response_id_workflow import (
+ PreviousResponseIdWorkflow,
+)
+from openai_agents.basic.workflows.remote_image_workflow import RemoteImageWorkflow
+from openai_agents.basic.workflows.tools_workflow import ToolsWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(
+ model_params=ModelActivityParameters(
+ start_to_close_timeout=timedelta(seconds=30)
+ )
+ ),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="openai-agents-basic-task-queue",
+ workflows=[
+ HelloWorldAgent,
+ ToolsWorkflow,
+ AgentLifecycleWorkflow,
+ DynamicSystemPromptWorkflow,
+ NonStrictOutputWorkflow,
+ LocalImageWorkflow,
+ RemoteImageWorkflow,
+ LifecycleWorkflow,
+ PreviousResponseIdWorkflow,
+ ],
+ activities=[
+ get_weather,
+ multiply_by_two,
+ random_number,
+ read_image_as_base64,
+ ],
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/basic/workflows/agent_lifecycle_workflow.py b/openai_agents/basic/workflows/agent_lifecycle_workflow.py
new file mode 100644
index 00000000..c016d150
--- /dev/null
+++ b/openai_agents/basic/workflows/agent_lifecycle_workflow.py
@@ -0,0 +1,96 @@
+from typing import Any
+
+from agents import Agent, AgentHooks, RunContextWrapper, Runner, function_tool
+from pydantic import BaseModel
+from temporalio import workflow
+
+
+class CustomAgentHooks(AgentHooks):
+ def __init__(self, display_name: str):
+ self.event_counter = 0
+ self.display_name = display_name
+
+ async def on_start(self, context: RunContextWrapper, agent: Agent) -> None:
+ self.event_counter += 1
+ print(
+ f"### ({self.display_name}) {self.event_counter}: Agent {agent.name} started"
+ )
+
+ async def on_end(
+ self, context: RunContextWrapper, agent: Agent, output: Any
+ ) -> None:
+ self.event_counter += 1
+ print(
+ f"### ({self.display_name}) {self.event_counter}: Agent {agent.name} ended with output {output}"
+ )
+
+ async def on_handoff(
+ self, context: RunContextWrapper, agent: Agent, source: Agent
+ ) -> None:
+ self.event_counter += 1
+ print(
+ f"### ({self.display_name}) {self.event_counter}: Agent {source.name} handed off to {agent.name}"
+ )
+
+ async def on_tool_start(
+ self, context: RunContextWrapper, agent: Agent, tool
+ ) -> None:
+ self.event_counter += 1
+ print(
+ f"### ({self.display_name}) {self.event_counter}: Agent {agent.name} started tool {tool.name}"
+ )
+
+ async def on_tool_end(
+ self, context: RunContextWrapper, agent: Agent, tool, result: str
+ ) -> None:
+ self.event_counter += 1
+ print(
+ f"### ({self.display_name}) {self.event_counter}: Agent {agent.name} ended tool {tool.name} with result {result}"
+ )
+
+
+@function_tool
+def random_number_tool(max: int) -> int:
+ """
+ Generate a random number up to the provided maximum.
+ """
+ return workflow.random().randint(0, max)
+
+
+@function_tool
+def multiply_by_two_tool(x: int) -> int:
+ """Simple multiplication by two."""
+ return x * 2
+
+
+class FinalResult(BaseModel):
+ number: int
+
+
+@workflow.defn
+class AgentLifecycleWorkflow:
+ @workflow.run
+ async def run(self, max_number: int) -> FinalResult:
+ multiply_agent = Agent(
+ name="Multiply Agent",
+ instructions="Multiply the number by 2 and then return the final result.",
+ tools=[multiply_by_two_tool],
+ output_type=FinalResult,
+ hooks=CustomAgentHooks(display_name="Multiply Agent"),
+ )
+
+ start_agent = Agent(
+ name="Start Agent",
+ instructions="Generate a random number. If it's even, stop. If it's odd, hand off to the multiply agent.",
+ tools=[random_number_tool],
+ output_type=FinalResult,
+ handoffs=[multiply_agent],
+ hooks=CustomAgentHooks(display_name="Start Agent"),
+ )
+
+ result = await Runner.run(
+ start_agent,
+ input=f"Generate a random number between 0 and {max_number}.",
+ )
+
+ return result.final_output
diff --git a/openai_agents/basic/workflows/dynamic_system_prompt_workflow.py b/openai_agents/basic/workflows/dynamic_system_prompt_workflow.py
new file mode 100644
index 00000000..fb7ce109
--- /dev/null
+++ b/openai_agents/basic/workflows/dynamic_system_prompt_workflow.py
@@ -0,0 +1,48 @@
+from typing import Literal, Optional
+
+from agents import Agent, RunContextWrapper, Runner
+from temporalio import workflow
+
+
+class CustomContext:
+ def __init__(self, style: Literal["haiku", "pirate", "robot"]):
+ self.style = style
+
+
+def custom_instructions(
+ run_context: RunContextWrapper[CustomContext], agent: Agent[CustomContext]
+) -> str:
+ context = run_context.context
+ if context.style == "haiku":
+ return "Only respond in haikus."
+ elif context.style == "pirate":
+ return "Respond as a pirate."
+ else:
+ return "Respond as a robot and say 'beep boop' a lot."
+
+
+@workflow.defn
+class DynamicSystemPromptWorkflow:
+ @workflow.run
+ async def run(self, user_message: str, style: Optional[str] = None) -> str:
+ if style is None:
+ selected_style: Literal["haiku", "pirate", "robot"] = (
+ workflow.random().choice(["haiku", "pirate", "robot"])
+ )
+ else:
+ # Validate that the provided style is one of the allowed values
+ if style not in ["haiku", "pirate", "robot"]:
+ raise ValueError(
+ f"Invalid style: {style}. Must be one of: haiku, pirate, robot"
+ )
+ selected_style = style # type: ignore
+
+ context = CustomContext(style=selected_style)
+
+ agent = Agent(
+ name="Chat agent",
+ instructions=custom_instructions,
+ )
+
+ result = await Runner.run(agent, user_message, context=context)
+ return f"Style: {selected_style}\nResponse: {result.final_output}"
diff --git a/openai_agents/basic/workflows/hello_world_workflow.py b/openai_agents/basic/workflows/hello_world_workflow.py
new file mode 100644
index 00000000..dd6b2e41
--- /dev/null
+++ b/openai_agents/basic/workflows/hello_world_workflow.py
@@ -0,0 +1,15 @@
+from agents import Agent, Runner
+from temporalio import workflow
+
+
+@workflow.defn
+class HelloWorldAgent:
+ @workflow.run
+ async def run(self, prompt: str) -> str:
+ agent = Agent(
+ name="Assistant",
+ instructions="You only respond in haikus.",
+ )
+
+ result = await Runner.run(agent, input=prompt)
+ return result.final_output
diff --git a/openai_agents/basic/workflows/lifecycle_workflow.py b/openai_agents/basic/workflows/lifecycle_workflow.py
new file mode 100644
index 00000000..cf72de4a
--- /dev/null
+++ b/openai_agents/basic/workflows/lifecycle_workflow.py
@@ -0,0 +1,106 @@
+from typing import Any
+
+from agents import (
+ Agent,
+ RunContextWrapper,
+ RunHooks,
+ Runner,
+ Tool,
+ Usage,
+ function_tool,
+)
+from pydantic import BaseModel
+from temporalio import workflow
+
+
+class ExampleHooks(RunHooks):
+ def __init__(self):
+ self.event_counter = 0
+
+ def _usage_to_str(self, usage: Usage) -> str:
+ return f"{usage.requests} requests, {usage.input_tokens} input tokens, {usage.output_tokens} output tokens, {usage.total_tokens} total tokens"
+
+ async def on_agent_start(self, context: RunContextWrapper, agent: Agent) -> None:
+ self.event_counter += 1
+ print(
+ f"### {self.event_counter}: Agent {agent.name} started. Usage: {self._usage_to_str(context.usage)}"
+ )
+
+ async def on_agent_end(
+ self, context: RunContextWrapper, agent: Agent, output: Any
+ ) -> None:
+ self.event_counter += 1
+ print(
+ f"### {self.event_counter}: Agent {agent.name} ended with output {output}. Usage: {self._usage_to_str(context.usage)}"
+ )
+
+ async def on_tool_start(
+ self, context: RunContextWrapper, agent: Agent, tool: Tool
+ ) -> None:
+ self.event_counter += 1
+ print(
+ f"### {self.event_counter}: Tool {tool.name} started. Usage: {self._usage_to_str(context.usage)}"
+ )
+
+ async def on_tool_end(
+ self, context: RunContextWrapper, agent: Agent, tool: Tool, result: str
+ ) -> None:
+ self.event_counter += 1
+ print(
+ f"### {self.event_counter}: Tool {tool.name} ended with result {result}. Usage: {self._usage_to_str(context.usage)}"
+ )
+
+ async def on_handoff(
+ self, context: RunContextWrapper, from_agent: Agent, to_agent: Agent
+ ) -> None:
+ self.event_counter += 1
+ print(
+ f"### {self.event_counter}: Handoff from {from_agent.name} to {to_agent.name}. Usage: {self._usage_to_str(context.usage)}"
+ )
+
+
+@function_tool
+def random_number(max: int) -> int:
+ """Generate a random number up to the provided max."""
+ return workflow.random().randint(0, max)
+
+
+@function_tool
+def multiply_by_two(x: int) -> int:
+ """Return x times two."""
+ return x * 2
+
+
+class FinalResult(BaseModel):
+ number: int
+
+
+@workflow.defn
+class LifecycleWorkflow:
+ @workflow.run
+ async def run(self, max_number: int) -> FinalResult:
+ hooks = ExampleHooks()
+
+ multiply_agent = Agent(
+ name="Multiply Agent",
+ instructions="Multiply the number by 2 and then return the final result.",
+ tools=[multiply_by_two],
+ output_type=FinalResult,
+ )
+
+ start_agent = Agent(
+ name="Start Agent",
+ instructions="Generate a random number. If it's even, stop. If it's odd, hand off to the multiplier agent.",
+ tools=[random_number],
+ output_type=FinalResult,
+ handoffs=[multiply_agent],
+ )
+
+ result = await Runner.run(
+ start_agent,
+ hooks=hooks,
+ input=f"Generate a random number between 0 and {max_number}.",
+ )
+
+ print("Done!")
+ return result.final_output
diff --git a/openai_agents/basic/workflows/local_image_workflow.py b/openai_agents/basic/workflows/local_image_workflow.py
new file mode 100644
index 00000000..b536f517
--- /dev/null
+++ b/openai_agents/basic/workflows/local_image_workflow.py
@@ -0,0 +1,54 @@
+from agents import Agent, Runner
+from temporalio import workflow
+
+from openai_agents.basic.activities.image_activities import read_image_as_base64
+
+
+@workflow.defn
+class LocalImageWorkflow:
+ @workflow.run
+ async def run(
+ self, image_path: str, question: str = "What do you see in this image?"
+ ) -> str:
+ """
+ Process a local image file with an AI agent.
+
+ Args:
+ image_path: Path to the local image file
+ question: Question to ask about the image
+
+ Returns:
+ Agent's response about the image
+ """
+ # Convert image to base64 using activity
+ b64_image = await workflow.execute_activity(
+ read_image_as_base64,
+ image_path,
+ start_to_close_timeout=workflow.timedelta(seconds=30),
+ )
+
+ agent = Agent(
+ name="Assistant",
+ instructions="You are a helpful assistant.",
+ )
+
+ result = await Runner.run(
+ agent,
+ [
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "input_image",
+ "detail": "auto",
+ "image_url": f"data:image/jpeg;base64,{b64_image}",
+ }
+ ],
+ },
+ {
+ "role": "user",
+ "content": question,
+ },
+ ],
+ )
+ return result.final_output
diff --git a/openai_agents/basic/workflows/non_strict_output_workflow.py b/openai_agents/basic/workflows/non_strict_output_workflow.py
new file mode 100644
index 00000000..f56716c1
--- /dev/null
+++ b/openai_agents/basic/workflows/non_strict_output_workflow.py
@@ -0,0 +1,86 @@
+import json
+from dataclasses import dataclass
+from typing import Any
+
+from agents import Agent, AgentOutputSchema, AgentOutputSchemaBase, Runner
+from temporalio import workflow
+
+
+@dataclass
+class OutputType:
+ jokes: dict[int, str]
+ """A list of jokes, indexed by joke number."""
+
+
+class CustomOutputSchema(AgentOutputSchemaBase):
+ """A demonstration of a custom output schema."""
+
+ def is_plain_text(self) -> bool:
+ return False
+
+ def name(self) -> str:
+ return "CustomOutputSchema"
+
+ def json_schema(self) -> dict[str, Any]:
+ return {
+ "type": "object",
+ "properties": {
+ "jokes": {"type": "object", "properties": {"joke": {"type": "string"}}}
+ },
+ }
+
+ def is_strict_json_schema(self) -> bool:
+ return False
+
+ def validate_json(self, json_str: str) -> Any:
+ json_obj = json.loads(json_str)
+ # Just for demonstration, we'll return a list.
+ return list(json_obj["jokes"].values())
+
+
+@workflow.defn
+class NonStrictOutputWorkflow:
+ @workflow.run
+ async def run(self, input_text: str) -> dict[str, Any]:
+ """
+ Demonstrates non-strict output types that require special handling.
+
+ Args:
+ input_text: The input message to the agent
+
+ Returns:
+ Dictionary with results from different output type approaches
+ """
+ results = {}
+
+ agent = Agent(
+ name="Assistant",
+ instructions="You are a helpful assistant.",
+ output_type=OutputType,
+ )
+
+ # First, try with strict output type (this should fail)
+ try:
+ result = await Runner.run(agent, input_text)
+ results["strict_result"] = "Unexpected success"
+ except Exception as e:
+ results["strict_error"] = str(e)
+
+ # Now try with non-strict output type
+ try:
+ agent.output_type = AgentOutputSchema(OutputType, strict_json_schema=False)
+ result = await Runner.run(agent, input_text)
+ results["non_strict_result"] = result.final_output
+ except Exception as e:
+ results["non_strict_error"] = str(e)
+
+ # Finally, try with custom output type
+ # Not presently supported by Temporal
+ # try:
+ # agent.output_type = CustomOutputSchema()
+ # result = await Runner.run(agent, input_text)
+ # results["custom_result"] = result.final_output
+ # except Exception as e:
+ # results["custom_error"] = str(e)
+
+ return results
diff --git a/openai_agents/basic/workflows/previous_response_id_workflow.py b/openai_agents/basic/workflows/previous_response_id_workflow.py
new file mode 100644
index 00000000..64015159
--- /dev/null
+++ b/openai_agents/basic/workflows/previous_response_id_workflow.py
@@ -0,0 +1,48 @@
+from typing import Tuple
+
+from agents import Agent, Runner
+from temporalio import workflow
+
+
+@workflow.defn
+class PreviousResponseIdWorkflow:
+ @workflow.run
+ async def run(
+ self, first_question: str, follow_up_question: str
+ ) -> Tuple[str, str]:
+ """
+ Demonstrates usage of the `previous_response_id` parameter to continue a conversation.
+ The second run passes the previous response ID to the model, which allows it to continue the
+ conversation without re-sending the previous messages.
+
+ Notes:
+ 1. This only applies to the OpenAI Responses API. Other models will ignore this parameter.
+ 2. Responses are only stored for 30 days as of this writing, so in production you should
+ store the response ID along with an expiration date; if the response is no longer valid,
+ you'll need to re-send the previous conversation history.
+
+ Args:
+ first_question: The initial question to ask
+ follow_up_question: The follow-up question that references the first response
+
+ Returns:
+ Tuple of (first_response, second_response)
+ """
+ agent = Agent(
+ name="Assistant",
+ instructions="You are a helpful assistant. be VERY concise.",
+ )
+
+ # First question
+ result1 = await Runner.run(agent, first_question)
+ first_response = result1.final_output
+
+ # Follow-up question using previous response ID
+ result2 = await Runner.run(
+ agent,
+ follow_up_question,
+ previous_response_id=result1.last_response_id,
+ )
+ second_response = result2.final_output
+
+ return first_response, second_response
diff --git a/openai_agents/basic/workflows/remote_image_workflow.py b/openai_agents/basic/workflows/remote_image_workflow.py
new file mode 100644
index 00000000..ce07bd21
--- /dev/null
+++ b/openai_agents/basic/workflows/remote_image_workflow.py
@@ -0,0 +1,45 @@
+from agents import Agent, Runner
+from temporalio import workflow
+
+
+@workflow.defn
+class RemoteImageWorkflow:
+ @workflow.run
+ async def run(
+ self, image_url: str, question: str = "What do you see in this image?"
+ ) -> str:
+ """
+ Process a remote image URL with an AI agent.
+
+ Args:
+ image_url: URL of the remote image
+ question: Question to ask about the image
+
+ Returns:
+ Agent's response about the image
+ """
+ agent = Agent(
+ name="Assistant",
+ instructions="You are a helpful assistant.",
+ )
+
+ result = await Runner.run(
+ agent,
+ [
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "input_image",
+ "detail": "auto",
+ "image_url": image_url,
+ }
+ ],
+ },
+ {
+ "role": "user",
+ "content": question,
+ },
+ ],
+ )
+ return result.final_output
diff --git a/openai_agents/basic/workflows/tools_workflow.py b/openai_agents/basic/workflows/tools_workflow.py
new file mode 100644
index 00000000..70964dc0
--- /dev/null
+++ b/openai_agents/basic/workflows/tools_workflow.py
@@ -0,0 +1,27 @@
+from __future__ import annotations
+
+from datetime import timedelta
+
+from agents import Agent, Runner
+from temporalio import workflow
+from temporalio.contrib import openai_agents as temporal_agents
+
+from openai_agents.basic.activities.get_weather_activity import get_weather
+
+
+@workflow.defn
+class ToolsWorkflow:
+ @workflow.run
+ async def run(self, question: str) -> str:
+ agent = Agent(
+ name="Hello world",
+ instructions="You are a helpful agent.",
+ tools=[
+ temporal_agents.workflow.activity_as_tool(
+ get_weather, start_to_close_timeout=timedelta(seconds=10)
+ )
+ ],
+ )
+
+ result = await Runner.run(agent, input=question)
+ return result.final_output
diff --git a/openai_agents/customer_service/README.md b/openai_agents/customer_service/README.md
new file mode 100644
index 00000000..34b9504d
--- /dev/null
+++ b/openai_agents/customer_service/README.md
@@ -0,0 +1,21 @@
+# Customer Service
+
+Interactive customer service agent with escalation capabilities, extended with Temporal's durable conversational workflows.
+
+*Adapted from [OpenAI Agents SDK customer service](https://github.com/openai/openai-agents-python/tree/main/examples/customer_service)*
+
+This example demonstrates how to build persistent, stateful conversations where each conversation maintains state across multiple interactions and can survive system restarts and failures.
+
+## Running the Example
+
+First, start the worker:
+```bash
+uv run openai_agents/customer_service/run_worker.py
+```
+
+Then start a customer service conversation:
+```bash
+uv run openai_agents/customer_service/run_customer_service_client.py --conversation-id my-conversation-123
+```
+
+You can start a new conversation with any unique conversation ID, or resume existing conversations by using the same conversation ID. The conversation state is persisted in the Temporal workflow, allowing you to resume conversations even after restarting the client.
\ No newline at end of file
diff --git a/openai_agents/customer_service/customer_service.py b/openai_agents/customer_service/customer_service.py
new file mode 100644
index 00000000..45997033
--- /dev/null
+++ b/openai_agents/customer_service/customer_service.py
@@ -0,0 +1,136 @@
+from __future__ import annotations as _annotations
+
+from typing import Dict, Tuple
+
+from agents import Agent, RunContextWrapper, function_tool, handoff
+from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
+from pydantic import BaseModel
+from temporalio import workflow
+
+### CONTEXT
+
+
+class AirlineAgentContext(BaseModel):
+ passenger_name: str | None = None
+ confirmation_number: str | None = None
+ seat_number: str | None = None
+ flight_number: str | None = None
+
+
+### TOOLS
+
+
+@function_tool(
+ name_override="faq_lookup_tool",
+ description_override="Lookup frequently asked questions.",
+)
+async def faq_lookup_tool(question: str) -> str:
+ question_lower = question.lower()
+ if "bag" in question_lower or "baggage" in question_lower:
+ return (
+ "You are allowed to bring one bag on the plane. "
+ "It must be under 50 pounds and 22 inches x 14 inches x 9 inches."
+ )
+ elif "seats" in question_lower or "plane" in question_lower:
+ return (
+ "There are 120 seats on the plane. "
+ "There are 22 business class seats and 98 economy seats. "
+ "Exit rows are rows 4 and 16. "
+ "Rows 5-8 are Economy Plus, with extra legroom. "
+ )
+ elif "wifi" in question_lower:
+ return "We have free wifi on the plane, join Airline-Wifi"
+ return "I'm sorry, I don't know the answer to that question."
+
+
+@function_tool
+async def update_seat(
+ context: RunContextWrapper[AirlineAgentContext],
+ confirmation_number: str,
+ new_seat: str,
+) -> str:
+ """
+ Update the seat for a given confirmation number.
+
+ Args:
+ confirmation_number: The confirmation number for the flight.
+ new_seat: The new seat to update to.
+ """
+ # Update the context based on the customer's input
+ context.context.confirmation_number = confirmation_number
+ context.context.seat_number = new_seat
+ # Ensure that the flight number has been set by the incoming handoff
+ assert context.context.flight_number is not None, "Flight number is required"
+ return f"Updated seat to {new_seat} for confirmation number {confirmation_number}"
+
+
+### HOOKS
+
+
+async def on_seat_booking_handoff(
+ context: RunContextWrapper[AirlineAgentContext],
+) -> None:
+ flight_number = f"FLT-{workflow.random().randint(100, 999)}"
+ context.context.flight_number = flight_number
+
+
+### AGENTS
+
+
+def init_agents() -> (
+ Tuple[Agent[AirlineAgentContext], Dict[str, Agent[AirlineAgentContext]]]
+):
+ """
+ Initialize the agents for the airline customer service workflow.
+ :return: triage agent
+ """
+ faq_agent = Agent[AirlineAgentContext](
+ name="FAQ Agent",
+ handoff_description="A helpful agent that can answer questions about the airline.",
+ instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
+ You are an FAQ agent. If you are speaking to a customer, you probably were transferred to from the triage agent.
+ Use the following routine to support the customer.
+ # Routine
+ 1. Identify the last question asked by the customer.
+ 2. Use the faq lookup tool to answer the question. Do not rely on your own knowledge.
+ 3. If you cannot answer the question, transfer back to the triage agent.""",
+ tools=[faq_lookup_tool],
+ )
+
+ seat_booking_agent = Agent[AirlineAgentContext](
+ name="Seat Booking Agent",
+ handoff_description="A helpful agent that can update a seat on a flight.",
+ instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
+ You are a seat booking agent. If you are speaking to a customer, you probably were transferred to from the triage agent.
+ Use the following routine to support the customer.
+ # Routine
+ 1. Ask for their confirmation number.
+ 2. Ask the customer what their desired seat number is.
+ 3. Use the update seat tool to update the seat on the flight.
+ If the customer asks a question that is not related to the routine, transfer back to the triage agent. """,
+ tools=[update_seat],
+ )
+
+ triage_agent = Agent[AirlineAgentContext](
+ name="Triage Agent",
+ handoff_description="A triage agent that can delegate a customer's request to the appropriate agent.",
+ instructions=(
+ f"{RECOMMENDED_PROMPT_PREFIX} "
+ "You are a helpful triaging agent. You can use your tools to delegate questions to other appropriate agents."
+ ),
+ handoffs=[
+ faq_agent,
+ handoff(agent=seat_booking_agent, on_handoff=on_seat_booking_handoff),
+ ],
+ )
+
+ faq_agent.handoffs.append(triage_agent)
+ seat_booking_agent.handoffs.append(triage_agent)
+ return triage_agent, {
+ agent.name: agent for agent in [faq_agent, seat_booking_agent, triage_agent]
+ }
+
+
+class ProcessUserMessageInput(BaseModel):
+ user_input: str
+ chat_length: int
diff --git a/openai_agents/customer_service/run_customer_service_client.py b/openai_agents/customer_service/run_customer_service_client.py
new file mode 100644
index 00000000..044e0775
--- /dev/null
+++ b/openai_agents/customer_service/run_customer_service_client.py
@@ -0,0 +1,82 @@
+import argparse
+import asyncio
+
+from temporalio.client import (
+ Client,
+ WorkflowQueryRejectedError,
+ WorkflowUpdateFailedError,
+)
+from temporalio.common import QueryRejectCondition
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+from temporalio.service import RPCError, RPCStatusCode
+
+from openai_agents.customer_service.workflows.customer_service_workflow import (
+ CustomerServiceWorkflow,
+ ProcessUserMessageInput,
+)
+
+
+async def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--conversation-id", type=str, required=True)
+ args = parser.parse_args()
+
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ handle = client.get_workflow_handle(args.conversation_id)
+
+ # Query the workflow for the chat history
+ # If the workflow is not open, start a new one
+ start = False
+ history = []
+ try:
+ history = await handle.query(
+ CustomerServiceWorkflow.get_chat_history,
+ reject_condition=QueryRejectCondition.NOT_OPEN,
+ )
+ except WorkflowQueryRejectedError:
+ start = True
+ except RPCError as e:
+ if e.status == RPCStatusCode.NOT_FOUND:
+ start = True
+ else:
+ raise e
+ if start:
+ await client.start_workflow(
+ CustomerServiceWorkflow.run,
+ id=args.conversation_id,
+ task_queue="openai-agents-task-queue",
+ )
+ history = []
+ print(*history, sep="\n")
+
+ # Loop to send messages to the workflow
+ while True:
+ user_input = input("Enter your message: ")
+ message_input = ProcessUserMessageInput(
+ user_input=user_input, chat_length=len(history)
+ )
+ try:
+ new_history = await handle.execute_update(
+ CustomerServiceWorkflow.process_user_message, message_input
+ )
+ history.extend(new_history)
+ print(*new_history[1:], sep="\n")
+ except WorkflowUpdateFailedError:
+ print("** Stale conversation. Reloading...")
+ length = len(history)
+ history = await handle.query(
+ CustomerServiceWorkflow.get_chat_history,
+ reject_condition=QueryRejectCondition.NOT_OPEN,
+ )
+ print(*history[length:], sep="\n")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/customer_service/run_worker.py b/openai_agents/customer_service/run_worker.py
new file mode 100644
index 00000000..b82f6919
--- /dev/null
+++ b/openai_agents/customer_service/run_worker.py
@@ -0,0 +1,39 @@
+from __future__ import annotations
+
+import asyncio
+from datetime import timedelta
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import ModelActivityParameters, OpenAIAgentsPlugin
+from temporalio.worker import Worker
+
+from openai_agents.customer_service.workflows.customer_service_workflow import (
+ CustomerServiceWorkflow,
+)
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(
+ model_params=ModelActivityParameters(
+ start_to_close_timeout=timedelta(seconds=30)
+ )
+ ),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="openai-agents-task-queue",
+ workflows=[
+ CustomerServiceWorkflow,
+ ],
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/customer_service/workflows/customer_service_workflow.py b/openai_agents/customer_service/workflows/customer_service_workflow.py
new file mode 100644
index 00000000..0157d050
--- /dev/null
+++ b/openai_agents/customer_service/workflows/customer_service_workflow.py
@@ -0,0 +1,130 @@
+from __future__ import annotations as _annotations
+
+from agents import (
+ HandoffCallItem,
+ HandoffOutputItem,
+ ItemHelpers,
+ MessageOutputItem,
+ RunConfig,
+ Runner,
+ ToolCallItem,
+ ToolCallOutputItem,
+ TResponseInputItem,
+ trace,
+)
+from pydantic import dataclasses
+from temporalio import workflow
+
+from openai_agents.customer_service.customer_service import (
+ AirlineAgentContext,
+ ProcessUserMessageInput,
+ init_agents,
+)
+
+
+@dataclasses.dataclass
+class CustomerServiceWorkflowState:
+ printed_history: list[str]
+ current_agent_name: str
+ context: AirlineAgentContext
+ input_items: list[TResponseInputItem]
+
+
+@workflow.defn
+class CustomerServiceWorkflow:
+ @workflow.init
+ def __init__(
+ self, customer_service_state: CustomerServiceWorkflowState | None = None
+ ):
+ self.run_config = RunConfig()
+
+ starting_agent, self.agent_map = init_agents()
+ self.current_agent = (
+ self.agent_map[customer_service_state.current_agent_name]
+ if customer_service_state
+ else starting_agent
+ )
+ self.context = (
+ customer_service_state.context
+ if customer_service_state
+ else AirlineAgentContext()
+ )
+ self.printed_history: list[str] = (
+ customer_service_state.printed_history if customer_service_state else []
+ )
+ self.input_items = (
+ customer_service_state.input_items if customer_service_state else []
+ )
+
+ @workflow.run
+ async def run(
+ self, customer_service_state: CustomerServiceWorkflowState | None = None
+ ):
+ await workflow.wait_condition(
+ lambda: workflow.info().is_continue_as_new_suggested()
+ and workflow.all_handlers_finished()
+ )
+ workflow.continue_as_new(
+ CustomerServiceWorkflowState(
+ printed_history=self.printed_history,
+ current_agent_name=self.current_agent.name,
+ context=self.context,
+ input_items=self.input_items,
+ )
+ )
+
+ @workflow.query
+ def get_chat_history(self) -> list[str]:
+ return self.printed_history
+
+ @workflow.update
+ async def process_user_message(self, input: ProcessUserMessageInput) -> list[str]:
+ length = len(self.printed_history)
+ self.printed_history.append(f"User: {input.user_input}")
+ with trace("Customer service", group_id=workflow.info().workflow_id):
+ self.input_items.append({"content": input.user_input, "role": "user"})
+ result = await Runner.run(
+ self.current_agent,
+ self.input_items,
+ context=self.context,
+ run_config=self.run_config,
+ )
+
+ for new_item in result.new_items:
+ agent_name = new_item.agent.name
+ if isinstance(new_item, MessageOutputItem):
+ self.printed_history.append(
+ f"{agent_name}: {ItemHelpers.text_message_output(new_item)}"
+ )
+ elif isinstance(new_item, HandoffOutputItem):
+ self.printed_history.append(
+ f"Handed off from {new_item.source_agent.name} to {new_item.target_agent.name}"
+ )
+ elif isinstance(new_item, HandoffCallItem):
+ self.printed_history.append(
+ f"{agent_name}: Handed off to tool {new_item.raw_item.name}"
+ )
+ elif isinstance(new_item, ToolCallItem):
+ self.printed_history.append(f"{agent_name}: Calling a tool")
+ elif isinstance(new_item, ToolCallOutputItem):
+ self.printed_history.append(
+ f"{agent_name}: Tool call output: {new_item.output}"
+ )
+ else:
+ self.printed_history.append(
+ f"{agent_name}: Skipping item: {new_item.__class__.__name__}"
+ )
+ self.input_items = result.to_input_list()
+ self.current_agent = result.last_agent
+ workflow.set_current_details("\n\n".join(self.printed_history))
+
+ return self.printed_history[length:]
+
+ @process_user_message.validator
+ def validate_process_user_message(self, input: ProcessUserMessageInput) -> None:
+ if not input.user_input:
+ raise ValueError("User input cannot be empty.")
+ if len(input.user_input) > 1000:
+ raise ValueError("User input is too long. Please limit to 1000 characters.")
+ if input.chat_length != len(self.printed_history):
+ raise ValueError("Stale chat history. Please refresh the chat.")
diff --git a/openai_agents/financial_research_agent/README.md b/openai_agents/financial_research_agent/README.md
new file mode 100644
index 00000000..fed8e5b2
--- /dev/null
+++ b/openai_agents/financial_research_agent/README.md
@@ -0,0 +1,61 @@
+# Financial Research Agent
+
+Multi-agent financial research system with specialized roles, extended with Temporal's durable execution.
+
+*Adapted from [OpenAI Agents SDK financial research agent](https://github.com/openai/openai-agents-python/tree/main/examples/financial_research_agent)*
+
+## Architecture
+
+This example shows how you might compose a richer financial research agent using the Agents SDK. The pattern is similar to the `research_bot` example, but with more specialized sub-agents and a verification step.
+
+The flow is:
+
+1. **Planning**: A planner agent turns the end user's request into a list of search terms relevant to financial analysis – recent news, earnings calls, corporate filings, industry commentary, etc.
+2. **Search**: A search agent uses the built-in `WebSearchTool` to retrieve terse summaries for each search term. (You could also add `FileSearchTool` if you have indexed PDFs or 10-Ks.)
+3. **Sub-analysts**: Additional agents (e.g. a fundamentals analyst and a risk analyst) are exposed as tools so the writer can call them inline and incorporate their outputs.
+4. **Writing**: A senior writer agent brings together the search snippets and any sub-analyst summaries into a long-form markdown report plus a short executive summary.
+5. **Verification**: A final verifier agent audits the report for obvious inconsistencies or missing sourcing.
+
+## Running the Example
+
+First, start the worker:
+```bash
+uv run openai_agents/financial_research_agent/run_worker.py
+```
+
+Then run the financial research workflow:
+```bash
+uv run openai_agents/financial_research_agent/run_financial_research_workflow.py
+```
+
+Enter a query like:
+```
+Write up an analysis of Apple Inc.'s most recent quarter.
+```
+
+You can also just hit enter to run this query, which is provided as the default.
+
+## Components
+
+### Agents
+
+- **Planner Agent**: Creates a search plan with 5-15 relevant search terms
+- **Search Agent**: Uses web search to gather financial information
+- **Financials Agent**: Analyzes company fundamentals (revenue, profit, margins)
+- **Risk Agent**: Identifies potential red flags and risk factors
+- **Writer Agent**: Synthesizes information into a comprehensive report
+- **Verifier Agent**: Audits the final report for consistency and accuracy
+
+### Writer Agent Tools
+
+The writer agent has access to tools that invoke the specialist analysts:
+- `fundamentals_analysis`: Get financial performance analysis
+- `risk_analysis`: Get risk factor assessment
+
+## Temporal Integration
+
+The example demonstrates several Temporal patterns:
+- Durable execution of multi-step research workflows
+- Parallel execution of web searches using `asyncio.create_task`
+- Use of `workflow.as_completed` for handling concurrent tasks
+- Proper import handling with `workflow.unsafe.imports_passed_through()`
diff --git a/openai_agents/financial_research_agent/agents/financials_agent.py b/openai_agents/financial_research_agent/agents/financials_agent.py
new file mode 100644
index 00000000..72a2be95
--- /dev/null
+++ b/openai_agents/financial_research_agent/agents/financials_agent.py
@@ -0,0 +1,23 @@
+from agents import Agent
+from pydantic import BaseModel
+
+# A sub-agent focused on analyzing a company's fundamentals.
+FINANCIALS_PROMPT = (
+ "You are a financial analyst focused on company fundamentals such as revenue, "
+ "profit, margins and growth trajectory. Given a collection of web (and optional file) "
+ "search results about a company, write a concise analysis of its recent financial "
+ "performance. Pull out key metrics or quotes. Keep it under 2 paragraphs."
+)
+
+
+class AnalysisSummary(BaseModel):
+ summary: str
+ """Short text summary for this aspect of the analysis."""
+
+
+def new_financials_agent() -> Agent:
+ return Agent(
+ name="FundamentalsAnalystAgent",
+ instructions=FINANCIALS_PROMPT,
+ output_type=AnalysisSummary,
+ )
diff --git a/openai_agents/financial_research_agent/agents/planner_agent.py b/openai_agents/financial_research_agent/agents/planner_agent.py
new file mode 100644
index 00000000..8c7ffcb9
--- /dev/null
+++ b/openai_agents/financial_research_agent/agents/planner_agent.py
@@ -0,0 +1,35 @@
+from agents import Agent
+from pydantic import BaseModel
+
+# Generate a plan of searches to ground the financial analysis.
+# For a given financial question or company, we want to search for
+# recent news, official filings, analyst commentary, and other
+# relevant background.
+PROMPT = (
+ "You are a financial research planner. Given a request for financial analysis, "
+ "produce a set of web searches to gather the context needed. Aim for recent "
+ "headlines, earnings calls or 10-K snippets, analyst commentary, and industry background. "
+ "Output between 5 and 15 search terms to query for."
+)
+
+
+class FinancialSearchItem(BaseModel):
+ reason: str
+ """Your reasoning for why this search is relevant."""
+
+ query: str
+ """The search term to feed into a web (or file) search."""
+
+
+class FinancialSearchPlan(BaseModel):
+ searches: list[FinancialSearchItem]
+ """A list of searches to perform."""
+
+
+def new_planner_agent() -> Agent:
+ return Agent(
+ name="FinancialPlannerAgent",
+ instructions=PROMPT,
+ model="o3-mini",
+ output_type=FinancialSearchPlan,
+ )
diff --git a/openai_agents/financial_research_agent/agents/risk_agent.py b/openai_agents/financial_research_agent/agents/risk_agent.py
new file mode 100644
index 00000000..c73e94ef
--- /dev/null
+++ b/openai_agents/financial_research_agent/agents/risk_agent.py
@@ -0,0 +1,22 @@
+from agents import Agent
+from pydantic import BaseModel
+
+# A sub-agent specializing in identifying risk factors or concerns.
+RISK_PROMPT = (
+ "You are a risk analyst looking for potential red flags in a company's outlook. "
+ "Given background research, produce a short analysis of risks such as competitive threats, "
+ "regulatory issues, supply chain problems, or slowing growth. Keep it under 2 paragraphs."
+)
+
+
+class AnalysisSummary(BaseModel):
+ summary: str
+ """Short text summary for this aspect of the analysis."""
+
+
+def new_risk_agent() -> Agent:
+ return Agent(
+ name="RiskAnalystAgent",
+ instructions=RISK_PROMPT,
+ output_type=AnalysisSummary,
+ )
diff --git a/openai_agents/financial_research_agent/agents/search_agent.py b/openai_agents/financial_research_agent/agents/search_agent.py
new file mode 100644
index 00000000..e40e357e
--- /dev/null
+++ b/openai_agents/financial_research_agent/agents/search_agent.py
@@ -0,0 +1,20 @@
+from agents import Agent, WebSearchTool
+from agents.model_settings import ModelSettings
+
+# Given a search term, use web search to pull back a brief summary.
+# Summaries should be concise but capture the main financial points.
+INSTRUCTIONS = (
+ "You are a research assistant specializing in financial topics. "
+ "Given a search term, use web search to retrieve up-to-date context and "
+ "produce a short summary of at most 300 words. Focus on key numbers, events, "
+ "or quotes that will be useful to a financial analyst."
+)
+
+
+def new_search_agent() -> Agent:
+ return Agent(
+ name="FinancialSearchAgent",
+ instructions=INSTRUCTIONS,
+ tools=[WebSearchTool()],
+ model_settings=ModelSettings(tool_choice="required"),
+ )
diff --git a/openai_agents/financial_research_agent/agents/verifier_agent.py b/openai_agents/financial_research_agent/agents/verifier_agent.py
new file mode 100644
index 00000000..9d3f0a01
--- /dev/null
+++ b/openai_agents/financial_research_agent/agents/verifier_agent.py
@@ -0,0 +1,27 @@
+from agents import Agent
+from pydantic import BaseModel
+
+# Agent to sanity-check a synthesized report for consistency and recall.
+# This can be used to flag potential gaps or obvious mistakes.
+VERIFIER_PROMPT = (
+ "You are a meticulous auditor. You have been handed a financial analysis report. "
+ "Your job is to verify the report is internally consistent, clearly sourced, and makes "
+ "no unsupported claims. Point out any issues or uncertainties."
+)
+
+
+class VerificationResult(BaseModel):
+ verified: bool
+ """Whether the report seems coherent and plausible."""
+
+ issues: str
+ """If not verified, describe the main issues or concerns."""
+
+
+def new_verifier_agent() -> Agent:
+ return Agent(
+ name="VerificationAgent",
+ instructions=VERIFIER_PROMPT,
+ model="gpt-4o",
+ output_type=VerificationResult,
+ )
diff --git a/openai_agents/financial_research_agent/agents/writer_agent.py b/openai_agents/financial_research_agent/agents/writer_agent.py
new file mode 100644
index 00000000..9accc202
--- /dev/null
+++ b/openai_agents/financial_research_agent/agents/writer_agent.py
@@ -0,0 +1,34 @@
+from agents import Agent
+from pydantic import BaseModel
+
+# Writer agent brings together the raw search results and optionally calls out
+# to sub-analyst tools for specialized commentary, then returns a cohesive markdown report.
+WRITER_PROMPT = (
+ "You are a senior financial analyst. You will be provided with the original query and "
+ "a set of raw search summaries. Your task is to synthesize these into a long-form markdown "
+ "report (at least several paragraphs) including a short executive summary and follow-up "
+ "questions. If needed, you can call the available analysis tools (e.g. fundamentals_analysis, "
+ "risk_analysis) to get short specialist write-ups to incorporate."
+)
+
+
+class FinancialReportData(BaseModel):
+ short_summary: str
+ """A short 2-3 sentence executive summary."""
+
+ markdown_report: str
+ """The full markdown report."""
+
+ follow_up_questions: list[str]
+ """Suggested follow-up questions for further research."""
+
+
+# Note: We will attach tools to specialist analyst agents at runtime in the manager.
+# This shows how an agent can use tools to delegate to specialized subagents.
+def new_writer_agent() -> Agent:
+ return Agent(
+ name="FinancialWriterAgent",
+ instructions=WRITER_PROMPT,
+ model="gpt-4.1-2025-04-14",
+ output_type=FinancialReportData,
+ )
diff --git a/openai_agents/financial_research_agent/financial_research_manager.py b/openai_agents/financial_research_agent/financial_research_manager.py
new file mode 100644
index 00000000..9a437a45
--- /dev/null
+++ b/openai_agents/financial_research_agent/financial_research_manager.py
@@ -0,0 +1,142 @@
+from __future__ import annotations
+
+import asyncio
+from collections.abc import Sequence
+
+from agents import RunConfig, Runner, RunResult, custom_span, trace
+from temporalio import workflow
+
+from openai_agents.financial_research_agent.agents.financials_agent import (
+ new_financials_agent,
+)
+from openai_agents.financial_research_agent.agents.planner_agent import (
+ FinancialSearchItem,
+ FinancialSearchPlan,
+ new_planner_agent,
+)
+from openai_agents.financial_research_agent.agents.risk_agent import new_risk_agent
+from openai_agents.financial_research_agent.agents.search_agent import new_search_agent
+from openai_agents.financial_research_agent.agents.verifier_agent import (
+ VerificationResult,
+ new_verifier_agent,
+)
+from openai_agents.financial_research_agent.agents.writer_agent import (
+ FinancialReportData,
+ new_writer_agent,
+)
+
+
+async def _summary_extractor(run_result: RunResult) -> str:
+ """Custom output extractor for sub-agents that return an AnalysisSummary."""
+ # The financial/risk analyst agents emit an AnalysisSummary with a `summary` field.
+ # We want the tool call to return just that summary text so the writer can drop it inline.
+ return str(run_result.final_output.summary)
+
+
+class FinancialResearchManager:
+ """
+ Orchestrates the full flow: planning, searching, sub-analysis, writing, and verification.
+ """
+
+ def __init__(self) -> None:
+ self.run_config = RunConfig()
+ self.planner_agent = new_planner_agent()
+ self.search_agent = new_search_agent()
+ self.financials_agent = new_financials_agent()
+ self.risk_agent = new_risk_agent()
+ self.writer_agent = new_writer_agent()
+ self.verifier_agent = new_verifier_agent()
+
+ async def run(self, query: str) -> str:
+ with trace("Financial research trace"):
+ search_plan = await self._plan_searches(query)
+ search_results = await self._perform_searches(search_plan)
+ report = await self._write_report(query, search_results)
+ verification = await self._verify_report(report)
+
+ # Return formatted output
+ result = f"""=====REPORT=====
+
+{report.markdown_report}
+
+=====FOLLOW UP QUESTIONS=====
+
+{chr(10).join(report.follow_up_questions)}
+
+=====VERIFICATION=====
+
+Verified: {verification.verified}
+Issues: {verification.issues}"""
+
+ return result
+
+ async def _plan_searches(self, query: str) -> FinancialSearchPlan:
+ result = await Runner.run(
+ self.planner_agent,
+ f"Query: {query}",
+ run_config=self.run_config,
+ )
+ return result.final_output_as(FinancialSearchPlan)
+
+ async def _perform_searches(
+ self, search_plan: FinancialSearchPlan
+ ) -> Sequence[str]:
+ with custom_span("Search the web"):
+ tasks = [
+ asyncio.create_task(self._search(item)) for item in search_plan.searches
+ ]
+ results: list[str] = []
+ for task in workflow.as_completed(tasks):
+ result = await task
+ if result is not None:
+ results.append(result)
+ return results
+
+ async def _search(self, item: FinancialSearchItem) -> str | None:
+ input_data = f"Search term: {item.query}\nReason: {item.reason}"
+ try:
+ result = await Runner.run(
+ self.search_agent,
+ input_data,
+ run_config=self.run_config,
+ )
+ return str(result.final_output)
+ except Exception:
+ return None
+
+ async def _write_report(
+ self, query: str, search_results: Sequence[str]
+ ) -> FinancialReportData:
+ # Expose the specialist analysts as tools so the writer can invoke them inline
+ # and still produce the final FinancialReportData output.
+ fundamentals_tool = self.financials_agent.as_tool(
+ tool_name="fundamentals_analysis",
+ tool_description="Use to get a short write-up of key financial metrics",
+ custom_output_extractor=_summary_extractor,
+ )
+ risk_tool = self.risk_agent.as_tool(
+ tool_name="risk_analysis",
+ tool_description="Use to get a short write-up of potential red flags",
+ custom_output_extractor=_summary_extractor,
+ )
+ writer_with_tools = self.writer_agent.clone(
+ tools=[fundamentals_tool, risk_tool]
+ )
+
+ input_data = (
+ f"Original query: {query}\nSummarized search results: {search_results}"
+ )
+ result = await Runner.run(
+ writer_with_tools,
+ input_data,
+ run_config=self.run_config,
+ )
+ return result.final_output_as(FinancialReportData)
+
+ async def _verify_report(self, report: FinancialReportData) -> VerificationResult:
+ result = await Runner.run(
+ self.verifier_agent,
+ report.markdown_report,
+ run_config=self.run_config,
+ )
+ return result.final_output_as(VerificationResult)
diff --git a/openai_agents/financial_research_agent/run_financial_research_workflow.py b/openai_agents/financial_research_agent/run_financial_research_workflow.py
new file mode 100644
index 00000000..80adc86b
--- /dev/null
+++ b/openai_agents/financial_research_agent/run_financial_research_workflow.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.financial_research_agent.workflows.financial_research_workflow import (
+ FinancialResearchWorkflow,
+)
+
+
+async def main():
+ # Get the query from user input
+ query = input("Enter a financial research query: ")
+ if not query.strip():
+ query = "Write up an analysis of Apple Inc.'s most recent quarter."
+ print(f"Using default query: {query}")
+
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ print(f"Starting financial research for: {query}")
+ print("This may take several minutes to complete...\n")
+
+ result = await client.execute_workflow(
+ FinancialResearchWorkflow.run,
+ query,
+ id=f"financial-research-{hash(query)}",
+ task_queue="financial-research-task-queue",
+ )
+
+ print(result)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/financial_research_agent/run_worker.py b/openai_agents/financial_research_agent/run_worker.py
new file mode 100644
index 00000000..507bd77c
--- /dev/null
+++ b/openai_agents/financial_research_agent/run_worker.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+from temporalio.worker import Worker
+
+from openai_agents.financial_research_agent.workflows.financial_research_workflow import (
+ FinancialResearchWorkflow,
+)
+
+
+async def main():
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="financial-research-task-queue",
+ workflows=[FinancialResearchWorkflow],
+ )
+
+ print("Starting financial research worker...")
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/financial_research_agent/workflows/financial_research_workflow.py b/openai_agents/financial_research_agent/workflows/financial_research_workflow.py
new file mode 100644
index 00000000..487e3fd5
--- /dev/null
+++ b/openai_agents/financial_research_agent/workflows/financial_research_workflow.py
@@ -0,0 +1,13 @@
+from temporalio import workflow
+
+from openai_agents.financial_research_agent.financial_research_manager import (
+ FinancialResearchManager,
+)
+
+
+@workflow.defn
+class FinancialResearchWorkflow:
+ @workflow.run
+ async def run(self, query: str) -> str:
+ manager = FinancialResearchManager()
+ return await manager.run(query)
diff --git a/openai_agents/handoffs/README.md b/openai_agents/handoffs/README.md
new file mode 100644
index 00000000..c38b0919
--- /dev/null
+++ b/openai_agents/handoffs/README.md
@@ -0,0 +1,44 @@
+# Handoffs Examples
+
+Agent handoff patterns with message filtering in Temporal workflows.
+
+*Adapted from [OpenAI Agents SDK handoffs examples](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs)*
+
+Before running these examples, be sure to review the [prerequisites and background on the integration](../README.md).
+
+## Running the Examples
+
+First, start the worker:
+```bash
+uv run openai_agents/handoffs/run_worker.py
+```
+
+Then run the workflow:
+
+### Message Filter Workflow
+Demonstrates agent handoffs with message history filtering:
+```bash
+uv run openai_agents/handoffs/run_message_filter_workflow.py
+```
+
+## Workflow Pattern
+
+The workflow demonstrates a 4-step conversation with message filtering:
+
+1. **Introduction**: User greets first agent with name
+2. **Tool Usage**: First agent generates random number using function tool
+3. **Agent Switch**: Conversation moves to second agent for general questions
+4. **Spanish Handoff**: Second agent detects Spanish and hands off to Spanish specialist
+
+During the Spanish handoff, message filtering occurs:
+- All tool-related messages are removed from history
+- First two messages are dropped (demonstration of selective context)
+- Filtered conversation continues with Spanish agent
+
+The workflow returns both the final response and complete message history for inspection.
+
+## Omitted Examples
+
+The following patterns from the [reference repository](https://github.com/openai/openai-agents-python/tree/main/examples/handoffs) are not included in this Temporal adaptation:
+
+- **Message Filter Streaming**: Streaming capabilities are not yet available in the Temporal integration
\ No newline at end of file
diff --git a/openai_agents/handoffs/run_message_filter_workflow.py b/openai_agents/handoffs/run_message_filter_workflow.py
new file mode 100644
index 00000000..c2b2ba3d
--- /dev/null
+++ b/openai_agents/handoffs/run_message_filter_workflow.py
@@ -0,0 +1,42 @@
+import asyncio
+import json
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+from temporalio.envconfig import ClientConfig
+
+from openai_agents.handoffs.workflows.message_filter_workflow import (
+ MessageFilterWorkflow,
+)
+
+
+async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ **config,
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ MessageFilterWorkflow.run,
+ "Sora",
+ id="message-filter-workflow",
+ task_queue="openai-agents-handoffs-task-queue",
+ )
+
+ print(f"Final output: {result.final_output}")
+ print("\n===Final messages===\n")
+
+ # Print the final message history to see the effect of the message filter
+ for message in result.final_messages:
+ print(json.dumps(message, indent=2))
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/handoffs/run_worker.py b/openai_agents/handoffs/run_worker.py
new file mode 100644
index 00000000..8941f4d8
--- /dev/null
+++ b/openai_agents/handoffs/run_worker.py
@@ -0,0 +1,42 @@
+from __future__ import annotations
+
+import asyncio
+from datetime import timedelta
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import ModelActivityParameters, OpenAIAgentsPlugin
+from temporalio.worker import Worker
+
+from openai_agents.handoffs.workflows.message_filter_workflow import (
+ MessageFilterWorkflow,
+)
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(
+ model_params=ModelActivityParameters(
+ start_to_close_timeout=timedelta(seconds=60)
+ )
+ ),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="openai-agents-handoffs-task-queue",
+ workflows=[
+ MessageFilterWorkflow,
+ ],
+ activities=[
+ # No custom activities needed for these workflows
+ ],
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/handoffs/workflows/message_filter_workflow.py b/openai_agents/handoffs/workflows/message_filter_workflow.py
new file mode 100644
index 00000000..8adc9453
--- /dev/null
+++ b/openai_agents/handoffs/workflows/message_filter_workflow.py
@@ -0,0 +1,112 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import List
+
+from agents import Agent, HandoffInputData, Runner, function_tool, handoff
+from agents.extensions import handoff_filters
+from agents.items import TResponseInputItem
+from temporalio import workflow
+
+
+@dataclass
+class MessageFilterResult:
+ final_output: str
+ final_messages: List[TResponseInputItem]
+
+
+@function_tool
+def random_number_tool(max: int) -> int:
+ """Return a random integer between 0 and the given maximum."""
+ return workflow.random().randint(0, max)
+
+
+def spanish_handoff_message_filter(
+ handoff_message_data: HandoffInputData,
+) -> HandoffInputData:
+ # First, we'll remove any tool-related messages from the message history
+ handoff_message_data = handoff_filters.remove_all_tools(handoff_message_data)
+
+ # Second, we'll also remove the first two items from the history, just for demonstration
+ history = (
+ tuple(handoff_message_data.input_history[2:])
+ if isinstance(handoff_message_data.input_history, tuple)
+ else handoff_message_data.input_history
+ )
+
+ return HandoffInputData(
+ input_history=history,
+ pre_handoff_items=tuple(handoff_message_data.pre_handoff_items),
+ new_items=tuple(handoff_message_data.new_items),
+ )
+
+
+@workflow.defn
+class MessageFilterWorkflow:
+ @workflow.run
+ async def run(self, user_name: str = "Sora") -> MessageFilterResult:
+ first_agent = Agent(
+ name="Assistant",
+ instructions="Be extremely concise.",
+ tools=[random_number_tool],
+ )
+
+ spanish_agent = Agent(
+ name="Spanish Assistant",
+ instructions="You only speak Spanish and are extremely concise.",
+ handoff_description="A Spanish-speaking assistant.",
+ )
+
+ second_agent = Agent(
+ name="Assistant",
+ instructions=(
+ "Be a helpful assistant. If the user speaks Spanish, handoff to the Spanish assistant."
+ ),
+ handoffs=[
+ handoff(spanish_agent, input_filter=spanish_handoff_message_filter)
+ ],
+ )
+
+ # 1. Send a regular message to the first agent
+ result = await Runner.run(first_agent, input=f"Hi, my name is {user_name}.")
+
+ # 2. Ask it to generate a number
+ result = await Runner.run(
+ first_agent,
+ input=result.to_input_list()
+ + [
+ {
+ "content": "Can you generate a random number between 0 and 100?",
+ "role": "user",
+ }
+ ],
+ )
+
+ # 3. Call the second agent
+ result = await Runner.run(
+ second_agent,
+ input=result.to_input_list()
+ + [
+ {
+ "content": "I live in New York City. What's the population of the city?",
+ "role": "user",
+ }
+ ],
+ )
+
+ # 4. Cause a handoff to occur
+ result = await Runner.run(
+ second_agent,
+ input=result.to_input_list()
+ + [
+ {
+ "content": "Por favor habla en español. ¿Cuál es mi nombre y dónde vivo?",
+ "role": "user",
+ }
+ ],
+ )
+
+ # Return the final result and message history
+ return MessageFilterResult(
+ final_output=result.final_output, final_messages=result.to_input_list()
+ )
diff --git a/openai_agents/hosted_mcp/README.md b/openai_agents/hosted_mcp/README.md
new file mode 100644
index 00000000..78a37131
--- /dev/null
+++ b/openai_agents/hosted_mcp/README.md
@@ -0,0 +1,39 @@
+# Hosted MCP Examples
+
+Integration with hosted MCP (Model Context Protocol) servers using OpenAI agents in Temporal workflows.
+
+*Adapted from [OpenAI Agents SDK hosted_mcp examples](https://github.com/openai/openai-agents-python/tree/main/examples/hosted_mcp)*
+
+Before running these examples, be sure to review the [prerequisites and background on the integration](../README.md).
+
+## Running the Examples
+
+First, start the worker (supports all MCP workflows):
+```bash
+uv run openai_agents/hosted_mcp/run_worker.py
+```
+
+Then run individual examples in separate terminals:
+
+### Simple MCP Connection
+Connect to a hosted MCP server without approval requirements (trusted servers):
+```bash
+uv run openai_agents/hosted_mcp/run_simple_mcp_workflow.py
+```
+
+### MCP with Approval Callbacks
+Connect to a hosted MCP server with approval workflow for tool execution:
+```bash
+uv run openai_agents/hosted_mcp/run_approval_mcp_workflow.py
+```
+
+## MCP Server Configuration
+
+Both examples default to using the GitMCP server (`https://gitmcp.io/openai/codex`) which provides repository analysis capabilities. The workflows can be easily modified to use different MCP servers by changing the `server_url` parameter.
+
+### Approval Workflow Notes
+
+The approval example demonstrates the callback structure for tool approvals in a Temporal context. In this implementation:
+
+- The approval callback automatically approves requests for demonstration purposes
+- In production environments, approvals would typically be handled by communicating with a human user. Because the approval executes in the Temporal workflow, you can use signals or updates to communicate approval status.
diff --git a/openai_agents/hosted_mcp/run_approval_mcp_workflow.py b/openai_agents/hosted_mcp/run_approval_mcp_workflow.py
new file mode 100644
index 00000000..8821c78a
--- /dev/null
+++ b/openai_agents/hosted_mcp/run_approval_mcp_workflow.py
@@ -0,0 +1,30 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.hosted_mcp.workflows.approval_mcp_workflow import ApprovalMCPWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ ApprovalMCPWorkflow.run,
+ "Which language is this repo written in?",
+ id="approval-mcp-workflow",
+ task_queue="openai-agents-hosted-mcp-task-queue",
+ )
+
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/hosted_mcp/run_simple_mcp_workflow.py b/openai_agents/hosted_mcp/run_simple_mcp_workflow.py
new file mode 100644
index 00000000..5e0c064f
--- /dev/null
+++ b/openai_agents/hosted_mcp/run_simple_mcp_workflow.py
@@ -0,0 +1,30 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.hosted_mcp.workflows.simple_mcp_workflow import SimpleMCPWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ SimpleMCPWorkflow.run,
+ "Which language is this repo written in?",
+ id="simple-mcp-workflow",
+ task_queue="openai-agents-hosted-mcp-task-queue",
+ )
+
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/hosted_mcp/run_worker.py b/openai_agents/hosted_mcp/run_worker.py
new file mode 100644
index 00000000..fb25f7b6
--- /dev/null
+++ b/openai_agents/hosted_mcp/run_worker.py
@@ -0,0 +1,42 @@
+from __future__ import annotations
+
+import asyncio
+from datetime import timedelta
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import ModelActivityParameters, OpenAIAgentsPlugin
+from temporalio.worker import Worker
+
+from openai_agents.hosted_mcp.workflows.approval_mcp_workflow import ApprovalMCPWorkflow
+from openai_agents.hosted_mcp.workflows.simple_mcp_workflow import SimpleMCPWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(
+ model_params=ModelActivityParameters(
+ start_to_close_timeout=timedelta(seconds=60)
+ )
+ ),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="openai-agents-hosted-mcp-task-queue",
+ workflows=[
+ SimpleMCPWorkflow,
+ ApprovalMCPWorkflow,
+ ],
+ activities=[
+ # No custom activities needed for these workflows
+ ],
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/hosted_mcp/workflows/approval_mcp_workflow.py b/openai_agents/hosted_mcp/workflows/approval_mcp_workflow.py
new file mode 100644
index 00000000..1b5b7b6f
--- /dev/null
+++ b/openai_agents/hosted_mcp/workflows/approval_mcp_workflow.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from agents import (
+ Agent,
+ HostedMCPTool,
+ MCPToolApprovalFunctionResult,
+ MCPToolApprovalRequest,
+ Runner,
+)
+from temporalio import workflow
+
+
+def approval_callback(request: MCPToolApprovalRequest) -> MCPToolApprovalFunctionResult:
+ """Simple approval callback that logs the request and approves by default.
+
+ In a real application, user input would be provided through a UI or API.
+ The approval callback executes within the Temporal workflow, so the application
+ can use signals or updates to receive user input.
+ """
+ workflow.logger.info(f"MCP tool approval requested for: {request.data.name}")
+
+ result: MCPToolApprovalFunctionResult = {"approve": True}
+ return result
+
+
+@workflow.defn
+class ApprovalMCPWorkflow:
+ @workflow.run
+ async def run(
+ self, question: str, server_url: str = "https://gitmcp.io/openai/codex"
+ ) -> str:
+ agent = Agent(
+ name="Assistant",
+ tools=[
+ HostedMCPTool(
+ tool_config={
+ "type": "mcp",
+ "server_label": "gitmcp",
+ "server_url": server_url,
+ "require_approval": "always",
+ },
+ on_approval_request=approval_callback,
+ )
+ ],
+ )
+
+ result = await Runner.run(agent, question)
+ return result.final_output
diff --git a/openai_agents/hosted_mcp/workflows/simple_mcp_workflow.py b/openai_agents/hosted_mcp/workflows/simple_mcp_workflow.py
new file mode 100644
index 00000000..2fac64bc
--- /dev/null
+++ b/openai_agents/hosted_mcp/workflows/simple_mcp_workflow.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from agents import Agent, HostedMCPTool, Runner
+from temporalio import workflow
+
+
+@workflow.defn
+class SimpleMCPWorkflow:
+ @workflow.run
+ async def run(
+ self, question: str, server_url: str = "https://gitmcp.io/openai/codex"
+ ) -> str:
+ agent = Agent(
+ name="Assistant",
+ tools=[
+ HostedMCPTool(
+ tool_config={
+ "type": "mcp",
+ "server_label": "gitmcp",
+ "server_url": server_url,
+ "require_approval": "never",
+ }
+ )
+ ],
+ )
+
+ result = await Runner.run(agent, question)
+ return result.final_output
diff --git a/openai_agents/mcp/README.md b/openai_agents/mcp/README.md
new file mode 100644
index 00000000..d9a172f3
--- /dev/null
+++ b/openai_agents/mcp/README.md
@@ -0,0 +1,91 @@
+# MCP Examples
+
+Integration with MCP (Model Context Protocol) servers using OpenAI agents in Temporal workflows.
+
+*Adapted from [OpenAI Agents SDK MCP examples](https://github.com/openai/openai-agents-python/tree/main/examples/mcp)*
+
+Before running these examples, be sure to review the [prerequisites and background on the integration](../README.md).
+
+
+## Running the Examples
+
+### Stdio MCP
+
+First, start the worker:
+```bash
+uv run openai_agents/mcp/run_file_system_worker.py
+```
+
+Run the workflow:
+```bash
+uv run openai_agents/mcp/run_file_system_workflow.py
+```
+
+This sample assumes that the worker and `run_file_system_workflow.py` are on the same machine.
+
+
+### Streamable HTTP MCP
+
+First, start the worker:
+```bash
+uv run openai_agents/mcp/servers/tools_server.py --transport=streamable-http
+```
+
+Then start the worker:
+```bash
+uv run openai_agents/mcp/run_streamable_http_worker.py
+```
+
+Finally, run the workflow:
+```bash
+uv run openai_agents/mcp/run_streamable_http_workflow.py
+```
+
+### SSE MCP
+
+First, start the MCP server:
+```bash
+uv run openai_agents/mcp/servers/tools_server.py --transport=sse
+```
+
+Then start the worker:
+```bash
+uv run openai_agents/mcp/run_sse_worker.py
+```
+
+Finally, run the workflow:
+```bash
+uv run openai_agents/mcp/run_sse_workflow.py
+```
+
+### Prompt Server MCP
+
+First, start the MCP server:
+```bash
+uv run openai_agents/mcp/servers/prompt_server.py
+```
+
+Then start the worker:
+```bash
+uv run openai_agents/mcp/run_prompt_server_worker.py
+```
+
+Finally, run the workflow:
+```bash
+uv run openai_agents/mcp/run_prompt_server_workflow.py
+```
+
+
+### Memory MCP (Research Scratchpad)
+
+Demonstrates durable note-taking with the Memory MCP server: write seed notes, query by tags, synthesize a brief with citations, then update and delete notes.
+
+Start the worker:
+```bash
+uv run openai_agents/mcp/run_memory_research_scratchpad_worker.py
+```
+
+Run the research scratchpad workflow:
+```bash
+uv run openai_agents/mcp/run_memory_research_scratchpad_workflow.py
+```
diff --git a/openai_agents/mcp/run_file_system_worker.py b/openai_agents/mcp/run_file_system_worker.py
new file mode 100644
index 00000000..2ed8dffd
--- /dev/null
+++ b/openai_agents/mcp/run_file_system_worker.py
@@ -0,0 +1,66 @@
+from __future__ import annotations
+
+import asyncio
+import logging
+import os
+from datetime import timedelta
+
+from agents.mcp import MCPServerStdio
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import (
+ ModelActivityParameters,
+ OpenAIAgentsPlugin,
+ StatelessMCPServerProvider,
+)
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from openai_agents.mcp.workflows.file_system_workflow import FileSystemWorkflow
+
+
+async def main():
+ logging.basicConfig(level=logging.INFO)
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ samples_dir = os.path.join(current_dir, "sample_files")
+
+ file_system_server = StatelessMCPServerProvider(
+ "FileSystemServer",
+ lambda: MCPServerStdio(
+ name="FileSystemServer",
+ params={
+ "command": "npx",
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", samples_dir],
+ },
+ ),
+ )
+
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(
+ **config,
+ plugins=[
+ OpenAIAgentsPlugin(
+ model_params=ModelActivityParameters(
+ start_to_close_timeout=timedelta(seconds=60)
+ ),
+ mcp_server_providers=[file_system_server],
+ ),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue=f"openai-agents-mcp-filesystem-task-queue",
+ workflows=[
+ FileSystemWorkflow,
+ ],
+ activities=[
+ # No custom activities needed for these workflows
+ ],
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/mcp/run_file_system_workflow.py b/openai_agents/mcp/run_file_system_workflow.py
new file mode 100644
index 00000000..d076d3d5
--- /dev/null
+++ b/openai_agents/mcp/run_file_system_workflow.py
@@ -0,0 +1,32 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+from temporalio.envconfig import ClientConfig
+
+from openai_agents.mcp.workflows.file_system_workflow import FileSystemWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(
+ **config,
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ FileSystemWorkflow.run,
+ id="file-system-workflow",
+ task_queue="openai-agents-mcp-filesystem-task-queue",
+ )
+
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/mcp/run_memory_research_scratchpad_worker.py b/openai_agents/mcp/run_memory_research_scratchpad_worker.py
new file mode 100644
index 00000000..536ab974
--- /dev/null
+++ b/openai_agents/mcp/run_memory_research_scratchpad_worker.py
@@ -0,0 +1,63 @@
+from __future__ import annotations
+
+import asyncio
+import logging
+from datetime import timedelta
+
+from agents.mcp import MCPServerStdio
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import (
+ ModelActivityParameters,
+ OpenAIAgentsPlugin,
+ StatefulMCPServerProvider,
+)
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from openai_agents.mcp.workflows.memory_research_scratchpad_workflow import (
+ MemoryResearchScratchpadWorkflow,
+)
+
+
+async def main():
+ logging.basicConfig(level=logging.INFO)
+
+ memory_server_provider = StatefulMCPServerProvider(
+ "MemoryServer",
+ lambda _: MCPServerStdio(
+ name="MemoryServer",
+ params={
+ "command": "npx",
+ "args": ["-y", "@modelcontextprotocol/server-memory"],
+ },
+ ),
+ )
+
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(
+ **config,
+ plugins=[
+ OpenAIAgentsPlugin(
+ model_params=ModelActivityParameters(
+ start_to_close_timeout=timedelta(seconds=60)
+ ),
+ mcp_server_providers=[memory_server_provider],
+ ),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="openai-agents-mcp-memory-task-queue",
+ workflows=[
+ MemoryResearchScratchpadWorkflow,
+ ],
+ activities=[],
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/mcp/run_memory_research_scratchpad_workflow.py b/openai_agents/mcp/run_memory_research_scratchpad_workflow.py
new file mode 100644
index 00000000..bf724f9a
--- /dev/null
+++ b/openai_agents/mcp/run_memory_research_scratchpad_workflow.py
@@ -0,0 +1,34 @@
+from __future__ import annotations
+
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+from temporalio.envconfig import ClientConfig
+
+from openai_agents.mcp.workflows.memory_research_scratchpad_workflow import (
+ MemoryResearchScratchpadWorkflow,
+)
+
+
+async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(
+ **config,
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ result = await client.execute_workflow(
+ MemoryResearchScratchpadWorkflow.run,
+ id="memory-research-scratchpad-workflow",
+ task_queue="openai-agents-mcp-memory-task-queue",
+ )
+
+ print(f"Result:\n{result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/mcp/run_prompt_server_worker.py b/openai_agents/mcp/run_prompt_server_worker.py
new file mode 100644
index 00000000..08b68fae
--- /dev/null
+++ b/openai_agents/mcp/run_prompt_server_worker.py
@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+import asyncio
+import logging
+from datetime import timedelta
+
+from agents.mcp import MCPServerStreamableHttp
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import (
+ ModelActivityParameters,
+ OpenAIAgentsPlugin,
+ StatelessMCPServerProvider,
+)
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from openai_agents.mcp.workflows.prompt_server_workflow import PromptServerWorkflow
+
+
+async def main():
+ logging.basicConfig(level=logging.INFO)
+
+ print("Setting up worker...\n")
+
+ try:
+ prompt_server_provider = StatelessMCPServerProvider(
+ "PromptServer",
+ lambda: MCPServerStreamableHttp(
+ name="PromptServer",
+ params={
+ "url": "http://localhost:8000/mcp",
+ },
+ ),
+ )
+
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(
+ **config,
+ plugins=[
+ OpenAIAgentsPlugin(
+ model_params=ModelActivityParameters(
+ start_to_close_timeout=timedelta(seconds=120)
+ ),
+ mcp_server_providers=[prompt_server_provider],
+ ),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="openai-agents-mcp-prompt-task-queue",
+ workflows=[
+ PromptServerWorkflow,
+ ],
+ activities=[
+ # No custom activities needed for these workflows
+ ],
+ )
+ await worker.run()
+ except Exception as e:
+ print(f"Worker failed: {e}")
+ raise
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/mcp/run_prompt_server_workflow.py b/openai_agents/mcp/run_prompt_server_workflow.py
new file mode 100644
index 00000000..9cad2725
--- /dev/null
+++ b/openai_agents/mcp/run_prompt_server_workflow.py
@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+from temporalio.envconfig import ClientConfig
+
+from openai_agents.mcp.workflows.prompt_server_workflow import PromptServerWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(
+ **config,
+ plugins=[OpenAIAgentsPlugin()],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ PromptServerWorkflow.run,
+ id="prompt-server-workflow",
+ task_queue="openai-agents-mcp-prompt-task-queue",
+ )
+
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/mcp/run_sse_worker.py b/openai_agents/mcp/run_sse_worker.py
new file mode 100644
index 00000000..8ed719bd
--- /dev/null
+++ b/openai_agents/mcp/run_sse_worker.py
@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+import asyncio
+import logging
+from datetime import timedelta
+
+from agents.mcp import MCPServerSse
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import (
+ ModelActivityParameters,
+ OpenAIAgentsPlugin,
+ StatelessMCPServerProvider,
+)
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from openai_agents.mcp.workflows.sse_workflow import SseWorkflow
+
+
+async def main():
+ logging.basicConfig(level=logging.INFO)
+
+ print("Setting up worker...\n")
+
+ try:
+ sse_server_provider = StatelessMCPServerProvider(
+ "SseServer",
+ lambda: MCPServerSse(
+ name="SseServer",
+ params={
+ "url": "http://localhost:8000/sse",
+ },
+ ),
+ )
+
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(
+ **config,
+ plugins=[
+ OpenAIAgentsPlugin(
+ model_params=ModelActivityParameters(
+ start_to_close_timeout=timedelta(seconds=60)
+ ),
+ mcp_server_providers=[sse_server_provider],
+ ),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="openai-agents-mcp-sse-task-queue",
+ workflows=[
+ SseWorkflow,
+ ],
+ activities=[
+ # No custom activities needed for these workflows
+ ],
+ )
+ await worker.run()
+ except Exception as e:
+ print(f"Worker failed: {e}")
+ raise
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/mcp/run_sse_workflow.py b/openai_agents/mcp/run_sse_workflow.py
new file mode 100644
index 00000000..8effd5fe
--- /dev/null
+++ b/openai_agents/mcp/run_sse_workflow.py
@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+from temporalio.envconfig import ClientConfig
+
+from openai_agents.mcp.workflows.sse_workflow import SseWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(
+ **config,
+ plugins=[OpenAIAgentsPlugin()],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ SseWorkflow.run,
+ id="sse-workflow",
+ task_queue="openai-agents-mcp-sse-task-queue",
+ )
+
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/mcp/run_streamable_http_worker.py b/openai_agents/mcp/run_streamable_http_worker.py
new file mode 100644
index 00000000..72414727
--- /dev/null
+++ b/openai_agents/mcp/run_streamable_http_worker.py
@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+import asyncio
+import logging
+from datetime import timedelta
+
+from agents.mcp import MCPServerStreamableHttp
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import (
+ ModelActivityParameters,
+ OpenAIAgentsPlugin,
+ StatelessMCPServerProvider,
+)
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from openai_agents.mcp.workflows.streamable_http_workflow import StreamableHttpWorkflow
+
+
+async def main():
+ logging.basicConfig(level=logging.INFO)
+
+ print("Setting up worker...\n")
+
+ try:
+ streamable_http_server_provider = StatelessMCPServerProvider(
+ "StreamableHttpServer",
+ lambda: MCPServerStreamableHttp(
+ name="StreamableHttpServer",
+ params={
+ "url": "http://localhost:8000/mcp",
+ },
+ ),
+ )
+
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(
+ **config,
+ plugins=[
+ OpenAIAgentsPlugin(
+ model_params=ModelActivityParameters(
+ start_to_close_timeout=timedelta(seconds=60)
+ ),
+ mcp_server_providers=[streamable_http_server_provider],
+ ),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="openai-agents-mcp-streamable-http-task-queue",
+ workflows=[
+ StreamableHttpWorkflow,
+ ],
+ activities=[
+ # No custom activities needed for these workflows
+ ],
+ )
+ await worker.run()
+ except Exception as e:
+ print(f"Worker failed: {e}")
+ raise
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/mcp/run_streamable_http_workflow.py b/openai_agents/mcp/run_streamable_http_workflow.py
new file mode 100644
index 00000000..b691e456
--- /dev/null
+++ b/openai_agents/mcp/run_streamable_http_workflow.py
@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+from temporalio.envconfig import ClientConfig
+
+from openai_agents.mcp.workflows.streamable_http_workflow import StreamableHttpWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(
+ **config,
+ plugins=[OpenAIAgentsPlugin()],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ StreamableHttpWorkflow.run,
+ id="streamable-http-workflow",
+ task_queue="openai-agents-mcp-streamable-http-task-queue",
+ )
+
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/mcp/sample_files/favorite_books.txt b/openai_agents/mcp/sample_files/favorite_books.txt
new file mode 100644
index 00000000..c55f457e
--- /dev/null
+++ b/openai_agents/mcp/sample_files/favorite_books.txt
@@ -0,0 +1,20 @@
+1. To Kill a Mockingbird – Harper Lee
+2. Pride and Prejudice – Jane Austen
+3. 1984 – George Orwell
+4. The Hobbit – J.R.R. Tolkien
+5. Harry Potter and the Sorcerer’s Stone – J.K. Rowling
+6. The Great Gatsby – F. Scott Fitzgerald
+7. Charlotte’s Web – E.B. White
+8. Anne of Green Gables – Lucy Maud Montgomery
+9. The Alchemist – Paulo Coelho
+10. Little Women – Louisa May Alcott
+11. The Catcher in the Rye – J.D. Salinger
+12. Animal Farm – George Orwell
+13. The Chronicles of Narnia: The Lion, the Witch, and the Wardrobe – C.S. Lewis
+14. The Book Thief – Markus Zusak
+15. A Wrinkle in Time – Madeleine L’Engle
+16. The Secret Garden – Frances Hodgson Burnett
+17. Moby-Dick – Herman Melville
+18. Fahrenheit 451 – Ray Bradbury
+19. Jane Eyre – Charlotte Brontë
+20. The Little Prince – Antoine de Saint-Exupéry
\ No newline at end of file
diff --git a/openai_agents/mcp/sample_files/favorite_cities.txt b/openai_agents/mcp/sample_files/favorite_cities.txt
new file mode 100644
index 00000000..1d3354f2
--- /dev/null
+++ b/openai_agents/mcp/sample_files/favorite_cities.txt
@@ -0,0 +1,4 @@
+- In the summer, I love visiting London.
+- In the winter, Tokyo is great.
+- In the spring, San Francisco.
+- In the fall, New York is the best.
\ No newline at end of file
diff --git a/openai_agents/mcp/sample_files/favorite_songs.txt b/openai_agents/mcp/sample_files/favorite_songs.txt
new file mode 100644
index 00000000..d659bb58
--- /dev/null
+++ b/openai_agents/mcp/sample_files/favorite_songs.txt
@@ -0,0 +1,10 @@
+1. "Here Comes the Sun" – The Beatles
+2. "Imagine" – John Lennon
+3. "Bohemian Rhapsody" – Queen
+4. "Shake It Off" – Taylor Swift
+5. "Billie Jean" – Michael Jackson
+6. "Uptown Funk" – Mark Ronson ft. Bruno Mars
+7. "Don’t Stop Believin’" – Journey
+8. "Dancing Queen" – ABBA
+9. "Happy" – Pharrell Williams
+10. "Wonderwall" – Oasis
diff --git a/openai_agents/mcp/servers/prompt_server.py b/openai_agents/mcp/servers/prompt_server.py
new file mode 100644
index 00000000..07f025ce
--- /dev/null
+++ b/openai_agents/mcp/servers/prompt_server.py
@@ -0,0 +1,58 @@
+from mcp.server.fastmcp import FastMCP
+
+# Create server
+mcp = FastMCP("Prompt Server")
+
+
+# Instruction-generating prompts (user-controlled)
+@mcp.prompt()
+def generate_code_review_instructions(
+ focus: str = "general code quality", language: str = "python"
+) -> str:
+ """Generate agent instructions for code review tasks"""
+ print(f"[debug-server] generate_code_review_instructions({focus}, {language})")
+
+ return f"""You are a senior {language} code review specialist. Your role is to provide comprehensive code analysis with focus on {focus}.
+
+INSTRUCTIONS:
+- Analyze code for quality, security, performance, and best practices
+- Provide specific, actionable feedback with examples
+- Identify potential bugs, vulnerabilities, and optimization opportunities
+- Suggest improvements with code examples when applicable
+- Be constructive and educational in your feedback
+- Focus particularly on {focus} aspects
+
+RESPONSE FORMAT:
+1. Overall Assessment
+2. Specific Issues Found
+3. Security Considerations
+4. Performance Notes
+5. Recommended Improvements
+6. Best Practices Suggestions
+
+Use the available tools to check current time if you need timestamps for your analysis."""
+
+
+@mcp.prompt()
+def generate_review_rubric(target: str = "application code") -> str:
+ """Generate a scoring rubric for reviewing code or plans"""
+ return f"""You are evaluating {target}. Score each category 1-5 and justify briefly.
+
+CATEGORIES:
+- Correctness and Reliability
+- Security and Risk
+- Performance and Efficiency
+- Readability and Maintainability
+- Testability and Coverage
+- Compliance with Style/Guidelines
+
+FORMAT:
+- Overall Score (1-5)
+- Category Scores (bulleted)
+- Top 3 Issues (with impact level and suggested fix)
+- Quick Wins (3 bullets)
+"""
+
+
+if __name__ == "__main__":
+ mcp.run(transport="streamable-http")
diff --git a/openai_agents/mcp/servers/tools_server.py b/openai_agents/mcp/servers/tools_server.py
new file mode 100644
index 00000000..a329c7ee
--- /dev/null
+++ b/openai_agents/mcp/servers/tools_server.py
@@ -0,0 +1,44 @@
+import argparse
+import random
+
+import requests
+from mcp.server.fastmcp import FastMCP
+
+# Create server
+mcp = FastMCP("Tools Server")
+
+
+@mcp.tool()
+def add(a: int, b: int) -> int:
+ """Add two numbers"""
+ print(f"[debug-server] add({a}, {b})")
+ return a + b
+
+
+@mcp.tool()
+def get_secret_word() -> str:
+ print("[debug-server] get_secret_word()")
+ return random.choice(["apple", "banana", "cherry"])
+
+
+@mcp.tool()
+def get_current_weather(city: str) -> str:
+ print(f"[debug-server] get_current_weather({city})")
+
+ endpoint = "https://wttr.in"
+ response = requests.get(f"{endpoint}/{city}")
+ return response.text
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="MCP Tools Server")
+ parser.add_argument(
+ "--transport",
+ choices=["streamable-http", "sse"],
+ default="streamable-http",
+ help="Transport type (default: streamable-http)",
+ )
+ args = parser.parse_args()
+
+ print(f"Starting Tools Server with {args.transport} transport...")
+ mcp.run(transport=args.transport)
diff --git a/openai_agents/mcp/workflows/file_system_workflow.py b/openai_agents/mcp/workflows/file_system_workflow.py
new file mode 100644
index 00000000..b3528c18
--- /dev/null
+++ b/openai_agents/mcp/workflows/file_system_workflow.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+from agents import Agent, Runner, trace
+from agents.mcp import MCPServer
+from temporalio import workflow
+from temporalio.contrib import openai_agents
+
+
+@workflow.defn
+class FileSystemWorkflow:
+ @workflow.run
+ async def run(self) -> str:
+ with trace(workflow_name="MCP File System Example"):
+ server: MCPServer = openai_agents.workflow.stateless_mcp_server(
+ "FileSystemServer"
+ )
+ agent = Agent(
+ name="Assistant",
+ instructions="Use the tools to read the filesystem and answer questions based on those files.",
+ mcp_servers=[server],
+ )
+
+ # List the files it can read
+ message = "Read the files and list them."
+ workflow.logger.info(f"Running: {message}")
+ result1 = await Runner.run(starting_agent=agent, input=message)
+
+ # Ask about books
+ message = "What is my #1 favorite book?"
+ workflow.logger.info(f"Running: {message}")
+ result2 = await Runner.run(starting_agent=agent, input=message)
+
+ # Ask a question that reads then reasons.
+ message = (
+ "Look at my favorite songs. Suggest one new song that I might like."
+ )
+ workflow.logger.info(f"Running: {message}")
+ result3 = await Runner.run(starting_agent=agent, input=message)
+
+ return f"{result1.final_output}\n\n{result2.final_output}\n\n{result3.final_output}"
diff --git a/openai_agents/mcp/workflows/memory_research_scratchpad_workflow.py b/openai_agents/mcp/workflows/memory_research_scratchpad_workflow.py
new file mode 100644
index 00000000..1812a452
--- /dev/null
+++ b/openai_agents/mcp/workflows/memory_research_scratchpad_workflow.py
@@ -0,0 +1,122 @@
+from __future__ import annotations
+
+from agents import Agent, Runner, trace
+from agents.model_settings import ModelSettings
+from temporalio import workflow
+from temporalio.contrib import openai_agents as temporal_openai_agents
+
+SEED_NOTES = [
+ (
+ "scratchpad/ai-sum/001",
+ "Study A (2024-04)",
+ "Summaries reduced triage time by 22% (n=60).",
+ ["ai-summarization", "email", "kpi"],
+ ),
+ (
+ "scratchpad/ai-sum/002",
+ "User preference",
+ "Users prefer action-first summaries with 5–8 bullets max.",
+ ["ai-summarization", "email", "ux"],
+ ),
+ (
+ "scratchpad/ai-sum/003",
+ "Risk: misleading summaries",
+ "Hallucination risk; mitigation: confidence thresholds + easy fallback to original email.",
+ ["ai-summarization", "email", "risk"],
+ ),
+ (
+ "scratchpad/ai-sum/004",
+ "Latency consideration",
+ "Cold-start latency noticeable on first open; can cache or precompute in background.",
+ ["ai-summarization", "email", "perf"],
+ ),
+ (
+ "scratchpad/ai-sum/005",
+ "Adoption insight",
+ "Admin controls improve enterprise adoption; opt-in increases trust and perceived control.",
+ ["ai-summarization", "email", "adoption"],
+ ),
+]
+
+
+@workflow.defn
+class MemoryResearchScratchpadWorkflow:
+ @workflow.run
+ async def run(self) -> str:
+ async with temporal_openai_agents.workflow.stateful_mcp_server(
+ "MemoryServer",
+ ) as server:
+ with trace(workflow_name="MCP Memory Scratchpad Example"):
+ agent = Agent(
+ name="Research Scratchpad Agent",
+ instructions=(
+ "Use the Memory MCP tools to persist, query, update, and delete notes."
+ " Keep IDs short and consistent. Synthesis must rely only on recalled notes and include simple"
+ " citations of the form '(Note: id)'. Keep the brief to 5 bullets."
+ ),
+ mcp_servers=[server],
+ model_settings=ModelSettings(tool_choice="required"),
+ )
+
+ # Step 1: Write seed notes to memory
+ write_prompt_lines = [
+ "Store the following notes in memory. Use the given id and tags for each entry.",
+ "After storing, confirm each (id, tags) that was written.",
+ "",
+ ]
+ for note_id, title, content, tags in SEED_NOTES:
+ # Store tags as separate observation lines so search can reliably match them
+ tag_obs = ", ".join([f"tag: {t}" for t in tags])
+ write_prompt_lines.append(
+ f"- id: {note_id}; title: {title}; content: {content}; observations: [{tag_obs}]"
+ )
+ write_prompt = "\n".join(write_prompt_lines)
+ workflow.logger.info("Writing seed notes to memory")
+ r1 = await Runner.run(starting_agent=agent, input=write_prompt)
+
+ # Step 2: Query by tags
+ query_prompt = (
+ "Search memory for notes that contain BOTH observations 'tag: ai-summarization' and 'tag: email'. "
+ "If the search returns empty, list entities with the name prefix 'scratchpad/ai-sum/' and filter to those that have both tag observations. "
+ "For the resulting ids, call retrieve_entities to fetch their observations, then return a normalized list of (id, title, 1–2 key points) based on the retrieved entities."
+ )
+ workflow.logger.info("Querying notes by tags")
+ r2 = await Runner.run(
+ starting_agent=agent,
+ input=query_prompt,
+ previous_response_id=r1.last_response_id,
+ )
+
+ # Step 3: Synthesis with citations
+ synth_prompt = (
+ "Using only the recalled notes, produce a 5-bullet brief. "
+ "Include one citation per bullet in the form '(Note: id)'. Do not introduce new facts."
+ )
+ workflow.logger.info("Synthesizing brief from recalled notes")
+ r3 = await Runner.run(
+ starting_agent=agent,
+ input=synth_prompt,
+ previous_response_id=r2.last_response_id,
+ )
+
+ # Step 4: Update and re-query (optional demonstration)
+ update_prompt = (
+ "Update the note 'scratchpad/ai-sum/003' to include more precise mitigation:"
+ " 'threshold=0.7; fallback to full email on low confidence'. Then delete the note"
+ " 'scratchpad/ai-sum/005'. Finally, list only the remaining 'risk' notes with (id, updated content)."
+ )
+ workflow.logger.info(
+ "Updating one note and deleting another, then re-listing risk notes"
+ )
+ r4 = await Runner.run(
+ starting_agent=agent,
+ input=update_prompt,
+ previous_response_id=r3.last_response_id,
+ )
+
+ return (
+ f"WRITE CONFIRMATIONS:\n{r1.final_output}\n\n"
+ f"QUERY RESULTS:\n{r2.final_output}\n\n"
+ f"SYNTHESIS:\n{r3.final_output}\n\n"
+ f"UPDATES:\n{r4.final_output}"
+ )
diff --git a/openai_agents/mcp/workflows/prompt_server_workflow.py b/openai_agents/mcp/workflows/prompt_server_workflow.py
new file mode 100644
index 00000000..0a873561
--- /dev/null
+++ b/openai_agents/mcp/workflows/prompt_server_workflow.py
@@ -0,0 +1,79 @@
+from __future__ import annotations
+
+from agents import Agent, Runner, trace
+from agents.mcp import MCPServer
+from temporalio import workflow
+from temporalio.contrib import openai_agents
+
+
+@workflow.defn
+class PromptServerWorkflow:
+ @workflow.run
+ async def run(self) -> str:
+ with trace(workflow_name="Prompt Server Example"):
+ outputs: list[str] = []
+ server: MCPServer = openai_agents.workflow.stateless_mcp_server(
+ "PromptServer"
+ )
+
+ # Show available prompts
+ workflow.logger.info("=== AVAILABLE PROMPTS ===")
+ outputs.append("=== AVAILABLE PROMPTS ===")
+ prompts_result = await server.list_prompts()
+ workflow.logger.info("User can select from these prompts:")
+ outputs.append("User can select from these prompts:")
+ for i, prompt in enumerate(prompts_result.prompts, 1):
+ line = f" {i}. {prompt.name} - {prompt.description}"
+ workflow.logger.info(line)
+ outputs.append(line)
+ workflow.logger.info("")
+ outputs.append("")
+
+ # Demo code review with user-selected prompt
+ workflow.logger.info("=== CODE REVIEW DEMO ===")
+
+ # Get instructions from prompt
+ workflow.logger.info(
+ "Getting instructions from prompt: generate_code_review_instructions"
+ )
+ try:
+ prompt_result = await server.get_prompt(
+ "generate_code_review_instructions",
+ {"focus": "security vulnerabilities", "language": "python"},
+ )
+ content = prompt_result.messages[0].content
+ instructions = (
+ content.text if hasattr(content, "text") else str(content)
+ )
+ workflow.logger.info("Generated instructions")
+ preview = instructions[:200].replace("\n", " ") + (
+ "..." if len(instructions) > 200 else ""
+ )
+ outputs.append("=== INSTRUCTIONS (PREVIEW) ===")
+ outputs.append(preview)
+ except Exception as e:
+ workflow.logger.info(f"Failed to get instructions: {e}")
+ instructions = f"You are a helpful assistant. Error: {e}"
+ outputs.append(f"Failed to get instructions: {e}")
+
+ agent = Agent(
+ name="Code Reviewer Agent",
+ instructions=instructions,
+ )
+
+ message = """Please review this code:
+
+def process_user_input(user_input):
+ command = f"echo {user_input}"
+ os.system(command)
+ return "Command executed"
+
+"""
+
+ workflow.logger.info(f"Running: {message[:60]}...")
+ outputs.append("=== REVIEW OUTPUT ===")
+ result = await Runner.run(starting_agent=agent, input=message)
+ workflow.logger.info(result.final_output)
+ outputs.append(result.final_output)
+ workflow.logger.info("\n" + "=" * 50 + "\n")
+ return "\n".join(outputs)
diff --git a/openai_agents/mcp/workflows/sse_workflow.py b/openai_agents/mcp/workflows/sse_workflow.py
new file mode 100644
index 00000000..c8b80e03
--- /dev/null
+++ b/openai_agents/mcp/workflows/sse_workflow.py
@@ -0,0 +1,38 @@
+from __future__ import annotations
+
+from agents import Agent, Runner, trace
+from agents.mcp import MCPServer
+from agents.model_settings import ModelSettings
+from temporalio import workflow
+from temporalio.contrib import openai_agents
+
+
+@workflow.defn
+class SseWorkflow:
+ @workflow.run
+ async def run(self) -> str:
+ with trace(workflow_name="SSE Example"):
+ server: MCPServer = openai_agents.workflow.stateless_mcp_server("SseServer")
+ agent = Agent(
+ name="Assistant",
+ instructions="Use the tools to answer the questions.",
+ mcp_servers=[server],
+ model_settings=ModelSettings(tool_choice="required"),
+ )
+
+ # Use the `add` tool to add two numbers
+ message = "Add these numbers: 7 and 22."
+ workflow.logger.info(f"Running: {message}")
+ result1 = await Runner.run(starting_agent=agent, input=message)
+
+ # Run the `get_weather` tool
+ message = "What's the weather in Tokyo?"
+ workflow.logger.info(f"Running: {message}")
+ result2 = await Runner.run(starting_agent=agent, input=message)
+
+ # Run the `get_secret_word` tool
+ message = "What's the secret word?"
+ workflow.logger.info(f"Running: {message}")
+ result3 = await Runner.run(starting_agent=agent, input=message)
+
+ return f"{result1.final_output}\n\n{result2.final_output}\n\n{result3.final_output}"
diff --git a/openai_agents/mcp/workflows/streamable_http_workflow.py b/openai_agents/mcp/workflows/streamable_http_workflow.py
new file mode 100644
index 00000000..98b7d576
--- /dev/null
+++ b/openai_agents/mcp/workflows/streamable_http_workflow.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+from agents import Agent, Runner, trace
+from agents.mcp import MCPServer
+from agents.model_settings import ModelSettings
+from temporalio import workflow
+from temporalio.contrib import openai_agents
+
+
+@workflow.defn
+class StreamableHttpWorkflow:
+ @workflow.run
+ async def run(self) -> str:
+ with trace(workflow_name="Streamable HTTP Example"):
+ server: MCPServer = openai_agents.workflow.stateless_mcp_server(
+ "StreamableHttpServer"
+ )
+ agent = Agent(
+ name="Assistant",
+ instructions="Use the tools to answer the questions.",
+ mcp_servers=[server],
+ model_settings=ModelSettings(tool_choice="required"),
+ )
+
+ # Use the `add` tool to add two numbers
+ message = "Add these numbers: 7 and 22."
+ workflow.logger.info(f"Running: {message}")
+ result1 = await Runner.run(starting_agent=agent, input=message)
+
+ # Run the `get_weather` tool
+ message = "What's the weather in Tokyo?"
+ workflow.logger.info(f"Running: {message}")
+ result2 = await Runner.run(starting_agent=agent, input=message)
+
+ # Run the `get_secret_word` tool
+ message = "What's the secret word?"
+ workflow.logger.info(f"Running: {message}")
+ result3 = await Runner.run(starting_agent=agent, input=message)
+
+ return f"{result1.final_output}\n\n{result2.final_output}\n\n{result3.final_output}"
diff --git a/openai_agents/model_providers/README.md b/openai_agents/model_providers/README.md
new file mode 100644
index 00000000..df8f286e
--- /dev/null
+++ b/openai_agents/model_providers/README.md
@@ -0,0 +1,67 @@
+# Model Providers Examples
+
+Custom LLM provider integration examples for OpenAI Agents SDK with Temporal workflows.
+
+*Adapted from [OpenAI Agents SDK model providers examples](https://github.com/openai/openai-agents-python/tree/main/examples/model_providers)*
+
+Before running these examples, be sure to review the [prerequisites and background on the integration](../README.md).
+
+## Running the Examples
+
+### Currently Implemented
+
+#### LiteLLM Auto
+Uses built-in LiteLLM support to connect to various model providers.
+
+Start the LiteLLM provider worker:
+```bash
+# Set the required environment variable for your chosen provider
+export ANTHROPIC_API_KEY="your_anthropic_api_key" # For Anthropic
+
+uv run openai_agents/model_providers/run_litellm_provider_worker.py
+```
+
+Then run the example in a separate terminal:
+```bash
+uv run openai_agents/model_providers/run_litellm_auto_workflow.py
+```
+
+The example uses Anthropic Claude by default but can be modified to use other LiteLLM-supported providers.
+
+Find more LiteLLM providers at: https://docs.litellm.ai/docs/providers
+
+### Extra
+
+#### GPT-OSS with Ollama
+
+This example demonstrates tool calling using the gpt-oss reasoning model with a local Ollama server.
+Running this example requires sufficiently powerful hardware (and involves a 14 GB model download.
+It is adapted from the [OpenAI Cookbook example](https://cookbook.openai.com/articles/gpt-oss/run-locally-ollama#agents-sdk-integration).
+
+
+Make sure you have [Ollama](https://ollama.com/) installed:
+```bash
+ollama serve
+```
+
+Download the `gpt-oss` model:
+```bash
+ollama pull gpt-oss:20b
+```
+
+Start the gpt-oss worker:
+```bash
+uv run openai_agents/model_providers/run_gpt_oss_worker.py
+```
+
+Then run the example in a separate terminal:
+```bash
+uv run openai_agents/model_providers/run_gpt_oss_workflow.py
+```
+
+### Not Yet Implemented
+
+- **Custom Example Agent** - Custom OpenAI client integration
+- **Custom Example Global** - Global default client configuration
+- **Custom Example Provider** - Custom ModelProvider pattern
+- **LiteLLM Provider** - Interactive model/API key input
\ No newline at end of file
diff --git a/openai_agents/model_providers/run_gpt_oss_worker.py b/openai_agents/model_providers/run_gpt_oss_worker.py
new file mode 100644
index 00000000..59798a1d
--- /dev/null
+++ b/openai_agents/model_providers/run_gpt_oss_worker.py
@@ -0,0 +1,68 @@
+import asyncio
+import logging
+from datetime import timedelta
+from typing import Optional
+
+from agents import (
+ Model,
+ ModelProvider,
+ OpenAIChatCompletionsModel,
+ set_tracing_disabled,
+)
+from openai import AsyncOpenAI
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import ModelActivityParameters, OpenAIAgentsPlugin
+from temporalio.worker import Worker
+
+from openai_agents.model_providers.workflows.gpt_oss_workflow import GptOssWorkflow
+
+ollama_client = AsyncOpenAI(
+ base_url="http://localhost:11434/v1", # Local Ollama API endpoint
+ api_key="ollama", # Ignored by Ollama
+)
+
+
+class CustomModelProvider(ModelProvider):
+ def get_model(self, model_name: Optional[str]) -> Model:
+ model = OpenAIChatCompletionsModel(
+ model=model_name if model_name else "gpt-oss:20b",
+ openai_client=ollama_client,
+ )
+ return model
+
+
+async def main():
+ # Disable Agents SDK tracing — the default exporter sends traces to OpenAI's
+ # backend, which requires an OpenAI API key not available in these samples.
+ # Call here rather than in the workflow because it's a global side effect.
+ set_tracing_disabled(disabled=True)
+
+ # Configure logging to show workflow debug messages
+ logging.basicConfig(level=logging.WARNING)
+ logging.getLogger("temporalio.workflow").setLevel(logging.DEBUG)
+
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(
+ model_params=ModelActivityParameters(
+ start_to_close_timeout=timedelta(seconds=30)
+ ),
+ model_provider=CustomModelProvider(),
+ ),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="openai-agents-model-providers-task-queue",
+ workflows=[
+ GptOssWorkflow,
+ ],
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/model_providers/run_gpt_oss_workflow.py b/openai_agents/model_providers/run_gpt_oss_workflow.py
new file mode 100644
index 00000000..35df5979
--- /dev/null
+++ b/openai_agents/model_providers/run_gpt_oss_workflow.py
@@ -0,0 +1,27 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.model_providers.workflows.gpt_oss_workflow import GptOssWorkflow
+
+
+async def main():
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ result = await client.execute_workflow(
+ GptOssWorkflow.run,
+ "What's the weather in Tokyo?",
+ id="litellm-gpt-oss-workflow-id",
+ task_queue="openai-agents-model-providers-task-queue",
+ )
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/model_providers/run_litellm_auto_workflow.py b/openai_agents/model_providers/run_litellm_auto_workflow.py
new file mode 100644
index 00000000..0e4e05f1
--- /dev/null
+++ b/openai_agents/model_providers/run_litellm_auto_workflow.py
@@ -0,0 +1,29 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.model_providers.workflows.litellm_auto_workflow import (
+ LitellmAutoWorkflow,
+)
+
+
+async def main():
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ result = await client.execute_workflow(
+ LitellmAutoWorkflow.run,
+ "What's the weather in Tokyo?",
+ id="litellm-auto-workflow-id",
+ task_queue="openai-agents-model-providers-task-queue",
+ )
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/model_providers/run_litellm_provider_worker.py b/openai_agents/model_providers/run_litellm_provider_worker.py
new file mode 100644
index 00000000..3441013c
--- /dev/null
+++ b/openai_agents/model_providers/run_litellm_provider_worker.py
@@ -0,0 +1,45 @@
+import asyncio
+from datetime import timedelta
+
+from agents import set_tracing_disabled
+from agents.extensions.models.litellm_provider import LitellmProvider
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import ModelActivityParameters, OpenAIAgentsPlugin
+from temporalio.worker import Worker
+
+from openai_agents.model_providers.workflows.litellm_auto_workflow import (
+ LitellmAutoWorkflow,
+)
+
+
+async def main():
+ # Disable Agents SDK tracing — the default exporter sends traces to OpenAI's
+ # backend, which requires an OpenAI API key not available in these samples.
+ # Call here rather than in the workflow because it's a global side effect.
+ set_tracing_disabled(disabled=True)
+
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(
+ model_params=ModelActivityParameters(
+ start_to_close_timeout=timedelta(seconds=30)
+ ),
+ model_provider=LitellmProvider(),
+ ),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="openai-agents-model-providers-task-queue",
+ workflows=[
+ LitellmAutoWorkflow,
+ ],
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/model_providers/workflows/gpt_oss_workflow.py b/openai_agents/model_providers/workflows/gpt_oss_workflow.py
new file mode 100644
index 00000000..821844b3
--- /dev/null
+++ b/openai_agents/model_providers/workflows/gpt_oss_workflow.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+
+from agents import Agent, Runner, function_tool
+from temporalio import workflow
+
+
+@workflow.defn
+class GptOssWorkflow:
+ @workflow.run
+ async def run(self, prompt: str) -> str:
+ @function_tool
+ def get_weather(city: str):
+ workflow.logger.debug(f"Getting weather for {city}")
+ return f"The weather in {city} is sunny."
+
+ agent = Agent(
+ name="Assistant",
+ instructions="You only respond in haikus. When asked about the weather always use the tool to get the current weather..",
+ model="gpt-oss:20b",
+ tools=[get_weather],
+ )
+
+ result = await Runner.run(agent, prompt)
+ return result.final_output
diff --git a/openai_agents/model_providers/workflows/litellm_auto_workflow.py b/openai_agents/model_providers/workflows/litellm_auto_workflow.py
new file mode 100644
index 00000000..9fe3d40b
--- /dev/null
+++ b/openai_agents/model_providers/workflows/litellm_auto_workflow.py
@@ -0,0 +1,23 @@
+from __future__ import annotations
+
+from agents import Agent, Runner, function_tool
+from temporalio import workflow
+
+
+@workflow.defn
+class LitellmAutoWorkflow:
+ @workflow.run
+ async def run(self, prompt: str) -> str:
+ @function_tool
+ def get_weather(city: str):
+ return f"The weather in {city} is sunny."
+
+ agent = Agent(
+ name="Assistant",
+ instructions="You only respond in haikus.",
+ model="anthropic/claude-3-5-sonnet-20240620",
+ tools=[get_weather],
+ )
+
+ result = await Runner.run(agent, prompt)
+ return result.final_output
diff --git a/openai_agents/reasoning_content/README.md b/openai_agents/reasoning_content/README.md
new file mode 100644
index 00000000..c654d266
--- /dev/null
+++ b/openai_agents/reasoning_content/README.md
@@ -0,0 +1,37 @@
+# Reasoning Content
+
+Example demonstrating how to use the reasoning content feature with models that support it, running in the context of Temporal's durable execution.
+
+*Adapted from [OpenAI Agents SDK reasoning content](https://github.com/openai/openai-agents-python/tree/main/examples/reasoning_content)*
+
+## Overview
+
+Some models, like deepseek-reasoner, provide a reasoning_content field in addition to the regular content. This example shows how to access and use this reasoning content within Temporal workflows. The reasoning content contains the model's step-by-step thinking process before providing the final answer.
+
+## Architecture
+
+This example uses an activity to handle the OpenAI model calls. The workflow orchestrates the process by calling the `get_reasoning_response` activity, which uses the OpenAI provider to get a response from a reasoning-capable model and extracts both reasoning content and regular content.
+
+The model calls are run in an activity rather than directly in the workflow because Temporal's the involve I/O.
+
+## Running the Example
+
+First, start the worker:
+```bash
+uv run openai_agents/reasoning_content/run_worker.py
+```
+
+Then run the reasoning content workflow:
+```bash
+uv run openai_agents/reasoning_content/run_reasoning_content_workflow.py
+```
+
+## Requirements
+
+- Set your `OPENAI_API_KEY` environment variable
+- Use a model that supports reasoning content (e.g., `deepseek-reasoner`)
+- Optionally set `EXAMPLE_MODEL_NAME` environment variable to specify the model
+
+## Note on Streaming
+
+The original OpenAI Agents SDK example includes streaming capabilities, but since Temporal workflows do not support streaming yet, this example contains only the non-streaming approach.
\ No newline at end of file
diff --git a/openai_agents/reasoning_content/activities/reasoning_activities.py b/openai_agents/reasoning_content/activities/reasoning_activities.py
new file mode 100644
index 00000000..1f7ef9ef
--- /dev/null
+++ b/openai_agents/reasoning_content/activities/reasoning_activities.py
@@ -0,0 +1,53 @@
+import os
+from typing import Any, cast
+
+from agents import ModelSettings
+from agents.models.interface import ModelTracing
+from agents.models.openai_provider import OpenAIProvider
+from openai.types.responses import ResponseOutputRefusal, ResponseOutputText
+from temporalio import activity
+
+
+@activity.defn
+async def get_reasoning_response(
+ prompt: str, model_name: str | None = None
+) -> tuple[str | None, str | None]:
+ """
+ Activity to get response from a reasoning-capable model.
+ Returns tuple of (reasoning_content, regular_content).
+ """
+ model_name = model_name or os.getenv("EXAMPLE_MODEL_NAME") or "deepseek-reasoner"
+
+ provider = OpenAIProvider()
+ model = provider.get_model(model_name)
+
+ response = await model.get_response(
+ system_instructions="You are a helpful assistant that explains your reasoning step by step.",
+ input=prompt,
+ model_settings=ModelSettings(),
+ tools=[],
+ output_schema=None,
+ handoffs=[],
+ tracing=ModelTracing.DISABLED,
+ previous_response_id=None,
+ prompt=None,
+ conversation_id=None,
+ )
+
+ # Extract reasoning content and regular content from the response
+ reasoning_content = None
+ regular_content = None
+
+ for item in response.output:
+ if hasattr(item, "type") and item.type == "reasoning":
+ reasoning_content = item.summary[0].text
+ elif hasattr(item, "type") and item.type == "message":
+ if item.content and len(item.content) > 0:
+ content_item = item.content[0]
+ if isinstance(content_item, ResponseOutputText):
+ regular_content = content_item.text
+ elif isinstance(content_item, ResponseOutputRefusal):
+ refusal_item = cast(Any, content_item)
+ regular_content = refusal_item.refusal
+
+ return reasoning_content, regular_content
diff --git a/openai_agents/reasoning_content/run_reasoning_content_workflow.py b/openai_agents/reasoning_content/run_reasoning_content_workflow.py
new file mode 100644
index 00000000..79e5d7ba
--- /dev/null
+++ b/openai_agents/reasoning_content/run_reasoning_content_workflow.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+
+import asyncio
+import os
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.reasoning_content.workflows.reasoning_content_workflow import (
+ ReasoningContentWorkflow,
+ ReasoningResult,
+)
+
+
+async def main():
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Demo prompts that benefit from reasoning
+ demo_prompts = [
+ "What is the square root of 841? Please explain your reasoning.",
+ "Explain the concept of recursion in programming",
+ "Write a haiku about recursion in programming",
+ ]
+
+ model_name = os.getenv("EXAMPLE_MODEL_NAME") or "deepseek-reasoner"
+ print(f"Using model: {model_name}")
+ print("Note: This example requires a model that supports reasoning content.")
+ print("You may need to use a specific model like deepseek-reasoner or similar.\n")
+
+ for i, prompt in enumerate(demo_prompts, 1):
+ print(f"=== Example {i}: {prompt} ===")
+
+ result: ReasoningResult = await client.execute_workflow(
+ ReasoningContentWorkflow.run,
+ args=[prompt, model_name],
+ id=f"reasoning-content-{i}",
+ task_queue="reasoning-content-task-queue",
+ )
+
+ print(f"\nPrompt: {result.prompt}")
+ print("\nReasoning Content:")
+ print(result.reasoning_content or "No reasoning content provided")
+ print("\nRegular Content:")
+ print(result.regular_content or "No regular content provided")
+ print("-" * 50 + "\n")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/reasoning_content/run_worker.py b/openai_agents/reasoning_content/run_worker.py
new file mode 100644
index 00000000..51393b2e
--- /dev/null
+++ b/openai_agents/reasoning_content/run_worker.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+from temporalio.worker import Worker
+
+from openai_agents.reasoning_content.activities.reasoning_activities import (
+ get_reasoning_response,
+)
+from openai_agents.reasoning_content.workflows.reasoning_content_workflow import (
+ ReasoningContentWorkflow,
+)
+
+
+async def main():
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="reasoning-content-task-queue",
+ workflows=[ReasoningContentWorkflow],
+ activities=[get_reasoning_response],
+ )
+
+ print("Starting reasoning content worker...")
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/reasoning_content/workflows/reasoning_content_workflow.py b/openai_agents/reasoning_content/workflows/reasoning_content_workflow.py
new file mode 100644
index 00000000..0a9f0a15
--- /dev/null
+++ b/openai_agents/reasoning_content/workflows/reasoning_content_workflow.py
@@ -0,0 +1,32 @@
+from dataclasses import dataclass
+
+from temporalio import workflow
+
+from openai_agents.reasoning_content.activities.reasoning_activities import (
+ get_reasoning_response,
+)
+
+
+@dataclass
+class ReasoningResult:
+ reasoning_content: str | None
+ regular_content: str | None
+ prompt: str
+
+
+@workflow.defn
+class ReasoningContentWorkflow:
+ @workflow.run
+ async def run(self, prompt: str, model_name: str | None = None) -> ReasoningResult:
+ # Call the activity to get the reasoning response
+ reasoning_content, regular_content = await workflow.execute_activity(
+ get_reasoning_response,
+ args=[prompt, model_name],
+ start_to_close_timeout=workflow.timedelta(minutes=5),
+ )
+
+ return ReasoningResult(
+ reasoning_content=reasoning_content,
+ regular_content=regular_content,
+ prompt=prompt,
+ )
diff --git a/openai_agents/research_bot/README.md b/openai_agents/research_bot/README.md
new file mode 100644
index 00000000..f4f4a074
--- /dev/null
+++ b/openai_agents/research_bot/README.md
@@ -0,0 +1,35 @@
+# Research Bot
+
+Multi-agent research system with specialized roles, extended with Temporal's durable execution.
+
+*Adapted from [OpenAI Agents SDK research bot](https://github.com/openai/openai-agents-python/tree/main/examples/research_bot)*
+
+## Architecture
+
+The flow is:
+
+1. User enters their research topic
+2. `planner_agent` comes up with a plan to search the web for information. The plan is a list of search queries, with a search term and a reason for each query.
+3. For each search item, we run a `search_agent`, which uses the Web Search tool to search for that term and summarize the results. These all run in parallel.
+4. Finally, the `writer_agent` receives the search summaries, and creates a written report.
+
+## Running the Example
+
+First, start the worker:
+```bash
+uv run openai_agents/research_bot/run_worker.py
+```
+
+Then run the research workflow:
+```bash
+uv run openai_agents/research_bot/run_research_workflow.py
+```
+
+## Suggested Improvements
+
+If you're building your own research bot, some ideas to add to this are:
+
+1. Retrieval: Add support for fetching relevant information from a vector store. You could use the File Search tool for this.
+2. Image and file upload: Allow users to attach PDFs or other files, as baseline context for the research.
+3. More planning and thinking: Models often produce better results given more time to think. Improve the planning process to come up with a better plan, and add an evaluation step so that the model can choose to improve its results, search for more stuff, etc.
+4. Code execution: Allow running code, which is useful for data analysis.
\ No newline at end of file
diff --git a/openai_agents/research_bot/agents/planner_agent.py b/openai_agents/research_bot/agents/planner_agent.py
new file mode 100644
index 00000000..3e2f26de
--- /dev/null
+++ b/openai_agents/research_bot/agents/planner_agent.py
@@ -0,0 +1,29 @@
+from agents import Agent
+from pydantic import BaseModel
+
+PROMPT = (
+ "You are a helpful research assistant. Given a query, come up with a set of web searches "
+ "to perform to best answer the query. Output between 5 and 20 terms to query for."
+)
+
+
+class WebSearchItem(BaseModel):
+ reason: str
+ "Your reasoning for why this search is important to the query."
+
+ query: str
+ "The search term to use for the web search."
+
+
+class WebSearchPlan(BaseModel):
+ searches: list[WebSearchItem]
+ """A list of web searches to perform to best answer the query."""
+
+
+def new_planner_agent():
+ return Agent(
+ name="PlannerAgent",
+ instructions=PROMPT,
+ model="gpt-4o",
+ output_type=WebSearchPlan,
+ )
diff --git a/openai_agents/research_bot/agents/research_manager.py b/openai_agents/research_bot/agents/research_manager.py
new file mode 100644
index 00000000..62a77a07
--- /dev/null
+++ b/openai_agents/research_bot/agents/research_manager.py
@@ -0,0 +1,80 @@
+from __future__ import annotations
+
+import asyncio
+
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ # TODO: Restore progress updates
+ from agents import RunConfig, Runner, custom_span, trace
+
+ from openai_agents.research_bot.agents.planner_agent import (
+ WebSearchItem,
+ WebSearchPlan,
+ new_planner_agent,
+ )
+ from openai_agents.research_bot.agents.search_agent import new_search_agent
+ from openai_agents.research_bot.agents.writer_agent import (
+ ReportData,
+ new_writer_agent,
+ )
+
+
+class ResearchManager:
+ def __init__(self):
+ self.run_config = RunConfig()
+ self.search_agent = new_search_agent()
+ self.planner_agent = new_planner_agent()
+ self.writer_agent = new_writer_agent()
+
+ async def run(self, query: str) -> str:
+ with trace("Research trace"):
+ search_plan = await self._plan_searches(query)
+ search_results = await self._perform_searches(search_plan)
+ report = await self._write_report(query, search_results)
+
+ return report.markdown_report
+
+ async def _plan_searches(self, query: str) -> WebSearchPlan:
+ result = await Runner.run(
+ self.planner_agent,
+ f"Query: {query}",
+ run_config=self.run_config,
+ )
+ return result.final_output_as(WebSearchPlan)
+
+ async def _perform_searches(self, search_plan: WebSearchPlan) -> list[str]:
+ with custom_span("Search the web"):
+ num_completed = 0
+ tasks = [
+ asyncio.create_task(self._search(item)) for item in search_plan.searches
+ ]
+ results = []
+ for task in workflow.as_completed(tasks):
+ result = await task
+ if result is not None:
+ results.append(result)
+ num_completed += 1
+ return results
+
+ async def _search(self, item: WebSearchItem) -> str | None:
+ input = f"Search term: {item.query}\nReason for searching: {item.reason}"
+ try:
+ result = await Runner.run(
+ self.search_agent,
+ input,
+ run_config=self.run_config,
+ )
+ return str(result.final_output)
+ except Exception:
+ return None
+
+ async def _write_report(self, query: str, search_results: list[str]) -> ReportData:
+ input = f"Original query: {query}\nSummarized search results: {search_results}"
+ result = await Runner.run(
+ self.writer_agent,
+ input,
+ run_config=self.run_config,
+ )
+
+ return result.final_output_as(ReportData)
diff --git a/openai_agents/research_bot/agents/search_agent.py b/openai_agents/research_bot/agents/search_agent.py
new file mode 100644
index 00000000..43c30ed8
--- /dev/null
+++ b/openai_agents/research_bot/agents/search_agent.py
@@ -0,0 +1,20 @@
+from agents import Agent, WebSearchTool
+from agents.model_settings import ModelSettings
+
+INSTRUCTIONS = (
+ "You are a research assistant. Given a search term, you search the web for that term and "
+ "produce a concise summary of the results. The summary must 2-3 paragraphs and less than 300 "
+ "words. Capture the main points. Write succinctly, no need to have complete sentences or good "
+ "grammar. This will be consumed by someone synthesizing a report, so its vital you capture the "
+ "essence and ignore any fluff. Do not include any additional commentary other than the summary "
+ "itself."
+)
+
+
+def new_search_agent():
+ return Agent(
+ name="Search agent",
+ instructions=INSTRUCTIONS,
+ tools=[WebSearchTool()],
+ model_settings=ModelSettings(tool_choice="required"),
+ )
diff --git a/openai_agents/research_bot/agents/writer_agent.py b/openai_agents/research_bot/agents/writer_agent.py
new file mode 100644
index 00000000..9d8a34b0
--- /dev/null
+++ b/openai_agents/research_bot/agents/writer_agent.py
@@ -0,0 +1,33 @@
+# Agent used to synthesize a final report from the individual summaries.
+from agents import Agent
+from pydantic import BaseModel
+
+PROMPT = (
+ "You are a senior researcher tasked with writing a cohesive report for a research query. "
+ "You will be provided with the original query, and some initial research done by a research "
+ "assistant.\n"
+ "You should first come up with an outline for the report that describes the structure and "
+ "flow of the report. Then, generate the report and return that as your final output.\n"
+ "The final output should be in markdown format, and it should be lengthy and detailed. Aim "
+ "for 5-10 pages of content, at least 1000 words."
+)
+
+
+class ReportData(BaseModel):
+ short_summary: str
+ """A short 2-3 sentence summary of the findings."""
+
+ markdown_report: str
+ """The final report"""
+
+ follow_up_questions: list[str]
+ """Suggested topics to research further"""
+
+
+def new_writer_agent():
+ return Agent(
+ name="WriterAgent",
+ instructions=PROMPT,
+ model="o3-mini",
+ output_type=ReportData,
+ )
diff --git a/openai_agents/research_bot/run_research_workflow.py b/openai_agents/research_bot/run_research_workflow.py
new file mode 100644
index 00000000..e2739ef5
--- /dev/null
+++ b/openai_agents/research_bot/run_research_workflow.py
@@ -0,0 +1,30 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.research_bot.workflows.research_bot_workflow import ResearchWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ ResearchWorkflow.run,
+ "Caribbean vacation spots in April, optimizing for surfing, hiking and water sports",
+ id="research-workflow",
+ task_queue="openai-agents-task-queue",
+ )
+
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/research_bot/run_worker.py b/openai_agents/research_bot/run_worker.py
new file mode 100644
index 00000000..fd6827d6
--- /dev/null
+++ b/openai_agents/research_bot/run_worker.py
@@ -0,0 +1,37 @@
+from __future__ import annotations
+
+import asyncio
+from datetime import timedelta
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import ModelActivityParameters, OpenAIAgentsPlugin
+from temporalio.worker import Worker
+
+from openai_agents.research_bot.workflows.research_bot_workflow import ResearchWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(
+ model_params=ModelActivityParameters(
+ start_to_close_timeout=timedelta(seconds=120)
+ )
+ ),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="openai-agents-task-queue",
+ workflows=[
+ ResearchWorkflow,
+ ],
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/research_bot/workflows/research_bot_workflow.py b/openai_agents/research_bot/workflows/research_bot_workflow.py
new file mode 100644
index 00000000..c2523c8c
--- /dev/null
+++ b/openai_agents/research_bot/workflows/research_bot_workflow.py
@@ -0,0 +1,10 @@
+from temporalio import workflow
+
+from openai_agents.research_bot.agents.research_manager import ResearchManager
+
+
+@workflow.defn
+class ResearchWorkflow:
+ @workflow.run
+ async def run(self, query: str) -> str:
+ return await ResearchManager().run(query)
diff --git a/openai_agents/tools/README.md b/openai_agents/tools/README.md
new file mode 100644
index 00000000..6f65e432
--- /dev/null
+++ b/openai_agents/tools/README.md
@@ -0,0 +1,61 @@
+# Tools Examples
+
+Demonstrations of various OpenAI agent tools integrated with Temporal workflows.
+
+*Adapted from [OpenAI Agents SDK tools examples](https://github.com/openai/openai-agents-python/tree/main/examples/tools)*
+
+Before running these examples, be sure to review the [prerequisites and background on the integration](../README.md).
+
+## Setup
+
+### Knowledge Base Setup (Required for File Search)
+Create a vector store with sample documents for file search testing:
+```bash
+uv run openai_agents/tools/setup_knowledge_base.py
+```
+
+This script:
+- Creates 6 sample documents
+- Uploads files to OpenAI with proper cleanup using context managers
+- Creates an assistant with vector store for file search capabilities
+- Updates workflow files with the new vector store ID automatically
+
+## Running the Examples
+
+First, start the worker (supports all tools):
+```bash
+uv run openai_agents/tools/run_worker.py
+```
+
+Then run individual examples in separate terminals:
+
+### Code Interpreter
+Execute Python code for mathematical calculations and data analysis:
+```bash
+uv run openai_agents/tools/run_code_interpreter_workflow.py
+```
+
+### File Search
+Search through uploaded documents using vector similarity:
+```bash
+uv run openai_agents/tools/run_file_search_workflow.py
+```
+
+### Image Generation
+Generate images:
+```bash
+uv run openai_agents/tools/run_image_generator_workflow.py
+```
+
+### Web Search
+Search the web for current information with location context:
+```bash
+uv run openai_agents/tools/run_web_search_workflow.py
+```
+
+
+## Omitted Examples
+
+The following tools from the [reference repository](https://github.com/openai/openai-agents-python/tree/main/examples/tools) are not included in this Temporal adaptation:
+
+- **Computer Use**: Complex browser automation not suitable for distributed systems implementation.
\ No newline at end of file
diff --git a/openai_agents/tools/run_code_interpreter_workflow.py b/openai_agents/tools/run_code_interpreter_workflow.py
new file mode 100644
index 00000000..2b819712
--- /dev/null
+++ b/openai_agents/tools/run_code_interpreter_workflow.py
@@ -0,0 +1,32 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.tools.workflows.code_interpreter_workflow import (
+ CodeInterpreterWorkflow,
+)
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ CodeInterpreterWorkflow.run,
+ "What is the square root of 273 * 312821 plus 1782?",
+ id="code-interpreter-workflow",
+ task_queue="openai-agents-tools-task-queue",
+ )
+
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/tools/run_file_search_workflow.py b/openai_agents/tools/run_file_search_workflow.py
new file mode 100644
index 00000000..09b8bb57
--- /dev/null
+++ b/openai_agents/tools/run_file_search_workflow.py
@@ -0,0 +1,33 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.tools.workflows.file_search_workflow import FileSearchWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ FileSearchWorkflow.run,
+ args=[
+ "Be concise, and tell me 1 sentence about Arrakis I might not know.",
+ "vs_68855c27140c8191849b5f1887d8d335", # Vector store with Arrakis knowledge
+ ],
+ id="file-search-workflow",
+ task_queue="openai-agents-tools-task-queue",
+ )
+
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/tools/run_image_generator_workflow.py b/openai_agents/tools/run_image_generator_workflow.py
new file mode 100644
index 00000000..77a60d26
--- /dev/null
+++ b/openai_agents/tools/run_image_generator_workflow.py
@@ -0,0 +1,60 @@
+import asyncio
+import base64
+import os
+import subprocess
+import sys
+import tempfile
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.tools.workflows.image_generator_workflow import (
+ ImageGeneratorWorkflow,
+)
+
+
+def open_file(path: str) -> None:
+ if sys.platform.startswith("darwin"):
+ subprocess.run(["open", path], check=False) # macOS
+ elif os.name == "nt": # Windows
+ os.startfile(path) # type: ignore
+ elif os.name == "posix":
+ subprocess.run(["xdg-open", path], check=False) # Linux/Unix
+ else:
+ print(f"Don't know how to open files on this platform: {sys.platform}")
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ ImageGeneratorWorkflow.run,
+ "Create an image of a frog eating a pizza, comic book style.",
+ id="image-generator-workflow",
+ task_queue="openai-agents-tools-task-queue",
+ )
+
+ print(f"Text result: {result.final_output}")
+
+ if result.image_data:
+ # Save and open the image
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
+ tmp.write(base64.b64decode(result.image_data))
+ temp_path = tmp.name
+
+ print(f"Image saved to: {temp_path}")
+ # Open the image
+ open_file(temp_path)
+ else:
+ print("No image data found in result")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/tools/run_web_search_workflow.py b/openai_agents/tools/run_web_search_workflow.py
new file mode 100644
index 00000000..dd7f992f
--- /dev/null
+++ b/openai_agents/tools/run_web_search_workflow.py
@@ -0,0 +1,33 @@
+import asyncio
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import OpenAIAgentsPlugin
+
+from openai_agents.tools.workflows.web_search_workflow import WebSearchWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(),
+ ],
+ )
+
+ # Execute a workflow
+ result = await client.execute_workflow(
+ WebSearchWorkflow.run,
+ args=[
+ "search the web for 'local sports news' and give me 1 interesting update in a sentence.",
+ "New York",
+ ],
+ id="web-search-workflow",
+ task_queue="openai-agents-tools-task-queue",
+ )
+
+ print(f"Result: {result}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/tools/run_worker.py b/openai_agents/tools/run_worker.py
new file mode 100644
index 00000000..a58df470
--- /dev/null
+++ b/openai_agents/tools/run_worker.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+import asyncio
+from datetime import timedelta
+
+from temporalio.client import Client
+from temporalio.contrib.openai_agents import ModelActivityParameters, OpenAIAgentsPlugin
+from temporalio.worker import Worker
+
+from openai_agents.tools.workflows.code_interpreter_workflow import (
+ CodeInterpreterWorkflow,
+)
+from openai_agents.tools.workflows.file_search_workflow import FileSearchWorkflow
+from openai_agents.tools.workflows.image_generator_workflow import (
+ ImageGeneratorWorkflow,
+)
+from openai_agents.tools.workflows.web_search_workflow import WebSearchWorkflow
+
+
+async def main():
+ # Create client connected to server at the given address
+ client = await Client.connect(
+ "localhost:7233",
+ plugins=[
+ OpenAIAgentsPlugin(
+ model_params=ModelActivityParameters(
+ start_to_close_timeout=timedelta(seconds=60)
+ )
+ ),
+ ],
+ )
+
+ worker = Worker(
+ client,
+ task_queue="openai-agents-tools-task-queue",
+ workflows=[
+ CodeInterpreterWorkflow,
+ FileSearchWorkflow,
+ ImageGeneratorWorkflow,
+ WebSearchWorkflow,
+ ],
+ activities=[
+ # No custom activities needed for these workflows
+ ],
+ )
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/tools/setup_knowledge_base.py b/openai_agents/tools/setup_knowledge_base.py
new file mode 100644
index 00000000..fc467805
--- /dev/null
+++ b/openai_agents/tools/setup_knowledge_base.py
@@ -0,0 +1,270 @@
+#!/usr/bin/env python3
+"""
+Setup script to create vector store with sample documents for testing FileSearchWorkflow.
+Creates documents about Arrakis/Dune and uploads them to OpenAI for file search testing.
+"""
+
+import asyncio
+import os
+import tempfile
+from contextlib import asynccontextmanager
+from pathlib import Path
+from typing import Dict, List
+
+from openai import AsyncOpenAI
+
+# Sample knowledge base content
+KNOWLEDGE_BASE = {
+ "arrakis_overview": """
+Arrakis: The Desert Planet
+
+Arrakis, also known as Dune, is the third planet of the Canopus system. This harsh desert world is the sole source of the spice melange, the most valuable substance in the known universe.
+
+Key characteristics:
+- Single biome: Desert covering the entire planet
+- No natural precipitation
+- Extreme temperature variations between day and night
+- Home to the giant sandworms (Shai-Hulud)
+- Indigenous population: the Fremen
+
+The planet's ecology is entirely dependent on the sandworms, which produce the spice as a byproduct of their life cycle. Water is incredibly scarce, leading to the development of stillsuits and other water conservation technologies.
+""",
+ "spice_melange": """
+The Spice Melange
+
+Melange, commonly known as "the spice," is the most important substance in the Dune universe. This geriatric spice extends life, expands consciousness, and is essential for space navigation.
+
+Properties of Spice:
+- Extends human lifespan significantly
+- Enhances mental abilities and prescient vision
+- Required for Guild Navigators to fold space
+- Highly addictive with fatal withdrawal symptoms
+- Turns eyes blue over time (the "Eyes of Ibad")
+
+Production:
+The spice is created through the interaction of sandworms with pre-spice masses in the deep desert. The presence of water is toxic to sandworms, making Arrakis the only known source of spice in the universe.
+
+Economic Impact:
+Control of spice production grants immense political and economic power, making Arrakis the most strategically important planet in the Imperium.
+""",
+ "sandworms": """
+Sandworms of Arrakis
+
+The sandworms, known to the Fremen as Shai-Hulud ("Old Man of the Desert"), are colossal creatures that dominate Arrakis. These massive beings can grow to lengths of over 400 meters and live for thousands of years.
+
+Characteristics:
+- Enormous size: up to 400+ meters in length
+- Extreme sensitivity to water and moisture
+- Produce the spice melange as part of their life cycle
+- Territorial and attracted to rhythmic vibrations
+- Crystalline teeth capable of crushing rock and metal
+
+Life Cycle:
+Sandworms begin as sandtrout, small creatures that sequester water. They eventually metamorphose into the giant sandworms through a complex process involving spice production.
+
+Cultural Significance:
+The Fremen worship sandworms as semi-divine beings and have developed elaborate rituals around them, including the dangerous practice of sandworm riding.
+""",
+ "fremen_culture": """
+The Fremen of Arrakis
+
+The Fremen are the indigenous people of Arrakis, perfectly adapted to life in the harsh desert environment. Their culture revolves around water conservation, survival, and reverence for the sandworms.
+
+Cultural Practices:
+- Water discipline: Every drop of moisture is preserved
+- Stillsuits: Advanced technology to recycle body moisture
+- Desert survival skills passed down through generations
+- Ritualistic relationship with sandworms
+- Sietch communities: Hidden underground settlements
+
+Religious Beliefs:
+The Fremen follow a syncretic religion combining elements of Islam, Buddhism, and Christianity, adapted to their desert environment. They believe in prophecies of a messiah who will transform Arrakis.
+
+Military Prowess:
+Despite their seemingly primitive lifestyle, the Fremen are formidable warriors, using their intimate knowledge of the desert and unconventional tactics to great effect.
+""",
+ "house_atreides": """
+House Atreides and Arrakis
+
+House Atreides, led by Duke Leto Atreides, was granted control of Arrakis by Emperor Shaddam IV in a political trap designed to destroy the noble house. This transition from House Harkonnen marked the beginning of the events in Dune.
+
+Key Figures:
+- Duke Leto Atreides: Noble leader focused on honor and justice
+- Lady Jessica: Bene Gesserit concubine and mother of Paul
+- Paul Atreides: Heir to the duchy and potential Kwisatz Haderach
+- Duncan Idaho: Loyal swordmaster and warrior
+- Gurney Halleck: Weapons master and troubadour
+
+The Atreides approach to ruling Arrakis differed dramatically from the Harkonnens, seeking to work with the Fremen rather than exploit them. This philosophy, while noble, ultimately led to their downfall when the Emperor and Harkonnens betrayed them.
+
+Legacy:
+Though House Atreides was destroyed in the coup, Paul's survival and alliance with the Fremen would eventually lead to an even greater destiny.
+""",
+ "ecology_arrakis": """
+The Ecology of Arrakis
+
+Arrakis presents a unique ecosystem entirely based on the water cycle created by sandworms and sandtrout. This closed ecological system has evolved over millennia to support life in extreme desert conditions.
+
+Water Cycle:
+- Sandtrout sequester all available water deep underground
+- This creates the desert conditions necessary for spice production
+- Adult sandworms are killed by water, maintaining the cycle
+- Plants and animals have evolved extreme water conservation
+
+Flora and Fauna:
+- Desert plants with deep root systems and water storage
+- Small desert animals adapted to minimal water consumption
+- No large surface water bodies exist naturally
+- All life forms show evolutionary adaptation to water scarcity
+
+The ecosystem is incredibly fragile - any significant introduction of water could disrupt the entire balance and potentially eliminate spice production, fundamentally changing the planet and the universe's economy.
+""",
+}
+
+
+@asynccontextmanager
+async def temporary_files(content_dict: Dict[str, str]):
+ """Context manager to create and cleanup temporary files."""
+ temp_files = []
+ try:
+ for name, content in content_dict.items():
+ temp_file = tempfile.NamedTemporaryFile(
+ mode="w", suffix=".txt", prefix=f"{name}_", delete=False
+ )
+ temp_file.write(content)
+ temp_file.close()
+ temp_files.append((name, temp_file.name))
+
+ yield temp_files
+ finally:
+ for _, temp_path in temp_files:
+ try:
+ os.unlink(temp_path)
+ except OSError:
+ pass
+
+
+async def upload_files_to_openai(temp_files: List[tuple[str, str]]) -> List[str]:
+ """Upload temporary files to OpenAI and return file IDs."""
+ client = AsyncOpenAI()
+ file_ids = []
+
+ for name, temp_path in temp_files:
+ try:
+ with open(temp_path, "rb") as f:
+ file_obj = await client.files.create(file=f, purpose="assistants")
+ file_ids.append(file_obj.id)
+ print(f"Uploaded {name}: {file_obj.id}")
+ except Exception as e:
+ print(f"Error uploading {name}: {e}")
+
+ return file_ids
+
+
+async def create_vector_store_with_assistant(file_ids: List[str]) -> str:
+ """Create an assistant with vector store containing the uploaded files."""
+ client = AsyncOpenAI()
+
+ try:
+ assistant = await client.beta.assistants.create(
+ name="Arrakis Knowledge Assistant",
+ instructions="You are an expert on Arrakis and the Dune universe. Use the uploaded files to answer questions about the desert planet, spice, sandworms, Fremen culture, and related topics.",
+ model="gpt-4o",
+ tools=[{"type": "file_search"}],
+ tool_resources={
+ "file_search": {
+ "vector_stores": [
+ {
+ "file_ids": file_ids,
+ "metadata": {"name": "Arrakis Knowledge Base"},
+ }
+ ]
+ }
+ },
+ )
+
+ # Extract vector store ID from assistant
+ if assistant.tool_resources and assistant.tool_resources.file_search:
+ vector_store_ids = assistant.tool_resources.file_search.vector_store_ids
+ if vector_store_ids:
+ return vector_store_ids[0]
+
+ raise Exception("No vector store ID found in assistant response")
+
+ except Exception as e:
+ print(f"Error creating assistant: {e}")
+ raise
+
+
+def update_workflow_files(vector_store_id: str):
+ """Update workflow files with the new vector store ID."""
+ import re
+
+ files_to_update = ["run_file_search_workflow.py"]
+
+ # Pattern to match any vector store ID with the specific comment
+ pattern = r'(vs_[a-f0-9]+)",\s*#\s*Vector store with Arrakis knowledge'
+ replacement = f'{vector_store_id}", # Vector store with Arrakis knowledge'
+
+ for filename in files_to_update:
+ file_path = Path(__file__).parent / filename
+ if file_path.exists():
+ try:
+ content = file_path.read_text()
+ if re.search(pattern, content):
+ updated_content = re.sub(pattern, replacement, content)
+ file_path.write_text(updated_content)
+ print(f"Updated {filename} with vector store ID")
+ else:
+ print(f"No matching pattern found in {filename}")
+ except Exception as e:
+ print(f"Error updating {filename}: {e}")
+
+
+async def main():
+ """Main function to set up the knowledge base."""
+ # Check for API key
+ if not os.getenv("OPENAI_API_KEY"):
+ print("Error: OPENAI_API_KEY environment variable not set")
+ print("Please set your OpenAI API key:")
+ print("export OPENAI_API_KEY='your-api-key-here'")
+ return
+
+ print("Setting up Arrakis knowledge base...")
+
+ try:
+ # Create temporary files and upload them
+ async with temporary_files(KNOWLEDGE_BASE) as temp_files:
+ print(f"Created {len(temp_files)} temporary files")
+
+ file_ids = await upload_files_to_openai(temp_files)
+
+ if not file_ids:
+ print("Error: No files were successfully uploaded")
+ return
+
+ print(f"Successfully uploaded {len(file_ids)} files")
+
+ # Create vector store via assistant
+ vector_store_id = await create_vector_store_with_assistant(file_ids)
+
+ print(f"Created vector store: {vector_store_id}")
+
+ # Update workflow files
+ update_workflow_files(vector_store_id)
+
+ print()
+ print("=" * 60)
+ print("KNOWLEDGE BASE SETUP COMPLETE")
+ print("=" * 60)
+ print(f"Vector Store ID: {vector_store_id}")
+ print(f"Files indexed: {len(file_ids)}")
+ print("Content: Arrakis/Dune universe knowledge")
+ print("=" * 60)
+
+ except Exception as e:
+ print(f"Setup failed: {e}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/openai_agents/tools/workflows/code_interpreter_workflow.py b/openai_agents/tools/workflows/code_interpreter_workflow.py
new file mode 100644
index 00000000..6715bc50
--- /dev/null
+++ b/openai_agents/tools/workflows/code_interpreter_workflow.py
@@ -0,0 +1,25 @@
+from __future__ import annotations
+
+from agents import Agent, CodeInterpreterTool, Runner
+from temporalio import workflow
+
+
+@workflow.defn
+class CodeInterpreterWorkflow:
+ @workflow.run
+ async def run(self, question: str) -> str:
+ agent = Agent(
+ name="Code interpreter",
+ instructions="You love doing math.",
+ tools=[
+ CodeInterpreterTool(
+ tool_config={
+ "type": "code_interpreter",
+ "container": {"type": "auto"},
+ },
+ )
+ ],
+ )
+
+ result = await Runner.run(agent, question)
+ return result.final_output
diff --git a/openai_agents/tools/workflows/file_search_workflow.py b/openai_agents/tools/workflows/file_search_workflow.py
new file mode 100644
index 00000000..915dcec4
--- /dev/null
+++ b/openai_agents/tools/workflows/file_search_workflow.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+
+from agents import Agent, FileSearchTool, Runner
+from temporalio import workflow
+
+
+@workflow.defn
+class FileSearchWorkflow:
+ @workflow.run
+ async def run(self, question: str, vector_store_id: str) -> str:
+ agent = Agent(
+ name="File searcher",
+ instructions="You are a helpful agent.",
+ tools=[
+ FileSearchTool(
+ max_num_results=3,
+ vector_store_ids=[vector_store_id],
+ include_search_results=True,
+ )
+ ],
+ )
+
+ result = await Runner.run(agent, question)
+ return result.final_output
diff --git a/openai_agents/tools/workflows/image_generator_workflow.py b/openai_agents/tools/workflows/image_generator_workflow.py
new file mode 100644
index 00000000..7c58091b
--- /dev/null
+++ b/openai_agents/tools/workflows/image_generator_workflow.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Optional
+
+from agents import Agent, ImageGenerationTool, Runner
+from temporalio import workflow
+
+
+@dataclass
+class ImageGenerationResult:
+ final_output: str
+ image_data: Optional[str] = None
+
+
+@workflow.defn
+class ImageGeneratorWorkflow:
+ @workflow.run
+ async def run(self, prompt: str) -> ImageGenerationResult:
+ agent = Agent(
+ name="Image generator",
+ instructions="You are a helpful agent.",
+ tools=[
+ ImageGenerationTool(
+ tool_config={"type": "image_generation", "quality": "low"},
+ )
+ ],
+ )
+
+ result = await Runner.run(agent, prompt)
+
+ # Extract image data if available
+ image_data = None
+ for item in result.new_items:
+ if (
+ item.type == "tool_call_item"
+ and item.raw_item.type == "image_generation_call"
+ and (img_result := item.raw_item.result)
+ ):
+ image_data = img_result
+ break
+
+ return ImageGenerationResult(
+ final_output=result.final_output, image_data=image_data
+ )
diff --git a/openai_agents/tools/workflows/web_search_workflow.py b/openai_agents/tools/workflows/web_search_workflow.py
new file mode 100644
index 00000000..8b505ac1
--- /dev/null
+++ b/openai_agents/tools/workflows/web_search_workflow.py
@@ -0,0 +1,20 @@
+from __future__ import annotations
+
+from agents import Agent, Runner, WebSearchTool
+from temporalio import workflow
+
+
+@workflow.defn
+class WebSearchWorkflow:
+ @workflow.run
+ async def run(self, question: str, user_city: str = "New York") -> str:
+ agent = Agent(
+ name="Web searcher",
+ instructions="You are a helpful agent.",
+ tools=[
+ WebSearchTool(user_location={"type": "approximate", "city": user_city})
+ ],
+ )
+
+ result = await Runner.run(agent, question)
+ return result.final_output
diff --git a/patching/README.md b/patching/README.md
index 61106f31..24744075 100644
--- a/patching/README.md
+++ b/patching/README.md
@@ -8,15 +8,15 @@ To run, first see [README.md](../README.md) for prerequisites. Then follow the p
This stage is for existing running workflows. To simulate our initial workflow, run the worker in a separate terminal:
- poetry run python worker.py --workflow initial
+ uv run patching/worker.py --workflow initial
Now we can start this workflow:
- poetry run python starter.py --start-workflow initial-workflow-id
+ uv run patching/starter.py --start-workflow initial-workflow-id
This will output "Started workflow with ID initial-workflow-id and ...". Now query this workflow:
- poetry run python starter.py --query-workflow initial-workflow-id
+ uv run patching/starter.py --query-workflow initial-workflow-id
This will output "Query result for ID initial-workflow-id: pre-patch".
@@ -25,21 +25,21 @@ This will output "Query result for ID initial-workflow-id: pre-patch".
This stage is for needing to run old and new workflows at the same time. To simulate our patched workflow, stop the
worker from before and start it again with the patched workflow:
- poetry run python worker.py --workflow patched
+ uv run patching/worker.py --workflow patched
Now let's start another workflow with this patched code:
- poetry run python starter.py --start-workflow patched-workflow-id
+ uv run patching/starter.py --start-workflow patched-workflow-id
This will output "Started workflow with ID patched-workflow-id and ...". Now query the old workflow that's still
running:
- poetry run python starter.py --query-workflow initial-workflow-id
+ uv run patching/starter.py --query-workflow initial-workflow-id
This will output "Query result for ID initial-workflow-id: pre-patch" since it is pre-patch. But if we execute a query
against the new code:
- poetry run python starter.py --query-workflow patched-workflow-id
+ uv run patching/starter.py --query-workflow patched-workflow-id
We get "Query result for ID patched-workflow-id: post-patch". This is how old workflow code can take old paths and new
workflow code can take new paths.
@@ -50,22 +50,22 @@ Once we know that all workflows that started with the initial code from "Stage 1
the patch so we can deprecate it. To use the patch deprecated workflow, stop the workflow from before and start it again
with:
- poetry run python worker.py --workflow patch-deprecated
+ uv run patching/worker.py --workflow patch-deprecated
All workflows in "Stage 2" and any new workflows will work. Now let's start another workflow with this patch deprecated
code:
- poetry run python starter.py --start-workflow patch-deprecated-workflow-id
+ uv run patching/starter.py --start-workflow patch-deprecated-workflow-id
This will output "Started workflow with ID patch-deprecated-workflow-id and ...". Now query the patched workflow that's
still running:
- poetry run python starter.py --query-workflow patched-workflow-id
+ uv run patching/starter.py --query-workflow patched-workflow-id
This will output "Query result for ID patched-workflow-id: post-patch". And if we execute a query against the latest
workflow:
- poetry run python starter.py --query-workflow patch-deprecated-workflow-id
+ uv run patching/starter.py --query-workflow patch-deprecated-workflow-id
As expected, this will output "Query result for ID patch-deprecated-workflow-id: post-patch".
@@ -75,22 +75,22 @@ Once we know we don't even have any workflows running on "Stage 2" or before (i.
both code paths), we can just remove the patch deprecation altogether. To use the patch complete workflow, stop the
workflow from before and start it again with:
- poetry run python worker.py --workflow patch-complete
+ uv run patching/worker.py --workflow patch-complete
All workflows in "Stage 3" and any new workflows will work. Now let's start another workflow with this patch complete
code:
- poetry run python starter.py --start-workflow patch-complete-workflow-id
+ uv run patching/starter.py --start-workflow patch-complete-workflow-id
This will output "Started workflow with ID patch-complete-workflow-id and ...". Now query the patch deprecated workflow
that's still running:
- poetry run python starter.py --query-workflow patch-deprecated-workflow-id
+ uv run patching/starter.py --query-workflow patch-deprecated-workflow-id
This will output "Query result for ID patch-deprecated-workflow-id: post-patch". And if we execute a query against the
latest workflow:
- poetry run python starter.py --query-workflow patch-complete-workflow-id
+ uv run patching/starter.py --query-workflow patch-complete-workflow-id
As expected, this will output "Query result for ID patch-complete-workflow-id: post-patch".
diff --git a/patching/starter.py b/patching/starter.py
index 9e6d7f31..f1b92f32 100644
--- a/patching/starter.py
+++ b/patching/starter.py
@@ -2,6 +2,7 @@
import asyncio
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
# Since it's just used for typing purposes, it doesn't matter which one we
# import
@@ -17,7 +18,9 @@ async def main():
raise RuntimeError("Either --start-workflow or --query-workflow is required")
# Connect client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
if args.start_workflow:
handle = await client.start_workflow(
diff --git a/patching/worker.py b/patching/worker.py
index 8f1e3c82..417c8ef9 100644
--- a/patching/worker.py
+++ b/patching/worker.py
@@ -2,6 +2,7 @@
import asyncio
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
from patching.activities import post_patch_activity, pre_patch_activity
@@ -30,7 +31,9 @@ async def main():
raise RuntimeError("Unrecognized workflow")
# Connect client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
diff --git a/poetry.lock b/poetry.lock
deleted file mode 100644
index 0bd2e852..00000000
--- a/poetry.lock
+++ /dev/null
@@ -1,1345 +0,0 @@
-# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
-
-[[package]]
-name = "aiohttp"
-version = "3.8.3"
-description = "Async http client/server framework (asyncio)"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ba71c9b4dcbb16212f334126cc3d8beb6af377f6703d9dc2d9fb3874fd667ee9"},
- {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d24b8bb40d5c61ef2d9b6a8f4528c2f17f1c5d2d31fed62ec860f6006142e83e"},
- {file = "aiohttp-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f88df3a83cf9df566f171adba39d5bd52814ac0b94778d2448652fc77f9eb491"},
- {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97decbb3372d4b69e4d4c8117f44632551c692bb1361b356a02b97b69e18a62"},
- {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309aa21c1d54b8ef0723181d430347d7452daaff93e8e2363db8e75c72c2fb2d"},
- {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad5383a67514e8e76906a06741febd9126fc7c7ff0f599d6fcce3e82b80d026f"},
- {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20acae4f268317bb975671e375493dbdbc67cddb5f6c71eebdb85b34444ac46b"},
- {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05a3c31c6d7cd08c149e50dc7aa2568317f5844acd745621983380597f027a18"},
- {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6f76310355e9fae637c3162936e9504b4767d5c52ca268331e2756e54fd4ca5"},
- {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:256deb4b29fe5e47893fa32e1de2d73c3afe7407738bd3c63829874661d4822d"},
- {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5c59fcd80b9049b49acd29bd3598cada4afc8d8d69bd4160cd613246912535d7"},
- {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:059a91e88f2c00fe40aed9031b3606c3f311414f86a90d696dd982e7aec48142"},
- {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2feebbb6074cdbd1ac276dbd737b40e890a1361b3cc30b74ac2f5e24aab41f7b"},
- {file = "aiohttp-3.8.3-cp310-cp310-win32.whl", hash = "sha256:5bf651afd22d5f0c4be16cf39d0482ea494f5c88f03e75e5fef3a85177fecdeb"},
- {file = "aiohttp-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:653acc3880459f82a65e27bd6526e47ddf19e643457d36a2250b85b41a564715"},
- {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:86fc24e58ecb32aee09f864cb11bb91bc4c1086615001647dbfc4dc8c32f4008"},
- {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75e14eac916f024305db517e00a9252714fce0abcb10ad327fb6dcdc0d060f1d"},
- {file = "aiohttp-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1fde0f44029e02d02d3993ad55ce93ead9bb9b15c6b7ccd580f90bd7e3de476"},
- {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab94426ddb1ecc6a0b601d832d5d9d421820989b8caa929114811369673235c"},
- {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89d2e02167fa95172c017732ed7725bc8523c598757f08d13c5acca308e1a061"},
- {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02f9a2c72fc95d59b881cf38a4b2be9381b9527f9d328771e90f72ac76f31ad8"},
- {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7149272fb5834fc186328e2c1fa01dda3e1fa940ce18fded6d412e8f2cf76d"},
- {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:512bd5ab136b8dc0ffe3fdf2dfb0c4b4f49c8577f6cae55dca862cd37a4564e2"},
- {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7018ecc5fe97027214556afbc7c502fbd718d0740e87eb1217b17efd05b3d276"},
- {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:88c70ed9da9963d5496d38320160e8eb7e5f1886f9290475a881db12f351ab5d"},
- {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:da22885266bbfb3f78218dc40205fed2671909fbd0720aedba39b4515c038091"},
- {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:e65bc19919c910127c06759a63747ebe14f386cda573d95bcc62b427ca1afc73"},
- {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:08c78317e950e0762c2983f4dd58dc5e6c9ff75c8a0efeae299d363d439c8e34"},
- {file = "aiohttp-3.8.3-cp311-cp311-win32.whl", hash = "sha256:45d88b016c849d74ebc6f2b6e8bc17cabf26e7e40c0661ddd8fae4c00f015697"},
- {file = "aiohttp-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:96372fc29471646b9b106ee918c8eeb4cca423fcbf9a34daa1b93767a88a2290"},
- {file = "aiohttp-3.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c971bf3786b5fad82ce5ad570dc6ee420f5b12527157929e830f51c55dc8af77"},
- {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff25f48fc8e623d95eca0670b8cc1469a83783c924a602e0fbd47363bb54aaca"},
- {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e381581b37db1db7597b62a2e6b8b57c3deec95d93b6d6407c5b61ddc98aca6d"},
- {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db19d60d846283ee275d0416e2a23493f4e6b6028825b51290ac05afc87a6f97"},
- {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25892c92bee6d9449ffac82c2fe257f3a6f297792cdb18ad784737d61e7a9a85"},
- {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398701865e7a9565d49189f6c90868efaca21be65c725fc87fc305906be915da"},
- {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4a4fbc769ea9b6bd97f4ad0b430a6807f92f0e5eb020f1e42ece59f3ecfc4585"},
- {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:b29bfd650ed8e148f9c515474a6ef0ba1090b7a8faeee26b74a8ff3b33617502"},
- {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:1e56b9cafcd6531bab5d9b2e890bb4937f4165109fe98e2b98ef0dcfcb06ee9d"},
- {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ec40170327d4a404b0d91855d41bfe1fe4b699222b2b93e3d833a27330a87a6d"},
- {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2df5f139233060578d8c2c975128fb231a89ca0a462b35d4b5fcf7c501ebdbe1"},
- {file = "aiohttp-3.8.3-cp36-cp36m-win32.whl", hash = "sha256:f973157ffeab5459eefe7b97a804987876dd0a55570b8fa56b4e1954bf11329b"},
- {file = "aiohttp-3.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:437399385f2abcd634865705bdc180c8314124b98299d54fe1d4c8990f2f9494"},
- {file = "aiohttp-3.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:09e28f572b21642128ef31f4e8372adb6888846f32fecb288c8b0457597ba61a"},
- {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f3553510abdbec67c043ca85727396ceed1272eef029b050677046d3387be8d"},
- {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e168a7560b7c61342ae0412997b069753f27ac4862ec7867eff74f0fe4ea2ad9"},
- {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db4c979b0b3e0fa7e9e69ecd11b2b3174c6963cebadeecfb7ad24532ffcdd11a"},
- {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e164e0a98e92d06da343d17d4e9c4da4654f4a4588a20d6c73548a29f176abe2"},
- {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a78079d9a39ca9ca99a8b0ac2fdc0c4d25fc80c8a8a82e5c8211509c523363"},
- {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:21b30885a63c3f4ff5b77a5d6caf008b037cb521a5f33eab445dc566f6d092cc"},
- {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4b0f30372cef3fdc262f33d06e7b411cd59058ce9174ef159ad938c4a34a89da"},
- {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8135fa153a20d82ffb64f70a1b5c2738684afa197839b34cc3e3c72fa88d302c"},
- {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ad61a9639792fd790523ba072c0555cd6be5a0baf03a49a5dd8cfcf20d56df48"},
- {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:978b046ca728073070e9abc074b6299ebf3501e8dee5e26efacb13cec2b2dea0"},
- {file = "aiohttp-3.8.3-cp37-cp37m-win32.whl", hash = "sha256:0d2c6d8c6872df4a6ec37d2ede71eff62395b9e337b4e18efd2177de883a5033"},
- {file = "aiohttp-3.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:21d69797eb951f155026651f7e9362877334508d39c2fc37bd04ff55b2007091"},
- {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ca9af5f8f5812d475c5259393f52d712f6d5f0d7fdad9acdb1107dd9e3cb7eb"},
- {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d90043c1882067f1bd26196d5d2db9aa6d268def3293ed5fb317e13c9413ea4"},
- {file = "aiohttp-3.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d737fc67b9a970f3234754974531dc9afeea11c70791dcb7db53b0cf81b79784"},
- {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf909ea0a3fc9596e40d55d8000702a85e27fd578ff41a5500f68f20fd32e6c"},
- {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5835f258ca9f7c455493a57ee707b76d2d9634d84d5d7f62e77be984ea80b849"},
- {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da37dcfbf4b7f45d80ee386a5f81122501ec75672f475da34784196690762f4b"},
- {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f44875f2804bc0511a69ce44a9595d5944837a62caecc8490bbdb0e18b1342"},
- {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:527b3b87b24844ea7865284aabfab08eb0faf599b385b03c2aa91fc6edd6e4b6"},
- {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5ba88df9aa5e2f806650fcbeedbe4f6e8736e92fc0e73b0400538fd25a4dd96"},
- {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e7b8813be97cab8cb52b1375f41f8e6804f6507fe4660152e8ca5c48f0436017"},
- {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2dea10edfa1a54098703cb7acaa665c07b4e7568472a47f4e64e6319d3821ccf"},
- {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:713d22cd9643ba9025d33c4af43943c7a1eb8547729228de18d3e02e278472b6"},
- {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2d252771fc85e0cf8da0b823157962d70639e63cb9b578b1dec9868dd1f4f937"},
- {file = "aiohttp-3.8.3-cp38-cp38-win32.whl", hash = "sha256:66bd5f950344fb2b3dbdd421aaa4e84f4411a1a13fca3aeb2bcbe667f80c9f76"},
- {file = "aiohttp-3.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:84b14f36e85295fe69c6b9789b51a0903b774046d5f7df538176516c3e422446"},
- {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16c121ba0b1ec2b44b73e3a8a171c4f999b33929cd2397124a8c7fcfc8cd9e06"},
- {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d6aaa4e7155afaf994d7924eb290abbe81a6905b303d8cb61310a2aba1c68ba"},
- {file = "aiohttp-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43046a319664a04b146f81b40e1545d4c8ac7b7dd04c47e40bf09f65f2437346"},
- {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599418aaaf88a6d02a8c515e656f6faf3d10618d3dd95866eb4436520096c84b"},
- {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a2964319d359f494f16011e23434f6f8ef0434acd3cf154a6b7bec511e2fb7"},
- {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73a4131962e6d91109bca6536416aa067cf6c4efb871975df734f8d2fd821b37"},
- {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598adde339d2cf7d67beaccda3f2ce7c57b3b412702f29c946708f69cf8222aa"},
- {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75880ed07be39beff1881d81e4a907cafb802f306efd6d2d15f2b3c69935f6fb"},
- {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0239da9fbafd9ff82fd67c16704a7d1bccf0d107a300e790587ad05547681c8"},
- {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4e3a23ec214e95c9fe85a58470b660efe6534b83e6cbe38b3ed52b053d7cb6ad"},
- {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:47841407cc89a4b80b0c52276f3cc8138bbbfba4b179ee3acbd7d77ae33f7ac4"},
- {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:54d107c89a3ebcd13228278d68f1436d3f33f2dd2af5415e3feaeb1156e1a62c"},
- {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c37c5cce780349d4d51739ae682dec63573847a2a8dcb44381b174c3d9c8d403"},
- {file = "aiohttp-3.8.3-cp39-cp39-win32.whl", hash = "sha256:f178d2aadf0166be4df834c4953da2d7eef24719e8aec9a65289483eeea9d618"},
- {file = "aiohttp-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:88e5be56c231981428f4f506c68b6a46fa25c4123a2e86d156c58a8369d31ab7"},
- {file = "aiohttp-3.8.3.tar.gz", hash = "sha256:3828fb41b7203176b82fe5d699e0d845435f2374750a44b480ea6b930f6be269"},
-]
-
-[package.dependencies]
-aiosignal = ">=1.1.2"
-async-timeout = ">=4.0.0a3,<5.0"
-asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""}
-attrs = ">=17.3.0"
-charset-normalizer = ">=2.0,<3.0"
-frozenlist = ">=1.1.1"
-multidict = ">=4.5,<7.0"
-typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
-yarl = ">=1.0,<2.0"
-
-[package.extras]
-speedups = ["Brotli", "aiodns", "cchardet"]
-
-[[package]]
-name = "aiosignal"
-version = "1.2.0"
-description = "aiosignal: a list of registered asynchronous callbacks"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
- {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
-]
-
-[package.dependencies]
-frozenlist = ">=1.1.0"
-
-[[package]]
-name = "async-timeout"
-version = "4.0.2"
-description = "Timeout context manager for asyncio programs"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
- {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
-]
-
-[package.dependencies]
-typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""}
-
-[[package]]
-name = "asynctest"
-version = "0.13.0"
-description = "Enhance the standard unittest package with features for testing asyncio libraries"
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-files = [
- {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
- {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
-]
-
-[[package]]
-name = "attrs"
-version = "22.1.0"
-description = "Classes Without Boilerplate"
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-files = [
- {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
- {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
-]
-
-[package.extras]
-dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
-docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
-tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
-tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
-
-[[package]]
-name = "black"
-version = "22.10.0"
-description = "The uncompromising code formatter."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"},
- {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"},
- {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"},
- {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"},
- {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"},
- {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"},
- {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"},
- {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"},
- {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"},
- {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"},
- {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"},
- {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"},
- {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"},
- {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"},
- {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"},
- {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"},
- {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"},
- {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"},
- {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"},
- {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"},
- {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
-]
-
-[package.dependencies]
-click = ">=8.0.0"
-mypy-extensions = ">=0.4.3"
-pathspec = ">=0.9.0"
-platformdirs = ">=2"
-tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
-typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
-typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
-
-[package.extras]
-colorama = ["colorama (>=0.4.3)"]
-d = ["aiohttp (>=3.7.4)"]
-jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
-uvloop = ["uvloop (>=0.15.2)"]
-
-[[package]]
-name = "certifi"
-version = "2022.9.24"
-description = "Python package for providing Mozilla's CA Bundle."
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
- {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
-]
-
-[[package]]
-name = "cffi"
-version = "1.15.1"
-description = "Foreign Function Interface for Python calling C code."
-category = "dev"
-optional = false
-python-versions = "*"
-files = [
- {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
- {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
- {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
- {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
- {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
- {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
- {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
- {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
- {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
- {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
- {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
- {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
- {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
- {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
- {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
- {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
- {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
- {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
- {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
- {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
- {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
- {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
- {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
- {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
- {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
- {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
- {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
- {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
- {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
- {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
- {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
- {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
- {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
- {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
- {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
- {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
- {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
- {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
- {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
- {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
- {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
- {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
- {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
- {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
- {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
- {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
- {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
- {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
- {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
- {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
- {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
- {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
- {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
- {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
- {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
- {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
- {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
- {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
- {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
- {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
- {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
- {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
- {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
- {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
-]
-
-[package.dependencies]
-pycparser = "*"
-
-[[package]]
-name = "charset-normalizer"
-version = "2.1.1"
-description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-category = "dev"
-optional = false
-python-versions = ">=3.6.0"
-files = [
- {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
- {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
-]
-
-[package.extras]
-unicode-backport = ["unicodedata2"]
-
-[[package]]
-name = "click"
-version = "8.1.3"
-description = "Composable command line interface toolkit"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
- {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
-]
-
-[package.dependencies]
-colorama = {version = "*", markers = "platform_system == \"Windows\""}
-importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
-
-[[package]]
-name = "colorama"
-version = "0.4.6"
-description = "Cross-platform colored terminal text."
-category = "dev"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
-files = [
- {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
- {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
-]
-
-[[package]]
-name = "cryptography"
-version = "38.0.1"
-description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"},
- {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"},
- {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"},
- {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"},
- {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"},
- {file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"},
- {file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"},
- {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"},
- {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"},
- {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"},
- {file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"},
- {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"},
- {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"},
-]
-
-[package.dependencies]
-cffi = ">=1.12"
-
-[package.extras]
-docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
-docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
-pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
-sdist = ["setuptools-rust (>=0.11.4)"]
-ssh = ["bcrypt (>=3.1.5)"]
-test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"]
-
-[[package]]
-name = "Deprecated"
-version = "1.2.13"
-description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-files = [
- {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"},
- {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"},
-]
-
-[package.dependencies]
-wrapt = ">=1.10,<2"
-
-[package.extras]
-dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"]
-
-[[package]]
-name = "exceptiongroup"
-version = "1.0.0"
-description = "Backport of PEP 654 (exception groups)"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"},
- {file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"},
-]
-
-[package.extras]
-test = ["pytest (>=6)"]
-
-[[package]]
-name = "frozenlist"
-version = "1.3.1"
-description = "A list-like structure which implements collections.abc.MutableSequence"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f271c93f001748fc26ddea409241312a75e13466b06c94798d1a341cf0e6989"},
- {file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c6ef8014b842f01f5d2b55315f1af5cbfde284eb184075c189fd657c2fd8204"},
- {file = "frozenlist-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:219a9676e2eae91cb5cc695a78b4cb43d8123e4160441d2b6ce8d2c70c60e2f3"},
- {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b47d64cdd973aede3dd71a9364742c542587db214e63b7529fbb487ed67cddd9"},
- {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2af6f7a4e93f5d08ee3f9152bce41a6015b5cf87546cb63872cc19b45476e98a"},
- {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a718b427ff781c4f4e975525edb092ee2cdef6a9e7bc49e15063b088961806f8"},
- {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c56c299602c70bc1bb5d1e75f7d8c007ca40c9d7aebaf6e4ba52925d88ef826d"},
- {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717470bfafbb9d9be624da7780c4296aa7935294bd43a075139c3d55659038ca"},
- {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:31b44f1feb3630146cffe56344704b730c33e042ffc78d21f2125a6a91168131"},
- {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c3b31180b82c519b8926e629bf9f19952c743e089c41380ddca5db556817b221"},
- {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d82bed73544e91fb081ab93e3725e45dd8515c675c0e9926b4e1f420a93a6ab9"},
- {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49459f193324fbd6413e8e03bd65789e5198a9fa3095e03f3620dee2f2dabff2"},
- {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:94e680aeedc7fd3b892b6fa8395b7b7cc4b344046c065ed4e7a1e390084e8cb5"},
- {file = "frozenlist-1.3.1-cp310-cp310-win32.whl", hash = "sha256:fabb953ab913dadc1ff9dcc3a7a7d3dc6a92efab3a0373989b8063347f8705be"},
- {file = "frozenlist-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:eee0c5ecb58296580fc495ac99b003f64f82a74f9576a244d04978a7e97166db"},
- {file = "frozenlist-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bc75692fb3770cf2b5856a6c2c9de967ca744863c5e89595df64e252e4b3944"},
- {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086ca1ac0a40e722d6833d4ce74f5bf1aba2c77cbfdc0cd83722ffea6da52a04"},
- {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b51eb355e7f813bcda00276b0114c4172872dc5fb30e3fea059b9367c18fbcb"},
- {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74140933d45271c1a1283f708c35187f94e1256079b3c43f0c2267f9db5845ff"},
- {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee4c5120ddf7d4dd1eaf079af3af7102b56d919fa13ad55600a4e0ebe532779b"},
- {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d9e00f3ac7c18e685320601f91468ec06c58acc185d18bb8e511f196c8d4b2"},
- {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e19add867cebfb249b4e7beac382d33215d6d54476bb6be46b01f8cafb4878b"},
- {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a027f8f723d07c3f21963caa7d585dcc9b089335565dabe9c814b5f70c52705a"},
- {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:61d7857950a3139bce035ad0b0945f839532987dfb4c06cfe160254f4d19df03"},
- {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:53b2b45052e7149ee8b96067793db8ecc1ae1111f2f96fe1f88ea5ad5fd92d10"},
- {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bbb1a71b1784e68870800b1bc9f3313918edc63dbb8f29fbd2e767ce5821696c"},
- {file = "frozenlist-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:ab6fa8c7871877810e1b4e9392c187a60611fbf0226a9e0b11b7b92f5ac72792"},
- {file = "frozenlist-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89139662cc4e65a4813f4babb9ca9544e42bddb823d2ec434e18dad582543bc"},
- {file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4c0c99e31491a1d92cde8648f2e7ccad0e9abb181f6ac3ddb9fc48b63301808e"},
- {file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61e8cb51fba9f1f33887e22488bad1e28dd8325b72425f04517a4d285a04c519"},
- {file = "frozenlist-1.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc2f3e368ee5242a2cbe28323a866656006382872c40869b49b265add546703f"},
- {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58fb94a01414cddcdc6839807db77ae8057d02ddafc94a42faee6004e46c9ba8"},
- {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e"},
- {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:572ce381e9fe027ad5e055f143763637dcbac2542cfe27f1d688846baeef5170"},
- {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19127f8dcbc157ccb14c30e6f00392f372ddb64a6ffa7106b26ff2196477ee9f"},
- {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42719a8bd3792744c9b523674b752091a7962d0d2d117f0b417a3eba97d1164b"},
- {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2743bb63095ef306041c8f8ea22bd6e4d91adabf41887b1ad7886c4c1eb43d5f"},
- {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fa47319a10e0a076709644a0efbcaab9e91902c8bd8ef74c6adb19d320f69b83"},
- {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52137f0aea43e1993264a5180c467a08a3e372ca9d378244c2d86133f948b26b"},
- {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:f5abc8b4d0c5b556ed8cd41490b606fe99293175a82b98e652c3f2711b452988"},
- {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1e1cf7bc8cbbe6ce3881863671bac258b7d6bfc3706c600008925fb799a256e2"},
- {file = "frozenlist-1.3.1-cp38-cp38-win32.whl", hash = "sha256:0dde791b9b97f189874d654c55c24bf7b6782343e14909c84beebd28b7217845"},
- {file = "frozenlist-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:9494122bf39da6422b0972c4579e248867b6b1b50c9b05df7e04a3f30b9a413d"},
- {file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31bf9539284f39ff9398deabf5561c2b0da5bb475590b4e13dd8b268d7a3c5c1"},
- {file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0c8c803f2f8db7217898d11657cb6042b9b0553a997c4a0601f48a691480fab"},
- {file = "frozenlist-1.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da5ba7b59d954f1f214d352308d1d86994d713b13edd4b24a556bcc43d2ddbc3"},
- {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e6b2b456f21fc93ce1aff2b9728049f1464428ee2c9752a4b4f61e98c4db96"},
- {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526d5f20e954d103b1d47232e3839f3453c02077b74203e43407b962ab131e7b"},
- {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b499c6abe62a7a8d023e2c4b2834fce78a6115856ae95522f2f974139814538c"},
- {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab386503f53bbbc64d1ad4b6865bf001414930841a870fc97f1546d4d133f141"},
- {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f63c308f82a7954bf8263a6e6de0adc67c48a8b484fab18ff87f349af356efd"},
- {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:12607804084d2244a7bd4685c9d0dca5df17a6a926d4f1967aa7978b1028f89f"},
- {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:da1cdfa96425cbe51f8afa43e392366ed0b36ce398f08b60de6b97e3ed4affef"},
- {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f810e764617b0748b49a731ffaa525d9bb36ff38332411704c2400125af859a6"},
- {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:35c3d79b81908579beb1fb4e7fcd802b7b4921f1b66055af2578ff7734711cfa"},
- {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c92deb5d9acce226a501b77307b3b60b264ca21862bd7d3e0c1f3594022f01bc"},
- {file = "frozenlist-1.3.1-cp39-cp39-win32.whl", hash = "sha256:5e77a8bd41e54b05e4fb2708dc6ce28ee70325f8c6f50f3df86a44ecb1d7a19b"},
- {file = "frozenlist-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:625d8472c67f2d96f9a4302a947f92a7adbc1e20bedb6aff8dbc8ff039ca6189"},
- {file = "frozenlist-1.3.1.tar.gz", hash = "sha256:3a735e4211a04ccfa3f4833547acdf5d2f863bfeb01cfd3edaffbc251f15cec8"},
-]
-
-[[package]]
-name = "idna"
-version = "3.4"
-description = "Internationalized Domain Names in Applications (IDNA)"
-category = "dev"
-optional = false
-python-versions = ">=3.5"
-files = [
- {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
- {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
-]
-
-[[package]]
-name = "importlib-metadata"
-version = "5.0.0"
-description = "Read metadata from Python packages"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"},
- {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"},
-]
-
-[package.dependencies]
-typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
-zipp = ">=0.5"
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
-perf = ["ipython"]
-testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
-
-[[package]]
-name = "iniconfig"
-version = "1.1.1"
-description = "iniconfig: brain-dead simple config-ini parsing"
-category = "dev"
-optional = false
-python-versions = "*"
-files = [
- {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
- {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
-]
-
-[[package]]
-name = "isort"
-version = "5.10.1"
-description = "A Python utility / library to sort Python imports."
-category = "dev"
-optional = false
-python-versions = ">=3.6.1,<4.0"
-files = [
- {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
- {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
-]
-
-[package.extras]
-colors = ["colorama (>=0.4.3,<0.5.0)"]
-pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
-plugins = ["setuptools"]
-requirements-deprecated-finder = ["pip-api", "pipreqs"]
-
-[[package]]
-name = "multidict"
-version = "6.0.2"
-description = "multidict implementation"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"},
- {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"},
- {file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"},
- {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"},
- {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"},
- {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"},
- {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"},
- {file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"},
- {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"},
- {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"},
- {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"},
- {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"},
- {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"},
- {file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"},
- {file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"},
- {file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"},
- {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"},
- {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"},
- {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"},
- {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"},
- {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"},
- {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"},
- {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"},
- {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"},
- {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"},
- {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"},
- {file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"},
- {file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"},
- {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"},
- {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"},
- {file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"},
- {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"},
- {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"},
- {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"},
- {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"},
- {file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"},
- {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"},
- {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"},
- {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"},
- {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"},
- {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"},
- {file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"},
- {file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"},
- {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"},
- {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"},
- {file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"},
- {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"},
- {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"},
- {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"},
- {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"},
- {file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"},
- {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"},
- {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"},
- {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"},
- {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"},
- {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"},
- {file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"},
- {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"},
- {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"},
-]
-
-[[package]]
-name = "mypy"
-version = "0.961"
-description = "Optional static typing for Python"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "mypy-0.961-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:697540876638ce349b01b6786bc6094ccdaba88af446a9abb967293ce6eaa2b0"},
- {file = "mypy-0.961-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b117650592e1782819829605a193360a08aa99f1fc23d1d71e1a75a142dc7e15"},
- {file = "mypy-0.961-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bdd5ca340beffb8c44cb9dc26697628d1b88c6bddf5c2f6eb308c46f269bb6f3"},
- {file = "mypy-0.961-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3e09f1f983a71d0672bbc97ae33ee3709d10c779beb613febc36805a6e28bb4e"},
- {file = "mypy-0.961-cp310-cp310-win_amd64.whl", hash = "sha256:e999229b9f3198c0c880d5e269f9f8129c8862451ce53a011326cad38b9ccd24"},
- {file = "mypy-0.961-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b24be97351084b11582fef18d79004b3e4db572219deee0212078f7cf6352723"},
- {file = "mypy-0.961-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4a21d01fc0ba4e31d82f0fff195682e29f9401a8bdb7173891070eb260aeb3b"},
- {file = "mypy-0.961-cp36-cp36m-win_amd64.whl", hash = "sha256:439c726a3b3da7ca84a0199a8ab444cd8896d95012c4a6c4a0d808e3147abf5d"},
- {file = "mypy-0.961-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5a0b53747f713f490affdceef835d8f0cb7285187a6a44c33821b6d1f46ed813"},
- {file = "mypy-0.961-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e9f70df36405c25cc530a86eeda1e0867863d9471fe76d1273c783df3d35c2e"},
- {file = "mypy-0.961-cp37-cp37m-win_amd64.whl", hash = "sha256:b88f784e9e35dcaa075519096dc947a388319cb86811b6af621e3523980f1c8a"},
- {file = "mypy-0.961-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d5aaf1edaa7692490f72bdb9fbd941fbf2e201713523bdb3f4038be0af8846c6"},
- {file = "mypy-0.961-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9f5f5a74085d9a81a1f9c78081d60a0040c3efb3f28e5c9912b900adf59a16e6"},
- {file = "mypy-0.961-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f4b794db44168a4fc886e3450201365c9526a522c46ba089b55e1f11c163750d"},
- {file = "mypy-0.961-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:64759a273d590040a592e0f4186539858c948302c653c2eac840c7a3cd29e51b"},
- {file = "mypy-0.961-cp38-cp38-win_amd64.whl", hash = "sha256:63e85a03770ebf403291ec50097954cc5caf2a9205c888ce3a61bd3f82e17569"},
- {file = "mypy-0.961-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f1332964963d4832a94bebc10f13d3279be3ce8f6c64da563d6ee6e2eeda932"},
- {file = "mypy-0.961-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:006be38474216b833eca29ff6b73e143386f352e10e9c2fbe76aa8549e5554f5"},
- {file = "mypy-0.961-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9940e6916ed9371809b35b2154baf1f684acba935cd09928952310fbddaba648"},
- {file = "mypy-0.961-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a5ea0875a049de1b63b972456542f04643daf320d27dc592d7c3d9cd5d9bf950"},
- {file = "mypy-0.961-cp39-cp39-win_amd64.whl", hash = "sha256:1ece702f29270ec6af25db8cf6185c04c02311c6bb21a69f423d40e527b75c56"},
- {file = "mypy-0.961-py3-none-any.whl", hash = "sha256:03c6cc893e7563e7b2949b969e63f02c000b32502a1b4d1314cabe391aa87d66"},
- {file = "mypy-0.961.tar.gz", hash = "sha256:f730d56cb924d371c26b8eaddeea3cc07d78ff51c521c6d04899ac6904b75492"},
-]
-
-[package.dependencies]
-mypy-extensions = ">=0.4.3"
-tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
-typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""}
-typing-extensions = ">=3.10"
-
-[package.extras]
-dmypy = ["psutil (>=4.0)"]
-python2 = ["typed-ast (>=1.4.0,<2)"]
-reports = ["lxml"]
-
-[[package]]
-name = "mypy-extensions"
-version = "0.4.3"
-description = "Experimental type system extensions for programs checked with the mypy typechecker."
-category = "dev"
-optional = false
-python-versions = "*"
-files = [
- {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
- {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
-]
-
-[[package]]
-name = "opentelemetry-api"
-version = "1.13.0"
-description = "OpenTelemetry Python API"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "opentelemetry_api-1.13.0-py3-none-any.whl", hash = "sha256:2db1e8713f48a119bae457cd22304a7919d5e57190a380485c442c4f731a46dd"},
- {file = "opentelemetry_api-1.13.0.tar.gz", hash = "sha256:e683e869471b99e77238c8739d6ee2f368803329f3b808dfa86a02d0b519c682"},
-]
-
-[package.dependencies]
-deprecated = ">=1.2.6"
-setuptools = ">=16.0"
-
-[[package]]
-name = "opentelemetry-exporter-jaeger-thrift"
-version = "1.13.0"
-description = "Jaeger Thrift Exporter for OpenTelemetry"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "opentelemetry_exporter_jaeger_thrift-1.13.0-py3-none-any.whl", hash = "sha256:795f396eaea008359ff195153745947816b3a278b5b8958eb36fe40dc455d920"},
- {file = "opentelemetry_exporter_jaeger_thrift-1.13.0.tar.gz", hash = "sha256:f10865c5efcdf1b53906604c56797f92261f8825fd8a9ddc913167ff053e99cb"},
-]
-
-[package.dependencies]
-opentelemetry-api = ">=1.3,<2.0"
-opentelemetry-sdk = ">=1.11,<2.0"
-thrift = ">=0.10.0"
-
-[[package]]
-name = "opentelemetry-sdk"
-version = "1.13.0"
-description = "OpenTelemetry Python SDK"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "opentelemetry_sdk-1.13.0-py3-none-any.whl", hash = "sha256:c7b88e06ebedd22c226b374c207792d30b3f34074a6b8ad8c6dad04a8d16326b"},
- {file = "opentelemetry_sdk-1.13.0.tar.gz", hash = "sha256:0eddcacd5a484fe2918116b9a4e31867e3d10322ff8392b1c7b0dae1ac724d48"},
-]
-
-[package.dependencies]
-opentelemetry-api = "1.13.0"
-opentelemetry-semantic-conventions = "0.34b0"
-setuptools = ">=16.0"
-typing-extensions = ">=3.7.4"
-
-[[package]]
-name = "opentelemetry-semantic-conventions"
-version = "0.34b0"
-description = "OpenTelemetry Semantic Conventions"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "opentelemetry_semantic_conventions-0.34b0-py3-none-any.whl", hash = "sha256:b236bd027d2d470c5f7f7a466676182c7e02f486db8296caca25fae0649c3fa3"},
- {file = "opentelemetry_semantic_conventions-0.34b0.tar.gz", hash = "sha256:0c88a5d1f45b820272e0c421fd52ff2188b74582b1bab7ba0f57891dc2f31edf"},
-]
-
-[[package]]
-name = "packaging"
-version = "21.3"
-description = "Core utilities for Python packages"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
- {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
-]
-
-[package.dependencies]
-pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
-
-[[package]]
-name = "pathspec"
-version = "0.10.1"
-description = "Utility library for gitignore style pattern matching of file paths."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
- {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
-]
-
-[[package]]
-name = "platformdirs"
-version = "2.5.2"
-description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
- {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
-]
-
-[package.extras]
-docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
-test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
-
-[[package]]
-name = "pluggy"
-version = "1.0.0"
-description = "plugin and hook calling mechanisms for python"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
- {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
-]
-
-[package.dependencies]
-importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
-
-[package.extras]
-dev = ["pre-commit", "tox"]
-testing = ["pytest", "pytest-benchmark"]
-
-[[package]]
-name = "protobuf"
-version = "4.21.9"
-description = ""
-category = "main"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "protobuf-4.21.9-cp310-abi3-win32.whl", hash = "sha256:6e0be9f09bf9b6cf497b27425487706fa48c6d1632ddd94dab1a5fe11a422392"},
- {file = "protobuf-4.21.9-cp310-abi3-win_amd64.whl", hash = "sha256:a7d0ea43949d45b836234f4ebb5ba0b22e7432d065394b532cdca8f98415e3cf"},
- {file = "protobuf-4.21.9-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5ab0b8918c136345ff045d4b3d5f719b505b7c8af45092d7f45e304f55e50a1"},
- {file = "protobuf-4.21.9-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:2c9c2ed7466ad565f18668aa4731c535511c5d9a40c6da39524bccf43e441719"},
- {file = "protobuf-4.21.9-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:e575c57dc8b5b2b2caa436c16d44ef6981f2235eb7179bfc847557886376d740"},
- {file = "protobuf-4.21.9-cp37-cp37m-win32.whl", hash = "sha256:9227c14010acd9ae7702d6467b4625b6fe853175a6b150e539b21d2b2f2b409c"},
- {file = "protobuf-4.21.9-cp37-cp37m-win_amd64.whl", hash = "sha256:a419cc95fca8694804709b8c4f2326266d29659b126a93befe210f5bbc772536"},
- {file = "protobuf-4.21.9-cp38-cp38-win32.whl", hash = "sha256:5b0834e61fb38f34ba8840d7dcb2e5a2f03de0c714e0293b3963b79db26de8ce"},
- {file = "protobuf-4.21.9-cp38-cp38-win_amd64.whl", hash = "sha256:84ea107016244dfc1eecae7684f7ce13c788b9a644cd3fca5b77871366556444"},
- {file = "protobuf-4.21.9-cp39-cp39-win32.whl", hash = "sha256:f9eae277dd240ae19bb06ff4e2346e771252b0e619421965504bd1b1bba7c5fa"},
- {file = "protobuf-4.21.9-cp39-cp39-win_amd64.whl", hash = "sha256:6e312e280fbe3c74ea9e080d9e6080b636798b5e3939242298b591064470b06b"},
- {file = "protobuf-4.21.9-py2.py3-none-any.whl", hash = "sha256:7eb8f2cc41a34e9c956c256e3ac766cf4e1a4c9c925dc757a41a01be3e852965"},
- {file = "protobuf-4.21.9-py3-none-any.whl", hash = "sha256:48e2cd6b88c6ed3d5877a3ea40df79d08374088e89bedc32557348848dff250b"},
- {file = "protobuf-4.21.9.tar.gz", hash = "sha256:61f21493d96d2a77f9ca84fefa105872550ab5ef71d21c458eb80edcf4885a99"},
-]
-
-[[package]]
-name = "pycparser"
-version = "2.21"
-description = "C parser in Python"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-files = [
- {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
- {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
-]
-
-[[package]]
-name = "pydantic"
-version = "1.10.4"
-description = "Data validation and settings management using python type hints"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "pydantic-1.10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854"},
- {file = "pydantic-1.10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817"},
- {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c"},
- {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e"},
- {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85"},
- {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6"},
- {file = "pydantic-1.10.4-cp310-cp310-win_amd64.whl", hash = "sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d"},
- {file = "pydantic-1.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53"},
- {file = "pydantic-1.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3"},
- {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a"},
- {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d"},
- {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72"},
- {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857"},
- {file = "pydantic-1.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3"},
- {file = "pydantic-1.10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00"},
- {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978"},
- {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e"},
- {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa"},
- {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8"},
- {file = "pydantic-1.10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903"},
- {file = "pydantic-1.10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423"},
- {file = "pydantic-1.10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f"},
- {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06"},
- {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15"},
- {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c"},
- {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416"},
- {file = "pydantic-1.10.4-cp38-cp38-win_amd64.whl", hash = "sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6"},
- {file = "pydantic-1.10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d"},
- {file = "pydantic-1.10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f"},
- {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024"},
- {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c"},
- {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28"},
- {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f"},
- {file = "pydantic-1.10.4-cp39-cp39-win_amd64.whl", hash = "sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4"},
- {file = "pydantic-1.10.4-py3-none-any.whl", hash = "sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774"},
- {file = "pydantic-1.10.4.tar.gz", hash = "sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648"},
-]
-
-[package.dependencies]
-typing-extensions = ">=4.2.0"
-
-[package.extras]
-dotenv = ["python-dotenv (>=0.10.4)"]
-email = ["email-validator (>=1.0.3)"]
-
-[[package]]
-name = "pyparsing"
-version = "3.0.9"
-description = "pyparsing module - Classes and methods to define and execute parsing grammars"
-category = "dev"
-optional = false
-python-versions = ">=3.6.8"
-files = [
- {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
- {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
-]
-
-[package.extras]
-diagrams = ["jinja2", "railroad-diagrams"]
-
-[[package]]
-name = "pytest"
-version = "7.2.0"
-description = "pytest: simple powerful testing with Python"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
- {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
-]
-
-[package.dependencies]
-attrs = ">=19.2.0"
-colorama = {version = "*", markers = "sys_platform == \"win32\""}
-exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
-importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
-iniconfig = "*"
-packaging = "*"
-pluggy = ">=0.12,<2.0"
-tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
-
-[package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
-
-[[package]]
-name = "pytest-asyncio"
-version = "0.18.3"
-description = "Pytest support for asyncio"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"},
- {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"},
- {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"},
-]
-
-[package.dependencies]
-pytest = ">=6.1.0"
-typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""}
-
-[package.extras]
-testing = ["coverage (==6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"]
-
-[[package]]
-name = "python-dateutil"
-version = "2.8.2"
-description = "Extensions to the standard Python datetime module"
-category = "main"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
-files = [
- {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
- {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
-]
-
-[package.dependencies]
-six = ">=1.5"
-
-[[package]]
-name = "sentry-sdk"
-version = "1.11.1"
-description = "Python client for Sentry (https://sentry.io)"
-category = "dev"
-optional = false
-python-versions = "*"
-files = [
- {file = "sentry-sdk-1.11.1.tar.gz", hash = "sha256:675f6279b6bb1fea09fd61751061f9a90dca3b5929ef631dd50dc8b3aeb245e9"},
- {file = "sentry_sdk-1.11.1-py2.py3-none-any.whl", hash = "sha256:8b4ff696c0bdcceb3f70bbb87a57ba84fd3168b1332d493fcd16c137f709578c"},
-]
-
-[package.dependencies]
-certifi = "*"
-urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""}
-
-[package.extras]
-aiohttp = ["aiohttp (>=3.5)"]
-beam = ["apache-beam (>=2.12)"]
-bottle = ["bottle (>=0.12.13)"]
-celery = ["celery (>=3)"]
-chalice = ["chalice (>=1.16.0)"]
-django = ["django (>=1.8)"]
-falcon = ["falcon (>=1.4)"]
-fastapi = ["fastapi (>=0.79.0)"]
-flask = ["blinker (>=1.1)", "flask (>=0.11)"]
-httpx = ["httpx (>=0.16.0)"]
-pure-eval = ["asttokens", "executing", "pure-eval"]
-pymongo = ["pymongo (>=3.1)"]
-pyspark = ["pyspark (>=2.4.4)"]
-quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
-rq = ["rq (>=0.6)"]
-sanic = ["sanic (>=0.8)"]
-sqlalchemy = ["sqlalchemy (>=1.2)"]
-starlette = ["starlette (>=0.19.1)"]
-tornado = ["tornado (>=5)"]
-
-[[package]]
-name = "setuptools"
-version = "65.5.0"
-description = "Easily download, build, install, upgrade, and uninstall Python packages"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"},
- {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"},
-]
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
-testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
-
-[[package]]
-name = "six"
-version = "1.16.0"
-description = "Python 2 and 3 compatibility utilities"
-category = "main"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
-files = [
- {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
- {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
-]
-
-[[package]]
-name = "temporalio"
-version = "1.1.0"
-description = "Temporal.io Python SDK"
-category = "main"
-optional = false
-python-versions = ">=3.7,<4.0"
-files = [
- {file = "temporalio-1.1.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:1729336d974ee8a4f9fa3802024f3cc5e67c9ce9f75b4e833729e70d28aef78e"},
- {file = "temporalio-1.1.0-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864bd436b1f2432a36ace5667e68020a574f2d8e80799e8d7c35b84c2f40359f"},
- {file = "temporalio-1.1.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b4b2dd9df66c611d9b062e1d55eb003832f1dd3c5ae218470ac1ed0a26c737bd"},
- {file = "temporalio-1.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:7ef2cfe05b4d2f0adde700cf532629be252276ccd5f84d6ca990c75ed86a0ef4"},
- {file = "temporalio-1.1.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:001ff14d5e057b2562d78851d1064f90f5c4a8d4424c4f8caac63792761cdf4f"},
- {file = "temporalio-1.1.0.tar.gz", hash = "sha256:ee50d6414195d17366a071c190dbdc027ed30d12322a1b0ad70be99a47820542"},
-]
-
-[package.dependencies]
-opentelemetry-api = {version = ">=1.11.1,<2.0.0", optional = true, markers = "extra == \"opentelemetry\""}
-opentelemetry-sdk = {version = ">=1.11.1,<2.0.0", optional = true, markers = "extra == \"opentelemetry\""}
-protobuf = ">=3.20"
-python-dateutil = {version = ">=2.8.2,<3.0.0", markers = "python_version < \"3.11\""}
-types-protobuf = ">=3.20"
-typing-extensions = ">=4.2.0,<5.0.0"
-
-[package.extras]
-grpc = ["grpcio (>=1.48.0,<2.0.0)"]
-opentelemetry = ["opentelemetry-api (>=1.11.1,<2.0.0)", "opentelemetry-sdk (>=1.11.1,<2.0.0)"]
-
-[[package]]
-name = "thrift"
-version = "0.16.0"
-description = "Python bindings for the Apache Thrift RPC system"
-category = "dev"
-optional = false
-python-versions = "*"
-files = [
- {file = "thrift-0.16.0.tar.gz", hash = "sha256:2b5b6488fcded21f9d312aa23c9ff6a0195d0f6ae26ddbd5ad9e3e25dfc14408"},
-]
-
-[package.dependencies]
-six = ">=1.7.2"
-
-[package.extras]
-all = ["tornado (>=4.0)", "twisted"]
-tornado = ["tornado (>=4.0)"]
-twisted = ["twisted"]
-
-[[package]]
-name = "tomli"
-version = "2.0.1"
-description = "A lil' TOML parser"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
- {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
-]
-
-[[package]]
-name = "typed-ast"
-version = "1.5.4"
-description = "a fork of Python 2 and 3 ast modules with type comment support"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
- {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
- {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
- {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"},
- {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"},
- {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"},
- {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"},
- {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"},
- {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"},
- {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"},
- {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"},
- {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"},
- {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"},
- {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"},
- {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"},
- {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"},
- {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"},
- {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"},
- {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"},
- {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"},
- {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"},
- {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"},
- {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
- {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
-]
-
-[[package]]
-name = "types-protobuf"
-version = "3.20.4.2"
-description = "Typing stubs for protobuf"
-category = "main"
-optional = false
-python-versions = "*"
-files = [
- {file = "types-protobuf-3.20.4.2.tar.gz", hash = "sha256:fd65ab8502f9a08e089f2cad26d52fab04cd57322cf896c570ab3391ca760bae"},
- {file = "types_protobuf-3.20.4.2-py3-none-any.whl", hash = "sha256:329bec368fd11a8cb95192b33aa61e59e0b30f659ec14be75301c651026c4cc1"},
-]
-
-[[package]]
-name = "typing-extensions"
-version = "4.4.0"
-description = "Backported and Experimental Type Hints for Python 3.7+"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
- {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
-]
-
-[[package]]
-name = "urllib3"
-version = "1.26.13"
-description = "HTTP library with thread-safe connection pooling, file post, and more."
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
-files = [
- {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"},
- {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"},
-]
-
-[package.extras]
-brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
-secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
-socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
-
-[[package]]
-name = "wrapt"
-version = "1.14.1"
-description = "Module for decorators, wrappers and monkey patching."
-category = "dev"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
-files = [
- {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"},
- {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"},
- {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"},
- {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"},
- {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"},
- {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"},
- {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"},
- {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"},
- {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"},
- {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"},
- {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"},
- {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"},
- {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"},
- {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"},
- {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"},
- {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"},
- {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"},
- {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"},
- {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"},
- {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"},
- {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"},
- {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"},
- {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"},
- {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"},
- {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"},
- {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"},
- {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"},
- {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"},
- {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"},
- {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"},
- {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"},
- {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"},
- {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"},
- {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"},
- {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"},
- {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"},
- {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"},
- {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"},
- {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"},
- {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"},
- {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"},
- {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"},
- {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"},
- {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"},
- {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"},
- {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"},
- {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"},
- {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"},
- {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"},
- {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"},
- {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"},
- {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"},
- {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"},
- {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"},
- {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"},
- {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"},
- {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"},
- {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"},
- {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"},
- {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"},
- {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"},
- {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"},
- {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"},
- {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"},
-]
-
-[[package]]
-name = "yarl"
-version = "1.8.1"
-description = "Yet another URL library"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"},
- {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3"},
- {file = "yarl-1.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880"},
- {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40"},
- {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b"},
- {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497"},
- {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a"},
- {file = "yarl-1.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f"},
- {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd"},
- {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957"},
- {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28"},
- {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843"},
- {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453"},
- {file = "yarl-1.8.1-cp310-cp310-win32.whl", hash = "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272"},
- {file = "yarl-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0"},
- {file = "yarl-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035"},
- {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc"},
- {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b"},
- {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231"},
- {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda"},
- {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507"},
- {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee"},
- {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0"},
- {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d"},
- {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd"},
- {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780"},
- {file = "yarl-1.8.1-cp37-cp37m-win32.whl", hash = "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07"},
- {file = "yarl-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802"},
- {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a"},
- {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1"},
- {file = "yarl-1.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548"},
- {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461"},
- {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe"},
- {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae"},
- {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc"},
- {file = "yarl-1.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae"},
- {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546"},
- {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead"},
- {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4"},
- {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e"},
- {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae"},
- {file = "yarl-1.8.1-cp38-cp38-win32.whl", hash = "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0"},
- {file = "yarl-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4"},
- {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936"},
- {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350"},
- {file = "yarl-1.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357"},
- {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b"},
- {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54"},
- {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb"},
- {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c"},
- {file = "yarl-1.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6"},
- {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64"},
- {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae"},
- {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3"},
- {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0"},
- {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e"},
- {file = "yarl-1.8.1-cp39-cp39-win32.whl", hash = "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6"},
- {file = "yarl-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be"},
- {file = "yarl-1.8.1.tar.gz", hash = "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf"},
-]
-
-[package.dependencies]
-idna = ">=2.0"
-multidict = ">=4.0"
-typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
-
-[[package]]
-name = "zipp"
-version = "3.10.0"
-description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"},
- {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"},
-]
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
-testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
-
-[metadata]
-lock-version = "2.0"
-python-versions = "^3.7"
-content-hash = "f79522ae4299315690b84509b7538289c2ad4edf1775818833abb6722f55ff68"
diff --git a/polling/frequent/README.md b/polling/frequent/README.md
index 33b6bc0a..9968e18c 100644
--- a/polling/frequent/README.md
+++ b/polling/frequent/README.md
@@ -6,12 +6,13 @@ To ensure that polling Activity is restarted in a timely manner, we make sure th
To run, first see [README.md](../../README.md) for prerequisites.
-Then, run the following from this directory to run the sample:
+Then, run the following from the root directory to run the sample:
-```bash
-poetry run python run_worker.py
-poetry run python run_frequent.py
-```
+ uv run polling/frequent/run_worker.py
+
+Then, in another terminal, run the following to execute the workflow:
+
+ uv run polling/frequent/run_frequent.py
The Workflow will continue to poll the service and heartbeat on every iteration until it succeeds.
diff --git a/polling/frequent/activities.py b/polling/frequent/activities.py
index bc667b7f..a112b417 100644
--- a/polling/frequent/activities.py
+++ b/polling/frequent/activities.py
@@ -1,23 +1,26 @@
import asyncio
-from dataclasses import dataclass
from temporalio import activity
-from polling.test_service import TestService
-
-
-@dataclass
-class ComposeGreetingInput:
- greeting: str
- name: str
+from polling.test_service import ComposeGreetingInput, get_service_result
@activity.defn
async def compose_greeting(input: ComposeGreetingInput) -> str:
- test_service = TestService()
while True:
try:
- result = test_service.get_service_result(input)
- return result
- except Exception:
+ try:
+ result = await get_service_result(input)
+ activity.logger.info(f"Exiting activity ${result}")
+ return result
+ except Exception:
+ # swallow exception since service is down
+ activity.logger.debug("Failed, trying again shortly", exc_info=True)
+
activity.heartbeat("Invoking activity")
+ await asyncio.sleep(1)
+ except asyncio.CancelledError:
+ # activity was either cancelled or workflow was completed or worker shut down
+ # if you need to clean up you can catch this.
+ # Here we are just reraising the exception
+ raise
diff --git a/polling/frequent/run_frequent.py b/polling/frequent/run_frequent.py
index bc43ccfd..42048092 100644
--- a/polling/frequent/run_frequent.py
+++ b/polling/frequent/run_frequent.py
@@ -1,11 +1,16 @@
import asyncio
from temporalio.client import Client
-from workflows import GreetingWorkflow
+from temporalio.envconfig import ClientConfig
+
+from polling.frequent.workflows import GreetingWorkflow
async def main():
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
result = await client.execute_workflow(
GreetingWorkflow.run,
"World",
diff --git a/polling/frequent/run_worker.py b/polling/frequent/run_worker.py
index 97b327ff..cf5ccb78 100644
--- a/polling/frequent/run_worker.py
+++ b/polling/frequent/run_worker.py
@@ -1,13 +1,17 @@
import asyncio
-from activities import compose_greeting
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
-from workflows import GreetingWorkflow
+
+from polling.frequent.activities import compose_greeting
+from polling.frequent.workflows import GreetingWorkflow
async def main():
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
worker = Worker(
client,
diff --git a/polling/frequent/workflows.py b/polling/frequent/workflows.py
index c5329ad9..0dac0a49 100644
--- a/polling/frequent/workflows.py
+++ b/polling/frequent/workflows.py
@@ -3,7 +3,7 @@
from temporalio import workflow
with workflow.unsafe.imports_passed_through():
- from activities import ComposeGreetingInput, compose_greeting
+ from polling.frequent.activities import ComposeGreetingInput, compose_greeting
@workflow.defn
diff --git a/polling/infrequent/README.md b/polling/infrequent/README.md
index 7c0a3225..7e86ea43 100644
--- a/polling/infrequent/README.md
+++ b/polling/infrequent/README.md
@@ -11,12 +11,14 @@ This will enable the Activity to be retried exactly on the set interval.
To run, first see [README.md](../../README.md) for prerequisites.
-Then, run the following from this directory to run the sample:
+Then, run the following from the root directory to run the sample:
+
+ uv run polling/infrequent/run_worker.py
+
+Then, in another terminal, run the following to execute the workflow:
+
+ uv run polling/infrequent/run_infrequent.py
-```bash
-poetry run python run_worker.py
-poetry run python run_infrequent.py
-```
Since the test service simulates being _down_ for four polling attempts and then returns _OK_ on the fifth poll attempt, the Workflow will perform four Activity retries with a 60-second poll interval, and then return the service result on the successful fifth attempt.
diff --git a/polling/infrequent/activities.py b/polling/infrequent/activities.py
index bc667b7f..b3db1aed 100644
--- a/polling/infrequent/activities.py
+++ b/polling/infrequent/activities.py
@@ -1,23 +1,10 @@
-import asyncio
-from dataclasses import dataclass
-
from temporalio import activity
-from polling.test_service import TestService
-
-
-@dataclass
-class ComposeGreetingInput:
- greeting: str
- name: str
+from polling.test_service import ComposeGreetingInput, get_service_result
@activity.defn
async def compose_greeting(input: ComposeGreetingInput) -> str:
- test_service = TestService()
- while True:
- try:
- result = test_service.get_service_result(input)
- return result
- except Exception:
- activity.heartbeat("Invoking activity")
+ # If this raises an exception because it's not done yet, the activity will
+ # continually be scheduled for retry
+ return await get_service_result(input)
diff --git a/polling/infrequent/run_infrequent.py b/polling/infrequent/run_infrequent.py
index 0056a36c..8a0ea871 100644
--- a/polling/infrequent/run_infrequent.py
+++ b/polling/infrequent/run_infrequent.py
@@ -1,11 +1,16 @@
import asyncio
from temporalio.client import Client
-from workflows import GreetingWorkflow
+from temporalio.envconfig import ClientConfig
+
+from polling.infrequent.workflows import GreetingWorkflow
async def main():
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
result = await client.execute_workflow(
GreetingWorkflow.run,
"World",
diff --git a/polling/infrequent/run_worker.py b/polling/infrequent/run_worker.py
index 27db61de..f52a8082 100644
--- a/polling/infrequent/run_worker.py
+++ b/polling/infrequent/run_worker.py
@@ -1,13 +1,17 @@
import asyncio
-from activities import compose_greeting
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
-from workflows import GreetingWorkflow
+
+from polling.infrequent.activities import compose_greeting
+from polling.infrequent.workflows import GreetingWorkflow
async def main():
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
worker = Worker(
client,
diff --git a/polling/infrequent/workflows.py b/polling/infrequent/workflows.py
index 013a3b91..35769573 100644
--- a/polling/infrequent/workflows.py
+++ b/polling/infrequent/workflows.py
@@ -4,7 +4,7 @@
from temporalio.common import RetryPolicy
with workflow.unsafe.imports_passed_through():
- from activities import ComposeGreetingInput, compose_greeting
+ from polling.infrequent.activities import ComposeGreetingInput, compose_greeting
@workflow.defn
diff --git a/polling/periodic_sequence/README.md b/polling/periodic_sequence/README.md
index 65fe0669..8c5cb950 100644
--- a/polling/periodic_sequence/README.md
+++ b/polling/periodic_sequence/README.md
@@ -6,12 +6,14 @@ This is a rare scenario where polling requires execution of a Sequence of Activi
To run, first see [README.md](../../README.md) for prerequisites.
-Then, run the following from this directory to run the sample:
+Then, run the following from the root directory to run the sample:
+
+ uv run polling/periodic_sequence/run_worker.py
+
+Then, in another terminal, run the following to execute the workflow:
+
+ uv run polling/periodic_sequence/run_periodic.py
-```bash
-poetry run python run_worker.py
-poetry run python run_periodic.py
-```
This will start a Workflow and Child Workflow to periodically poll an Activity.
The Parent Workflow is not aware about the Child Workflow calling Continue-As-New, and it gets notified when it completes (or fails).
\ No newline at end of file
diff --git a/polling/periodic_sequence/activities.py b/polling/periodic_sequence/activities.py
index 1a1196c6..87b69890 100644
--- a/polling/periodic_sequence/activities.py
+++ b/polling/periodic_sequence/activities.py
@@ -1,14 +1,8 @@
-from dataclasses import dataclass
+from typing import Any, NoReturn
from temporalio import activity
-@dataclass
-class ComposeGreetingInput:
- greeting: str
- name: str
-
-
@activity.defn
-async def compose_greeting(input: ComposeGreetingInput) -> str:
+async def compose_greeting(input: Any) -> NoReturn:
raise RuntimeError("Service is down")
diff --git a/polling/periodic_sequence/run_periodic.py b/polling/periodic_sequence/run_periodic.py
index 5853d467..393fb8be 100644
--- a/polling/periodic_sequence/run_periodic.py
+++ b/polling/periodic_sequence/run_periodic.py
@@ -1,11 +1,16 @@
import asyncio
from temporalio.client import Client
-from workflows import GreetingWorkflow
+from temporalio.envconfig import ClientConfig
+
+from polling.periodic_sequence.workflows import GreetingWorkflow
async def main():
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
result = await client.execute_workflow(
GreetingWorkflow.run,
"World",
diff --git a/polling/periodic_sequence/run_worker.py b/polling/periodic_sequence/run_worker.py
index 8c0591f9..9689ef2f 100644
--- a/polling/periodic_sequence/run_worker.py
+++ b/polling/periodic_sequence/run_worker.py
@@ -1,13 +1,17 @@
import asyncio
-from activities import compose_greeting
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
-from workflows import ChildWorkflow, GreetingWorkflow
+
+from polling.periodic_sequence.activities import compose_greeting
+from polling.periodic_sequence.workflows import ChildWorkflow, GreetingWorkflow
async def main():
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
worker = Worker(
client,
diff --git a/polling/periodic_sequence/workflows.py b/polling/periodic_sequence/workflows.py
index 12045bc8..3c6d7555 100644
--- a/polling/periodic_sequence/workflows.py
+++ b/polling/periodic_sequence/workflows.py
@@ -6,7 +6,8 @@
from temporalio.exceptions import ActivityError
with workflow.unsafe.imports_passed_through():
- from activities import ComposeGreetingInput, compose_greeting
+ from polling.periodic_sequence.activities import compose_greeting
+ from polling.test_service import ComposeGreetingInput
@workflow.defn
@@ -37,6 +38,5 @@ async def run(self, name: str) -> str:
except ActivityError:
workflow.logger.error("Activity failed, retrying in 1 seconds")
await asyncio.sleep(1)
- workflow.continue_as_new(name)
- raise Exception("Polling failed after all attempts")
+ workflow.continue_as_new(name)
diff --git a/polling/test_service.py b/polling/test_service.py
index 5bcec7cd..994b98ce 100644
--- a/polling/test_service.py
+++ b/polling/test_service.py
@@ -1,14 +1,30 @@
-class TestService:
- def __init__(self):
- self.try_attempts = 0
- self.error_attempts = 5
-
- def get_service_result(self, input):
- print(
- f"Attempt {self.try_attempts}"
- f" of {self.error_attempts} to invoke service"
- )
- self.try_attempts += 1
- if self.try_attempts % self.error_attempts == 0:
- return f"{input.greeting}, {input.name}!"
- raise Exception("service is down")
+from dataclasses import dataclass
+from typing import Counter
+
+from temporalio import activity
+from temporalio.exceptions import ApplicationError, ApplicationErrorCategory
+
+attempts = Counter[str | None]()
+ERROR_ATTEMPTS = 5
+
+
+@dataclass
+class ComposeGreetingInput:
+ greeting: str
+ name: str
+
+
+async def get_service_result(input):
+ workflow_id = activity.info().workflow_id
+ attempts[workflow_id] += 1
+
+ print(f"Attempt {attempts[workflow_id]} of {ERROR_ATTEMPTS} to invoke service")
+ if attempts[workflow_id] == ERROR_ATTEMPTS:
+ return f"{input.greeting}, {input.name}!"
+ raise ApplicationError(
+ message="service is down",
+ # Set the error as BENIGN to indicate it is an expected error.
+ # BENIGN errors have activity failure logs downgraded to DEBUG level
+ # and do not emit activity failure metrics.
+ category=ApplicationErrorCategory.BENIGN,
+ )
diff --git a/prometheus/README.md b/prometheus/README.md
index 6f605d62..c0b5eb8f 100644
--- a/prometheus/README.md
+++ b/prometheus/README.md
@@ -2,16 +2,16 @@
This sample shows how to have SDK Prometheus metrics made available via HTTP.
-To run, first see [README.md](../README.md) for prerequisites. Then, run the following from this directory to start the
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from the root directory to start the
worker:
- poetry run python worker.py
+ uv run prometheus/worker.py
This will start the worker and the metrics will be visible for this process at http://127.0.0.1:9000/metrics.
Then, in another terminal, run the following to execute a workflow:
- poetry run python starter.py
+ uv run prometheus/starter.py
After executing the workflow, the process will stay open so the metrics if this separate process can be accessed at
http://127.0.0.1:9001/metrics.
\ No newline at end of file
diff --git a/prometheus/starter.py b/prometheus/starter.py
index b94f5601..571aee07 100644
--- a/prometheus/starter.py
+++ b/prometheus/starter.py
@@ -1,6 +1,7 @@
import asyncio
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from prometheus.worker import GreetingWorkflow, init_runtime_with_prometheus
@@ -10,9 +11,12 @@
async def main():
runtime = init_runtime_with_prometheus(9001)
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
# Connect client
client = await Client.connect(
- "localhost:7233",
+ **config,
runtime=runtime,
)
diff --git a/prometheus/worker.py b/prometheus/worker.py
index 5e7d64ab..b41b75c5 100644
--- a/prometheus/worker.py
+++ b/prometheus/worker.py
@@ -3,6 +3,7 @@
from temporalio import activity, workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.runtime import PrometheusConfig, Runtime, TelemetryConfig
from temporalio.worker import Worker
@@ -38,9 +39,12 @@ def init_runtime_with_prometheus(port: int) -> Runtime:
async def main():
runtime = init_runtime_with_prometheus(9000)
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
# Connect client
client = await Client.connect(
- "localhost:7233",
+ **config,
runtime=runtime,
)
diff --git a/pydantic_converter/README.md b/pydantic_converter/README.md
index 5914fa3d..e215e735 100644
--- a/pydantic_converter/README.md
+++ b/pydantic_converter/README.md
@@ -1,31 +1,19 @@
# Pydantic Converter Sample
-This sample shows how to create a custom Pydantic converter to properly serialize Pydantic models.
+This sample shows how to use the Pydantic data converter.
-For this sample, the optional `pydantic` dependency group must be included. To include, run:
+For this sample, the optional `pydantic_converter` dependency group must be included. To include, run:
- poetry install --with pydantic
+ uv sync --group pydantic-converter
-To run, first see [README.md](../README.md) for prerequisites. Then, run the following from this directory to start the
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from the root directory to start the
worker:
- poetry run python worker.py
+ uv run pydantic_converter/worker.py
This will start the worker. Then, in another terminal, run the following to execute the workflow:
- poetry run python starter.py
+ uv run pydantic_converter/starter.py
In the worker terminal, the workflow and its activity will log that it received the Pydantic models. In the starter
terminal, the Pydantic models in the workflow result will be logged.
-
-### Notes
-
-This is the preferred way to use Pydantic models with Temporal Python SDK. The converter code is small and meant to
-embed into other projects.
-
-This sample also demonstrates use of `datetime` inside of Pydantic models. Due to a known issue with the Temporal
-sandbox, this class is seen by Pydantic as `date` instead of `datetime` upon deserialization. This is due to a
-[known Python issue](https://github.com/python/cpython/issues/89010) where, when we proxy the `datetime` class in the
-sandbox to prevent non-deterministic calls like `now()`, `issubclass` fails for the proxy type causing Pydantic to think
-it's a `date` instead. In `worker.py`, we have shown a workaround of disabling restrictions on `datetime` which solves
-this issue but no longer protects against workflow developers making non-deterministic calls in that module.
\ No newline at end of file
diff --git a/pydantic_converter/starter.py b/pydantic_converter/starter.py
index 5aceca6e..47766be6 100644
--- a/pydantic_converter/starter.py
+++ b/pydantic_converter/starter.py
@@ -4,16 +4,23 @@
from ipaddress import IPv4Address
from temporalio.client import Client
+from temporalio.contrib.pydantic import pydantic_data_converter
+from temporalio.envconfig import ClientConfig
-from pydantic_converter.converter import pydantic_data_converter
from pydantic_converter.worker import MyPydanticModel, MyWorkflow
async def main():
logging.basicConfig(level=logging.INFO)
+
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
# Connect client using the Pydantic converter
+
client = await Client.connect(
- "localhost:7233", data_converter=pydantic_data_converter
+ **config,
+ data_converter=pydantic_data_converter,
)
# Run workflow
@@ -29,7 +36,7 @@ async def main():
some_date=datetime(2001, 2, 3, 4, 5, 6),
),
],
- id=f"pydantic_converter-workflow-id",
+ id="pydantic_converter-workflow-id",
task_queue="pydantic_converter-task-queue",
)
logging.info("Got models from client: %s" % result)
diff --git a/pydantic_converter/worker.py b/pydantic_converter/worker.py
index b7e0bedf..8acc1125 100644
--- a/pydantic_converter/worker.py
+++ b/pydantic_converter/worker.py
@@ -1,5 +1,4 @@
import asyncio
-import dataclasses
import logging
from datetime import datetime, timedelta
from ipaddress import IPv4Address
@@ -7,18 +6,14 @@
from temporalio import activity, workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
-from temporalio.worker.workflow_sandbox import (
- SandboxedWorkflowRunner,
- SandboxRestrictions,
-)
-# We always want to pass through external modules to the sandbox that we know
-# are safe for workflow use
+# Always pass through external modules to the sandbox that you know are safe for
+# workflow use
with workflow.unsafe.imports_passed_through():
from pydantic import BaseModel
-
- from pydantic_converter.converter import pydantic_data_converter
+ from temporalio.contrib.pydantic import pydantic_data_converter
class MyPydanticModel(BaseModel):
@@ -42,37 +37,19 @@ async def run(self, models: List[MyPydanticModel]) -> List[MyPydanticModel]:
)
-# Due to known issues with Pydantic's use of issubclass and our inability to
-# override the check in sandbox, Pydantic will think datetime is actually date
-# in the sandbox. At the expense of protecting against datetime.now() use in
-# workflows, we're going to remove datetime module restrictions. See sdk-python
-# README's discussion of known sandbox issues for more details.
-def new_sandbox_runner() -> SandboxedWorkflowRunner:
- # TODO(cretz): Use with_child_unrestricted when https://github.com/temporalio/sdk-python/issues/254
- # is fixed and released
- invalid_module_member_children = dict(
- SandboxRestrictions.invalid_module_members_default.children
- )
- del invalid_module_member_children["datetime"]
- return SandboxedWorkflowRunner(
- restrictions=dataclasses.replace(
- SandboxRestrictions.default,
- invalid_module_members=dataclasses.replace(
- SandboxRestrictions.invalid_module_members_default,
- children=invalid_module_member_children,
- ),
- )
- )
-
-
interrupt_event = asyncio.Event()
async def main():
logging.basicConfig(level=logging.INFO)
+
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
# Connect client using the Pydantic converter
client = await Client.connect(
- "localhost:7233", data_converter=pydantic_data_converter
+ **config,
+ data_converter=pydantic_data_converter,
)
# Run a worker for the workflow
@@ -81,7 +58,6 @@ async def main():
task_queue="pydantic_converter-task-queue",
workflows=[MyWorkflow],
activities=[my_activity],
- workflow_runner=new_sandbox_runner(),
):
# Wait until interrupted
print("Worker started, ctrl+c to exit")
diff --git a/pydantic_converter_v1/README.md b/pydantic_converter_v1/README.md
new file mode 100644
index 00000000..526e6930
--- /dev/null
+++ b/pydantic_converter_v1/README.md
@@ -0,0 +1,31 @@
+# Pydantic v1 Converter Sample
+
+**This sample shows how to use Pydantic v1 with Temporal. This is not recommended: use Pydantic v2 if possible, and use the
+main [pydantic_converter](../pydantic_converter/README.md) sample.**
+
+To install, run:
+
+ uv sync --group pydantic-converter
+ uv run pip uninstall pydantic pydantic-core
+ uv run pip install pydantic==1.10
+
+To run, first see the root [README.md](../README.md) for prerequisites. Then, run the following from the root directory to start the
+worker:
+
+ uv run pydantic_converter_v1/worker.py
+
+This will start the worker. Then, in another terminal, run the following to execute the workflow:
+
+ uv run pydantic_converter_v1/starter.py
+
+In the worker terminal, the workflow and its activity will log that it received the Pydantic models. In the starter
+terminal, the Pydantic models in the workflow result will be logged.
+
+### Notes
+
+This sample also demonstrates use of `datetime` inside of Pydantic v1 models. Due to a known issue with the Temporal
+sandbox, this class is seen by Pydantic v1 as `date` instead of `datetime` upon deserialization. This is due to a
+[known Python issue](https://github.com/python/cpython/issues/89010) where, when we proxy the `datetime` class in the
+sandbox to prevent non-deterministic calls like `now()`, `issubclass` fails for the proxy type causing Pydantic v1 to think
+it's a `date` instead. In `worker.py`, we have shown a workaround of disabling restrictions on `datetime` which solves
+this issue but no longer protects against workflow developers making non-deterministic calls in that module.
\ No newline at end of file
diff --git a/pydantic_converter_v1/__init__.py b/pydantic_converter_v1/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/pydantic_converter/converter.py b/pydantic_converter_v1/converter.py
similarity index 89%
rename from pydantic_converter/converter.py
rename to pydantic_converter_v1/converter.py
index a3c1cee6..81997e81 100644
--- a/pydantic_converter/converter.py
+++ b/pydantic_converter_v1/converter.py
@@ -42,9 +42,11 @@ class PydanticPayloadConverter(CompositePayloadConverter):
def __init__(self) -> None:
super().__init__(
*(
- c
- if not isinstance(c, JSONPlainPayloadConverter)
- else PydanticJSONPayloadConverter()
+ (
+ c
+ if not isinstance(c, JSONPlainPayloadConverter)
+ else PydanticJSONPayloadConverter()
+ )
for c in DefaultPayloadConverter.default_encoding_payload_converters
)
)
diff --git a/pydantic_converter_v1/starter.py b/pydantic_converter_v1/starter.py
new file mode 100644
index 00000000..33b0ad28
--- /dev/null
+++ b/pydantic_converter_v1/starter.py
@@ -0,0 +1,46 @@
+import asyncio
+import logging
+from datetime import datetime
+from ipaddress import IPv4Address
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from pydantic_converter_v1.converter import pydantic_data_converter
+from pydantic_converter_v1.worker import MyPydanticModel, MyWorkflow
+
+
+async def main():
+ logging.basicConfig(level=logging.INFO)
+
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
+ # Connect client using the Pydantic converter
+
+ client = await Client.connect(
+ **config,
+ data_converter=pydantic_data_converter,
+ )
+
+ # Run workflow
+ result = await client.execute_workflow(
+ MyWorkflow.run,
+ [
+ MyPydanticModel(
+ some_ip=IPv4Address("127.0.0.1"),
+ some_date=datetime(2000, 1, 2, 3, 4, 5),
+ ),
+ MyPydanticModel(
+ some_ip=IPv4Address("127.0.0.2"),
+ some_date=datetime(2001, 2, 3, 4, 5, 6),
+ ),
+ ],
+ id="pydantic_converter-workflow-id",
+ task_queue="pydantic_converter-task-queue",
+ )
+ logging.info("Got models from client: %s" % result)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/pydantic_converter_v1/worker.py b/pydantic_converter_v1/worker.py
new file mode 100644
index 00000000..5ff65e1e
--- /dev/null
+++ b/pydantic_converter_v1/worker.py
@@ -0,0 +1,104 @@
+import asyncio
+import dataclasses
+import logging
+from datetime import datetime, timedelta
+from ipaddress import IPv4Address
+from typing import List
+
+from temporalio import activity, workflow
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+from temporalio.worker.workflow_sandbox import (
+ SandboxedWorkflowRunner,
+ SandboxRestrictions,
+)
+
+# We always want to pass through external modules to the sandbox that we know
+# are safe for workflow use
+with workflow.unsafe.imports_passed_through():
+ from pydantic import BaseModel
+
+ from pydantic_converter_v1.converter import pydantic_data_converter
+
+
+class MyPydanticModel(BaseModel):
+ some_ip: IPv4Address
+ some_date: datetime
+
+
+@activity.defn
+async def my_activity(models: List[MyPydanticModel]) -> List[MyPydanticModel]:
+ activity.logger.info("Got models in activity: %s" % models)
+ return models
+
+
+@workflow.defn
+class MyWorkflow:
+ @workflow.run
+ async def run(self, models: List[MyPydanticModel]) -> List[MyPydanticModel]:
+ workflow.logger.info("Got models in workflow: %s" % models)
+ return await workflow.execute_activity(
+ my_activity, models, start_to_close_timeout=timedelta(minutes=1)
+ )
+
+
+# Due to known issues with Pydantic's use of issubclass and our inability to
+# override the check in sandbox, Pydantic will think datetime is actually date
+# in the sandbox. At the expense of protecting against datetime.now() use in
+# workflows, we're going to remove datetime module restrictions. See sdk-python
+# README's discussion of known sandbox issues for more details.
+def new_sandbox_runner() -> SandboxedWorkflowRunner:
+ # TODO(cretz): Use with_child_unrestricted when https://github.com/temporalio/sdk-python/issues/254
+ # is fixed and released
+ invalid_module_member_children = dict(
+ SandboxRestrictions.invalid_module_members_default.children
+ )
+ del invalid_module_member_children["datetime"]
+ return SandboxedWorkflowRunner(
+ restrictions=dataclasses.replace(
+ SandboxRestrictions.default,
+ invalid_module_members=dataclasses.replace(
+ SandboxRestrictions.invalid_module_members_default,
+ children=invalid_module_member_children,
+ ),
+ )
+ )
+
+
+interrupt_event = asyncio.Event()
+
+
+async def main():
+ logging.basicConfig(level=logging.INFO)
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
+ # Connect client using the Pydantic converter
+
+ client = await Client.connect(
+ **config,
+ data_converter=pydantic_data_converter,
+ )
+
+ # Run a worker for the workflow
+ async with Worker(
+ client,
+ task_queue="pydantic_converter-task-queue",
+ workflows=[MyWorkflow],
+ activities=[my_activity],
+ workflow_runner=new_sandbox_runner(),
+ ):
+ # Wait until interrupted
+ print("Worker started, ctrl+c to exit")
+ await interrupt_event.wait()
+ print("Shutting down")
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/pyproject.toml b/pyproject.toml
index 86ae4841..caae123c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,61 +1,124 @@
-[tool.poetry]
+[project]
name = "temporalio-samples"
version = "0.1a1"
description = "Temporal.io Python SDK samples"
-license = "MIT"
-authors = ["Temporal Technologies Inc "]
+authors = [{ name = "Temporal Technologies Inc", email = "sdk@temporal.io" }]
+requires-python = ">=3.10"
readme = "README.md"
-homepage = "https://github.com/temporalio/samples-python"
-repository = "https://github.com/temporalio/samples-python"
-documentation = "https://docs.temporal.io/docs/python"
-packages = [
- { include = "**/*.py", from = "." }
-]
+license = "MIT"
+dependencies = ["temporalio>=1.23.0,<2"]
-[tool.poetry.urls]
+[project.urls]
+Homepage = "https://github.com/temporalio/samples-python"
+Repository = "https://github.com/temporalio/samples-python"
+Documentation = "https://docs.temporal.io/docs/python"
"Bug Tracker" = "https://github.com/temporalio/samples-python/issues"
-[tool.poetry.dependencies]
-python = "^3.7"
-temporalio = "^1.1.0"
-
-[tool.poetry.dev-dependencies]
-black = "^22.3.0"
-isort = "^5.10.1"
-mypy = "^0.961"
-pytest = "^7.1.2"
-pytest-asyncio = "^0.18.3"
+[dependency-groups]
+dev = [
+ "ruff>=0.5.0,<0.6",
+ "mypy>=1.4.1,<2",
+ "pytest>=7.1.2,<8",
+ "pytest-asyncio>=0.18.3,<0.19",
+ "frozenlist>=1.4.0,<2",
+ "pyright>=1.1.394",
+ "types-pyyaml>=6.0.12.20241230,<7",
+ "pytest-pretty>=1.3.0",
+ "poethepoet>=0.36.0",
+]
+bedrock = ["boto3>=1.34.92,<2"]
+dsl = ["pyyaml>=6.0.1,<7", "types-pyyaml>=6.0.12,<7", "dacite>=1.8.1,<2"]
+encryption = ["cryptography>=38.0.1,<39", "aiohttp>=3.8.1,<4"]
+gevent = ["gevent>=25.4.2 ; python_version >= '3.8'"]
+langchain = [
+ "langchain>=0.1.7,<0.2 ; python_version >= '3.8.1' and python_version < '4.0'",
+ "langchain-openai>=0.0.6,<0.0.7 ; python_version >= '3.8.1' and python_version < '4.0'",
+ "langsmith>=0.1.22,<0.2 ; python_version >= '3.8.1' and python_version < '4.0'",
+ "openai>=1.4.0,<2",
+ "fastapi>=0.115.12",
+ "tqdm>=4.62.0,<5",
+ "uvicorn[standard]>=0.24.0.post1,<0.25",
+]
+nexus = ["nexus-rpc>=1.1.0,<2"]
+open-telemetry = [
+ "temporalio[opentelemetry]",
+ "opentelemetry-exporter-otlp-proto-grpc",
+]
+openai-agents = [
+ "openai-agents[litellm] == 0.3.2",
+ "temporalio[openai-agents] >= 1.18.0",
+ "requests>=2.32.0,<3",
+]
+pydantic-converter = ["pydantic>=2.10.6,<3"]
+sentry = ["sentry-sdk>=2.13.0"]
+trio-async = ["trio>=0.28.0,<0.29", "trio-asyncio>=0.15.0,<0.16"]
+cloud-export-to-parquet = [
+ "pandas>=2.2.2,<3 ; python_version >= '3.10' and python_version < '4.0'",
+ "numpy>=1.26.0,<2 ; python_version >= '3.10' and python_version < '3.13'",
+ "boto3>=1.34.89,<2",
+ "pyarrow>=19.0.1",
+]
-# All sample-specific dependencies are in optional groups below, named after the
-# sample they apply to
+[tool.hatch.metadata]
+allow-direct-references = true
-[tool.poetry.group.encryption]
-optional = true
-dependencies = { cryptography = "^38.0.1", aiohttp = "^3.8.1" }
+[tool.hatch.build.targets.sdist]
+include = ["./**/*.py"]
-[tool.poetry.group.open_telemetry]
-optional = true
-[tool.poetry.group.open_telemetry.dependencies]
-temporalio = { version = "*", extras = ["opentelemetry"] }
-opentelemetry-exporter-jaeger-thrift = "^1.13.0"
+[tool.hatch.build.targets.wheel]
+include = ["./**/*.py"]
+packages = [
+ "activity_worker",
+ "bedrock",
+ "cloud_export_to_parquet",
+ "context_propagation",
+ "custom_converter",
+ "custom_decorator",
+ "custom_metric",
+ "dsl",
+ "encryption",
+ "gevent_async",
+ "hello",
+ "langchain",
+ "message_passing",
+ "nexus",
+ "open_telemetry",
+ "patching",
+ "polling",
+ "prometheus",
+ "pydantic_converter",
+ "pydantic_converter_v1",
+ "pyproject.toml",
+ "replay",
+ "schedules",
+ "sentry",
+ "sleep_for_days",
+ "tests",
+ "trio_async",
+ "updatable_timer",
+ "worker_specific_task_queues",
+ "worker_versioning",
+]
-[tool.poetry.group.pydantic]
-optional = true
-dependencies = { pydantic = "^1.10.4" }
+[tool.hatch.build.targets.wheel.sources]
+"./**/*.py" = "**/*.py"
-[tool.poetry.group.sentry]
-optional = true
-dependencies = { sentry-sdk = "^1.11.0" }
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
[tool.poe.tasks]
-format = [{cmd = "black ."}, {cmd = "isort ."}]
-lint = [{cmd = "black --check ."}, {cmd = "isort --check-only ."}, {ref = "lint-types" }]
-lint-types = "mypy --check-untyped-defs --namespace-packages ."
-test = "pytest"
-
-[build-system]
-requires = ["poetry-core>=1.0.0"]
-build-backend = "poetry.core.masonry.api"
+format = [
+ { cmd = "uv run ruff check --select I --fix" },
+ { cmd = "uv run ruff format" },
+]
+lint = [
+ { cmd = "uv run ruff check --select I" },
+ { cmd = "uv run ruff format --check" },
+ { ref = "lint-types" },
+]
+lint-types = "uv run --all-groups mypy --check-untyped-defs --namespace-packages ."
+test = "uv run --all-groups pytest"
[tool.pytest.ini_options]
asyncio_mode = "auto"
@@ -63,9 +126,8 @@ log_cli = true
log_cli_level = "INFO"
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
-[tool.isort]
-profile = "black"
-skip_gitignore = true
+[tool.ruff]
+target-version = "py310"
[tool.mypy]
ignore_missing_imports = true
diff --git a/replay/README.md b/replay/README.md
index 3a67868a..e9fc9ccf 100644
--- a/replay/README.md
+++ b/replay/README.md
@@ -3,18 +3,18 @@
This sample shows you how you can verify changes to workflow code are compatible with existing
workflow histories.
-To run, first see [README.md](../README.md) for prerequisites. Then, run the following from this directory to start the
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from the root directory to start the
worker:
- poetry run python worker.py
+ uv run replay/worker.py
This will start the worker. Then, in another terminal, run the following to execute a workflow:
- poetry run python starter.py
+ uv run replay/starter.py
Next, run the replayer:
- poetry run python replayer.py
+ uv run replay/replayer.py
Which should produce some output like:
diff --git a/replay/replayer.py b/replay/replayer.py
index 49f16313..4787b33b 100644
--- a/replay/replayer.py
+++ b/replay/replayer.py
@@ -1,6 +1,7 @@
import asyncio
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Replayer
from replay.worker import JustActivity, JustTimer, TimerThenActivity
@@ -8,7 +9,9 @@
async def main():
# Connect client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Fetch the histories of the workflows to be replayed
workflows = client.list_workflows('WorkflowId="replayer-workflow-id"')
diff --git a/replay/starter.py b/replay/starter.py
index daf07098..228e50c3 100644
--- a/replay/starter.py
+++ b/replay/starter.py
@@ -1,13 +1,16 @@
import asyncio
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from replay.worker import JustActivity, JustTimer, TimerThenActivity
async def main():
# Connect client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a few workflows
# Importantly, normally we would *not* advise re-using the same workflow ID for all of these,
diff --git a/replay/worker.py b/replay/worker.py
index 3aebc099..4ac57da2 100644
--- a/replay/worker.py
+++ b/replay/worker.py
@@ -5,6 +5,7 @@
from temporalio import activity, workflow
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
@@ -71,7 +72,9 @@ async def main():
logging.basicConfig(level=logging.INFO)
# Start client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
async with Worker(
diff --git a/resource_pool/README.md b/resource_pool/README.md
new file mode 100644
index 00000000..5b746d7b
--- /dev/null
+++ b/resource_pool/README.md
@@ -0,0 +1,48 @@
+# Resource Pool Sample
+
+This sample shows how to use a long-lived `ResourcePoolWorkflow` to allocate `resources` to `ResourceUserWorkflows`.
+Each `ResourceUserWorkflow` runs several activities while it has ownership of a resource. Note that
+`ResourcePoolWorkflow` is making resource allocation decisions based on in-memory state.
+
+Run the following from the root directory to start the worker:
+
+ uv run resource_pool/worker.py
+
+This will start the worker. Then, in another terminal, run the following to execute several `ResourceUserWorkflows`.
+
+ uv run resource_pool/starter.py
+
+You should see output indicating that the `ResourcePoolWorkflow` serialized access to each resource.
+
+You can query the set of current resource resource holders with:
+
+ temporal workflow query --workflow-id resource_pool --name get_current_holders
+
+# Other approaches
+
+There are simpler ways to manage concurrent access to resources. Consider using resource-specific workers/task queues,
+and limiting the number of activity slots on the workers. The golang SDK also [sessions](https://docs.temporal.io/develop/go/sessions)
+that allow workflows to pin themselves to workers.
+
+The technique in this sample is capable of more complex resource allocation than the options above, but it doesn't scale
+as well. Specifically, it can:
+- Manage access to a set of resources that is decoupled from the set of workers and task queues
+- Run arbitrary code to place workloads on resources as they become available
+
+# Caveats
+
+This sample uses true locking (not leasing!) to avoid complexity and scaling concerns associated with heartbeating via
+signals. Locking carries a risk where failure to unlock permanently removing a resource from the pool. However, with
+Temporal's durable execution guarantees, this can only happen if:
+
+- A ResourceUserWorkflows times out (prohibited in the sample code)
+- An operator terminates a ResourceUserWorkflows. (Temporal recommends canceling workflows instead of terminating them whenever possible.)
+- You shut down your workers and never restart them (unhandled, but irrelevant)
+
+If a leak were to happen, you could discover the identity of the leaker using the query above, then:
+
+ temporal workflow signal --workflow-id resource_pool --name release_resource --input '{ "release_key": "" }'
+
+Performance: A single ResourcePoolWorkflow scales to tens, but not hundreds, of request/release events per second. It is
+best suited for allocating resources to long-running workflows. Actual performance will depend on your temporal server's
+persistence layer.
\ No newline at end of file
diff --git a/resource_pool/__init__.py b/resource_pool/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/resource_pool/pool_client/__init__.py b/resource_pool/pool_client/__init__.py
new file mode 100644
index 00000000..b8471d8a
--- /dev/null
+++ b/resource_pool/pool_client/__init__.py
@@ -0,0 +1,2 @@
+from .resource_pool_client import ResourcePoolClient
+from .resource_pool_workflow import ResourcePoolWorkflow
diff --git a/resource_pool/pool_client/resource_pool_client.py b/resource_pool/pool_client/resource_pool_client.py
new file mode 100644
index 00000000..b7183afa
--- /dev/null
+++ b/resource_pool/pool_client/resource_pool_client.py
@@ -0,0 +1,101 @@
+from contextlib import asynccontextmanager
+from datetime import timedelta
+from typing import AsyncGenerator, Optional
+
+from temporalio import workflow
+
+from resource_pool.pool_client.resource_pool_workflow import ResourcePoolWorkflow
+from resource_pool.shared import (
+ AcquiredResource,
+ AcquireRequest,
+ AcquireResponse,
+ DetachedResource,
+)
+
+
+# Use this class in workflow code that that needs to run on locked resources.
+class ResourcePoolClient:
+ def __init__(self, pool_workflow_id: str) -> None:
+ self.pool_workflow_id = pool_workflow_id
+ self.acquired_resources: list[AcquiredResource] = []
+
+ signal_name = f"assign_resource_{self.pool_workflow_id}"
+ if workflow.get_signal_handler(signal_name) is None:
+ workflow.set_signal_handler(signal_name, self._handle_acquire_response)
+ else:
+ raise RuntimeError(
+ f"{signal_name} already registered - if you use multiple ResourcePoolClients within the "
+ f"same workflow, they must use different pool_workflow_ids"
+ )
+
+ def _handle_acquire_response(self, response: AcquireResponse) -> None:
+ self.acquired_resources.append(
+ AcquiredResource(
+ resource=response.resource, release_key=response.release_key
+ )
+ )
+
+ async def _send_acquire_signal(self) -> None:
+ await workflow.get_external_workflow_handle_for(
+ ResourcePoolWorkflow.run, self.pool_workflow_id
+ ).signal("acquire_resource", AcquireRequest(workflow.info().workflow_id))
+
+ async def _send_release_signal(self, acquired_resource: AcquiredResource) -> None:
+ await workflow.get_external_workflow_handle_for(
+ ResourcePoolWorkflow.run, self.pool_workflow_id
+ ).signal(
+ "release_resource",
+ AcquireResponse(
+ resource=acquired_resource.resource,
+ release_key=acquired_resource.release_key,
+ ),
+ )
+
+ @asynccontextmanager
+ async def acquire_resource(
+ self,
+ *,
+ reattach: Optional[DetachedResource] = None,
+ max_wait_time: timedelta = timedelta(minutes=5),
+ ) -> AsyncGenerator[AcquiredResource, None]:
+ _warn_when_workflow_has_timeouts()
+
+ if reattach is None:
+ await self._send_acquire_signal()
+ await workflow.wait_condition(
+ lambda: len(self.acquired_resources) > 0, timeout=max_wait_time
+ )
+ resource = self.acquired_resources.pop(0)
+ else:
+ resource = AcquiredResource(
+ resource=reattach.resource, release_key=reattach.release_key
+ )
+
+ # Can't happen, but the typechecker doesn't know about workflow.wait_condition
+ if resource is None:
+ raise RuntimeError("resource was None when it can't be")
+
+ # During the yield, the calling workflow owns the resource. Note that this is a lock, not a lease! Our
+ # finally block will release the resource if an activity fails. This is why we asserted the lack of
+ # workflow-level timeouts above - the finally block wouldn't run if there was a timeout.
+ try:
+ yield resource
+ finally:
+ if not resource.detached:
+ await self._send_release_signal(resource)
+
+
+def _warn_when_workflow_has_timeouts() -> None:
+ def has_timeout(timeout: Optional[timedelta]) -> bool:
+ # After continue_as_new, timeouts are 0, even if they were None before continue_as_new (and were not set in the
+ # continue_as_new call).
+ return timeout is not None and timeout > timedelta(0)
+
+ if has_timeout(workflow.info().run_timeout):
+ workflow.logger.warning(
+ f"ResourceLockingWorkflow cannot have a run_timeout (found {workflow.info().run_timeout}) - this will leak locks"
+ )
+ if has_timeout(workflow.info().execution_timeout):
+ workflow.logger.warning(
+ f"ResourceLockingWorkflow cannot have an execution_timeout (found {workflow.info().execution_timeout}) - this will leak locks"
+ )
diff --git a/resource_pool/pool_client/resource_pool_workflow.py b/resource_pool/pool_client/resource_pool_workflow.py
new file mode 100644
index 00000000..2321f82d
--- /dev/null
+++ b/resource_pool/pool_client/resource_pool_workflow.py
@@ -0,0 +1,149 @@
+from dataclasses import dataclass
+from typing import Optional
+
+from temporalio import workflow
+from temporalio.exceptions import ApplicationError
+
+from resource_pool.shared import AcquireRequest, AcquireResponse
+
+
+# Internal to this workflow, we'll associate randomly generated release signal names with each acquire request.
+@dataclass
+class InternalAcquireRequest(AcquireRequest):
+ release_signal: Optional[str]
+
+
+@dataclass
+class ResourcePoolWorkflowInput:
+ # Key is resource, value is current holder of the resource (None if not held)
+ resources: dict[str, Optional[InternalAcquireRequest]]
+ waiters: list[InternalAcquireRequest]
+
+
+@workflow.defn
+class ResourcePoolWorkflow:
+ @workflow.init
+ def __init__(self, input: ResourcePoolWorkflowInput) -> None:
+ self.resources = input.resources
+ self.waiters = input.waiters
+ self.release_key_to_resource: dict[str, str] = {}
+
+ for resource, holder in self.resources.items():
+ if holder is not None and holder.release_signal is not None:
+ self.release_key_to_resource[holder.release_signal] = resource
+
+ @workflow.signal
+ async def add_resources(self, resources: list[str]) -> None:
+ for resource in resources:
+ if resource in self.resources:
+ workflow.logger.warning(
+ f"Ignoring attempt to add already-existing resource: {resource}"
+ )
+ else:
+ self.resources[resource] = None
+
+ @workflow.signal
+ async def acquire_resource(self, request: AcquireRequest) -> None:
+ self.waiters.append(
+ InternalAcquireRequest(workflow_id=request.workflow_id, release_signal=None)
+ )
+ workflow.logger.info(
+ f"workflow_id={request.workflow_id} is waiting for a resource"
+ )
+
+ @workflow.signal
+ async def release_resource(self, acquire_response: AcquireResponse) -> None:
+ release_key = acquire_response.release_key
+ resource = self.release_key_to_resource.get(release_key)
+ if resource is None:
+ workflow.logger.warning(f"Ignoring unknown release_key: {release_key}")
+ return
+
+ holder = self.resources[resource]
+ if holder is None:
+ workflow.logger.warning(
+ f"Ignoring request to release resource that is not held: {resource}"
+ )
+ return
+
+ # Remove the current holder
+ workflow.logger.info(
+ f"workflow_id={holder.workflow_id} released resource {resource}"
+ )
+ self.resources[resource] = None
+ del self.release_key_to_resource[release_key]
+
+ @workflow.query
+ def get_current_holders(self) -> dict[str, Optional[InternalAcquireRequest]]:
+ return self.resources
+
+ async def assign_resource(
+ self, resource: str, internal_request: InternalAcquireRequest
+ ) -> None:
+ workflow.logger.info(
+ f"workflow_id={internal_request.workflow_id} acquired resource {resource}"
+ )
+
+ requester = workflow.get_external_workflow_handle(internal_request.workflow_id)
+ try:
+ release_signal = str(workflow.uuid4())
+ await requester.signal(
+ f"assign_resource_{workflow.info().workflow_id}",
+ AcquireResponse(release_key=release_signal, resource=resource),
+ )
+
+ internal_request.release_signal = release_signal
+ self.resources[resource] = internal_request
+ self.release_key_to_resource[release_signal] = resource
+ except ApplicationError as e:
+ if e.type == "ExternalWorkflowExecutionNotFound":
+ workflow.logger.info(
+ f"Could not assign resource {resource} to {internal_request.workflow_id}: {e.message}"
+ )
+ else:
+ raise e
+
+ async def assign_next_resource(self) -> bool:
+ if len(self.waiters) == 0:
+ return False
+
+ next_free_resource = self.get_free_resource()
+ if next_free_resource is None:
+ return False
+
+ next_waiter = self.waiters.pop(0)
+ await self.assign_resource(next_free_resource, next_waiter)
+ return True
+
+ def get_free_resource(self) -> Optional[str]:
+ return next(
+ (resource for resource, holder in self.resources.items() if holder is None),
+ None,
+ )
+
+ def can_assign_resource(self) -> bool:
+ return len(self.waiters) > 0 and self.get_free_resource() is not None
+
+ def should_continue_as_new(self) -> bool:
+ return (
+ workflow.info().is_continue_as_new_suggested()
+ and workflow.all_handlers_finished()
+ )
+
+ @workflow.run
+ async def run(self, _: ResourcePoolWorkflowInput) -> None:
+ while True:
+ await workflow.wait_condition(
+ lambda: self.can_assign_resource() or self.should_continue_as_new()
+ )
+
+ if await self.assign_next_resource():
+ continue
+
+ if self.should_continue_as_new():
+ workflow.continue_as_new(
+ ResourcePoolWorkflowInput(
+ resources=self.resources,
+ waiters=self.waiters,
+ )
+ )
diff --git a/resource_pool/resource_user_workflow.py b/resource_pool/resource_user_workflow.py
new file mode 100644
index 00000000..80ee7fba
--- /dev/null
+++ b/resource_pool/resource_user_workflow.py
@@ -0,0 +1,88 @@
+import asyncio
+from dataclasses import dataclass, field
+from datetime import timedelta
+from typing import Optional
+
+from temporalio import activity, workflow
+
+from resource_pool.pool_client import ResourcePoolClient
+from resource_pool.shared import DetachedResource
+
+
+@dataclass
+class UseResourceActivityInput:
+ resource: str
+ iteration: str
+
+
+@activity.defn
+async def use_resource(input: UseResourceActivityInput) -> None:
+ info = activity.info()
+ activity.logger.info(
+ f"{info.workflow_id} starts using {input.resource} the {input.iteration} time"
+ )
+ await asyncio.sleep(3)
+ activity.logger.info(
+ f"{info.workflow_id} done using {input.resource} the {input.iteration} time"
+ )
+
+
+@dataclass
+class ResourceUserWorkflowInput:
+ # The id of the resource pool workflow to request a resource from
+ resource_pool_workflow_id: str
+
+ # If set, this workflow will fail after the "first" or "second" activity.
+ iteration_to_fail_after: Optional[str]
+
+ # If True, this workflow will continue as new after the last activity. The next iteration will run more activities,
+ # but will not continue as new.
+ should_continue_as_new: bool
+
+ # Used to transfer resource ownership between iterations during continue_as_new
+ already_acquired_resource: Optional[DetachedResource] = field(default=None)
+
+
+class FailWorkflowException(Exception):
+ pass
+
+
+# Wait this long for a resource before giving up
+MAX_RESOURCE_WAIT_TIME = timedelta(minutes=5)
+
+
+@workflow.defn(failure_exception_types=[FailWorkflowException])
+class ResourceUserWorkflow:
+ @workflow.run
+ async def run(self, input: ResourceUserWorkflowInput) -> None:
+ pool_client = ResourcePoolClient(input.resource_pool_workflow_id)
+
+ async with pool_client.acquire_resource(
+ reattach=input.already_acquired_resource
+ ) as acquired_resource:
+ for iteration in ["first", "second"]:
+ await workflow.execute_activity(
+ use_resource,
+ UseResourceActivityInput(acquired_resource.resource, iteration),
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+
+ if iteration == input.iteration_to_fail_after:
+ workflow.logger.info(
+ f"Failing after iteration {input.iteration_to_fail_after}"
+ )
+ raise FailWorkflowException()
+
+ # This workflow only continues as new so it can demonstrate how to pass acquired resources across
+ # iterations. Ordinarily, such a short workflow would not use continue as new.
+ if input.should_continue_as_new:
+ detached_resource = acquired_resource.detach()
+
+ next_input = ResourceUserWorkflowInput(
+ resource_pool_workflow_id=input.resource_pool_workflow_id,
+ iteration_to_fail_after=input.iteration_to_fail_after,
+ should_continue_as_new=False,
+ already_acquired_resource=detached_resource,
+ )
+
+ workflow.continue_as_new(next_input)
diff --git a/resource_pool/shared.py b/resource_pool/shared.py
new file mode 100644
index 00000000..3930bb72
--- /dev/null
+++ b/resource_pool/shared.py
@@ -0,0 +1,31 @@
+from dataclasses import dataclass, field
+
+RESOURCE_POOL_WORKFLOW_ID = "resource_pool"
+
+
+@dataclass
+class AcquireRequest:
+ workflow_id: str
+
+
+@dataclass
+class AcquireResponse:
+ release_key: str
+ resource: str
+
+
+@dataclass
+class DetachedResource:
+ resource: str
+ release_key: str
+
+
+@dataclass
+class AcquiredResource:
+ resource: str
+ release_key: str
+ detached: bool = field(default=False)
+
+ def detach(self) -> DetachedResource:
+ self.detached = True
+ return DetachedResource(resource=self.resource, release_key=self.release_key)
diff --git a/resource_pool/starter.py b/resource_pool/starter.py
new file mode 100644
index 00000000..2a50357a
--- /dev/null
+++ b/resource_pool/starter.py
@@ -0,0 +1,69 @@
+import asyncio
+from typing import Any
+
+from temporalio.client import Client, WorkflowFailureError, WorkflowHandle
+from temporalio.common import WorkflowIDConflictPolicy
+from temporalio.envconfig import ClientConfig
+
+from resource_pool.pool_client.resource_pool_workflow import (
+ ResourcePoolWorkflow,
+ ResourcePoolWorkflowInput,
+)
+from resource_pool.resource_user_workflow import (
+ ResourceUserWorkflow,
+ ResourceUserWorkflowInput,
+)
+from resource_pool.shared import RESOURCE_POOL_WORKFLOW_ID
+
+
+async def main() -> None:
+ # Connect client
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Initialize the resource pool
+ resource_pool_handle = await client.start_workflow(
+ workflow=ResourcePoolWorkflow.run,
+ arg=ResourcePoolWorkflowInput(
+ resources={"resource_a": None, "resource_b": None},
+ waiters=[],
+ ),
+ id=RESOURCE_POOL_WORKFLOW_ID,
+ task_queue="resource_pool-task-queue",
+ id_conflict_policy=WorkflowIDConflictPolicy.USE_EXISTING,
+ )
+
+ # Start the ResourceUserWorkflows
+ resource_user_handles: list[WorkflowHandle[Any, Any]] = []
+ for i in range(0, 4):
+ input = ResourceUserWorkflowInput(
+ resource_pool_workflow_id=RESOURCE_POOL_WORKFLOW_ID,
+ iteration_to_fail_after=None,
+ should_continue_as_new=False,
+ )
+ if i == 0:
+ input.should_continue_as_new = True
+ if i == 1:
+ input.iteration_to_fail_after = "first"
+
+ handle = await client.start_workflow(
+ workflow=ResourceUserWorkflow.run,
+ arg=input,
+ id=f"resource-user-workflow-{i}",
+ task_queue="resource_pool-task-queue",
+ )
+ resource_user_handles.append(handle)
+
+ for handle in resource_user_handles:
+ try:
+ await handle.result()
+ except WorkflowFailureError:
+ pass
+
+ # Clean up after ourselves. In the real world, the resource pool workflow would run forever.
+ await resource_pool_handle.terminate()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/resource_pool/worker.py b/resource_pool/worker.py
new file mode 100644
index 00000000..253e5f8e
--- /dev/null
+++ b/resource_pool/worker.py
@@ -0,0 +1,34 @@
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from resource_pool.pool_client.resource_pool_workflow import ResourcePoolWorkflow
+from resource_pool.resource_user_workflow import ResourceUserWorkflow, use_resource
+
+
+async def main() -> None:
+ logging.basicConfig(level=logging.INFO)
+
+ # Start client
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Run a worker for the workflow
+ worker = Worker(
+ client,
+ task_queue="resource_pool-task-queue",
+ workflows=[ResourcePoolWorkflow, ResourceUserWorkflow],
+ activities=[
+ use_resource,
+ ],
+ )
+
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/schedules/README.md b/schedules/README.md
index 9bf40ca0..e5eed069 100644
--- a/schedules/README.md
+++ b/schedules/README.md
@@ -2,20 +2,20 @@
These samples show how to schedule a Workflow Execution and control certain action.
-To run, first see [README.md](../README.md) for prerequisites. Then, run the following from this directory to run the `schedules/` sample:
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from the root directory to run the `schedules/` sample:
- poetry run python run_worker.py
- poetry run python start_schedule.py
+ uv run schedules/run_worker.py
+ uv run schedules/start_schedule.py
-Replace `start_schedule.py` in the command with any other example filename to run it instead.
+Replace `schedules/start_schedule.py` in the command with any other example filename to run it instead.
- poetry run python backfill_schedule.py
- poetry run python delete_schedule.py
- poetry run python describe_schedule.py
- poetry run python list_schedule.py
- poetry run python pause_schedule.py
- python run python trigger_schedule.py
- poetry run python update_schedule.py
+ uv run schedules/backfill_schedule.py
+ uv run schedules/delete_schedule.py
+ uv run schedules/describe_schedule.py
+ uv run schedules/list_schedule.py
+ uv run schedules/pause_schedule.py
+ uv run schedules/trigger_schedule.py
+ uv run schedules/update_schedule.py
- create: Creates a new Schedule. Newly created Schedules return a Schedule ID to be used in other Schedule commands.
- backfill: Backfills the Schedule by going through the specified time periods as if they passed right now.
diff --git a/schedules/backfill_schedule.py b/schedules/backfill_schedule.py
index 65658409..708ad07f 100644
--- a/schedules/backfill_schedule.py
+++ b/schedules/backfill_schedule.py
@@ -2,10 +2,14 @@
from datetime import datetime, timedelta
from temporalio.client import Client, ScheduleBackfill, ScheduleOverlapPolicy
+from temporalio.envconfig import ClientConfig
async def main():
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
handle = client.get_schedule_handle(
"workflow-schedule-id",
)
@@ -16,7 +20,7 @@ async def main():
end_at=now - timedelta(minutes=9),
overlap=ScheduleOverlapPolicy.ALLOW_ALL,
),
- ),
+ )
if __name__ == "__main__":
diff --git a/schedules/delete_schedule.py b/schedules/delete_schedule.py
index b6265636..d7b64394 100644
--- a/schedules/delete_schedule.py
+++ b/schedules/delete_schedule.py
@@ -1,10 +1,14 @@
import asyncio
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
async def main():
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
handle = client.get_schedule_handle(
"workflow-schedule-id",
)
diff --git a/schedules/describe_schedule.py b/schedules/describe_schedule.py
index 22bb832d..0db3fba5 100644
--- a/schedules/describe_schedule.py
+++ b/schedules/describe_schedule.py
@@ -1,10 +1,14 @@
import asyncio
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
async def main():
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
handle = client.get_schedule_handle(
"workflow-schedule-id",
)
diff --git a/schedules/list_schedule.py b/schedules/list_schedule.py
index a863aeee..15c0fd6a 100644
--- a/schedules/list_schedule.py
+++ b/schedules/list_schedule.py
@@ -1,10 +1,13 @@
import asyncio
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
async def main() -> None:
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
async for schedule in await client.list_schedules():
print(f"List Schedule Info: {schedule.info}.")
diff --git a/schedules/pause_schedule.py b/schedules/pause_schedule.py
index a6f8721c..79a9ca03 100644
--- a/schedules/pause_schedule.py
+++ b/schedules/pause_schedule.py
@@ -1,10 +1,14 @@
import asyncio
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
async def main():
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
handle = client.get_schedule_handle(
"workflow-schedule-id",
)
diff --git a/schedules/run_worker.py b/schedules/run_worker.py
index 5252b1f7..00d14aaa 100644
--- a/schedules/run_worker.py
+++ b/schedules/run_worker.py
@@ -1,13 +1,17 @@
import asyncio
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
from your_activities import your_activity
from your_workflows import YourSchedulesWorkflow
async def main():
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
worker = Worker(
client,
task_queue="schedules-task-queue",
diff --git a/schedules/start_schedule.py b/schedules/start_schedule.py
index 6089be95..ead6202b 100644
--- a/schedules/start_schedule.py
+++ b/schedules/start_schedule.py
@@ -9,11 +9,15 @@
ScheduleSpec,
ScheduleState,
)
+from temporalio.envconfig import ClientConfig
from your_workflows import YourSchedulesWorkflow
async def main():
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
await client.create_schedule(
"workflow-schedule-id",
Schedule(
diff --git a/schedules/trigger_schedule.py b/schedules/trigger_schedule.py
index ca1f38f1..30939d32 100644
--- a/schedules/trigger_schedule.py
+++ b/schedules/trigger_schedule.py
@@ -1,10 +1,14 @@
import asyncio
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
async def main():
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
handle = client.get_schedule_handle(
"workflow-schedule-id",
)
diff --git a/schedules/update_schedule.py b/schedules/update_schedule.py
index 979c23a8..709eeda3 100644
--- a/schedules/update_schedule.py
+++ b/schedules/update_schedule.py
@@ -6,10 +6,14 @@
ScheduleUpdate,
ScheduleUpdateInput,
)
+from temporalio.envconfig import ClientConfig
async def main():
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
handle = client.get_schedule_handle(
"workflow-schedule-id",
)
diff --git a/sentry/README.md b/sentry/README.md
index 45977731..0062f815 100644
--- a/sentry/README.md
+++ b/sentry/README.md
@@ -1,19 +1,42 @@
# Sentry Sample
-This sample shows how to configure [Sentry](https://sentry.io) to intercept and capture errors from the Temporal SDK.
+This sample shows how to configure [Sentry](https://sentry.io) SDK (version 2) to intercept and capture errors from the Temporal SDK
+for workflows and activities. The integration adds some useful context to the errors, such as the activity type, task queue, etc.
+
+Note: Sentry currently does not support Python 3.14, likewise this sample does not support Python 3.14.
+
+## Further details
+
+This is a small modification of the original example Sentry integration in this repo based on SDK v1. The integration
+didn't work properly with Sentry SDK v2 due to some internal changes in the Sentry SDK that broke the worker sandbox.
+Additionally, the v1 SDK has been deprecated and is only receiving security patches and will reach EOL some time in the future.
+If you still need to use Sentry SDK v1, check the original example at this [commit](https://github.com/temporalio/samples-python/blob/090b96d750bafc10d4aad5ad506bb2439c413d5e/sentry).
+
+## Running the Sample
For this sample, the optional `sentry` dependency group must be included. To include, run:
- poetry install --with sentry
+ uv sync --no-default-groups --dev --group sentry
+
+> Note: this integration breaks when `gevent` is installed (e.g. by the gevent sample) so make sure to only install
+> the `sentry` group and run the scripts below as described.
To run, first see [README.md](../README.md) for prerequisites. Set `SENTRY_DSN` environment variable to the Sentry DSN.
-Then, run the following from this directory to start the worker:
+Then, run the following from the root directory to start the worker:
- poetry run python worker.py
+ export SENTRY_DSN= # You'll need a Sentry account to test against
+ export ENVIRONMENT=dev
+ uv run sentry/worker.py
This will start the worker. Then, in another terminal, run the following to execute the workflow:
- poetry run python starter.py
+ uv run sentry/starter.py
+
+You should see the activity fail causing an error to be reported to Sentry.
+
+## Screenshot
+
+The screenshot below shows the extra tags and context included in the
+Sentry error from the exception thrown in the activity.
-The workflow should complete with the hello result. If you alter the workflow or the activity to raise an
-`ApplicationError` instead, it should appear in Sentry.
\ No newline at end of file
+
diff --git a/sentry/activity.py b/sentry/activity.py
new file mode 100644
index 00000000..148cd0d2
--- /dev/null
+++ b/sentry/activity.py
@@ -0,0 +1,25 @@
+from dataclasses import dataclass
+
+from temporalio import activity
+
+
+@dataclass
+class WorkingActivityInput:
+ message: str
+
+
+@activity.defn
+async def working_activity(input: WorkingActivityInput) -> str:
+ activity.logger.info("Running activity with parameter %s" % input)
+ return "Success"
+
+
+@dataclass
+class BrokenActivityInput:
+ message: str
+
+
+@activity.defn
+async def broken_activity(input: BrokenActivityInput) -> str:
+ activity.logger.info("Running activity with parameter %s" % input)
+ raise Exception("Activity failed!")
diff --git a/sentry/images/sentry.jpeg b/sentry/images/sentry.jpeg
new file mode 100644
index 00000000..0f62825b
Binary files /dev/null and b/sentry/images/sentry.jpeg differ
diff --git a/sentry/interceptor.py b/sentry/interceptor.py
index f1737ed2..d016acef 100644
--- a/sentry/interceptor.py
+++ b/sentry/interceptor.py
@@ -1,5 +1,6 @@
+import logging
from dataclasses import asdict, is_dataclass
-from typing import Any, Optional, Type, Union
+from typing import Any, Optional, Type
from temporalio import activity, workflow
from temporalio.worker import (
@@ -12,59 +13,65 @@
)
with workflow.unsafe.imports_passed_through():
- from sentry_sdk import Hub, capture_exception, set_context, set_tag
+ import sentry_sdk
-def _set_common_workflow_tags(info: Union[workflow.Info, activity.Info]):
- set_tag("temporal.workflow.type", info.workflow_type)
- set_tag("temporal.workflow.id", info.workflow_id)
+logger = logging.getLogger(__name__)
class _SentryActivityInboundInterceptor(ActivityInboundInterceptor):
async def execute_activity(self, input: ExecuteActivityInput) -> Any:
# https://docs.sentry.io/platforms/python/troubleshooting/#addressing-concurrency-issues
- with Hub(Hub.current):
- set_tag("temporal.execution_type", "activity")
- set_tag("module", input.fn.__module__ + "." + input.fn.__qualname__)
-
+ with sentry_sdk.isolation_scope() as scope:
+ scope.set_tag("temporal.execution_type", "activity")
+ scope.set_tag("module", input.fn.__module__ + "." + input.fn.__qualname__)
activity_info = activity.info()
- _set_common_workflow_tags(activity_info)
- set_tag("temporal.activity.id", activity_info.activity_id)
- set_tag("temporal.activity.type", activity_info.activity_type)
- set_tag("temporal.activity.task_queue", activity_info.task_queue)
- set_tag("temporal.workflow.namespace", activity_info.workflow_namespace)
- set_tag("temporal.workflow.run_id", activity_info.workflow_run_id)
+ scope.set_tag("temporal.workflow.type", activity_info.workflow_type)
+ scope.set_tag("temporal.workflow.id", activity_info.workflow_id)
+ scope.set_tag("temporal.activity.id", activity_info.activity_id)
+ scope.set_tag("temporal.activity.type", activity_info.activity_type)
+ scope.set_tag("temporal.activity.task_queue", activity_info.task_queue)
+ scope.set_tag(
+ "temporal.workflow.namespace", activity_info.workflow_namespace
+ )
+ scope.set_tag("temporal.workflow.run_id", activity_info.workflow_run_id)
try:
return await super().execute_activity(input)
except Exception as e:
- if len(input.args) == 1 and is_dataclass(input.args[0]):
- set_context("temporal.activity.input", asdict(input.args[0]))
- set_context("temporal.activity.info", activity.info().__dict__)
- capture_exception()
+ if len(input.args) == 1:
+ [arg] = input.args
+ if is_dataclass(arg) and not isinstance(arg, type):
+ scope.set_context("temporal.activity.input", asdict(arg))
+ scope.set_context("temporal.activity.info", activity.info().__dict__)
+ scope.capture_exception()
raise e
class _SentryWorkflowInterceptor(WorkflowInboundInterceptor):
async def execute_workflow(self, input: ExecuteWorkflowInput) -> Any:
# https://docs.sentry.io/platforms/python/troubleshooting/#addressing-concurrency-issues
- with Hub(Hub.current):
- set_tag("temporal.execution_type", "workflow")
- set_tag("module", input.run_fn.__module__ + "." + input.run_fn.__qualname__)
+ with sentry_sdk.isolation_scope() as scope:
+ scope.set_tag("temporal.execution_type", "workflow")
+ scope.set_tag(
+ "module", input.run_fn.__module__ + "." + input.run_fn.__qualname__
+ )
workflow_info = workflow.info()
- _set_common_workflow_tags(workflow_info)
- set_tag("temporal.workflow.task_queue", workflow_info.task_queue)
- set_tag("temporal.workflow.namespace", workflow_info.namespace)
- set_tag("temporal.workflow.run_id", workflow_info.run_id)
+ scope.set_tag("temporal.workflow.type", workflow_info.workflow_type)
+ scope.set_tag("temporal.workflow.id", workflow_info.workflow_id)
+ scope.set_tag("temporal.workflow.task_queue", workflow_info.task_queue)
+ scope.set_tag("temporal.workflow.namespace", workflow_info.namespace)
+ scope.set_tag("temporal.workflow.run_id", workflow_info.run_id)
try:
return await super().execute_workflow(input)
except Exception as e:
- if len(input.args) == 1 and is_dataclass(input.args[0]):
- set_context("temporal.workflow.input", asdict(input.args[0]))
- set_context("temporal.workflow.info", workflow.info().__dict__)
-
+ if len(input.args) == 1:
+ [arg] = input.args
+ if is_dataclass(arg) and not isinstance(arg, type):
+ scope.set_context("temporal.workflow.input", asdict(arg))
+ scope.set_context("temporal.workflow.info", workflow.info().__dict__)
if not workflow.unsafe.is_replaying():
with workflow.unsafe.sandbox_unrestricted():
- capture_exception()
+ scope.capture_exception()
raise e
@@ -74,9 +81,6 @@ class SentryInterceptor(Interceptor):
def intercept_activity(
self, next: ActivityInboundInterceptor
) -> ActivityInboundInterceptor:
- """Implementation of
- :py:meth:`temporalio.worker.Interceptor.intercept_activity`.
- """
return _SentryActivityInboundInterceptor(super().intercept_activity(next))
def workflow_interceptor_class(
diff --git a/sentry/starter.py b/sentry/starter.py
index 9d0a0dc7..aa3f6271 100644
--- a/sentry/starter.py
+++ b/sentry/starter.py
@@ -1,23 +1,29 @@
import asyncio
-import os
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
-from sentry.worker import GreetingWorkflow
+from sentry.workflow import SentryExampleWorkflow, SentryExampleWorkflowInput
async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+
# Connect client
- client = await Client.connect("localhost:7233")
+ client = await Client.connect(**config)
# Run workflow
- result = await client.execute_workflow(
- GreetingWorkflow.run,
- "World",
- id="sentry-workflow-id",
- task_queue="sentry-task-queue",
- )
- print(f"Workflow result: {result}")
+ try:
+ result = await client.execute_workflow(
+ SentryExampleWorkflow.run,
+ SentryExampleWorkflowInput(option="broken"),
+ id="sentry-workflow-id",
+ task_queue="sentry-task-queue",
+ )
+ print(f"Workflow result: {result}")
+ except Exception:
+ print("Workflow failed - check Sentry for details")
if __name__ == "__main__":
diff --git a/sentry/worker.py b/sentry/worker.py
index 1db0826b..1bcd153e 100644
--- a/sentry/worker.py
+++ b/sentry/worker.py
@@ -1,64 +1,96 @@
import asyncio
-import logging
import os
-from dataclasses import dataclass
-from datetime import timedelta
import sentry_sdk
-from temporalio import activity, workflow
+from sentry_sdk.integrations.asyncio import AsyncioIntegration
+from sentry_sdk.types import Event, Hint
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
+from temporalio.worker.workflow_sandbox import (
+ SandboxedWorkflowRunner,
+ SandboxRestrictions,
+)
+from sentry.activity import broken_activity, working_activity
from sentry.interceptor import SentryInterceptor
+from sentry.workflow import SentryExampleWorkflow
+interrupt_event = asyncio.Event()
-@dataclass
-class ComposeGreetingInput:
- greeting: str
- name: str
+def before_send(event: Event, hint: Hint) -> Event | None:
+ # Filter out __ShutdownRequested events raised by the worker's internals
+ if str(hint.get("exc_info", [None])[0].__name__) == "_ShutdownRequested":
+ return None
-@activity.defn
-async def compose_greeting(input: ComposeGreetingInput) -> str:
- activity.logger.info("Running activity with parameter %s" % input)
- return f"{input.greeting}, {input.name}!"
+ return event
-@workflow.defn
-class GreetingWorkflow:
- @workflow.run
- async def run(self, name: str) -> str:
- workflow.logger.info("Running workflow with parameter %s" % name)
- return await workflow.execute_activity(
- compose_greeting,
- ComposeGreetingInput("Hello", name),
- start_to_close_timeout=timedelta(seconds=10),
+def initialise_sentry() -> None:
+ sentry_dsn = os.environ.get("SENTRY_DSN")
+ if not sentry_dsn:
+ print(
+ "SENTRY_DSN environment variable is not set. Sentry will not be initialized."
)
+ return
+ environment = os.environ.get("ENVIRONMENT")
+ sentry_sdk.init(
+ dsn=sentry_dsn,
+ environment=environment,
+ integrations=[
+ AsyncioIntegration(),
+ ],
+ attach_stacktrace=True,
+ before_send=before_send,
+ )
+ print(f"Sentry SDK initialized for environment: {environment!r}")
-async def main():
- # Uncomment the line below to see logging
- # logging.basicConfig(level=logging.INFO)
+async def main():
# Initialize the Sentry SDK
- sentry_sdk.init(
- dsn=os.environ.get("SENTRY_DSN"),
- )
+ initialise_sentry()
+
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
# Start client
- client = await Client.connect("localhost:7233")
+ client = await Client.connect(**config)
# Run a worker for the workflow
- worker = Worker(
+ async with Worker(
client,
task_queue="sentry-task-queue",
- workflows=[GreetingWorkflow],
- activities=[compose_greeting],
+ workflows=[SentryExampleWorkflow],
+ activities=[broken_activity, working_activity],
interceptors=[SentryInterceptor()], # Use SentryInterceptor for error reporting
- )
-
- await worker.run()
+ workflow_runner=SandboxedWorkflowRunner(
+ restrictions=SandboxRestrictions.default.with_passthrough_modules(
+ "sentry_sdk"
+ )
+ ),
+ ):
+ # Wait until interrupted
+ print("Worker started, ctrl+c to exit")
+ await interrupt_event.wait()
+ print("Shutting down")
if __name__ == "__main__":
- asyncio.run(main())
+ # Note: "Addressing Concurrency Issues" section in Sentry docs recommends using
+ # the AsyncioIntegration: "If you do concurrency with asyncio coroutines, make
+ # sure to use the AsyncioIntegration which will clone the correct scope in your Tasks"
+ # See https://docs.sentry.io/platforms/python/troubleshooting/
+ #
+ # However, this captures all unhandled exceptions in the event loop.
+ # So handle shutdown gracefully to avoid CancelledError and KeyboardInterrupt
+ # exceptions being captured as errors. Sentry also captures the worker's
+ # _ShutdownRequested exception, which is probably not useful. We've filtered this
+ # out in Sentry's before_send function.
+ loop = asyncio.new_event_loop()
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/sentry/workflow.py b/sentry/workflow.py
new file mode 100644
index 00000000..4cb779ea
--- /dev/null
+++ b/sentry/workflow.py
@@ -0,0 +1,38 @@
+import typing
+from dataclasses import dataclass
+from datetime import timedelta
+
+from temporalio import workflow
+from temporalio.common import RetryPolicy
+
+from sentry.activity import WorkingActivityInput, working_activity
+
+with workflow.unsafe.imports_passed_through():
+ from sentry.activity import BrokenActivityInput, broken_activity
+
+
+@dataclass
+class SentryExampleWorkflowInput:
+ option: typing.Literal["working", "broken"]
+
+
+@workflow.defn
+class SentryExampleWorkflow:
+ @workflow.run
+ async def run(self, input: SentryExampleWorkflowInput) -> str:
+ workflow.logger.info("Running workflow with parameter %r" % input)
+
+ if input.option == "working":
+ return await workflow.execute_activity(
+ working_activity,
+ WorkingActivityInput(message="Hello, Temporal!"),
+ start_to_close_timeout=timedelta(seconds=10),
+ retry_policy=RetryPolicy(maximum_attempts=1),
+ )
+
+ return await workflow.execute_activity(
+ broken_activity,
+ BrokenActivityInput(message="Hello, Temporal!"),
+ start_to_close_timeout=timedelta(seconds=10),
+ retry_policy=RetryPolicy(maximum_attempts=1),
+ )
diff --git a/sleep_for_days/README.md b/sleep_for_days/README.md
new file mode 100644
index 00000000..5117fcdb
--- /dev/null
+++ b/sleep_for_days/README.md
@@ -0,0 +1,18 @@
+# Sleep for Days
+
+This sample demonstrates how to create a Temporal workflow that runs forever, sending an email every 30 days.
+
+To run, first see the main [README.md](../README.md) for prerequisites.
+
+Then create two terminals.
+
+Run the worker in one terminal:
+
+ uv run sleep_for_days/worker.py
+
+And execute the workflow in the other terminal:
+
+ uv run sleep_for_days/starter.py
+
+This sample will run indefinitely until you send a signal to `complete`. See how to send a signal via Temporal CLI [here](https://docs.temporal.io/cli/workflow#signal).
+
diff --git a/sleep_for_days/__init__.py b/sleep_for_days/__init__.py
new file mode 100644
index 00000000..04611d30
--- /dev/null
+++ b/sleep_for_days/__init__.py
@@ -0,0 +1 @@
+TASK_QUEUE = "sleep-for-days-task-queue"
diff --git a/sleep_for_days/activities.py b/sleep_for_days/activities.py
new file mode 100644
index 00000000..30972098
--- /dev/null
+++ b/sleep_for_days/activities.py
@@ -0,0 +1,18 @@
+from dataclasses import dataclass
+
+from temporalio import activity
+
+
+@dataclass
+class SendEmailInput:
+ email_msg: str
+
+
+@activity.defn()
+async def send_email(input: SendEmailInput) -> str:
+ """
+ A stub Activity for sending an email.
+ """
+ result = f"Email message: {input.email_msg}, sent"
+ activity.logger.info(result)
+ return result
diff --git a/sleep_for_days/starter.py b/sleep_for_days/starter.py
new file mode 100644
index 00000000..98dc083d
--- /dev/null
+++ b/sleep_for_days/starter.py
@@ -0,0 +1,28 @@
+import asyncio
+import uuid
+from typing import Optional
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from sleep_for_days import TASK_QUEUE
+from sleep_for_days.workflows import SleepForDaysWorkflow
+
+
+async def main(client: Optional[Client] = None):
+ if not client:
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ wf_handle = await client.start_workflow(
+ SleepForDaysWorkflow.run,
+ id=f"sleep-for-days-workflow-id-{uuid.uuid4()}",
+ task_queue=TASK_QUEUE,
+ )
+ # Wait for workflow completion (runs indefinitely until it receives a signal)
+ print(await wf_handle.result())
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/sleep_for_days/worker.py b/sleep_for_days/worker.py
new file mode 100644
index 00000000..59799607
--- /dev/null
+++ b/sleep_for_days/worker.py
@@ -0,0 +1,30 @@
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from sleep_for_days import TASK_QUEUE
+from sleep_for_days.activities import send_email
+from sleep_for_days.workflows import SleepForDaysWorkflow
+
+
+async def main():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ worker = Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[SleepForDaysWorkflow],
+ activities=[send_email],
+ )
+
+ await worker.run()
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
+ asyncio.run(main())
diff --git a/sleep_for_days/workflows.py b/sleep_for_days/workflows.py
new file mode 100644
index 00000000..eff0b273
--- /dev/null
+++ b/sleep_for_days/workflows.py
@@ -0,0 +1,37 @@
+import asyncio
+from dataclasses import dataclass
+from datetime import timedelta
+
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from sleep_for_days.activities import SendEmailInput, send_email
+
+
+@workflow.defn()
+class SleepForDaysWorkflow:
+ def __init__(self) -> None:
+ self.is_complete = False
+
+ @workflow.run
+ async def run(self) -> str:
+ while not self.is_complete:
+ await workflow.execute_activity(
+ send_email,
+ SendEmailInput("30 days until the next email"),
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+ await workflow.wait(
+ [
+ asyncio.create_task(workflow.sleep(timedelta(days=30))),
+ asyncio.create_task(
+ workflow.wait_condition(lambda: self.is_complete)
+ ),
+ ],
+ return_when=asyncio.FIRST_COMPLETED,
+ )
+ return "done!"
+
+ @workflow.signal
+ def complete(self):
+ self.is_complete = True
diff --git a/tests/activity_sticky_queues/activity_sticky_queues_activity_test.py b/tests/activity_sticky_queues/activity_sticky_queues_activity_test.py
index 8a0889a7..2525ba69 100644
--- a/tests/activity_sticky_queues/activity_sticky_queues_activity_test.py
+++ b/tests/activity_sticky_queues/activity_sticky_queues_activity_test.py
@@ -1,7 +1,7 @@
from pathlib import Path
from unittest import mock
-from activity_sticky_queues import tasks
+from worker_specific_task_queues import tasks
RETURNED_PATH = "valid/path"
tasks._get_delay_secs = mock.MagicMock(return_value=0.0001)
diff --git a/tests/activity_sticky_queues/activity_sticky_worker_workflow_test.py b/tests/activity_sticky_queues/activity_sticky_worker_workflow_test.py
index 58ba824c..fa72e18b 100644
--- a/tests/activity_sticky_queues/activity_sticky_worker_workflow_test.py
+++ b/tests/activity_sticky_queues/activity_sticky_worker_workflow_test.py
@@ -9,7 +9,7 @@
from temporalio.testing import WorkflowEnvironment
from temporalio.worker import Worker
-from activity_sticky_queues import tasks
+from worker_specific_task_queues import tasks
CHECKSUM = "a checksum"
RETURNED_PATH = "valid/path"
diff --git a/tests/conftest.py b/tests/conftest.py
index 95294fb4..65de246e 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -41,7 +41,14 @@ def event_loop():
async def env(request) -> AsyncGenerator[WorkflowEnvironment, None]:
env_type = request.config.getoption("--workflow-environment")
if env_type == "local":
- env = await WorkflowEnvironment.start_local()
+ env = await WorkflowEnvironment.start_local(
+ dev_server_extra_args=[
+ "--dynamic-config-value",
+ "frontend.enableExecuteMultiOperation=true",
+ "--dynamic-config-value",
+ "system.enableEagerWorkflowStart=true",
+ ]
+ )
elif env_type == "time-skipping":
env = await WorkflowEnvironment.start_time_skipping()
else:
diff --git a/tests/context_propagation/__init__.py b/tests/context_propagation/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/context_propagation/workflow_test.py b/tests/context_propagation/workflow_test.py
new file mode 100644
index 00000000..8de2120d
--- /dev/null
+++ b/tests/context_propagation/workflow_test.py
@@ -0,0 +1,46 @@
+import uuid
+
+from temporalio import activity
+from temporalio.client import Client
+from temporalio.exceptions import ApplicationError
+from temporalio.worker import Worker
+
+from context_propagation.interceptor import ContextPropagationInterceptor
+from context_propagation.shared import user_id
+from context_propagation.workflows import SayHelloWorkflow
+
+
+async def test_workflow_with_context_propagator(client: Client):
+ # Mock out the activity to assert the context value
+ @activity.defn(name="say_hello_activity")
+ async def say_hello_activity_mock(name: str) -> str:
+ try:
+ assert user_id.get() == "test-user"
+ except Exception as err:
+ raise ApplicationError("Assertion fail", non_retryable=True) from err
+ return f"Mock for {name}"
+
+ # Replace interceptors in client
+ new_config = client.config()
+ new_config["interceptors"] = [ContextPropagationInterceptor()]
+ client = Client(**new_config)
+ task_queue = f"tq-{uuid.uuid4()}"
+
+ async with Worker(
+ client,
+ task_queue=task_queue,
+ activities=[say_hello_activity_mock],
+ workflows=[SayHelloWorkflow],
+ ):
+ # Set the user during start/signal, but unset after
+ token = user_id.set("test-user")
+ handle = await client.start_workflow(
+ SayHelloWorkflow.run,
+ "some-name",
+ id=f"wf-{uuid.uuid4()}",
+ task_queue=task_queue,
+ )
+ await handle.signal(SayHelloWorkflow.signal_complete)
+ user_id.reset(token)
+ result = await handle.result()
+ assert result == "Mock for some-name"
diff --git a/tests/custom_converter/__init__.py b/tests/custom_converter/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/custom_converter/workflow_test.py b/tests/custom_converter/workflow_test.py
new file mode 100644
index 00000000..05c1aa0c
--- /dev/null
+++ b/tests/custom_converter/workflow_test.py
@@ -0,0 +1,28 @@
+import uuid
+
+from temporalio.client import Client
+from temporalio.worker import Worker
+
+from custom_converter.shared import (
+ GreetingInput,
+ GreetingOutput,
+ greeting_data_converter,
+)
+from custom_converter.workflow import GreetingWorkflow
+
+
+async def test_workflow_with_custom_converter(client: Client):
+ # Replace data converter in client
+ new_config = client.config()
+ new_config["data_converter"] = greeting_data_converter
+ client = Client(**new_config)
+ task_queue = f"tq-{uuid.uuid4()}"
+ async with Worker(client, task_queue=task_queue, workflows=[GreetingWorkflow]):
+ result = await client.execute_workflow(
+ GreetingWorkflow.run,
+ GreetingInput("Temporal"),
+ id=f"wf-{uuid.uuid4()}",
+ task_queue=task_queue,
+ )
+ assert isinstance(result, GreetingOutput)
+ assert result.result == "Hello, Temporal"
diff --git a/tests/custom_metric/__init__.py b/tests/custom_metric/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/custom_metric/workflow_test.py b/tests/custom_metric/workflow_test.py
new file mode 100644
index 00000000..bac5c07f
--- /dev/null
+++ b/tests/custom_metric/workflow_test.py
@@ -0,0 +1,34 @@
+import uuid
+from concurrent.futures import ThreadPoolExecutor
+
+from temporalio import activity
+from temporalio.client import Client
+from temporalio.worker import Worker
+
+from custom_metric.worker import StartTwoActivitiesWorkflow
+
+_TASK_QUEUE = "custom-metric-task-queue"
+
+activity_counter = 0
+
+
+async def test_custom_metric_workflow(client: Client):
+ @activity.defn(name="print_and_sleep")
+ async def print_message_mock():
+ global activity_counter
+ activity_counter += 1
+
+ async with Worker(
+ client,
+ task_queue=_TASK_QUEUE,
+ workflows=[StartTwoActivitiesWorkflow],
+ activities=[print_message_mock],
+ activity_executor=ThreadPoolExecutor(5),
+ ):
+ result = await client.execute_workflow(
+ StartTwoActivitiesWorkflow.run,
+ id=str(uuid.uuid4()),
+ task_queue=_TASK_QUEUE,
+ )
+ assert result is None
+ assert activity_counter == 2
diff --git a/tests/eager_wf_start/__init__.py b/tests/eager_wf_start/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/eager_wf_start/workflow_test.py b/tests/eager_wf_start/workflow_test.py
new file mode 100644
index 00000000..1080f5bc
--- /dev/null
+++ b/tests/eager_wf_start/workflow_test.py
@@ -0,0 +1,29 @@
+import uuid
+
+from temporalio.client import Client
+from temporalio.worker import Worker
+
+from eager_wf_start.activities import greeting
+from eager_wf_start.workflows import EagerWorkflow
+
+
+async def test_eager_wf_start(client: Client):
+ task_queue_name = str(uuid.uuid4())
+
+ async with Worker(
+ client,
+ task_queue=task_queue_name,
+ workflows=[EagerWorkflow],
+ activities=[greeting],
+ ):
+ handle = await client.start_workflow(
+ EagerWorkflow.run,
+ "Temporal",
+ id=f"workflow-{uuid.uuid4()}",
+ task_queue=task_queue_name,
+ request_eager_start=True,
+ )
+ print("HANDLE", handle.__temporal_eagerly_started)
+ assert handle.__temporal_eagerly_started
+ result = await handle.result()
+ assert result == "Hello Temporal!"
diff --git a/tests/hello/hello_activity_choice_test.py b/tests/hello/hello_activity_choice_test.py
new file mode 100644
index 00000000..1dadbb88
--- /dev/null
+++ b/tests/hello/hello_activity_choice_test.py
@@ -0,0 +1,31 @@
+import pytest
+from temporalio.testing import ActivityEnvironment
+
+from hello.hello_activity_choice import (
+ order_apples,
+ order_bananas,
+ order_cherries,
+ order_oranges,
+)
+
+# A list of tuples where each tuple contains:
+# - The activity function
+# - The order amount
+# - The expected result string
+activity_test_data = [
+ (order_apples, 5, "Ordered 5 Apples..."),
+ (order_bananas, 5, "Ordered 5 Bananas..."),
+ (order_cherries, 5, "Ordered 5 Cherries..."),
+ (order_oranges, 5, "Ordered 5 Oranges..."),
+]
+
+
+@pytest.mark.parametrize(
+ "activity_func, order_amount, expected_result", activity_test_data
+)
+def test_order_fruit(activity_func, order_amount, expected_result):
+ activity_environment = ActivityEnvironment()
+
+ result = activity_environment.run(activity_func, order_amount)
+
+ assert result == expected_result
diff --git a/tests/hello/hello_activity_test.py b/tests/hello/hello_activity_test.py
index 4bd8e40e..5bf9c1b8 100644
--- a/tests/hello/hello_activity_test.py
+++ b/tests/hello/hello_activity_test.py
@@ -1,4 +1,5 @@
import uuid
+from concurrent.futures import ThreadPoolExecutor
from temporalio import activity
from temporalio.client import Client
@@ -18,6 +19,7 @@ async def test_execute_workflow(client: Client):
client,
task_queue=task_queue_name,
workflows=[GreetingWorkflow],
+ activity_executor=ThreadPoolExecutor(5),
activities=[compose_greeting],
):
assert "Hello, World!" == await client.execute_workflow(
diff --git a/tests/hello/hello_cancellation_test.py b/tests/hello/hello_cancellation_test.py
index 2d4b946e..b511b50b 100644
--- a/tests/hello/hello_cancellation_test.py
+++ b/tests/hello/hello_cancellation_test.py
@@ -1,5 +1,6 @@
import asyncio
import uuid
+from concurrent.futures import ThreadPoolExecutor
import pytest
from temporalio.client import Client, WorkflowExecutionStatus, WorkflowFailureError
@@ -21,6 +22,7 @@ async def test_cancel_workflow(client: Client):
task_queue=task_queue_name,
workflows=[CancellationWorkflow],
activities=[cleanup_activity, never_complete_activity],
+ activity_executor=ThreadPoolExecutor(5),
):
handle = await client.start_workflow(
CancellationWorkflow.run,
diff --git a/tests/hello/hello_change_log_level_test.py b/tests/hello/hello_change_log_level_test.py
new file mode 100644
index 00000000..e83e08c4
--- /dev/null
+++ b/tests/hello/hello_change_log_level_test.py
@@ -0,0 +1,42 @@
+import asyncio
+import io
+import logging
+import uuid
+
+from temporalio.client import Client
+from temporalio.worker import Worker
+
+from hello.hello_change_log_level import LOG_MESSAGE, GreetingWorkflow
+
+
+async def test_workflow_with_log_capture(client: Client):
+ log_stream = io.StringIO()
+ handler = logging.StreamHandler(log_stream)
+ handler.setLevel(logging.ERROR)
+
+ logger = logging.getLogger()
+ logger.addHandler(handler)
+ logger.setLevel(logging.DEBUG)
+
+ task_queue = f"tq-{uuid.uuid4()}"
+
+ async with Worker(
+ client,
+ task_queue=task_queue,
+ workflows=[GreetingWorkflow],
+ ):
+ handle = await client.start_workflow(
+ GreetingWorkflow.run,
+ id=f"wf-{uuid.uuid4()}",
+ task_queue=task_queue,
+ )
+ await asyncio.sleep(
+ 0.2
+ ) # small wait to ensure the workflow has started, failed, and logged
+ await handle.terminate()
+
+ logger.removeHandler(handler)
+ handler.flush()
+
+ logs = log_stream.getvalue()
+ assert LOG_MESSAGE in logs
diff --git a/tests/hello/hello_child_test.py b/tests/hello/hello_child_test.py
new file mode 100644
index 00000000..38192912
--- /dev/null
+++ b/tests/hello/hello_child_test.py
@@ -0,0 +1,48 @@
+import uuid
+
+from temporalio import workflow
+from temporalio.client import Client
+from temporalio.worker import Worker
+
+from hello.hello_child_workflow import (
+ ComposeGreetingInput,
+ ComposeGreetingWorkflow,
+ GreetingWorkflow,
+)
+
+
+async def test_child_workflow(client: Client):
+ task_queue_name = str(uuid.uuid4())
+ async with Worker(
+ client,
+ task_queue=task_queue_name,
+ workflows=[GreetingWorkflow, ComposeGreetingWorkflow],
+ ):
+ assert "Hello, World!" == await client.execute_workflow(
+ GreetingWorkflow.run,
+ "World",
+ id=str(uuid.uuid4()),
+ task_queue=task_queue_name,
+ )
+
+
+@workflow.defn(name="ComposeGreetingWorkflow")
+class MockedComposeGreetingWorkflow:
+ @workflow.run
+ async def run(self, input: ComposeGreetingInput) -> str:
+ return f"{input.greeting}, {input.name} from mocked child!"
+
+
+async def test_mock_child_workflow(client: Client):
+ task_queue_name = str(uuid.uuid4())
+ async with Worker(
+ client,
+ task_queue=task_queue_name,
+ workflows=[GreetingWorkflow, MockedComposeGreetingWorkflow],
+ ):
+ assert "Hello, World from mocked child!" == await client.execute_workflow(
+ GreetingWorkflow.run,
+ "World",
+ id=str(uuid.uuid4()),
+ task_queue=task_queue_name,
+ )
diff --git a/tests/hello/hello_standalone_activity_test.py b/tests/hello/hello_standalone_activity_test.py
new file mode 100644
index 00000000..d40b09fa
--- /dev/null
+++ b/tests/hello/hello_standalone_activity_test.py
@@ -0,0 +1,31 @@
+import uuid
+from concurrent.futures import ThreadPoolExecutor
+from datetime import timedelta
+
+import pytest
+from temporalio.client import Client
+from temporalio.worker import Worker
+
+from hello_standalone_activity.my_activity import ComposeGreetingInput, compose_greeting
+
+
+async def test_execute_standalone_activity(client: Client):
+ pytest.skip(
+ "Standalone Activity is not yet supported by `temporal server start-dev`"
+ )
+ task_queue_name = str(uuid.uuid4())
+
+ async with Worker(
+ client,
+ task_queue=task_queue_name,
+ activities=[compose_greeting],
+ activity_executor=ThreadPoolExecutor(5),
+ ):
+ result = await client.execute_activity(
+ compose_greeting,
+ args=[ComposeGreetingInput("Hello", "World")],
+ id=str(uuid.uuid4()),
+ task_queue=task_queue_name,
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+ assert result == "Hello, World!"
diff --git a/tests/hello/hello_update_test.py b/tests/hello/hello_update_test.py
new file mode 100644
index 00000000..f7664ecb
--- /dev/null
+++ b/tests/hello/hello_update_test.py
@@ -0,0 +1,25 @@
+import uuid
+
+import pytest
+from temporalio.client import Client, WorkflowExecutionStatus
+from temporalio.testing import WorkflowEnvironment
+from temporalio.worker import Worker
+
+from hello.hello_update import GreetingWorkflow
+
+
+async def test_update_workflow(client: Client, env: WorkflowEnvironment):
+ task_queue_name = str(uuid.uuid4())
+ async with Worker(client, task_queue=task_queue_name, workflows=[GreetingWorkflow]):
+ handle = await client.start_workflow(
+ GreetingWorkflow.run, id=str(uuid.uuid4()), task_queue=task_queue_name
+ )
+
+ assert WorkflowExecutionStatus.RUNNING == (await handle.describe()).status
+
+ update_result = await handle.execute_update(
+ GreetingWorkflow.update_workflow_status
+ )
+ assert "Workflow status updated" == update_result
+ assert "Hello, World!" == (await handle.result())
+ assert WorkflowExecutionStatus.COMPLETED == (await handle.describe()).status
diff --git a/tests/hello_nexus/hello_nexus_test.py b/tests/hello_nexus/hello_nexus_test.py
new file mode 100644
index 00000000..fecfe17c
--- /dev/null
+++ b/tests/hello_nexus/hello_nexus_test.py
@@ -0,0 +1,48 @@
+import asyncio
+import sys
+
+import pytest
+from temporalio.client import Client
+from temporalio.testing import WorkflowEnvironment
+
+import hello_nexus.caller.app
+import hello_nexus.caller.workflows
+import hello_nexus.handler.worker
+from tests.helpers.nexus import create_nexus_endpoint, delete_nexus_endpoint
+
+
+async def test_nexus_service_basic(client: Client, env: WorkflowEnvironment):
+ if env.supports_time_skipping:
+ pytest.skip("Nexus tests don't work under the Java test server")
+
+ if sys.version_info[:2] < (3, 10):
+ pytest.skip("Sample is written for Python >= 3.10")
+
+ create_response = await create_nexus_endpoint(
+ name=hello_nexus.caller.workflows.NEXUS_ENDPOINT,
+ task_queue=hello_nexus.handler.worker.TASK_QUEUE,
+ client=client,
+ )
+ try:
+ handler_worker_task = asyncio.create_task(
+ hello_nexus.handler.worker.main(
+ client,
+ )
+ )
+ await asyncio.sleep(1)
+ results = await hello_nexus.caller.app.execute_caller_workflow(
+ client,
+ )
+ hello_nexus.handler.worker.interrupt_event.set()
+ await handler_worker_task
+ hello_nexus.handler.worker.interrupt_event.clear()
+ assert [r.message for r in results] == [
+ "Hello world from sync operation!",
+ "Hello world from workflow run operation!",
+ ]
+ finally:
+ await delete_nexus_endpoint(
+ id=create_response.endpoint.id,
+ version=create_response.endpoint.version,
+ client=client,
+ )
diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/helpers/nexus.py b/tests/helpers/nexus.py
new file mode 100644
index 00000000..dee8dc18
--- /dev/null
+++ b/tests/helpers/nexus.py
@@ -0,0 +1,39 @@
+import temporalio.api
+import temporalio.api.common
+import temporalio.api.common.v1
+import temporalio.api.enums.v1
+import temporalio.api.nexus
+import temporalio.api.nexus.v1
+import temporalio.api.operatorservice
+import temporalio.api.operatorservice.v1
+from temporalio.client import Client
+
+
+# TODO: copied from sdk-python tests/helpers/nexus
+async def create_nexus_endpoint(
+ name: str, task_queue: str, client: Client
+) -> temporalio.api.operatorservice.v1.CreateNexusEndpointResponse:
+ return await client.operator_service.create_nexus_endpoint(
+ temporalio.api.operatorservice.v1.CreateNexusEndpointRequest(
+ spec=temporalio.api.nexus.v1.EndpointSpec(
+ name=name,
+ target=temporalio.api.nexus.v1.EndpointTarget(
+ worker=temporalio.api.nexus.v1.EndpointTarget.Worker(
+ namespace=client.namespace,
+ task_queue=task_queue,
+ )
+ ),
+ )
+ )
+ )
+
+
+async def delete_nexus_endpoint(
+ id: str, version: int, client: Client
+) -> temporalio.api.operatorservice.v1.DeleteNexusEndpointResponse:
+ return await client.operator_service.delete_nexus_endpoint(
+ temporalio.api.operatorservice.v1.DeleteNexusEndpointRequest(
+ id=id,
+ version=version,
+ )
+ )
diff --git a/tests/message_passing/introduction/test_introduction_sample.py b/tests/message_passing/introduction/test_introduction_sample.py
new file mode 100644
index 00000000..17c1b14d
--- /dev/null
+++ b/tests/message_passing/introduction/test_introduction_sample.py
@@ -0,0 +1,125 @@
+import uuid
+
+import pytest
+from temporalio.client import Client, WorkflowUpdateFailedError
+from temporalio.testing import WorkflowEnvironment
+from temporalio.worker import Worker
+
+from message_passing.introduction.starter import TASK_QUEUE
+from message_passing.introduction.workflows import (
+ GetLanguagesInput,
+ GreetingWorkflow,
+ Language,
+ SetLanguageInput,
+ call_greeting_service,
+)
+
+
+async def test_queries(client: Client, env: WorkflowEnvironment):
+ if env.supports_time_skipping:
+ pytest.skip(
+ "Java test server: https://github.com/temporalio/sdk-java/issues/1903"
+ )
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[GreetingWorkflow],
+ ):
+ wf_handle = await client.start_workflow(
+ GreetingWorkflow.run,
+ id=str(uuid.uuid4()),
+ task_queue=TASK_QUEUE,
+ )
+ assert await wf_handle.query(GreetingWorkflow.get_language) == Language.ENGLISH
+ assert await wf_handle.query(
+ GreetingWorkflow.get_languages, GetLanguagesInput(include_unsupported=False)
+ ) == [Language.CHINESE, Language.ENGLISH]
+ assert await wf_handle.query(
+ GreetingWorkflow.get_languages, GetLanguagesInput(include_unsupported=True)
+ ) == [
+ Language.ARABIC,
+ Language.CHINESE,
+ Language.ENGLISH,
+ Language.FRENCH,
+ Language.HINDI,
+ Language.PORTUGUESE,
+ Language.SPANISH,
+ ]
+
+
+async def test_set_language(client: Client, env: WorkflowEnvironment):
+ if env.supports_time_skipping:
+ pytest.skip(
+ "Java test server: https://github.com/temporalio/sdk-java/issues/1903"
+ )
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[GreetingWorkflow],
+ ):
+ wf_handle = await client.start_workflow(
+ GreetingWorkflow.run,
+ id=str(uuid.uuid4()),
+ task_queue=TASK_QUEUE,
+ )
+ assert await wf_handle.query(GreetingWorkflow.get_language) == Language.ENGLISH
+ previous_language = await wf_handle.execute_update(
+ GreetingWorkflow.set_language, SetLanguageInput(language=Language.CHINESE)
+ )
+ assert previous_language == Language.ENGLISH
+ assert await wf_handle.query(GreetingWorkflow.get_language) == Language.CHINESE
+
+
+async def test_set_invalid_language(client: Client, env: WorkflowEnvironment):
+ if env.supports_time_skipping:
+ pytest.skip(
+ "Java test server: https://github.com/temporalio/sdk-java/issues/1903"
+ )
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[GreetingWorkflow],
+ ):
+ wf_handle = await client.start_workflow(
+ GreetingWorkflow.run,
+ id=str(uuid.uuid4()),
+ task_queue=TASK_QUEUE,
+ )
+ assert await wf_handle.query(GreetingWorkflow.get_language) == Language.ENGLISH
+
+ with pytest.raises(WorkflowUpdateFailedError):
+ await wf_handle.execute_update(
+ GreetingWorkflow.set_language,
+ SetLanguageInput(language=Language.ARABIC),
+ )
+
+
+async def test_set_language_that_is_only_available_via_remote_service(
+ client: Client, env: WorkflowEnvironment
+):
+ """
+ Similar to test_set_invalid_language, but this time Arabic is available
+ since we use the remote service.
+ """
+ if env.supports_time_skipping:
+ pytest.skip(
+ "Java test server: https://github.com/temporalio/sdk-java/issues/1903"
+ )
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[GreetingWorkflow],
+ activities=[call_greeting_service],
+ ):
+ wf_handle = await client.start_workflow(
+ GreetingWorkflow.run,
+ id=str(uuid.uuid4()),
+ task_queue=TASK_QUEUE,
+ )
+ assert await wf_handle.query(GreetingWorkflow.get_language) == Language.ENGLISH
+ previous_language = await wf_handle.execute_update(
+ GreetingWorkflow.set_language_using_activity,
+ SetLanguageInput(language=Language.ARABIC),
+ )
+ assert previous_language == Language.ENGLISH
+ assert await wf_handle.query(GreetingWorkflow.get_language) == Language.ARABIC
diff --git a/tests/message_passing/lazy_initialization/test_lazy_initialization.py b/tests/message_passing/lazy_initialization/test_lazy_initialization.py
new file mode 100644
index 00000000..dcf94582
--- /dev/null
+++ b/tests/message_passing/lazy_initialization/test_lazy_initialization.py
@@ -0,0 +1,60 @@
+import pytest
+from temporalio import common
+from temporalio.client import Client, WithStartWorkflowOperation
+from temporalio.testing import WorkflowEnvironment
+from temporalio.worker import Worker
+
+from message_passing.update_with_start.lazy_initialization.workflows import (
+ ShoppingCartItem,
+ ShoppingCartWorkflow,
+ get_price,
+)
+
+
+async def test_shopping_cart_workflow(client: Client, env: WorkflowEnvironment):
+ if env.supports_time_skipping:
+ pytest.skip(
+ "Java test server: https://github.com/temporalio/sdk-java/issues/1903"
+ )
+ async with Worker(
+ client,
+ task_queue="lazy-initialization-test",
+ workflows=[ShoppingCartWorkflow],
+ activities=[get_price],
+ ):
+ cart_id = "cart--session-1234"
+ make_start_op = lambda: WithStartWorkflowOperation(
+ ShoppingCartWorkflow.run,
+ id=cart_id,
+ id_conflict_policy=common.WorkflowIDConflictPolicy.USE_EXISTING,
+ task_queue="lazy-initialization-test",
+ )
+ start_op_1 = make_start_op()
+ price = await client.execute_update_with_start_workflow(
+ ShoppingCartWorkflow.add_item,
+ ShoppingCartItem(sku="item-1", quantity=2),
+ start_workflow_operation=start_op_1,
+ )
+
+ assert price == 1198
+
+ workflow_handle = await start_op_1.workflow_handle()
+
+ start_op_2 = make_start_op()
+ price = await client.execute_update_with_start_workflow(
+ ShoppingCartWorkflow.add_item,
+ ShoppingCartItem(sku="item-2", quantity=1),
+ start_workflow_operation=start_op_2,
+ )
+ assert price == 1797
+
+ workflow_handle = await start_op_2.workflow_handle()
+
+ await workflow_handle.signal(ShoppingCartWorkflow.checkout)
+
+ finalized_order = await workflow_handle.result()
+ assert finalized_order.items == [
+ (ShoppingCartItem(sku="item-1", quantity=2), 1198),
+ (ShoppingCartItem(sku="item-2", quantity=1), 599),
+ ]
+ assert finalized_order.total == 1797
diff --git a/tests/message_passing/safe_message_handlers/workflow_test.py b/tests/message_passing/safe_message_handlers/workflow_test.py
new file mode 100644
index 00000000..8fd28d8d
--- /dev/null
+++ b/tests/message_passing/safe_message_handlers/workflow_test.py
@@ -0,0 +1,170 @@
+import asyncio
+import uuid
+from typing import Callable, Sequence
+
+import pytest
+from temporalio.client import Client, WorkflowUpdateFailedError
+from temporalio.exceptions import ApplicationError
+from temporalio.testing import WorkflowEnvironment
+from temporalio.worker import Worker
+
+from message_passing.safe_message_handlers.activities import (
+ assign_nodes_to_job,
+ find_bad_nodes,
+ start_cluster,
+ unassign_nodes_for_job,
+)
+from message_passing.safe_message_handlers.workflow import (
+ ClusterManagerAssignNodesToJobInput,
+ ClusterManagerDeleteJobInput,
+ ClusterManagerInput,
+ ClusterManagerWorkflow,
+)
+
+ACTIVITIES: Sequence[Callable] = [
+ assign_nodes_to_job,
+ unassign_nodes_for_job,
+ find_bad_nodes,
+ start_cluster,
+]
+
+
+async def test_safe_message_handlers(client: Client, env: WorkflowEnvironment):
+ if env.supports_time_skipping:
+ pytest.skip(
+ "Java test server: https://github.com/temporalio/sdk-java/issues/1903"
+ )
+ task_queue = f"tq-{uuid.uuid4()}"
+ async with Worker(
+ client,
+ task_queue=task_queue,
+ workflows=[ClusterManagerWorkflow],
+ activities=ACTIVITIES,
+ ):
+ cluster_manager_handle = await client.start_workflow(
+ ClusterManagerWorkflow.run,
+ ClusterManagerInput(),
+ id=f"ClusterManagerWorkflow-{uuid.uuid4()}",
+ task_queue=task_queue,
+ )
+ await cluster_manager_handle.execute_update(
+ ClusterManagerWorkflow.wait_until_cluster_started
+ )
+
+ allocation_updates = []
+ for i in range(6):
+ allocation_updates.append(
+ cluster_manager_handle.execute_update(
+ ClusterManagerWorkflow.assign_nodes_to_job,
+ ClusterManagerAssignNodesToJobInput(
+ total_num_nodes=2, job_name=f"task-{i}"
+ ),
+ )
+ )
+ results = await asyncio.gather(*allocation_updates)
+ for result in results:
+ assert len(result.nodes_assigned) == 2
+
+ await asyncio.sleep(1)
+
+ deletion_updates = []
+ for i in range(6):
+ deletion_updates.append(
+ cluster_manager_handle.execute_update(
+ ClusterManagerWorkflow.delete_job,
+ ClusterManagerDeleteJobInput(job_name=f"task-{i}"),
+ )
+ )
+ await asyncio.gather(*deletion_updates)
+
+ await cluster_manager_handle.signal(ClusterManagerWorkflow.shutdown_cluster)
+
+ cluster_manager_result = await cluster_manager_handle.result()
+ assert cluster_manager_result.num_currently_assigned_nodes == 0
+
+
+async def test_update_idempotency(client: Client, env: WorkflowEnvironment):
+ if env.supports_time_skipping:
+ pytest.skip(
+ "Java test server: https://github.com/temporalio/sdk-java/issues/1903"
+ )
+ task_queue = f"tq-{uuid.uuid4()}"
+ async with Worker(
+ client,
+ task_queue=task_queue,
+ workflows=[ClusterManagerWorkflow],
+ activities=ACTIVITIES,
+ ):
+ cluster_manager_handle = await client.start_workflow(
+ ClusterManagerWorkflow.run,
+ ClusterManagerInput(),
+ id=f"ClusterManagerWorkflow-{uuid.uuid4()}",
+ task_queue=task_queue,
+ )
+
+ await cluster_manager_handle.execute_update(
+ ClusterManagerWorkflow.wait_until_cluster_started
+ )
+
+ result_1 = await cluster_manager_handle.execute_update(
+ ClusterManagerWorkflow.assign_nodes_to_job,
+ ClusterManagerAssignNodesToJobInput(
+ total_num_nodes=5, job_name="jobby-job"
+ ),
+ )
+ # simulate that in calling it twice, the operation is idempotent
+ result_2 = await cluster_manager_handle.execute_update(
+ ClusterManagerWorkflow.assign_nodes_to_job,
+ ClusterManagerAssignNodesToJobInput(
+ total_num_nodes=5, job_name="jobby-job"
+ ),
+ )
+ # the second call should not assign more nodes (it may return fewer if the health check finds bad nodes
+ # in between the two signals.)
+ assert result_1.nodes_assigned >= result_2.nodes_assigned
+
+
+async def test_update_failure(client: Client, env: WorkflowEnvironment):
+ if env.supports_time_skipping:
+ pytest.skip(
+ "Java test server: https://github.com/temporalio/sdk-java/issues/1903"
+ )
+ task_queue = f"tq-{uuid.uuid4()}"
+ async with Worker(
+ client,
+ task_queue=task_queue,
+ workflows=[ClusterManagerWorkflow],
+ activities=ACTIVITIES,
+ ):
+ cluster_manager_handle = await client.start_workflow(
+ ClusterManagerWorkflow.run,
+ ClusterManagerInput(),
+ id=f"ClusterManagerWorkflow-{uuid.uuid4()}",
+ task_queue=task_queue,
+ )
+
+ await cluster_manager_handle.execute_update(
+ ClusterManagerWorkflow.wait_until_cluster_started
+ )
+
+ await cluster_manager_handle.execute_update(
+ ClusterManagerWorkflow.assign_nodes_to_job,
+ ClusterManagerAssignNodesToJobInput(
+ total_num_nodes=24, job_name="big-task"
+ ),
+ )
+ try:
+ # Try to assign too many nodes
+ await cluster_manager_handle.execute_update(
+ ClusterManagerWorkflow.assign_nodes_to_job,
+ ClusterManagerAssignNodesToJobInput(
+ total_num_nodes=3, job_name="little-task"
+ ),
+ )
+ except WorkflowUpdateFailedError as e:
+ assert isinstance(e.cause, ApplicationError)
+ assert e.cause.message == "Cannot assign 3 nodes; have only 1 available"
+ finally:
+ await cluster_manager_handle.signal(ClusterManagerWorkflow.shutdown_cluster)
+ result = await cluster_manager_handle.result()
+ assert result.num_currently_assigned_nodes + result.num_bad_nodes == 24
diff --git a/tests/message_passing/waiting_for_handlers/waiting_for_handlers_test.py b/tests/message_passing/waiting_for_handlers/waiting_for_handlers_test.py
new file mode 100644
index 00000000..e6d200e3
--- /dev/null
+++ b/tests/message_passing/waiting_for_handlers/waiting_for_handlers_test.py
@@ -0,0 +1,80 @@
+from enum import Enum
+
+import pytest
+from temporalio import client, worker
+from temporalio.testing import WorkflowEnvironment
+
+from message_passing.waiting_for_handlers import WorkflowExitType, WorkflowInput
+from message_passing.waiting_for_handlers.activities import (
+ activity_executed_by_update_handler,
+)
+from message_passing.waiting_for_handlers.starter import TASK_QUEUE
+from message_passing.waiting_for_handlers.workflows import WaitingForHandlersWorkflow
+
+
+class UpdateExpect(Enum):
+ SUCCESS = "success"
+ FAILURE = "failure"
+
+
+class WorkflowExpect(Enum):
+ SUCCESS = "success"
+ FAILURE = "failure"
+
+
+@pytest.mark.parametrize(
+ ["exit_type_name", "update_expect", "workflow_expect"],
+ [
+ (WorkflowExitType.SUCCESS.name, UpdateExpect.SUCCESS, WorkflowExpect.SUCCESS),
+ (WorkflowExitType.FAILURE.name, UpdateExpect.SUCCESS, WorkflowExpect.FAILURE),
+ (
+ WorkflowExitType.CANCELLATION.name,
+ UpdateExpect.SUCCESS,
+ WorkflowExpect.FAILURE,
+ ),
+ ],
+)
+async def test_waiting_for_handlers(
+ env: WorkflowEnvironment,
+ exit_type_name: str,
+ update_expect: UpdateExpect,
+ workflow_expect: WorkflowExpect,
+):
+ [exit_type] = [t for t in WorkflowExitType if t.name == exit_type_name]
+ if env.supports_time_skipping:
+ pytest.skip(
+ "Java test server: https://github.com/temporalio/sdk-java/issues/1903"
+ )
+ async with worker.Worker(
+ env.client,
+ task_queue=TASK_QUEUE,
+ workflows=[WaitingForHandlersWorkflow],
+ activities=[
+ activity_executed_by_update_handler,
+ ],
+ ):
+ wf_handle = await env.client.start_workflow(
+ WaitingForHandlersWorkflow.run,
+ WorkflowInput(exit_type=exit_type),
+ id="waiting-for-handlers-test",
+ task_queue=TASK_QUEUE,
+ )
+ up_handle = await wf_handle.start_update(
+ WaitingForHandlersWorkflow.my_update,
+ wait_for_stage=client.WorkflowUpdateStage.ACCEPTED,
+ )
+
+ if exit_type == WorkflowExitType.CANCELLATION:
+ await wf_handle.cancel()
+
+ if update_expect == UpdateExpect.SUCCESS:
+ await up_handle.result()
+ else:
+ with pytest.raises(client.WorkflowUpdateFailedError):
+ await up_handle.result()
+
+ if workflow_expect == WorkflowExpect.SUCCESS:
+ await wf_handle.result()
+ else:
+ with pytest.raises(client.WorkflowFailureError):
+ await wf_handle.result()
diff --git a/tests/message_passing/waiting_for_handlers_and_compensation/waiting_for_handlers_and_compensation_test.py b/tests/message_passing/waiting_for_handlers_and_compensation/waiting_for_handlers_and_compensation_test.py
new file mode 100644
index 00000000..2b10d396
--- /dev/null
+++ b/tests/message_passing/waiting_for_handlers_and_compensation/waiting_for_handlers_and_compensation_test.py
@@ -0,0 +1,106 @@
+import uuid
+from enum import Enum
+
+import pytest
+from temporalio import client, worker
+from temporalio.testing import WorkflowEnvironment
+
+from message_passing.waiting_for_handlers_and_compensation import (
+ WorkflowExitType,
+ WorkflowInput,
+)
+from message_passing.waiting_for_handlers_and_compensation.activities import (
+ activity_executed_by_update_handler,
+ activity_executed_by_update_handler_to_perform_compensation,
+ activity_executed_to_perform_workflow_compensation,
+)
+from message_passing.waiting_for_handlers_and_compensation.starter import TASK_QUEUE
+from message_passing.waiting_for_handlers_and_compensation.workflows import (
+ WaitingForHandlersAndCompensationWorkflow,
+)
+
+
+class UpdateExpect(Enum):
+ SUCCESS = "success"
+ FAILURE = "failure"
+
+
+class WorkflowExpect(Enum):
+ SUCCESS = "success"
+ FAILURE = "failure"
+
+
+@pytest.mark.parametrize(
+ ["exit_type_name", "update_expect", "workflow_expect"],
+ [
+ (WorkflowExitType.SUCCESS.name, UpdateExpect.SUCCESS, WorkflowExpect.SUCCESS),
+ (WorkflowExitType.FAILURE.name, UpdateExpect.FAILURE, WorkflowExpect.FAILURE),
+ (
+ WorkflowExitType.CANCELLATION.name,
+ UpdateExpect.FAILURE,
+ WorkflowExpect.FAILURE,
+ ),
+ ],
+)
+async def test_waiting_for_handlers_and_compensation(
+ env: WorkflowEnvironment,
+ exit_type_name: str,
+ update_expect: UpdateExpect,
+ workflow_expect: WorkflowExpect,
+):
+ [exit_type] = [t for t in WorkflowExitType if t.name == exit_type_name]
+ if env.supports_time_skipping:
+ pytest.skip(
+ "Java test server: https://github.com/temporalio/sdk-java/issues/1903"
+ )
+ async with worker.Worker(
+ env.client,
+ task_queue=TASK_QUEUE,
+ workflows=[WaitingForHandlersAndCompensationWorkflow],
+ activities=[
+ activity_executed_by_update_handler,
+ activity_executed_by_update_handler_to_perform_compensation,
+ activity_executed_to_perform_workflow_compensation,
+ ],
+ ):
+ wf_handle = await env.client.start_workflow(
+ WaitingForHandlersAndCompensationWorkflow.run,
+ WorkflowInput(exit_type=exit_type),
+ id=str(uuid.uuid4()),
+ task_queue=TASK_QUEUE,
+ )
+ up_handle = await wf_handle.start_update(
+ WaitingForHandlersAndCompensationWorkflow.my_update,
+ wait_for_stage=client.WorkflowUpdateStage.ACCEPTED,
+ )
+
+ if exit_type == WorkflowExitType.CANCELLATION:
+ await wf_handle.cancel()
+
+ if update_expect == UpdateExpect.SUCCESS:
+ await up_handle.result()
+ assert not (
+ await wf_handle.query(
+ WaitingForHandlersAndCompensationWorkflow.update_compensation_done
+ )
+ )
+ else:
+ with pytest.raises(client.WorkflowUpdateFailedError):
+ await up_handle.result()
+ assert await wf_handle.query(
+ WaitingForHandlersAndCompensationWorkflow.update_compensation_done
+ )
+
+ if workflow_expect == WorkflowExpect.SUCCESS:
+ await wf_handle.result()
+ assert not (
+ await wf_handle.query(
+ WaitingForHandlersAndCompensationWorkflow.workflow_compensation_done
+ )
+ )
+ else:
+ with pytest.raises(client.WorkflowFailureError):
+ await wf_handle.result()
+ assert await wf_handle.query(
+ WaitingForHandlersAndCompensationWorkflow.workflow_compensation_done
+ )
diff --git a/tests/nexus_multiple_args/__init__.py b/tests/nexus_multiple_args/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/nexus_multiple_args/nexus_multiple_args_test.py b/tests/nexus_multiple_args/nexus_multiple_args_test.py
new file mode 100644
index 00000000..682b8f20
--- /dev/null
+++ b/tests/nexus_multiple_args/nexus_multiple_args_test.py
@@ -0,0 +1,50 @@
+import asyncio
+import sys
+
+import pytest
+from temporalio.client import Client
+from temporalio.testing import WorkflowEnvironment
+
+import nexus_multiple_args.caller.app
+import nexus_multiple_args.caller.workflows
+import nexus_multiple_args.handler.worker
+from tests.helpers.nexus import create_nexus_endpoint, delete_nexus_endpoint
+
+
+async def test_nexus_multiple_args(client: Client, env: WorkflowEnvironment):
+ if env.supports_time_skipping:
+ pytest.skip("Nexus tests don't work under the Java test server")
+
+ if sys.version_info[:2] < (3, 10):
+ pytest.skip("Sample is written for Python >= 3.10")
+
+ create_response = await create_nexus_endpoint(
+ name=nexus_multiple_args.caller.workflows.NEXUS_ENDPOINT,
+ task_queue=nexus_multiple_args.handler.worker.TASK_QUEUE,
+ client=client,
+ )
+ try:
+ handler_worker_task = asyncio.create_task(
+ nexus_multiple_args.handler.worker.main(
+ client,
+ )
+ )
+ await asyncio.sleep(1)
+ results = await nexus_multiple_args.caller.app.execute_caller_workflow(
+ client,
+ )
+ nexus_multiple_args.handler.worker.interrupt_event.set()
+ await handler_worker_task
+ nexus_multiple_args.handler.worker.interrupt_event.clear()
+
+ # Verify the expected output messages
+ assert results == (
+ "Hello Nexus 👋",
+ "¡Hola! Nexus 👋",
+ )
+ finally:
+ await delete_nexus_endpoint(
+ id=create_response.endpoint.id,
+ version=create_response.endpoint.version,
+ client=client,
+ )
diff --git a/tests/nexus_sync_operations/nexus_sync_operations_test.py b/tests/nexus_sync_operations/nexus_sync_operations_test.py
new file mode 100644
index 00000000..d74168cb
--- /dev/null
+++ b/tests/nexus_sync_operations/nexus_sync_operations_test.py
@@ -0,0 +1,118 @@
+import asyncio
+import uuid
+from typing import Type
+
+import pytest
+from temporalio import workflow
+from temporalio.client import Client
+from temporalio.testing import WorkflowEnvironment
+from temporalio.worker import Worker
+
+import nexus_sync_operations.handler.service_handler
+import nexus_sync_operations.handler.worker
+from message_passing.introduction import Language
+from message_passing.introduction.workflows import GetLanguagesInput, SetLanguageInput
+from nexus_sync_operations.caller.workflows import CallerWorkflow
+from tests.helpers.nexus import create_nexus_endpoint, delete_nexus_endpoint
+
+with workflow.unsafe.imports_passed_through():
+ from nexus_sync_operations.service import GreetingService
+
+
+NEXUS_ENDPOINT = "nexus-sync-operations-nexus-endpoint"
+
+
+@workflow.defn
+class TestCallerWorkflow:
+ """Test workflow that calls Nexus operations and makes assertions."""
+
+ @workflow.run
+ async def run(self) -> None:
+ nexus_client = workflow.create_nexus_client(
+ service=GreetingService,
+ endpoint=NEXUS_ENDPOINT,
+ )
+
+ supported_languages = await nexus_client.execute_operation(
+ GreetingService.get_languages, GetLanguagesInput(include_unsupported=False)
+ )
+ assert supported_languages == [Language.CHINESE, Language.ENGLISH]
+
+ initial_language = await nexus_client.execute_operation(
+ GreetingService.get_language, None
+ )
+ assert initial_language == Language.ENGLISH
+
+ previous_language = await nexus_client.execute_operation(
+ GreetingService.set_language,
+ SetLanguageInput(language=Language.CHINESE),
+ )
+ assert previous_language == Language.ENGLISH
+
+ current_language = await nexus_client.execute_operation(
+ GreetingService.get_language, None
+ )
+ assert current_language == Language.CHINESE
+
+ previous_language = await nexus_client.execute_operation(
+ GreetingService.set_language,
+ SetLanguageInput(language=Language.ARABIC),
+ )
+ assert previous_language == Language.CHINESE
+
+ current_language = await nexus_client.execute_operation(
+ GreetingService.get_language, None
+ )
+ assert current_language == Language.ARABIC
+
+
+async def test_nexus_sync_operations(client: Client, env: WorkflowEnvironment):
+ if env.supports_time_skipping:
+ pytest.skip("Nexus tests don't work under the Java test server")
+
+ await _run_caller_workflow(client, TestCallerWorkflow)
+
+
+async def test_nexus_sync_operations_caller_workflow(
+ client: Client, env: WorkflowEnvironment
+):
+ """
+ Runs the CallerWorkflow from the sample to ensure it executes without errors.
+ """
+ if env.supports_time_skipping:
+ pytest.skip("Nexus tests don't work under the Java test server")
+
+ await _run_caller_workflow(client, CallerWorkflow)
+
+
+async def _run_caller_workflow(client: Client, workflow: Type):
+ create_response = await create_nexus_endpoint(
+ name=NEXUS_ENDPOINT,
+ task_queue=nexus_sync_operations.handler.worker.TASK_QUEUE,
+ client=client,
+ )
+ try:
+ handler_worker_task = asyncio.create_task(
+ nexus_sync_operations.handler.worker.main(client)
+ )
+ try:
+ async with Worker(
+ client,
+ task_queue="test-caller-task-queue",
+ workflows=[workflow],
+ ):
+ await client.execute_workflow(
+ workflow.run,
+ id=str(uuid.uuid4()),
+ task_queue="test-caller-task-queue",
+ )
+ finally:
+ nexus_sync_operations.handler.worker.interrupt_event.set()
+ await handler_worker_task
+ nexus_sync_operations.handler.worker.interrupt_event.clear()
+ finally:
+ await delete_nexus_endpoint(
+ id=create_response.endpoint.id,
+ version=create_response.endpoint.version,
+ client=client,
+ )
diff --git a/tests/polling/infrequent/__init__.py b/tests/polling/infrequent/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/polling/infrequent/workflow_test.py b/tests/polling/infrequent/workflow_test.py
new file mode 100644
index 00000000..31f3f987
--- /dev/null
+++ b/tests/polling/infrequent/workflow_test.py
@@ -0,0 +1,31 @@
+import uuid
+
+import pytest
+from temporalio.client import Client
+from temporalio.testing import WorkflowEnvironment
+from temporalio.worker import Worker
+
+from polling.infrequent.activities import compose_greeting
+from polling.infrequent.workflows import GreetingWorkflow
+
+
+async def test_infrequent_polling_workflow(client: Client, env: WorkflowEnvironment):
+ if not env.supports_time_skipping:
+ pytest.skip("Too slow to test with time-skipping disabled")
+
+ # Start a worker that hosts the workflow and activity implementations.
+ task_queue = f"tq-{uuid.uuid4()}"
+ async with Worker(
+ client,
+ task_queue=task_queue,
+ workflows=[GreetingWorkflow],
+ activities=[compose_greeting],
+ ):
+ handle = await client.start_workflow(
+ GreetingWorkflow.run,
+ "Temporal",
+ id=f"infrequent-polling-{uuid.uuid4()}",
+ task_queue=task_queue,
+ )
+ result = await handle.result()
+ assert result == "Hello, Temporal!"
diff --git a/tests/pydantic_converter/workflow_test.py b/tests/pydantic_converter/workflow_test.py
index a547caf0..c673164e 100644
--- a/tests/pydantic_converter/workflow_test.py
+++ b/tests/pydantic_converter/workflow_test.py
@@ -3,15 +3,10 @@
from ipaddress import IPv4Address
from temporalio.client import Client
+from temporalio.contrib.pydantic import pydantic_data_converter
from temporalio.worker import Worker
-from pydantic_converter.converter import pydantic_data_converter
-from pydantic_converter.worker import (
- MyPydanticModel,
- MyWorkflow,
- my_activity,
- new_sandbox_runner,
-)
+from pydantic_converter.worker import MyPydanticModel, MyWorkflow, my_activity
async def test_workflow_with_pydantic_model(client: Client):
@@ -35,7 +30,6 @@ async def test_workflow_with_pydantic_model(client: Client):
task_queue=task_queue_name,
workflows=[MyWorkflow],
activities=[my_activity],
- workflow_runner=new_sandbox_runner(),
):
result = await client.execute_workflow(
MyWorkflow.run,
diff --git a/tests/pydantic_converter_v1/__init__.py b/tests/pydantic_converter_v1/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/pydantic_converter_v1/workflow_test.py b/tests/pydantic_converter_v1/workflow_test.py
new file mode 100644
index 00000000..fd0af800
--- /dev/null
+++ b/tests/pydantic_converter_v1/workflow_test.py
@@ -0,0 +1,46 @@
+import uuid
+from datetime import datetime
+from ipaddress import IPv4Address
+
+from temporalio.client import Client
+from temporalio.worker import Worker
+
+from pydantic_converter_v1.converter import pydantic_data_converter
+from pydantic_converter_v1.worker import (
+ MyPydanticModel,
+ MyWorkflow,
+ my_activity,
+ new_sandbox_runner,
+)
+
+
+async def test_workflow_with_pydantic_model(client: Client):
+ # Replace data converter in client
+ new_config = client.config()
+ new_config["data_converter"] = pydantic_data_converter
+ client = Client(**new_config)
+ task_queue_name = str(uuid.uuid4())
+
+ orig_models = [
+ MyPydanticModel(
+ some_ip=IPv4Address("127.0.0.1"), some_date=datetime(2000, 1, 2, 3, 4, 5)
+ ),
+ MyPydanticModel(
+ some_ip=IPv4Address("127.0.0.2"), some_date=datetime(2001, 2, 3, 4, 5, 6)
+ ),
+ ]
+
+ async with Worker(
+ client,
+ task_queue=task_queue_name,
+ workflows=[MyWorkflow],
+ activities=[my_activity],
+ workflow_runner=new_sandbox_runner(),
+ ):
+ result = await client.execute_workflow(
+ MyWorkflow.run,
+ orig_models,
+ id=str(uuid.uuid4()),
+ task_queue=task_queue_name,
+ )
+ assert orig_models == result
diff --git a/tests/resource_pool/__init__.py b/tests/resource_pool/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/resource_pool/workflow_test.py b/tests/resource_pool/workflow_test.py
new file mode 100644
index 00000000..0d2eb654
--- /dev/null
+++ b/tests/resource_pool/workflow_test.py
@@ -0,0 +1,120 @@
+import asyncio
+from collections import defaultdict
+from typing import Any, Optional, Sequence
+
+from temporalio import activity
+from temporalio.client import Client, WorkflowFailureError, WorkflowHandle
+from temporalio.common import WorkflowIDConflictPolicy
+from temporalio.worker import Worker
+
+from resource_pool.pool_client.resource_pool_workflow import (
+ ResourcePoolWorkflow,
+ ResourcePoolWorkflowInput,
+)
+from resource_pool.resource_user_workflow import (
+ ResourceUserWorkflow,
+ ResourceUserWorkflowInput,
+ UseResourceActivityInput,
+)
+from resource_pool.shared import RESOURCE_POOL_WORKFLOW_ID
+
+TASK_QUEUE = "resource_pool-task-queue"
+
+
+async def test_resource_pool_workflow(client: Client):
+ # key is resource, value is a description of resource usage
+ resource_usage: defaultdict[str, list[Sequence[str]]] = defaultdict(list)
+
+ # Mock out the activity to count executions
+ @activity.defn(name="use_resource")
+ async def use_resource_mock(input: UseResourceActivityInput) -> None:
+ workflow_id = activity.info().workflow_id or ""
+ resource_usage[input.resource].append((workflow_id, "start"))
+ # We need a small sleep here to bait out races
+ await asyncio.sleep(0.05)
+ resource_usage[input.resource].append((workflow_id, "end"))
+
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[ResourcePoolWorkflow, ResourceUserWorkflow],
+ activities=[use_resource_mock],
+ ):
+ await run_all_workflows(client)
+
+ # Did any workflow run in more than one place?
+ workflow_id_to_resource: dict[str, str] = {}
+ for resource, events in resource_usage.items():
+ for workflow_id, event in events:
+ if workflow_id in workflow_id_to_resource:
+ existing_resource = workflow_id_to_resource[workflow_id]
+ assert (
+ existing_resource == resource
+ ), f"{workflow_id} ran on both {resource} and {existing_resource}"
+ else:
+ workflow_id_to_resource[workflow_id] = resource
+
+ # Did any resource have more than one workflow on it at a time?
+ for resource, events in resource_usage.items():
+ holder: Optional[str] = None
+ for workflow_id, event in events:
+ if event == "start":
+ assert (
+ holder is None
+ ), f"{workflow_id} started on {resource} held by {holder}"
+ holder = workflow_id
+ else:
+ assert (
+ holder == workflow_id
+ ), f"{workflow_id} ended on {resource} held by {holder}"
+ holder = None
+
+ # Are all the resources free, per the query?
+ handle: WorkflowHandle[ResourcePoolWorkflow, None] = (
+ client.get_workflow_handle_for(
+ ResourcePoolWorkflow.run, RESOURCE_POOL_WORKFLOW_ID
+ )
+ )
+ query_result = await handle.query(ResourcePoolWorkflow.get_current_holders)
+ assert query_result == {"r_a": None, "r_b": None, "r_c": None}
+
+
+async def run_all_workflows(client: Client):
+ resource_pool_handle = await client.start_workflow(
+ workflow=ResourcePoolWorkflow.run,
+ arg=ResourcePoolWorkflowInput(
+ resources={"r_a": None, "r_b": None, "r_c": None},
+ waiters=[],
+ ),
+ id=RESOURCE_POOL_WORKFLOW_ID,
+ task_queue=TASK_QUEUE,
+ id_conflict_policy=WorkflowIDConflictPolicy.USE_EXISTING,
+ )
+
+ resource_user_handles: list[WorkflowHandle[Any, Any]] = []
+ for i in range(0, 8):
+ input = ResourceUserWorkflowInput(
+ resource_pool_workflow_id=RESOURCE_POOL_WORKFLOW_ID,
+ iteration_to_fail_after=None,
+ should_continue_as_new=False,
+ )
+ if i == 0:
+ input.should_continue_as_new = True
+ if i == 1:
+ input.iteration_to_fail_after = "first"
+
+ handle = await client.start_workflow(
+ workflow=ResourceUserWorkflow.run,
+ arg=input,
+ id=f"resource-user-workflow-{i}",
+ task_queue=TASK_QUEUE,
+ )
+ resource_user_handles.append(handle)
+
+ for handle in resource_user_handles:
+ try:
+ await handle.result()
+ except WorkflowFailureError:
+ pass
+
+ await resource_pool_handle.terminate()
diff --git a/tests/sentry/fake_sentry_transport.py b/tests/sentry/fake_sentry_transport.py
new file mode 100644
index 00000000..7d1b087d
--- /dev/null
+++ b/tests/sentry/fake_sentry_transport.py
@@ -0,0 +1,17 @@
+import sentry_sdk
+import sentry_sdk.types
+
+
+class FakeSentryTransport:
+ """A fake transport that captures Sentry events in memory"""
+
+ # Note: we could extend from sentry_sdk.transport.Transport
+ # but `sentry_sdk.init` also takes a simple callable that takes
+ # an Event rather than a serialised Envelope object, so testing
+ # is easier.
+
+ def __init__(self):
+ self.events: list[sentry_sdk.types.Event] = []
+
+ def __call__(self, event: sentry_sdk.types.Event) -> None:
+ self.events.append(event)
diff --git a/tests/sentry/test_interceptor.py b/tests/sentry/test_interceptor.py
new file mode 100644
index 00000000..f9f931e8
--- /dev/null
+++ b/tests/sentry/test_interceptor.py
@@ -0,0 +1,164 @@
+import sys
+import unittest.mock
+from collections import abc
+
+import pytest
+
+if sys.version_info >= (3, 14):
+ pytest.skip(
+ "Sentry does not support Python 3.14 yet.",
+ allow_module_level=True,
+ )
+
+import sentry_sdk
+import temporalio.activity
+import temporalio.workflow
+from sentry_sdk.integrations.asyncio import AsyncioIntegration
+from temporalio.client import Client
+from temporalio.worker import Worker
+from temporalio.worker.workflow_sandbox import (
+ SandboxedWorkflowRunner,
+ SandboxRestrictions,
+)
+
+from sentry.activity import broken_activity, working_activity
+from sentry.interceptor import SentryInterceptor
+from sentry.workflow import SentryExampleWorkflow, SentryExampleWorkflowInput
+from tests.sentry.fake_sentry_transport import FakeSentryTransport
+
+
+@pytest.fixture
+def transport() -> FakeSentryTransport:
+ """Fixture to provide a fake transport for Sentry SDK."""
+ return FakeSentryTransport()
+
+
+@pytest.fixture(autouse=True)
+def sentry_init(transport: FakeSentryTransport) -> None:
+ """Initialize Sentry for testing."""
+ sentry_sdk.init(
+ transport=transport,
+ integrations=[
+ AsyncioIntegration(),
+ ],
+ )
+
+
+@pytest.fixture
+async def worker(client: Client) -> abc.AsyncIterator[Worker]:
+ """Fixture to provide a worker for testing."""
+ async with Worker(
+ client,
+ task_queue="sentry-task-queue",
+ workflows=[SentryExampleWorkflow],
+ activities=[broken_activity, working_activity],
+ interceptors=[SentryInterceptor()],
+ workflow_runner=SandboxedWorkflowRunner(
+ restrictions=SandboxRestrictions.default.with_passthrough_modules(
+ "sentry_sdk"
+ )
+ ),
+ ) as worker:
+ yield worker
+
+
+async def test_sentry_interceptor_reports_no_errors_when_workflow_succeeds(
+ client: Client, worker: Worker, transport: FakeSentryTransport
+) -> None:
+ """Test that Sentry interceptor reports no errors when workflow succeeds."""
+ # WHEN
+ try:
+ await client.execute_workflow(
+ SentryExampleWorkflow.run,
+ SentryExampleWorkflowInput(option="working"),
+ id="sentry-workflow-id",
+ task_queue=worker.task_queue,
+ )
+ except Exception:
+ pytest.fail("Workflow should not raise an exception")
+
+ # THEN
+ assert len(transport.events) == 0, "No events should be captured"
+
+
+async def test_sentry_interceptor_captures_errors(
+ client: Client, worker: Worker, transport: FakeSentryTransport
+) -> None:
+ """Test that errors are captured with correct Sentry metadata."""
+ # WHEN
+ try:
+ await client.execute_workflow(
+ SentryExampleWorkflow.run,
+ SentryExampleWorkflowInput(option="broken"),
+ id="sentry-workflow-id",
+ task_queue=worker.task_queue,
+ )
+ pytest.fail("Workflow should raise an exception")
+ except Exception:
+ pass
+
+ # THEN
+ # there should be two events: one for the failed activity and one for the failed workflow
+ assert len(transport.events) == 2, "Two events should be captured"
+
+ # Check the first event - should be the activity exception
+ # --------------------------------------------------------
+ event = transport.events[0]
+
+ # Check exception was captured
+ assert event["exception"]["values"][0]["type"] == "Exception"
+ assert event["exception"]["values"][0]["value"] == "Activity failed!"
+
+ # Check useful metadata were captured as tags
+ assert event["tags"] == {
+ "temporal.execution_type": "activity",
+ "module": "sentry.activity.broken_activity",
+ "temporal.workflow.type": "SentryExampleWorkflow",
+ "temporal.workflow.id": "sentry-workflow-id",
+ "temporal.activity.id": "1",
+ "temporal.activity.type": "broken_activity",
+ "temporal.activity.task_queue": "sentry-task-queue",
+ "temporal.workflow.namespace": "default",
+ "temporal.workflow.run_id": unittest.mock.ANY,
+ }
+
+ # Check activity input was captured as context
+ assert event["contexts"]["temporal.activity.input"] == {
+ "message": "Hello, Temporal!",
+ }
+
+ # Check activity info was captured as context
+ activity_info = temporalio.activity.Info(
+ **event["contexts"]["temporal.activity.info"] # type: ignore
+ )
+ assert activity_info.activity_type == "broken_activity"
+
+ # Check the second event - should be the workflow exception
+ # ---------------------------------------------------------
+ event = transport.events[1]
+
+ # Check exception was captured
+ assert event["exception"]["values"][0]["type"] == "ApplicationError"
+ assert event["exception"]["values"][0]["value"] == "Activity failed!"
+
+ # Check useful metadata were captured as tags
+ assert event["tags"] == {
+ "temporal.execution_type": "workflow",
+ "module": "sentry.workflow.SentryExampleWorkflow.run",
+ "temporal.workflow.type": "SentryExampleWorkflow",
+ "temporal.workflow.id": "sentry-workflow-id",
+ "temporal.workflow.task_queue": "sentry-task-queue",
+ "temporal.workflow.namespace": "default",
+ "temporal.workflow.run_id": unittest.mock.ANY,
+ }
+
+ # Check workflow input was captured as context
+ assert event["contexts"]["temporal.workflow.input"] == {
+ "option": "broken",
+ }
+
+ # Check workflow info was captured as context
+ workflow_info = temporalio.workflow.Info(
+ **event["contexts"]["temporal.workflow.info"] # type: ignore
+ )
+ assert workflow_info.workflow_type == "SentryExampleWorkflow"
diff --git a/tests/sleep_for_days/__init__.py b/tests/sleep_for_days/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/sleep_for_days/workflow_test.py b/tests/sleep_for_days/workflow_test.py
new file mode 100644
index 00000000..fa3f0686
--- /dev/null
+++ b/tests/sleep_for_days/workflow_test.py
@@ -0,0 +1,53 @@
+import uuid
+from datetime import timedelta
+
+from temporalio import activity
+from temporalio.testing import WorkflowEnvironment
+from temporalio.worker import Worker
+
+from sleep_for_days.starter import TASK_QUEUE
+from sleep_for_days.workflows import SendEmailInput, SleepForDaysWorkflow
+
+
+async def test_sleep_for_days_workflow():
+ num_activity_executions = 0
+
+ # Mock out the activity to count executions
+ @activity.defn(name="send_email")
+ async def send_email_mock(input: SendEmailInput) -> str:
+ nonlocal num_activity_executions
+ num_activity_executions += 1
+ return input.email_msg
+
+ async with await WorkflowEnvironment.start_time_skipping() as env:
+ # if env.supports_time_skipping:
+ # pytest.skip(
+ # "Java test server: https://github.com/temporalio/sdk-java/issues/1903"
+ # )
+ async with Worker(
+ env.client,
+ task_queue=TASK_QUEUE,
+ workflows=[SleepForDaysWorkflow],
+ activities=[send_email_mock],
+ ):
+ handle = await env.client.start_workflow(
+ SleepForDaysWorkflow.run,
+ id=str(uuid.uuid4()),
+ task_queue=TASK_QUEUE,
+ )
+
+ start_time = await env.get_current_time()
+ # Time-skip 5 minutes.
+ await env.sleep(timedelta(minutes=5))
+ # Check that the activity has been called, we're now waiting for the sleep to finish.
+ assert num_activity_executions == 1
+ # Time-skip 3 days.
+ await env.sleep(timedelta(days=90))
+ # Expect 3 more activity calls.
+ assert num_activity_executions == 4
+ # Send the signal to complete the workflow.
+ await handle.signal(SleepForDaysWorkflow.complete)
+ # Expect no more activity calls to have been made - workflow is complete.
+ assert num_activity_executions == 4
+ # Expect more than 90 days to have passed.
+ assert (await env.get_current_time() - start_time) > timedelta(days=90)
diff --git a/tests/trio_async/__init__.py b/tests/trio_async/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/trio_async/workflow_test.py b/tests/trio_async/workflow_test.py
new file mode 100644
index 00000000..15d678aa
--- /dev/null
+++ b/tests/trio_async/workflow_test.py
@@ -0,0 +1,51 @@
+import sys
+import uuid
+
+import pytest
+
+if sys.version_info >= (3, 14):
+ pytest.skip("trio-asyncio not supported on Python 3.14+", allow_module_level=True)
+
+
+import trio_asyncio
+from temporalio.client import Client
+from temporalio.worker import Worker
+
+from trio_async import activities, workflows
+
+
+async def test_workflow_with_trio(client: Client):
+ @trio_asyncio.aio_as_trio
+ async def inside_trio(client: Client) -> list[str]:
+ # Create Trio thread executor
+ with trio_asyncio.TrioExecutor(max_workers=200) as thread_executor:
+ task_queue = f"tq-{uuid.uuid4()}"
+ # Run worker
+ async with Worker(
+ client,
+ task_queue=task_queue,
+ activities=[
+ activities.say_hello_activity_async,
+ activities.say_hello_activity_sync,
+ ],
+ workflows=[workflows.SayHelloWorkflow],
+ activity_executor=thread_executor,
+ workflow_task_executor=thread_executor,
+ ):
+ # Run workflow and return result
+ return await client.execute_workflow(
+ workflows.SayHelloWorkflow.run,
+ "some-user",
+ id=f"wf-{uuid.uuid4()}",
+ task_queue=task_queue,
+ )
+
+ if sys.version_info[:2] < (3, 12):
+ pytest.skip("Trio support requires >= 3.12")
+
+ result = trio_asyncio.run(inside_trio, client)
+
+ assert result == [
+ "Hello, some-user! (from asyncio)",
+ "Hello, some-user! (from thread)",
+ ]
diff --git a/tests/updatable_timer/__init__.py b/tests/updatable_timer/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/updatable_timer/updatable_timer_test.py b/tests/updatable_timer/updatable_timer_test.py
new file mode 100644
index 00000000..1f3d8ae0
--- /dev/null
+++ b/tests/updatable_timer/updatable_timer_test.py
@@ -0,0 +1,33 @@
+import datetime
+import logging
+import math
+import uuid
+
+from temporalio.client import Client, WorkflowExecutionStatus
+from temporalio.testing import WorkflowEnvironment
+from temporalio.worker import Worker
+
+from updatable_timer.workflow import Workflow
+
+
+async def test_updatable_timer_workflow():
+ logging.basicConfig(level=logging.DEBUG)
+
+ task_queue_name = str(uuid.uuid4())
+ async with await WorkflowEnvironment.start_time_skipping() as env:
+ async with Worker(env.client, task_queue=task_queue_name, workflows=[Workflow]):
+ in_a_day = float(
+ (datetime.datetime.now() + datetime.timedelta(days=1)).timestamp()
+ )
+ in_an_hour = float(
+ (datetime.datetime.now() + datetime.timedelta(hours=1)).timestamp()
+ )
+ handle = await env.client.start_workflow(
+ Workflow.run, in_a_day, id=str(uuid.uuid4()), task_queue=task_queue_name
+ )
+ wake_up_time1 = await handle.query(Workflow.get_wake_up_time)
+ assert math.isclose(wake_up_time1, in_a_day)
+ await handle.signal(Workflow.update_wake_up_time, in_an_hour)
+ wake_up_time2 = await handle.query(Workflow.get_wake_up_time)
+ assert math.isclose(wake_up_time2, in_an_hour)
+ await handle.result()
diff --git a/trio_async/README.md b/trio_async/README.md
new file mode 100644
index 00000000..dfa02eab
--- /dev/null
+++ b/trio_async/README.md
@@ -0,0 +1,25 @@
+# Trio Async Sample
+
+This sample shows how to use Temporal asyncio with [Trio](https://trio.readthedocs.io) using
+[Trio asyncio](https://trio-asyncio.readthedocs.io). Specifically it demonstrates using a traditional Temporal client
+and worker in a Trio setting, and how Trio-based code can run in both asyncio async activities and threaded sync
+activities.
+
+NOTE: This sample only works on Python versions < 3.14.
+
+For this sample, the optional `trio_async` dependency group must be included. To include, run:
+
+ uv sync --group trio_async
+
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from the root directory to start the
+worker:
+
+ uv run trio_async/worker.py
+
+This will start the worker. Then, in another terminal, run the following to execute the workflow:
+
+ uv run trio_async/starter.py
+
+The starter should complete with:
+
+ INFO:root:Workflow result: ['Hello, Temporal! (from asyncio)', 'Hello, Temporal! (from thread)']
\ No newline at end of file
diff --git a/trio_async/__init__.py b/trio_async/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/trio_async/activities.py b/trio_async/activities.py
new file mode 100644
index 00000000..c253c93e
--- /dev/null
+++ b/trio_async/activities.py
@@ -0,0 +1,51 @@
+import asyncio
+import time
+
+import trio
+import trio_asyncio
+from temporalio import activity
+
+
+# An asyncio-based async activity
+@activity.defn
+async def say_hello_activity_async(name: str) -> str:
+ # Demonstrate a sleep in both asyncio and Trio, showing that both asyncio
+ # and Trio primitives can be used
+
+ # First asyncio
+ activity.logger.info("Sleeping in asyncio")
+ await asyncio.sleep(0.1)
+
+ # Now Trio. We have to invoke the function separately decorated.
+ # We cannot use the @trio_as_aio decorator on the activity itself because
+ # it doesn't use functools wrap or similar so it doesn't respond to things
+ # like __name__ that @activity.defn needs.
+ return await say_hello_in_trio_from_asyncio(name)
+
+
+@trio_asyncio.trio_as_aio
+async def say_hello_in_trio_from_asyncio(name: str) -> str:
+ activity.logger.info("Sleeping in Trio (from asyncio)")
+ await trio.sleep(0.1)
+ return f"Hello, {name}! (from asyncio)"
+
+
+# A thread-based sync activity
+@activity.defn
+def say_hello_activity_sync(name: str) -> str:
+ # Demonstrate a sleep in both threaded and Trio, showing that both
+ # primitives can be used
+
+ # First, thread-blocking
+ activity.logger.info("Sleeping normally")
+ time.sleep(0.1)
+
+ # Now Trio. We have to use Trio's thread sync tools to run trio calls from
+ # a different thread.
+ return trio.from_thread.run(say_hello_in_trio_from_sync, name)
+
+
+async def say_hello_in_trio_from_sync(name: str) -> str:
+ activity.logger.info("Sleeping in Trio (from thread)")
+ await trio.sleep(0.1)
+ return f"Hello, {name}! (from thread)"
diff --git a/trio_async/starter.py b/trio_async/starter.py
new file mode 100644
index 00000000..6535ca98
--- /dev/null
+++ b/trio_async/starter.py
@@ -0,0 +1,31 @@
+import logging
+
+import trio_asyncio
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from trio_async import workflows
+
+
+@trio_asyncio.aio_as_trio # Note this decorator which allows asyncio primitives
+async def main():
+ logging.basicConfig(level=logging.INFO)
+
+ # Connect client
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Execute the workflow
+ result = await client.execute_workflow(
+ workflows.SayHelloWorkflow.run,
+ "Temporal",
+ id=f"trio-async-workflow-id",
+ task_queue="trio-async-task-queue",
+ )
+ logging.info(f"Workflow result: {result}")
+
+
+if __name__ == "__main__":
+ # Note how we're using Trio event loop, not asyncio
+ trio_asyncio.run(main)
diff --git a/trio_async/worker.py b/trio_async/worker.py
new file mode 100644
index 00000000..7cb9685f
--- /dev/null
+++ b/trio_async/worker.py
@@ -0,0 +1,68 @@
+import asyncio
+import logging
+import os
+import sys
+
+import trio_asyncio
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from trio_async import activities, workflows
+
+
+@trio_asyncio.aio_as_trio # Note this decorator which allows asyncio primitives
+async def main():
+ logging.basicConfig(level=logging.INFO)
+
+ # Connect client
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Temporal runs threaded activities and workflow tasks via run_in_executor.
+ # Due to how trio_asyncio works, you can only do run_in_executor with their
+ # specific executor. We make sure to give it 200 max since we are using it
+ # for both activities and workflow tasks and by default the worker supports
+ # 100 max concurrent activity tasks and 100 max concurrent workflow tasks.
+ with trio_asyncio.TrioExecutor(max_workers=200) as thread_executor:
+ # Run a worker for the workflow
+ async with Worker(
+ client,
+ task_queue="trio-async-task-queue",
+ activities=[
+ activities.say_hello_activity_async,
+ activities.say_hello_activity_sync,
+ ],
+ workflows=[workflows.SayHelloWorkflow],
+ activity_executor=thread_executor,
+ workflow_task_executor=thread_executor,
+ ):
+ # Wait until interrupted
+ logging.info("Worker started, ctrl+c to exit")
+ try:
+ await asyncio.Future()
+ except asyncio.CancelledError:
+ # Ignore, happens on ctrl+C
+ pass
+ finally:
+ logging.info("Shutting down")
+
+
+if __name__ == "__main__":
+ # Note how we're using Trio event loop, not asyncio
+ try:
+ trio_asyncio.run(main)
+ except KeyboardInterrupt:
+ # Ignore ctrl+c
+ pass
+ except BaseException as err:
+ # On Python 3.11+ Trio represents keyboard interrupt inside an exception
+ # group
+ is_interrupt = (
+ sys.version_info >= (3, 11)
+ and isinstance(err, BaseExceptionGroup)
+ and err.subgroup(KeyboardInterrupt)
+ )
+ if not is_interrupt:
+ raise
diff --git a/trio_async/workflows.py b/trio_async/workflows.py
new file mode 100644
index 00000000..adcb4761
--- /dev/null
+++ b/trio_async/workflows.py
@@ -0,0 +1,30 @@
+from datetime import timedelta
+
+from temporalio import workflow
+
+with workflow.unsafe.imports_passed_through():
+ from trio_async.activities import say_hello_activity_async, say_hello_activity_sync
+
+
+@workflow.defn
+class SayHelloWorkflow:
+ @workflow.run
+ async def run(self, name: str) -> list[str]:
+ # Workflows don't use default asyncio event loop or Trio, they use a
+ # custom event loop. Therefore Trio primitives should never be used in a
+ # workflow, only asyncio helpers (which delegate to the custom loop).
+ return [
+ # That these are two different activities for async or sync means
+ # nothing to the workflow, we just have both to demonstrate the
+ # activity side
+ await workflow.execute_activity(
+ say_hello_activity_async,
+ name,
+ start_to_close_timeout=timedelta(minutes=5),
+ ),
+ await workflow.execute_activity(
+ say_hello_activity_sync,
+ name,
+ start_to_close_timeout=timedelta(minutes=5),
+ ),
+ ]
diff --git a/updatable_timer/README.md b/updatable_timer/README.md
new file mode 100644
index 00000000..3b738db8
--- /dev/null
+++ b/updatable_timer/README.md
@@ -0,0 +1,42 @@
+# Updatable Timer Sample
+
+Demonstrates a helper class which relies on `workflow.wait_condition` to implement a blocking sleep that can be updated at any moment.
+
+The sample is composed of the three executables:
+
+* `worker.py` hosts the Workflow Executions.
+* `starter.py` starts Workflow Executions.
+* `wake_up_timer_updater.py` Signals the Workflow Execution with the new time to wake up.
+
+First start the Worker:
+
+```bash
+uv run updatable_timer/worker.py
+```
+Check the output of the Worker window. The expected output is:
+
+```
+Worker started, ctrl+c to exit
+```
+
+Then in a different terminal window start the Workflow Execution:
+
+```bash
+uv run updatable_timer/starter.py
+```
+Check the output of the Worker window. The expected output is:
+```
+Workflow started: run_id=...
+```
+
+Then run the updater as many times as you want to change timer to 10 seconds from now:
+
+```bash
+uv run updatable_timer/wake_up_time_updater.py
+```
+
+Check the output of the worker window. The expected output is:
+
+```
+Updated wake up time to 10 seconds from now
+```
\ No newline at end of file
diff --git a/updatable_timer/__init__.py b/updatable_timer/__init__.py
new file mode 100644
index 00000000..a5ee5055
--- /dev/null
+++ b/updatable_timer/__init__.py
@@ -0,0 +1 @@
+TASK_QUEUE = "updatable-timer"
diff --git a/updatable_timer/starter.py b/updatable_timer/starter.py
new file mode 100644
index 00000000..8ce0f4f9
--- /dev/null
+++ b/updatable_timer/starter.py
@@ -0,0 +1,36 @@
+import asyncio
+import logging
+from datetime import datetime, timedelta
+from typing import Optional
+
+from temporalio import exceptions
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from updatable_timer import TASK_QUEUE
+from updatable_timer.workflow import Workflow
+
+
+async def main(client: Optional[Client] = None):
+ logging.basicConfig(level=logging.INFO)
+
+ if not client:
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+ try:
+ handle = await client.start_workflow(
+ Workflow.run,
+ (datetime.now() + timedelta(days=1)).timestamp(),
+ id=f"updatable-timer-workflow",
+ task_queue=TASK_QUEUE,
+ )
+ logging.info(f"Workflow started: run_id={handle.result_run_id}")
+ except exceptions.WorkflowAlreadyStartedError as e:
+ logging.info(
+ f"Workflow already running: workflow_id={e.workflow_id}, run_id={e.run_id}"
+ )
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/updatable_timer/updatable_timer_lib.py b/updatable_timer/updatable_timer_lib.py
new file mode 100644
index 00000000..b90ae868
--- /dev/null
+++ b/updatable_timer/updatable_timer_lib.py
@@ -0,0 +1,39 @@
+import asyncio
+from datetime import datetime, timedelta
+
+from temporalio import workflow
+
+
+class UpdatableTimer:
+ def __init__(self, wake_up_time: datetime) -> None:
+ self.wake_up_time = wake_up_time
+ self.wake_up_time_updated = False
+
+ async def sleep(self) -> None:
+ workflow.logger.info(f"sleep_until: {self.wake_up_time}")
+ while True:
+ now = workflow.now()
+
+ sleep_interval = self.wake_up_time - now
+ if sleep_interval <= timedelta(0):
+ break
+ workflow.logger.info(f"Going to sleep for {sleep_interval}")
+
+ try:
+ self.wake_up_time_updated = False
+ await workflow.wait_condition(
+ lambda: self.wake_up_time_updated,
+ timeout=sleep_interval,
+ )
+ except asyncio.TimeoutError:
+ # checks condition at the beginning of the loop
+ continue
+ workflow.logger.info(f"sleep_until completed")
+
+ def update_wake_up_time(self, wake_up_time: datetime) -> None:
+ workflow.logger.info(f"update_wake_up_time: {wake_up_time}")
+ self.wake_up_time = wake_up_time
+ self.wake_up_time_updated = True
+
+ def get_wake_up_time(self) -> datetime:
+ return self.wake_up_time
diff --git a/updatable_timer/wake_up_time_updater.py b/updatable_timer/wake_up_time_updater.py
new file mode 100644
index 00000000..43d24838
--- /dev/null
+++ b/updatable_timer/wake_up_time_updater.py
@@ -0,0 +1,31 @@
+import asyncio
+import logging
+from datetime import datetime, timedelta
+from typing import Optional
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from updatable_timer.workflow import Workflow
+
+
+async def main(client: Optional[Client] = None):
+ logging.basicConfig(level=logging.INFO)
+
+ if not client:
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ handle = client.get_workflow_handle(workflow_id="updatable-timer-workflow")
+ # signal workflow about the wake up time change
+ await handle.signal(
+ Workflow.update_wake_up_time,
+ (datetime.now() + timedelta(seconds=10)).timestamp(),
+ )
+
+ logging.info("Updated wake up time to 10 seconds from now")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/updatable_timer/worker.py b/updatable_timer/worker.py
new file mode 100644
index 00000000..8bb3d47a
--- /dev/null
+++ b/updatable_timer/worker.py
@@ -0,0 +1,39 @@
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker
+
+from updatable_timer import TASK_QUEUE
+from updatable_timer.workflow import Workflow
+
+interrupt_event = asyncio.Event()
+
+
+async def main():
+ logging.basicConfig(level=logging.INFO)
+
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ async with Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[Workflow],
+ ):
+ logging.info("Worker started, ctrl+c to exit")
+ # Wait until interrupted
+ await interrupt_event.wait()
+ logging.info("Interrupt received, shutting down...")
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ try:
+ loop.run_until_complete(main())
+ except KeyboardInterrupt:
+ interrupt_event.set()
+ loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/updatable_timer/workflow.py b/updatable_timer/workflow.py
new file mode 100644
index 00000000..749df6fc
--- /dev/null
+++ b/updatable_timer/workflow.py
@@ -0,0 +1,32 @@
+from datetime import datetime, timezone
+from typing import Optional
+
+from temporalio import workflow
+
+from updatable_timer.updatable_timer_lib import UpdatableTimer
+
+
+@workflow.defn
+class Workflow:
+ @workflow.init
+ def __init__(self, wake_up_time: float) -> None:
+ self.timer = UpdatableTimer(
+ datetime.fromtimestamp(wake_up_time, tz=timezone.utc)
+ )
+
+ @workflow.run
+ async def run(self, wake_up_time: float):
+ await self.timer.sleep()
+
+ @workflow.signal
+ async def update_wake_up_time(self, wake_up_time: float) -> None:
+ workflow.logger.info(f"update_wake_up_time: {wake_up_time}")
+
+ self.timer.update_wake_up_time(
+ datetime.fromtimestamp(wake_up_time, tz=timezone.utc)
+ )
+
+ @workflow.query
+ def get_wake_up_time(self) -> float:
+ workflow.logger.info(f"get_wake_up_time")
+ return float(self.timer.get_wake_up_time().timestamp())
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 00000000..dd39daa7
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,3446 @@
+version = 1
+revision = 3
+requires-python = ">=3.10"
+resolution-markers = [
+ "python_full_version >= '3.13'",
+ "python_full_version >= '3.12.4' and python_full_version < '3.13'",
+ "python_full_version >= '3.12' and python_full_version < '3.12.4'",
+ "python_full_version == '3.11.*'",
+ "python_full_version < '3.11'",
+]
+
+[[package]]
+name = "aiohappyeyeballs"
+version = "2.6.1"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.12.14"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "aiohappyeyeballs" },
+ { name = "aiosignal" },
+ { name = "async-timeout", marker = "python_full_version < '3.11'" },
+ { name = "attrs" },
+ { name = "frozenlist" },
+ { name = "multidict" },
+ { name = "propcache" },
+ { name = "yarl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e6/0b/e39ad954107ebf213a2325038a3e7a506be3d98e1435e1f82086eec4cde2/aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2", size = 7822921, upload-time = "2025-07-10T13:05:33.968Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/88/f161f429f9de391eee6a5c2cffa54e2ecd5b7122ae99df247f7734dfefcb/aiohttp-3.12.14-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:906d5075b5ba0dd1c66fcaaf60eb09926a9fef3ca92d912d2a0bbdbecf8b1248", size = 702641, upload-time = "2025-07-10T13:02:38.98Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/b5/24fa382a69a25d242e2baa3e56d5ea5227d1b68784521aaf3a1a8b34c9a4/aiohttp-3.12.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c875bf6fc2fd1a572aba0e02ef4e7a63694778c5646cdbda346ee24e630d30fb", size = 479005, upload-time = "2025-07-10T13:02:42.714Z" },
+ { url = "https://files.pythonhosted.org/packages/09/67/fda1bc34adbfaa950d98d934a23900918f9d63594928c70e55045838c943/aiohttp-3.12.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbb284d15c6a45fab030740049d03c0ecd60edad9cd23b211d7e11d3be8d56fd", size = 466781, upload-time = "2025-07-10T13:02:44.639Z" },
+ { url = "https://files.pythonhosted.org/packages/36/96/3ce1ea96d3cf6928b87cfb8cdd94650367f5c2f36e686a1f5568f0f13754/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38e360381e02e1a05d36b223ecab7bc4a6e7b5ab15760022dc92589ee1d4238c", size = 1648841, upload-time = "2025-07-10T13:02:46.356Z" },
+ { url = "https://files.pythonhosted.org/packages/be/04/ddea06cb4bc7d8db3745cf95e2c42f310aad485ca075bd685f0e4f0f6b65/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aaf90137b5e5d84a53632ad95ebee5c9e3e7468f0aab92ba3f608adcb914fa95", size = 1622896, upload-time = "2025-07-10T13:02:48.422Z" },
+ { url = "https://files.pythonhosted.org/packages/73/66/63942f104d33ce6ca7871ac6c1e2ebab48b88f78b2b7680c37de60f5e8cd/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e532a25e4a0a2685fa295a31acf65e027fbe2bea7a4b02cdfbbba8a064577663", size = 1695302, upload-time = "2025-07-10T13:02:50.078Z" },
+ { url = "https://files.pythonhosted.org/packages/20/00/aab615742b953f04b48cb378ee72ada88555b47b860b98c21c458c030a23/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eab9762c4d1b08ae04a6c77474e6136da722e34fdc0e6d6eab5ee93ac29f35d1", size = 1737617, upload-time = "2025-07-10T13:02:52.123Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/4f/ef6d9f77225cf27747368c37b3d69fac1f8d6f9d3d5de2d410d155639524/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abe53c3812b2899889a7fca763cdfaeee725f5be68ea89905e4275476ffd7e61", size = 1642282, upload-time = "2025-07-10T13:02:53.899Z" },
+ { url = "https://files.pythonhosted.org/packages/37/e1/e98a43c15aa52e9219a842f18c59cbae8bbe2d50c08d298f17e9e8bafa38/aiohttp-3.12.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5760909b7080aa2ec1d320baee90d03b21745573780a072b66ce633eb77a8656", size = 1582406, upload-time = "2025-07-10T13:02:55.515Z" },
+ { url = "https://files.pythonhosted.org/packages/71/5c/29c6dfb49323bcdb0239bf3fc97ffcf0eaf86d3a60426a3287ec75d67721/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:02fcd3f69051467bbaa7f84d7ec3267478c7df18d68b2e28279116e29d18d4f3", size = 1626255, upload-time = "2025-07-10T13:02:57.343Z" },
+ { url = "https://files.pythonhosted.org/packages/79/60/ec90782084090c4a6b459790cfd8d17be2c5662c9c4b2d21408b2f2dc36c/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4dcd1172cd6794884c33e504d3da3c35648b8be9bfa946942d353b939d5f1288", size = 1637041, upload-time = "2025-07-10T13:02:59.008Z" },
+ { url = "https://files.pythonhosted.org/packages/22/89/205d3ad30865c32bc472ac13f94374210745b05bd0f2856996cb34d53396/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:224d0da41355b942b43ad08101b1b41ce633a654128ee07e36d75133443adcda", size = 1612494, upload-time = "2025-07-10T13:03:00.618Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ae/2f66edaa8bd6db2a4cba0386881eb92002cdc70834e2a93d1d5607132c7e/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e387668724f4d734e865c1776d841ed75b300ee61059aca0b05bce67061dcacc", size = 1692081, upload-time = "2025-07-10T13:03:02.154Z" },
+ { url = "https://files.pythonhosted.org/packages/08/3a/fa73bfc6e21407ea57f7906a816f0dc73663d9549da703be05dbd76d2dc3/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:dec9cde5b5a24171e0b0a4ca064b1414950904053fb77c707efd876a2da525d8", size = 1715318, upload-time = "2025-07-10T13:03:04.322Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/b3/751124b8ceb0831c17960d06ee31a4732cb4a6a006fdbfa1153d07c52226/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bbad68a2af4877cc103cd94af9160e45676fc6f0c14abb88e6e092b945c2c8e3", size = 1643660, upload-time = "2025-07-10T13:03:06.406Z" },
+ { url = "https://files.pythonhosted.org/packages/81/3c/72477a1d34edb8ab8ce8013086a41526d48b64f77e381c8908d24e1c18f5/aiohttp-3.12.14-cp310-cp310-win32.whl", hash = "sha256:ee580cb7c00bd857b3039ebca03c4448e84700dc1322f860cf7a500a6f62630c", size = 428289, upload-time = "2025-07-10T13:03:08.274Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/c4/8aec4ccf1b822ec78e7982bd5cf971113ecce5f773f04039c76a083116fc/aiohttp-3.12.14-cp310-cp310-win_amd64.whl", hash = "sha256:cf4f05b8cea571e2ccc3ca744e35ead24992d90a72ca2cf7ab7a2efbac6716db", size = 451328, upload-time = "2025-07-10T13:03:10.146Z" },
+ { url = "https://files.pythonhosted.org/packages/53/e1/8029b29316971c5fa89cec170274582619a01b3d82dd1036872acc9bc7e8/aiohttp-3.12.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f4552ff7b18bcec18b60a90c6982049cdb9dac1dba48cf00b97934a06ce2e597", size = 709960, upload-time = "2025-07-10T13:03:11.936Z" },
+ { url = "https://files.pythonhosted.org/packages/96/bd/4f204cf1e282041f7b7e8155f846583b19149e0872752711d0da5e9cc023/aiohttp-3.12.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8283f42181ff6ccbcf25acaae4e8ab2ff7e92b3ca4a4ced73b2c12d8cd971393", size = 482235, upload-time = "2025-07-10T13:03:14.118Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/0f/2a580fcdd113fe2197a3b9df30230c7e85bb10bf56f7915457c60e9addd9/aiohttp-3.12.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:040afa180ea514495aaff7ad34ec3d27826eaa5d19812730fe9e529b04bb2179", size = 470501, upload-time = "2025-07-10T13:03:16.153Z" },
+ { url = "https://files.pythonhosted.org/packages/38/78/2c1089f6adca90c3dd74915bafed6d6d8a87df5e3da74200f6b3a8b8906f/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b413c12f14c1149f0ffd890f4141a7471ba4b41234fe4fd4a0ff82b1dc299dbb", size = 1740696, upload-time = "2025-07-10T13:03:18.4Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/c8/ce6c7a34d9c589f007cfe064da2d943b3dee5aabc64eaecd21faf927ab11/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d6f607ce2e1a93315414e3d448b831238f1874b9968e1195b06efaa5c87e245", size = 1689365, upload-time = "2025-07-10T13:03:20.629Z" },
+ { url = "https://files.pythonhosted.org/packages/18/10/431cd3d089de700756a56aa896faf3ea82bee39d22f89db7ddc957580308/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:565e70d03e924333004ed101599902bba09ebb14843c8ea39d657f037115201b", size = 1788157, upload-time = "2025-07-10T13:03:22.44Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/b2/26f4524184e0f7ba46671c512d4b03022633bcf7d32fa0c6f1ef49d55800/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4699979560728b168d5ab63c668a093c9570af2c7a78ea24ca5212c6cdc2b641", size = 1827203, upload-time = "2025-07-10T13:03:24.628Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/30/aadcdf71b510a718e3d98a7bfeaea2396ac847f218b7e8edb241b09bd99a/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad5fdf6af93ec6c99bf800eba3af9a43d8bfd66dce920ac905c817ef4a712afe", size = 1729664, upload-time = "2025-07-10T13:03:26.412Z" },
+ { url = "https://files.pythonhosted.org/packages/67/7f/7ccf11756ae498fdedc3d689a0c36ace8fc82f9d52d3517da24adf6e9a74/aiohttp-3.12.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ac76627c0b7ee0e80e871bde0d376a057916cb008a8f3ffc889570a838f5cc7", size = 1666741, upload-time = "2025-07-10T13:03:28.167Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/4d/35ebc170b1856dd020c92376dbfe4297217625ef4004d56587024dc2289c/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:798204af1180885651b77bf03adc903743a86a39c7392c472891649610844635", size = 1715013, upload-time = "2025-07-10T13:03:30.018Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/24/46dc0380146f33e2e4aa088b92374b598f5bdcde1718c77e8d1a0094f1a4/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4f1205f97de92c37dd71cf2d5bcfb65fdaed3c255d246172cce729a8d849b4da", size = 1710172, upload-time = "2025-07-10T13:03:31.821Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/0a/46599d7d19b64f4d0fe1b57bdf96a9a40b5c125f0ae0d8899bc22e91fdce/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:76ae6f1dd041f85065d9df77c6bc9c9703da9b5c018479d20262acc3df97d419", size = 1690355, upload-time = "2025-07-10T13:03:34.754Z" },
+ { url = "https://files.pythonhosted.org/packages/08/86/b21b682e33d5ca317ef96bd21294984f72379454e689d7da584df1512a19/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a194ace7bc43ce765338ca2dfb5661489317db216ea7ea700b0332878b392cab", size = 1783958, upload-time = "2025-07-10T13:03:36.53Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/45/f639482530b1396c365f23c5e3b1ae51c9bc02ba2b2248ca0c855a730059/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:16260e8e03744a6fe3fcb05259eeab8e08342c4c33decf96a9dad9f1187275d0", size = 1804423, upload-time = "2025-07-10T13:03:38.504Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/e5/39635a9e06eed1d73671bd4079a3caf9cf09a49df08490686f45a710b80e/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c779e5ebbf0e2e15334ea404fcce54009dc069210164a244d2eac8352a44b28", size = 1717479, upload-time = "2025-07-10T13:03:40.158Z" },
+ { url = "https://files.pythonhosted.org/packages/51/e1/7f1c77515d369b7419c5b501196526dad3e72800946c0099594c1f0c20b4/aiohttp-3.12.14-cp311-cp311-win32.whl", hash = "sha256:a289f50bf1bd5be227376c067927f78079a7bdeccf8daa6a9e65c38bae14324b", size = 427907, upload-time = "2025-07-10T13:03:41.801Z" },
+ { url = "https://files.pythonhosted.org/packages/06/24/a6bf915c85b7a5b07beba3d42b3282936b51e4578b64a51e8e875643c276/aiohttp-3.12.14-cp311-cp311-win_amd64.whl", hash = "sha256:0b8a69acaf06b17e9c54151a6c956339cf46db4ff72b3ac28516d0f7068f4ced", size = 452334, upload-time = "2025-07-10T13:03:43.485Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/0d/29026524e9336e33d9767a1e593ae2b24c2b8b09af7c2bd8193762f76b3e/aiohttp-3.12.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a0ecbb32fc3e69bc25efcda7d28d38e987d007096cbbeed04f14a6662d0eee22", size = 701055, upload-time = "2025-07-10T13:03:45.59Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/b8/a5e8e583e6c8c1056f4b012b50a03c77a669c2e9bf012b7cf33d6bc4b141/aiohttp-3.12.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0400f0ca9bb3e0b02f6466421f253797f6384e9845820c8b05e976398ac1d81a", size = 475670, upload-time = "2025-07-10T13:03:47.249Z" },
+ { url = "https://files.pythonhosted.org/packages/29/e8/5202890c9e81a4ec2c2808dd90ffe024952e72c061729e1d49917677952f/aiohttp-3.12.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a56809fed4c8a830b5cae18454b7464e1529dbf66f71c4772e3cfa9cbec0a1ff", size = 468513, upload-time = "2025-07-10T13:03:49.377Z" },
+ { url = "https://files.pythonhosted.org/packages/23/e5/d11db8c23d8923d3484a27468a40737d50f05b05eebbb6288bafcb467356/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f2e373276e4755691a963e5d11756d093e346119f0627c2d6518208483fb6d", size = 1715309, upload-time = "2025-07-10T13:03:51.556Z" },
+ { url = "https://files.pythonhosted.org/packages/53/44/af6879ca0eff7a16b1b650b7ea4a827301737a350a464239e58aa7c387ef/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ca39e433630e9a16281125ef57ece6817afd1d54c9f1bf32e901f38f16035869", size = 1697961, upload-time = "2025-07-10T13:03:53.511Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/94/18457f043399e1ec0e59ad8674c0372f925363059c276a45a1459e17f423/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c748b3f8b14c77720132b2510a7d9907a03c20ba80f469e58d5dfd90c079a1c", size = 1753055, upload-time = "2025-07-10T13:03:55.368Z" },
+ { url = "https://files.pythonhosted.org/packages/26/d9/1d3744dc588fafb50ff8a6226d58f484a2242b5dd93d8038882f55474d41/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a568abe1b15ce69d4cc37e23020720423f0728e3cb1f9bcd3f53420ec3bfe7", size = 1799211, upload-time = "2025-07-10T13:03:57.216Z" },
+ { url = "https://files.pythonhosted.org/packages/73/12/2530fb2b08773f717ab2d249ca7a982ac66e32187c62d49e2c86c9bba9b4/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9888e60c2c54eaf56704b17feb558c7ed6b7439bca1e07d4818ab878f2083660", size = 1718649, upload-time = "2025-07-10T13:03:59.469Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/34/8d6015a729f6571341a311061b578e8b8072ea3656b3d72329fa0faa2c7c/aiohttp-3.12.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3006a1dc579b9156de01e7916d38c63dc1ea0679b14627a37edf6151bc530088", size = 1634452, upload-time = "2025-07-10T13:04:01.698Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/4b/08b83ea02595a582447aeb0c1986792d0de35fe7a22fb2125d65091cbaf3/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa8ec5c15ab80e5501a26719eb48a55f3c567da45c6ea5bb78c52c036b2655c7", size = 1695511, upload-time = "2025-07-10T13:04:04.165Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/66/9c7c31037a063eec13ecf1976185c65d1394ded4a5120dd5965e3473cb21/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:39b94e50959aa07844c7fe2206b9f75d63cc3ad1c648aaa755aa257f6f2498a9", size = 1716967, upload-time = "2025-07-10T13:04:06.132Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/02/84406e0ad1acb0fb61fd617651ab6de760b2d6a31700904bc0b33bd0894d/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:04c11907492f416dad9885d503fbfc5dcb6768d90cad8639a771922d584609d3", size = 1657620, upload-time = "2025-07-10T13:04:07.944Z" },
+ { url = "https://files.pythonhosted.org/packages/07/53/da018f4013a7a179017b9a274b46b9a12cbeb387570f116964f498a6f211/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:88167bd9ab69bb46cee91bd9761db6dfd45b6e76a0438c7e884c3f8160ff21eb", size = 1737179, upload-time = "2025-07-10T13:04:10.182Z" },
+ { url = "https://files.pythonhosted.org/packages/49/e8/ca01c5ccfeaafb026d85fa4f43ceb23eb80ea9c1385688db0ef322c751e9/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:791504763f25e8f9f251e4688195e8b455f8820274320204f7eafc467e609425", size = 1765156, upload-time = "2025-07-10T13:04:12.029Z" },
+ { url = "https://files.pythonhosted.org/packages/22/32/5501ab525a47ba23c20613e568174d6c63aa09e2caa22cded5c6ea8e3ada/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785b112346e435dd3a1a67f67713a3fe692d288542f1347ad255683f066d8e0", size = 1724766, upload-time = "2025-07-10T13:04:13.961Z" },
+ { url = "https://files.pythonhosted.org/packages/06/af/28e24574801fcf1657945347ee10df3892311c2829b41232be6089e461e7/aiohttp-3.12.14-cp312-cp312-win32.whl", hash = "sha256:15f5f4792c9c999a31d8decf444e79fcfd98497bf98e94284bf390a7bb8c1729", size = 422641, upload-time = "2025-07-10T13:04:16.018Z" },
+ { url = "https://files.pythonhosted.org/packages/98/d5/7ac2464aebd2eecac38dbe96148c9eb487679c512449ba5215d233755582/aiohttp-3.12.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b66e1a182879f579b105a80d5c4bd448b91a57e8933564bf41665064796a338", size = 449316, upload-time = "2025-07-10T13:04:18.289Z" },
+ { url = "https://files.pythonhosted.org/packages/06/48/e0d2fa8ac778008071e7b79b93ab31ef14ab88804d7ba71b5c964a7c844e/aiohttp-3.12.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767", size = 695471, upload-time = "2025-07-10T13:04:20.124Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/e7/f73206afa33100804f790b71092888f47df65fd9a4cd0e6800d7c6826441/aiohttp-3.12.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e", size = 473128, upload-time = "2025-07-10T13:04:21.928Z" },
+ { url = "https://files.pythonhosted.org/packages/df/e2/4dd00180be551a6e7ee979c20fc7c32727f4889ee3fd5b0586e0d47f30e1/aiohttp-3.12.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63", size = 465426, upload-time = "2025-07-10T13:04:24.071Z" },
+ { url = "https://files.pythonhosted.org/packages/de/dd/525ed198a0bb674a323e93e4d928443a680860802c44fa7922d39436b48b/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:077b4488411a9724cecc436cbc8c133e0d61e694995b8de51aaf351c7578949d", size = 1704252, upload-time = "2025-07-10T13:04:26.049Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/b1/01e542aed560a968f692ab4fc4323286e8bc4daae83348cd63588e4f33e3/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d8c35632575653f297dcbc9546305b2c1133391089ab925a6a3706dfa775ccab", size = 1685514, upload-time = "2025-07-10T13:04:28.186Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/06/93669694dc5fdabdc01338791e70452d60ce21ea0946a878715688d5a191/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8ce87963f0035c6834b28f061df90cf525ff7c9b6283a8ac23acee6502afd4", size = 1737586, upload-time = "2025-07-10T13:04:30.195Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/3a/18991048ffc1407ca51efb49ba8bcc1645961f97f563a6c480cdf0286310/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a2cf66e32a2563bb0766eb24eae7e9a269ac0dc48db0aae90b575dc9583026", size = 1786958, upload-time = "2025-07-10T13:04:32.482Z" },
+ { url = "https://files.pythonhosted.org/packages/30/a8/81e237f89a32029f9b4a805af6dffc378f8459c7b9942712c809ff9e76e5/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdea089caf6d5cde975084a884c72d901e36ef9c2fd972c9f51efbbc64e96fbd", size = 1709287, upload-time = "2025-07-10T13:04:34.493Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/e3/bd67a11b0fe7fc12c6030473afd9e44223d456f500f7cf526dbaa259ae46/aiohttp-3.12.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7865f27db67d49e81d463da64a59365ebd6b826e0e4847aa111056dcb9dc88", size = 1622990, upload-time = "2025-07-10T13:04:36.433Z" },
+ { url = "https://files.pythonhosted.org/packages/83/ba/e0cc8e0f0d9ce0904e3cf2d6fa41904e379e718a013c721b781d53dcbcca/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ab5b38a6a39781d77713ad930cb5e7feea6f253de656a5f9f281a8f5931b086", size = 1676015, upload-time = "2025-07-10T13:04:38.958Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/b3/1e6c960520bda094c48b56de29a3d978254637ace7168dd97ddc273d0d6c/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b3b15acee5c17e8848d90a4ebc27853f37077ba6aec4d8cb4dbbea56d156933", size = 1707678, upload-time = "2025-07-10T13:04:41.275Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/19/929a3eb8c35b7f9f076a462eaa9830b32c7f27d3395397665caa5e975614/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4c972b0bdaac167c1e53e16a16101b17c6d0ed7eac178e653a07b9f7fad7151", size = 1650274, upload-time = "2025-07-10T13:04:43.483Z" },
+ { url = "https://files.pythonhosted.org/packages/22/e5/81682a6f20dd1b18ce3d747de8eba11cbef9b270f567426ff7880b096b48/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7442488b0039257a3bdbc55f7209587911f143fca11df9869578db6c26feeeb8", size = 1726408, upload-time = "2025-07-10T13:04:45.577Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/17/884938dffaa4048302985483f77dfce5ac18339aad9b04ad4aaa5e32b028/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f68d3067eecb64c5e9bab4a26aa11bd676f4c70eea9ef6536b0a4e490639add3", size = 1759879, upload-time = "2025-07-10T13:04:47.663Z" },
+ { url = "https://files.pythonhosted.org/packages/95/78/53b081980f50b5cf874359bde707a6eacd6c4be3f5f5c93937e48c9d0025/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f88d3704c8b3d598a08ad17d06006cb1ca52a1182291f04979e305c8be6c9758", size = 1708770, upload-time = "2025-07-10T13:04:49.944Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/91/228eeddb008ecbe3ffa6c77b440597fdf640307162f0c6488e72c5a2d112/aiohttp-3.12.14-cp313-cp313-win32.whl", hash = "sha256:a3c99ab19c7bf375c4ae3debd91ca5d394b98b6089a03231d4c580ef3c2ae4c5", size = 421688, upload-time = "2025-07-10T13:04:51.993Z" },
+ { url = "https://files.pythonhosted.org/packages/66/5f/8427618903343402fdafe2850738f735fd1d9409d2a8f9bcaae5e630d3ba/aiohttp-3.12.14-cp313-cp313-win_amd64.whl", hash = "sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa", size = 448098, upload-time = "2025-07-10T13:04:53.999Z" },
+]
+
+[[package]]
+name = "aiosignal"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "frozenlist" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "sniffio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
+]
+
+[[package]]
+name = "async-timeout"
+version = "4.0.3"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", size = 8345, upload-time = "2023-08-10T16:35:56.907Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721, upload-time = "2023-08-10T16:35:55.203Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.3.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
+]
+
+[[package]]
+name = "boto3"
+version = "1.39.4"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "botocore" },
+ { name = "jmespath" },
+ { name = "s3transfer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6a/1f/b7510dcd26eb14735d6f4b2904e219b825660425a0cf0b6f35b84c7249b0/boto3-1.39.4.tar.gz", hash = "sha256:6c955729a1d70181bc8368e02a7d3f350884290def63815ebca8408ee6d47571", size = 111829, upload-time = "2025-07-09T19:23:01.512Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/5c/93292e4d8c809950c13950b3168e0eabdac828629c21047959251ad3f28c/boto3-1.39.4-py3-none-any.whl", hash = "sha256:f8e9534b429121aa5c5b7c685c6a94dd33edf14f87926e9a182d5b50220ba284", size = 139908, upload-time = "2025-07-09T19:22:59.808Z" },
+]
+
+[[package]]
+name = "botocore"
+version = "1.39.4"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "jmespath" },
+ { name = "python-dateutil" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e6/9f/21c823ea2fae3fa5a6c9e8caaa1f858acd55018e6d317505a4f14c5bb999/botocore-1.39.4.tar.gz", hash = "sha256:e662ac35c681f7942a93f2ec7b4cde8f8b56dd399da47a79fa3e370338521a56", size = 14136116, upload-time = "2025-07-09T19:22:49.811Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/44/f120319e0a9afface645e99f300175b9b308e4724cb400b32e1bd6eb3060/botocore-1.39.4-py3-none-any.whl", hash = "sha256:c41e167ce01cfd1973c3fa9856ef5244a51ddf9c82cb131120d8617913b6812a", size = 13795516, upload-time = "2025-07-09T19:22:44.446Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.7.9"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/de/8a/c729b6b60c66a38f590c4e774decc4b2ec7b0576be8f1aa984a53ffa812a/certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079", size = 160386, upload-time = "2025-07-09T02:13:58.874Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/66/f3/80a3f974c8b535d394ff960a11ac20368e06b736da395b551a49ce950cce/certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39", size = 159230, upload-time = "2025-07-09T02:13:57.007Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "pycparser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" },
+ { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" },
+ { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" },
+ { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" },
+ { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" },
+ { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" },
+ { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" },
+ { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" },
+ { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" },
+ { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" },
+ { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" },
+ { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" },
+ { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
+ { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
+ { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
+ { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" },
+ { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" },
+ { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" },
+ { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" },
+ { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" },
+ { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" },
+ { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" },
+ { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" },
+ { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" },
+ { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" },
+ { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
+ { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
+ { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
+ { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
+ { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
+ { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
+ { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
+ { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
+ { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
+ { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
+ { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
+ { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
+ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.2.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "cryptography"
+version = "38.0.4"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "cffi" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e3/3f/41186b1f2fd86a542d399175f6b8e43f82cd4dfa51235a0b030a042b811a/cryptography-38.0.4.tar.gz", hash = "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290", size = 599786, upload-time = "2022-11-27T19:02:47.023Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/75/7a/2ea7dd2202638cf1053aaa8fbbaddded0b78c78832b3d03cafa0416a6c84/cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70", size = 5393399, upload-time = "2022-11-27T19:01:41.672Z" },
+ { url = "https://files.pythonhosted.org/packages/52/1b/49ebc2b59e9126f1f378ae910e98704d54a3f48b78e2d6d6c8cfe6fbe06f/cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb", size = 2845386, upload-time = "2022-11-27T19:01:46.627Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/47/929f07e12ebbcfedddb95397c49677dd82bb5a0bb648582b10d5f65e321c/cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d", size = 3646085, upload-time = "2022-11-27T19:03:08.155Z" },
+ { url = "https://files.pythonhosted.org/packages/32/ed/d7de730e1452ed714f2f8eee123669d4819080e03ec523b131d9b709d060/cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1", size = 3970064, upload-time = "2022-11-27T19:03:10.965Z" },
+ { url = "https://files.pythonhosted.org/packages/63/d4/66b3b4ffe51b47a065b5a5a00e6a4c8aa6cdfa4f2453adfa0aac77fd3511/cryptography-38.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8", size = 4147583, upload-time = "2022-11-27T19:02:22.123Z" },
+ { url = "https://files.pythonhosted.org/packages/12/9c/e44f95e71aedc5fefe3425df662dd17c6f94fbf68470b56c4873c43f27d2/cryptography-38.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db", size = 4048952, upload-time = "2022-11-27T19:01:55.976Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/8f/6c52b1f9d650863e8f67edbe062c04f1c8455579eaace1593d8fe469319a/cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b", size = 3966337, upload-time = "2022-11-27T19:03:13.798Z" },
+ { url = "https://files.pythonhosted.org/packages/26/f8/a81170a816679fca9ccd907b801992acfc03c33f952440421c921af2cc57/cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c", size = 4166037, upload-time = "2022-11-27T19:02:09.987Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/44/6d6cb7cff7f2dbc59fde50e5b82bc6df075e73af89a25eba1a6193c22165/cryptography-38.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00", size = 4070539, upload-time = "2022-11-27T19:03:16.685Z" },
+ { url = "https://files.pythonhosted.org/packages/64/4e/04dced6a515032b7bf3e8f287c7ff73a7d1b438c8394aa50b9fceb4077e2/cryptography-38.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0", size = 4231944, upload-time = "2022-11-27T19:02:34.288Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/83/2cc749fdc39345c1343cb29dc38bc7de9a0a55b7761663e098410f98f902/cryptography-38.0.4-cp36-abi3-win32.whl", hash = "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744", size = 2073540, upload-time = "2022-11-27T19:02:36.361Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/eb/f52b165db2abd662cda0a76efb7579a291fed1a7979cf41146cdc19e0d7a/cryptography-38.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d", size = 2432391, upload-time = "2022-11-27T19:02:38.848Z" },
+]
+
+[[package]]
+name = "dacite"
+version = "1.9.2"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/55/a0/7ca79796e799a3e782045d29bf052b5cde7439a2bbb17f15ff44f7aacc63/dacite-1.9.2.tar.gz", hash = "sha256:6ccc3b299727c7aa17582f0021f6ae14d5de47c7227932c47fec4cdfefd26f09", size = 22420, upload-time = "2025-02-05T09:27:29.757Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/35/386550fd60316d1e37eccdda609b074113298f23cef5bddb2049823fe666/dacite-1.9.2-py3-none-any.whl", hash = "sha256:053f7c3f5128ca2e9aceb66892b1a3c8936d02c686e707bee96e19deef4bc4a0", size = 16600, upload-time = "2025-02-05T09:27:24.345Z" },
+]
+
+[[package]]
+name = "dataclasses-json"
+version = "0.6.7"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "marshmallow" },
+ { name = "typing-inspect" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" },
+]
+
+[[package]]
+name = "distro"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
+]
+
+[[package]]
+name = "fastapi"
+version = "0.116.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.18.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
+]
+
+[[package]]
+name = "frozenlist"
+version = "1.7.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/36/0da0a49409f6b47cc2d060dc8c9040b897b5902a8a4e37d9bc1deb11f680/frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a", size = 81304, upload-time = "2025-06-09T22:59:46.226Z" },
+ { url = "https://files.pythonhosted.org/packages/77/f0/77c11d13d39513b298e267b22eb6cb559c103d56f155aa9a49097221f0b6/frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61", size = 47735, upload-time = "2025-06-09T22:59:48.133Z" },
+ { url = "https://files.pythonhosted.org/packages/37/12/9d07fa18971a44150593de56b2f2947c46604819976784bcf6ea0d5db43b/frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d", size = 46775, upload-time = "2025-06-09T22:59:49.564Z" },
+ { url = "https://files.pythonhosted.org/packages/70/34/f73539227e06288fcd1f8a76853e755b2b48bca6747e99e283111c18bcd4/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e", size = 224644, upload-time = "2025-06-09T22:59:51.35Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/68/c1d9c2f4a6e438e14613bad0f2973567586610cc22dcb1e1241da71de9d3/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9", size = 222125, upload-time = "2025-06-09T22:59:52.884Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/d0/98e8f9a515228d708344d7c6986752be3e3192d1795f748c24bcf154ad99/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c", size = 233455, upload-time = "2025-06-09T22:59:54.74Z" },
+ { url = "https://files.pythonhosted.org/packages/79/df/8a11bcec5600557f40338407d3e5bea80376ed1c01a6c0910fcfdc4b8993/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981", size = 227339, upload-time = "2025-06-09T22:59:56.187Z" },
+ { url = "https://files.pythonhosted.org/packages/50/82/41cb97d9c9a5ff94438c63cc343eb7980dac4187eb625a51bdfdb7707314/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615", size = 212969, upload-time = "2025-06-09T22:59:57.604Z" },
+ { url = "https://files.pythonhosted.org/packages/13/47/f9179ee5ee4f55629e4f28c660b3fdf2775c8bfde8f9c53f2de2d93f52a9/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50", size = 222862, upload-time = "2025-06-09T22:59:59.498Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/52/df81e41ec6b953902c8b7e3a83bee48b195cb0e5ec2eabae5d8330c78038/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa", size = 222492, upload-time = "2025-06-09T23:00:01.026Z" },
+ { url = "https://files.pythonhosted.org/packages/84/17/30d6ea87fa95a9408245a948604b82c1a4b8b3e153cea596421a2aef2754/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577", size = 238250, upload-time = "2025-06-09T23:00:03.401Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/00/ecbeb51669e3c3df76cf2ddd66ae3e48345ec213a55e3887d216eb4fbab3/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59", size = 218720, upload-time = "2025-06-09T23:00:05.282Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/c0/c224ce0e0eb31cc57f67742071bb470ba8246623c1823a7530be0e76164c/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e", size = 232585, upload-time = "2025-06-09T23:00:07.962Z" },
+ { url = "https://files.pythonhosted.org/packages/55/3c/34cb694abf532f31f365106deebdeac9e45c19304d83cf7d51ebbb4ca4d1/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd", size = 234248, upload-time = "2025-06-09T23:00:09.428Z" },
+ { url = "https://files.pythonhosted.org/packages/98/c0/2052d8b6cecda2e70bd81299e3512fa332abb6dcd2969b9c80dfcdddbf75/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718", size = 221621, upload-time = "2025-06-09T23:00:11.32Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/bf/7dcebae315436903b1d98ffb791a09d674c88480c158aa171958a3ac07f0/frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e", size = 39578, upload-time = "2025-06-09T23:00:13.526Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/5f/f69818f017fa9a3d24d1ae39763e29b7f60a59e46d5f91b9c6b21622f4cd/frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464", size = 43830, upload-time = "2025-06-09T23:00:14.98Z" },
+ { url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", size = 82251, upload-time = "2025-06-09T23:00:16.279Z" },
+ { url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", size = 48183, upload-time = "2025-06-09T23:00:17.698Z" },
+ { url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", size = 47107, upload-time = "2025-06-09T23:00:18.952Z" },
+ { url = "https://files.pythonhosted.org/packages/79/26/85314b8a83187c76a37183ceed886381a5f992975786f883472fcb6dc5f2/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", size = 237333, upload-time = "2025-06-09T23:00:20.275Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/fd/e5b64f7d2c92a41639ffb2ad44a6a82f347787abc0c7df5f49057cf11770/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", size = 231724, upload-time = "2025-06-09T23:00:21.705Z" },
+ { url = "https://files.pythonhosted.org/packages/20/fb/03395c0a43a5976af4bf7534759d214405fbbb4c114683f434dfdd3128ef/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", size = 245842, upload-time = "2025-06-09T23:00:23.148Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/15/c01c8e1dffdac5d9803507d824f27aed2ba76b6ed0026fab4d9866e82f1f/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", size = 239767, upload-time = "2025-06-09T23:00:25.103Z" },
+ { url = "https://files.pythonhosted.org/packages/14/99/3f4c6fe882c1f5514b6848aa0a69b20cb5e5d8e8f51a339d48c0e9305ed0/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", size = 224130, upload-time = "2025-06-09T23:00:27.061Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/83/220a374bd7b2aeba9d0725130665afe11de347d95c3620b9b82cc2fcab97/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", size = 235301, upload-time = "2025-06-09T23:00:29.02Z" },
+ { url = "https://files.pythonhosted.org/packages/03/3c/3e3390d75334a063181625343e8daab61b77e1b8214802cc4e8a1bb678fc/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", size = 234606, upload-time = "2025-06-09T23:00:30.514Z" },
+ { url = "https://files.pythonhosted.org/packages/23/1e/58232c19608b7a549d72d9903005e2d82488f12554a32de2d5fb59b9b1ba/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", size = 248372, upload-time = "2025-06-09T23:00:31.966Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/a4/e4a567e01702a88a74ce8a324691e62a629bf47d4f8607f24bf1c7216e7f/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", size = 229860, upload-time = "2025-06-09T23:00:33.375Z" },
+ { url = "https://files.pythonhosted.org/packages/73/a6/63b3374f7d22268b41a9db73d68a8233afa30ed164c46107b33c4d18ecdd/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5", size = 245893, upload-time = "2025-06-09T23:00:35.002Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/eb/d18b3f6e64799a79673c4ba0b45e4cfbe49c240edfd03a68be20002eaeaa/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", size = 246323, upload-time = "2025-06-09T23:00:36.468Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f5/720f3812e3d06cd89a1d5db9ff6450088b8f5c449dae8ffb2971a44da506/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", size = 233149, upload-time = "2025-06-09T23:00:37.963Z" },
+ { url = "https://files.pythonhosted.org/packages/69/68/03efbf545e217d5db8446acfd4c447c15b7c8cf4dbd4a58403111df9322d/frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", size = 39565, upload-time = "2025-06-09T23:00:39.753Z" },
+ { url = "https://files.pythonhosted.org/packages/58/17/fe61124c5c333ae87f09bb67186d65038834a47d974fc10a5fadb4cc5ae1/frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", size = 44019, upload-time = "2025-06-09T23:00:40.988Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" },
+ { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" },
+ { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" },
+ { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" },
+ { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" },
+ { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" },
+ { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" },
+ { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" },
+ { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" },
+ { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" },
+ { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" },
+ { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" },
+ { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" },
+ { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" },
+ { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" },
+ { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" },
+ { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" },
+ { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" },
+ { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" },
+ { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" },
+]
+
+[[package]]
+name = "fsspec"
+version = "2025.7.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/8b/02/0835e6ab9cfc03916fe3f78c0956cfcdb6ff2669ffa6651065d5ebf7fc98/fsspec-2025.7.0.tar.gz", hash = "sha256:786120687ffa54b8283d942929540d8bc5ccfa820deb555a2b5d0ed2b737bf58", size = 304432, upload-time = "2025-07-15T16:05:21.19Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl", hash = "sha256:8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21", size = 199597, upload-time = "2025-07-15T16:05:19.529Z" },
+]
+
+[[package]]
+name = "gevent"
+version = "25.9.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation == 'CPython' and sys_platform == 'win32'" },
+ { name = "greenlet", marker = "platform_python_implementation == 'CPython'" },
+ { name = "zope-event" },
+ { name = "zope-interface" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9e/48/b3ef2673ffb940f980966694e40d6d32560f3ffa284ecaeb5ea3a90a6d3f/gevent-25.9.1.tar.gz", hash = "sha256:adf9cd552de44a4e6754c51ff2e78d9193b7fa6eab123db9578a210e657235dd", size = 5059025, upload-time = "2025-09-17T16:15:34.528Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ae/c7/2c60fc4e5c9144f2b91e23af8d87c626870ad3183cfd09d2b3ba6d699178/gevent-25.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:856b990be5590e44c3a3dc6c8d48a40eaccbb42e99d2b791d11d1e7711a4297e", size = 1831980, upload-time = "2025-09-17T15:41:22.597Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/ae/49bf0a01f95a1c92c001d7b3f482a2301626b8a0617f448c4cd14ca9b5d4/gevent-25.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fe1599d0b30e6093eb3213551751b24feeb43db79f07e89d98dd2f3330c9063e", size = 1918777, upload-time = "2025-09-17T15:48:57.223Z" },
+ { url = "https://files.pythonhosted.org/packages/88/3f/266d2eb9f5d75c184a55a39e886b53a4ea7f42ff31f195220a363f0e3f9e/gevent-25.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:f0d8b64057b4bf1529b9ef9bd2259495747fba93d1f836c77bfeaacfec373fd0", size = 1869235, upload-time = "2025-09-17T15:49:18.255Z" },
+ { url = "https://files.pythonhosted.org/packages/76/24/c0c7c7db70ca74c7b1918388ebda7c8c2a3c3bff0bbfbaa9280ed04b3340/gevent-25.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b56cbc820e3136ba52cd690bdf77e47a4c239964d5f80dc657c1068e0fe9521c", size = 2177334, upload-time = "2025-09-17T15:15:10.073Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/1e/de96bd033c03955f54c455b51a5127b1d540afcfc97838d1801fafce6d2e/gevent-25.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5fa9ce5122c085983e33e0dc058f81f5264cebe746de5c401654ab96dddfca8", size = 1847708, upload-time = "2025-09-17T15:52:38.475Z" },
+ { url = "https://files.pythonhosted.org/packages/26/8b/6851e9cd3e4f322fa15c1d196cbf1a8a123da69788b078227dd13dd4208f/gevent-25.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:03c74fec58eda4b4edc043311fca8ba4f8744ad1632eb0a41d5ec25413581975", size = 2234274, upload-time = "2025-09-17T15:24:07.797Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/d8/b1178b70538c91493bec283018b47c16eab4bac9ddf5a3d4b7dd905dab60/gevent-25.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a8ae9f895e8651d10b0a8328a61c9c53da11ea51b666388aa99b0ce90f9fdc27", size = 1695326, upload-time = "2025-09-17T20:10:25.455Z" },
+ { url = "https://files.pythonhosted.org/packages/81/86/03f8db0704fed41b0fa830425845f1eb4e20c92efa3f18751ee17809e9c6/gevent-25.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5aff9e8342dc954adb9c9c524db56c2f3557999463445ba3d9cbe3dada7b7", size = 1792418, upload-time = "2025-09-17T15:41:24.384Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/35/f6b3a31f0849a62cfa2c64574bcc68a781d5499c3195e296e892a121a3cf/gevent-25.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1cdf6db28f050ee103441caa8b0448ace545364f775059d5e2de089da975c457", size = 1875700, upload-time = "2025-09-17T15:48:59.652Z" },
+ { url = "https://files.pythonhosted.org/packages/66/1e/75055950aa9b48f553e061afa9e3728061b5ccecca358cef19166e4ab74a/gevent-25.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:812debe235a8295be3b2a63b136c2474241fa5c58af55e6a0f8cfc29d4936235", size = 1831365, upload-time = "2025-09-17T15:49:19.426Z" },
+ { url = "https://files.pythonhosted.org/packages/31/e8/5c1f6968e5547e501cfa03dcb0239dff55e44c3660a37ec534e32a0c008f/gevent-25.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b28b61ff9216a3d73fe8f35669eefcafa957f143ac534faf77e8a19eb9e6883a", size = 2122087, upload-time = "2025-09-17T15:15:12.329Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/2c/ebc5d38a7542af9fb7657bfe10932a558bb98c8a94e4748e827d3823fced/gevent-25.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5e4b6278b37373306fc6b1e5f0f1cf56339a1377f67c35972775143d8d7776ff", size = 1808776, upload-time = "2025-09-17T15:52:40.16Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/26/e1d7d6c8ffbf76fe1fbb4e77bdb7f47d419206adc391ec40a8ace6ebbbf0/gevent-25.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d99f0cb2ce43c2e8305bf75bee61a8bde06619d21b9d0316ea190fc7a0620a56", size = 2179141, upload-time = "2025-09-17T15:24:09.895Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/6c/bb21fd9c095506aeeaa616579a356aa50935165cc0f1e250e1e0575620a7/gevent-25.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:72152517ecf548e2f838c61b4be76637d99279dbaa7e01b3924df040aa996586", size = 1677941, upload-time = "2025-09-17T19:59:50.185Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/49/e55930ba5259629eb28ac7ee1abbca971996a9165f902f0249b561602f24/gevent-25.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:46b188248c84ffdec18a686fcac5dbb32365d76912e14fda350db5dc0bfd4f86", size = 2955991, upload-time = "2025-09-17T14:52:30.568Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/88/63dc9e903980e1da1e16541ec5c70f2b224ec0a8e34088cb42794f1c7f52/gevent-25.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f2b54ea3ca6f0c763281cd3f96010ac7e98c2e267feb1221b5a26e2ca0b9a692", size = 1808503, upload-time = "2025-09-17T15:41:25.59Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/8d/7236c3a8f6ef7e94c22e658397009596fa90f24c7d19da11ad7ab3a9248e/gevent-25.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7a834804ac00ed8a92a69d3826342c677be651b1c3cd66cc35df8bc711057aa2", size = 1890001, upload-time = "2025-09-17T15:49:01.227Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/63/0d7f38c4a2085ecce26b50492fc6161aa67250d381e26d6a7322c309b00f/gevent-25.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:323a27192ec4da6b22a9e51c3d9d896ff20bc53fdc9e45e56eaab76d1c39dd74", size = 1855335, upload-time = "2025-09-17T15:49:20.582Z" },
+ { url = "https://files.pythonhosted.org/packages/95/18/da5211dfc54c7a57e7432fd9a6ffeae1ce36fe5a313fa782b1c96529ea3d/gevent-25.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ea78b39a2c51d47ff0f130f4c755a9a4bbb2dd9721149420ad4712743911a51", size = 2109046, upload-time = "2025-09-17T15:15:13.817Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/5a/7bb5ec8e43a2c6444853c4a9f955f3e72f479d7c24ea86c95fb264a2de65/gevent-25.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dc45cd3e1cc07514a419960af932a62eb8515552ed004e56755e4bf20bad30c5", size = 1827099, upload-time = "2025-09-17T15:52:41.384Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/d4/b63a0a60635470d7d986ef19897e893c15326dd69e8fb342c76a4f07fe9e/gevent-25.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34e01e50c71eaf67e92c186ee0196a039d6e4f4b35670396baed4a2d8f1b347f", size = 2172623, upload-time = "2025-09-17T15:24:12.03Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/98/caf06d5d22a7c129c1fb2fc1477306902a2c8ddfd399cd26bbbd4caf2141/gevent-25.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acd6bcd5feabf22c7c5174bd3b9535ee9f088d2bbce789f740ad8d6554b18f3", size = 1682837, upload-time = "2025-09-17T19:48:47.318Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/77/b97f086388f87f8ad3e01364f845004aef0123d4430241c7c9b1f9bde742/gevent-25.9.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:4f84591d13845ee31c13f44bdf6bd6c3dbf385b5af98b2f25ec328213775f2ed", size = 2973739, upload-time = "2025-09-17T14:53:30.279Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/2e/9d5f204ead343e5b27bbb2fedaec7cd0009d50696b2266f590ae845d0331/gevent-25.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9cdbb24c276a2d0110ad5c978e49daf620b153719ac8a548ce1250a7eb1b9245", size = 1809165, upload-time = "2025-09-17T15:41:27.193Z" },
+ { url = "https://files.pythonhosted.org/packages/10/3e/791d1bf1eb47748606d5f2c2aa66571f474d63e0176228b1f1fd7b77ab37/gevent-25.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:88b6c07169468af631dcf0fdd3658f9246d6822cc51461d43f7c44f28b0abb82", size = 1890638, upload-time = "2025-09-17T15:49:02.45Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/5c/9ad0229b2b4d81249ca41e4f91dd8057deaa0da6d4fbe40bf13cdc5f7a47/gevent-25.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b7bb0e29a7b3e6ca9bed2394aa820244069982c36dc30b70eb1004dd67851a48", size = 1857118, upload-time = "2025-09-17T15:49:22.125Z" },
+ { url = "https://files.pythonhosted.org/packages/49/2a/3010ed6c44179a3a5c5c152e6de43a30ff8bc2c8de3115ad8733533a018f/gevent-25.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2951bb070c0ee37b632ac9134e4fdaad70d2e660c931bb792983a0837fe5b7d7", size = 2111598, upload-time = "2025-09-17T15:15:15.226Z" },
+ { url = "https://files.pythonhosted.org/packages/08/75/6bbe57c19a7aa4527cc0f9afcdf5a5f2aed2603b08aadbccb5bf7f607ff4/gevent-25.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4e17c2d57e9a42e25f2a73d297b22b60b2470a74be5a515b36c984e1a246d47", size = 1829059, upload-time = "2025-09-17T15:52:42.596Z" },
+ { url = "https://files.pythonhosted.org/packages/06/6e/19a9bee9092be45679cb69e4dd2e0bf5f897b7140b4b39c57cc123d24829/gevent-25.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d94936f8f8b23d9de2251798fcb603b84f083fdf0d7f427183c1828fb64f117", size = 2173529, upload-time = "2025-09-17T15:24:13.897Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/4f/50de9afd879440e25737e63f5ba6ee764b75a3abe17376496ab57f432546/gevent-25.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb51c5f9537b07da673258b4832f6635014fee31690c3f0944d34741b69f92fa", size = 1681518, upload-time = "2025-09-17T19:39:47.488Z" },
+ { url = "https://files.pythonhosted.org/packages/15/1a/948f8167b2cdce573cf01cec07afc64d0456dc134b07900b26ac7018b37e/gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1a3fe4ea1c312dbf6b375b416925036fe79a40054e6bf6248ee46526ea628be1", size = 2982934, upload-time = "2025-09-17T14:54:11.302Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/ec/726b146d1d3aad82e03d2e1e1507048ab6072f906e83f97f40667866e582/gevent-25.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0adb937f13e5fb90cca2edf66d8d7e99d62a299687400ce2edee3f3504009356", size = 1813982, upload-time = "2025-09-17T15:41:28.506Z" },
+ { url = "https://files.pythonhosted.org/packages/35/5d/5f83f17162301662bd1ce702f8a736a8a8cac7b7a35e1d8b9866938d1f9d/gevent-25.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:427f869a2050a4202d93cf7fd6ab5cffb06d3e9113c10c967b6e2a0d45237cb8", size = 1894902, upload-time = "2025-09-17T15:49:03.702Z" },
+ { url = "https://files.pythonhosted.org/packages/83/cd/cf5e74e353f60dab357829069ffc300a7bb414c761f52cf8c0c6e9728b8d/gevent-25.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c049880175e8c93124188f9d926af0a62826a3b81aa6d3074928345f8238279e", size = 1861792, upload-time = "2025-09-17T15:49:23.279Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/65/b9a4526d4a4edce26fe4b3b993914ec9dc64baabad625a3101e51adb17f3/gevent-25.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5a67a0974ad9f24721034d1e008856111e0535f1541499f72a733a73d658d1c", size = 2113215, upload-time = "2025-09-17T15:15:16.34Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/be/7d35731dfaf8370795b606e515d964a0967e129db76ea7873f552045dd39/gevent-25.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d0f5d8d73f97e24ea8d24d8be0f51e0cf7c54b8021c1fddb580bf239474690f", size = 1833449, upload-time = "2025-09-17T15:52:43.75Z" },
+ { url = "https://files.pythonhosted.org/packages/65/58/7bc52544ea5e63af88c4a26c90776feb42551b7555a1c89c20069c168a3f/gevent-25.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddd3ff26e5c4240d3fbf5516c2d9d5f2a998ef87cfb73e1429cfaeaaec860fa6", size = 2176034, upload-time = "2025-09-17T15:24:15.676Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/69/a7c4ba2ffbc7c7dbf6d8b4f5d0f0a421f7815d229f4909854266c445a3d4/gevent-25.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:bb63c0d6cb9950cc94036a4995b9cc4667b8915366613449236970f4394f94d7", size = 1703019, upload-time = "2025-09-17T19:30:55.272Z" },
+]
+
+[[package]]
+name = "googleapis-common-protos"
+version = "1.70.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903, upload-time = "2025-04-14T10:17:02.924Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530, upload-time = "2025-04-14T10:17:01.271Z" },
+]
+
+[[package]]
+name = "greenlet"
+version = "3.2.3"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/db/b4c12cff13ebac2786f4f217f06588bccd8b53d260453404ef22b121fc3a/greenlet-3.2.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be", size = 268977, upload-time = "2025-06-05T16:10:24.001Z" },
+ { url = "https://files.pythonhosted.org/packages/52/61/75b4abd8147f13f70986df2801bf93735c1bd87ea780d70e3b3ecda8c165/greenlet-3.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac", size = 627351, upload-time = "2025-06-05T16:38:50.685Z" },
+ { url = "https://files.pythonhosted.org/packages/35/aa/6894ae299d059d26254779a5088632874b80ee8cf89a88bca00b0709d22f/greenlet-3.2.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a433dbc54e4a37e4fff90ef34f25a8c00aed99b06856f0119dcf09fbafa16392", size = 638599, upload-time = "2025-06-05T16:41:34.057Z" },
+ { url = "https://files.pythonhosted.org/packages/30/64/e01a8261d13c47f3c082519a5e9dbf9e143cc0498ed20c911d04e54d526c/greenlet-3.2.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:72e77ed69312bab0434d7292316d5afd6896192ac4327d44f3d613ecb85b037c", size = 634482, upload-time = "2025-06-05T16:48:16.26Z" },
+ { url = "https://files.pythonhosted.org/packages/47/48/ff9ca8ba9772d083a4f5221f7b4f0ebe8978131a9ae0909cf202f94cd879/greenlet-3.2.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68671180e3849b963649254a882cd544a3c75bfcd2c527346ad8bb53494444db", size = 633284, upload-time = "2025-06-05T16:13:01.599Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/45/626e974948713bc15775b696adb3eb0bd708bec267d6d2d5c47bb47a6119/greenlet-3.2.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49c8cfb18fb419b3d08e011228ef8a25882397f3a859b9fe1436946140b6756b", size = 582206, upload-time = "2025-06-05T16:12:48.51Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/8e/8b6f42c67d5df7db35b8c55c9a850ea045219741bb14416255616808c690/greenlet-3.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:efc6dc8a792243c31f2f5674b670b3a95d46fa1c6a912b8e310d6f542e7b0712", size = 1111412, upload-time = "2025-06-05T16:36:45.479Z" },
+ { url = "https://files.pythonhosted.org/packages/05/46/ab58828217349500a7ebb81159d52ca357da747ff1797c29c6023d79d798/greenlet-3.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:731e154aba8e757aedd0781d4b240f1225b075b4409f1bb83b05ff410582cf00", size = 1135054, upload-time = "2025-06-05T16:12:36.478Z" },
+ { url = "https://files.pythonhosted.org/packages/68/7f/d1b537be5080721c0f0089a8447d4ef72839039cdb743bdd8ffd23046e9a/greenlet-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:96c20252c2f792defe9a115d3287e14811036d51e78b3aaddbee23b69b216302", size = 296573, upload-time = "2025-06-05T16:34:26.521Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219, upload-time = "2025-06-05T16:10:10.414Z" },
+ { url = "https://files.pythonhosted.org/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383, upload-time = "2025-06-05T16:38:51.785Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422, upload-time = "2025-06-05T16:41:35.259Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/49/445fd1a210f4747fedf77615d941444349c6a3a4a1135bba9701337cd966/greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", size = 638375, upload-time = "2025-06-05T16:48:18.235Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627, upload-time = "2025-06-05T16:13:02.858Z" },
+ { url = "https://files.pythonhosted.org/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502, upload-time = "2025-06-05T16:12:49.642Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498, upload-time = "2025-06-05T16:36:46.598Z" },
+ { url = "https://files.pythonhosted.org/packages/89/5f/b16dec0cbfd3070658e0d744487919740c6d45eb90946f6787689a7efbce/greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba", size = 1139977, upload-time = "2025-06-05T16:12:38.262Z" },
+ { url = "https://files.pythonhosted.org/packages/66/77/d48fb441b5a71125bcac042fc5b1494c806ccb9a1432ecaa421e72157f77/greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", size = 297017, upload-time = "2025-06-05T16:25:05.225Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" },
+ { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload-time = "2025-06-05T16:48:19.604Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" },
+ { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload-time = "2025-06-05T16:12:40.457Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" },
+ { url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368, upload-time = "2025-06-05T16:48:21.467Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload-time = "2025-06-05T16:13:06.402Z" },
+ { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload-time = "2025-06-05T16:12:51.91Z" },
+ { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload-time = "2025-06-05T16:36:49.787Z" },
+ { url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121, upload-time = "2025-06-05T16:12:42.527Z" },
+ { url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603, upload-time = "2025-06-05T16:20:12.651Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479, upload-time = "2025-06-05T16:10:47.525Z" },
+ { url = "https://files.pythonhosted.org/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952, upload-time = "2025-06-05T16:38:55.125Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917, upload-time = "2025-06-05T16:41:38.959Z" },
+ { url = "https://files.pythonhosted.org/packages/22/cc/0bd1a7eb759d1f3e3cc2d1bc0f0b487ad3cc9f34d74da4b80f226fde4ec3/greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", size = 692443, upload-time = "2025-06-05T16:48:23.113Z" },
+ { url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995, upload-time = "2025-06-05T16:13:07.972Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320, upload-time = "2025-06-05T16:12:53.453Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" },
+]
+
+[[package]]
+name = "griffe"
+version = "1.7.3"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "colorama" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137, upload-time = "2025-04-23T11:29:09.147Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303, upload-time = "2025-04-23T11:29:07.145Z" },
+]
+
+[[package]]
+name = "grpcio"
+version = "1.76.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037, upload-time = "2025-10-21T16:20:25.069Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482, upload-time = "2025-10-21T16:20:30.113Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178, upload-time = "2025-10-21T16:20:32.733Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684, upload-time = "2025-10-21T16:20:35.435Z" },
+ { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133, upload-time = "2025-10-21T16:20:37.541Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507, upload-time = "2025-10-21T16:20:39.643Z" },
+ { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651, upload-time = "2025-10-21T16:20:42.492Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568, upload-time = "2025-10-21T16:20:45.995Z" },
+ { url = "https://files.pythonhosted.org/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", size = 3998879, upload-time = "2025-10-21T16:20:48.592Z" },
+ { url = "https://files.pythonhosted.org/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", size = 4706892, upload-time = "2025-10-21T16:20:50.697Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" },
+ { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" },
+ { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" },
+ { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" },
+ { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" },
+ { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" },
+ { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" },
+ { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" },
+ { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" },
+ { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" },
+ { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" },
+ { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" },
+ { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" },
+ { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" },
+ { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" },
+ { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "hf-xet"
+version = "1.1.5"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/ed/d4/7685999e85945ed0d7f0762b686ae7015035390de1161dcea9d5276c134c/hf_xet-1.1.5.tar.gz", hash = "sha256:69ebbcfd9ec44fdc2af73441619eeb06b94ee34511bbcf57cd423820090f5694", size = 495969, upload-time = "2025-06-20T21:48:38.007Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/89/a1119eebe2836cb25758e7661d6410d3eae982e2b5e974bcc4d250be9012/hf_xet-1.1.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f52c2fa3635b8c37c7764d8796dfa72706cc4eded19d638331161e82b0792e23", size = 2687929, upload-time = "2025-06-20T21:48:32.284Z" },
+ { url = "https://files.pythonhosted.org/packages/de/5f/2c78e28f309396e71ec8e4e9304a6483dcbc36172b5cea8f291994163425/hf_xet-1.1.5-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:9fa6e3ee5d61912c4a113e0708eaaef987047616465ac7aa30f7121a48fc1af8", size = 2556338, upload-time = "2025-06-20T21:48:30.079Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/2f/6cad7b5fe86b7652579346cb7f85156c11761df26435651cbba89376cd2c/hf_xet-1.1.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc874b5c843e642f45fd85cda1ce599e123308ad2901ead23d3510a47ff506d1", size = 3102894, upload-time = "2025-06-20T21:48:28.114Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/54/0fcf2b619720a26fbb6cc941e89f2472a522cd963a776c089b189559447f/hf_xet-1.1.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dbba1660e5d810bd0ea77c511a99e9242d920790d0e63c0e4673ed36c4022d18", size = 3002134, upload-time = "2025-06-20T21:48:25.906Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/92/1d351ac6cef7c4ba8c85744d37ffbfac2d53d0a6c04d2cabeba614640a78/hf_xet-1.1.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ab34c4c3104133c495785d5d8bba3b1efc99de52c02e759cf711a91fd39d3a14", size = 3171009, upload-time = "2025-06-20T21:48:33.987Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/65/4b2ddb0e3e983f2508528eb4501288ae2f84963586fbdfae596836d5e57a/hf_xet-1.1.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:83088ecea236d5113de478acb2339f92c95b4fb0462acaa30621fac02f5a534a", size = 3279245, upload-time = "2025-06-20T21:48:36.051Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/55/ef77a85ee443ae05a9e9cba1c9f0dd9241eb42da2aeba1dc50f51154c81a/hf_xet-1.1.5-cp37-abi3-win_amd64.whl", hash = "sha256:73e167d9807d166596b4b2f0b585c6d5bd84a26dea32843665a8b58f6edba245", size = 2738931, upload-time = "2025-06-20T21:48:39.482Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httptools"
+version = "0.6.4"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", size = 198780, upload-time = "2024-10-16T19:44:06.882Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", size = 103297, upload-time = "2024-10-16T19:44:08.129Z" },
+ { url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", size = 443130, upload-time = "2024-10-16T19:44:09.45Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", size = 442148, upload-time = "2024-10-16T19:44:11.539Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", size = 415949, upload-time = "2024-10-16T19:44:13.388Z" },
+ { url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", size = 417591, upload-time = "2024-10-16T19:44:15.258Z" },
+ { url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", size = 88344, upload-time = "2024-10-16T19:44:16.54Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029, upload-time = "2024-10-16T19:44:18.427Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492, upload-time = "2024-10-16T19:44:19.515Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891, upload-time = "2024-10-16T19:44:21.067Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788, upload-time = "2024-10-16T19:44:22.958Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214, upload-time = "2024-10-16T19:44:24.513Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120, upload-time = "2024-10-16T19:44:26.295Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565, upload-time = "2024-10-16T19:44:29.188Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683, upload-time = "2024-10-16T19:44:30.175Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337, upload-time = "2024-10-16T19:44:31.786Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796, upload-time = "2024-10-16T19:44:32.825Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837, upload-time = "2024-10-16T19:44:33.974Z" },
+ { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289, upload-time = "2024-10-16T19:44:35.111Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779, upload-time = "2024-10-16T19:44:36.253Z" },
+ { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634, upload-time = "2024-10-16T19:44:37.357Z" },
+ { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload-time = "2024-10-16T19:44:38.738Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload-time = "2024-10-16T19:44:39.818Z" },
+ { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload-time = "2024-10-16T19:44:41.189Z" },
+ { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload-time = "2024-10-16T19:44:42.384Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload-time = "2024-10-16T19:44:43.959Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload-time = "2024-10-16T19:44:45.071Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" },
+]
+
+[[package]]
+name = "huggingface-hub"
+version = "0.34.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "tqdm" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/cd/841bc8e0550d69f632a15cdd70004e95ba92cd0fbe13087d6669e2bb5f44/huggingface_hub-0.34.1.tar.gz", hash = "sha256:6978ed89ef981de3c78b75bab100a214843be1cc9d24f8e9c0dc4971808ef1b1", size = 456783, upload-time = "2025-07-25T14:54:54.758Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8c/cf/dd53c0132f50f258b06dd37a4616817b1f1f6a6b38382c06effd04bb6881/huggingface_hub-0.34.1-py3-none-any.whl", hash = "sha256:60d843dcb7bc335145b20e7d2f1dfe93910f6787b2b38a936fb772ce2a83757c", size = 558788, upload-time = "2025-07-25T14:54:52.957Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
+]
+
+[[package]]
+name = "importlib-metadata"
+version = "8.7.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "zipp" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "jiter"
+version = "0.10.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/7e/4011b5c77bec97cb2b572f566220364e3e21b51c48c5bd9c4a9c26b41b67/jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303", size = 317215, upload-time = "2025-05-18T19:03:04.303Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/4f/144c1b57c39692efc7ea7d8e247acf28e47d0912800b34d0ad815f6b2824/jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e", size = 322814, upload-time = "2025-05-18T19:03:06.433Z" },
+ { url = "https://files.pythonhosted.org/packages/63/1f/db977336d332a9406c0b1f0b82be6f71f72526a806cbb2281baf201d38e3/jiter-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8b3e0068c26ddedc7abc6fac37da2d0af16b921e288a5a613f4b86f050354f", size = 345237, upload-time = "2025-05-18T19:03:07.833Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/1c/aa30a4a775e8a672ad7f21532bdbfb269f0706b39c6ff14e1f86bdd9e5ff/jiter-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:286299b74cc49e25cd42eea19b72aa82c515d2f2ee12d11392c56d8701f52224", size = 370999, upload-time = "2025-05-18T19:03:09.338Z" },
+ { url = "https://files.pythonhosted.org/packages/35/df/f8257abc4207830cb18880781b5f5b716bad5b2a22fb4330cfd357407c5b/jiter-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ed5649ceeaeffc28d87fb012d25a4cd356dcd53eff5acff1f0466b831dda2a7", size = 491109, upload-time = "2025-05-18T19:03:11.13Z" },
+ { url = "https://files.pythonhosted.org/packages/06/76/9e1516fd7b4278aa13a2cc7f159e56befbea9aa65c71586305e7afa8b0b3/jiter-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ab0051160cb758a70716448908ef14ad476c3774bd03ddce075f3c1f90a3d6", size = 388608, upload-time = "2025-05-18T19:03:12.911Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/64/67750672b4354ca20ca18d3d1ccf2c62a072e8a2d452ac3cf8ced73571ef/jiter-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03997d2f37f6b67d2f5c475da4412be584e1cec273c1cfc03d642c46db43f8cf", size = 352454, upload-time = "2025-05-18T19:03:14.741Z" },
+ { url = "https://files.pythonhosted.org/packages/96/4d/5c4e36d48f169a54b53a305114be3efa2bbffd33b648cd1478a688f639c1/jiter-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c404a99352d839fed80d6afd6c1d66071f3bacaaa5c4268983fc10f769112e90", size = 391833, upload-time = "2025-05-18T19:03:16.426Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/de/ce4a6166a78810bd83763d2fa13f85f73cbd3743a325469a4a9289af6dae/jiter-0.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66e989410b6666d3ddb27a74c7e50d0829704ede652fd4c858e91f8d64b403d0", size = 523646, upload-time = "2025-05-18T19:03:17.704Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/a6/3bc9acce53466972964cf4ad85efecb94f9244539ab6da1107f7aed82934/jiter-0.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b532d3af9ef4f6374609a3bcb5e05a1951d3bf6190dc6b176fdb277c9bbf15ee", size = 514735, upload-time = "2025-05-18T19:03:19.44Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/d8/243c2ab8426a2a4dea85ba2a2ba43df379ccece2145320dfd4799b9633c5/jiter-0.10.0-cp310-cp310-win32.whl", hash = "sha256:da9be20b333970e28b72edc4dff63d4fec3398e05770fb3205f7fb460eb48dd4", size = 210747, upload-time = "2025-05-18T19:03:21.184Z" },
+ { url = "https://files.pythonhosted.org/packages/37/7a/8021bd615ef7788b98fc76ff533eaac846322c170e93cbffa01979197a45/jiter-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f59e533afed0c5b0ac3eba20d2548c4a550336d8282ee69eb07b37ea526ee4e5", size = 207484, upload-time = "2025-05-18T19:03:23.046Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/dd/6cefc6bd68b1c3c979cecfa7029ab582b57690a31cd2f346c4d0ce7951b6/jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978", size = 317473, upload-time = "2025-05-18T19:03:25.942Z" },
+ { url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971, upload-time = "2025-05-18T19:03:27.255Z" },
+ { url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574, upload-time = "2025-05-18T19:03:28.63Z" },
+ { url = "https://files.pythonhosted.org/packages/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028, upload-time = "2025-05-18T19:03:30.292Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083, upload-time = "2025-05-18T19:03:31.654Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821, upload-time = "2025-05-18T19:03:33.184Z" },
+ { url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174, upload-time = "2025-05-18T19:03:34.965Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/c0/61eeec33b8c75b31cae42be14d44f9e6fe3ac15a4e58010256ac3abf3638/jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606", size = 391869, upload-time = "2025-05-18T19:03:36.436Z" },
+ { url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741, upload-time = "2025-05-18T19:03:38.168Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527, upload-time = "2025-05-18T19:03:39.577Z" },
+ { url = "https://files.pythonhosted.org/packages/73/6d/29b7c2dc76ce93cbedabfd842fc9096d01a0550c52692dfc33d3cc889815/jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7", size = 210765, upload-time = "2025-05-18T19:03:41.271Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c9/d394706deb4c660137caf13e33d05a031d734eb99c051142e039d8ceb794/jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812", size = 209234, upload-time = "2025-05-18T19:03:42.918Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/b5/348b3313c58f5fbfb2194eb4d07e46a35748ba6e5b3b3046143f3040bafa/jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b", size = 312262, upload-time = "2025-05-18T19:03:44.637Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124, upload-time = "2025-05-18T19:03:46.341Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330, upload-time = "2025-05-18T19:03:47.596Z" },
+ { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670, upload-time = "2025-05-18T19:03:49.334Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057, upload-time = "2025-05-18T19:03:50.66Z" },
+ { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372, upload-time = "2025-05-18T19:03:51.98Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038, upload-time = "2025-05-18T19:03:53.703Z" },
+ { url = "https://files.pythonhosted.org/packages/67/27/c62568e3ccb03368dbcc44a1ef3a423cb86778a4389e995125d3d1aaa0a4/jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95", size = 391538, upload-time = "2025-05-18T19:03:55.046Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557, upload-time = "2025-05-18T19:03:56.386Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202, upload-time = "2025-05-18T19:03:57.675Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781, upload-time = "2025-05-18T19:03:59.025Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176, upload-time = "2025-05-18T19:04:00.305Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617, upload-time = "2025-05-18T19:04:02.078Z" },
+ { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618, upload-time = "2025-05-18T19:04:04.709Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829, upload-time = "2025-05-18T19:04:06.912Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034, upload-time = "2025-05-18T19:04:08.222Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529, upload-time = "2025-05-18T19:04:09.566Z" },
+ { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671, upload-time = "2025-05-18T19:04:10.98Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864, upload-time = "2025-05-18T19:04:12.722Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989, upload-time = "2025-05-18T19:04:14.261Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495, upload-time = "2025-05-18T19:04:15.603Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289, upload-time = "2025-05-18T19:04:17.541Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074, upload-time = "2025-05-18T19:04:19.21Z" },
+ { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225, upload-time = "2025-05-18T19:04:20.583Z" },
+ { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235, upload-time = "2025-05-18T19:04:22.363Z" },
+ { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278, upload-time = "2025-05-18T19:04:23.627Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866, upload-time = "2025-05-18T19:04:24.891Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772, upload-time = "2025-05-18T19:04:26.161Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534, upload-time = "2025-05-18T19:04:27.495Z" },
+ { url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087, upload-time = "2025-05-18T19:04:28.896Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694, upload-time = "2025-05-18T19:04:30.183Z" },
+ { url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992, upload-time = "2025-05-18T19:04:32.028Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723, upload-time = "2025-05-18T19:04:33.467Z" },
+ { url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215, upload-time = "2025-05-18T19:04:34.827Z" },
+ { url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762, upload-time = "2025-05-18T19:04:36.19Z" },
+ { url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427, upload-time = "2025-05-18T19:04:37.544Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127, upload-time = "2025-05-18T19:04:38.837Z" },
+ { url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527, upload-time = "2025-05-18T19:04:40.612Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" },
+]
+
+[[package]]
+name = "jmespath"
+version = "1.0.1"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" },
+]
+
+[[package]]
+name = "jsonpatch"
+version = "1.33"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "jsonpointer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" },
+]
+
+[[package]]
+name = "jsonpointer"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.24.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload-time = "2025-05-26T18:48:10.459Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload-time = "2025-05-26T18:48:08.417Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.4.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" },
+]
+
+[[package]]
+name = "langchain"
+version = "0.1.20"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "async-timeout", marker = "python_full_version < '3.11'" },
+ { name = "dataclasses-json" },
+ { name = "langchain-community" },
+ { name = "langchain-core" },
+ { name = "langchain-text-splitters" },
+ { name = "langsmith" },
+ { name = "numpy" },
+ { name = "pydantic" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "sqlalchemy" },
+ { name = "tenacity" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/88/94/8d917da143b30c3088be9f51719634827ab19207cb290a51de3859747783/langchain-0.1.20.tar.gz", hash = "sha256:f35c95eed8c8375e02dce95a34f2fd4856a4c98269d6dc34547a23dba5beab7e", size = 420688, upload-time = "2024-05-10T21:59:40.736Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4b/28/da40a6b12e7842a0c8b443f8cc5c6f59e49d7a9071cfad064b9639c6b044/langchain-0.1.20-py3-none-any.whl", hash = "sha256:09991999fbd6c3421a12db3c7d1f52d55601fc41d9b2a3ef51aab2e0e9c38da9", size = 1014619, upload-time = "2024-05-10T21:59:36.417Z" },
+]
+
+[[package]]
+name = "langchain-community"
+version = "0.0.38"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "dataclasses-json" },
+ { name = "langchain-core" },
+ { name = "langsmith" },
+ { name = "numpy" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "sqlalchemy" },
+ { name = "tenacity" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/b7/c20502452183d27b8c0466febb227fae3213f77e9a13683de685e7227f39/langchain_community-0.0.38.tar.gz", hash = "sha256:127fc4b75bc67b62fe827c66c02e715a730fef8fe69bd2023d466bab06b5810d", size = 1373468, upload-time = "2024-05-08T22:44:26.295Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/d3/1f4d1941ae5a627299c8ea052847b99ad6674b97b699d8a08fc4faf25d3e/langchain_community-0.0.38-py3-none-any.whl", hash = "sha256:ecb48660a70a08c90229be46b0cc5f6bc9f38f2833ee44c57dfab9bf3a2c121a", size = 2028164, upload-time = "2024-05-08T22:44:23.434Z" },
+]
+
+[[package]]
+name = "langchain-core"
+version = "0.1.53"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "jsonpatch" },
+ { name = "langsmith" },
+ { name = "packaging" },
+ { name = "pydantic" },
+ { name = "pyyaml" },
+ { name = "tenacity" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e9/65/3aaff91481b9d629a31630a40000d403bff24b3c62d9abc87dc998298cce/langchain_core-0.1.53.tar.gz", hash = "sha256:df3773a553b5335eb645827b99a61a7018cea4b11dc45efa2613fde156441cec", size = 236665, upload-time = "2024-11-02T00:27:25.16Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/10/285fa149ce95300d91ea0bb124eec28889e5ebbcb59434d1fe2f31098d72/langchain_core-0.1.53-py3-none-any.whl", hash = "sha256:02a88a21e3bd294441b5b741625fa4b53b1c684fd58ba6e5d9028e53cbe8542f", size = 303059, upload-time = "2024-11-02T00:27:23.144Z" },
+]
+
+[[package]]
+name = "langchain-openai"
+version = "0.0.6"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "langchain-core" },
+ { name = "numpy" },
+ { name = "openai" },
+ { name = "tiktoken" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/36/bd/2963a5b9f7dcf5759144bbe590984730daccfd8ced01d9de5cbf23072ac5/langchain_openai-0.0.6.tar.gz", hash = "sha256:f5c4ebe46f2c8635c8f0c26cc8df27700aacafea025410e418d5a080039974dd", size = 22653, upload-time = "2024-02-13T21:20:07.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/07/48/84e1840c25592bb76deea48d187d9fc8f864c9c82ddf3f084da4c9b8a15b/langchain_openai-0.0.6-py3-none-any.whl", hash = "sha256:2ef040e4447a26a9d3bd45dfac9cefa00797ea58555a3d91ab4f88699eb3a005", size = 29200, upload-time = "2024-02-13T21:20:04.664Z" },
+]
+
+[[package]]
+name = "langchain-text-splitters"
+version = "0.0.2"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "langchain-core" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e8/fa/88d65b0f696d8d4f37037f1418f89bc1078cd74d20054623bb7fffcecaf1/langchain_text_splitters-0.0.2.tar.gz", hash = "sha256:ac8927dc0ba08eba702f6961c9ed7df7cead8de19a9f7101ab2b5ea34201b3c1", size = 18638, upload-time = "2024-05-16T03:16:36.815Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/68/6a/804fe5ca07129046a4cedc0697222ddde6156cd874c4c4ba29e4d271828a/langchain_text_splitters-0.0.2-py3-none-any.whl", hash = "sha256:13887f32705862c1e1454213cb7834a63aae57c26fcd80346703a1d09c46168d", size = 23539, upload-time = "2024-05-16T03:16:35.727Z" },
+]
+
+[[package]]
+name = "langsmith"
+version = "0.1.147"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "httpx" },
+ { name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "pydantic" },
+ { name = "requests" },
+ { name = "requests-toolbelt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6c/56/201dd94d492ae47c1bf9b50cacc1985113dc2288d8f15857e1f4a6818376/langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a", size = 300453, upload-time = "2024-11-27T17:32:41.297Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/f0/63b06b99b730b9954f8709f6f7d9b8d076fa0a973e472efe278089bde42b/langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15", size = 311812, upload-time = "2024-11-27T17:32:39.569Z" },
+]
+
+[[package]]
+name = "litellm"
+version = "1.74.8"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "click" },
+ { name = "httpx" },
+ { name = "importlib-metadata" },
+ { name = "jinja2" },
+ { name = "jsonschema" },
+ { name = "openai" },
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "tiktoken" },
+ { name = "tokenizers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/16/89c5123c808cbc51e398afc2f1a56da1d75d5e8ef7be417895a3794f0416/litellm-1.74.8.tar.gz", hash = "sha256:6e0a18aecf62459d465ee6d9a2526fcb33719a595b972500519abe95fe4906e0", size = 9639701, upload-time = "2025-07-23T23:38:02.903Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/4a/eba1b617acb7fa597d169cdd1b5ce98502bd179138f130721a2367d2deb8/litellm-1.74.8-py3-none-any.whl", hash = "sha256:f9433207d1e12e545495e5960fe02d93e413ecac4a28225c522488e1ab1157a1", size = 8713698, upload-time = "2025-07-23T23:38:00.708Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" },
+ { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" },
+ { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" },
+ { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" },
+ { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" },
+ { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" },
+ { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" },
+ { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" },
+ { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" },
+ { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" },
+ { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
+ { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
+ { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
+ { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
+ { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
+ { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
+ { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
+ { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
+ { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
+]
+
+[[package]]
+name = "marshmallow"
+version = "3.26.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825, upload-time = "2025-02-03T15:32:25.093Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload-time = "2025-02-03T15:32:22.295Z" },
+]
+
+[[package]]
+name = "mcp"
+version = "1.11.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "python-multipart" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3a/f5/9506eb5578d5bbe9819ee8ba3198d0ad0e2fbe3bab8b257e4131ceb7dfb6/mcp-1.11.0.tar.gz", hash = "sha256:49a213df56bb9472ff83b3132a4825f5c8f5b120a90246f08b0dac6bedac44c8", size = 406907, upload-time = "2025-07-10T16:41:09.388Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/9c/c9ca79f9c512e4113a5d07043013110bb3369fc7770040c61378c7fbcf70/mcp-1.11.0-py3-none-any.whl", hash = "sha256:58deac37f7483e4b338524b98bc949b7c2b7c33d978f5fafab5bde041c5e2595", size = 155880, upload-time = "2025-07-10T16:41:07.935Z" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
+[[package]]
+name = "multidict"
+version = "6.6.3"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006, upload-time = "2025-06-30T15:53:46.929Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/67/414933982bce2efce7cbcb3169eaaf901e0f25baec69432b4874dfb1f297/multidict-6.6.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2be5b7b35271f7fff1397204ba6708365e3d773579fe2a30625e16c4b4ce817", size = 77017, upload-time = "2025-06-30T15:50:58.931Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/fe/d8a3ee1fad37dc2ef4f75488b0d9d4f25bf204aad8306cbab63d97bff64a/multidict-6.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12f4581d2930840295c461764b9a65732ec01250b46c6b2c510d7ee68872b140", size = 44897, upload-time = "2025-06-30T15:51:00.999Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/e0/265d89af8c98240265d82b8cbcf35897f83b76cd59ee3ab3879050fd8c45/multidict-6.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dd7793bab517e706c9ed9d7310b06c8672fd0aeee5781bfad612f56b8e0f7d14", size = 44574, upload-time = "2025-06-30T15:51:02.449Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/05/6b759379f7e8e04ccc97cfb2a5dcc5cdbd44a97f072b2272dc51281e6a40/multidict-6.6.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:72d8815f2cd3cf3df0f83cac3f3ef801d908b2d90409ae28102e0553af85545a", size = 225729, upload-time = "2025-06-30T15:51:03.794Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/f5/8d5a15488edd9a91fa4aad97228d785df208ed6298580883aa3d9def1959/multidict-6.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:531e331a2ee53543ab32b16334e2deb26f4e6b9b28e41f8e0c87e99a6c8e2d69", size = 242515, upload-time = "2025-06-30T15:51:05.002Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/b5/a8f317d47d0ac5bb746d6d8325885c8967c2a8ce0bb57be5399e3642cccb/multidict-6.6.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:42ca5aa9329a63be8dc49040f63817d1ac980e02eeddba763a9ae5b4027b9c9c", size = 222224, upload-time = "2025-06-30T15:51:06.148Z" },
+ { url = "https://files.pythonhosted.org/packages/76/88/18b2a0d5e80515fa22716556061189c2853ecf2aa2133081ebbe85ebea38/multidict-6.6.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:208b9b9757060b9faa6f11ab4bc52846e4f3c2fb8b14d5680c8aac80af3dc751", size = 253124, upload-time = "2025-06-30T15:51:07.375Z" },
+ { url = "https://files.pythonhosted.org/packages/62/bf/ebfcfd6b55a1b05ef16d0775ae34c0fe15e8dab570d69ca9941073b969e7/multidict-6.6.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:acf6b97bd0884891af6a8b43d0f586ab2fcf8e717cbd47ab4bdddc09e20652d8", size = 251529, upload-time = "2025-06-30T15:51:08.691Z" },
+ { url = "https://files.pythonhosted.org/packages/44/11/780615a98fd3775fc309d0234d563941af69ade2df0bb82c91dda6ddaea1/multidict-6.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68e9e12ed00e2089725669bdc88602b0b6f8d23c0c95e52b95f0bc69f7fe9b55", size = 241627, upload-time = "2025-06-30T15:51:10.605Z" },
+ { url = "https://files.pythonhosted.org/packages/28/3d/35f33045e21034b388686213752cabc3a1b9d03e20969e6fa8f1b1d82db1/multidict-6.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05db2f66c9addb10cfa226e1acb363450fab2ff8a6df73c622fefe2f5af6d4e7", size = 239351, upload-time = "2025-06-30T15:51:12.18Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/cc/ff84c03b95b430015d2166d9aae775a3985d757b94f6635010d0038d9241/multidict-6.6.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0db58da8eafb514db832a1b44f8fa7906fdd102f7d982025f816a93ba45e3dcb", size = 233429, upload-time = "2025-06-30T15:51:13.533Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/f0/8cd49a0b37bdea673a4b793c2093f2f4ba8e7c9d6d7c9bd672fd6d38cd11/multidict-6.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14117a41c8fdb3ee19c743b1c027da0736fdb79584d61a766da53d399b71176c", size = 243094, upload-time = "2025-06-30T15:51:14.815Z" },
+ { url = "https://files.pythonhosted.org/packages/96/19/5d9a0cfdafe65d82b616a45ae950975820289069f885328e8185e64283c2/multidict-6.6.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:877443eaaabcd0b74ff32ebeed6f6176c71850feb7d6a1d2db65945256ea535c", size = 248957, upload-time = "2025-06-30T15:51:16.076Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/dc/c90066151da87d1e489f147b9b4327927241e65f1876702fafec6729c014/multidict-6.6.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:70b72e749a4f6e7ed8fb334fa8d8496384840319512746a5f42fa0aec79f4d61", size = 243590, upload-time = "2025-06-30T15:51:17.413Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/39/458afb0cccbb0ee9164365273be3e039efddcfcb94ef35924b7dbdb05db0/multidict-6.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43571f785b86afd02b3855c5ac8e86ec921b760298d6f82ff2a61daf5a35330b", size = 237487, upload-time = "2025-06-30T15:51:19.039Z" },
+ { url = "https://files.pythonhosted.org/packages/35/38/0016adac3990426610a081787011177e661875546b434f50a26319dc8372/multidict-6.6.3-cp310-cp310-win32.whl", hash = "sha256:20c5a0c3c13a15fd5ea86c42311859f970070e4e24de5a550e99d7c271d76318", size = 41390, upload-time = "2025-06-30T15:51:20.362Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d2/17897a8f3f2c5363d969b4c635aa40375fe1f09168dc09a7826780bfb2a4/multidict-6.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab0a34a007704c625e25a9116c6770b4d3617a071c8a7c30cd338dfbadfe6485", size = 45954, upload-time = "2025-06-30T15:51:21.383Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/5f/d4a717c1e457fe44072e33fa400d2b93eb0f2819c4d669381f925b7cba1f/multidict-6.6.3-cp310-cp310-win_arm64.whl", hash = "sha256:769841d70ca8bdd140a715746199fc6473414bd02efd678d75681d2d6a8986c5", size = 42981, upload-time = "2025-06-30T15:51:22.809Z" },
+ { url = "https://files.pythonhosted.org/packages/08/f0/1a39863ced51f639c81a5463fbfa9eb4df59c20d1a8769ab9ef4ca57ae04/multidict-6.6.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:18f4eba0cbac3546b8ae31e0bbc55b02c801ae3cbaf80c247fcdd89b456ff58c", size = 76445, upload-time = "2025-06-30T15:51:24.01Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/0e/a7cfa451c7b0365cd844e90b41e21fab32edaa1e42fc0c9f68461ce44ed7/multidict-6.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef43b5dd842382329e4797c46f10748d8c2b6e0614f46b4afe4aee9ac33159df", size = 44610, upload-time = "2025-06-30T15:51:25.158Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/bb/a14a4efc5ee748cc1904b0748be278c31b9295ce5f4d2ef66526f410b94d/multidict-6.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bd1fd5eec01494e0f2e8e446a74a85d5e49afb63d75a9934e4a5423dba21d", size = 44267, upload-time = "2025-06-30T15:51:26.326Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/f8/410677d563c2d55e063ef74fe578f9d53fe6b0a51649597a5861f83ffa15/multidict-6.6.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5bd8d6f793a787153956cd35e24f60485bf0651c238e207b9a54f7458b16d539", size = 230004, upload-time = "2025-06-30T15:51:27.491Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/df/2b787f80059314a98e1ec6a4cc7576244986df3e56b3c755e6fc7c99e038/multidict-6.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf99b4daf908c73856bd87ee0a2499c3c9a3d19bb04b9c6025e66af3fd07462", size = 247196, upload-time = "2025-06-30T15:51:28.762Z" },
+ { url = "https://files.pythonhosted.org/packages/05/f2/f9117089151b9a8ab39f9019620d10d9718eec2ac89e7ca9d30f3ec78e96/multidict-6.6.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b9e59946b49dafaf990fd9c17ceafa62976e8471a14952163d10a7a630413a9", size = 225337, upload-time = "2025-06-30T15:51:30.025Z" },
+ { url = "https://files.pythonhosted.org/packages/93/2d/7115300ec5b699faa152c56799b089a53ed69e399c3c2d528251f0aeda1a/multidict-6.6.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e2db616467070d0533832d204c54eea6836a5e628f2cb1e6dfd8cd6ba7277cb7", size = 257079, upload-time = "2025-06-30T15:51:31.716Z" },
+ { url = "https://files.pythonhosted.org/packages/15/ea/ff4bab367623e39c20d3b07637225c7688d79e4f3cc1f3b9f89867677f9a/multidict-6.6.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7394888236621f61dcdd25189b2768ae5cc280f041029a5bcf1122ac63df79f9", size = 255461, upload-time = "2025-06-30T15:51:33.029Z" },
+ { url = "https://files.pythonhosted.org/packages/74/07/2c9246cda322dfe08be85f1b8739646f2c4c5113a1422d7a407763422ec4/multidict-6.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f114d8478733ca7388e7c7e0ab34b72547476b97009d643644ac33d4d3fe1821", size = 246611, upload-time = "2025-06-30T15:51:34.47Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/62/279c13d584207d5697a752a66ffc9bb19355a95f7659140cb1b3cf82180e/multidict-6.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cdf22e4db76d323bcdc733514bf732e9fb349707c98d341d40ebcc6e9318ef3d", size = 243102, upload-time = "2025-06-30T15:51:36.525Z" },
+ { url = "https://files.pythonhosted.org/packages/69/cc/e06636f48c6d51e724a8bc8d9e1db5f136fe1df066d7cafe37ef4000f86a/multidict-6.6.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e995a34c3d44ab511bfc11aa26869b9d66c2d8c799fa0e74b28a473a692532d6", size = 238693, upload-time = "2025-06-30T15:51:38.278Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a4/66c9d8fb9acf3b226cdd468ed009537ac65b520aebdc1703dd6908b19d33/multidict-6.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766a4a5996f54361d8d5a9050140aa5362fe48ce51c755a50c0bc3706460c430", size = 246582, upload-time = "2025-06-30T15:51:39.709Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/01/c69e0317be556e46257826d5449feb4e6aa0d18573e567a48a2c14156f1f/multidict-6.6.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3893a0d7d28a7fe6ca7a1f760593bc13038d1d35daf52199d431b61d2660602b", size = 253355, upload-time = "2025-06-30T15:51:41.013Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/da/9cc1da0299762d20e626fe0042e71b5694f9f72d7d3f9678397cbaa71b2b/multidict-6.6.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:934796c81ea996e61914ba58064920d6cad5d99140ac3167901eb932150e2e56", size = 247774, upload-time = "2025-06-30T15:51:42.291Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/91/b22756afec99cc31105ddd4a52f95ab32b1a4a58f4d417979c570c4a922e/multidict-6.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ed948328aec2072bc00f05d961ceadfd3e9bfc2966c1319aeaf7b7c21219183", size = 242275, upload-time = "2025-06-30T15:51:43.642Z" },
+ { url = "https://files.pythonhosted.org/packages/be/f1/adcc185b878036a20399d5be5228f3cbe7f823d78985d101d425af35c800/multidict-6.6.3-cp311-cp311-win32.whl", hash = "sha256:9f5b28c074c76afc3e4c610c488e3493976fe0e596dd3db6c8ddfbb0134dcac5", size = 41290, upload-time = "2025-06-30T15:51:45.264Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/d4/27652c1c6526ea6b4f5ddd397e93f4232ff5de42bea71d339bc6a6cc497f/multidict-6.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc7f6fbc61b1c16050a389c630da0b32fc6d4a3d191394ab78972bf5edc568c2", size = 45942, upload-time = "2025-06-30T15:51:46.377Z" },
+ { url = "https://files.pythonhosted.org/packages/16/18/23f4932019804e56d3c2413e237f866444b774b0263bcb81df2fdecaf593/multidict-6.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:d4e47d8faffaae822fb5cba20937c048d4f734f43572e7079298a6c39fb172cb", size = 42880, upload-time = "2025-06-30T15:51:47.561Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6", size = 76514, upload-time = "2025-06-30T15:51:48.728Z" },
+ { url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f", size = 45394, upload-time = "2025-06-30T15:51:49.986Z" },
+ { url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55", size = 43590, upload-time = "2025-06-30T15:51:51.331Z" },
+ { url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b", size = 237292, upload-time = "2025-06-30T15:51:52.584Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888", size = 258385, upload-time = "2025-06-30T15:51:53.913Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d", size = 242328, upload-time = "2025-06-30T15:51:55.672Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680", size = 268057, upload-time = "2025-06-30T15:51:57.037Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a", size = 269341, upload-time = "2025-06-30T15:51:59.111Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961", size = 256081, upload-time = "2025-06-30T15:52:00.533Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65", size = 253581, upload-time = "2025-06-30T15:52:02.43Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643", size = 250750, upload-time = "2025-06-30T15:52:04.26Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063", size = 251548, upload-time = "2025-06-30T15:52:06.002Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3", size = 262718, upload-time = "2025-06-30T15:52:07.707Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75", size = 259603, upload-time = "2025-06-30T15:52:09.58Z" },
+ { url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10", size = 251351, upload-time = "2025-06-30T15:52:10.947Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5", size = 41860, upload-time = "2025-06-30T15:52:12.334Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17", size = 45982, upload-time = "2025-06-30T15:52:13.6Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b", size = 43210, upload-time = "2025-06-30T15:52:14.893Z" },
+ { url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", size = 75843, upload-time = "2025-06-30T15:52:16.155Z" },
+ { url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", size = 45053, upload-time = "2025-06-30T15:52:17.429Z" },
+ { url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", size = 43273, upload-time = "2025-06-30T15:52:19.346Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/fe/6eb68927e823999e3683bc49678eb20374ba9615097d085298fd5b386564/multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3", size = 237124, upload-time = "2025-06-30T15:52:20.773Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/ab/320d8507e7726c460cb77117848b3834ea0d59e769f36fdae495f7669929/multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c", size = 256892, upload-time = "2025-06-30T15:52:22.242Z" },
+ { url = "https://files.pythonhosted.org/packages/76/60/38ee422db515ac69834e60142a1a69111ac96026e76e8e9aa347fd2e4591/multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6", size = 240547, upload-time = "2025-06-30T15:52:23.736Z" },
+ { url = "https://files.pythonhosted.org/packages/27/fb/905224fde2dff042b030c27ad95a7ae744325cf54b890b443d30a789b80e/multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8", size = 266223, upload-time = "2025-06-30T15:52:25.185Z" },
+ { url = "https://files.pythonhosted.org/packages/76/35/dc38ab361051beae08d1a53965e3e1a418752fc5be4d3fb983c5582d8784/multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca", size = 267262, upload-time = "2025-06-30T15:52:26.969Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/a3/0a485b7f36e422421b17e2bbb5a81c1af10eac1d4476f2ff92927c730479/multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884", size = 254345, upload-time = "2025-06-30T15:52:28.467Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/59/bcdd52c1dab7c0e0d75ff19cac751fbd5f850d1fc39172ce809a74aa9ea4/multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7", size = 252248, upload-time = "2025-06-30T15:52:29.938Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/a4/2d96aaa6eae8067ce108d4acee6f45ced5728beda55c0f02ae1072c730d1/multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b", size = 250115, upload-time = "2025-06-30T15:52:31.416Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d2/ed9f847fa5c7d0677d4f02ea2c163d5e48573de3f57bacf5670e43a5ffaa/multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c", size = 249649, upload-time = "2025-06-30T15:52:32.996Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/af/9155850372563fc550803d3f25373308aa70f59b52cff25854086ecb4a79/multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b", size = 261203, upload-time = "2025-06-30T15:52:34.521Z" },
+ { url = "https://files.pythonhosted.org/packages/36/2f/c6a728f699896252cf309769089568a33c6439626648843f78743660709d/multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1", size = 258051, upload-time = "2025-06-30T15:52:35.999Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/60/689880776d6b18fa2b70f6cc74ff87dd6c6b9b47bd9cf74c16fecfaa6ad9/multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6", size = 249601, upload-time = "2025-06-30T15:52:37.473Z" },
+ { url = "https://files.pythonhosted.org/packages/75/5e/325b11f2222a549019cf2ef879c1f81f94a0d40ace3ef55cf529915ba6cc/multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e", size = 41683, upload-time = "2025-06-30T15:52:38.927Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/ad/cf46e73f5d6e3c775cabd2a05976547f3f18b39bee06260369a42501f053/multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9", size = 45811, upload-time = "2025-06-30T15:52:40.207Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/c9/2e3fe950db28fb7c62e1a5f46e1e38759b072e2089209bc033c2798bb5ec/multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600", size = 43056, upload-time = "2025-06-30T15:52:41.575Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134", size = 82811, upload-time = "2025-06-30T15:52:43.281Z" },
+ { url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37", size = 48304, upload-time = "2025-06-30T15:52:45.026Z" },
+ { url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8", size = 46775, upload-time = "2025-06-30T15:52:46.459Z" },
+ { url = "https://files.pythonhosted.org/packages/49/ba/9fcc1b332f67cc0c0c8079e263bfab6660f87fe4e28a35921771ff3eea0d/multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1", size = 229773, upload-time = "2025-06-30T15:52:47.88Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/14/0145a251f555f7c754ce2dcbcd012939bbd1f34f066fa5d28a50e722a054/multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373", size = 250083, upload-time = "2025-06-30T15:52:49.366Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/d4/d5c0bd2bbb173b586c249a151a26d2fb3ec7d53c96e42091c9fef4e1f10c/multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e", size = 228980, upload-time = "2025-06-30T15:52:50.903Z" },
+ { url = "https://files.pythonhosted.org/packages/21/32/c9a2d8444a50ec48c4733ccc67254100c10e1c8ae8e40c7a2d2183b59b97/multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f", size = 257776, upload-time = "2025-06-30T15:52:52.764Z" },
+ { url = "https://files.pythonhosted.org/packages/68/d0/14fa1699f4ef629eae08ad6201c6b476098f5efb051b296f4c26be7a9fdf/multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0", size = 256882, upload-time = "2025-06-30T15:52:54.596Z" },
+ { url = "https://files.pythonhosted.org/packages/da/88/84a27570fbe303c65607d517a5f147cd2fc046c2d1da02b84b17b9bdc2aa/multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc", size = 247816, upload-time = "2025-06-30T15:52:56.175Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/60/dca352a0c999ce96a5d8b8ee0b2b9f729dcad2e0b0c195f8286269a2074c/multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f", size = 245341, upload-time = "2025-06-30T15:52:57.752Z" },
+ { url = "https://files.pythonhosted.org/packages/50/ef/433fa3ed06028f03946f3993223dada70fb700f763f70c00079533c34578/multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471", size = 235854, upload-time = "2025-06-30T15:52:59.74Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/1f/487612ab56fbe35715320905215a57fede20de7db40a261759690dc80471/multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2", size = 243432, upload-time = "2025-06-30T15:53:01.602Z" },
+ { url = "https://files.pythonhosted.org/packages/da/6f/ce8b79de16cd885c6f9052c96a3671373d00c59b3ee635ea93e6e81b8ccf/multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648", size = 252731, upload-time = "2025-06-30T15:53:03.517Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/fe/a2514a6aba78e5abefa1624ca85ae18f542d95ac5cde2e3815a9fbf369aa/multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d", size = 247086, upload-time = "2025-06-30T15:53:05.48Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/22/b788718d63bb3cce752d107a57c85fcd1a212c6c778628567c9713f9345a/multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c", size = 243338, upload-time = "2025-06-30T15:53:07.522Z" },
+ { url = "https://files.pythonhosted.org/packages/22/d6/fdb3d0670819f2228f3f7d9af613d5e652c15d170c83e5f1c94fbc55a25b/multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e", size = 47812, upload-time = "2025-06-30T15:53:09.263Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d", size = 53011, upload-time = "2025-06-30T15:53:11.038Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb", size = 45254, upload-time = "2025-06-30T15:53:12.421Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" },
+]
+
+[[package]]
+name = "mypy"
+version = "1.16.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "mypy-extensions" },
+ { name = "pathspec" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/12/2bf23a80fcef5edb75de9a1e295d778e0f46ea89eb8b115818b663eff42b/mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a", size = 10958644, upload-time = "2025-06-16T16:51:11.649Z" },
+ { url = "https://files.pythonhosted.org/packages/08/50/bfe47b3b278eacf348291742fd5e6613bbc4b3434b72ce9361896417cfe5/mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72", size = 10087033, upload-time = "2025-06-16T16:35:30.089Z" },
+ { url = "https://files.pythonhosted.org/packages/21/de/40307c12fe25675a0776aaa2cdd2879cf30d99eec91b898de00228dc3ab5/mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea", size = 11875645, upload-time = "2025-06-16T16:35:48.49Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/d8/85bdb59e4a98b7a31495bd8f1a4445d8ffc86cde4ab1f8c11d247c11aedc/mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574", size = 12616986, upload-time = "2025-06-16T16:48:39.526Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/d0/bb25731158fa8f8ee9e068d3e94fcceb4971fedf1424248496292512afe9/mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d", size = 12878632, upload-time = "2025-06-16T16:36:08.195Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/11/822a9beb7a2b825c0cb06132ca0a5183f8327a5e23ef89717c9474ba0bc6/mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6", size = 9484391, upload-time = "2025-06-16T16:37:56.151Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" },
+ { url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" },
+ { url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" },
+ { url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" },
+ { url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" },
+ { url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" },
+ { url = "https://files.pythonhosted.org/packages/28/e3/96964af4a75a949e67df4b95318fe2b7427ac8189bbc3ef28f92a1c5bc56/mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438", size = 11063480, upload-time = "2025-06-16T16:47:56.205Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/4d/cd1a42b8e5be278fab7010fb289d9307a63e07153f0ae1510a3d7b703193/mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536", size = 10090538, upload-time = "2025-06-16T16:46:43.92Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/4f/c3c6b4b66374b5f68bab07c8cabd63a049ff69796b844bc759a0ca99bb2a/mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f", size = 11836839, upload-time = "2025-06-16T16:36:28.039Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/7e/81ca3b074021ad9775e5cb97ebe0089c0f13684b066a750b7dc208438403/mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359", size = 12715634, upload-time = "2025-06-16T16:50:34.441Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/95/bdd40c8be346fa4c70edb4081d727a54d0a05382d84966869738cfa8a497/mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be", size = 12895584, upload-time = "2025-06-16T16:34:54.857Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/fd/d486a0827a1c597b3b48b1bdef47228a6e9ee8102ab8c28f944cb83b65dc/mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee", size = 9573886, upload-time = "2025-06-16T16:36:43.589Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "nexus-rpc"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2e/f2/d54f5c03d8f4672ccc0875787a385f53dcb61f98a8ae594b5620e85b9cb3/nexus_rpc-1.3.0.tar.gz", hash = "sha256:e56d3b57b60d707ce7a72f83f23f106b86eca1043aa658e44582ab5ff30ab9ad", size = 75650, upload-time = "2025-12-08T22:59:13.002Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d6/74/0afd841de3199c148146c1d43b4bfb5605b2f1dc4c9a9087fe395091ea5a/nexus_rpc-1.3.0-py3-none-any.whl", hash = "sha256:aee0707b4861b22d8124ecb3f27d62dafbe8777dc50c66c91e49c006f971b92d", size = 28873, upload-time = "2025-12-08T22:59:12.024Z" },
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.9.1"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "1.26.4"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" },
+ { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" },
+ { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" },
+ { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" },
+ { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" },
+ { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" },
+ { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" },
+ { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" },
+ { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" },
+ { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" },
+ { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" },
+ { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" },
+ { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" },
+ { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" },
+]
+
+[[package]]
+name = "openai"
+version = "1.108.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "anyio" },
+ { name = "distro" },
+ { name = "httpx" },
+ { name = "jiter" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "tqdm" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/25/7a/3f2fbdf82a22d48405c1872f7c3176a705eee80ff2d2715d29472089171f/openai-1.108.1.tar.gz", hash = "sha256:6648468c1aec4eacfa554001e933a9fa075f57bacfc27588c2e34456cee9fef9", size = 563735, upload-time = "2025-09-19T16:52:20.399Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/87/6ad18ce0e7b910e3706480451df48ff9e0af3b55e5db565adafd68a0706a/openai-1.108.1-py3-none-any.whl", hash = "sha256:952fc027e300b2ac23be92b064eac136a2bc58274cec16f5d2906c361340d59b", size = 948394, upload-time = "2025-09-19T16:52:18.369Z" },
+]
+
+[[package]]
+name = "openai-agents"
+version = "0.3.2"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "griffe" },
+ { name = "mcp" },
+ { name = "openai" },
+ { name = "pydantic" },
+ { name = "requests" },
+ { name = "types-requests" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8c/9f/dafa9f80653778179822e1abf77c7f0d9da5a16806c96b5bb9e0e46bd747/openai_agents-0.3.2.tar.gz", hash = "sha256:b71ac04ee9f502f1bc0f4d142407df4ec69db4442db86c4da252b4558fa90cd5", size = 1727988, upload-time = "2025-09-23T20:37:20.7Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/7e/6a8437f9f40937bb473ceb120a65e1b37bc87bcee6da67be4c05b25c6a89/openai_agents-0.3.2-py3-none-any.whl", hash = "sha256:55e02c57f2aaf3170ff0aa0ab7c337c28fd06b43b3bb9edc28b77ffd8142b425", size = 194221, upload-time = "2025-09-23T20:37:19.121Z" },
+]
+
+[package.optional-dependencies]
+litellm = [
+ { name = "litellm" },
+]
+
+[[package]]
+name = "opentelemetry-api"
+version = "1.35.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "importlib-metadata" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/99/c9/4509bfca6bb43220ce7f863c9f791e0d5001c2ec2b5867d48586008b3d96/opentelemetry_api-1.35.0.tar.gz", hash = "sha256:a111b959bcfa5b4d7dffc2fbd6a241aa72dd78dd8e79b5b1662bda896c5d2ffe", size = 64778, upload-time = "2025-07-11T12:23:28.804Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/5a/3f8d078dbf55d18442f6a2ecedf6786d81d7245844b2b20ce2b8ad6f0307/opentelemetry_api-1.35.0-py3-none-any.whl", hash = "sha256:c4ea7e258a244858daf18474625e9cc0149b8ee354f37843415771a40c25ee06", size = 65566, upload-time = "2025-07-11T12:23:07.944Z" },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp-proto-common"
+version = "1.35.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "opentelemetry-proto" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/56/d1/887f860529cba7fc3aba2f6a3597fefec010a17bd1b126810724707d9b51/opentelemetry_exporter_otlp_proto_common-1.35.0.tar.gz", hash = "sha256:6f6d8c39f629b9fa5c79ce19a2829dbd93034f8ac51243cdf40ed2196f00d7eb", size = 20299, upload-time = "2025-07-11T12:23:31.046Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/2c/e31dd3c719bff87fa77391eb7f38b1430d22868c52312cba8aad60f280e5/opentelemetry_exporter_otlp_proto_common-1.35.0-py3-none-any.whl", hash = "sha256:863465de697ae81279ede660f3918680b4480ef5f69dcdac04f30722ed7b74cc", size = 18349, upload-time = "2025-07-11T12:23:11.713Z" },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp-proto-grpc"
+version = "1.35.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "googleapis-common-protos" },
+ { name = "grpcio" },
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-exporter-otlp-proto-common" },
+ { name = "opentelemetry-proto" },
+ { name = "opentelemetry-sdk" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/20/de/222e4f2f8cd39250991f84d76b661534aef457cafc6a3eb3fcd513627698/opentelemetry_exporter_otlp_proto_grpc-1.35.0.tar.gz", hash = "sha256:ac4c2c3aa5674642db0df0091ab43ec08bbd91a9be469c8d9b18923eb742b9cc", size = 23794, upload-time = "2025-07-11T12:23:31.662Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/a6/3f60a77279e6a3dc21fc076dcb51be159a633b0bba5cba9fb804062a9332/opentelemetry_exporter_otlp_proto_grpc-1.35.0-py3-none-any.whl", hash = "sha256:ee31203eb3e50c7967b8fa71db366cc355099aca4e3726e489b248cdb2fd5a62", size = 18846, upload-time = "2025-07-11T12:23:12.957Z" },
+]
+
+[[package]]
+name = "opentelemetry-proto"
+version = "1.35.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/dc/a2/7366e32d9a2bccbb8614942dbea2cf93c209610385ea966cb050334f8df7/opentelemetry_proto-1.35.0.tar.gz", hash = "sha256:532497341bd3e1c074def7c5b00172601b28bb83b48afc41a4b779f26eb4ee05", size = 46151, upload-time = "2025-07-11T12:23:38.797Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/a7/3f05de580da7e8a8b8dff041d3d07a20bf3bb62d3bcc027f8fd669a73ff4/opentelemetry_proto-1.35.0-py3-none-any.whl", hash = "sha256:98fffa803164499f562718384e703be8d7dfbe680192279a0429cb150a2f8809", size = 72536, upload-time = "2025-07-11T12:23:23.247Z" },
+]
+
+[[package]]
+name = "opentelemetry-sdk"
+version = "1.35.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9a/cf/1eb2ed2ce55e0a9aa95b3007f26f55c7943aeef0a783bb006bdd92b3299e/opentelemetry_sdk-1.35.0.tar.gz", hash = "sha256:2a400b415ab68aaa6f04e8a6a9f6552908fb3090ae2ff78d6ae0c597ac581954", size = 160871, upload-time = "2025-07-11T12:23:39.566Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/4f/8e32b757ef3b660511b638ab52d1ed9259b666bdeeceba51a082ce3aea95/opentelemetry_sdk-1.35.0-py3-none-any.whl", hash = "sha256:223d9e5f5678518f4842311bb73966e0b6db5d1e0b74e35074c052cd2487f800", size = 119379, upload-time = "2025-07-11T12:23:24.521Z" },
+]
+
+[[package]]
+name = "opentelemetry-semantic-conventions"
+version = "0.56b0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/32/8e/214fa817f63b9f068519463d8ab46afd5d03b98930c39394a37ae3e741d0/opentelemetry_semantic_conventions-0.56b0.tar.gz", hash = "sha256:c114c2eacc8ff6d3908cb328c811eaf64e6d68623840be9224dc829c4fd6c2ea", size = 124221, upload-time = "2025-07-11T12:23:40.71Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/3f/e80c1b017066a9d999efffe88d1cce66116dcf5cb7f80c41040a83b6e03b/opentelemetry_semantic_conventions-0.56b0-py3-none-any.whl", hash = "sha256:df44492868fd6b482511cc43a942e7194be64e94945f572db24df2e279a001a2", size = 201625, upload-time = "2025-07-11T12:23:25.63Z" },
+]
+
+[[package]]
+name = "orjson"
+version = "3.11.4"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/c6/fe/ed708782d6709cc60eb4c2d8a361a440661f74134675c72990f2c48c785f/orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d", size = 5945188, upload-time = "2025-10-24T15:50:38.027Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/30/5aed63d5af1c8b02fbd2a8d83e2a6c8455e30504c50dbf08c8b51403d873/orjson-3.11.4-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e3aa2118a3ece0d25489cbe48498de8a5d580e42e8d9979f65bf47900a15aba1", size = 243870, upload-time = "2025-10-24T15:48:28.908Z" },
+ { url = "https://files.pythonhosted.org/packages/44/1f/da46563c08bef33c41fd63c660abcd2184b4d2b950c8686317d03b9f5f0c/orjson-3.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a69ab657a4e6733133a3dca82768f2f8b884043714e8d2b9ba9f52b6efef5c44", size = 130622, upload-time = "2025-10-24T15:48:31.361Z" },
+ { url = "https://files.pythonhosted.org/packages/02/bd/b551a05d0090eab0bf8008a13a14edc0f3c3e0236aa6f5b697760dd2817b/orjson-3.11.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3740bffd9816fc0326ddc406098a3a8f387e42223f5f455f2a02a9f834ead80c", size = 129344, upload-time = "2025-10-24T15:48:32.71Z" },
+ { url = "https://files.pythonhosted.org/packages/87/6c/9ddd5e609f443b2548c5e7df3c44d0e86df2c68587a0e20c50018cdec535/orjson-3.11.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65fd2f5730b1bf7f350c6dc896173d3460d235c4be007af73986d7cd9a2acd23", size = 136633, upload-time = "2025-10-24T15:48:34.128Z" },
+ { url = "https://files.pythonhosted.org/packages/95/f2/9f04f2874c625a9fb60f6918c33542320661255323c272e66f7dcce14df2/orjson-3.11.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fdc3ae730541086158d549c97852e2eea6820665d4faf0f41bf99df41bc11ea", size = 137695, upload-time = "2025-10-24T15:48:35.654Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/c2/c7302afcbdfe8a891baae0e2cee091583a30e6fa613e8bdf33b0e9c8a8c7/orjson-3.11.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e10b4d65901da88845516ce9f7f9736f9638d19a1d483b3883dc0182e6e5edba", size = 136879, upload-time = "2025-10-24T15:48:37.483Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/3a/b31c8f0182a3e27f48e703f46e61bb769666cd0dac4700a73912d07a1417/orjson-3.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb6a03a678085f64b97f9d4a9ae69376ce91a3a9e9b56a82b1580d8e1d501aff", size = 136374, upload-time = "2025-10-24T15:48:38.624Z" },
+ { url = "https://files.pythonhosted.org/packages/29/d0/fd9ab96841b090d281c46df566b7f97bc6c8cd9aff3f3ebe99755895c406/orjson-3.11.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c82e4f0b1c712477317434761fbc28b044c838b6b1240d895607441412371ac", size = 140519, upload-time = "2025-10-24T15:48:39.756Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/ce/36eb0f15978bb88e33a3480e1a3fb891caa0f189ba61ce7713e0ccdadabf/orjson-3.11.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d58c166a18f44cc9e2bad03a327dc2d1a3d2e85b847133cfbafd6bfc6719bd79", size = 406522, upload-time = "2025-10-24T15:48:41.198Z" },
+ { url = "https://files.pythonhosted.org/packages/85/11/e8af3161a288f5c6a00c188fc729c7ba193b0cbc07309a1a29c004347c30/orjson-3.11.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94f206766bf1ea30e1382e4890f763bd1eefddc580e08fec1ccdc20ddd95c827", size = 149790, upload-time = "2025-10-24T15:48:42.664Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/96/209d52db0cf1e10ed48d8c194841e383e23c2ced5a2ee766649fe0e32d02/orjson-3.11.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:41bf25fb39a34cf8edb4398818523277ee7096689db352036a9e8437f2f3ee6b", size = 140040, upload-time = "2025-10-24T15:48:44.042Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/0e/526db1395ccb74c3d59ac1660b9a325017096dc5643086b38f27662b4add/orjson-3.11.4-cp310-cp310-win32.whl", hash = "sha256:fa9627eba4e82f99ca6d29bc967f09aba446ee2b5a1ea728949ede73d313f5d3", size = 135955, upload-time = "2025-10-24T15:48:45.495Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/69/18a778c9de3702b19880e73c9866b91cc85f904b885d816ba1ab318b223c/orjson-3.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:23ef7abc7fca96632d8174ac115e668c1e931b8fe4dde586e92a500bf1914dcc", size = 131577, upload-time = "2025-10-24T15:48:46.609Z" },
+ { url = "https://files.pythonhosted.org/packages/63/1d/1ea6005fffb56715fd48f632611e163d1604e8316a5bad2288bee9a1c9eb/orjson-3.11.4-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e59d23cd93ada23ec59a96f215139753fbfe3a4d989549bcb390f8c00370b39", size = 243498, upload-time = "2025-10-24T15:48:48.101Z" },
+ { url = "https://files.pythonhosted.org/packages/37/d7/ffed10c7da677f2a9da307d491b9eb1d0125b0307019c4ad3d665fd31f4f/orjson-3.11.4-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5c3aedecfc1beb988c27c79d52ebefab93b6c3921dbec361167e6559aba2d36d", size = 128961, upload-time = "2025-10-24T15:48:49.571Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/96/3e4d10a18866d1368f73c8c44b7fe37cc8a15c32f2a7620be3877d4c55a3/orjson-3.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9e5301f1c2caa2a9a4a303480d79c9ad73560b2e7761de742ab39fe59d9175", size = 130321, upload-time = "2025-10-24T15:48:50.713Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/1f/465f66e93f434f968dd74d5b623eb62c657bdba2332f5a8be9f118bb74c7/orjson-3.11.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8873812c164a90a79f65368f8f96817e59e35d0cc02786a5356f0e2abed78040", size = 129207, upload-time = "2025-10-24T15:48:52.193Z" },
+ { url = "https://files.pythonhosted.org/packages/28/43/d1e94837543321c119dff277ae8e348562fe8c0fafbb648ef7cb0c67e521/orjson-3.11.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d7feb0741ebb15204e748f26c9638e6665a5fa93c37a2c73d64f1669b0ddc63", size = 136323, upload-time = "2025-10-24T15:48:54.806Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/04/93303776c8890e422a5847dd012b4853cdd88206b8bbd3edc292c90102d1/orjson-3.11.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ee5487fefee21e6910da4c2ee9eef005bee568a0879834df86f888d2ffbdd9", size = 137440, upload-time = "2025-10-24T15:48:56.326Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/ef/75519d039e5ae6b0f34d0336854d55544ba903e21bf56c83adc51cd8bf82/orjson-3.11.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d40d46f348c0321df01507f92b95a377240c4ec31985225a6668f10e2676f9a", size = 136680, upload-time = "2025-10-24T15:48:57.476Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/18/bf8581eaae0b941b44efe14fee7b7862c3382fbc9a0842132cfc7cf5ecf4/orjson-3.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95713e5fc8af84d8edc75b785d2386f653b63d62b16d681687746734b4dfc0be", size = 136160, upload-time = "2025-10-24T15:48:59.631Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/35/a6d582766d351f87fc0a22ad740a641b0a8e6fc47515e8614d2e4790ae10/orjson-3.11.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad73ede24f9083614d6c4ca9a85fe70e33be7bf047ec586ee2363bc7418fe4d7", size = 140318, upload-time = "2025-10-24T15:49:00.834Z" },
+ { url = "https://files.pythonhosted.org/packages/76/b3/5a4801803ab2e2e2d703bce1a56540d9f99a9143fbec7bf63d225044fef8/orjson-3.11.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:842289889de515421f3f224ef9c1f1efb199a32d76d8d2ca2706fa8afe749549", size = 406330, upload-time = "2025-10-24T15:49:02.327Z" },
+ { url = "https://files.pythonhosted.org/packages/80/55/a8f682f64833e3a649f620eafefee175cbfeb9854fc5b710b90c3bca45df/orjson-3.11.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3b2427ed5791619851c52a1261b45c233930977e7de8cf36de05636c708fa905", size = 149580, upload-time = "2025-10-24T15:49:03.517Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/e4/c132fa0c67afbb3eb88274fa98df9ac1f631a675e7877037c611805a4413/orjson-3.11.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c36e524af1d29982e9b190573677ea02781456b2e537d5840e4538a5ec41907", size = 139846, upload-time = "2025-10-24T15:49:04.761Z" },
+ { url = "https://files.pythonhosted.org/packages/54/06/dc3491489efd651fef99c5908e13951abd1aead1257c67f16135f95ce209/orjson-3.11.4-cp311-cp311-win32.whl", hash = "sha256:87255b88756eab4a68ec61837ca754e5d10fa8bc47dc57f75cedfeaec358d54c", size = 135781, upload-time = "2025-10-24T15:49:05.969Z" },
+ { url = "https://files.pythonhosted.org/packages/79/b7/5e5e8d77bd4ea02a6ac54c42c818afb01dd31961be8a574eb79f1d2cfb1e/orjson-3.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:e2d5d5d798aba9a0e1fede8d853fa899ce2cb930ec0857365f700dffc2c7af6a", size = 131391, upload-time = "2025-10-24T15:49:07.355Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/dc/9484127cc1aa213be398ed735f5f270eedcb0c0977303a6f6ddc46b60204/orjson-3.11.4-cp311-cp311-win_arm64.whl", hash = "sha256:6bb6bb41b14c95d4f2702bce9975fda4516f1db48e500102fc4d8119032ff045", size = 126252, upload-time = "2025-10-24T15:49:08.869Z" },
+ { url = "https://files.pythonhosted.org/packages/63/51/6b556192a04595b93e277a9ff71cd0cc06c21a7df98bcce5963fa0f5e36f/orjson-3.11.4-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d4371de39319d05d3f482f372720b841c841b52f5385bd99c61ed69d55d9ab50", size = 243571, upload-time = "2025-10-24T15:49:10.008Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/2c/2602392ddf2601d538ff11848b98621cd465d1a1ceb9db9e8043181f2f7b/orjson-3.11.4-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e41fd3b3cac850eaae78232f37325ed7d7436e11c471246b87b2cd294ec94853", size = 128891, upload-time = "2025-10-24T15:49:11.297Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/47/bf85dcf95f7a3a12bf223394a4f849430acd82633848d52def09fa3f46ad/orjson-3.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600e0e9ca042878c7fdf189cf1b028fe2c1418cc9195f6cb9824eb6ed99cb938", size = 130137, upload-time = "2025-10-24T15:49:12.544Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/4d/a0cb31007f3ab6f1fd2a1b17057c7c349bc2baf8921a85c0180cc7be8011/orjson-3.11.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7bbf9b333f1568ef5da42bc96e18bf30fd7f8d54e9ae066d711056add508e415", size = 129152, upload-time = "2025-10-24T15:49:13.754Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/ef/2811def7ce3d8576b19e3929fff8f8f0d44bc5eb2e0fdecb2e6e6cc6c720/orjson-3.11.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4806363144bb6e7297b8e95870e78d30a649fdc4e23fc84daa80c8ebd366ce44", size = 136834, upload-time = "2025-10-24T15:49:15.307Z" },
+ { url = "https://files.pythonhosted.org/packages/00/d4/9aee9e54f1809cec8ed5abd9bc31e8a9631d19460e3b8470145d25140106/orjson-3.11.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad355e8308493f527d41154e9053b86a5be892b3b359a5c6d5d95cda23601cb2", size = 137519, upload-time = "2025-10-24T15:49:16.557Z" },
+ { url = "https://files.pythonhosted.org/packages/db/ea/67bfdb5465d5679e8ae8d68c11753aaf4f47e3e7264bad66dc2f2249e643/orjson-3.11.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a7517482667fb9f0ff1b2f16fe5829296ed7a655d04d68cd9711a4d8a4e708", size = 136749, upload-time = "2025-10-24T15:49:17.796Z" },
+ { url = "https://files.pythonhosted.org/packages/01/7e/62517dddcfce6d53a39543cd74d0dccfcbdf53967017c58af68822100272/orjson-3.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97eb5942c7395a171cbfecc4ef6701fc3c403e762194683772df4c54cfbb2210", size = 136325, upload-time = "2025-10-24T15:49:19.347Z" },
+ { url = "https://files.pythonhosted.org/packages/18/ae/40516739f99ab4c7ec3aaa5cc242d341fcb03a45d89edeeaabc5f69cb2cf/orjson-3.11.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:149d95d5e018bdd822e3f38c103b1a7c91f88d38a88aada5c4e9b3a73a244241", size = 140204, upload-time = "2025-10-24T15:49:20.545Z" },
+ { url = "https://files.pythonhosted.org/packages/82/18/ff5734365623a8916e3a4037fcef1cd1782bfc14cf0992afe7940c5320bf/orjson-3.11.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:624f3951181eb46fc47dea3d221554e98784c823e7069edb5dbd0dc826ac909b", size = 406242, upload-time = "2025-10-24T15:49:21.884Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/43/96436041f0a0c8c8deca6a05ebeaf529bf1de04839f93ac5e7c479807aec/orjson-3.11.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:03bfa548cf35e3f8b3a96c4e8e41f753c686ff3d8e182ce275b1751deddab58c", size = 150013, upload-time = "2025-10-24T15:49:23.185Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/48/78302d98423ed8780479a1e682b9aecb869e8404545d999d34fa486e573e/orjson-3.11.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:525021896afef44a68148f6ed8a8bf8375553d6066c7f48537657f64823565b9", size = 139951, upload-time = "2025-10-24T15:49:24.428Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/7b/ad613fdcdaa812f075ec0875143c3d37f8654457d2af17703905425981bf/orjson-3.11.4-cp312-cp312-win32.whl", hash = "sha256:b58430396687ce0f7d9eeb3dd47761ca7d8fda8e9eb92b3077a7a353a75efefa", size = 136049, upload-time = "2025-10-24T15:49:25.973Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/3c/9cf47c3ff5f39b8350fb21ba65d789b6a1129d4cbb3033ba36c8a9023520/orjson-3.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:c6dbf422894e1e3c80a177133c0dda260f81428f9de16d61041949f6a2e5c140", size = 131461, upload-time = "2025-10-24T15:49:27.259Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/3b/e2425f61e5825dc5b08c2a5a2b3af387eaaca22a12b9c8c01504f8614c36/orjson-3.11.4-cp312-cp312-win_arm64.whl", hash = "sha256:d38d2bc06d6415852224fcc9c0bfa834c25431e466dc319f0edd56cca81aa96e", size = 126167, upload-time = "2025-10-24T15:49:28.511Z" },
+ { url = "https://files.pythonhosted.org/packages/23/15/c52aa7112006b0f3d6180386c3a46ae057f932ab3425bc6f6ac50431cca1/orjson-3.11.4-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534", size = 243525, upload-time = "2025-10-24T15:49:29.737Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/38/05340734c33b933fd114f161f25a04e651b0c7c33ab95e9416ade5cb44b8/orjson-3.11.4-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff", size = 128871, upload-time = "2025-10-24T15:49:31.109Z" },
+ { url = "https://files.pythonhosted.org/packages/55/b9/ae8d34899ff0c012039b5a7cb96a389b2476e917733294e498586b45472d/orjson-3.11.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad", size = 130055, upload-time = "2025-10-24T15:49:33.382Z" },
+ { url = "https://files.pythonhosted.org/packages/33/aa/6346dd5073730451bee3681d901e3c337e7ec17342fb79659ec9794fc023/orjson-3.11.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5", size = 129061, upload-time = "2025-10-24T15:49:34.935Z" },
+ { url = "https://files.pythonhosted.org/packages/39/e4/8eea51598f66a6c853c380979912d17ec510e8e66b280d968602e680b942/orjson-3.11.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a", size = 136541, upload-time = "2025-10-24T15:49:36.923Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/47/cb8c654fa9adcc60e99580e17c32b9e633290e6239a99efa6b885aba9dbc/orjson-3.11.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436", size = 137535, upload-time = "2025-10-24T15:49:38.307Z" },
+ { url = "https://files.pythonhosted.org/packages/43/92/04b8cc5c2b729f3437ee013ce14a60ab3d3001465d95c184758f19362f23/orjson-3.11.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9", size = 136703, upload-time = "2025-10-24T15:49:40.795Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/fd/d0733fcb9086b8be4ebcfcda2d0312865d17d0d9884378b7cffb29d0763f/orjson-3.11.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73", size = 136293, upload-time = "2025-10-24T15:49:42.347Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/d7/3c5514e806837c210492d72ae30ccf050ce3f940f45bf085bab272699ef4/orjson-3.11.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0", size = 140131, upload-time = "2025-10-24T15:49:43.638Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/dd/ba9d32a53207babf65bd510ac4d0faaa818bd0df9a9c6f472fe7c254f2e3/orjson-3.11.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196", size = 406164, upload-time = "2025-10-24T15:49:45.498Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/f9/f68ad68f4af7c7bde57cd514eaa2c785e500477a8bc8f834838eb696a685/orjson-3.11.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a", size = 149859, upload-time = "2025-10-24T15:49:46.981Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/d2/7f847761d0c26818395b3d6b21fb6bc2305d94612a35b0a30eae65a22728/orjson-3.11.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6", size = 139926, upload-time = "2025-10-24T15:49:48.321Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/37/acd14b12dc62db9a0e1d12386271b8661faae270b22492580d5258808975/orjson-3.11.4-cp313-cp313-win32.whl", hash = "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839", size = 136007, upload-time = "2025-10-24T15:49:49.938Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/a9/967be009ddf0a1fffd7a67de9c36656b28c763659ef91352acc02cbe364c/orjson-3.11.4-cp313-cp313-win_amd64.whl", hash = "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a", size = 131314, upload-time = "2025-10-24T15:49:51.248Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/db/399abd6950fbd94ce125cb8cd1a968def95174792e127b0642781e040ed4/orjson-3.11.4-cp313-cp313-win_arm64.whl", hash = "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de", size = 126152, upload-time = "2025-10-24T15:49:52.922Z" },
+ { url = "https://files.pythonhosted.org/packages/25/e3/54ff63c093cc1697e758e4fceb53164dd2661a7d1bcd522260ba09f54533/orjson-3.11.4-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803", size = 243501, upload-time = "2025-10-24T15:49:54.288Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/7d/e2d1076ed2e8e0ae9badca65bf7ef22710f93887b29eaa37f09850604e09/orjson-3.11.4-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54", size = 128862, upload-time = "2025-10-24T15:49:55.961Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/37/ca2eb40b90621faddfa9517dfe96e25f5ae4d8057a7c0cdd613c17e07b2c/orjson-3.11.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e", size = 130047, upload-time = "2025-10-24T15:49:57.406Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/62/1021ed35a1f2bad9040f05fa4cc4f9893410df0ba3eaa323ccf899b1c90a/orjson-3.11.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316", size = 129073, upload-time = "2025-10-24T15:49:58.782Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/3f/f84d966ec2a6fd5f73b1a707e7cd876813422ae4bf9f0145c55c9c6a0f57/orjson-3.11.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1", size = 136597, upload-time = "2025-10-24T15:50:00.12Z" },
+ { url = "https://files.pythonhosted.org/packages/32/78/4fa0aeca65ee82bbabb49e055bd03fa4edea33f7c080c5c7b9601661ef72/orjson-3.11.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc", size = 137515, upload-time = "2025-10-24T15:50:01.57Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/9d/0c102e26e7fde40c4c98470796d050a2ec1953897e2c8ab0cb95b0759fa2/orjson-3.11.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f", size = 136703, upload-time = "2025-10-24T15:50:02.944Z" },
+ { url = "https://files.pythonhosted.org/packages/df/ac/2de7188705b4cdfaf0b6c97d2f7849c17d2003232f6e70df98602173f788/orjson-3.11.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf", size = 136311, upload-time = "2025-10-24T15:50:04.441Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/52/847fcd1a98407154e944feeb12e3b4d487a0e264c40191fb44d1269cbaa1/orjson-3.11.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606", size = 140127, upload-time = "2025-10-24T15:50:07.398Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ae/21d208f58bdb847dd4d0d9407e2929862561841baa22bdab7aea10ca088e/orjson-3.11.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780", size = 406201, upload-time = "2025-10-24T15:50:08.796Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/55/0789d6de386c8366059db098a628e2ad8798069e94409b0d8935934cbcb9/orjson-3.11.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23", size = 149872, upload-time = "2025-10-24T15:50:10.234Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/1d/7ff81ea23310e086c17b41d78a72270d9de04481e6113dbe2ac19118f7fb/orjson-3.11.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155", size = 139931, upload-time = "2025-10-24T15:50:11.623Z" },
+ { url = "https://files.pythonhosted.org/packages/77/92/25b886252c50ed64be68c937b562b2f2333b45afe72d53d719e46a565a50/orjson-3.11.4-cp314-cp314-win32.whl", hash = "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394", size = 136065, upload-time = "2025-10-24T15:50:13.025Z" },
+ { url = "https://files.pythonhosted.org/packages/63/b8/718eecf0bb7e9d64e4956afaafd23db9f04c776d445f59fe94f54bdae8f0/orjson-3.11.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1", size = 131310, upload-time = "2025-10-24T15:50:14.46Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/bf/def5e25d4d8bfce296a9a7c8248109bf58622c21618b590678f945a2c59c/orjson-3.11.4-cp314-cp314-win_arm64.whl", hash = "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d", size = 126151, upload-time = "2025-10-24T15:50:15.878Z" },
+]
+
+[[package]]
+name = "outcome"
+version = "1.3.0.post0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "attrs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "23.2"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/fb/2b/9b9c33ffed44ee921d0967086d653047286054117d584f1b1a7c22ceaf7b/packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", size = 146714, upload-time = "2023-10-01T13:50:05.279Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/1a/610693ac4ee14fcdf2d9bf3c493370e4f2ef7ae2e19217d7a237ff42367d/packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7", size = 53011, upload-time = "2023-10-01T13:50:03.745Z" },
+]
+
+[[package]]
+name = "pandas"
+version = "2.3.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "numpy" },
+ { name = "python-dateutil" },
+ { name = "pytz" },
+ { name = "tzdata" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c4/ca/aa97b47287221fa37a49634532e520300088e290b20d690b21ce3e448143/pandas-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22c2e866f7209ebc3a8f08d75766566aae02bcc91d196935a1d9e59c7b990ac9", size = 11542731, upload-time = "2025-07-07T19:18:12.619Z" },
+ { url = "https://files.pythonhosted.org/packages/80/bf/7938dddc5f01e18e573dcfb0f1b8c9357d9b5fa6ffdee6e605b92efbdff2/pandas-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3583d348546201aff730c8c47e49bc159833f971c2899d6097bce68b9112a4f1", size = 10790031, upload-time = "2025-07-07T19:18:16.611Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/2f/9af748366763b2a494fed477f88051dbf06f56053d5c00eba652697e3f94/pandas-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f951fbb702dacd390561e0ea45cdd8ecfa7fb56935eb3dd78e306c19104b9b0", size = 11724083, upload-time = "2025-07-07T19:18:20.512Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/95/79ab37aa4c25d1e7df953dde407bb9c3e4ae47d154bc0dd1692f3a6dcf8c/pandas-2.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd05b72ec02ebfb993569b4931b2e16fbb4d6ad6ce80224a3ee838387d83a191", size = 12342360, upload-time = "2025-07-07T19:18:23.194Z" },
+ { url = "https://files.pythonhosted.org/packages/75/a7/d65e5d8665c12c3c6ff5edd9709d5836ec9b6f80071b7f4a718c6106e86e/pandas-2.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1b916a627919a247d865aed068eb65eb91a344b13f5b57ab9f610b7716c92de1", size = 13202098, upload-time = "2025-07-07T19:18:25.558Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f3/4c1dbd754dbaa79dbf8b537800cb2fa1a6e534764fef50ab1f7533226c5c/pandas-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fe67dc676818c186d5a3d5425250e40f179c2a89145df477dd82945eaea89e97", size = 13837228, upload-time = "2025-07-07T19:18:28.344Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/d6/d7f5777162aa9b48ec3910bca5a58c9b5927cfd9cfde3aa64322f5ba4b9f/pandas-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:2eb789ae0274672acbd3c575b0598d213345660120a257b47b5dafdc618aec83", size = 11336561, upload-time = "2025-07-07T19:18:31.211Z" },
+ { url = "https://files.pythonhosted.org/packages/76/1c/ccf70029e927e473a4476c00e0d5b32e623bff27f0402d0a92b7fc29bb9f/pandas-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b0540963d83431f5ce8870ea02a7430adca100cec8a050f0811f8e31035541b", size = 11566608, upload-time = "2025-07-07T19:18:33.86Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/d3/3c37cb724d76a841f14b8f5fe57e5e3645207cc67370e4f84717e8bb7657/pandas-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fe7317f578c6a153912bd2292f02e40c1d8f253e93c599e82620c7f69755c74f", size = 10823181, upload-time = "2025-07-07T19:18:36.151Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/4c/367c98854a1251940edf54a4df0826dcacfb987f9068abf3e3064081a382/pandas-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6723a27ad7b244c0c79d8e7007092d7c8f0f11305770e2f4cd778b3ad5f9f85", size = 11793570, upload-time = "2025-07-07T19:18:38.385Z" },
+ { url = "https://files.pythonhosted.org/packages/07/5f/63760ff107bcf5146eee41b38b3985f9055e710a72fdd637b791dea3495c/pandas-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3462c3735fe19f2638f2c3a40bd94ec2dc5ba13abbb032dd2fa1f540a075509d", size = 12378887, upload-time = "2025-07-07T19:18:41.284Z" },
+ { url = "https://files.pythonhosted.org/packages/15/53/f31a9b4dfe73fe4711c3a609bd8e60238022f48eacedc257cd13ae9327a7/pandas-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:98bcc8b5bf7afed22cc753a28bc4d9e26e078e777066bc53fac7904ddef9a678", size = 13230957, upload-time = "2025-07-07T19:18:44.187Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/94/6fce6bf85b5056d065e0a7933cba2616dcb48596f7ba3c6341ec4bcc529d/pandas-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d544806b485ddf29e52d75b1f559142514e60ef58a832f74fb38e48d757b299", size = 13883883, upload-time = "2025-07-07T19:18:46.498Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/7b/bdcb1ed8fccb63d04bdb7635161d0ec26596d92c9d7a6cce964e7876b6c1/pandas-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b3cd4273d3cb3707b6fffd217204c52ed92859533e31dc03b7c5008aa933aaab", size = 11340212, upload-time = "2025-07-07T19:18:49.293Z" },
+ { url = "https://files.pythonhosted.org/packages/46/de/b8445e0f5d217a99fe0eeb2f4988070908979bec3587c0633e5428ab596c/pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3", size = 11588172, upload-time = "2025-07-07T19:18:52.054Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/e0/801cdb3564e65a5ac041ab99ea6f1d802a6c325bb6e58c79c06a3f1cd010/pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232", size = 10717365, upload-time = "2025-07-07T19:18:54.785Z" },
+ { url = "https://files.pythonhosted.org/packages/51/a5/c76a8311833c24ae61a376dbf360eb1b1c9247a5d9c1e8b356563b31b80c/pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e", size = 11280411, upload-time = "2025-07-07T19:18:57.045Z" },
+ { url = "https://files.pythonhosted.org/packages/da/01/e383018feba0a1ead6cf5fe8728e5d767fee02f06a3d800e82c489e5daaf/pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4", size = 11988013, upload-time = "2025-07-07T19:18:59.771Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/14/cec7760d7c9507f11c97d64f29022e12a6cc4fc03ac694535e89f88ad2ec/pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8", size = 12767210, upload-time = "2025-07-07T19:19:02.944Z" },
+ { url = "https://files.pythonhosted.org/packages/50/b9/6e2d2c6728ed29fb3d4d4d302504fb66f1a543e37eb2e43f352a86365cdf/pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679", size = 13440571, upload-time = "2025-07-07T19:19:06.82Z" },
+ { url = "https://files.pythonhosted.org/packages/80/a5/3a92893e7399a691bad7664d977cb5e7c81cf666c81f89ea76ba2bff483d/pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8", size = 10987601, upload-time = "2025-07-07T19:19:09.589Z" },
+ { url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869, upload-time = "2025-07-07T19:19:19.265Z" },
+ { url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218, upload-time = "2025-07-07T19:19:21.547Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763, upload-time = "2025-07-07T19:19:23.939Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482, upload-time = "2025-07-07T19:19:42.699Z" },
+ { url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159, upload-time = "2025-07-07T19:19:26.362Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287, upload-time = "2025-07-07T19:19:29.157Z" },
+ { url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381, upload-time = "2025-07-07T19:19:31.436Z" },
+ { url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" },
+ { url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" },
+]
+
+[[package]]
+name = "pastel"
+version = "0.2.1"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555, upload-time = "2020-09-16T19:21:12.43Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "poethepoet"
+version = "0.36.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "pastel" },
+ { name = "pyyaml" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cf/ac/311c8a492dc887f0b7a54d0ec3324cb2f9538b7b78ea06e5f7ae1f167e52/poethepoet-0.36.0.tar.gz", hash = "sha256:2217b49cb4e4c64af0b42ff8c4814b17f02e107d38bc461542517348ede25663", size = 66854, upload-time = "2025-06-29T19:54:50.444Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/03/29/dedb3a6b7e17ea723143b834a2da428a7d743c80d5cd4d22ed28b5e8c441/poethepoet-0.36.0-py3-none-any.whl", hash = "sha256:693e3c1eae9f6731d3613c3c0c40f747d3c5c68a375beda42e590a63c5623308", size = 88031, upload-time = "2025-06-29T19:54:48.884Z" },
+]
+
+[[package]]
+name = "propcache"
+version = "0.3.2"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ab/14/510deed325e262afeb8b360043c5d7c960da7d3ecd6d6f9496c9c56dc7f4/propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770", size = 73178, upload-time = "2025-06-09T22:53:40.126Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/4e/ad52a7925ff01c1325653a730c7ec3175a23f948f08626a534133427dcff/propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3", size = 43133, upload-time = "2025-06-09T22:53:41.965Z" },
+ { url = "https://files.pythonhosted.org/packages/63/7c/e9399ba5da7780871db4eac178e9c2e204c23dd3e7d32df202092a1ed400/propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3", size = 43039, upload-time = "2025-06-09T22:53:43.268Z" },
+ { url = "https://files.pythonhosted.org/packages/22/e1/58da211eb8fdc6fc854002387d38f415a6ca5f5c67c1315b204a5d3e9d7a/propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e", size = 201903, upload-time = "2025-06-09T22:53:44.872Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/0a/550ea0f52aac455cb90111c8bab995208443e46d925e51e2f6ebdf869525/propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220", size = 213362, upload-time = "2025-06-09T22:53:46.707Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/af/9893b7d878deda9bb69fcf54600b247fba7317761b7db11fede6e0f28bd0/propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb", size = 210525, upload-time = "2025-06-09T22:53:48.547Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/bb/38fd08b278ca85cde36d848091ad2b45954bc5f15cce494bb300b9285831/propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614", size = 198283, upload-time = "2025-06-09T22:53:50.067Z" },
+ { url = "https://files.pythonhosted.org/packages/78/8c/9fe55bd01d362bafb413dfe508c48753111a1e269737fa143ba85693592c/propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50", size = 191872, upload-time = "2025-06-09T22:53:51.438Z" },
+ { url = "https://files.pythonhosted.org/packages/54/14/4701c33852937a22584e08abb531d654c8bcf7948a8f87ad0a4822394147/propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339", size = 199452, upload-time = "2025-06-09T22:53:53.229Z" },
+ { url = "https://files.pythonhosted.org/packages/16/44/447f2253d859602095356007657ee535e0093215ea0b3d1d6a41d16e5201/propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0", size = 191567, upload-time = "2025-06-09T22:53:54.541Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/b3/e4756258749bb2d3b46defcff606a2f47410bab82be5824a67e84015b267/propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2", size = 193015, upload-time = "2025-06-09T22:53:56.44Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/df/e6d3c7574233164b6330b9fd697beeac402afd367280e6dc377bb99b43d9/propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7", size = 204660, upload-time = "2025-06-09T22:53:57.839Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/53/e4d31dd5170b4a0e2e6b730f2385a96410633b4833dc25fe5dffd1f73294/propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b", size = 206105, upload-time = "2025-06-09T22:53:59.638Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/fe/74d54cf9fbe2a20ff786e5f7afcfde446588f0cf15fb2daacfbc267b866c/propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c", size = 196980, upload-time = "2025-06-09T22:54:01.071Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ec/c469c9d59dada8a7679625e0440b544fe72e99311a4679c279562051f6fc/propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70", size = 37679, upload-time = "2025-06-09T22:54:03.003Z" },
+ { url = "https://files.pythonhosted.org/packages/38/35/07a471371ac89d418f8d0b699c75ea6dca2041fbda360823de21f6a9ce0a/propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9", size = 41459, upload-time = "2025-06-09T22:54:04.134Z" },
+ { url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207, upload-time = "2025-06-09T22:54:05.399Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648, upload-time = "2025-06-09T22:54:08.023Z" },
+ { url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496, upload-time = "2025-06-09T22:54:09.228Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288, upload-time = "2025-06-09T22:54:10.466Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456, upload-time = "2025-06-09T22:54:11.828Z" },
+ { url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429, upload-time = "2025-06-09T22:54:13.823Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472, upload-time = "2025-06-09T22:54:15.232Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480, upload-time = "2025-06-09T22:54:17.104Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530, upload-time = "2025-06-09T22:54:18.512Z" },
+ { url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230, upload-time = "2025-06-09T22:54:19.947Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754, upload-time = "2025-06-09T22:54:21.716Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430, upload-time = "2025-06-09T22:54:23.17Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884, upload-time = "2025-06-09T22:54:25.539Z" },
+ { url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480, upload-time = "2025-06-09T22:54:26.892Z" },
+ { url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757, upload-time = "2025-06-09T22:54:28.241Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500, upload-time = "2025-06-09T22:54:29.4Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" },
+ { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" },
+ { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" },
+ { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" },
+ { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" },
+ { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" },
+ { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" },
+ { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" },
+ { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" },
+ { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" },
+ { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" },
+ { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" },
+ { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" },
+ { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" },
+ { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" },
+]
+
+[[package]]
+name = "protobuf"
+version = "5.29.5"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226, upload-time = "2025-05-28T23:51:59.82Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963, upload-time = "2025-05-28T23:51:41.204Z" },
+ { url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818, upload-time = "2025-05-28T23:51:44.297Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091, upload-time = "2025-05-28T23:51:45.907Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824, upload-time = "2025-05-28T23:51:47.545Z" },
+ { url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942, upload-time = "2025-05-28T23:51:49.11Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823, upload-time = "2025-05-28T23:51:58.157Z" },
+]
+
+[[package]]
+name = "pyarrow"
+version = "22.0.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/9b/cb3f7e0a345353def531ca879053e9ef6b9f38ed91aebcf68b09ba54dec0/pyarrow-22.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:77718810bd3066158db1e95a63c160ad7ce08c6b0710bc656055033e39cdad88", size = 34223968, upload-time = "2025-10-24T10:03:31.21Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/41/3184b8192a120306270c5307f105b70320fdaa592c99843c5ef78aaefdcf/pyarrow-22.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:44d2d26cda26d18f7af7db71453b7b783788322d756e81730acb98f24eb90ace", size = 35942085, upload-time = "2025-10-24T10:03:38.146Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/3d/a1eab2f6f08001f9fb714b8ed5cfb045e2fe3e3e3c0c221f2c9ed1e6d67d/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b9d71701ce97c95480fecb0039ec5bb889e75f110da72005743451339262f4ce", size = 44964613, upload-time = "2025-10-24T10:03:46.516Z" },
+ { url = "https://files.pythonhosted.org/packages/46/46/a1d9c24baf21cfd9ce994ac820a24608decf2710521b29223d4334985127/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710624ab925dc2b05a6229d47f6f0dac1c1155e6ed559be7109f684eba048a48", size = 47627059, upload-time = "2025-10-24T10:03:55.353Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/4c/f711acb13075c1391fd54bc17e078587672c575f8de2a6e62509af026dcf/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f963ba8c3b0199f9d6b794c90ec77545e05eadc83973897a4523c9e8d84e9340", size = 47947043, upload-time = "2025-10-24T10:04:05.408Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/70/1f3180dd7c2eab35c2aca2b29ace6c519f827dcd4cfeb8e0dca41612cf7a/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd0d42297ace400d8febe55f13fdf46e86754842b860c978dfec16f081e5c653", size = 50206505, upload-time = "2025-10-24T10:04:15.786Z" },
+ { url = "https://files.pythonhosted.org/packages/80/07/fea6578112c8c60ffde55883a571e4c4c6bc7049f119d6b09333b5cc6f73/pyarrow-22.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:00626d9dc0f5ef3a75fe63fd68b9c7c8302d2b5bbc7f74ecaedba83447a24f84", size = 28101641, upload-time = "2025-10-24T10:04:22.57Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" },
+ { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" },
+ { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480, upload-time = "2025-10-24T10:04:51.486Z" },
+ { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148, upload-time = "2025-10-24T10:04:59.585Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964, upload-time = "2025-10-24T10:05:08.175Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517, upload-time = "2025-10-24T10:05:14.314Z" },
+ { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" },
+ { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315, upload-time = "2025-10-24T10:05:47.314Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906, upload-time = "2025-10-24T10:05:58.254Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783, upload-time = "2025-10-24T10:06:08.08Z" },
+ { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883, upload-time = "2025-10-24T10:06:14.204Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629, upload-time = "2025-10-24T10:06:20.274Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783, upload-time = "2025-10-24T10:06:27.301Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999, upload-time = "2025-10-24T10:06:35.387Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601, upload-time = "2025-10-24T10:06:43.551Z" },
+ { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050, upload-time = "2025-10-24T10:06:52.284Z" },
+ { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877, upload-time = "2025-10-24T10:07:02.405Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099, upload-time = "2025-10-24T10:08:07.259Z" },
+ { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685, upload-time = "2025-10-24T10:07:11.47Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158, upload-time = "2025-10-24T10:07:18.626Z" },
+ { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060, upload-time = "2025-10-24T10:07:26.002Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395, upload-time = "2025-10-24T10:07:34.09Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216, upload-time = "2025-10-24T10:07:43.528Z" },
+ { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552, upload-time = "2025-10-24T10:07:53.519Z" },
+ { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062, upload-time = "2025-10-24T10:08:14.101Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057, upload-time = "2025-10-24T10:08:21.842Z" },
+ { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002, upload-time = "2025-10-24T10:08:29.034Z" },
+ { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765, upload-time = "2025-10-24T10:08:38.559Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139, upload-time = "2025-10-24T10:08:46.784Z" },
+ { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244, upload-time = "2025-10-24T10:08:55.771Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501, upload-time = "2025-10-24T10:09:59.891Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506, upload-time = "2025-10-24T10:09:02.953Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312, upload-time = "2025-10-24T10:09:10.334Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609, upload-time = "2025-10-24T10:09:18.61Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663, upload-time = "2025-10-24T10:09:27.369Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543, upload-time = "2025-10-24T10:09:34.908Z" },
+ { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838, upload-time = "2025-10-24T10:09:44.394Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594, upload-time = "2025-10-24T10:09:53.111Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" },
+ { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" },
+ { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" },
+ { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" },
+ { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.10.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pyright"
+version = "1.1.403"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "nodeenv" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526, upload-time = "2025-07-09T07:15:52.882Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504, upload-time = "2025-07-09T07:15:50.958Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "7.4.4"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.18.3"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4d/73/769d29676fb36a36e5a57c198154171081aabcfd08112a24a4e3fb5c9f10/pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91", size = 28052, upload-time = "2022-03-25T09:43:58.406Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/d6/4ecdd0c5b49a2209131b6af78baa643cec35f213abbc54d0eb1542b3786d/pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213", size = 14768, upload-time = "2022-03-28T13:53:15.727Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/4b/7c400506ec484ec999b10133aa8e31af39dfc727042dc6944cd45fd927d0/pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84", size = 14597, upload-time = "2022-03-25T09:43:57.106Z" },
+]
+
+[[package]]
+name = "pytest-pretty"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "pytest" },
+ { name = "rich" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ba/d7/c699e0be5401fe9ccad484562f0af9350b4e48c05acf39fb3dab1932128f/pytest_pretty-1.3.0.tar.gz", hash = "sha256:97e9921be40f003e40ae78db078d4a0c1ea42bf73418097b5077970c2cc43bf3", size = 219297, upload-time = "2025-06-04T12:54:37.322Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ab/85/2f97a1b65178b0f11c9c77c35417a4cc5b99a80db90dad4734a129844ea5/pytest_pretty-1.3.0-py3-none-any.whl", hash = "sha256:074b9d5783cef9571494543de07e768a4dda92a3e85118d6c7458c67297159b7", size = 5620, upload-time = "2025-06-04T12:54:36.229Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.20"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
+]
+
+[[package]]
+name = "pytz"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
+]
+
+[[package]]
+name = "pywin32"
+version = "311"
+source = { registry = "https://pypi.org/simple/" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" },
+ { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" },
+ { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" },
+ { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
+ { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
+ { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" },
+ { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" },
+ { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" },
+ { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" },
+ { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" },
+ { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" },
+ { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" },
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.36.2"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" },
+]
+
+[[package]]
+name = "regex"
+version = "2024.11.6"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674, upload-time = "2024-11-06T20:08:57.575Z" },
+ { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684, upload-time = "2024-11-06T20:08:59.787Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589, upload-time = "2024-11-06T20:09:01.896Z" },
+ { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511, upload-time = "2024-11-06T20:09:04.062Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149, upload-time = "2024-11-06T20:09:06.237Z" },
+ { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707, upload-time = "2024-11-06T20:09:07.715Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702, upload-time = "2024-11-06T20:09:10.101Z" },
+ { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976, upload-time = "2024-11-06T20:09:11.566Z" },
+ { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397, upload-time = "2024-11-06T20:09:13.119Z" },
+ { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726, upload-time = "2024-11-06T20:09:14.85Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098, upload-time = "2024-11-06T20:09:16.504Z" },
+ { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325, upload-time = "2024-11-06T20:09:18.698Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277, upload-time = "2024-11-06T20:09:21.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197, upload-time = "2024-11-06T20:09:24.092Z" },
+ { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714, upload-time = "2024-11-06T20:09:26.36Z" },
+ { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042, upload-time = "2024-11-06T20:09:28.762Z" },
+ { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669, upload-time = "2024-11-06T20:09:31.064Z" },
+ { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684, upload-time = "2024-11-06T20:09:32.915Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589, upload-time = "2024-11-06T20:09:35.504Z" },
+ { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121, upload-time = "2024-11-06T20:09:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275, upload-time = "2024-11-06T20:09:40.371Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257, upload-time = "2024-11-06T20:09:43.059Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727, upload-time = "2024-11-06T20:09:48.19Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667, upload-time = "2024-11-06T20:09:49.828Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963, upload-time = "2024-11-06T20:09:51.819Z" },
+ { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700, upload-time = "2024-11-06T20:09:53.982Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592, upload-time = "2024-11-06T20:09:56.222Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929, upload-time = "2024-11-06T20:09:58.642Z" },
+ { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213, upload-time = "2024-11-06T20:10:00.867Z" },
+ { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734, upload-time = "2024-11-06T20:10:03.361Z" },
+ { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052, upload-time = "2024-11-06T20:10:05.179Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781, upload-time = "2024-11-06T20:10:07.07Z" },
+ { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455, upload-time = "2024-11-06T20:10:09.117Z" },
+ { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759, upload-time = "2024-11-06T20:10:11.155Z" },
+ { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976, upload-time = "2024-11-06T20:10:13.24Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077, upload-time = "2024-11-06T20:10:15.37Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160, upload-time = "2024-11-06T20:10:19.027Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896, upload-time = "2024-11-06T20:10:21.85Z" },
+ { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997, upload-time = "2024-11-06T20:10:24.329Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725, upload-time = "2024-11-06T20:10:28.067Z" },
+ { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481, upload-time = "2024-11-06T20:10:31.612Z" },
+ { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896, upload-time = "2024-11-06T20:10:34.054Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138, upload-time = "2024-11-06T20:10:36.142Z" },
+ { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692, upload-time = "2024-11-06T20:10:38.394Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135, upload-time = "2024-11-06T20:10:40.367Z" },
+ { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567, upload-time = "2024-11-06T20:10:43.467Z" },
+ { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525, upload-time = "2024-11-06T20:10:45.19Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324, upload-time = "2024-11-06T20:10:47.177Z" },
+ { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617, upload-time = "2024-11-06T20:10:49.312Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023, upload-time = "2024-11-06T20:10:51.102Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072, upload-time = "2024-11-06T20:10:52.926Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130, upload-time = "2024-11-06T20:10:54.828Z" },
+ { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857, upload-time = "2024-11-06T20:10:56.634Z" },
+ { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006, upload-time = "2024-11-06T20:10:59.369Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650, upload-time = "2024-11-06T20:11:02.042Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545, upload-time = "2024-11-06T20:11:03.933Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045, upload-time = "2024-11-06T20:11:06.497Z" },
+ { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182, upload-time = "2024-11-06T20:11:09.06Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733, upload-time = "2024-11-06T20:11:11.256Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122, upload-time = "2024-11-06T20:11:13.161Z" },
+ { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545, upload-time = "2024-11-06T20:11:15Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.4"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
+]
+
+[[package]]
+name = "requests-toolbelt"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
+]
+
+[[package]]
+name = "rich"
+version = "14.0.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.26.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385, upload-time = "2025-07-01T15:57:13.958Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/31/1459645f036c3dfeacef89e8e5825e430c77dde8489f3b99eaafcd4a60f5/rpds_py-0.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37", size = 372466, upload-time = "2025-07-01T15:53:40.55Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/ff/3d0727f35836cc8773d3eeb9a46c40cc405854e36a8d2e951f3a8391c976/rpds_py-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0", size = 357825, upload-time = "2025-07-01T15:53:42.247Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ce/badc5e06120a54099ae287fa96d82cbb650a5f85cf247ffe19c7b157fd1f/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd", size = 381530, upload-time = "2025-07-01T15:53:43.585Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/a5/fa5d96a66c95d06c62d7a30707b6a4cfec696ab8ae280ee7be14e961e118/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79", size = 396933, upload-time = "2025-07-01T15:53:45.78Z" },
+ { url = "https://files.pythonhosted.org/packages/00/a7/7049d66750f18605c591a9db47d4a059e112a0c9ff8de8daf8fa0f446bba/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3", size = 513973, upload-time = "2025-07-01T15:53:47.085Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/f1/528d02c7d6b29d29fac8fd784b354d3571cc2153f33f842599ef0cf20dd2/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf", size = 402293, upload-time = "2025-07-01T15:53:48.117Z" },
+ { url = "https://files.pythonhosted.org/packages/15/93/fde36cd6e4685df2cd08508f6c45a841e82f5bb98c8d5ecf05649522acb5/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc", size = 383787, upload-time = "2025-07-01T15:53:50.874Z" },
+ { url = "https://files.pythonhosted.org/packages/69/f2/5007553aaba1dcae5d663143683c3dfd03d9395289f495f0aebc93e90f24/rpds_py-0.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19", size = 416312, upload-time = "2025-07-01T15:53:52.046Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/a7/ce52c75c1e624a79e48a69e611f1c08844564e44c85db2b6f711d76d10ce/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11", size = 558403, upload-time = "2025-07-01T15:53:53.192Z" },
+ { url = "https://files.pythonhosted.org/packages/79/d5/e119db99341cc75b538bf4cb80504129fa22ce216672fb2c28e4a101f4d9/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f", size = 588323, upload-time = "2025-07-01T15:53:54.336Z" },
+ { url = "https://files.pythonhosted.org/packages/93/94/d28272a0b02f5fe24c78c20e13bbcb95f03dc1451b68e7830ca040c60bd6/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323", size = 554541, upload-time = "2025-07-01T15:53:55.469Z" },
+ { url = "https://files.pythonhosted.org/packages/93/e0/8c41166602f1b791da892d976057eba30685486d2e2c061ce234679c922b/rpds_py-0.26.0-cp310-cp310-win32.whl", hash = "sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45", size = 220442, upload-time = "2025-07-01T15:53:56.524Z" },
+ { url = "https://files.pythonhosted.org/packages/87/f0/509736bb752a7ab50fb0270c2a4134d671a7b3038030837e5536c3de0e0b/rpds_py-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84", size = 231314, upload-time = "2025-07-01T15:53:57.842Z" },
+ { url = "https://files.pythonhosted.org/packages/09/4c/4ee8f7e512030ff79fda1df3243c88d70fc874634e2dbe5df13ba4210078/rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed", size = 372610, upload-time = "2025-07-01T15:53:58.844Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/9d/3dc16be00f14fc1f03c71b1d67c8df98263ab2710a2fbd65a6193214a527/rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0", size = 358032, upload-time = "2025-07-01T15:53:59.985Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/5a/7f1bf8f045da2866324a08ae80af63e64e7bfaf83bd31f865a7b91a58601/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1", size = 381525, upload-time = "2025-07-01T15:54:01.162Z" },
+ { url = "https://files.pythonhosted.org/packages/45/8a/04479398c755a066ace10e3d158866beb600867cacae194c50ffa783abd0/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7", size = 397089, upload-time = "2025-07-01T15:54:02.319Z" },
+ { url = "https://files.pythonhosted.org/packages/72/88/9203f47268db488a1b6d469d69c12201ede776bb728b9d9f29dbfd7df406/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6", size = 514255, upload-time = "2025-07-01T15:54:03.38Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/b4/01ce5d1e853ddf81fbbd4311ab1eff0b3cf162d559288d10fd127e2588b5/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e", size = 402283, upload-time = "2025-07-01T15:54:04.923Z" },
+ { url = "https://files.pythonhosted.org/packages/34/a2/004c99936997bfc644d590a9defd9e9c93f8286568f9c16cdaf3e14429a7/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d", size = 383881, upload-time = "2025-07-01T15:54:06.482Z" },
+ { url = "https://files.pythonhosted.org/packages/05/1b/ef5fba4a8f81ce04c427bfd96223f92f05e6cd72291ce9d7523db3b03a6c/rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3", size = 415822, upload-time = "2025-07-01T15:54:07.605Z" },
+ { url = "https://files.pythonhosted.org/packages/16/80/5c54195aec456b292f7bd8aa61741c8232964063fd8a75fdde9c1e982328/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107", size = 558347, upload-time = "2025-07-01T15:54:08.591Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/1c/1845c1b1fd6d827187c43afe1841d91678d7241cbdb5420a4c6de180a538/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a", size = 587956, upload-time = "2025-07-01T15:54:09.963Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/ff/9e979329dd131aa73a438c077252ddabd7df6d1a7ad7b9aacf6261f10faa/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318", size = 554363, upload-time = "2025-07-01T15:54:11.073Z" },
+ { url = "https://files.pythonhosted.org/packages/00/8b/d78cfe034b71ffbe72873a136e71acc7a831a03e37771cfe59f33f6de8a2/rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a", size = 220123, upload-time = "2025-07-01T15:54:12.382Z" },
+ { url = "https://files.pythonhosted.org/packages/94/c1/3c8c94c7dd3905dbfde768381ce98778500a80db9924731d87ddcdb117e9/rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03", size = 231732, upload-time = "2025-07-01T15:54:13.434Z" },
+ { url = "https://files.pythonhosted.org/packages/67/93/e936fbed1b734eabf36ccb5d93c6a2e9246fbb13c1da011624b7286fae3e/rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41", size = 221917, upload-time = "2025-07-01T15:54:14.559Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/86/90eb87c6f87085868bd077c7a9938006eb1ce19ed4d06944a90d3560fce2/rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d", size = 363933, upload-time = "2025-07-01T15:54:15.734Z" },
+ { url = "https://files.pythonhosted.org/packages/63/78/4469f24d34636242c924626082b9586f064ada0b5dbb1e9d096ee7a8e0c6/rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136", size = 350447, upload-time = "2025-07-01T15:54:16.922Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/91/c448ed45efdfdade82348d5e7995e15612754826ea640afc20915119734f/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582", size = 384711, upload-time = "2025-07-01T15:54:18.101Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/43/e5c86fef4be7f49828bdd4ecc8931f0287b1152c0bb0163049b3218740e7/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e", size = 400865, upload-time = "2025-07-01T15:54:19.295Z" },
+ { url = "https://files.pythonhosted.org/packages/55/34/e00f726a4d44f22d5c5fe2e5ddd3ac3d7fd3f74a175607781fbdd06fe375/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15", size = 517763, upload-time = "2025-07-01T15:54:20.858Z" },
+ { url = "https://files.pythonhosted.org/packages/52/1c/52dc20c31b147af724b16104500fba13e60123ea0334beba7b40e33354b4/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8", size = 406651, upload-time = "2025-07-01T15:54:22.508Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/77/87d7bfabfc4e821caa35481a2ff6ae0b73e6a391bb6b343db2c91c2b9844/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a", size = 386079, upload-time = "2025-07-01T15:54:23.987Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/d4/7f2200c2d3ee145b65b3cddc4310d51f7da6a26634f3ac87125fd789152a/rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323", size = 421379, upload-time = "2025-07-01T15:54:25.073Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/13/9fdd428b9c820869924ab62236b8688b122baa22d23efdd1c566938a39ba/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158", size = 562033, upload-time = "2025-07-01T15:54:26.225Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/e1/b69686c3bcbe775abac3a4c1c30a164a2076d28df7926041f6c0eb5e8d28/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3", size = 591639, upload-time = "2025-07-01T15:54:27.424Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/c9/1e3d8c8863c84a90197ac577bbc3d796a92502124c27092413426f670990/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2", size = 557105, upload-time = "2025-07-01T15:54:29.93Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/c5/90c569649057622959f6dcc40f7b516539608a414dfd54b8d77e3b201ac0/rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44", size = 223272, upload-time = "2025-07-01T15:54:31.128Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/16/19f5d9f2a556cfed454eebe4d354c38d51c20f3db69e7b4ce6cff904905d/rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c", size = 234995, upload-time = "2025-07-01T15:54:32.195Z" },
+ { url = "https://files.pythonhosted.org/packages/83/f0/7935e40b529c0e752dfaa7880224771b51175fce08b41ab4a92eb2fbdc7f/rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8", size = 223198, upload-time = "2025-07-01T15:54:33.271Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917, upload-time = "2025-07-01T15:54:34.755Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073, upload-time = "2025-07-01T15:54:36.292Z" },
+ { url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214, upload-time = "2025-07-01T15:54:37.469Z" },
+ { url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113, upload-time = "2025-07-01T15:54:38.954Z" },
+ { url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189, upload-time = "2025-07-01T15:54:40.57Z" },
+ { url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998, upload-time = "2025-07-01T15:54:43.025Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903, upload-time = "2025-07-01T15:54:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785, upload-time = "2025-07-01T15:54:46.043Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329, upload-time = "2025-07-01T15:54:47.64Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875, upload-time = "2025-07-01T15:54:48.9Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636, upload-time = "2025-07-01T15:54:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663, upload-time = "2025-07-01T15:54:52.023Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428, upload-time = "2025-07-01T15:54:53.692Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571, upload-time = "2025-07-01T15:54:54.822Z" },
+ { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475, upload-time = "2025-07-01T15:54:56.228Z" },
+ { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692, upload-time = "2025-07-01T15:54:58.561Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415, upload-time = "2025-07-01T15:54:59.751Z" },
+ { url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783, upload-time = "2025-07-01T15:55:00.898Z" },
+ { url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844, upload-time = "2025-07-01T15:55:02.201Z" },
+ { url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105, upload-time = "2025-07-01T15:55:03.698Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440, upload-time = "2025-07-01T15:55:05.398Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759, upload-time = "2025-07-01T15:55:08.316Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032, upload-time = "2025-07-01T15:55:09.52Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416, upload-time = "2025-07-01T15:55:11.216Z" },
+ { url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049, upload-time = "2025-07-01T15:55:13.004Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428, upload-time = "2025-07-01T15:55:14.486Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524, upload-time = "2025-07-01T15:55:15.745Z" },
+ { url = "https://files.pythonhosted.org/packages/55/07/029b7c45db910c74e182de626dfdae0ad489a949d84a468465cd0ca36355/rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a", size = 364292, upload-time = "2025-07-01T15:55:17.001Z" },
+ { url = "https://files.pythonhosted.org/packages/13/d1/9b3d3f986216b4d1f584878dca15ce4797aaf5d372d738974ba737bf68d6/rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf", size = 350334, upload-time = "2025-07-01T15:55:18.922Z" },
+ { url = "https://files.pythonhosted.org/packages/18/98/16d5e7bc9ec715fa9668731d0cf97f6b032724e61696e2db3d47aeb89214/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12", size = 384875, upload-time = "2025-07-01T15:55:20.399Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/13/aa5e2b1ec5ab0e86a5c464d53514c0467bec6ba2507027d35fc81818358e/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20", size = 399993, upload-time = "2025-07-01T15:55:21.729Z" },
+ { url = "https://files.pythonhosted.org/packages/17/03/8021810b0e97923abdbab6474c8b77c69bcb4b2c58330777df9ff69dc559/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331", size = 516683, upload-time = "2025-07-01T15:55:22.918Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/b1/da8e61c87c2f3d836954239fdbbfb477bb7b54d74974d8f6fcb34342d166/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f", size = 408825, upload-time = "2025-07-01T15:55:24.207Z" },
+ { url = "https://files.pythonhosted.org/packages/38/bc/1fc173edaaa0e52c94b02a655db20697cb5fa954ad5a8e15a2c784c5cbdd/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246", size = 387292, upload-time = "2025-07-01T15:55:25.554Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/eb/3a9bb4bd90867d21916f253caf4f0d0be7098671b6715ad1cead9fe7bab9/rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387", size = 420435, upload-time = "2025-07-01T15:55:27.798Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/16/e066dcdb56f5632713445271a3f8d3d0b426d51ae9c0cca387799df58b02/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af", size = 562410, upload-time = "2025-07-01T15:55:29.057Z" },
+ { url = "https://files.pythonhosted.org/packages/60/22/ddbdec7eb82a0dc2e455be44c97c71c232983e21349836ce9f272e8a3c29/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33", size = 590724, upload-time = "2025-07-01T15:55:30.719Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/b4/95744085e65b7187d83f2fcb0bef70716a1ea0a9e5d8f7f39a86e5d83424/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953", size = 558285, upload-time = "2025-07-01T15:55:31.981Z" },
+ { url = "https://files.pythonhosted.org/packages/37/37/6309a75e464d1da2559446f9c811aa4d16343cebe3dbb73701e63f760caa/rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9", size = 223459, upload-time = "2025-07-01T15:55:33.312Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/6f/8e9c11214c46098b1d1391b7e02b70bb689ab963db3b19540cba17315291/rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37", size = 236083, upload-time = "2025-07-01T15:55:34.933Z" },
+ { url = "https://files.pythonhosted.org/packages/47/af/9c4638994dd623d51c39892edd9d08e8be8220a4b7e874fa02c2d6e91955/rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867", size = 223291, upload-time = "2025-07-01T15:55:36.202Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/db/669a241144460474aab03e254326b32c42def83eb23458a10d163cb9b5ce/rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da", size = 361445, upload-time = "2025-07-01T15:55:37.483Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/2d/133f61cc5807c6c2fd086a46df0eb8f63a23f5df8306ff9f6d0fd168fecc/rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7", size = 347206, upload-time = "2025-07-01T15:55:38.828Z" },
+ { url = "https://files.pythonhosted.org/packages/05/bf/0e8fb4c05f70273469eecf82f6ccf37248558526a45321644826555db31b/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad", size = 380330, upload-time = "2025-07-01T15:55:40.175Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/a8/060d24185d8b24d3923322f8d0ede16df4ade226a74e747b8c7c978e3dd3/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d", size = 392254, upload-time = "2025-07-01T15:55:42.015Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/7b/7c2e8a9ee3e6bc0bae26bf29f5219955ca2fbb761dca996a83f5d2f773fe/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca", size = 516094, upload-time = "2025-07-01T15:55:43.603Z" },
+ { url = "https://files.pythonhosted.org/packages/75/d6/f61cafbed8ba1499b9af9f1777a2a199cd888f74a96133d8833ce5eaa9c5/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19", size = 402889, upload-time = "2025-07-01T15:55:45.275Z" },
+ { url = "https://files.pythonhosted.org/packages/92/19/c8ac0a8a8df2dd30cdec27f69298a5c13e9029500d6d76718130f5e5be10/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8", size = 384301, upload-time = "2025-07-01T15:55:47.098Z" },
+ { url = "https://files.pythonhosted.org/packages/41/e1/6b1859898bc292a9ce5776016c7312b672da00e25cec74d7beced1027286/rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b", size = 412891, upload-time = "2025-07-01T15:55:48.412Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/b9/ceb39af29913c07966a61367b3c08b4f71fad841e32c6b59a129d5974698/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a", size = 557044, upload-time = "2025-07-01T15:55:49.816Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/27/35637b98380731a521f8ec4f3fd94e477964f04f6b2f8f7af8a2d889a4af/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170", size = 585774, upload-time = "2025-07-01T15:55:51.192Z" },
+ { url = "https://files.pythonhosted.org/packages/52/d9/3f0f105420fecd18551b678c9a6ce60bd23986098b252a56d35781b3e7e9/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e", size = 554886, upload-time = "2025-07-01T15:55:52.541Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/c5/347c056a90dc8dd9bc240a08c527315008e1b5042e7a4cf4ac027be9d38a/rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f", size = 219027, upload-time = "2025-07-01T15:55:53.874Z" },
+ { url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821, upload-time = "2025-07-01T15:55:55.167Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/9a/1f033b0b31253d03d785b0cd905bc127e555ab496ea6b4c7c2e1f951f2fd/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958", size = 373226, upload-time = "2025-07-01T15:56:16.578Z" },
+ { url = "https://files.pythonhosted.org/packages/58/29/5f88023fd6aaaa8ca3c4a6357ebb23f6f07da6079093ccf27c99efce87db/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e", size = 359230, upload-time = "2025-07-01T15:56:17.978Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/6c/13eaebd28b439da6964dde22712b52e53fe2824af0223b8e403249d10405/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08", size = 382363, upload-time = "2025-07-01T15:56:19.977Z" },
+ { url = "https://files.pythonhosted.org/packages/55/fc/3bb9c486b06da19448646f96147796de23c5811ef77cbfc26f17307b6a9d/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6", size = 397146, upload-time = "2025-07-01T15:56:21.39Z" },
+ { url = "https://files.pythonhosted.org/packages/15/18/9d1b79eb4d18e64ba8bba9e7dec6f9d6920b639f22f07ee9368ca35d4673/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871", size = 514804, upload-time = "2025-07-01T15:56:22.78Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/5a/175ad7191bdbcd28785204621b225ad70e85cdfd1e09cc414cb554633b21/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4", size = 402820, upload-time = "2025-07-01T15:56:24.584Z" },
+ { url = "https://files.pythonhosted.org/packages/11/45/6a67ecf6d61c4d4aff4bc056e864eec4b2447787e11d1c2c9a0242c6e92a/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f", size = 384567, upload-time = "2025-07-01T15:56:26.064Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/ba/16589da828732b46454c61858950a78fe4c931ea4bf95f17432ffe64b241/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73", size = 416520, upload-time = "2025-07-01T15:56:27.608Z" },
+ { url = "https://files.pythonhosted.org/packages/81/4b/00092999fc7c0c266045e984d56b7314734cc400a6c6dc4d61a35f135a9d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f", size = 559362, upload-time = "2025-07-01T15:56:29.078Z" },
+ { url = "https://files.pythonhosted.org/packages/96/0c/43737053cde1f93ac4945157f7be1428724ab943e2132a0d235a7e161d4e/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84", size = 588113, upload-time = "2025-07-01T15:56:30.485Z" },
+ { url = "https://files.pythonhosted.org/packages/46/46/8e38f6161466e60a997ed7e9951ae5de131dedc3cf778ad35994b4af823d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b", size = 555429, upload-time = "2025-07-01T15:56:31.956Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ac/65da605e9f1dd643ebe615d5bbd11b6efa1d69644fc4bf623ea5ae385a82/rpds_py-0.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8", size = 231950, upload-time = "2025-07-01T15:56:33.337Z" },
+ { url = "https://files.pythonhosted.org/packages/51/f2/b5c85b758a00c513bb0389f8fc8e61eb5423050c91c958cdd21843faa3e6/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674", size = 373505, upload-time = "2025-07-01T15:56:34.716Z" },
+ { url = "https://files.pythonhosted.org/packages/23/e0/25db45e391251118e915e541995bb5f5ac5691a3b98fb233020ba53afc9b/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696", size = 359468, upload-time = "2025-07-01T15:56:36.219Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/73/dd5ee6075bb6491be3a646b301dfd814f9486d924137a5098e61f0487e16/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb", size = 382680, upload-time = "2025-07-01T15:56:37.644Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/10/84b522ff58763a5c443f5bcedc1820240e454ce4e620e88520f04589e2ea/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88", size = 397035, upload-time = "2025-07-01T15:56:39.241Z" },
+ { url = "https://files.pythonhosted.org/packages/06/ea/8667604229a10a520fcbf78b30ccc278977dcc0627beb7ea2c96b3becef0/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8", size = 514922, upload-time = "2025-07-01T15:56:40.645Z" },
+ { url = "https://files.pythonhosted.org/packages/24/e6/9ed5b625c0661c4882fc8cdf302bf8e96c73c40de99c31e0b95ed37d508c/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5", size = 402822, upload-time = "2025-07-01T15:56:42.137Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/58/212c7b6fd51946047fb45d3733da27e2fa8f7384a13457c874186af691b1/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7", size = 384336, upload-time = "2025-07-01T15:56:44.239Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/f5/a40ba78748ae8ebf4934d4b88e77b98497378bc2c24ba55ebe87a4e87057/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b", size = 416871, upload-time = "2025-07-01T15:56:46.284Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/a6/33b1fc0c9f7dcfcfc4a4353daa6308b3ece22496ceece348b3e7a7559a09/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb", size = 559439, upload-time = "2025-07-01T15:56:48.549Z" },
+ { url = "https://files.pythonhosted.org/packages/71/2d/ceb3f9c12f8cfa56d34995097f6cd99da1325642c60d1b6680dd9df03ed8/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0", size = 588380, upload-time = "2025-07-01T15:56:50.086Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334, upload-time = "2025-07-01T15:56:51.703Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.5.7"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/bf/2b/69e5e412f9d390adbdbcbf4f64d6914fa61b44b08839a6584655014fc524/ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5", size = 2449817, upload-time = "2024-08-08T15:43:07.467Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6b/eb/06e06aaf96af30a68e83b357b037008c54a2ddcbad4f989535007c700394/ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a", size = 9570571, upload-time = "2024-08-08T15:41:56.537Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/10/1be32aeaab8728f78f673e7a47dd813222364479b2d6573dbcf0085e83ea/ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be", size = 8685138, upload-time = "2024-08-08T15:42:02.833Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/1d/c218ce83beb4394ba04d05e9aa2ae6ce9fba8405688fe878b0fdb40ce855/ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e", size = 8266785, upload-time = "2024-08-08T15:42:08.321Z" },
+ { url = "https://files.pythonhosted.org/packages/26/79/7f49509bd844476235b40425756def366b227a9714191c91f02fb2178635/ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8", size = 9983964, upload-time = "2024-08-08T15:42:12.419Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/b1/939836b70bf9fcd5e5cd3ea67fdb8abb9eac7631351d32f26544034a35e4/ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea", size = 9359490, upload-time = "2024-08-08T15:42:16.713Z" },
+ { url = "https://files.pythonhosted.org/packages/32/7d/b3db19207de105daad0c8b704b2c6f2a011f9c07017bd58d8d6e7b8eba19/ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc", size = 10170833, upload-time = "2024-08-08T15:42:20.54Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/45/eae9da55f3357a1ac04220230b8b07800bf516e6dd7e1ad20a2ff3b03b1b/ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692", size = 10896360, upload-time = "2024-08-08T15:42:25.2Z" },
+ { url = "https://files.pythonhosted.org/packages/99/67/4388b36d145675f4c51ebec561fcd4298a0e2550c81e629116f83ce45a39/ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf", size = 10477094, upload-time = "2024-08-08T15:42:29.553Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/9c/f5e6ed1751dc187a4ecf19a4970dd30a521c0ee66b7941c16e292a4043fb/ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb", size = 11480896, upload-time = "2024-08-08T15:42:33.772Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/3b/2b683be597bbd02046678fc3fc1c199c641512b20212073b58f173822bb3/ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e", size = 10179702, upload-time = "2024-08-08T15:42:38.038Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/38/c2d94054dc4b3d1ea4c2ba3439b2a7095f08d1c8184bc41e6abe2a688be7/ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499", size = 9982855, upload-time = "2024-08-08T15:42:42.031Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/e7/1433db2da505ffa8912dcf5b28a8743012ee780cbc20ad0bf114787385d9/ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e", size = 9433156, upload-time = "2024-08-08T15:42:45.339Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/36/4fa43250e67741edeea3d366f59a1dc993d4d89ad493a36cbaa9889895f2/ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5", size = 9782971, upload-time = "2024-08-08T15:42:49.354Z" },
+ { url = "https://files.pythonhosted.org/packages/80/0e/8c276103d518e5cf9202f70630aaa494abf6fc71c04d87c08b6d3cd07a4b/ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e", size = 10247775, upload-time = "2024-08-08T15:42:53.294Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/b9/673096d61276f39291b729dddde23c831a5833d98048349835782688a0ec/ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a", size = 7841772, upload-time = "2024-08-08T15:42:57.488Z" },
+ { url = "https://files.pythonhosted.org/packages/67/1c/4520c98bfc06b9c73cd1457686d4d3935d40046b1ddea08403e5a6deff51/ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3", size = 8699779, upload-time = "2024-08-08T15:43:00.429Z" },
+ { url = "https://files.pythonhosted.org/packages/38/23/b3763a237d2523d40a31fe2d1a301191fe392dd48d3014977d079cf8c0bd/ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4", size = 8091891, upload-time = "2024-08-08T15:43:04.162Z" },
+]
+
+[[package]]
+name = "s3transfer"
+version = "0.13.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "botocore" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ed/5d/9dcc100abc6711e8247af5aa561fc07c4a046f72f659c3adea9a449e191a/s3transfer-0.13.0.tar.gz", hash = "sha256:f5e6db74eb7776a37208001113ea7aa97695368242b364d73e91c981ac522177", size = 150232, upload-time = "2025-05-22T19:24:50.245Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/17/22bf8155aa0ea2305eefa3a6402e040df7ebe512d1310165eda1e233c3f8/s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:0148ef34d6dd964d0d8cf4311b2b21c474693e57c2e069ec708ce043d2b527be", size = 85152, upload-time = "2025-05-22T19:24:48.703Z" },
+]
+
+[[package]]
+name = "sentry-sdk"
+version = "2.34.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "certifi" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3a/38/10d6bfe23df1bfc65ac2262ed10b45823f47f810b0057d3feeea1ca5c7ed/sentry_sdk-2.34.1.tar.gz", hash = "sha256:69274eb8c5c38562a544c3e9f68b5be0a43be4b697f5fd385bf98e4fbe672687", size = 336969, upload-time = "2025-07-30T11:13:37.93Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2d/3e/bb34de65a5787f76848a533afbb6610e01fbcdd59e76d8679c254e02255c/sentry_sdk-2.34.1-py2.py3-none-any.whl", hash = "sha256:b7a072e1cdc5abc48101d5146e1ae680fa81fe886d8d95aaa25a0b450c818d32", size = 357743, upload-time = "2025-07-30T11:13:36.145Z" },
+]
+
+[[package]]
+name = "setuptools"
+version = "80.9.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "sortedcontainers"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
+]
+
+[[package]]
+name = "sqlalchemy"
+version = "2.0.41"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/12/d7c445b1940276a828efce7331cb0cb09d6e5f049651db22f4ebb0922b77/sqlalchemy-2.0.41-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b", size = 2117967, upload-time = "2025-05-14T17:48:15.841Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/b8/cb90f23157e28946b27eb01ef401af80a1fab7553762e87df51507eaed61/sqlalchemy-2.0.41-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5", size = 2107583, upload-time = "2025-05-14T17:48:18.688Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/c2/eef84283a1c8164a207d898e063edf193d36a24fb6a5bb3ce0634b92a1e8/sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747", size = 3186025, upload-time = "2025-05-14T17:51:51.226Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/72/49d52bd3c5e63a1d458fd6d289a1523a8015adedbddf2c07408ff556e772/sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30", size = 3186259, upload-time = "2025-05-14T17:55:22.526Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/9e/e3ffc37d29a3679a50b6bbbba94b115f90e565a2b4545abb17924b94c52d/sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29", size = 3126803, upload-time = "2025-05-14T17:51:53.277Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/76/56b21e363f6039978ae0b72690237b38383e4657281285a09456f313dd77/sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11", size = 3148566, upload-time = "2025-05-14T17:55:24.398Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/92/11b8e1b69bf191bc69e300a99badbbb5f2f1102f2b08b39d9eee2e21f565/sqlalchemy-2.0.41-cp310-cp310-win32.whl", hash = "sha256:118c16cd3f1b00c76d69343e38602006c9cfb9998fa4f798606d28d63f23beda", size = 2086696, upload-time = "2025-05-14T17:55:59.136Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/88/2d706c9cc4502654860f4576cd54f7db70487b66c3b619ba98e0be1a4642/sqlalchemy-2.0.41-cp310-cp310-win_amd64.whl", hash = "sha256:7492967c3386df69f80cf67efd665c0f667cee67032090fe01d7d74b0e19bb08", size = 2110200, upload-time = "2025-05-14T17:56:00.757Z" },
+ { url = "https://files.pythonhosted.org/packages/37/4e/b00e3ffae32b74b5180e15d2ab4040531ee1bef4c19755fe7926622dc958/sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", size = 2121232, upload-time = "2025-05-14T17:48:20.444Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/30/6547ebb10875302074a37e1970a5dce7985240665778cfdee2323709f749/sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", size = 2110897, upload-time = "2025-05-14T17:48:21.634Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/21/59df2b41b0f6c62da55cd64798232d7349a9378befa7f1bb18cf1dfd510a/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", size = 3273313, upload-time = "2025-05-14T17:51:56.205Z" },
+ { url = "https://files.pythonhosted.org/packages/62/e4/b9a7a0e5c6f79d49bcd6efb6e90d7536dc604dab64582a9dec220dab54b6/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", size = 3273807, upload-time = "2025-05-14T17:55:26.928Z" },
+ { url = "https://files.pythonhosted.org/packages/39/d8/79f2427251b44ddee18676c04eab038d043cff0e764d2d8bb08261d6135d/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", size = 3209632, upload-time = "2025-05-14T17:51:59.384Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/16/730a82dda30765f63e0454918c982fb7193f6b398b31d63c7c3bd3652ae5/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", size = 3233642, upload-time = "2025-05-14T17:55:29.901Z" },
+ { url = "https://files.pythonhosted.org/packages/04/61/c0d4607f7799efa8b8ea3c49b4621e861c8f5c41fd4b5b636c534fcb7d73/sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", size = 2086475, upload-time = "2025-05-14T17:56:02.095Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8e/8344f8ae1cb6a479d0741c02cd4f666925b2bf02e2468ddaf5ce44111f30/sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", size = 2110903, upload-time = "2025-05-14T17:56:03.499Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" },
+ { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/07/3e/eae74d8d33e3262bae0a7e023bb43d8bdd27980aa3557333f4632611151f/sse_starlette-2.4.1.tar.gz", hash = "sha256:7c8a800a1ca343e9165fc06bbda45c78e4c6166320707ae30b416c42da070926", size = 18635, upload-time = "2025-07-06T09:41:33.631Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e4/f1/6c7eaa8187ba789a6dd6d74430307478d2a91c23a5452ab339b6fbe15a08/sse_starlette-2.4.1-py3-none-any.whl", hash = "sha256:08b77ea898ab1a13a428b2b6f73cfe6d0e607a7b4e15b9bb23e4a37b087fd39a", size = 10824, upload-time = "2025-07-06T09:41:32.321Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.47.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0a/69/662169fdb92fb96ec3eaee218cf540a629d629c86d7993d9651226a6789b/starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b", size = 2583072, upload-time = "2025-06-21T04:03:17.337Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/82/95/38ef0cd7fa11eaba6a99b3c4f5ac948d8bc6ff199aabd327a29cc000840c/starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527", size = 72747, upload-time = "2025-06-21T04:03:15.705Z" },
+]
+
+[[package]]
+name = "temporalio"
+version = "1.23.0"
+source = { registry = "https://test.pypi.org/simple/" }
+dependencies = [
+ { name = "nexus-rpc" },
+ { name = "protobuf" },
+ { name = "python-dateutil", marker = "python_full_version < '3.11'" },
+ { name = "types-protobuf" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://test-files.pythonhosted.org/packages/67/48/ba7413e2fab8dcd277b9df00bafa572da24e9ca32de2f38d428dc3a2825c/temporalio-1.23.0.tar.gz", hash = "sha256:72750494b00eb73ded9db76195e3a9b53ff548780f73d878ec3f807ee3191410", size = 1933051, upload-time = "2026-02-18T17:40:03.902Z" }
+wheels = [
+ { url = "https://test-files.pythonhosted.org/packages/6f/71/26c8f21dca9092201b3b9cb7aff42460b4864b5999aa4c6a4343ac66f1fd/temporalio-1.23.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6b69ac8d75f2d90e66f4edce4316f6a33badc4a30b22efc50e9eddaa9acdc216", size = 12311037, upload-time = "2026-02-18T17:39:27.941Z" },
+ { url = "https://test-files.pythonhosted.org/packages/ec/47/43102816139f2d346680cb7cc1e53da5f6968355ac65b4d35d4edbfca896/temporalio-1.23.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:1bbbb2f9c3cdd09451565163f6d741e51f109694c49435d475fdfa42b597219d", size = 11821906, upload-time = "2026-02-18T17:39:35.343Z" },
+ { url = "https://test-files.pythonhosted.org/packages/00/b0/899ff28464a0e17adf17476bdfac8faf4ea41870358ff2d14737e43f9e66/temporalio-1.23.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf6570e0ee696f99a38d855da4441a890c7187357c16505ed458ac9ef274ed70", size = 12063601, upload-time = "2026-02-18T17:39:43.299Z" },
+ { url = "https://test-files.pythonhosted.org/packages/ed/17/b8c6d2ec3e113c6a788322513a5ff635bdd54b3791d092ed0e273467748a/temporalio-1.23.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b82d6cca54c9f376b50e941dd10d12f7fe5b692a314fb087be72cd2898646a79", size = 12394579, upload-time = "2026-02-18T17:39:52.935Z" },
+ { url = "https://test-files.pythonhosted.org/packages/b4/b7/f9ef7fd5ee65aef7d59ab1e95cb1b45df2fe49c17e3aa4d650ae3322f015/temporalio-1.23.0-cp310-abi3-win_amd64.whl", hash = "sha256:43c3b99a46dd329761a256f3855710c4a5b322afc879785e468bdd0b94faace6", size = 12834494, upload-time = "2026-02-18T17:40:00.858Z" },
+]
+
+[package.optional-dependencies]
+openai-agents = [
+ { name = "mcp" },
+ { name = "openai-agents" },
+]
+opentelemetry = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-sdk" },
+]
+
+[[package]]
+name = "temporalio-samples"
+version = "0.1a1"
+source = { editable = "." }
+dependencies = [
+ { name = "temporalio" },
+]
+
+[package.dev-dependencies]
+bedrock = [
+ { name = "boto3" },
+]
+cloud-export-to-parquet = [
+ { name = "boto3" },
+ { name = "numpy", marker = "python_full_version < '3.13'" },
+ { name = "pandas", marker = "python_full_version < '4'" },
+ { name = "pyarrow" },
+]
+dev = [
+ { name = "frozenlist" },
+ { name = "mypy" },
+ { name = "poethepoet" },
+ { name = "pyright" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-pretty" },
+ { name = "ruff" },
+ { name = "types-pyyaml" },
+]
+dsl = [
+ { name = "dacite" },
+ { name = "pyyaml" },
+ { name = "types-pyyaml" },
+]
+encryption = [
+ { name = "aiohttp" },
+ { name = "cryptography" },
+]
+gevent = [
+ { name = "gevent" },
+]
+langchain = [
+ { name = "fastapi" },
+ { name = "langchain", marker = "python_full_version < '4'" },
+ { name = "langchain-openai", marker = "python_full_version < '4'" },
+ { name = "langsmith", marker = "python_full_version < '4'" },
+ { name = "openai" },
+ { name = "tqdm" },
+ { name = "uvicorn", extra = ["standard"] },
+]
+nexus = [
+ { name = "nexus-rpc" },
+]
+open-telemetry = [
+ { name = "opentelemetry-exporter-otlp-proto-grpc" },
+ { name = "temporalio", extra = ["opentelemetry"] },
+]
+openai-agents = [
+ { name = "openai-agents", extra = ["litellm"] },
+ { name = "requests" },
+ { name = "temporalio", extra = ["openai-agents"] },
+]
+pydantic-converter = [
+ { name = "pydantic" },
+]
+sentry = [
+ { name = "sentry-sdk" },
+]
+trio-async = [
+ { name = "trio" },
+ { name = "trio-asyncio" },
+]
+
+[package.metadata]
+requires-dist = [{ name = "temporalio", specifier = ">=1.23.0,<2" }]
+
+[package.metadata.requires-dev]
+bedrock = [{ name = "boto3", specifier = ">=1.34.92,<2" }]
+cloud-export-to-parquet = [
+ { name = "boto3", specifier = ">=1.34.89,<2" },
+ { name = "numpy", marker = "python_full_version >= '3.10' and python_full_version < '3.13'", specifier = ">=1.26.0,<2" },
+ { name = "pandas", marker = "python_full_version >= '3.10' and python_full_version < '4'", specifier = ">=2.2.2,<3" },
+ { name = "pyarrow", specifier = ">=19.0.1" },
+]
+dev = [
+ { name = "frozenlist", specifier = ">=1.4.0,<2" },
+ { name = "mypy", specifier = ">=1.4.1,<2" },
+ { name = "poethepoet", specifier = ">=0.36.0" },
+ { name = "pyright", specifier = ">=1.1.394" },
+ { name = "pytest", specifier = ">=7.1.2,<8" },
+ { name = "pytest-asyncio", specifier = ">=0.18.3,<0.19" },
+ { name = "pytest-pretty", specifier = ">=1.3.0" },
+ { name = "ruff", specifier = ">=0.5.0,<0.6" },
+ { name = "types-pyyaml", specifier = ">=6.0.12.20241230,<7" },
+]
+dsl = [
+ { name = "dacite", specifier = ">=1.8.1,<2" },
+ { name = "pyyaml", specifier = ">=6.0.1,<7" },
+ { name = "types-pyyaml", specifier = ">=6.0.12,<7" },
+]
+encryption = [
+ { name = "aiohttp", specifier = ">=3.8.1,<4" },
+ { name = "cryptography", specifier = ">=38.0.1,<39" },
+]
+gevent = [{ name = "gevent", marker = "python_full_version >= '3.8'", specifier = ">=25.4.2" }]
+langchain = [
+ { name = "fastapi", specifier = ">=0.115.12" },
+ { name = "langchain", marker = "python_full_version >= '3.9' and python_full_version < '4'", specifier = ">=0.1.7,<0.2" },
+ { name = "langchain-openai", marker = "python_full_version >= '3.9' and python_full_version < '4'", specifier = ">=0.0.6,<0.0.7" },
+ { name = "langsmith", marker = "python_full_version >= '3.9' and python_full_version < '4'", specifier = ">=0.1.22,<0.2" },
+ { name = "openai", specifier = ">=1.4.0,<2" },
+ { name = "tqdm", specifier = ">=4.62.0,<5" },
+ { name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0.post1,<0.25" },
+]
+nexus = [{ name = "nexus-rpc", specifier = ">=1.1.0,<2" }]
+open-telemetry = [
+ { name = "opentelemetry-exporter-otlp-proto-grpc" },
+ { name = "temporalio", extras = ["opentelemetry"] },
+]
+openai-agents = [
+ { name = "openai-agents", extras = ["litellm"], specifier = "==0.3.2" },
+ { name = "requests", specifier = ">=2.32.0,<3" },
+ { name = "temporalio", extras = ["openai-agents"], specifier = ">=1.18.0" },
+]
+pydantic-converter = [{ name = "pydantic", specifier = ">=2.10.6,<3" }]
+sentry = [{ name = "sentry-sdk", specifier = ">=2.13.0" }]
+trio-async = [
+ { name = "trio", specifier = ">=0.28.0,<0.29" },
+ { name = "trio-asyncio", specifier = ">=0.15.0,<0.16" },
+]
+
+[[package]]
+name = "tenacity"
+version = "8.5.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", size = 47309, upload-time = "2024-07-05T07:25:31.836Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165, upload-time = "2024-07-05T07:25:29.591Z" },
+]
+
+[[package]]
+name = "tiktoken"
+version = "0.12.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "regex" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" },
+ { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" },
+ { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" },
+ { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" },
+ { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" },
+ { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" },
+ { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" },
+ { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" },
+ { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" },
+ { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" },
+ { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" },
+ { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" },
+ { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" },
+ { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" },
+ { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" },
+ { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" },
+ { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" },
+ { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" },
+ { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" },
+ { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" },
+ { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" },
+]
+
+[[package]]
+name = "tokenizers"
+version = "0.21.2"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "huggingface-hub" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ab/2d/b0fce2b8201635f60e8c95990080f58461cc9ca3d5026de2e900f38a7f21/tokenizers-0.21.2.tar.gz", hash = "sha256:fdc7cffde3e2113ba0e6cc7318c40e3438a4d74bbc62bf04bcc63bdfb082ac77", size = 351545, upload-time = "2025-06-24T10:24:52.449Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/cc/2936e2d45ceb130a21d929743f1e9897514691bec123203e10837972296f/tokenizers-0.21.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:342b5dfb75009f2255ab8dec0041287260fed5ce00c323eb6bab639066fef8ec", size = 2875206, upload-time = "2025-06-24T10:24:42.755Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/e6/33f41f2cc7861faeba8988e7a77601407bf1d9d28fc79c5903f8f77df587/tokenizers-0.21.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:126df3205d6f3a93fea80c7a8a266a78c1bd8dd2fe043386bafdd7736a23e45f", size = 2732655, upload-time = "2025-06-24T10:24:41.56Z" },
+ { url = "https://files.pythonhosted.org/packages/33/2b/1791eb329c07122a75b01035b1a3aa22ad139f3ce0ece1b059b506d9d9de/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a32cd81be21168bd0d6a0f0962d60177c447a1aa1b1e48fa6ec9fc728ee0b12", size = 3019202, upload-time = "2025-06-24T10:24:31.791Z" },
+ { url = "https://files.pythonhosted.org/packages/05/15/fd2d8104faa9f86ac68748e6f7ece0b5eb7983c7efc3a2c197cb98c99030/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8bd8999538c405133c2ab999b83b17c08b7fc1b48c1ada2469964605a709ef91", size = 2934539, upload-time = "2025-06-24T10:24:34.567Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/2e/53e8fd053e1f3ffbe579ca5f9546f35ac67cf0039ed357ad7ec57f5f5af0/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e9944e61239b083a41cf8fc42802f855e1dca0f499196df37a8ce219abac6eb", size = 3248665, upload-time = "2025-06-24T10:24:39.024Z" },
+ { url = "https://files.pythonhosted.org/packages/00/15/79713359f4037aa8f4d1f06ffca35312ac83629da062670e8830917e2153/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:514cd43045c5d546f01142ff9c79a96ea69e4b5cda09e3027708cb2e6d5762ab", size = 3451305, upload-time = "2025-06-24T10:24:36.133Z" },
+ { url = "https://files.pythonhosted.org/packages/38/5f/959f3a8756fc9396aeb704292777b84f02a5c6f25c3fc3ba7530db5feb2c/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1b9405822527ec1e0f7d8d2fdb287a5730c3a6518189c968254a8441b21faae", size = 3214757, upload-time = "2025-06-24T10:24:37.784Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/74/f41a432a0733f61f3d21b288de6dfa78f7acff309c6f0f323b2833e9189f/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed9a4d51c395103ad24f8e7eb976811c57fbec2af9f133df471afcd922e5020", size = 3121887, upload-time = "2025-06-24T10:24:40.293Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/6a/bc220a11a17e5d07b0dfb3b5c628621d4dcc084bccd27cfaead659963016/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c41862df3d873665ec78b6be36fcc30a26e3d4902e9dd8608ed61d49a48bc19", size = 9091965, upload-time = "2025-06-24T10:24:44.431Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/bd/ac386d79c4ef20dc6f39c4706640c24823dca7ebb6f703bfe6b5f0292d88/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed21dc7e624e4220e21758b2e62893be7101453525e3d23264081c9ef9a6d00d", size = 9053372, upload-time = "2025-06-24T10:24:46.455Z" },
+ { url = "https://files.pythonhosted.org/packages/63/7b/5440bf203b2a5358f074408f7f9c42884849cd9972879e10ee6b7a8c3b3d/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:0e73770507e65a0e0e2a1affd6b03c36e3bc4377bd10c9ccf51a82c77c0fe365", size = 9298632, upload-time = "2025-06-24T10:24:48.446Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/d2/faa1acac3f96a7427866e94ed4289949b2524f0c1878512516567d80563c/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:106746e8aa9014a12109e58d540ad5465b4c183768ea96c03cbc24c44d329958", size = 9470074, upload-time = "2025-06-24T10:24:50.378Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/a5/896e1ef0707212745ae9f37e84c7d50269411aef2e9ccd0de63623feecdf/tokenizers-0.21.2-cp39-abi3-win32.whl", hash = "sha256:cabda5a6d15d620b6dfe711e1af52205266d05b379ea85a8a301b3593c60e962", size = 2330115, upload-time = "2025-06-24T10:24:55.069Z" },
+ { url = "https://files.pythonhosted.org/packages/13/c3/cc2755ee10be859c4338c962a35b9a663788c0c0b50c0bdd8078fb6870cf/tokenizers-0.21.2-cp39-abi3-win_amd64.whl", hash = "sha256:58747bb898acdb1007f37a7bbe614346e98dc28708ffb66a3fd50ce169ac6c98", size = 2509918, upload-time = "2025-06-24T10:24:53.71Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
+ { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
+ { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
+ { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
+ { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
+ { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
+ { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
+ { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
+ { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
+ { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
+ { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
+ { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.67.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
+]
+
+[[package]]
+name = "trio"
+version = "0.28.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "attrs" },
+ { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "outcome" },
+ { name = "sniffio" },
+ { name = "sortedcontainers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/73/57efab729506a8d4b89814f1e356ec8f3369de0ed4fd7e7616974d09646d/trio-0.28.0.tar.gz", hash = "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05", size = 580318, upload-time = "2024-12-25T17:00:59.83Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b4/04/9954a59e1fb6732f5436225c9af963811d7b24ea62a8bf96991f2cb8c26e/trio-0.28.0-py3-none-any.whl", hash = "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94", size = 486317, upload-time = "2024-12-25T17:00:57.665Z" },
+]
+
+[[package]]
+name = "trio-asyncio"
+version = "0.15.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "greenlet" },
+ { name = "outcome" },
+ { name = "sniffio" },
+ { name = "trio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b2/29/f1b5dd48796526dc00849d2f6ca276724930aa2f96c32bca9bed01802c3b/trio_asyncio-0.15.0.tar.gz", hash = "sha256:061e31a71fb039d5074f064ec868dc0e6759e6cca33bf3080733a20ee9667781", size = 75674, upload-time = "2024-04-24T22:23:59.29Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2b/3f/a529b02ae6a4145721eaf952cdf19f2627bd4f5e248b010f77c0064eb4f6/trio_asyncio-0.15.0-py3-none-any.whl", hash = "sha256:7dad5a5edcc7c90c5b80b777dcaef11c22668ce7ddc374633068c2b35d683d62", size = 39786, upload-time = "2024-04-24T22:23:57.699Z" },
+]
+
+[[package]]
+name = "types-protobuf"
+version = "6.30.2.20250703"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/54/d63ce1eee8e93c4d710bbe2c663ec68e3672cf4f2fca26eecd20981c0c5d/types_protobuf-6.30.2.20250703.tar.gz", hash = "sha256:609a974754bbb71fa178fc641f51050395e8e1849f49d0420a6281ed8d1ddf46", size = 62300, upload-time = "2025-07-03T03:14:05.74Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/2b/5d0377c3d6e0f49d4847ad2c40629593fee4a5c9ec56eba26a15c708fbc0/types_protobuf-6.30.2.20250703-py3-none-any.whl", hash = "sha256:fa5aff9036e9ef432d703abbdd801b436a249b6802e4df5ef74513e272434e57", size = 76489, upload-time = "2025-07-03T03:14:04.453Z" },
+]
+
+[[package]]
+name = "types-pyyaml"
+version = "6.0.12.20250516"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/22/59e2aeb48ceeee1f7cd4537db9568df80d62bdb44a7f9e743502ea8aab9c/types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba", size = 17378, upload-time = "2025-05-16T03:08:04.897Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312, upload-time = "2025-05-16T03:08:04.019Z" },
+]
+
+[[package]]
+name = "types-requests"
+version = "2.32.4.20250611"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118, upload-time = "2025-06-11T03:11:41.272Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643, upload-time = "2025-06-11T03:11:40.186Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.14.1"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
+]
+
+[[package]]
+name = "typing-inspect"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "mypy-extensions" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.5.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.24.0.post1"
+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/e5/84/d43ce8fe6b31a316ef0ed04ea0d58cab981bdf7f17f8423491fa8b4f50b6/uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e", size = 40102, upload-time = "2023-11-06T06:37:42.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/17/4b7a76fffa7babf397481040d8aef2725b2b81ae19f1a31b5ca0c17d49e6/uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e", size = 59687, upload-time = "2023-11-06T06:37:37.726Z" },
+]
+
+[package.optional-dependencies]
+standard = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "httptools" },
+ { name = "python-dotenv" },
+ { name = "pyyaml" },
+ { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
+ { name = "watchfiles" },
+ { name = "websockets" },
+]
+
+[[package]]
+name = "uvloop"
+version = "0.21.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019, upload-time = "2024-10-14T23:37:20.068Z" },
+ { url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898, upload-time = "2024-10-14T23:37:22.663Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735, upload-time = "2024-10-14T23:37:25.129Z" },
+ { url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126, upload-time = "2024-10-14T23:37:27.59Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789, upload-time = "2024-10-14T23:37:29.385Z" },
+ { url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523, upload-time = "2024-10-14T23:37:32.048Z" },
+ { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410, upload-time = "2024-10-14T23:37:33.612Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476, upload-time = "2024-10-14T23:37:36.11Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855, upload-time = "2024-10-14T23:37:37.683Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185, upload-time = "2024-10-14T23:37:40.226Z" },
+ { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256, upload-time = "2024-10-14T23:37:42.839Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323, upload-time = "2024-10-14T23:37:45.337Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" },
+ { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" },
+ { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" },
+ { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" },
+ { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" },
+ { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" },
+ { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" },
+]
+
+[[package]]
+name = "watchfiles"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload-time = "2025-06-15T19:06:59.42Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/dd/579d1dc57f0f895426a1211c4ef3b0cb37eb9e642bb04bdcd962b5df206a/watchfiles-1.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc", size = 405757, upload-time = "2025-06-15T19:04:51.058Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/a0/7a0318cd874393344d48c34d53b3dd419466adf59a29ba5b51c88dd18b86/watchfiles-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df", size = 397511, upload-time = "2025-06-15T19:04:52.79Z" },
+ { url = "https://files.pythonhosted.org/packages/06/be/503514656d0555ec2195f60d810eca29b938772e9bfb112d5cd5ad6f6a9e/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68", size = 450739, upload-time = "2025-06-15T19:04:54.203Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/0d/a05dd9e5f136cdc29751816d0890d084ab99f8c17b86f25697288ca09bc7/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc", size = 458106, upload-time = "2025-06-15T19:04:55.607Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/fa/9cd16e4dfdb831072b7ac39e7bea986e52128526251038eb481effe9f48e/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97", size = 484264, upload-time = "2025-06-15T19:04:57.009Z" },
+ { url = "https://files.pythonhosted.org/packages/32/04/1da8a637c7e2b70e750a0308e9c8e662ada0cca46211fa9ef24a23937e0b/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c", size = 597612, upload-time = "2025-06-15T19:04:58.409Z" },
+ { url = "https://files.pythonhosted.org/packages/30/01/109f2762e968d3e58c95731a206e5d7d2a7abaed4299dd8a94597250153c/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5", size = 477242, upload-time = "2025-06-15T19:04:59.786Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/b8/46f58cf4969d3b7bc3ca35a98e739fa4085b0657a1540ccc29a1a0bc016f/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9", size = 453148, upload-time = "2025-06-15T19:05:01.103Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/cd/8267594263b1770f1eb76914940d7b2d03ee55eca212302329608208e061/watchfiles-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72", size = 626574, upload-time = "2025-06-15T19:05:02.582Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/2f/7f2722e85899bed337cba715723e19185e288ef361360718973f891805be/watchfiles-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc", size = 624378, upload-time = "2025-06-15T19:05:03.719Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/20/64c88ec43d90a568234d021ab4b2a6f42a5230d772b987c3f9c00cc27b8b/watchfiles-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587", size = 279829, upload-time = "2025-06-15T19:05:04.822Z" },
+ { url = "https://files.pythonhosted.org/packages/39/5c/a9c1ed33de7af80935e4eac09570de679c6e21c07070aa99f74b4431f4d6/watchfiles-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82", size = 292192, upload-time = "2025-06-15T19:05:06.348Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/78/7401154b78ab484ccaaeef970dc2af0cb88b5ba8a1b415383da444cdd8d3/watchfiles-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2", size = 405751, upload-time = "2025-06-15T19:05:07.679Z" },
+ { url = "https://files.pythonhosted.org/packages/76/63/e6c3dbc1f78d001589b75e56a288c47723de28c580ad715eb116639152b5/watchfiles-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c", size = 397313, upload-time = "2025-06-15T19:05:08.764Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/a2/8afa359ff52e99af1632f90cbf359da46184207e893a5f179301b0c8d6df/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d", size = 450792, upload-time = "2025-06-15T19:05:09.869Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/bf/7446b401667f5c64972a57a0233be1104157fc3abf72c4ef2666c1bd09b2/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7", size = 458196, upload-time = "2025-06-15T19:05:11.91Z" },
+ { url = "https://files.pythonhosted.org/packages/58/2f/501ddbdfa3fa874ea5597c77eeea3d413579c29af26c1091b08d0c792280/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c", size = 484788, upload-time = "2025-06-15T19:05:13.373Z" },
+ { url = "https://files.pythonhosted.org/packages/61/1e/9c18eb2eb5c953c96bc0e5f626f0e53cfef4bd19bd50d71d1a049c63a575/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575", size = 597879, upload-time = "2025-06-15T19:05:14.725Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/6c/1467402e5185d89388b4486745af1e0325007af0017c3384cc786fff0542/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8", size = 477447, upload-time = "2025-06-15T19:05:15.775Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/a1/ec0a606bde4853d6c4a578f9391eeb3684a9aea736a8eb217e3e00aa89a1/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f", size = 453145, upload-time = "2025-06-15T19:05:17.17Z" },
+ { url = "https://files.pythonhosted.org/packages/90/b9/ef6f0c247a6a35d689fc970dc7f6734f9257451aefb30def5d100d6246a5/watchfiles-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4", size = 626539, upload-time = "2025-06-15T19:05:18.557Z" },
+ { url = "https://files.pythonhosted.org/packages/34/44/6ffda5537085106ff5aaa762b0d130ac6c75a08015dd1621376f708c94de/watchfiles-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d", size = 624472, upload-time = "2025-06-15T19:05:19.588Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/e3/71170985c48028fa3f0a50946916a14055e741db11c2e7bc2f3b61f4d0e3/watchfiles-1.1.0-cp311-cp311-win32.whl", hash = "sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2", size = 279348, upload-time = "2025-06-15T19:05:20.856Z" },
+ { url = "https://files.pythonhosted.org/packages/89/1b/3e39c68b68a7a171070f81fc2561d23ce8d6859659406842a0e4bebf3bba/watchfiles-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12", size = 292607, upload-time = "2025-06-15T19:05:21.937Z" },
+ { url = "https://files.pythonhosted.org/packages/61/9f/2973b7539f2bdb6ea86d2c87f70f615a71a1fc2dba2911795cea25968aea/watchfiles-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a", size = 285056, upload-time = "2025-06-15T19:05:23.12Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/b8/858957045a38a4079203a33aaa7d23ea9269ca7761c8a074af3524fbb240/watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179", size = 402339, upload-time = "2025-06-15T19:05:24.516Z" },
+ { url = "https://files.pythonhosted.org/packages/80/28/98b222cca751ba68e88521fabd79a4fab64005fc5976ea49b53fa205d1fa/watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5", size = 394409, upload-time = "2025-06-15T19:05:25.469Z" },
+ { url = "https://files.pythonhosted.org/packages/86/50/dee79968566c03190677c26f7f47960aff738d32087087bdf63a5473e7df/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297", size = 450939, upload-time = "2025-06-15T19:05:26.494Z" },
+ { url = "https://files.pythonhosted.org/packages/40/45/a7b56fb129700f3cfe2594a01aa38d033b92a33dddce86c8dfdfc1247b72/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0", size = 457270, upload-time = "2025-06-15T19:05:27.466Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/c8/fa5ef9476b1d02dc6b5e258f515fcaaecf559037edf8b6feffcbc097c4b8/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e", size = 483370, upload-time = "2025-06-15T19:05:28.548Z" },
+ { url = "https://files.pythonhosted.org/packages/98/68/42cfcdd6533ec94f0a7aab83f759ec11280f70b11bfba0b0f885e298f9bd/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee", size = 598654, upload-time = "2025-06-15T19:05:29.997Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/74/b2a1544224118cc28df7e59008a929e711f9c68ce7d554e171b2dc531352/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd", size = 478667, upload-time = "2025-06-15T19:05:31.172Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/77/e3362fe308358dc9f8588102481e599c83e1b91c2ae843780a7ded939a35/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f", size = 452213, upload-time = "2025-06-15T19:05:32.299Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/17/c8f1a36540c9a1558d4faf08e909399e8133599fa359bf52ec8fcee5be6f/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4", size = 626718, upload-time = "2025-06-15T19:05:33.415Z" },
+ { url = "https://files.pythonhosted.org/packages/26/45/fb599be38b4bd38032643783d7496a26a6f9ae05dea1a42e58229a20ac13/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f", size = 623098, upload-time = "2025-06-15T19:05:34.534Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/e7/fdf40e038475498e160cd167333c946e45d8563ae4dd65caf757e9ffe6b4/watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd", size = 279209, upload-time = "2025-06-15T19:05:35.577Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/d3/3ae9d5124ec75143bdf088d436cba39812122edc47709cd2caafeac3266f/watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47", size = 292786, upload-time = "2025-06-15T19:05:36.559Z" },
+ { url = "https://files.pythonhosted.org/packages/26/2f/7dd4fc8b5f2b34b545e19629b4a018bfb1de23b3a496766a2c1165ca890d/watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6", size = 284343, upload-time = "2025-06-15T19:05:37.5Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/42/fae874df96595556a9089ade83be34a2e04f0f11eb53a8dbf8a8a5e562b4/watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30", size = 402004, upload-time = "2025-06-15T19:05:38.499Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/55/a77e533e59c3003d9803c09c44c3651224067cbe7fb5d574ddbaa31e11ca/watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a", size = 393671, upload-time = "2025-06-15T19:05:39.52Z" },
+ { url = "https://files.pythonhosted.org/packages/05/68/b0afb3f79c8e832e6571022611adbdc36e35a44e14f129ba09709aa4bb7a/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc", size = 449772, upload-time = "2025-06-15T19:05:40.897Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/05/46dd1f6879bc40e1e74c6c39a1b9ab9e790bf1f5a2fe6c08b463d9a807f4/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b", size = 456789, upload-time = "2025-06-15T19:05:42.045Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ca/0eeb2c06227ca7f12e50a47a3679df0cd1ba487ea19cf844a905920f8e95/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895", size = 482551, upload-time = "2025-06-15T19:05:43.781Z" },
+ { url = "https://files.pythonhosted.org/packages/31/47/2cecbd8694095647406645f822781008cc524320466ea393f55fe70eed3b/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a", size = 597420, upload-time = "2025-06-15T19:05:45.244Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/7e/82abc4240e0806846548559d70f0b1a6dfdca75c1b4f9fa62b504ae9b083/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b", size = 477950, upload-time = "2025-06-15T19:05:46.332Z" },
+ { url = "https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c", size = 451706, upload-time = "2025-06-15T19:05:47.459Z" },
+ { url = "https://files.pythonhosted.org/packages/81/b5/5516cf46b033192d544102ea07c65b6f770f10ed1d0a6d388f5d3874f6e4/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b", size = 625814, upload-time = "2025-06-15T19:05:48.654Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/dd/7c1331f902f30669ac3e754680b6edb9a0dd06dea5438e61128111fadd2c/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb", size = 622820, upload-time = "2025-06-15T19:05:50.088Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/14/36d7a8e27cd128d7b1009e7715a7c02f6c131be9d4ce1e5c3b73d0e342d8/watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9", size = 279194, upload-time = "2025-06-15T19:05:51.186Z" },
+ { url = "https://files.pythonhosted.org/packages/25/41/2dd88054b849aa546dbeef5696019c58f8e0774f4d1c42123273304cdb2e/watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7", size = 292349, upload-time = "2025-06-15T19:05:52.201Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/cf/421d659de88285eb13941cf11a81f875c176f76a6d99342599be88e08d03/watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5", size = 283836, upload-time = "2025-06-15T19:05:53.265Z" },
+ { url = "https://files.pythonhosted.org/packages/45/10/6faf6858d527e3599cc50ec9fcae73590fbddc1420bd4fdccfebffeedbc6/watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1", size = 400343, upload-time = "2025-06-15T19:05:54.252Z" },
+ { url = "https://files.pythonhosted.org/packages/03/20/5cb7d3966f5e8c718006d0e97dfe379a82f16fecd3caa7810f634412047a/watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339", size = 392916, upload-time = "2025-06-15T19:05:55.264Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/07/d8f1176328fa9e9581b6f120b017e286d2a2d22ae3f554efd9515c8e1b49/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633", size = 449582, upload-time = "2025-06-15T19:05:56.317Z" },
+ { url = "https://files.pythonhosted.org/packages/66/e8/80a14a453cf6038e81d072a86c05276692a1826471fef91df7537dba8b46/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011", size = 456752, upload-time = "2025-06-15T19:05:57.359Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/25/0853b3fe0e3c2f5af9ea60eb2e781eade939760239a72c2d38fc4cc335f6/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670", size = 481436, upload-time = "2025-06-15T19:05:58.447Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/9e/4af0056c258b861fbb29dcb36258de1e2b857be4a9509e6298abcf31e5c9/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf", size = 596016, upload-time = "2025-06-15T19:05:59.59Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/fa/95d604b58aa375e781daf350897aaaa089cff59d84147e9ccff2447c8294/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4", size = 476727, upload-time = "2025-06-15T19:06:01.086Z" },
+ { url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864, upload-time = "2025-06-15T19:06:02.144Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626, upload-time = "2025-06-15T19:06:03.578Z" },
+ { url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744, upload-time = "2025-06-15T19:06:05.066Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/00/70f75c47f05dea6fd30df90f047765f6fc2d6eb8b5a3921379b0b04defa2/watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297", size = 402114, upload-time = "2025-06-15T19:06:06.186Z" },
+ { url = "https://files.pythonhosted.org/packages/53/03/acd69c48db4a1ed1de26b349d94077cca2238ff98fd64393f3e97484cae6/watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018", size = 393879, upload-time = "2025-06-15T19:06:07.369Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/c8/a9a2a6f9c8baa4eceae5887fecd421e1b7ce86802bcfc8b6a942e2add834/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0", size = 450026, upload-time = "2025-06-15T19:06:08.476Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/51/d572260d98388e6e2b967425c985e07d47ee6f62e6455cefb46a6e06eda5/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12", size = 457917, upload-time = "2025-06-15T19:06:09.988Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/2d/4258e52917bf9f12909b6ec314ff9636276f3542f9d3807d143f27309104/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb", size = 483602, upload-time = "2025-06-15T19:06:11.088Z" },
+ { url = "https://files.pythonhosted.org/packages/84/99/bee17a5f341a4345fe7b7972a475809af9e528deba056f8963d61ea49f75/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77", size = 596758, upload-time = "2025-06-15T19:06:12.197Z" },
+ { url = "https://files.pythonhosted.org/packages/40/76/e4bec1d59b25b89d2b0716b41b461ed655a9a53c60dc78ad5771fda5b3e6/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92", size = 477601, upload-time = "2025-06-15T19:06:13.391Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/fa/a514292956f4a9ce3c567ec0c13cce427c158e9f272062685a8a727d08fc/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e", size = 451936, upload-time = "2025-06-15T19:06:14.656Z" },
+ { url = "https://files.pythonhosted.org/packages/32/5d/c3bf927ec3bbeb4566984eba8dd7a8eb69569400f5509904545576741f88/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b", size = 626243, upload-time = "2025-06-15T19:06:16.232Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/65/6e12c042f1a68c556802a84d54bb06d35577c81e29fba14019562479159c/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259", size = 623073, upload-time = "2025-06-15T19:06:17.457Z" },
+ { url = "https://files.pythonhosted.org/packages/89/ab/7f79d9bf57329e7cbb0a6fd4c7bd7d0cee1e4a8ef0041459f5409da3506c/watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f", size = 400872, upload-time = "2025-06-15T19:06:18.57Z" },
+ { url = "https://files.pythonhosted.org/packages/df/d5/3f7bf9912798e9e6c516094db6b8932df53b223660c781ee37607030b6d3/watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e", size = 392877, upload-time = "2025-06-15T19:06:19.55Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/c5/54ec7601a2798604e01c75294770dbee8150e81c6e471445d7601610b495/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa", size = 449645, upload-time = "2025-06-15T19:06:20.66Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/04/c2f44afc3b2fce21ca0b7802cbd37ed90a29874f96069ed30a36dfe57c2b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8", size = 457424, upload-time = "2025-06-15T19:06:21.712Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/b0/eec32cb6c14d248095261a04f290636da3df3119d4040ef91a4a50b29fa5/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f", size = 481584, upload-time = "2025-06-15T19:06:22.777Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/e2/ca4bb71c68a937d7145aa25709e4f5d68eb7698a25ce266e84b55d591bbd/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e", size = 596675, upload-time = "2025-06-15T19:06:24.226Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/dd/b0e4b7fb5acf783816bc950180a6cd7c6c1d2cf7e9372c0ea634e722712b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb", size = 477363, upload-time = "2025-06-15T19:06:25.42Z" },
+ { url = "https://files.pythonhosted.org/packages/69/c4/088825b75489cb5b6a761a4542645718893d395d8c530b38734f19da44d2/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147", size = 452240, upload-time = "2025-06-15T19:06:26.552Z" },
+ { url = "https://files.pythonhosted.org/packages/10/8c/22b074814970eeef43b7c44df98c3e9667c1f7bf5b83e0ff0201b0bd43f9/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8", size = 625607, upload-time = "2025-06-15T19:06:27.606Z" },
+ { url = "https://files.pythonhosted.org/packages/32/fa/a4f5c2046385492b2273213ef815bf71a0d4c1943b784fb904e184e30201/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db", size = 623315, upload-time = "2025-06-15T19:06:29.076Z" },
+ { url = "https://files.pythonhosted.org/packages/be/7c/a3d7c55cfa377c2f62c4ae3c6502b997186bc5e38156bafcb9b653de9a6d/watchfiles-1.1.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5", size = 406748, upload-time = "2025-06-15T19:06:44.2Z" },
+ { url = "https://files.pythonhosted.org/packages/38/d0/c46f1b2c0ca47f3667b144de6f0515f6d1c670d72f2ca29861cac78abaa1/watchfiles-1.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d", size = 398801, upload-time = "2025-06-15T19:06:45.774Z" },
+ { url = "https://files.pythonhosted.org/packages/70/9c/9a6a42e97f92eeed77c3485a43ea96723900aefa3ac739a8c73f4bff2cd7/watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea", size = 451528, upload-time = "2025-06-15T19:06:46.791Z" },
+ { url = "https://files.pythonhosted.org/packages/51/7b/98c7f4f7ce7ff03023cf971cd84a3ee3b790021ae7584ffffa0eb2554b96/watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6", size = 454095, upload-time = "2025-06-15T19:06:48.211Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/6b/686dcf5d3525ad17b384fd94708e95193529b460a1b7bf40851f1328ec6e/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3", size = 406910, upload-time = "2025-06-15T19:06:49.335Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d3/71c2dcf81dc1edcf8af9f4d8d63b1316fb0a2dd90cbfd427e8d9dd584a90/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c", size = 398816, upload-time = "2025-06-15T19:06:50.433Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/fa/12269467b2fc006f8fce4cd6c3acfa77491dd0777d2a747415f28ccc8c60/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432", size = 451584, upload-time = "2025-06-15T19:06:51.834Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/d3/254cea30f918f489db09d6a8435a7de7047f8cb68584477a515f160541d6/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792", size = 454009, upload-time = "2025-06-15T19:06:52.896Z" },
+]
+
+[[package]]
+name = "websockets"
+version = "15.0.1"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" },
+ { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" },
+ { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" },
+ { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
+ { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
+ { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
+ { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
+ { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
+ { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
+ { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
+ { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
+ { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
+ { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
+ { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
+ { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
+ { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
+ { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
+ { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" },
+ { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" },
+ { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
+]
+
+[[package]]
+name = "yarl"
+version = "1.20.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "idna" },
+ { name = "multidict" },
+ { name = "propcache" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/65/7fed0d774abf47487c64be14e9223749468922817b5e8792b8a64792a1bb/yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4", size = 132910, upload-time = "2025-06-10T00:42:31.108Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/7b/988f55a52da99df9e56dc733b8e4e5a6ae2090081dc2754fc8fd34e60aa0/yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a", size = 90644, upload-time = "2025-06-10T00:42:33.851Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/de/30d98f03e95d30c7e3cc093759982d038c8833ec2451001d45ef4854edc1/yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed", size = 89322, upload-time = "2025-06-10T00:42:35.688Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/7a/f2f314f5ebfe9200724b0b748de2186b927acb334cf964fd312eb86fc286/yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e", size = 323786, upload-time = "2025-06-10T00:42:37.817Z" },
+ { url = "https://files.pythonhosted.org/packages/15/3f/718d26f189db96d993d14b984ce91de52e76309d0fd1d4296f34039856aa/yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73", size = 319627, upload-time = "2025-06-10T00:42:39.937Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/76/8fcfbf5fa2369157b9898962a4a7d96764b287b085b5b3d9ffae69cdefd1/yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e", size = 339149, upload-time = "2025-06-10T00:42:42.627Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/95/d7fc301cc4661785967acc04f54a4a42d5124905e27db27bb578aac49b5c/yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8", size = 333327, upload-time = "2025-06-10T00:42:44.842Z" },
+ { url = "https://files.pythonhosted.org/packages/65/94/e21269718349582eee81efc5c1c08ee71c816bfc1585b77d0ec3f58089eb/yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23", size = 326054, upload-time = "2025-06-10T00:42:47.149Z" },
+ { url = "https://files.pythonhosted.org/packages/32/ae/8616d1f07853704523519f6131d21f092e567c5af93de7e3e94b38d7f065/yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70", size = 315035, upload-time = "2025-06-10T00:42:48.852Z" },
+ { url = "https://files.pythonhosted.org/packages/48/aa/0ace06280861ef055855333707db5e49c6e3a08840a7ce62682259d0a6c0/yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb", size = 338962, upload-time = "2025-06-10T00:42:51.024Z" },
+ { url = "https://files.pythonhosted.org/packages/20/52/1e9d0e6916f45a8fb50e6844f01cb34692455f1acd548606cbda8134cd1e/yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2", size = 335399, upload-time = "2025-06-10T00:42:53.007Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/65/60452df742952c630e82f394cd409de10610481d9043aa14c61bf846b7b1/yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30", size = 338649, upload-time = "2025-06-10T00:42:54.964Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/f5/6cd4ff38dcde57a70f23719a838665ee17079640c77087404c3d34da6727/yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309", size = 358563, upload-time = "2025-06-10T00:42:57.28Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/90/c42eefd79d0d8222cb3227bdd51b640c0c1d0aa33fe4cc86c36eccba77d3/yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24", size = 357609, upload-time = "2025-06-10T00:42:59.055Z" },
+ { url = "https://files.pythonhosted.org/packages/03/c8/cea6b232cb4617514232e0f8a718153a95b5d82b5290711b201545825532/yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13", size = 350224, upload-time = "2025-06-10T00:43:01.248Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/a3/eaa0ab9712f1f3d01faf43cf6f1f7210ce4ea4a7e9b28b489a2261ca8db9/yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8", size = 81753, upload-time = "2025-06-10T00:43:03.486Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/34/e4abde70a9256465fe31c88ed02c3f8502b7b5dead693a4f350a06413f28/yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16", size = 86817, upload-time = "2025-06-10T00:43:05.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833, upload-time = "2025-06-10T00:43:07.393Z" },
+ { url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070, upload-time = "2025-06-10T00:43:09.538Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818, upload-time = "2025-06-10T00:43:11.575Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003, upload-time = "2025-06-10T00:43:14.088Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537, upload-time = "2025-06-10T00:43:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358, upload-time = "2025-06-10T00:43:18.704Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362, upload-time = "2025-06-10T00:43:20.888Z" },
+ { url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979, upload-time = "2025-06-10T00:43:23.169Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274, upload-time = "2025-06-10T00:43:27.111Z" },
+ { url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294, upload-time = "2025-06-10T00:43:28.96Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169, upload-time = "2025-06-10T00:43:30.701Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776, upload-time = "2025-06-10T00:43:32.51Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341, upload-time = "2025-06-10T00:43:34.543Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988, upload-time = "2025-06-10T00:43:36.489Z" },
+ { url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113, upload-time = "2025-06-10T00:43:38.592Z" },
+ { url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485, upload-time = "2025-06-10T00:43:41.038Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686, upload-time = "2025-06-10T00:43:42.692Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" },
+ { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" },
+ { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" },
+ { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" },
+ { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" },
+ { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" },
+ { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" },
+ { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" },
+ { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" },
+ { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" },
+ { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" },
+ { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" },
+ { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" },
+ { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" },
+ { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" },
+ { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" },
+ { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" },
+ { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" },
+ { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" },
+ { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" },
+ { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" },
+ { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" },
+ { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" },
+]
+
+[[package]]
+name = "zipp"
+version = "3.23.0"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
+]
+
+[[package]]
+name = "zope-event"
+version = "5.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "setuptools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/c7/31e6f40282a2c548602c177826df281177caf79efaa101dd14314fb4ee73/zope_event-5.1.tar.gz", hash = "sha256:a153660e0c228124655748e990396b9d8295d6e4f546fa1b34f3319e1c666e7f", size = 18632, upload-time = "2025-06-26T07:14:22.72Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/ed/d8c3f56c1edb0ee9b51461dd08580382e9589850f769b69f0dedccff5215/zope_event-5.1-py3-none-any.whl", hash = "sha256:53de8f0e9f61dc0598141ac591f49b042b6d74784dab49971b9cc91d0f73a7df", size = 6905, upload-time = "2025-06-26T07:14:21.779Z" },
+]
+
+[[package]]
+name = "zope-interface"
+version = "7.2"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "setuptools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/30/93/9210e7606be57a2dfc6277ac97dcc864fd8d39f142ca194fdc186d596fda/zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe", size = 252960, upload-time = "2024-11-28T08:45:39.224Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/71/e6177f390e8daa7e75378505c5ab974e0bf59c1d3b19155638c7afbf4b2d/zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2", size = 208243, upload-time = "2024-11-28T08:47:29.781Z" },
+ { url = "https://files.pythonhosted.org/packages/52/db/7e5f4226bef540f6d55acfd95cd105782bc6ee044d9b5587ce2c95558a5e/zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a", size = 208759, upload-time = "2024-11-28T08:47:31.908Z" },
+ { url = "https://files.pythonhosted.org/packages/28/ea/fdd9813c1eafd333ad92464d57a4e3a82b37ae57c19497bcffa42df673e4/zope.interface-7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550f1c6588ecc368c9ce13c44a49b8d6b6f3ca7588873c679bd8fd88a1b557b6", size = 254922, upload-time = "2024-11-28T09:18:11.795Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/d3/0000a4d497ef9fbf4f66bb6828b8d0a235e690d57c333be877bec763722f/zope.interface-7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ef9e2f865721553c6f22a9ff97da0f0216c074bd02b25cf0d3af60ea4d6931d", size = 249367, upload-time = "2024-11-28T08:48:24.238Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/e5/0b359e99084f033d413419eff23ee9c2bd33bca2ca9f4e83d11856f22d10/zope.interface-7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27f926f0dcb058211a3bb3e0e501c69759613b17a553788b2caeb991bed3b61d", size = 254488, upload-time = "2024-11-28T08:48:28.816Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/90/12d50b95f40e3b2fc0ba7f7782104093b9fd62806b13b98ef4e580f2ca61/zope.interface-7.2-cp310-cp310-win_amd64.whl", hash = "sha256:144964649eba4c5e4410bb0ee290d338e78f179cdbfd15813de1a664e7649b3b", size = 211947, upload-time = "2024-11-28T08:48:18.831Z" },
+ { url = "https://files.pythonhosted.org/packages/98/7d/2e8daf0abea7798d16a58f2f3a2bf7588872eee54ac119f99393fdd47b65/zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2", size = 208776, upload-time = "2024-11-28T08:47:53.009Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/2a/0c03c7170fe61d0d371e4c7ea5b62b8cb79b095b3d630ca16719bf8b7b18/zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22", size = 209296, upload-time = "2024-11-28T08:47:57.993Z" },
+ { url = "https://files.pythonhosted.org/packages/49/b4/451f19448772b4a1159519033a5f72672221e623b0a1bd2b896b653943d8/zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7", size = 260997, upload-time = "2024-11-28T09:18:13.935Z" },
+ { url = "https://files.pythonhosted.org/packages/65/94/5aa4461c10718062c8f8711161faf3249d6d3679c24a0b81dd6fc8ba1dd3/zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c", size = 255038, upload-time = "2024-11-28T08:48:26.381Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/aa/1a28c02815fe1ca282b54f6705b9ddba20328fabdc37b8cf73fc06b172f0/zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a", size = 259806, upload-time = "2024-11-28T08:48:30.78Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/2c/82028f121d27c7e68632347fe04f4a6e0466e77bb36e104c8b074f3d7d7b/zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1", size = 212305, upload-time = "2024-11-28T08:49:14.525Z" },
+ { url = "https://files.pythonhosted.org/packages/68/0b/c7516bc3bad144c2496f355e35bd699443b82e9437aa02d9867653203b4a/zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7", size = 208959, upload-time = "2024-11-28T08:47:47.788Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/e9/1463036df1f78ff8c45a02642a7bf6931ae4a38a4acd6a8e07c128e387a7/zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465", size = 209357, upload-time = "2024-11-28T08:47:50.897Z" },
+ { url = "https://files.pythonhosted.org/packages/07/a8/106ca4c2add440728e382f1b16c7d886563602487bdd90004788d45eb310/zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89", size = 264235, upload-time = "2024-11-28T09:18:15.56Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/ca/57286866285f4b8a4634c12ca1957c24bdac06eae28fd4a3a578e30cf906/zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54", size = 259253, upload-time = "2024-11-28T08:48:29.025Z" },
+ { url = "https://files.pythonhosted.org/packages/96/08/2103587ebc989b455cf05e858e7fbdfeedfc3373358320e9c513428290b1/zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d", size = 264702, upload-time = "2024-11-28T08:48:37.363Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/c7/3c67562e03b3752ba4ab6b23355f15a58ac2d023a6ef763caaca430f91f2/zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5", size = 212466, upload-time = "2024-11-28T08:49:14.397Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/3b/e309d731712c1a1866d61b5356a069dd44e5b01e394b6cb49848fa2efbff/zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98", size = 208961, upload-time = "2024-11-28T08:48:29.865Z" },
+ { url = "https://files.pythonhosted.org/packages/49/65/78e7cebca6be07c8fc4032bfbb123e500d60efdf7b86727bb8a071992108/zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d", size = 209356, upload-time = "2024-11-28T08:48:33.297Z" },
+ { url = "https://files.pythonhosted.org/packages/11/b1/627384b745310d082d29e3695db5f5a9188186676912c14b61a78bbc6afe/zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c", size = 264196, upload-time = "2024-11-28T09:18:17.584Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/f6/54548df6dc73e30ac6c8a7ff1da73ac9007ba38f866397091d5a82237bd3/zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398", size = 259237, upload-time = "2024-11-28T08:48:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/66/ac05b741c2129fdf668b85631d2268421c5cd1a9ff99be1674371139d665/zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b", size = 264696, upload-time = "2024-11-28T08:48:41.161Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/2f/1bccc6f4cc882662162a1158cda1a7f616add2ffe322b28c99cb031b4ffc/zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd", size = 212472, upload-time = "2024-11-28T08:49:56.587Z" },
+]
diff --git a/worker_multiprocessing/README.md b/worker_multiprocessing/README.md
new file mode 100644
index 00000000..a4f06f86
--- /dev/null
+++ b/worker_multiprocessing/README.md
@@ -0,0 +1,93 @@
+# Worker Multiprocessing Sample
+
+
+## Python Concurrency Limitations
+
+CPU-bound tasks effectively cannot run in parallel in Python due to the [Global Interpreter Lock (GIL)](https://docs.python.org/3/glossary.html#term-global-interpreter-lock). The Python standard library's [`threading` module](https://docs.python.org/3/library/threading.html) provides the following guidance:
+
+> CPython implementation detail: In CPython, due to the Global Interpreter Lock, only one thread can execute Python code at once (even though certain performance-oriented libraries might overcome this limitation). If you want your application to make better use of the computational resources of multi-core machines, you are advised to use multiprocessing or concurrent.futures.ProcessPoolExecutor. However, threading is still an appropriate model if you want to run multiple I/O-bound tasks simultaneously.
+
+## Temporal Workflow Tasks in Python
+
+[Temporal Workflow Tasks](https://docs.temporal.io/tasks#workflow-task) are CPU-bound operations and therefore cannot be run concurrently using threads or an async runtime. Instead, we can use [`concurrent.futures.ProcessPoolExecutor`](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ProcessPoolExecutor) or the [`multiprocessing` module](https://docs.python.org/3/library/multiprocessing.html), as suggested by the `threading` documentation, to more appropriately utilize machine resources.
+
+This sample demonstrates how to use `concurrent.futures.ProcessPoolExecutor` to run multiple workflow worker processes.
+
+## Running the Sample
+
+To run, first see the root [README.md](../README.md) for prerequisites. Then execute the following commands from the root directory:
+
+```
+uv run worker_multiprocessing/worker.py
+uv run worker_multiprocessing/starter.py
+```
+
+Both `worker.py` and `starter.py` have minimal arguments that can be adjusted to modify how the sample runs.
+
+```
+uv run worker_multiprocessing/worker.py -h
+
+usage: worker.py [-h] [-w NUM_WORKFLOW_WORKERS] [-a NUM_ACTIVITY_WORKERS]
+
+options:
+ -h, --help show this help message and exit
+ -w, --num-workflow-workers NUM_WORKFLOW_WORKERS
+ -a, --num-activity-workers NUM_ACTIVITY_WORKERS
+```
+
+```
+uv run worker_multiprocessing/starter.py -h
+
+usage: starter.py [-h] [-n NUM_WORKFLOWS]
+
+options:
+ -h, --help show this help message and exit
+ -n, --num-workflows NUM_WORKFLOWS
+ the number of workflows to execute
+```
+
+## Example Output
+
+```
+uv run worker_multiprocessing/worker.py
+
+starting 2 workflow worker(s) and 1 activity worker(s)
+waiting for keyboard interrupt or for all workers to exit
+workflow-worker:0 starting
+workflow-worker:1 starting
+activity-worker:0 starting
+workflow-worker:0 shutting down
+activity-worker:0 shutting down
+workflow-worker:1 shutting down
+```
+
+
+```
+uv run worker_multiprocessing/starter.py
+
+wf-starting-pid:19178 | activity-pid:19180 | wf-ending-pid:19178
+wf-starting-pid:19179 | activity-pid:19180 | wf-ending-pid:19179
+wf-starting-pid:19178 | activity-pid:19180 | wf-ending-pid:19178
+wf-starting-pid:19179 | activity-pid:19180 | wf-ending-pid:19179
+wf-starting-pid:19178 | activity-pid:19180 | wf-ending-pid:19178
+wf-starting-pid:19179 | activity-pid:19180 | wf-ending-pid:19179
+wf-starting-pid:19178 | activity-pid:19180 | wf-ending-pid:19178
+wf-starting-pid:19179 | activity-pid:19180 | wf-ending-pid:19179
+wf-starting-pid:19178 | activity-pid:19180 | wf-ending-pid:19178
+wf-starting-pid:19179 | activity-pid:19180 | wf-ending-pid:19179
+wf-starting-pid:19178 | activity-pid:19180 | wf-ending-pid:19178
+wf-starting-pid:19179 | activity-pid:19180 | wf-ending-pid:19179
+wf-starting-pid:19178 | activity-pid:19180 | wf-ending-pid:19178
+wf-starting-pid:19179 | activity-pid:19180 | wf-ending-pid:19179
+wf-starting-pid:19178 | activity-pid:19180 | wf-ending-pid:19178
+wf-starting-pid:19179 | activity-pid:19180 | wf-ending-pid:19179
+wf-starting-pid:19178 | activity-pid:19180 | wf-ending-pid:19178
+wf-starting-pid:19179 | activity-pid:19180 | wf-ending-pid:19179
+wf-starting-pid:19179 | activity-pid:19180 | wf-ending-pid:19179
+wf-starting-pid:19178 | activity-pid:19180 | wf-ending-pid:19178
+wf-starting-pid:19179 | activity-pid:19180 | wf-ending-pid:19179
+wf-starting-pid:19178 | activity-pid:19180 | wf-ending-pid:19178
+wf-starting-pid:19179 | activity-pid:19180 | wf-ending-pid:19179
+wf-starting-pid:19178 | activity-pid:19180 | wf-ending-pid:19178
+wf-starting-pid:19178 | activity-pid:19180 | wf-ending-pid:19178
+```
diff --git a/worker_multiprocessing/__init__.py b/worker_multiprocessing/__init__.py
new file mode 100644
index 00000000..d9e30b8a
--- /dev/null
+++ b/worker_multiprocessing/__init__.py
@@ -0,0 +1,2 @@
+WORKFLOW_TASK_QUEUE = "workflow-task-queue"
+ACTIVITY_TASK_QUEUE = "activity-task-queue"
diff --git a/worker_multiprocessing/activities.py b/worker_multiprocessing/activities.py
new file mode 100644
index 00000000..dbbbc677
--- /dev/null
+++ b/worker_multiprocessing/activities.py
@@ -0,0 +1,8 @@
+import os
+
+from temporalio import activity
+
+
+@activity.defn
+async def echo_pid_activity(input: str) -> str:
+ return f"{input} | activity-pid:{os.getpid()}"
diff --git a/worker_multiprocessing/starter.py b/worker_multiprocessing/starter.py
new file mode 100644
index 00000000..3267694d
--- /dev/null
+++ b/worker_multiprocessing/starter.py
@@ -0,0 +1,48 @@
+import argparse
+import asyncio
+import uuid
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+from worker_multiprocessing import WORKFLOW_TASK_QUEUE
+from worker_multiprocessing.workflows import ParallelizedWorkflow
+
+
+class Args(argparse.Namespace):
+ num_workflows: int
+
+
+async def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "-n",
+ "--num-workflows",
+ help="the number of workflows to execute",
+ type=int,
+ default=25,
+ )
+ args = parser.parse_args(namespace=Args())
+
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Start several workflows
+ wf_handles = [
+ client.execute_workflow(
+ ParallelizedWorkflow.run,
+ id=f"greeting-workflow-id-{uuid.uuid4()}",
+ task_queue=WORKFLOW_TASK_QUEUE,
+ )
+ for _ in range(args.num_workflows)
+ ]
+
+ # Wait for workflow completion
+ for wf in asyncio.as_completed(wf_handles):
+ result = await wf
+ print(result)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/worker_multiprocessing/worker.py b/worker_multiprocessing/worker.py
new file mode 100644
index 00000000..bda69ba6
--- /dev/null
+++ b/worker_multiprocessing/worker.py
@@ -0,0 +1,146 @@
+import argparse
+import asyncio
+import concurrent.futures
+import dataclasses
+import multiprocessing
+import traceback
+from typing import Literal
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+from temporalio.runtime import Runtime, TelemetryConfig
+from temporalio.worker import PollerBehaviorSimpleMaximum, Worker
+from temporalio.worker.workflow_sandbox import (
+ SandboxedWorkflowRunner,
+ SandboxRestrictions,
+)
+
+from worker_multiprocessing import ACTIVITY_TASK_QUEUE, WORKFLOW_TASK_QUEUE
+from worker_multiprocessing.activities import echo_pid_activity
+from worker_multiprocessing.workflows import ParallelizedWorkflow
+
+# Immediately prevent the default Runtime from being created to ensure
+# each process creates it's own
+Runtime.prevent_default()
+
+
+class Args(argparse.Namespace):
+ num_workflow_workers: int
+ num_activity_workers: int
+
+ @property
+ def total_workers(self) -> int:
+ return self.num_activity_workers + self.num_workflow_workers
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-w", "--num-workflow-workers", type=int, default=2)
+ parser.add_argument("-a", "--num-activity-workers", type=int, default=1)
+ args = parser.parse_args(namespace=Args())
+ print(
+ f"starting {args.num_workflow_workers} workflow worker(s) and {args.num_activity_workers} activity worker(s)"
+ )
+
+ # This sample prefers fork to avoid re-importing modules
+ # and decrease startup time. Fork is not available on all
+ # operating systems, so we fallback to 'spawn' when not available
+ try:
+ mp_ctx = multiprocessing.get_context("fork")
+ except ValueError:
+ mp_ctx = multiprocessing.get_context("spawn") # type: ignore
+
+ with concurrent.futures.ProcessPoolExecutor(
+ args.total_workers, mp_context=mp_ctx
+ ) as executor:
+ # Start workflow workers by submitting them to the
+ # ProcessPoolExecutor
+ worker_futures = [
+ executor.submit(worker_entry, "workflow", i)
+ for i in range(args.num_workflow_workers)
+ ]
+
+ # In this sample, we start activity workers as separate processes in the
+ # same way we do workflow workers. In production, activity workers
+ # are often deployed separately from workflow workers to account for
+ # differing scaling characteristics.
+ worker_futures.extend(
+ [
+ executor.submit(worker_entry, "activity", i)
+ for i in range(args.num_activity_workers)
+ ]
+ )
+
+ try:
+ print("waiting for keyboard interrupt or for all workers to exit")
+ for worker in concurrent.futures.as_completed(worker_futures):
+ print("ERROR: worker exited unexpectedly")
+ if worker.exception():
+ traceback.print_exception(worker.exception())
+ except KeyboardInterrupt:
+ pass
+
+
+def worker_entry(worker_type: Literal["workflow", "activity"], id: int):
+ Runtime.set_default(Runtime(telemetry=TelemetryConfig()))
+
+ async def run_worker():
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ if worker_type == "workflow":
+ worker = workflow_worker(client)
+ else:
+ worker = activity_worker(client)
+
+ try:
+ print(f"{worker_type}-worker:{id} starting")
+ await asyncio.shield(worker.run())
+ except asyncio.CancelledError:
+ print(f"{worker_type}-worker:{id} shutting down")
+ await worker.shutdown()
+
+ asyncio.run(run_worker())
+
+
+def workflow_worker(client: Client) -> Worker:
+ """
+ Create a workflow worker that is configured to leverage being run
+ as many child processes.
+ """
+ return Worker(
+ client,
+ task_queue=WORKFLOW_TASK_QUEUE,
+ workflows=[ParallelizedWorkflow],
+ # Workflow tasks are CPU bound, but generally execute quickly.
+ # Because we're leveraging multiprocessing to achieve parallelism,
+ # we want each workflow worker to be confirgured for small workflow
+ # task processing.
+ max_concurrent_workflow_tasks=2,
+ workflow_task_poller_behavior=PollerBehaviorSimpleMaximum(2),
+ # Allow workflows to access the os module to access the pid
+ workflow_runner=SandboxedWorkflowRunner(
+ restrictions=dataclasses.replace(
+ SandboxRestrictions.default,
+ invalid_module_members=SandboxRestrictions.invalid_module_members_default.with_child_unrestricted(
+ "os"
+ ),
+ )
+ ),
+ )
+
+
+def activity_worker(client: Client) -> Worker:
+ """
+ Create a basic activity worker
+ """
+ return Worker(
+ client,
+ task_queue=ACTIVITY_TASK_QUEUE,
+ activities=[echo_pid_activity],
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/worker_multiprocessing/workflows.py b/worker_multiprocessing/workflows.py
new file mode 100644
index 00000000..04f6b635
--- /dev/null
+++ b/worker_multiprocessing/workflows.py
@@ -0,0 +1,22 @@
+import os
+from datetime import timedelta
+
+from temporalio import workflow
+
+from worker_multiprocessing import ACTIVITY_TASK_QUEUE
+from worker_multiprocessing.activities import echo_pid_activity
+
+
+@workflow.defn
+class ParallelizedWorkflow:
+ @workflow.run
+ async def run(self) -> str:
+ pid = os.getpid()
+ activity_result = await workflow.execute_activity(
+ echo_pid_activity,
+ f"wf-starting-pid:{pid}",
+ task_queue=ACTIVITY_TASK_QUEUE,
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+
+ return f"{activity_result} | wf-ending-pid:{pid}"
diff --git a/worker_specific_task_queues/README.md b/worker_specific_task_queues/README.md
new file mode 100644
index 00000000..6fb17e6e
--- /dev/null
+++ b/worker_specific_task_queues/README.md
@@ -0,0 +1,56 @@
+# Worker-Specific Task Queues
+
+Use a unique Task Queue for each Worker in order to have certain Activities run on a specific Worker. In the Go SDK, this is explicitly supported via the Session option, but in other SDKs a different approach is required.
+
+Typical use cases include tasks where interaction with a filesystem is required, such as data processing or interacting with legacy access structures. This example will write text files to folders corresponding to each worker, located in the `demo_fs` folder. In production, these folders would typically be independent machines in a worker cluster.
+
+This strategy is:
+
+- Each Worker process runs two `Worker`s:
+ - One `Worker` listens on the `worker_specific_task_queue-distribution-queue` Task Queue.
+ - Another `Worker` listens on a uniquely generated Task Queue.
+- The Workflow and the first Activity are run on `worker_specific_task_queue-distribution-queue`.
+- The first Activity returns one of the uniquely generated Task Queues (that only one Worker is listening on—i.e. the **Worker-specific Task Queue**).
+- The rest of the Activities do the file processing and are run on the Worker-specific Task Queue.
+
+Check the Temporal Web UI to confirm tasks were staying with their respective worker.
+
+It doesn't matter where the `get_available_task_queue` activity is run, so it can be executed on the shared Task Queue. In this demo, `unique_worker_task_queue` is simply a `uuid` initialized in the Worker, but you can inject smart logic here to uniquely identify the Worker, [as Netflix did](https://community.temporal.io/t/using-dynamic-task-queues-for-traffic-routing/3045).
+
+Activities have been artificially slowed with `time.sleep(3)` to simulate doing more work.
+
+### Running This Sample
+
+To run, first see [README.md](../README.md) for prerequisites. Then, run the following from the root directory to start the
+worker:
+
+ uv run worker_specific_task_queues/worker.py
+
+This will start the worker. Then, in another terminal, run the following to execute the workflow:
+
+ uv run worker_specific_task_queues/starter.py
+
+#### Example output:
+
+```bash
+(temporalio-samples-py3.10) user@machine:~/samples-python/activities_sticky_queues$ uv run starter.py
+Output checksums:
+49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
+49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
+49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
+49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
+49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
+49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
+49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
+49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
+49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
+49d7419e6cba3575b3158f62d053f922aa08b23c64f05411cda3213b56c84ba4
+```
+
+
+Checking the history to see where activities are run
+All activities for the one workflow are running against the same task queue, which corresponds to unique workers:
+
+
+
+
diff --git a/worker_specific_task_queues/__init__.py b/worker_specific_task_queues/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/activity_sticky_queues/demo_fs/.gitignore b/worker_specific_task_queues/demo_fs/.gitignore
similarity index 100%
rename from activity_sticky_queues/demo_fs/.gitignore
rename to worker_specific_task_queues/demo_fs/.gitignore
diff --git a/activity_sticky_queues/starter.py b/worker_specific_task_queues/starter.py
similarity index 55%
rename from activity_sticky_queues/starter.py
rename to worker_specific_task_queues/starter.py
index 98c91bfc..61009436 100644
--- a/activity_sticky_queues/starter.py
+++ b/worker_specific_task_queues/starter.py
@@ -2,21 +2,24 @@
from uuid import uuid4
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
-from activity_sticky_queues.tasks import FileProcessing
+from worker_specific_task_queues.tasks import FileProcessing
async def main():
# Connect client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Start 10 concurrent workflows
futures = []
for idx in range(10):
result = client.execute_workflow(
FileProcessing.run,
- id=f"activity_sticky_queue-workflow-id-{idx}",
- task_queue="activity_sticky_queue-distribution-queue",
+ id=f"worker_specific_task_queue-workflow-id-{idx}",
+ task_queue="worker_specific_task_queue-distribution-queue",
)
await asyncio.sleep(0.1)
futures.append(result)
diff --git a/activity_sticky_queues/static/all-activitites-on-same-task-queue.png b/worker_specific_task_queues/static/all-activitites-on-same-task-queue.png
similarity index 100%
rename from activity_sticky_queues/static/all-activitites-on-same-task-queue.png
rename to worker_specific_task_queues/static/all-activitites-on-same-task-queue.png
diff --git a/activity_sticky_queues/tasks.py b/worker_specific_task_queues/tasks.py
similarity index 91%
rename from activity_sticky_queues/tasks.py
rename to worker_specific_task_queues/tasks.py
index c6764816..c54eb8b5 100644
--- a/activity_sticky_queues/tasks.py
+++ b/worker_specific_task_queues/tasks.py
@@ -63,6 +63,7 @@ async def get_available_task_queue() -> str:
async def download_file_to_worker_filesystem(details: DownloadObj) -> str:
"""Simulates downloading a file to a local filesystem"""
# FS ops
+ print(details.unique_worker_id, details.workflow_uuid)
path = create_filepath(details.unique_worker_id, details.workflow_uuid)
activity.logger.info(f"Downloading ${details.url} and saving to ${path}")
@@ -101,9 +102,11 @@ class FileProcessing:
async def run(self) -> str:
"""Workflow implementing the basic file processing example.
- First, a worker is selected randomly. This is the "sticky worker" on which
- the workflow runs. This consists of a file download and some processing task,
- with a file cleanup if an error occurs.
+ First, a task queue is selected randomly. A single worker is listening on
+ this queue, so when we execute all the file processing activities on this
+ queue, they will all be run on the same worker, and all be able to access
+ the same file on disk. The activities download the file, do some processing
+ task on the file, and clean up the file.
"""
workflow.logger.info("Searching for available worker")
unique_worker_task_queue = await workflow.execute_activity(
diff --git a/activity_sticky_queues/worker.py b/worker_specific_task_queues/worker.py
similarity index 55%
rename from activity_sticky_queues/worker.py
rename to worker_specific_task_queues/worker.py
index 721daa47..95824cfd 100644
--- a/activity_sticky_queues/worker.py
+++ b/worker_specific_task_queues/worker.py
@@ -6,9 +6,10 @@
from temporalio import activity
from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker
-from activity_sticky_queues import tasks
+from worker_specific_task_queues import tasks
interrupt_event = asyncio.Event()
@@ -21,44 +22,44 @@ async def main():
random.seed(667)
# Create random task queues and build task queue selection function
- task_queues: List[str] = [
- f"activity_sticky_queue-host-{UUID(int=random.getrandbits(128))}"
- for _ in range(5)
- ]
+ task_queue: str = (
+ f"worker_specific_task_queue-host-{UUID(int=random.getrandbits(128))}"
+ )
@activity.defn(name="get_available_task_queue")
- async def select_task_queue_random() -> str:
+ async def select_task_queue() -> str:
"""Randomly assign the job to a queue"""
- return random.choice(task_queues)
+ return task_queue
# Start client
- client = await Client.connect("localhost:7233")
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
# Run a worker to distribute the workflows
run_futures = []
handle = Worker(
client,
- task_queue="activity_sticky_queue-distribution-queue",
+ task_queue="worker_specific_task_queue-distribution-queue",
workflows=[tasks.FileProcessing],
- activities=[select_task_queue_random],
+ activities=[select_task_queue],
)
run_futures.append(handle.run())
print("Base worker started")
- # Run the workers for the individual task queues
- for queue_id in task_queues:
- handle = Worker(
- client,
- task_queue=queue_id,
- activities=[
- tasks.download_file_to_worker_filesystem,
- tasks.work_on_file_in_worker_filesystem,
- tasks.clean_up_file_from_worker_filesystem,
- ],
- )
- run_futures.append(handle.run())
- # Wait until interrupted
- print(f"Worker {queue_id} started")
+ # Run unique task queue for this particular host
+ handle = Worker(
+ client,
+ task_queue=task_queue,
+ activities=[
+ tasks.download_file_to_worker_filesystem,
+ tasks.work_on_file_in_worker_filesystem,
+ tasks.clean_up_file_from_worker_filesystem,
+ ],
+ )
+ run_futures.append(handle.run())
+ # Wait until interrupted
+ print(f"Worker {task_queue} started")
print("All workers started, ctrl+c to exit")
await asyncio.gather(*run_futures)
diff --git a/worker_versioning/README.md b/worker_versioning/README.md
new file mode 100644
index 00000000..0ff423f1
--- /dev/null
+++ b/worker_versioning/README.md
@@ -0,0 +1,26 @@
+## Worker Versioning
+
+This sample demonstrates how to use Temporal's Worker Versioning feature to safely deploy updates to workflow and activity code. It shows the difference between auto-upgrading and pinned workflows, and how to manage worker deployments with different build IDs.
+
+The sample creates multiple worker versions (1.0, 1.1, and 2.0) within one deployment and demonstrates:
+- **Auto-upgrading workflows**: Automatically and controllably migrate to newer worker versions
+- **Pinned workflows**: Stay on the original worker version throughout their lifecycle
+- **Compatible vs incompatible changes**: How to make safe updates using `workflow.patched`
+
+### Steps to run this sample:
+
+1) Run a [Temporal service](https://github.com/temporalio/samples-python/tree/main/#how-to-use).
+ Ensure that you're using at least Server version 1.28.0 (CLI version 1.4.0).
+
+2) Start the main application (this will guide you through the sample):
+```bash
+uv run worker_versioning/app.py
+```
+
+3) Follow the prompts to start workers in separate terminals:
+ - When prompted, run: `uv run worker_versioning/workerv1.py`
+ - When prompted, run: `uv run worker_versioning/workerv1_1.py`
+ - When prompted, run: `uv run worker_versioning/workerv2.py`
+
+The sample will show how auto-upgrading workflows migrate to newer workers while pinned workflows
+remain on their original version.
diff --git a/worker_versioning/__init__.py b/worker_versioning/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/worker_versioning/activities.py b/worker_versioning/activities.py
new file mode 100644
index 00000000..50c0700f
--- /dev/null
+++ b/worker_versioning/activities.py
@@ -0,0 +1,23 @@
+from dataclasses import dataclass
+
+from temporalio import activity
+
+
+@dataclass
+class IncompatibleActivityInput:
+ """Input for the incompatible activity."""
+
+ called_by: str
+ more_data: str
+
+
+@activity.defn
+async def some_activity(called_by: str) -> str:
+ """Basic activity for the workflow."""
+ return f"some_activity called by {called_by}"
+
+
+@activity.defn
+async def some_incompatible_activity(input_data: IncompatibleActivityInput) -> str:
+ """Incompatible activity that takes different input."""
+ return f"some_incompatible_activity called by {input_data.called_by} with {input_data.more_data}"
diff --git a/worker_versioning/app.py b/worker_versioning/app.py
new file mode 100644
index 00000000..78e1d641
--- /dev/null
+++ b/worker_versioning/app.py
@@ -0,0 +1,142 @@
+"""Main application for the worker versioning sample."""
+
+import asyncio
+import logging
+import uuid
+
+from temporalio.client import Client
+from temporalio.envconfig import ClientConfig
+
+TASK_QUEUE = "worker-versioning"
+DEPLOYMENT_NAME = "my-deployment"
+
+logging.basicConfig(level=logging.INFO)
+
+
+async def main() -> None:
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Wait for v1 worker and set as current version
+ logging.info(
+ "Waiting for v1 worker to appear. Run `python worker_versioning/workerv1.py` in another terminal"
+ )
+ await wait_for_worker_and_make_current(client, "1.0")
+
+ # Start auto-upgrading and pinned workflows. Importantly, note that when we start the workflows,
+ # we are using a workflow type name which does *not* include the version number. We defined them
+ # with versioned names so we could show changes to the code, but here when the client invokes
+ # them, we're demonstrating that the client remains version-agnostic.
+ auto_upgrade_workflow_id = "worker-versioning-versioning-autoupgrade_" + str(
+ uuid.uuid4()
+ )
+ auto_upgrade_execution = await client.start_workflow(
+ "AutoUpgrading",
+ id=auto_upgrade_workflow_id,
+ task_queue=TASK_QUEUE,
+ )
+
+ pinned_workflow_id = "worker-versioning-versioning-pinned_" + str(uuid.uuid4())
+ pinned_execution = await client.start_workflow(
+ "Pinned",
+ id=pinned_workflow_id,
+ task_queue=TASK_QUEUE,
+ )
+
+ logging.info("Started auto-upgrading workflow: %s", auto_upgrade_execution.id)
+ logging.info("Started pinned workflow: %s", pinned_execution.id)
+
+ # Signal both workflows a few times to drive them
+ await advance_workflows(auto_upgrade_execution, pinned_execution)
+
+ # Now wait for the v1.1 worker to appear and become current
+ logging.info(
+ "Waiting for v1.1 worker to appear. Run `python worker_versioning/workerv1_1.py` in another terminal"
+ )
+ await wait_for_worker_and_make_current(client, "1.1")
+
+ # Once it has, we will continue to advance the workflows.
+ # The auto-upgrade workflow will now make progress on the new worker, while the pinned one will
+ # keep progressing on the old worker.
+ await advance_workflows(auto_upgrade_execution, pinned_execution)
+
+ # Finally we'll start the v2 worker, and again it'll become the new current version
+ logging.info(
+ "Waiting for v2 worker to appear. Run `python worker_versioning/workerv2.py` in another terminal"
+ )
+ await wait_for_worker_and_make_current(client, "2.0")
+
+ # Once it has we'll start one more new workflow, another pinned one, to demonstrate that new
+ # pinned workflows start on the current version.
+ pinned_workflow_2_id = "worker-versioning-versioning-pinned-2_" + str(uuid.uuid4())
+ pinned_execution_2 = await client.start_workflow(
+ "Pinned",
+ id=pinned_workflow_2_id,
+ task_queue=TASK_QUEUE,
+ )
+ logging.info("Started pinned workflow v2: %s", pinned_execution_2.id)
+
+ # Now we'll conclude all workflows. You should be able to see in your server UI that the pinned
+ # workflow always stayed on 1.0, while the auto-upgrading workflow migrated.
+ for handle in [auto_upgrade_execution, pinned_execution, pinned_execution_2]:
+ await handle.signal("do_next_signal", "conclude")
+ await handle.result()
+
+ logging.info("All workflows completed")
+
+
+async def advance_workflows(auto_upgrade_execution, pinned_execution):
+ """Signal both workflows a few times to drive them."""
+ for i in range(3):
+ await auto_upgrade_execution.signal("do_next_signal", "do-activity")
+ await pinned_execution.signal("do_next_signal", "some-signal")
+
+
+async def wait_for_worker_and_make_current(client: Client, build_id: str) -> None:
+ import temporalio.api.workflowservice.v1 as wsv1
+ from temporalio.common import WorkerDeploymentVersion
+
+ target_version = WorkerDeploymentVersion(
+ deployment_name=DEPLOYMENT_NAME, build_id=build_id
+ )
+
+ while True:
+ try:
+ describe_request = wsv1.DescribeWorkerDeploymentRequest(
+ namespace=client.namespace,
+ deployment_name=DEPLOYMENT_NAME,
+ )
+ response = await client.workflow_service.describe_worker_deployment(
+ describe_request
+ )
+
+ for version_summary in response.worker_deployment_info.version_summaries:
+ if (
+ version_summary.deployment_version.deployment_name
+ == target_version.deployment_name
+ and version_summary.deployment_version.build_id
+ == target_version.build_id
+ ):
+ break
+ else:
+ await asyncio.sleep(1)
+ continue
+
+ break
+
+ except Exception:
+ await asyncio.sleep(1)
+ continue
+
+ # Once the version is available, set it as current
+ set_request = wsv1.SetWorkerDeploymentCurrentVersionRequest(
+ namespace=client.namespace,
+ deployment_name=DEPLOYMENT_NAME,
+ build_id=target_version.build_id,
+ )
+ await client.workflow_service.set_worker_deployment_current_version(set_request)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/worker_versioning/workerv1.py b/worker_versioning/workerv1.py
new file mode 100644
index 00000000..221b5ca9
--- /dev/null
+++ b/worker_versioning/workerv1.py
@@ -0,0 +1,43 @@
+"""Worker v1 for the worker versioning sample."""
+
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.common import WorkerDeploymentVersion
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker, WorkerDeploymentConfig
+
+from worker_versioning.activities import some_activity, some_incompatible_activity
+from worker_versioning.app import DEPLOYMENT_NAME, TASK_QUEUE
+from worker_versioning.workflows import AutoUpgradingWorkflowV1, PinnedWorkflowV1
+
+logging.basicConfig(level=logging.INFO)
+
+
+async def main() -> None:
+ """Run worker v1."""
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect("localhost:7233")
+
+ # Create worker v1
+ worker = Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[AutoUpgradingWorkflowV1, PinnedWorkflowV1],
+ activities=[some_activity, some_incompatible_activity],
+ deployment_config=WorkerDeploymentConfig(
+ version=WorkerDeploymentVersion(
+ deployment_name=DEPLOYMENT_NAME, build_id="1.0"
+ ),
+ use_worker_versioning=True,
+ ),
+ )
+
+ logging.info("Starting worker v1 (build 1.0)")
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/worker_versioning/workerv1_1.py b/worker_versioning/workerv1_1.py
new file mode 100644
index 00000000..4f21d616
--- /dev/null
+++ b/worker_versioning/workerv1_1.py
@@ -0,0 +1,43 @@
+"""Worker v1.1 for the worker versioning sample."""
+
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.common import WorkerDeploymentVersion
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker, WorkerDeploymentConfig
+
+from worker_versioning.activities import some_activity, some_incompatible_activity
+from worker_versioning.app import DEPLOYMENT_NAME, TASK_QUEUE
+from worker_versioning.workflows import AutoUpgradingWorkflowV1b, PinnedWorkflowV1
+
+logging.basicConfig(level=logging.INFO)
+
+
+async def main() -> None:
+ """Run worker v1.1."""
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Create worker v1.1
+ worker = Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[AutoUpgradingWorkflowV1b, PinnedWorkflowV1],
+ activities=[some_activity, some_incompatible_activity],
+ deployment_config=WorkerDeploymentConfig(
+ version=WorkerDeploymentVersion(
+ deployment_name=DEPLOYMENT_NAME, build_id="1.1"
+ ),
+ use_worker_versioning=True,
+ ),
+ )
+
+ logging.info("Starting worker v1.1 (build 1.1)")
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/worker_versioning/workerv2.py b/worker_versioning/workerv2.py
new file mode 100644
index 00000000..557f70ab
--- /dev/null
+++ b/worker_versioning/workerv2.py
@@ -0,0 +1,43 @@
+"""Worker v2 for the worker versioning sample."""
+
+import asyncio
+import logging
+
+from temporalio.client import Client
+from temporalio.common import WorkerDeploymentVersion
+from temporalio.envconfig import ClientConfig
+from temporalio.worker import Worker, WorkerDeploymentConfig
+
+from worker_versioning.activities import some_activity, some_incompatible_activity
+from worker_versioning.app import DEPLOYMENT_NAME, TASK_QUEUE
+from worker_versioning.workflows import AutoUpgradingWorkflowV1b, PinnedWorkflowV2
+
+logging.basicConfig(level=logging.INFO)
+
+
+async def main() -> None:
+ """Run worker v2."""
+ config = ClientConfig.load_client_connect_config()
+ config.setdefault("target_host", "localhost:7233")
+ client = await Client.connect(**config)
+
+ # Create worker v2
+ worker = Worker(
+ client,
+ task_queue=TASK_QUEUE,
+ workflows=[AutoUpgradingWorkflowV1b, PinnedWorkflowV2],
+ activities=[some_activity, some_incompatible_activity],
+ deployment_config=WorkerDeploymentConfig(
+ version=WorkerDeploymentVersion(
+ deployment_name=DEPLOYMENT_NAME, build_id="2.0"
+ ),
+ use_worker_versioning=True,
+ ),
+ )
+
+ logging.info("Starting worker v2 (build 2.0)")
+ await worker.run()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/worker_versioning/workflows.py b/worker_versioning/workflows.py
new file mode 100644
index 00000000..a09c371a
--- /dev/null
+++ b/worker_versioning/workflows.py
@@ -0,0 +1,189 @@
+"""Workflow definitions for the worker versioning sample."""
+
+from datetime import timedelta
+
+from temporalio import common, workflow
+
+with workflow.unsafe.imports_passed_through():
+ from worker_versioning.activities import (
+ IncompatibleActivityInput,
+ some_activity,
+ some_incompatible_activity,
+ )
+
+
+@workflow.defn(
+ name="AutoUpgrading", versioning_behavior=common.VersioningBehavior.AUTO_UPGRADE
+)
+class AutoUpgradingWorkflowV1:
+ """AutoUpgradingWorkflowV1 will automatically move to the latest worker version. We'll be making
+ changes to it, which must be replay safe.
+
+ Note that generally you won't want or need to include a version number in your workflow name if
+ you're using the worker versioning feature. This sample does it to illustrate changes to the
+ same code over time - but really what we're demonstrating here is the evolution of what would
+ have been one workflow definition.
+ """
+
+ def __init__(self) -> None:
+ self.signals: list[str] = []
+
+ @workflow.run
+ async def run(self) -> None:
+ workflow.logger.info(
+ "Changing workflow v1 started.", extra={"StartTime": workflow.now()}
+ )
+
+ # This workflow will listen for signals from our starter, and upon each signal either run
+ # an activity, or conclude execution.
+ while True:
+ await workflow.wait_condition(lambda: len(self.signals) > 0)
+ signal = self.signals.pop(0)
+
+ if signal == "do-activity":
+ workflow.logger.info("Changing workflow v1 running activity")
+ await workflow.execute_activity(
+ some_activity, "v1", start_to_close_timeout=timedelta(seconds=10)
+ )
+ else:
+ workflow.logger.info("Concluding workflow v1")
+ return
+
+ @workflow.signal
+ async def do_next_signal(self, signal: str) -> None:
+ """Signal to perform next action."""
+ self.signals.append(signal)
+
+
+@workflow.defn(
+ name="AutoUpgrading", versioning_behavior=common.VersioningBehavior.AUTO_UPGRADE
+)
+class AutoUpgradingWorkflowV1b:
+ """AutoUpgradingWorkflowV1b represents us having made *compatible* changes to
+ AutoUpgradingWorkflowV1.
+
+ The compatible changes we've made are:
+ - Altering the log lines
+ - Using the workflow.patched API to properly introduce branching behavior while maintaining
+ compatibility
+ """
+
+ def __init__(self) -> None:
+ self.signals: list[str] = []
+
+ @workflow.run
+ async def run(self) -> None:
+ workflow.logger.info(
+ "Changing workflow v1b started.", extra={"StartTime": workflow.now()}
+ )
+
+ # This workflow will listen for signals from our starter, and upon each signal either run
+ # an activity, or conclude execution.
+ while True:
+ await workflow.wait_condition(lambda: len(self.signals) > 0)
+ signal = self.signals.pop(0)
+
+ if signal == "do-activity":
+ workflow.logger.info("Changing workflow v1b running activity")
+ if workflow.patched("DifferentActivity"):
+ await workflow.execute_activity(
+ some_incompatible_activity,
+ IncompatibleActivityInput(called_by="v1b", more_data="hello!"),
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+ else:
+ # Note it is a valid compatible change to alter the input to an activity.
+ # However, because we're using the patched API, this branch will never be
+ # taken.
+ await workflow.execute_activity(
+ some_activity,
+ "v1b",
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+ else:
+ workflow.logger.info("Concluding workflow v1b")
+ break
+
+ @workflow.signal
+ async def do_next_signal(self, signal: str) -> None:
+ """Signal to perform next action."""
+ self.signals.append(signal)
+
+
+@workflow.defn(name="Pinned", versioning_behavior=common.VersioningBehavior.PINNED)
+class PinnedWorkflowV1:
+ """PinnedWorkflowV1 demonstrates a workflow that likely has a short lifetime, and we want to always
+ stay pinned to the same version it began on.
+
+ Note that generally you won't want or need to include a version number in your workflow name if
+ you're using the worker versioning feature. This sample does it to illustrate changes to the
+ same code over time - but really what we're demonstrating here is the evolution of what would
+ have been one workflow definition.
+ """
+
+ def __init__(self) -> None:
+ self.signals: list[str] = []
+
+ @workflow.run
+ async def run(self) -> None:
+ workflow.logger.info(
+ "Pinned Workflow v1 started.", extra={"StartTime": workflow.now()}
+ )
+
+ while True:
+ await workflow.wait_condition(lambda: len(self.signals) > 0)
+ signal = self.signals.pop(0)
+ if signal == "conclude":
+ break
+
+ await workflow.execute_activity(
+ some_activity,
+ "Pinned-v1",
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+
+ @workflow.signal
+ async def do_next_signal(self, signal: str) -> None:
+ """Signal to perform next action."""
+ self.signals.append(signal)
+
+
+@workflow.defn(name="Pinned", versioning_behavior=common.VersioningBehavior.PINNED)
+class PinnedWorkflowV2:
+ """PinnedWorkflowV2 has changes that would make it incompatible with v1, and aren't protected by
+ a patch.
+ """
+
+ def __init__(self) -> None:
+ self.signals: list[str] = []
+
+ @workflow.run
+ async def run(self) -> None:
+ workflow.logger.info(
+ "Pinned Workflow v2 started.", extra={"StartTime": workflow.now()}
+ )
+
+ # Here we call an activity where we didn't before, which is an incompatible change.
+ await workflow.execute_activity(
+ some_activity,
+ "Pinned-v2",
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+
+ while True:
+ await workflow.wait_condition(lambda: len(self.signals) > 0)
+ signal = self.signals.pop(0)
+ if signal == "conclude":
+ break
+
+ # We've also changed the activity type here, another incompatible change
+ await workflow.execute_activity(
+ some_incompatible_activity,
+ IncompatibleActivityInput(called_by="Pinned-v2", more_data="hi"),
+ start_to_close_timeout=timedelta(seconds=10),
+ )
+
+ @workflow.signal
+ async def do_next_signal(self, signal: str) -> None:
+ """Signal to perform next action."""
+ self.signals.append(signal)