diff --git a/docs/references.md b/docs/references.md index e264fc9..09c6be7 100644 --- a/docs/references.md +++ b/docs/references.md @@ -65,3 +65,13 @@ - [How To Install libportaudio2 on Ubuntu 22.04](https://www.installati.one/install-libportaudio2-ubuntu-22-04/): `sudo apt-get -y install libportaudio2` - [python-sounddevice](https://github.com/spatialaudio/python-sounddevice) - [python-soundfile](https://github.com/bastibe/python-soundfile) + +### Realtime API + +- [August 2025 / Realtime API audio model GA](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/whats-new#realtime-api-audio-model-ga) +- [Global Standard model availability](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/concepts/models?tabs=global-standard%2Cstandard-chat-completions#global-standard-model-availability) +- [specification/cognitiveservices/data-plane/AzureOpenAI/inference/preview/2025-04-01-preview/inference.json](https://github.com/Azure/azure-rest-api-specs/blob/main/specification/cognitiveservices/data-plane/AzureOpenAI/inference/preview/2025-04-01-preview/inference.json) +- [Realtime API with WebSocket](https://platform.openai.com/docs/guides/realtime-websocket) +- [GPT-4o Realtime API for speech and audio](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/realtime-audio-quickstart?tabs=keyless%2Clinux&pivots=programming-language-python) +- [OpenAI Python API library > examples/realtime](https://github.com/openai/openai-python/tree/main/examples/realtime) +- [How to use the GPT-4o Realtime API via WebRTC](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/realtime-audio-webrtc) diff --git a/pyproject.toml b/pyproject.toml index 57f3762..fdcbf38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "elasticsearch>=9.1.0", "fastapi[standard]>=0.116.1", "httpx>=0.28.1", + "jinja2>=3.1.2", "langchain-azure-ai>=0.1.4", "langchain-community>=0.3.27", "langchain-mcp-adapters>=0.1.9", @@ -20,7 +21,7 @@ dependencies = [ "langchain-text-splitters>=0.3.9", "langgraph>=0.6.2", "langgraph-supervisor>=0.0.29", - "openai>=1.98.0", + "openai[realtime]>=1.98.0", "opentelemetry-api>=1.36.0", "opentelemetry-exporter-otlp>=1.36.0", "opentelemetry-sdk>=1.36.0", diff --git a/scripts/realtime_operator.py b/scripts/realtime_operator.py new file mode 100644 index 0000000..360972b --- /dev/null +++ b/scripts/realtime_operator.py @@ -0,0 +1,185 @@ +import asyncio +import http.server +import json +import logging +import os +import socketserver +import tempfile +import webbrowser +from pathlib import Path + +# New imports for template rendering and serving +import jinja2 +import typer +from azure.identity.aio import DefaultAzureCredential, get_bearer_token_provider +from dotenv import load_dotenv +from openai import AsyncAzureOpenAI + +from template_langgraph.llms.azure_openais import Settings +from template_langgraph.loggers import get_logger + +# Initialize the Typer application +app = typer.Typer( + add_completion=False, + help="Realtime API operator CLI", +) + +# Set up logging +logger = get_logger(__name__) + + +async def chat_impl() -> None: + """ + When prompted for user input, type a message and hit enter to send it to the model. + Enter "q" to quit the conversation. + """ + credential = DefaultAzureCredential() + token_provider = get_bearer_token_provider(credential, "https://cognitiveservices.azure.com/.default") + settings = Settings() + client = AsyncAzureOpenAI( + azure_endpoint=settings.azure_openai_endpoint, + azure_ad_token_provider=token_provider, + api_version=settings.azure_openai_api_version, + ) + async with client.realtime.connect( + model="gpt-realtime", # name of your deployment + ) as connection: + await connection.session.update( + session={ + "output_modalities": [ + "text", + "audio", + ], + "model": "gpt-realtime", + "type": "realtime", + } + ) + while True: + user_input = input("Enter a message: ") + if user_input == "q": + break + + await connection.conversation.item.create( + item={ + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": user_input, + }, + ], + } + ) + await connection.response.create() + async for event in connection: + if event.type == "response.audio_transcript.delta": + print(event.delta, end="", flush=True) + elif event.type == "response.done": + print() + break + else: + logger.debug(f"event.type: {event.type}") + # logger.debug(f"event: {event.model_dump_json(indent=2)}") + + await credential.close() + + +@app.command() +def chat( + verbose: bool = typer.Option( + False, + "--verbose", + "-v", + help="Enable verbose output", + ), +): + # Set up logging + if verbose: + logger.setLevel(logging.DEBUG) + + asyncio.run(chat_impl()) + + +@app.command() +def webrtc( + template: str = typer.Option( + "scripts/realtime_webrtc.html", "--template", "-t", help="Path to the HTML Jinja2 template" + ), + host: str = typer.Option("0.0.0.0", "--host", "-h"), + port: int = typer.Option(8080, "--port", "-p"), + web_rtc_url: str = typer.Option( + "https://eastus2.realtimeapi-preview.ai.azure.com/v1/realtimertc", "--webrtc-url", help="WebRTC endpoint URL" + ), + sessions_url: str = typer.Option( + "https://YourAzureOpenAIResourceName.openai.azure.com/openai/realtimeapi/sessions?api-version=2025-04-01-preview", + "--sessions-url", + help="Sessions API URL", + ), + deployment: str = typer.Option("gpt-realtime", "--deployment", help="Deployment name"), + voice: str = typer.Option("verse", "--voice", help="Voice name"), + instructions: str = typer.Option( + "You are a helpful AI assistant responding in natural, engaging language.", + "--instructions", + "-i", + help="Initial assistant instructions for the realtime session", + ), +): + """ + Render the realtime_webrtc HTML template with provided parameters and serve it as a static site. + + The template is a Jinja2 template and will receive the following variables: + - WEBRTC_URL, SESSIONS_URL, API_KEY, DEPLOYMENT, VOICE, INSTRUCTIONS + """ + settings = Settings() + api_key = settings.azure_openai_api_key + if not api_key: + typer.secho( + "Warning: no API key provided; the rendered page will contain an empty API key.", fg=typer.colors.YELLOW + ) + + tpl_path = Path(template) + if not tpl_path.exists(): + typer.secho(f"Template not found: {tpl_path}", fg=typer.colors.RED) + raise typer.Exit(code=1) + + tpl_text = tpl_path.read_text(encoding="utf-8") + + # Use json.dumps to safely embed JS string literals in the template + rendered = jinja2.Template(tpl_text).render( + WEBRTC_URL=json.dumps(web_rtc_url), + SESSIONS_URL=json.dumps(sessions_url), + API_KEY=json.dumps(api_key), + DEPLOYMENT=json.dumps(deployment), + VOICE=json.dumps(voice), + INSTRUCTIONS=json.dumps(instructions), + ) + + tempdir = tempfile.TemporaryDirectory() + out_path = Path(tempdir.name) / "index.html" + out_path.write_text(rendered, encoding="utf-8") + + # Serve the temporary directory with the rendered HTML + os.chdir(tempdir.name) + handler = http.server.SimpleHTTPRequestHandler + with socketserver.TCPServer((host, port), handler) as httpd: + url = f"http://{host}:{port}/" + typer.secho(f"Serving rendered template at: {url}", fg=typer.colors.GREEN) + try: + webbrowser.open(url) + except Exception: + pass + try: + httpd.serve_forever() + except KeyboardInterrupt: + typer.secho("Shutting down server...", fg=typer.colors.YELLOW) + finally: + tempdir.cleanup() + + +if __name__ == "__main__": + load_dotenv( + override=True, + verbose=True, + ) + app() diff --git a/scripts/realtime_webrtc.html b/scripts/realtime_webrtc.html new file mode 100644 index 0000000..1602d03 --- /dev/null +++ b/scripts/realtime_webrtc.html @@ -0,0 +1,180 @@ + + +
+ + ++ WARNING: Don't use this code sample in production with the API key + hardcoded. Use a protected backend service to call the sessions API and + generate the ephemeral key. Then return the ephemeral key to the client. +
+ + + + + + + + diff --git a/uv.lock b/uv.lock index b881efc..079b263 100644 --- a/uv.lock +++ b/uv.lock @@ -1895,7 +1895,7 @@ wheels = [ [[package]] name = "jupyterlab" -version = "4.4.6" +version = "4.4.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, @@ -1913,9 +1913,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/5c/14f0852233d60d30bf0f22a817d6c20ac555d73526cc915274f97c07a2b9/jupyterlab-4.4.6.tar.gz", hash = "sha256:e0b720ff5392846bdbc01745f32f29f4d001c071a4bff94d8b516ba89b5a4157", size = 23040936, upload-time = "2025-08-15T11:44:15.915Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/07/b3beaeb5722d4a55e345a38884c67baebd9cec2269c5309ce494485a5858/jupyterlab-4.4.7.tar.gz", hash = "sha256:8c8e225492f4513ebde9bbbc00a05b651ab9a1f5b0013015d96fabf671c37188", size = 22965570, upload-time = "2025-09-03T13:26:40.461Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/38/6182d63f39428821e705e86fba61704fc69769a24ca5a9578c2c04986c9a/jupyterlab-4.4.6-py3-none-any.whl", hash = "sha256:e877e930f46dde2e3ee9da36a935c6cd4fdb15aa7440519d0fde696f9fadb833", size = 12268564, upload-time = "2025-08-15T11:44:11.42Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/44f35124896dd5c73b26705c25bb8af2089895b32f057a1e4a3488847333/jupyterlab-4.4.7-py3-none-any.whl", hash = "sha256:808bae6136b507a4d18f04254218bfe71ed8ba399a36ef3280d5f259e69abf80", size = 12291583, upload-time = "2025-09-03T13:26:35.862Z" }, ] [[package]] @@ -2098,7 +2098,7 @@ wheels = [ [[package]] name = "langgraph-api" -version = "0.4.9" +version = "0.4.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloudpickle", marker = "python_full_version >= '3.11'" }, @@ -2121,9 +2121,9 @@ dependencies = [ { name = "uvicorn", marker = "python_full_version >= '3.11'" }, { name = "watchfiles", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/91/671306db7502e50b148d695574f64fc762e04c0c790a93ddec1ed66af61e/langgraph_api-0.4.9.tar.gz", hash = "sha256:86203824c8aa6f81387422d261678a67e378109a1d8451e3d1fd20046d2055ae", size = 265672, upload-time = "2025-09-02T23:45:03.784Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/ad/3ebb6ceb3e5bd81383f8e5656d4ca97a81a96ad96040c123610b61f06d58/langgraph_api-0.4.11.tar.gz", hash = "sha256:3baada32aff2c136c271eeee0cd0bcb2335f94917242e82eab08937081ce8793", size = 265821, upload-time = "2025-09-04T01:38:43.876Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/8c/dfb010eabadf4c54fe77c0c19252092b8ca1e0ccacaad532b1f2d8fd6fc5/langgraph_api-0.4.9-py3-none-any.whl", hash = "sha256:b0dc66b8045025dbd0af2890c56fcc6ed3441f939daa45d2d9f29e317bd8e148", size = 216240, upload-time = "2025-09-02T23:45:02.232Z" }, + { url = "https://files.pythonhosted.org/packages/76/4c/f0ca287feea995556aaf292a6a9cd2718cdff8155c747b526d10e499b256/langgraph_api-0.4.11-py3-none-any.whl", hash = "sha256:afcb57c4b4563c3bae59f559804c24ab208a552ba4eae8e0172aa9cddb90fd5b", size = 216370, upload-time = "2025-09-04T01:38:42.152Z" }, ] [[package]] @@ -2174,7 +2174,7 @@ wheels = [ [[package]] name = "langgraph-runtime-inmem" -version = "0.10.0" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blockbuster", marker = "python_full_version >= '3.11'" }, @@ -2184,22 +2184,22 @@ dependencies = [ { name = "starlette", marker = "python_full_version >= '3.11'" }, { name = "structlog", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/ae/6a37fb49f0fc81bcad01daabd1943e2fbaf28cbdf1b10ab6f2caaf1f1a43/langgraph_runtime_inmem-0.10.0.tar.gz", hash = "sha256:0d36f5b4bde7984b59dff4dae9b4971c94b088ef71806fb2ba8dc37737660723", size = 82362, upload-time = "2025-09-02T23:35:07.045Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/b5/321cbba8925142db22f6524d45ea5642da76acbcfc2316a75e5d31cf94af/langgraph_runtime_inmem-0.11.0.tar.gz", hash = "sha256:cb629cbf2cf4c7f158afbf013ac4d026d96650b86845bc33c8f694012c147364", size = 82364, upload-time = "2025-09-04T01:28:35.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/37/38b46b8a7a0669d596317ef43451297363de5a062f30c610a8e43f0b941a/langgraph_runtime_inmem-0.10.0-py3-none-any.whl", hash = "sha256:22fb43e5a8fb80d2ec3ce5e62e4aa26756f71d2102b5ac08e7397df32a4bb5d8", size = 34436, upload-time = "2025-09-02T23:35:05.887Z" }, + { url = "https://files.pythonhosted.org/packages/30/8d/fbe4913a4f97bf5bf335aae10ad8551e1b4f4f11e664be128e67ae0f3701/langgraph_runtime_inmem-0.11.0-py3-none-any.whl", hash = "sha256:5b6ff9cff23fe61c287e000cd001445bd0ad7f8710f129aae082223d17123cd3", size = 34434, upload-time = "2025-09-04T01:28:34.592Z" }, ] [[package]] name = "langgraph-sdk" -version = "0.2.5" +version = "0.2.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "orjson" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/72/37943b367f480b4761c052e86b0c380ecb928cdccf1d5c85d569e036c06f/langgraph_sdk-0.2.5.tar.gz", hash = "sha256:b28aa0f485811388465ac5d2a19d728ab84b59a8900cdfcf0f4e8177802cbf62", size = 79816, upload-time = "2025-09-03T00:51:14.285Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/35/a1caf4fdb725adec30f1e9562f218524a92d8b675deb97be653687f086ee/langgraph_sdk-0.2.6.tar.gz", hash = "sha256:7db27cd86d1231fa614823ff416fcd2541b5565ad78ae950f31ae96d7af7c519", size = 80346, upload-time = "2025-09-04T01:51:11.262Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/2c/9e5a0259601253f89f8b50095a6a2541ff6beb9041c92e64bd8e05e77c29/langgraph_sdk-0.2.5-py3-none-any.whl", hash = "sha256:a837a7a3c5e16ba55a3952037f9d8e247d3e001e9a0e3b07aacef55861e5dc58", size = 54052, upload-time = "2025-09-03T00:51:13.057Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d2/c5fac919601b27a0af5df0bde46e7f1361d5e04505e404b75bed45d21fc8/langgraph_sdk-0.2.6-py3-none-any.whl", hash = "sha256:477216b573b8177bbd849f4c754782a81279fbbd88bfadfeda44422d14b18b08", size = 54565, upload-time = "2025-09-04T01:51:10.044Z" }, ] [[package]] @@ -2877,7 +2877,7 @@ wheels = [ [[package]] name = "openai" -version = "1.104.2" +version = "1.105.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2889,9 +2889,14 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/dc/965b3528ed0435b717acca45e2541d94bd827c0520ce172366323c9edcab/openai-1.104.2.tar.gz", hash = "sha256:9b582ead9dd208753f89dae8e36b6548c6ada076e87ba3db36630e29239661ab", size = 557160, upload-time = "2025-09-02T21:42:31.054Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/a9/c8c2dea8066a8f3079f69c242f7d0d75aaad4c4c3431da5b0df22a24e75d/openai-1.105.0.tar.gz", hash = "sha256:a68a47adce0506d34def22dd78a42cbb6cfecae1cf6a5fe37f38776d32bbb514", size = 557265, upload-time = "2025-09-03T14:14:08.586Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/9c/d0b56971e5584aea338bb00d3ca96a7f6694dff77006581b21cd773497ce/openai-1.104.2-py3-none-any.whl", hash = "sha256:0148951da12ea651f890ef38f8adef75b78c053dba37ea2bdba857c8945860d4", size = 928160, upload-time = "2025-09-02T21:42:28.678Z" }, + { url = "https://files.pythonhosted.org/packages/51/01/186845829d3a3609bb5b474067959076244dd62540d3e336797319b13924/openai-1.105.0-py3-none-any.whl", hash = "sha256:3ad7635132b0705769ccae31ca7319f59ec0c7d09e94e5e713ce2d130e5b021f", size = 928203, upload-time = "2025-09-03T14:14:06.842Z" }, +] + +[package.optional-dependencies] +realtime = [ + { name = "websockets" }, ] [[package]] @@ -4780,6 +4785,7 @@ dependencies = [ { name = "elasticsearch" }, { name = "fastapi", extra = ["standard"] }, { name = "httpx" }, + { name = "jinja2" }, { name = "langchain-azure-ai" }, { name = "langchain-community" }, { name = "langchain-mcp-adapters" }, @@ -4788,7 +4794,7 @@ dependencies = [ { name = "langchain-text-splitters" }, { name = "langgraph" }, { name = "langgraph-supervisor" }, - { name = "openai" }, + { name = "openai", extra = ["realtime"] }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp" }, { name = "opentelemetry-sdk" }, @@ -4829,6 +4835,7 @@ requires-dist = [ { name = "elasticsearch", specifier = ">=9.1.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.116.1" }, { name = "httpx", specifier = ">=0.28.1" }, + { name = "jinja2", specifier = ">=3.1.2" }, { name = "langchain-azure-ai", specifier = ">=0.1.4" }, { name = "langchain-community", specifier = ">=0.3.27" }, { name = "langchain-mcp-adapters", specifier = ">=0.1.9" }, @@ -4837,7 +4844,7 @@ requires-dist = [ { name = "langchain-text-splitters", specifier = ">=0.3.9" }, { name = "langgraph", specifier = ">=0.6.2" }, { name = "langgraph-supervisor", specifier = ">=0.0.29" }, - { name = "openai", specifier = ">=1.98.0" }, + { name = "openai", extras = ["realtime"], specifier = ">=1.98.0" }, { name = "opentelemetry-api", specifier = ">=1.36.0" }, { name = "opentelemetry-exporter-otlp", specifier = ">=1.36.0" }, { name = "opentelemetry-sdk", specifier = ">=1.36.0" }, @@ -5039,27 +5046,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.1a19" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/04/281c1a3c9c53dae5826b9d01a3412de653e3caf1ca50ce1265da66e06d73/ty-0.0.1a19.tar.gz", hash = "sha256:894f6a13a43989c8ef891ae079b3b60a0c0eae00244abbfbbe498a3840a235ac", size = 4098412, upload-time = "2025-08-19T13:29:58.559Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/65/a61cfcc7248b0257a3110bf98d3d910a4729c1063abdbfdcd1cad9012323/ty-0.0.1a19-py3-none-linux_armv6l.whl", hash = "sha256:e0e7762f040f4bab1b37c57cb1b43cc3bc5afb703fa5d916dfcafa2ef885190e", size = 8143744, upload-time = "2025-08-19T13:29:13.88Z" }, - { url = "https://files.pythonhosted.org/packages/02/d9/232afef97d9afa2274d23a4c49a3ad690282ca9696e1b6bbb6e4e9a1b072/ty-0.0.1a19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cd0a67ac875f49f34d9a0b42dcabf4724194558a5dd36867209d5695c67768f7", size = 8305799, upload-time = "2025-08-19T13:29:17.322Z" }, - { url = "https://files.pythonhosted.org/packages/20/14/099d268da7a9cccc6ba38dfc124f6742a1d669bc91f2c61a3465672b4f71/ty-0.0.1a19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ff8b1c0b85137333c39eccd96c42603af8ba7234d6e2ed0877f66a4a26750dd4", size = 7901431, upload-time = "2025-08-19T13:29:21.635Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cd/3f1ca6e1d7f77cc4d08910a3fc4826313c031c0aae72286ae859e737670c/ty-0.0.1a19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fef34a29f4b97d78aa30e60adbbb12137cf52b8b2b0f1a408dd0feb0466908a", size = 8051501, upload-time = "2025-08-19T13:29:23.741Z" }, - { url = "https://files.pythonhosted.org/packages/47/72/ddbec39f48ce3f5f6a3fa1f905c8fff2873e59d2030f738814032bd783e3/ty-0.0.1a19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b0f219cb43c0c50fc1091f8ebd5548d3ef31ee57866517b9521d5174978af9fd", size = 7981234, upload-time = "2025-08-19T13:29:25.839Z" }, - { url = "https://files.pythonhosted.org/packages/f2/0f/58e76b8d4634df066c790d362e8e73b25852279cd6f817f099b42a555a66/ty-0.0.1a19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22abb6c1f14c65c1a2fafd38e25dd3c87994b3ab88cb0b323235b51dbad082d9", size = 8916394, upload-time = "2025-08-19T13:29:27.932Z" }, - { url = "https://files.pythonhosted.org/packages/70/30/01bfd93ccde11540b503e2539e55f6a1fc6e12433a229191e248946eb753/ty-0.0.1a19-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5b49225c349a3866e38dd297cb023a92d084aec0e895ed30ca124704bff600e6", size = 9412024, upload-time = "2025-08-19T13:29:30.942Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a2/2216d752f5f22c5c0995f9b13f18337301220f2a7d952c972b33e6a63583/ty-0.0.1a19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:88f41728b3b07402e0861e3c34412ca963268e55f6ab1690208f25d37cb9d63c", size = 9032657, upload-time = "2025-08-19T13:29:33.933Z" }, - { url = "https://files.pythonhosted.org/packages/24/c7/e6650b0569be1b69a03869503d07420c9fb3e90c9109b09726c44366ce63/ty-0.0.1a19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33814a1197ec3e930fcfba6fb80969fe7353957087b42b88059f27a173f7510b", size = 8812775, upload-time = "2025-08-19T13:29:36.505Z" }, - { url = "https://files.pythonhosted.org/packages/35/c6/b8a20e06b97fe8203059d56d8f91cec4f9633e7ba65f413d80f16aa0be04/ty-0.0.1a19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d71b7f2b674a287258f628acafeecd87691b169522945ff6192cd8a69af15857", size = 8631417, upload-time = "2025-08-19T13:29:38.837Z" }, - { url = "https://files.pythonhosted.org/packages/be/99/821ca1581dcf3d58ffb7bbe1cde7e1644dbdf53db34603a16a459a0b302c/ty-0.0.1a19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a7f8ef9ac4c38e8651c18c7380649c5a3fa9adb1a6012c721c11f4bbdc0ce24", size = 7928900, upload-time = "2025-08-19T13:29:41.08Z" }, - { url = "https://files.pythonhosted.org/packages/08/cb/59f74a0522e57565fef99e2287b2bc803ee47ff7dac250af26960636939f/ty-0.0.1a19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:60f40e72f0fbf4e54aa83d9a6cb1959f551f83de73af96abbb94711c1546bd60", size = 8003310, upload-time = "2025-08-19T13:29:43.165Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b3/1209b9acb5af00a2755114042e48fb0f71decc20d9d77a987bf5b3d1a102/ty-0.0.1a19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:64971e4d3e3f83dc79deb606cc438255146cab1ab74f783f7507f49f9346d89d", size = 8496463, upload-time = "2025-08-19T13:29:46.136Z" }, - { url = "https://files.pythonhosted.org/packages/a2/d6/a4b6ba552d347a08196d83a4d60cb23460404a053dd3596e23a922bce544/ty-0.0.1a19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9aadbff487e2e1486e83543b4f4c2165557f17432369f419be9ba48dc47625ca", size = 8700633, upload-time = "2025-08-19T13:29:49.351Z" }, - { url = "https://files.pythonhosted.org/packages/96/c5/258f318d68b95685c8d98fb654a38882c9d01ce5d9426bed06124f690f04/ty-0.0.1a19-py3-none-win32.whl", hash = "sha256:00b75b446357ee22bcdeb837cb019dc3bc1dc5e5013ff0f46a22dfe6ce498fe2", size = 7811441, upload-time = "2025-08-19T13:29:52.077Z" }, - { url = "https://files.pythonhosted.org/packages/fb/bb/039227eee3c0c0cddc25f45031eea0f7f10440713f12d333f2f29cf8e934/ty-0.0.1a19-py3-none-win_amd64.whl", hash = "sha256:aaef76b2f44f6379c47adfe58286f0c56041cb2e374fd8462ae8368788634469", size = 8441186, upload-time = "2025-08-19T13:29:54.53Z" }, - { url = "https://files.pythonhosted.org/packages/74/5f/bceb29009670ae6f759340f9cb434121bc5ed84ad0f07bdc6179eaaa3204/ty-0.0.1a19-py3-none-win_arm64.whl", hash = "sha256:893755bb35f30653deb28865707e3b16907375c830546def2741f6ff9a764710", size = 8000810, upload-time = "2025-08-19T13:29:56.796Z" }, +version = "0.0.1a20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/82/a5e3b4bc5280ec49c4b0b43d0ff727d58c7df128752c9c6f97ad0b5f575f/ty-0.0.1a20.tar.gz", hash = "sha256:933b65a152f277aa0e23ba9027e5df2c2cc09e18293e87f2a918658634db5f15", size = 4194773, upload-time = "2025-09-03T12:35:46.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/c8/f7d39392043d5c04936f6cad90e50eb661965ed092ca4bfc01db917d7b8a/ty-0.0.1a20-py3-none-linux_armv6l.whl", hash = "sha256:f73a7aca1f0d38af4d6999b375eb00553f3bfcba102ae976756cc142e14f3450", size = 8443599, upload-time = "2025-09-03T12:35:04.289Z" }, + { url = "https://files.pythonhosted.org/packages/1e/57/5aec78f9b8a677b7439ccded7d66c3361e61247e0f6b14e659b00dd01008/ty-0.0.1a20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cad12c857ea4b97bf61e02f6796e13061ccca5e41f054cbd657862d80aa43bae", size = 8618102, upload-time = "2025-09-03T12:35:07.448Z" }, + { url = "https://files.pythonhosted.org/packages/15/20/50c9107d93cdb55676473d9dc4e2339af6af606660c9428d3b86a1b2a476/ty-0.0.1a20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f153b65c7fcb6b8b59547ddb6353761b3e8d8bb6f0edd15e3e3ac14405949f7a", size = 8192167, upload-time = "2025-09-03T12:35:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/85/28/018b2f330109cee19e81c5ca9df3dc29f06c5778440eb9af05d4550c4302/ty-0.0.1a20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8c4336987a6a781d4392a9fd7b3a39edb7e4f3dd4f860e03f46c932b52aefa2", size = 8349256, upload-time = "2025-09-03T12:35:11.76Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c9/2f8797a05587158f52b142278796ffd72c893bc5ad41840fce5aeb65c6f2/ty-0.0.1a20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ff75cd4c744d09914e8c9db8d99e02f82c9379ad56b0a3fc4c5c9c923cfa84e", size = 8271214, upload-time = "2025-09-03T12:35:13.741Z" }, + { url = "https://files.pythonhosted.org/packages/30/d4/2cac5e5eb9ee51941358cb3139aadadb59520cfaec94e4fcd2b166969748/ty-0.0.1a20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e26437772be7f7808868701f2bf9e14e706a6ec4c7d02dbd377ff94d7ba60c11", size = 9264939, upload-time = "2025-09-03T12:35:16.896Z" }, + { url = "https://files.pythonhosted.org/packages/93/96/a6f2b54e484b2c6a5488f217882237dbdf10f0fdbdb6cd31333d57afe494/ty-0.0.1a20-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:83a7ee12465841619b5eb3ca962ffc7d576bb1c1ac812638681aee241acbfbbe", size = 9743137, upload-time = "2025-09-03T12:35:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/6e/67/95b40dcbec3d222f3af5fe5dd1ce066d42f8a25a2f70d5724490457048e7/ty-0.0.1a20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:726d0738be4459ac7ffae312ba96c5f486d6cbc082723f322555d7cba9397871", size = 9368153, upload-time = "2025-09-03T12:35:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/2c/24/689fa4c4270b9ef9a53dc2b1d6ffade259ba2c4127e451f0629e130ea46a/ty-0.0.1a20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b481f26513f38543df514189fb16744690bcba8d23afee95a01927d93b46e36", size = 9099637, upload-time = "2025-09-03T12:35:24.94Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5b/913011cbf3ea4030097fb3c4ce751856114c9e1a5e1075561a4c5242af9b/ty-0.0.1a20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7abbe3c02218c12228b1d7c5f98c57240029cc3bcb15b6997b707c19be3908c1", size = 8952000, upload-time = "2025-09-03T12:35:27.288Z" }, + { url = "https://files.pythonhosted.org/packages/df/f9/f5ba2ae455b20c5bb003f9940ef8142a8c4ed9e27de16e8f7472013609db/ty-0.0.1a20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fff51c75ee3f7cc6d7722f2f15789ef8ffe6fd2af70e7269ac785763c906688e", size = 8217938, upload-time = "2025-09-03T12:35:29.54Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/17002cf9032f0981cdb8c898d02422c095c30eefd69ca62a8b705d15bd0f/ty-0.0.1a20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b4124ab75e0e6f09fe7bc9df4a77ee43c5e0ef7e61b0c149d7c089d971437cbd", size = 8292369, upload-time = "2025-09-03T12:35:31.748Z" }, + { url = "https://files.pythonhosted.org/packages/28/d6/0879b1fb66afe1d01d45c7658f3849aa641ac4ea10679404094f3b40053e/ty-0.0.1a20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8a138fa4f74e6ed34e9fd14652d132409700c7ff57682c2fed656109ebfba42f", size = 8811973, upload-time = "2025-09-03T12:35:33.997Z" }, + { url = "https://files.pythonhosted.org/packages/60/1e/70bf0348cfe8ba5f7532983f53c508c293ddf5fa9f942ed79a3c4d576df3/ty-0.0.1a20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8eff8871d6b88d150e2a67beba2c57048f20c090c219f38ed02eebaada04c124", size = 9010990, upload-time = "2025-09-03T12:35:36.766Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ca/03d85c7650359247b1ca3f38a0d869a608ef540450151920e7014ed58292/ty-0.0.1a20-py3-none-win32.whl", hash = "sha256:3c2ace3a22fab4bd79f84c74e3dab26e798bfba7006bea4008d6321c1bd6efc6", size = 8100746, upload-time = "2025-09-03T12:35:40.007Z" }, + { url = "https://files.pythonhosted.org/packages/94/53/7a1937b8c7a66d0c8ed7493de49ed454a850396fe137d2ae12ed247e0b2f/ty-0.0.1a20-py3-none-win_amd64.whl", hash = "sha256:f41e77ff118da3385915e13c3f366b3a2f823461de54abd2e0ca72b170ba0f19", size = 8748861, upload-time = "2025-09-03T12:35:42.175Z" }, + { url = "https://files.pythonhosted.org/packages/27/36/5a3a70c5d497d3332f9e63cabc9c6f13484783b832fecc393f4f1c0c4aa8/ty-0.0.1a20-py3-none-win_arm64.whl", hash = "sha256:d8ac1c5a14cda5fad1a8b53959d9a5d979fe16ce1cc2785ea8676fed143ac85f", size = 8269906, upload-time = "2025-09-03T12:35:45.045Z" }, ] [[package]]