├── .dockerignore ├── .gitignore ├── .python-version ├── Dockerfile ├── LICENSE ├── README.md ├── fly.toml ├── pipegate ├── __init__.py ├── auth.py ├── client.py ├── schemas.py └── server.py ├── pyproject.toml └── uv.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | # flyctl launch added from .gitignore 2 | # Python-generated files 3 | **/__pycache__ 4 | **/*.py[oc] 5 | **/build 6 | **/dist 7 | **/wheels 8 | **/*.egg-info 9 | 10 | # Virtual environments 11 | **/.venv 12 | 13 | # flyctl launch added from .mypy_cache/.gitignore 14 | # Automatically created by mypy 15 | .mypy_cache/**/* 16 | 17 | # flyctl launch added from .ruff_cache/.gitignore 18 | # Automatically created by ruff. 19 | .ruff_cache/**/* 20 | 21 | # flyctl launch added from .venv/.gitignore 22 | .venv/**/* 23 | fly.toml 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM python:3.12-slim-bookworm 3 | 4 | # Install UV package manager 5 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 6 | 7 | # Copy the project into the image 8 | ADD pipegate/ /app/pipegate 9 | ADD uv.lock /app 10 | ADD pyproject.toml /app 11 | 12 | # Sync the project into a new environment, using the frozen lockfile 13 | WORKDIR /app 14 | RUN uv sync --frozen 15 | 16 | ENV PATH="/app/.venv/bin:$PATH" 17 | 18 | # Workers must be one atm. 19 | CMD ["uvicorn", "pipegate.server:create_app", "--host", "0.0.0.0", "--port", "8000", "--factory", "--workers", "1"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 JeyBee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PipeGate 2 | 3 | PipeGate is a lightweight, self-hosted proxy built with FastAPI, designed as a "poor man's ngrok." It allows you to expose your local servers to the internet, providing a simple way to create tunnels from your local machine to the external world. 4 | 5 | ## Table of Contents 6 | 7 | - [Features](#features) 8 | - [Prerequisites](#prerequisites) 9 | - [Installation](#installation) 10 | - [Clone the Repository](#clone-the-repository) 11 | - [Install Dependencies](#install-dependencies) 12 | - [Alternatively, Install via pip](#alternatively-install-via-pip) 13 | - [Usage](#usage) 14 | - [Generating a JWT Bearer Token](#generating-a-jwt-bearer-token) 15 | - [Starting the Server](#starting-the-server) 16 | - [Starting the Client](#starting-the-client) 17 | - [Example Workflow](#example-workflow) 18 | - [Configuration](#configuration) 19 | - [Security Considerations](#security-considerations) 20 | - [Contributing](#contributing) 21 | - [License](#license) 22 | - [Acknowledgements](#acknowledgements) 23 | - [Contact](#contact) 24 | - [FAQ](#faq) 25 | 26 | ## Features 27 | 28 | - **Self-Hosted:** Deploy PipeGate on your own infrastructure, giving you control over your setup. 29 | - **Unique Connections:** Clients connect using unique UUIDs, ensuring each tunnel is distinct. 30 | - **Customizable:** Modify and extend PipeGate to fit your specific needs. 31 | - **Lightweight:** Minimal dependencies and straightforward setup make it easy to use. 32 | - **Educational:** A great tool for learning how tunneling services operate internally. 33 | 34 | ## Prerequisites 35 | 36 | Before you begin, ensure you have met the following requirements: 37 | 38 | - **Python 3.12+** 39 | - [Git](https://git-scm.com/) 40 | - [uv](https://github.com/astral-sh/uv) 41 | 42 | ## Installation 43 | 44 | ### Clone the Repository 45 | 46 | First, clone the PipeGate repository to your local machine: 47 | 48 | ```bash 49 | git clone https://github.com/janbjorge/pipegate.git 50 | cd pipegate 51 | ``` 52 | 53 | ### Install Dependencies 54 | 55 | Install the required dependencies using `uv`: 56 | 57 | ```bash 58 | uv sync 59 | ``` 60 | 61 | ### Alternatively, Install via pip 62 | 63 | You can also install PipeGate directly from GitHub using pip: 64 | 65 | ```bash 66 | pip install git+https://github.com/janbjorge/pipegate.git 67 | ``` 68 | 69 | ## Usage 70 | 71 | ### Generating a JWT Bearer Token 72 | 73 | PipeGate uses JWT (JSON Web Tokens) for authenticating client connections. To establish a secure tunnel, you need to generate a JWT bearer token that includes a unique connection ID. 74 | 75 | 1. **Generate the JWT Token:** 76 | 77 | Run the authentication helper script to generate a JWT bearer token and a corresponding connection ID. 78 | 79 | ```bash 80 | python -m pipegate.auth 81 | ``` 82 | 83 | **Output Example:** 84 | 85 | ``` 86 | Connection-id: 123e4567-e89b-12d3-a456-426614174000 87 | JWT Bearer: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... 88 | ``` 89 | 90 | - **Connection-id:** A unique UUID representing your tunnel connection. 91 | - **JWT Bearer:** The JWT token you will use to authenticate your requests to the PipeGate server. 92 | 93 | **Optional:** To customize the connection id, you can set the `PIPEGATE_CONNECTION_ID` environment variable when running the `auth` script. This may be useful if your PipeGate server is hosting multiple tunnels for well-known endpoints or APIs. 94 | 95 | ```bash 96 | PIPEGATE_CONNECTION_ID=my-api python -m pipegate.auth 97 | ``` 98 | 99 | **Output Example:** 100 | 101 | ``` 102 | Connection-id: my-api 103 | JWT Bearer: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... 104 | ``` 105 | 106 | 2. **Store the Credentials:** 107 | 108 | Keep the `Connection-id` and `JWT Bearer` token secure, as they are required to establish a connection between the server and client. 109 | 110 | ### Starting the Server 111 | 112 | Deploy the PipeGate server on your infrastructure. By default, the server runs on `http://0.0.0.0:8000`. 113 | 114 | 1. **Configure the Server:** 115 | 116 | Ensure that the server is configured to use the same JWT secret and algorithms as used when generating the JWT token. You can modify the `Settings` in your server configuration as needed, typically found in `server.py` or your configuration files. 117 | 118 | 2. **Run the Server:** 119 | 120 | ```bash 121 | python -m pipegate.server 122 | ``` 123 | 124 | **Optional:** To customize the host and port, modify the `uvicorn.run` parameters in `server.py` or set environment variables if implemented. 125 | 126 | ### Starting the Client 127 | 128 | Run the PipeGate client on your local machine to expose a local server. 129 | 130 | ```bash 131 | python -m pipegate.client 132 | ``` 133 | 134 | **Parameters:** 135 | 136 | - `TARGET_URL`: The local target (e.g., `http://127.0.0.1:9090`). 137 | - `SERVER_URL`: WebSocket URL of your PipeGate server, including the unique connection ID. 138 | 139 | **Example:** 140 | 141 | ```bash 142 | python -m pipegate.client http://127.0.0.1:9090 wss://yourserver.com/123e4567-e89b-12d3-a456-426614174000 143 | ``` 144 | 145 | ### Example Workflow 146 | 147 | 1. **Generate a JWT Bearer Token:** 148 | 149 | ```bash 150 | python -m pipegate.auth 151 | ``` 152 | 153 | *Sample Output:* 154 | 155 | ``` 156 | Connection-id: 123e4567-e89b-12d3-a456-426614174000 157 | JWT Bearer: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... 158 | ``` 159 | 160 | 2. **Start the Server:** 161 | 162 | Ensure your server is configured with the appropriate JWT settings, then run: 163 | 164 | ```bash 165 | python -m pipegate.server 166 | ``` 167 | 168 | 3. **Start the Client:** 169 | 170 | Use the generated `Connection-id` to start the client: 171 | 172 | ```bash 173 | python -m pipegate.client http://127.0.0.1:9090 wss://yourserver.com/123e4567-e89b-12d3-a456-426614174000 174 | ``` 175 | 176 | 4. **Expose Local Server:** 177 | 178 | Point your external webhooks or services to `https://yourserver.com/123e4567-e89b-12d3-a456-426614174000/path`, and PipeGate will forward the requests to your local server running on port `9090`. 179 | 180 | ## Configuration 181 | 182 | PipeGate is highly customizable. You can modify the server and client configurations to tailor the tool to your specific needs. Refer to the source code and documentation for detailed configuration options. 183 | 184 | **Possible Configuration Enhancements:** 185 | 186 | - **Authentication:** PipeGate uses JWT for authenticating client connections. Ensure that the JWT settings (`jwt_secret`, `jwt_algorithms`) in both server and client are consistent. 187 | - **Timeouts:** Adjust request and connection timeouts based on your requirements. 188 | - **Logging:** Configure logging levels and outputs to monitor activity. 189 | 190 | *Note: Future releases may include configuration files or environment variable support for easier customization.* 191 | 192 | ## Security Considerations 193 | 194 | **PipeGate** has minimal to no built-in security features beyond JWT authentication. It is essential to implement your own security measures to protect your infrastructure when using PipeGate. Consider the following: 195 | 196 | - **Authentication:** Ensure that only authorized clients can connect to your PipeGate server by using strong JWT secrets and managing token distribution securely. 197 | - **Network Security:** Utilize firewalls, VPNs, or other network security tools to restrict access to your PipeGate server. 198 | - **Input Validation:** Apply thorough validation and filtering of incoming requests to prevent malicious activities. 199 | - **Encryption:** Ensure that HTTPS is set up to encrypt data in transit, especially if transmitting sensitive information. 200 | - **Monitoring and Auditing:** Regularly monitor and audit your PipeGate setup to detect and respond to potential threats. 201 | - **Resource Limiting:** Implement rate limiting or throttling to prevent abuse and ensure fair usage of server resources. 202 | 203 | *Disclaimer: PipeGate is provided "as is" without any guarantees. Use it at your own risk.* 204 | 205 | ## Contributing 206 | 207 | Contributions are welcome! Whether you're fixing bugs, improving documentation, or adding new features, your help is appreciated. 208 | 209 | ### How to Contribute 210 | 211 | 1. **Fork the Repository:** Click the "Fork" button at the top right of the repository page. 212 | 213 | 2. **Clone Your Fork:** 214 | 215 | ```bash 216 | git clone https://github.com/janbjorge/pipegate.git 217 | cd pipegate 218 | ``` 219 | 220 | 3. **Create a New Branch:** 221 | 222 | ```bash 223 | git checkout -b feature/YourFeatureName 224 | ``` 225 | 226 | 4. **Make Your Changes:** Implement your feature or fix. 227 | 228 | 5. **Commit Your Changes:** 229 | 230 | ```bash 231 | git commit -m "Add your message here" 232 | ``` 233 | 234 | 6. **Push to Your Fork:** 235 | 236 | ```bash 237 | git push origin feature/YourFeatureName 238 | ``` 239 | 240 | 7. **Open a Pull Request:** Go to the original repository and create a pull request. 241 | 242 | ## License 243 | 244 | This project is licensed under the [MIT License](LICENSE). 245 | 246 | ## Acknowledgements 247 | 248 | - [FastAPI](https://fastapi.tiangolo.com/) 249 | - [HTTPX](https://www.python-httpx.org/) 250 | - [Typer](https://typer.tiangolo.com/) 251 | - Inspired by [ngrok](https://ngrok.com/) 252 | 253 | ## Contact 254 | 255 | For any questions or suggestions, feel free to open an issue. 256 | 257 | ## FAQ 258 | 259 | **Q:** How do I generate a unique connection ID? 260 | 261 | **A:** You can use Python's `uuid` module or any UUID generator to create a unique ID. Alternatively, use the provided authentication helper to generate a connection ID along with a JWT bearer token. 262 | 263 | **Q:** Can I run multiple clients with the same server? 264 | 265 | **A:** Yes, each client should use a unique connection ID and corresponding JWT bearer token to establish separate tunnels. 266 | 267 | **Q:** How do I renew my JWT bearer token? 268 | 269 | **A:** Generate a new JWT bearer token using the authentication helper script and update both the server and client configurations accordingly. 270 | 271 | **Q:** What happens if my JWT token expires? 272 | 273 | **A:** If the JWT token expires, the client will no longer be able to authenticate with the server. Generate a new token and restart the client with the updated token. 274 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for pipegate on 2024-12-12T19:22:26+01:00 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = 'pipegate' 7 | primary_region = 'arn' 8 | 9 | [build] 10 | 11 | [http_service] 12 | internal_port = 8000 13 | force_https = true 14 | auto_stop_machines = 'stop' 15 | auto_start_machines = true 16 | min_machines_running = 0 17 | processes = ['app'] 18 | 19 | [[vm]] 20 | size = 'shared-cpu-1x' 21 | -------------------------------------------------------------------------------- /pipegate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janbjorge/pipegate/dd20bee507d16aa0eb032f4e4f81be6a148d631c/pipegate/__init__.py -------------------------------------------------------------------------------- /pipegate/auth.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import uuid 4 | from datetime import UTC, datetime, timedelta 5 | 6 | import jwt 7 | 8 | from .schemas import JWTPayload, Settings 9 | 10 | 11 | def make_jwt_bearer() -> None: 12 | settings = Settings() 13 | connection_id = settings.connection_id or uuid.uuid4().hex 14 | 15 | jwt_payload = JWTPayload( 16 | sub=connection_id, 17 | exp=int((datetime.now(UTC) + timedelta(days=21)).timestamp()), 18 | ) 19 | 20 | jwt_bearer = jwt.encode( 21 | jwt_payload.model_dump(mode="json"), 22 | key=settings.jwt_secret.get_secret_value(), 23 | algorithm=settings.jwt_algorithms[0], 24 | ) 25 | 26 | print(f"Connection-id: {connection_id}") 27 | print(f"JWT Bearer: {jwt_bearer}") 28 | 29 | 30 | if __name__ == "__main__": 31 | make_jwt_bearer() 32 | -------------------------------------------------------------------------------- /pipegate/client.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | 5 | import httpx 6 | import orjson 7 | import typer 8 | from websockets.asyncio.client import ClientConnection, connect 9 | 10 | from .schemas import BufferGateRequest, BufferGateResponse 11 | 12 | app = typer.Typer() 13 | 14 | 15 | @app.command() 16 | def start_client(target_url: str, server_url: str): 17 | """ 18 | Start the PipeGate Client to expose a local server. 19 | 20 | Args: 21 | target_url (str): The server to route incoming traffic to. 22 | server_url (str): The WebSocket server URL to connect to. 23 | """ 24 | asyncio.run(main(target_url, server_url)) 25 | 26 | 27 | async def handle_request( 28 | target: str, 29 | request: BufferGateRequest, 30 | http_client: httpx.AsyncClient, 31 | ws_client: ClientConnection, 32 | ) -> None: 33 | """ 34 | Process an incoming request from the server, forward it to the local server, 35 | and send back the response via WebSocket. 36 | 37 | Args: 38 | target (str): The target URL for the local HTTP server. 39 | request (BufferGateRequest): The incoming request data. 40 | http_client (httpx.AsyncClient): The HTTP client for making requests. 41 | ws_client (ClientConnection): The WebSocket client for sending responses. 42 | """ 43 | try: 44 | response = await http_client.request( 45 | method=request.method, 46 | url=f"{target}/{request.url_path}", 47 | headers=orjson.loads(request.headers), 48 | params=orjson.loads(request.url_query), 49 | content=request.body.encode(), 50 | ) 51 | response_payload = BufferGateResponse( 52 | correlation_id=request.correlation_id, 53 | headers=orjson.dumps(dict(response.headers)).decode(), 54 | body=response.text, 55 | status_code=response.status_code, 56 | ) 57 | except Exception as e: 58 | typer.secho( 59 | f"Error processing request {request.correlation_id}: {e}", 60 | fg=typer.colors.RED, 61 | ) 62 | response_payload = BufferGateResponse( 63 | correlation_id=request.correlation_id, 64 | headers="", 65 | body="", 66 | status_code=504, 67 | ) 68 | 69 | await ws_client.send(response_payload.model_dump_json()) 70 | 71 | 72 | async def main(target_url: str, server_url: str) -> None: 73 | """ 74 | Establish a WebSocket connection to the PipeGate server and handle incoming requests. 75 | 76 | Args: 77 | port (int): The port number of the local HTTP server to expose. 78 | server_url (str): The WebSocket server URL to connect to. 79 | """ 80 | typer.secho( 81 | f"Connecting to server at {server_url}...", 82 | fg=typer.colors.BLUE, 83 | ) 84 | 85 | try: 86 | async with connect(server_url) as ws_client, httpx.AsyncClient() as http_client: 87 | typer.secho("Connected to server.", fg=typer.colors.GREEN) 88 | async with asyncio.TaskGroup() as task_group: 89 | while True: 90 | try: 91 | message = await ws_client.recv() 92 | request = BufferGateRequest.model_validate_json(message) 93 | task_group.create_task( 94 | handle_request( 95 | target_url, 96 | request, 97 | http_client, 98 | ws_client, 99 | ) 100 | ) 101 | except asyncio.CancelledError: 102 | break 103 | except Exception as e: 104 | typer.secho( 105 | f"Error receiving message: {e}", 106 | fg=typer.colors.RED, 107 | ) 108 | except (ConnectionRefusedError, OSError) as e: 109 | typer.secho( 110 | f"Failed to connect to the server: {e}", 111 | fg=typer.colors.RED, 112 | ) 113 | except Exception as e: 114 | typer.secho( 115 | f"An unexpected error occurred: {e}", 116 | fg=typer.colors.RED, 117 | ) 118 | 119 | 120 | if __name__ == "__main__": 121 | app() 122 | -------------------------------------------------------------------------------- /pipegate/schemas.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import uuid 4 | from typing import Literal 5 | 6 | from pydantic import UUID4, BaseModel, Field, SecretStr 7 | from pydantic_settings import BaseSettings, SettingsConfigDict 8 | 9 | Methods = Literal[ 10 | "GET", 11 | "POST", 12 | "PUT", 13 | "DELETE", 14 | "PATCH", 15 | "OPTIONS", 16 | "HEAD", 17 | ] 18 | 19 | 20 | class BufferGateCorrelationId(BaseModel): 21 | correlation_id: uuid.UUID 22 | 23 | 24 | class BufferGateRequest(BufferGateCorrelationId): 25 | url_path: str 26 | url_query: str 27 | 28 | method: Methods 29 | headers: str 30 | body: str 31 | 32 | 33 | class BufferGateResponse(BufferGateCorrelationId): 34 | headers: str 35 | body: str 36 | status_code: int 37 | 38 | 39 | class JWTPayload(BaseModel): 40 | sub: str # UUID of the user or connection_id 41 | exp: int # Expiration timestamp 42 | 43 | 44 | class Settings(BaseSettings): 45 | model_config = SettingsConfigDict(cli_parse_args=True) 46 | connection_id: str | None = Field(alias="PIPEGATE_CONNECTION_ID", default=None) 47 | jwt_secret: SecretStr = Field(alias="PIPEGATE_JWT_SECRET") 48 | jwt_algorithms: list[str] = Field(alias="PIPEGATE_JWT_ALGORITHMS") 49 | -------------------------------------------------------------------------------- /pipegate/server.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import collections 5 | import uuid 6 | from contextlib import asynccontextmanager 7 | from datetime import timedelta 8 | from typing import AsyncGenerator, cast, get_args 9 | 10 | import async_timeout 11 | import jwt 12 | import orjson 13 | import uvicorn 14 | from fastapi import ( 15 | Depends, 16 | FastAPI, 17 | HTTPException, 18 | Request, 19 | Response, 20 | WebSocket, 21 | WebSocketDisconnect, 22 | ) 23 | from pydantic import UUID4, ValidationError 24 | 25 | from .schemas import ( 26 | BufferGateRequest, 27 | BufferGateResponse, 28 | JWTPayload, 29 | Methods, 30 | Settings, 31 | ) 32 | 33 | 34 | def get_settings(request: Request) -> Settings: 35 | return request.app.extra["settings"] 36 | 37 | 38 | def verify_jwt_uuid_match( 39 | connection_id: str, 40 | request: Request, 41 | settings: Settings = Depends(get_settings), 42 | ) -> JWTPayload: 43 | """ 44 | Verify the JWT token, validate its structure, and ensure the 'uuid' matches the path parameter. 45 | """ 46 | authorization: str | None = request.headers.get("Authorization") 47 | if not authorization or not authorization.startswith("Bearer "): 48 | raise HTTPException( 49 | status_code=401, 50 | detail="Missing or invalid Authorization header", 51 | ) 52 | 53 | token = authorization.split(" ", 1)[1] 54 | 55 | decoded_payload = jwt.decode( 56 | token, 57 | settings.jwt_secret.get_secret_value(), 58 | algorithms=settings.jwt_algorithms, 59 | ) 60 | payload = JWTPayload.model_validate(decoded_payload) 61 | 62 | # Verify that the token's UUID (sub) matches the path parameter connection_id 63 | if payload.sub != connection_id: 64 | raise HTTPException( 65 | status_code=403, detail="Token UUID does not match path UUID" 66 | ) 67 | 68 | return payload 69 | 70 | 71 | def create_app() -> FastAPI: 72 | """ 73 | Initialize and configure the FastAPI application. 74 | 75 | Returns: 76 | FastAPI: The configured FastAPI application instance. 77 | """ 78 | buffers: collections.defaultdict[UUID4, asyncio.Queue[BufferGateRequest]] = ( 79 | collections.defaultdict(asyncio.Queue) 80 | ) 81 | futures: collections.defaultdict[UUID4, asyncio.Future[BufferGateResponse]] = ( 82 | collections.defaultdict(asyncio.Future) 83 | ) 84 | 85 | @asynccontextmanager 86 | async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 87 | """ 88 | Define the application's lifespan events. 89 | 90 | Args: 91 | _: FastAPI: The FastAPI application instance. 92 | 93 | Yields: 94 | AsyncGenerator[None, None]: Yields control back to FastAPI. 95 | """ 96 | app.extra["settings"] = Settings(_cli_parse_args=False) 97 | 98 | try: 99 | yield 100 | finally: 101 | # On shutdown, set exceptions for all pending futures to prevent hanging 102 | for fut in futures.values(): 103 | if not fut.done(): 104 | fut.set_exception( 105 | HTTPException(status_code=504, detail="Gateway Timeout") 106 | ) 107 | 108 | app = FastAPI(lifespan=lifespan) 109 | 110 | @app.api_route( 111 | "/{connection_id}/{path_slug:path}", 112 | methods=list(get_args(Methods)), 113 | ) 114 | async def handle_http_request( 115 | connection_id: str, 116 | request: Request, 117 | path_slug: str = "", 118 | payload: JWTPayload = Depends(verify_jwt_uuid_match), 119 | ) -> Response: 120 | """ 121 | Handle incoming HTTP requests and forward them to the corresponding WebSocket connection. 122 | 123 | Args: 124 | connection_id (str): The unique identifier for the connection. 125 | request (Request): The incoming HTTP request. 126 | path_slug (str, optional): Additional path after the connection ID. Defaults to "". 127 | 128 | Returns: 129 | Response: The HTTP response received from the WebSocket client. 130 | """ 131 | correlation_id = uuid.uuid4() 132 | 133 | try: 134 | await buffers[connection_id].put( 135 | BufferGateRequest( 136 | correlation_id=correlation_id, 137 | method=cast(Methods, request.method), 138 | url_path=path_slug, 139 | url_query=orjson.dumps( 140 | {k: v for k, v in request.query_params.multi_items()} 141 | ).decode(), 142 | headers=orjson.dumps( 143 | { 144 | **dict(request.headers), 145 | "x-pipegate-correlation-id": correlation_id.hex, 146 | } 147 | ).decode(), 148 | body=(await request.body()).decode(), 149 | ) 150 | ) 151 | except Exception as e: 152 | raise HTTPException( 153 | status_code=500, detail=f"Failed to enqueue request: {e}" 154 | ) from e 155 | 156 | timeout = timedelta(seconds=300) 157 | 158 | try: 159 | async with async_timeout.timeout(timeout.total_seconds()): 160 | response = await futures[correlation_id] 161 | except asyncio.TimeoutError: 162 | raise HTTPException(status_code=504, detail="Gateway Timeout") 163 | finally: 164 | futures.pop(correlation_id, None) 165 | 166 | return Response( 167 | content=response.body.encode(), 168 | headers=orjson.loads(response.headers) if response.headers else {}, 169 | status_code=response.status_code, 170 | ) 171 | 172 | @app.websocket("/{connection_id}") 173 | async def handle_websocket( 174 | connection_id: str, 175 | websocket: WebSocket, 176 | ): 177 | """ 178 | Manage WebSocket connections for sending and receiving data. 179 | 180 | Args: 181 | connection_id (str): The unique identifier for the WebSocket connection. 182 | websocket (WebSocket): The WebSocket connection object. 183 | """ 184 | await websocket.accept() 185 | print(f"WebSocket connection established for ID: {connection_id}") 186 | 187 | async def receive(): 188 | """ 189 | Receive messages from the WebSocket and resolve pending futures. 190 | """ 191 | try: 192 | while True: 193 | message_text = await websocket.receive_text() 194 | try: 195 | message = BufferGateResponse.model_validate_json(message_text) 196 | future = futures.get(message.correlation_id) 197 | if future and not future.done(): 198 | future.set_result(message) 199 | else: 200 | print( 201 | f"No pending future for correlation ID: {message.correlation_id}" 202 | ) 203 | except ValidationError as ve: 204 | print(f"Invalid message format: {ve}") 205 | except Exception as e: 206 | print(f"Error processing received message: {e}") 207 | except WebSocketDisconnect as e: 208 | print(f"WebSocket disconnected during receive: {e}") 209 | except Exception as e: 210 | print(f"Unexpected error in receive handler: {e}") 211 | 212 | async def send(): 213 | """ 214 | Send messages from the buffer queue to the WebSocket. 215 | """ 216 | try: 217 | while True: 218 | request = await buffers[connection_id].get() 219 | try: 220 | await websocket.send_text(request.model_dump_json()) 221 | except WebSocketDisconnect as e: 222 | print(f"WebSocket disconnected during send: {e}") 223 | break 224 | except Exception as e: 225 | print(f"Error sending message: {e}") 226 | except Exception as e: 227 | print(f"Unexpected error in send handler: {e}") 228 | 229 | await asyncio.gather(receive(), send()) 230 | print(f"WebSocket connection closed for ID: {connection_id}") 231 | 232 | return app 233 | 234 | 235 | if __name__ == "__main__": 236 | app = create_app() 237 | uvicorn.run(app, host="0.0.0.0", port=8000) 238 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pipegate" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "async-timeout>=5.0.1", 9 | "fastapi[standard]>=0.115.6", 10 | "httpx>=0.28.1", 11 | "orjson>=3.10.12", 12 | "pydantic-settings>=2.7.0", 13 | "pydantic>=2.10.3", 14 | "pyjwt>=2.10.1", 15 | "typer>=0.15.1", 16 | "uvicorn>=0.32.1", 17 | "websockets>=14.1", 18 | ] 19 | 20 | [dependency-groups] 21 | dev = [ 22 | "mypy>=1.13.0", 23 | "ruff>=0.8.2", 24 | ] 25 | 26 | [tool.mypy] 27 | plugins = ['pydantic.mypy'] -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.13" 3 | 4 | [[package]] 5 | name = "annotated-types" 6 | version = "0.7.0" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 11 | ] 12 | 13 | [[package]] 14 | name = "anyio" 15 | version = "4.7.0" 16 | source = { registry = "https://pypi.org/simple" } 17 | dependencies = [ 18 | { name = "idna" }, 19 | { name = "sniffio" }, 20 | ] 21 | sdist = { url = "https://files.pythonhosted.org/packages/f6/40/318e58f669b1a9e00f5c4453910682e2d9dd594334539c7b7817dabb765f/anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48", size = 177076 } 22 | wheels = [ 23 | { url = "https://files.pythonhosted.org/packages/a0/7a/4daaf3b6c08ad7ceffea4634ec206faeff697526421c20f07628c7372156/anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352", size = 93052 }, 24 | ] 25 | 26 | [[package]] 27 | name = "async-timeout" 28 | version = "5.0.1" 29 | source = { registry = "https://pypi.org/simple" } 30 | sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } 31 | wheels = [ 32 | { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, 33 | ] 34 | 35 | [[package]] 36 | name = "certifi" 37 | version = "2024.8.30" 38 | source = { registry = "https://pypi.org/simple" } 39 | sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } 40 | wheels = [ 41 | { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, 42 | ] 43 | 44 | [[package]] 45 | name = "click" 46 | version = "8.1.7" 47 | source = { registry = "https://pypi.org/simple" } 48 | dependencies = [ 49 | { name = "colorama", marker = "platform_system == 'Windows'" }, 50 | ] 51 | sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } 52 | wheels = [ 53 | { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, 54 | ] 55 | 56 | [[package]] 57 | name = "colorama" 58 | version = "0.4.6" 59 | source = { registry = "https://pypi.org/simple" } 60 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 61 | wheels = [ 62 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 63 | ] 64 | 65 | [[package]] 66 | name = "dnspython" 67 | version = "2.7.0" 68 | source = { registry = "https://pypi.org/simple" } 69 | sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 } 70 | wheels = [ 71 | { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 }, 72 | ] 73 | 74 | [[package]] 75 | name = "email-validator" 76 | version = "2.2.0" 77 | source = { registry = "https://pypi.org/simple" } 78 | dependencies = [ 79 | { name = "dnspython" }, 80 | { name = "idna" }, 81 | ] 82 | sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967 } 83 | wheels = [ 84 | { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 }, 85 | ] 86 | 87 | [[package]] 88 | name = "fastapi" 89 | version = "0.115.6" 90 | source = { registry = "https://pypi.org/simple" } 91 | dependencies = [ 92 | { name = "pydantic" }, 93 | { name = "starlette" }, 94 | { name = "typing-extensions" }, 95 | ] 96 | sdist = { url = "https://files.pythonhosted.org/packages/93/72/d83b98cd106541e8f5e5bfab8ef2974ab45a62e8a6c5b5e6940f26d2ed4b/fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654", size = 301336 } 97 | wheels = [ 98 | { url = "https://files.pythonhosted.org/packages/52/b3/7e4df40e585df024fac2f80d1a2d579c854ac37109675db2b0cc22c0bb9e/fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305", size = 94843 }, 99 | ] 100 | 101 | [package.optional-dependencies] 102 | standard = [ 103 | { name = "email-validator" }, 104 | { name = "fastapi-cli", extra = ["standard"] }, 105 | { name = "httpx" }, 106 | { name = "jinja2" }, 107 | { name = "python-multipart" }, 108 | { name = "uvicorn", extra = ["standard"] }, 109 | ] 110 | 111 | [[package]] 112 | name = "fastapi-cli" 113 | version = "0.0.6" 114 | source = { registry = "https://pypi.org/simple" } 115 | dependencies = [ 116 | { name = "rich-toolkit" }, 117 | { name = "typer" }, 118 | { name = "uvicorn", extra = ["standard"] }, 119 | ] 120 | sdist = { url = "https://files.pythonhosted.org/packages/36/9d/9659cee212eeaecd8f54ab5f73d6d6d49a1dc4a46d2519fb9c1e66c8913e/fastapi_cli-0.0.6.tar.gz", hash = "sha256:2835a8f0c44b68e464d5cafe5ec205265f02dc1ad1d640db33a994ba3338003b", size = 16516 } 121 | wheels = [ 122 | { url = "https://files.pythonhosted.org/packages/8f/ab/0c8b6ec19594fe4c2e7fe12e8074648f59903047787ac05512c2935d5f7a/fastapi_cli-0.0.6-py3-none-any.whl", hash = "sha256:43288efee46338fae8902f9bf4559aed3aed639f9516f5d394a7ff19edcc8faf", size = 10687 }, 123 | ] 124 | 125 | [package.optional-dependencies] 126 | standard = [ 127 | { name = "uvicorn", extra = ["standard"] }, 128 | ] 129 | 130 | [[package]] 131 | name = "h11" 132 | version = "0.14.0" 133 | source = { registry = "https://pypi.org/simple" } 134 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 135 | wheels = [ 136 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 137 | ] 138 | 139 | [[package]] 140 | name = "httpcore" 141 | version = "1.0.7" 142 | source = { registry = "https://pypi.org/simple" } 143 | dependencies = [ 144 | { name = "certifi" }, 145 | { name = "h11" }, 146 | ] 147 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 148 | wheels = [ 149 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 150 | ] 151 | 152 | [[package]] 153 | name = "httptools" 154 | version = "0.6.4" 155 | source = { registry = "https://pypi.org/simple" } 156 | sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } 157 | wheels = [ 158 | { 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 }, 159 | { 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 }, 160 | { 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 }, 161 | { 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 }, 162 | { 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 }, 163 | { 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 }, 164 | { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, 165 | ] 166 | 167 | [[package]] 168 | name = "httpx" 169 | version = "0.28.1" 170 | source = { registry = "https://pypi.org/simple" } 171 | dependencies = [ 172 | { name = "anyio" }, 173 | { name = "certifi" }, 174 | { name = "httpcore" }, 175 | { name = "idna" }, 176 | ] 177 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 178 | wheels = [ 179 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 180 | ] 181 | 182 | [[package]] 183 | name = "idna" 184 | version = "3.10" 185 | source = { registry = "https://pypi.org/simple" } 186 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 187 | wheels = [ 188 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 189 | ] 190 | 191 | [[package]] 192 | name = "jinja2" 193 | version = "3.1.4" 194 | source = { registry = "https://pypi.org/simple" } 195 | dependencies = [ 196 | { name = "markupsafe" }, 197 | ] 198 | sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } 199 | wheels = [ 200 | { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, 201 | ] 202 | 203 | [[package]] 204 | name = "markdown-it-py" 205 | version = "3.0.0" 206 | source = { registry = "https://pypi.org/simple" } 207 | dependencies = [ 208 | { name = "mdurl" }, 209 | ] 210 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } 211 | wheels = [ 212 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, 213 | ] 214 | 215 | [[package]] 216 | name = "markupsafe" 217 | version = "3.0.2" 218 | source = { registry = "https://pypi.org/simple" } 219 | sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } 220 | wheels = [ 221 | { 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 }, 222 | { 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 }, 223 | { 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 }, 224 | { 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 }, 225 | { 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 }, 226 | { 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 }, 227 | { 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 }, 228 | { 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 }, 229 | { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, 230 | { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, 231 | { 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 }, 232 | { 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 }, 233 | { 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 }, 234 | { 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 }, 235 | { 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 }, 236 | { 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 }, 237 | { 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 }, 238 | { 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 }, 239 | { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, 240 | { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, 241 | ] 242 | 243 | [[package]] 244 | name = "mdurl" 245 | version = "0.1.2" 246 | source = { registry = "https://pypi.org/simple" } 247 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 248 | wheels = [ 249 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, 250 | ] 251 | 252 | [[package]] 253 | name = "mypy" 254 | version = "1.13.0" 255 | source = { registry = "https://pypi.org/simple" } 256 | dependencies = [ 257 | { name = "mypy-extensions" }, 258 | { name = "typing-extensions" }, 259 | ] 260 | sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 } 261 | wheels = [ 262 | { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 }, 263 | { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 }, 264 | { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 }, 265 | { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 }, 266 | { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 }, 267 | { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, 268 | ] 269 | 270 | [[package]] 271 | name = "mypy-extensions" 272 | version = "1.0.0" 273 | source = { registry = "https://pypi.org/simple" } 274 | sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } 275 | wheels = [ 276 | { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, 277 | ] 278 | 279 | [[package]] 280 | name = "orjson" 281 | version = "3.10.12" 282 | source = { registry = "https://pypi.org/simple" } 283 | sdist = { url = "https://files.pythonhosted.org/packages/e0/04/bb9f72987e7f62fb591d6c880c0caaa16238e4e530cbc3bdc84a7372d75f/orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff", size = 5438647 } 284 | wheels = [ 285 | { url = "https://files.pythonhosted.org/packages/1b/bb/3f560735f46fa6f875a9d7c4c2171a58cfb19f56a633d5ad5037a924f35f/orjson-3.10.12-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47962841b2a8aa9a258b377f5188db31ba49af47d4003a32f55d6f8b19006543", size = 248662 }, 286 | { url = "https://files.pythonhosted.org/packages/a3/df/54817902350636cc9270db20486442ab0e4db33b38555300a1159b439d16/orjson-3.10.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6334730e2532e77b6054e87ca84f3072bee308a45a452ea0bffbbbc40a67e296", size = 126055 }, 287 | { url = "https://files.pythonhosted.org/packages/2e/77/55835914894e00332601a74540840f7665e81f20b3e2b9a97614af8565ed/orjson-3.10.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:accfe93f42713c899fdac2747e8d0d5c659592df2792888c6c5f829472e4f85e", size = 131507 }, 288 | { url = "https://files.pythonhosted.org/packages/33/9e/b91288361898e3158062a876b5013c519a5d13e692ac7686e3486c4133ab/orjson-3.10.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7974c490c014c48810d1dede6c754c3cc46598da758c25ca3b4001ac45b703f", size = 131686 }, 289 | { url = "https://files.pythonhosted.org/packages/b2/15/08ce117d60a4d2d3fd24e6b21db463139a658e9f52d22c9c30af279b4187/orjson-3.10.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3f250ce7727b0b2682f834a3facff88e310f52f07a5dcfd852d99637d386e79e", size = 415710 }, 290 | { url = "https://files.pythonhosted.org/packages/71/af/c09da5ed58f9c002cf83adff7a4cdf3e6cee742aa9723395f8dcdb397233/orjson-3.10.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f31422ff9486ae484f10ffc51b5ab2a60359e92d0716fcce1b3593d7bb8a9af6", size = 142305 }, 291 | { url = "https://files.pythonhosted.org/packages/17/d1/8612038d44f33fae231e9ba480d273bac2b0383ce9e77cb06bede1224ae3/orjson-3.10.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f29c5d282bb2d577c2a6bbde88d8fdcc4919c593f806aac50133f01b733846e", size = 130815 }, 292 | { url = "https://files.pythonhosted.org/packages/67/2c/d5f87834be3591555cfaf9aecdf28f480a6f0b4afeaac53bad534bf9518f/orjson-3.10.12-cp313-none-win32.whl", hash = "sha256:f45653775f38f63dc0e6cd4f14323984c3149c05d6007b58cb154dd080ddc0dc", size = 143664 }, 293 | { url = "https://files.pythonhosted.org/packages/6a/05/7d768fa3ca23c9b3e1e09117abeded1501119f1d8de0ab722938c91ab25d/orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825", size = 134944 }, 294 | ] 295 | 296 | [[package]] 297 | name = "pipegate" 298 | version = "0.1.0" 299 | source = { virtual = "." } 300 | dependencies = [ 301 | { name = "async-timeout" }, 302 | { name = "fastapi", extra = ["standard"] }, 303 | { name = "httpx" }, 304 | { name = "orjson" }, 305 | { name = "pydantic" }, 306 | { name = "pydantic-settings" }, 307 | { name = "pyjwt" }, 308 | { name = "typer" }, 309 | { name = "uvicorn" }, 310 | { name = "websockets" }, 311 | ] 312 | 313 | [package.dev-dependencies] 314 | dev = [ 315 | { name = "mypy" }, 316 | { name = "ruff" }, 317 | ] 318 | 319 | [package.metadata] 320 | requires-dist = [ 321 | { name = "async-timeout", specifier = ">=5.0.1" }, 322 | { name = "fastapi", extras = ["standard"], specifier = ">=0.115.6" }, 323 | { name = "httpx", specifier = ">=0.28.1" }, 324 | { name = "orjson", specifier = ">=3.10.12" }, 325 | { name = "pydantic", specifier = ">=2.10.3" }, 326 | { name = "pydantic-settings", specifier = ">=2.7.0" }, 327 | { name = "pyjwt", specifier = ">=2.10.1" }, 328 | { name = "typer", specifier = ">=0.15.1" }, 329 | { name = "uvicorn", specifier = ">=0.32.1" }, 330 | { name = "websockets", specifier = ">=14.1" }, 331 | ] 332 | 333 | [package.metadata.requires-dev] 334 | dev = [ 335 | { name = "mypy", specifier = ">=1.13.0" }, 336 | { name = "ruff", specifier = ">=0.8.2" }, 337 | ] 338 | 339 | [[package]] 340 | name = "pydantic" 341 | version = "2.10.3" 342 | source = { registry = "https://pypi.org/simple" } 343 | dependencies = [ 344 | { name = "annotated-types" }, 345 | { name = "pydantic-core" }, 346 | { name = "typing-extensions" }, 347 | ] 348 | sdist = { url = "https://files.pythonhosted.org/packages/45/0f/27908242621b14e649a84e62b133de45f84c255eecb350ab02979844a788/pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9", size = 786486 } 349 | wheels = [ 350 | { url = "https://files.pythonhosted.org/packages/62/51/72c18c55cf2f46ff4f91ebcc8f75aa30f7305f3d726be3f4ebffb4ae972b/pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d", size = 456997 }, 351 | ] 352 | 353 | [[package]] 354 | name = "pydantic-core" 355 | version = "2.27.1" 356 | source = { registry = "https://pypi.org/simple" } 357 | dependencies = [ 358 | { name = "typing-extensions" }, 359 | ] 360 | sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 } 361 | wheels = [ 362 | { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 }, 363 | { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 }, 364 | { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 }, 365 | { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 }, 366 | { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 }, 367 | { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 }, 368 | { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 }, 369 | { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 }, 370 | { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 }, 371 | { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 }, 372 | { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 }, 373 | { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, 374 | { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, 375 | { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, 376 | ] 377 | 378 | [[package]] 379 | name = "pydantic-settings" 380 | version = "2.7.0" 381 | source = { registry = "https://pypi.org/simple" } 382 | dependencies = [ 383 | { name = "pydantic" }, 384 | { name = "python-dotenv" }, 385 | ] 386 | sdist = { url = "https://files.pythonhosted.org/packages/86/41/19b62b99e7530cfa1d6ccd16199afd9289a12929bef1a03aa4382b22e683/pydantic_settings-2.7.0.tar.gz", hash = "sha256:ac4bfd4a36831a48dbf8b2d9325425b549a0a6f18cea118436d728eb4f1c4d66", size = 79743 } 387 | wheels = [ 388 | { url = "https://files.pythonhosted.org/packages/f9/00/57b4540deb5c3a39ba689bb519a4e03124b24ab8589e618be4aac2c769bd/pydantic_settings-2.7.0-py3-none-any.whl", hash = "sha256:e00c05d5fa6cbbb227c84bd7487c5c1065084119b750df7c8c1a554aed236eb5", size = 29549 }, 389 | ] 390 | 391 | [[package]] 392 | name = "pygments" 393 | version = "2.18.0" 394 | source = { registry = "https://pypi.org/simple" } 395 | sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } 396 | wheels = [ 397 | { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, 398 | ] 399 | 400 | [[package]] 401 | name = "pyjwt" 402 | version = "2.10.1" 403 | source = { registry = "https://pypi.org/simple" } 404 | sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } 405 | wheels = [ 406 | { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, 407 | ] 408 | 409 | [[package]] 410 | name = "python-dotenv" 411 | version = "1.0.1" 412 | source = { registry = "https://pypi.org/simple" } 413 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 414 | wheels = [ 415 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 416 | ] 417 | 418 | [[package]] 419 | name = "python-multipart" 420 | version = "0.0.19" 421 | source = { registry = "https://pypi.org/simple" } 422 | sdist = { url = "https://files.pythonhosted.org/packages/c1/19/93bfb43a3c41b1dd0fa1fa66a08286f6467d36d30297a7aaab8c0b176a26/python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc", size = 36886 } 423 | wheels = [ 424 | { url = "https://files.pythonhosted.org/packages/e1/f4/ddd0fcdc454cf3870153ae16a818256523d31c3c8136e216bc6836ed4cd1/python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d", size = 24448 }, 425 | ] 426 | 427 | [[package]] 428 | name = "pyyaml" 429 | version = "6.0.2" 430 | source = { registry = "https://pypi.org/simple" } 431 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } 432 | wheels = [ 433 | { 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 }, 434 | { 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 }, 435 | { 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 }, 436 | { 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 }, 437 | { 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 }, 438 | { 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 }, 439 | { 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 }, 440 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, 441 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, 442 | ] 443 | 444 | [[package]] 445 | name = "rich" 446 | version = "13.9.4" 447 | source = { registry = "https://pypi.org/simple" } 448 | dependencies = [ 449 | { name = "markdown-it-py" }, 450 | { name = "pygments" }, 451 | ] 452 | sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } 453 | wheels = [ 454 | { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, 455 | ] 456 | 457 | [[package]] 458 | name = "rich-toolkit" 459 | version = "0.12.0" 460 | source = { registry = "https://pypi.org/simple" } 461 | dependencies = [ 462 | { name = "click" }, 463 | { name = "rich" }, 464 | { name = "typing-extensions" }, 465 | ] 466 | sdist = { url = "https://files.pythonhosted.org/packages/d2/88/58c193e2e353b0ef8b4b9a91031bbcf8a9a3b431f5ebb4f55c3f3b1992e8/rich_toolkit-0.12.0.tar.gz", hash = "sha256:facb0b40418010309f77abd44e2583b4936656f6ee5c8625da807564806a6c40", size = 71673 } 467 | wheels = [ 468 | { url = "https://files.pythonhosted.org/packages/ac/3c/3b66696fc8a6c980674851108d7d57fbcbfedbefb3d8b61a64166dc9b18e/rich_toolkit-0.12.0-py3-none-any.whl", hash = "sha256:a2da4416384410ae871e890db7edf8623e1f5e983341dbbc8cc03603ce24f0ab", size = 13012 }, 469 | ] 470 | 471 | [[package]] 472 | name = "ruff" 473 | version = "0.8.2" 474 | source = { registry = "https://pypi.org/simple" } 475 | sdist = { url = "https://files.pythonhosted.org/packages/5e/2b/01245f4f3a727d60bebeacd7ee6d22586c7f62380a2597ddb22c2f45d018/ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5", size = 3349020 } 476 | wheels = [ 477 | { url = "https://files.pythonhosted.org/packages/91/29/366be70216dba1731a00a41f2f030822b0c96c7c4f3b2c0cdce15cbace74/ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d", size = 10530649 }, 478 | { url = "https://files.pythonhosted.org/packages/63/82/a733956540bb388f00df5a3e6a02467b16c0e529132625fe44ce4c5fb9c7/ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5", size = 10274069 }, 479 | { url = "https://files.pythonhosted.org/packages/3d/12/0b3aa14d1d71546c988a28e1b412981c1b80c8a1072e977a2f30c595cc4a/ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c", size = 9909400 }, 480 | { url = "https://files.pythonhosted.org/packages/23/08/f9f08cefb7921784c891c4151cce6ed357ff49e84b84978440cffbc87408/ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f", size = 10766782 }, 481 | { url = "https://files.pythonhosted.org/packages/e4/71/bf50c321ec179aa420c8ec40adac5ae9cc408d4d37283a485b19a2331ceb/ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897", size = 10286316 }, 482 | { url = "https://files.pythonhosted.org/packages/f2/83/c82688a2a6117539aea0ce63fdf6c08e60fe0202779361223bcd7f40bd74/ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58", size = 11338270 }, 483 | { url = "https://files.pythonhosted.org/packages/7f/d7/bc6a45e5a22e627640388e703160afb1d77c572b1d0fda8b4349f334fc66/ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29", size = 12058579 }, 484 | { url = "https://files.pythonhosted.org/packages/da/3b/64150c93946ec851e6f1707ff586bb460ca671581380c919698d6a9267dc/ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248", size = 11615172 }, 485 | { url = "https://files.pythonhosted.org/packages/e4/9e/cf12b697ea83cfe92ec4509ae414dc4c9b38179cc681a497031f0d0d9a8e/ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93", size = 12882398 }, 486 | { url = "https://files.pythonhosted.org/packages/a9/27/96d10863accf76a9c97baceac30b0a52d917eb985a8ac058bd4636aeede0/ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d", size = 11176094 }, 487 | { url = "https://files.pythonhosted.org/packages/eb/10/cd2fd77d4a4e7f03c29351be0f53278a393186b540b99df68beb5304fddd/ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0", size = 10771884 }, 488 | { url = "https://files.pythonhosted.org/packages/71/5d/beabb2ff18870fc4add05fa3a69a4cb1b1d2d6f83f3cf3ae5ab0d52f455d/ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa", size = 10382535 }, 489 | { url = "https://files.pythonhosted.org/packages/ae/29/6b3fdf3ad3e35b28d87c25a9ff4c8222ad72485ab783936b2b267250d7a7/ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f", size = 10886995 }, 490 | { url = "https://files.pythonhosted.org/packages/e9/dc/859d889b4d9356a1a2cdbc1e4a0dda94052bc5b5300098647e51a58c430b/ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22", size = 11220750 }, 491 | { url = "https://files.pythonhosted.org/packages/0b/08/e8f519f61f1d624264bfd6b8829e4c5f31c3c61193bc3cff1f19dbe7626a/ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1", size = 8729396 }, 492 | { url = "https://files.pythonhosted.org/packages/f8/d4/ba1c7ab72aba37a2b71fe48ab95b80546dbad7a7f35ea28cf66fc5cea5f6/ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea", size = 9594729 }, 493 | { url = "https://files.pythonhosted.org/packages/23/34/db20e12d3db11b8a2a8874258f0f6d96a9a4d631659d54575840557164c8/ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8", size = 9035131 }, 494 | ] 495 | 496 | [[package]] 497 | name = "shellingham" 498 | version = "1.5.4" 499 | source = { registry = "https://pypi.org/simple" } 500 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } 501 | wheels = [ 502 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, 503 | ] 504 | 505 | [[package]] 506 | name = "sniffio" 507 | version = "1.3.1" 508 | source = { registry = "https://pypi.org/simple" } 509 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 510 | wheels = [ 511 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 512 | ] 513 | 514 | [[package]] 515 | name = "starlette" 516 | version = "0.41.3" 517 | source = { registry = "https://pypi.org/simple" } 518 | dependencies = [ 519 | { name = "anyio" }, 520 | ] 521 | sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 } 522 | wheels = [ 523 | { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 }, 524 | ] 525 | 526 | [[package]] 527 | name = "typer" 528 | version = "0.15.1" 529 | source = { registry = "https://pypi.org/simple" } 530 | dependencies = [ 531 | { name = "click" }, 532 | { name = "rich" }, 533 | { name = "shellingham" }, 534 | { name = "typing-extensions" }, 535 | ] 536 | sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 } 537 | wheels = [ 538 | { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, 539 | ] 540 | 541 | [[package]] 542 | name = "typing-extensions" 543 | version = "4.12.2" 544 | source = { registry = "https://pypi.org/simple" } 545 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 546 | wheels = [ 547 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 548 | ] 549 | 550 | [[package]] 551 | name = "uvicorn" 552 | version = "0.32.1" 553 | source = { registry = "https://pypi.org/simple" } 554 | dependencies = [ 555 | { name = "click" }, 556 | { name = "h11" }, 557 | ] 558 | sdist = { url = "https://files.pythonhosted.org/packages/6a/3c/21dba3e7d76138725ef307e3d7ddd29b763119b3aa459d02cc05fefcff75/uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175", size = 77630 } 559 | wheels = [ 560 | { url = "https://files.pythonhosted.org/packages/50/c1/2d27b0a15826c2b71dcf6e2f5402181ef85acf439617bb2f1453125ce1f3/uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", size = 63828 }, 561 | ] 562 | 563 | [package.optional-dependencies] 564 | standard = [ 565 | { name = "colorama", marker = "sys_platform == 'win32'" }, 566 | { name = "httptools" }, 567 | { name = "python-dotenv" }, 568 | { name = "pyyaml" }, 569 | { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, 570 | { name = "watchfiles" }, 571 | { name = "websockets" }, 572 | ] 573 | 574 | [[package]] 575 | name = "uvloop" 576 | version = "0.21.0" 577 | source = { registry = "https://pypi.org/simple" } 578 | sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } 579 | wheels = [ 580 | { 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 }, 581 | { 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 }, 582 | { 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 }, 583 | { 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 }, 584 | { 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 }, 585 | { 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 }, 586 | ] 587 | 588 | [[package]] 589 | name = "watchfiles" 590 | version = "1.0.3" 591 | source = { registry = "https://pypi.org/simple" } 592 | dependencies = [ 593 | { name = "anyio" }, 594 | ] 595 | sdist = { url = "https://files.pythonhosted.org/packages/3c/7e/4569184ea04b501840771b8fcecee19b2233a8b72c196061263c0ef23c0b/watchfiles-1.0.3.tar.gz", hash = "sha256:f3ff7da165c99a5412fe5dd2304dd2dbaaaa5da718aad942dcb3a178eaa70c56", size = 38185 } 596 | wheels = [ 597 | { url = "https://files.pythonhosted.org/packages/36/77/0ceb864c854c59bc5326484f88a900c70b4a05e3792e0ce340689988dd5e/watchfiles-1.0.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e153a690b7255c5ced17895394b4f109d5dcc2a4f35cb809374da50f0e5c456a", size = 391061 }, 598 | { url = "https://files.pythonhosted.org/packages/00/66/327046cfe276a6e4af1a9a58fc99321e25783e501dc68c4c82de2d1bd3a7/watchfiles-1.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac1be85fe43b4bf9a251978ce5c3bb30e1ada9784290441f5423a28633a958a7", size = 381177 }, 599 | { url = "https://files.pythonhosted.org/packages/66/8a/420e2833deaa88e8ca7d94a497ec60fde610c66206a1776f049dc5ad3a4e/watchfiles-1.0.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec98e31e1844eac860e70d9247db9d75440fc8f5f679c37d01914568d18721", size = 441293 }, 600 | { url = "https://files.pythonhosted.org/packages/58/56/2627795ecdf3f0f361458cfa74c583d5041615b9ad81bc25f8c66a6c44a2/watchfiles-1.0.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0179252846be03fa97d4d5f8233d1c620ef004855f0717712ae1c558f1974a16", size = 446209 }, 601 | { url = "https://files.pythonhosted.org/packages/8f/d0/11c8dcd8a9995f0c075d76f1d06068bbb7a17583a19c5be75361497a4074/watchfiles-1.0.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:995c374e86fa82126c03c5b4630c4e312327ecfe27761accb25b5e1d7ab50ec8", size = 471227 }, 602 | { url = "https://files.pythonhosted.org/packages/cb/8f/baa06574eaf48173882c4cdc3636993d0854661be7d88193e015ef996c73/watchfiles-1.0.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29b9cb35b7f290db1c31fb2fdf8fc6d3730cfa4bca4b49761083307f441cac5a", size = 493205 }, 603 | { url = "https://files.pythonhosted.org/packages/ee/e8/9af886b4d3daa281047b542ffd2eb8f76dae9dd6ca0e21c5df4593b98574/watchfiles-1.0.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f8dc09ae69af50bead60783180f656ad96bd33ffbf6e7a6fce900f6d53b08f1", size = 489090 }, 604 | { url = "https://files.pythonhosted.org/packages/81/02/62085db54b151fc02e22d47b288d19e99031dc9af73151289a7ab6621f9a/watchfiles-1.0.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:489b80812f52a8d8c7b0d10f0d956db0efed25df2821c7a934f6143f76938bd6", size = 442610 }, 605 | { url = "https://files.pythonhosted.org/packages/61/81/980439c5d3fd3c69ba7124a56e1016d0b824ced2192ffbfe7062d53f524b/watchfiles-1.0.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:228e2247de583475d4cebf6b9af5dc9918abb99d1ef5ee737155bb39fb33f3c0", size = 614781 }, 606 | { url = "https://files.pythonhosted.org/packages/55/98/e11401d8e9cd5d2bd0e95e9bf750f397489681965ee0c72fb84732257912/watchfiles-1.0.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1550be1a5cb3be08a3fb84636eaafa9b7119b70c71b0bed48726fd1d5aa9b868", size = 612637 }, 607 | { url = "https://files.pythonhosted.org/packages/50/be/8393b68f2add0f839be6863f151bd6a7b242efc6eb2ce0c9f7d135d529cc/watchfiles-1.0.3-cp313-cp313-win32.whl", hash = "sha256:16db2d7e12f94818cbf16d4c8938e4d8aaecee23826344addfaaa671a1527b07", size = 271170 }, 608 | { url = "https://files.pythonhosted.org/packages/f0/da/725f97a8b1b4e7b3e4331cce3ef921b12568af3af403b9f0f61ede036898/watchfiles-1.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:160eff7d1267d7b025e983ca8460e8cc67b328284967cbe29c05f3c3163711a3", size = 285246 }, 609 | ] 610 | 611 | [[package]] 612 | name = "websockets" 613 | version = "14.1" 614 | source = { registry = "https://pypi.org/simple" } 615 | sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 } 616 | wheels = [ 617 | { url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 }, 618 | { url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 }, 619 | { url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 }, 620 | { url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 }, 621 | { url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 }, 622 | { url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 }, 623 | { url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 }, 624 | { url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 }, 625 | { url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 }, 626 | { url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 }, 627 | { url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 }, 628 | { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 }, 629 | ] 630 | --------------------------------------------------------------------------------